From 362e419a2db1353c06d926b4447d1a9105bd5bf9 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Fri, 6 Nov 2026 22:32:11 +0000 Subject: [PATCH 1/3] Update Lootable Bodies for Forge 1.20.6 --- .github/release120.json | 18 +- .github/scripts/test_release_workflow.py | 25 +- .github/workflows/artifact-audit-1.20.yml | 9 +- .github/workflows/codeql-analysis.yml | 10 +- .github/workflows/deploy-release.yml | 50 +-- .github/workflows/native120.yml | 12 +- .github/workflows/release-workflow-checks.yml | 4 +- CHANGELOG.txt | 4 +- README.md | 8 +- RELEASING.md | 4 +- TESTING.md | 8 +- build.gradle | 62 ++-- gradle.properties | 14 +- gradle/assets.gradle | 15 +- gradle/audit.gradle | 33 +- gradle/ide.gradle | 44 ++- gradle/release.gradle | 6 +- gradle/runtime.gradle | 39 +-- settings.gradle | 2 +- .../java/buildcontrol/ForgeControl.java | 16 +- .../java/buildcontrol/ResourceControl.java | 12 +- src/controlTest/resources/META-INF/mods.toml | 2 +- src/controlTest/resources/pack.mcmeta | 3 +- .../lootable/buildtest/ClientChecks.java | 4 +- .../buildtest/ConfigClientChecks.java | 28 +- .../buildtest/ConnectedClientChecks.java | 10 +- .../buildtest/ContainerInteractionChecks.java | 46 +-- .../lootable/buildtest/DropClientChecks.java | 6 +- .../lootable/buildtest/EffectHurtChecks.java | 53 ++- .../cyano/lootable/buildtest/FixtureApi.java | 28 +- .../buildtest/FixtureConnections.java | 6 +- .../lootable/buildtest/FixtureItems.java | 2 +- .../buildtest/FixtureProfileCache.java | 31 ++ .../lootable/buildtest/FixtureStackData.java | 38 +++ .../buildtest/HealthAttributeChecks.java | 19 +- .../buildtest/InventorySafetyChecks.java | 26 +- .../lootable/buildtest/MultiplayerChecks.java | 29 +- .../buildtest/MultiplayerClientChecks.java | 4 +- .../buildtest/NameLifetimeChecks.java | 2 +- .../buildtest/NamelessCorpseClientChecks.java | 4 +- .../buildtest/Native118BiomeChecks.java | 2 +- .../buildtest/NativePreflightChecks.java | 14 +- .../lootable/buildtest/NbtClientChecks.java | 8 +- .../lootable/buildtest/NbtRecoveryChecks.java | 127 ++++--- .../lootable/buildtest/NbtWorldChecks.java | 12 +- .../buildtest/PortBaselineChecks.java | 23 +- .../buildtest/PositionMotionChecks.java | 1 - .../buildtest/ReportedIssueChecks.java | 24 +- .../buildtest/ReportedIssueClientChecks.java | 14 +- .../buildtest/RespawnPacketChecks.java | 2 +- .../buildtest/RespawnPacketClientChecks.java | 18 +- .../buildtest/RespawnPacketTrace.java | 87 ++--- .../buildtest/RotationFallChecks.java | 1 - .../lootable/buildtest/RuntimeChecks.java | 18 +- .../lootable/buildtest/VisibilityChecks.java | 10 +- .../buildtest/VisibilityClientChecks.java | 14 +- .../buildtest/VisibilitySkinChecks.java | 90 +++-- .../buildtest/VisibilitySkinClientChecks.java | 7 +- .../lootable/buildtest/WallEscapeChecks.java | 4 +- .../buildtest/WorldUpgradeChecks.java | 233 ++++++++++--- .../resources/META-INF/mods.toml | 4 +- src/integrationTest/resources/pack.mcmeta | 3 +- src/main/java/cyano/lootable/ItemStacks.java | 3 + .../java/cyano/lootable/LootableBodies.java | 2 +- .../lootable/entities/EntityLootableBody.java | 309 +++++++++--------- .../entities/SavedEffectIdentity.java | 41 ++- .../lootable/graphics/CorpseConfigGui.java | 6 +- .../lootable/graphics/CorpseContainer.java | 4 +- .../lootable/graphics/CorpseRenderer.java | 34 +- .../cyano/lootable/graphics/GUIHandler.java | 3 +- .../network/ClientLoginReadGuard.java | 12 +- .../lootable/network/CorpseProfiles.java | 38 ++- .../upgrade/EarlyWorldPreservation.java | 26 +- .../cyano/lootable/upgrade/ItemMigration.java | 205 +++++++++++- .../upgrade/LegacyRegistryMappings.java | 42 +++ .../lootable/upgrade/NativeSafeFixer.java | 17 +- .../lootable/upgrade/UpgradeJournal.java | 51 ++- .../lootable/upgrade/UpgradePreservation.java | 18 +- .../resources/META-INF/accesstransformer.cfg | 3 - src/main/resources/META-INF/mods.toml | 6 +- .../coremods/early-world-preservation.js | 13 +- .../entity_types/can_breathe_under_water.json | 4 + .../ignores_poison_and_regen.json | 4 + .../inverted_healing_and_harm.json | 4 + .../tags/entity_types/sensitive_to_smite.json | 4 + .../minecraft/tags/entity_types/undead.json | 4 + src/main/resources/pack.mcmeta | 3 +- .../buildtest/Native115PacketTest.java | 32 +- .../buildtest/Native117ForgeConfigTest.java | 10 +- .../buildtest/Native117MenuProtocolTest.java | 26 +- .../Native117ProbeIsolationTest.java | 4 +- .../buildtest/Native117TestEnvironment.java | 38 ++- .../buildtest/Native119BundleTraceTest.java | 20 +- .../buildtest/Native119Registries.java | 5 +- .../buildtest/Native120FixtureGenerator.java | 41 +-- .../lootable/contracts/BuildContractTest.java | 7 +- .../contracts/NativeUpgradeBoundaryTest.java | 2 +- .../contracts/ReleaseContractTest.java | 4 +- .../contracts/UpgradeJournalTest.java | 4 +- .../entities/Native118EffectIdentityTest.java | 58 +++- .../entities/Native119DamageTest.java | 10 +- .../lootable/network/LoginReadGuardTest.java | 24 +- .../network/Native117ProfileLookupTest.java | 75 ++--- .../upgrade/EarlyWorldPreservationTest.java | 4 +- .../upgrade/HookRegistrationOrderTest.java | 6 +- .../upgrade/ItemNbtConversionTest.java | 10 +- .../upgrade/Native114UpgradeTest.java | 8 +- .../upgrade/Native115UpgradeTest.java | 6 +- .../upgrade/Native116DamagedStampTest.java | 6 +- .../upgrade/Native116UpgradeTest.java | 2 +- .../upgrade/Native117EntityChunkTest.java | 16 +- .../upgrade/Native117EntityRegionTest.java | 6 +- .../upgrade/Native117ExternalSidecarTest.java | 4 +- .../upgrade/Native118BiomeRegistryTest.java | 2 +- .../upgrade/Native118EarlyHookTest.java | 8 +- .../upgrade/Native118EntityBoundaryTest.java | 20 +- .../upgrade/Native118SavedPositionTest.java | 20 +- .../upgrade/Native118TerrainBoundaryTest.java | 62 +++- .../upgrade/Native119CoremodEngineTest.java | 2 +- .../upgrade/Native1206ComponentsTest.java | 215 ++++++++++++ .../upgrade/Native120RegistryRenameTest.java | 15 + .../lootable/upgrade/Native120TargetTest.java | 22 +- .../buildtest/HistoricalVersions.java | 1 + .../buildtest/NativeMenuObservation.java | 40 ++- .../buildtest/NativeRespawnDescription.java | 23 +- .../upgrade/Native117RegionFixtures.java | 2 +- tools/fixtures/forge50-server-default.toml | 14 + tools/fixtures/native1206-flat-level.dat | Bin 0 -> 39597 bytes .../native1206-flat-level.provenance.json | 12 + .../native1206-forge-only-flat-level.dat | Bin 0 -> 39505 bytes tools/hosted_policy.py | 10 +- tools/local_case_reuse.py | 4 +- tools/migration_ci.py | 4 +- tools/native1206_client_diagnostics.json | 14 + tools/native1206_server_inputs.json | 96 ++++++ tools/native1206_worker_budget.json | 11 + tools/native120_build.py | 2 +- tools/native120_client.py | 38 ++- tools/native120_client_logs.py | 10 +- tools/native120_logs.py | 4 +- tools/native120_persistence.py | 12 +- tools/native_evidence.py | 12 +- tools/native_runtime.py | 97 +++--- tools/owned_process.py | 6 +- tools/qualification-policy.json | 4 +- tools/test_historical_runtime_budget.py | 2 +- tools/test_hosted_policy.py | 10 +- tools/test_native120_build.py | 2 +- tools/test_native120_client_contracts.py | 28 +- tools/test_native120_client_logs.py | 6 +- tools/test_native120_flat_header.py | 18 +- tools/test_native120_infrastructure.py | 18 +- tools/test_native120_launch.py | 56 +++- tools/test_native120_persistence.py | 8 +- tools/test_native47_platform_config.py | 21 +- tools/test_native_evidence.py | 8 +- tools/test_native_server_preparation.py | 4 +- tools/test_owned_process.py | 6 +- tools/verify_multiplayer.py | 4 +- tools/verify_native_respawn_control.py | 27 +- tools/verify_nbt_recovery.py | 2 +- tools/verify_packaged.py | 4 +- tools/verify_packaged_client.py | 4 +- tools/verify_upgrade_abort.py | 17 +- tools/verify_visibility.py | 2 +- tools/verify_world_upgrades.py | 82 ++++- 166 files changed, 2530 insertions(+), 1317 deletions(-) create mode 100644 src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java create mode 100644 src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java create mode 100644 src/main/resources/data/minecraft/tags/entity_types/can_breathe_under_water.json create mode 100644 src/main/resources/data/minecraft/tags/entity_types/ignores_poison_and_regen.json create mode 100644 src/main/resources/data/minecraft/tags/entity_types/inverted_healing_and_harm.json create mode 100644 src/main/resources/data/minecraft/tags/entity_types/sensitive_to_smite.json create mode 100644 src/main/resources/data/minecraft/tags/entity_types/undead.json create mode 100644 src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java create mode 100644 tools/fixtures/forge50-server-default.toml create mode 100644 tools/fixtures/native1206-flat-level.dat create mode 100644 tools/fixtures/native1206-flat-level.provenance.json create mode 100644 tools/fixtures/native1206-forge-only-flat-level.dat create mode 100644 tools/native1206_client_diagnostics.json create mode 100644 tools/native1206_server_inputs.json create mode 100644 tools/native1206_worker_budget.json diff --git a/.github/release120.json b/.github/release120.json index 5505110..673815d 100644 --- a/.github/release120.json +++ b/.github/release120.json @@ -1,16 +1,16 @@ { - "version": "2.4.11.120011", - "minecraft": "1.20.1", - "forge": "47.4.28", + "version": "2.4.12.120061", + "minecraft": "1.20.6", + "forge": "50.2.14", "local_qualification_complete": true, "qualification_integrity_status": "verified", "manual_accepted": true, "files": { - "CHANGELOG.txt": "30D5C91D14146FC202B02CAF3786D017EEF39E19F2785E1CC4C2553D1F9576CC", - "LootableBodies-2.4.11.120011-javadoc.jar": "E7B2342A3BC0C698E923E60693FCD6EEDD93077167663306908E327A47B69DA0", - "LootableBodies-2.4.11.120011-sources.jar": "E40712C149FD9485EB5FCBA959BA13ACC2B87DC535A6D2593A266CEBE9E26218", - "LootableBodies-2.4.11.120011.jar": "FD9D040217D1949C04C5A6C123BEF8C7671B3A2ABE1170083C6769772942DD9E", - "LootableBodies-2.4.11.120011.pom": "A9177C59AF6D7FE1B4B86C40D8053FD7A3C7A81B639F6759079B82D27CA5152C", - "SHA256SUMS": "BFF65899B03C55A5D70E08FAA6B981250C9D787B6FE13865A058D5A94AAFDEDE" + "CHANGELOG.txt": "B546DA0EA212454D886C4F05BCC949A366E9EEC041EAB3E8CB8B36D0CAF0C007", + "LootableBodies-2.4.12.120061-javadoc.jar": "127405C2747FE4074C3EFD12A1BD449DC149AD3E2C90A37D8A8C1730C4D917C0", + "LootableBodies-2.4.12.120061-sources.jar": "92A02E80D1DE9D23DD529AC410D2792FDFE5E0353964ECDBD2A0B6993A397300", + "LootableBodies-2.4.12.120061.jar": "08C1D045970AEFDC08F5F114D308380256073B66BCCC36DE3532C0204BCEB0E0", + "LootableBodies-2.4.12.120061.pom": "E79A8BF254ACACD2EA86B0707A014FFB0A34F4617126D29947129C5C8E0B908A", + "SHA256SUMS": "F0FAA5AC25C97C6760781DE526298B87EF03E8D1FED5489CCAAF1F5445D8C737" } } diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index a2f642d..5b2eacf 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -71,13 +71,14 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.17.1': ('117011', 'master-1.17.1', '37.1.1'), '1.18.2': ('118021', 'master-1.18.2', '40.3.12'), '1.19.4': ('119041', 'master-1.19.4', '45.4.5'), - '1.20.1': ('120011', 'master-1.20.1', '47.4.28')}[minecraft] + '1.20.1': ('120011', 'master-1.20.1', '47.4.28'), + '1.20.6': ('120061', 'master-1.20.6', '50.2.14')}[minecraft] version = '2.4.2.' + suffix files={name:'C'*64 for name in ['main.jar','sources.jar','javadoc.jar','release.pom','CHANGELOG.txt','SHA256SUMS']} receipt={'schema':1,'source':SHA,'version':version,'files':files,'evidence_sha256':'F'*64,'qualified_locally':True,'manual_accepted':True} if local_receipt is not None:receipt=local_receipt values = dict(minecraft_version=minecraft, forge_version=forge, loader_name='forge', loader_code='1', - java_version='17' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='17.0.20.1+1' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='17', mod_id='lootablebodies', + java_version='21' if minecraft=='1.20.6' else '17' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='21.0.7+6' if minecraft=='1.20.6' else '17.0.20.1+1' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='21' if minecraft=='1.20.6' else '17', mod_id='lootablebodies', mod_artifact='LootableBodies', mod_group='zone.moddev.mc.lootablebodies', mod_license='LGPL-2.1-only', curseforge_project_id='240968', mod_version=version) if mutation: @@ -87,13 +88,13 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.14.4': MATRIX114 + ['Validate release workflow'], '1.15.2': MATRIX115, '1.16.5': MATRIX116}.get(minecraft, []) if names is None else names if minecraft=='1.17.1' and names is None:checks=CORE117+MATRIX117 - if minecraft in ('1.18.2','1.19.4','1.20.1') and names is None:checks=CORE118 + if minecraft in ('1.18.2','1.19.4','1.20.1','1.20.6') and names is None:checks=CORE118 def command(args, **options): if args[0] == 'git': return SHA + '\n' if any('/actions/workflows/native'+code+'.yml/' in args[-1] for code in ['117','118','119','120']): - code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120'}[minecraft] + code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120'}[minecraft] row=dict(id=int(code),head_sha=SHA,head_branch=branch,event='push',path='.github/workflows/native'+code+'.yml',conclusion='success') if qualification:row.update(qualification) return json.dumps({'workflow_runs':[row]}) @@ -117,10 +118,20 @@ def existing(args, **options): patch('subprocess.check_output', side_effect=command), patch('subprocess.run', side_effect=existing): exec(compile(code, 'release-preflight', 'exec'), {}) - def test_future_1206_candidate_cannot_dispatch_publication(self): + def test_1206_requires_exact_archives_and_java_21(self): code, values = self.select('2.4.12.120061') - self.assertNotEqual(0, code) - self.assertEqual({}, values) + self.assertEqual(0, code) + self.assertEqual({'minecraft_version': '1.20.6', 'source_branch': 'master-1.20.6'}, values) + self.validate('1.20.6') + for options in ({'mutation': {'java_version': '17'}}, {'audit_success': False}, + {'qualification': {'head_branch': 'master-1.20.1'}}, + {'local_receipt': {'manual_accepted': False}}): + with self.subTest(options=options), self.assertRaises(SystemExit): + self.validate('1.20.6', **options) + self.assertIn("minecraft_version == '1.20.6' && 'Java 21'", WORKFLOW.read_text()) + + def test_future_1211_candidate_cannot_dispatch_publication(self): + self.assertNotEqual(0, self.select('2.4.13.121011')[0]) def test_routes_all_supported_versions(self): for suffix, minecraft, branch in [('110021', '1.10.2', '1.10.2'), ('111021', '1.11.2', '1.11.2-dev'), ('112021', '1.12.2', 'master-1.12'), ('113021', '1.13.2', 'master-1.13.2'), ('114041', '1.14.4', 'master-1.14.4')]: diff --git a/.github/workflows/artifact-audit-1.20.yml b/.github/workflows/artifact-audit-1.20.yml index 1f6bb58..c1c93e0 100644 --- a/.github/workflows/artifact-audit-1.20.yml +++ b/.github/workflows/artifact-audit-1.20.yml @@ -1,8 +1,8 @@ name: Audit LootableBodies 1.20 artifacts -run-name: Audit 1.20.1 artifacts from ${{ github.event.workflow_run.head_sha }} +run-name: Audit ${{ github.event.workflow_run.head_branch == 'master-1.20.6' && '1.20.6' || '1.20.1' }} artifacts from ${{ github.event.workflow_run.head_sha }} on: workflow_run: - workflows: [LootableBodies 1.20 CI] + workflows: [LootableBodies 1.20 CI, LootableBodies 1.20.6 CI] types: [completed] permissions: contents: read @@ -12,7 +12,8 @@ jobs: if: >- github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && - github.event.workflow_run.head_branch == 'master-1.20.1' + ((github.event.workflow_run.head_branch == 'master-1.20.1' && github.event.workflow_run.name == 'LootableBodies 1.20 CI') || + (github.event.workflow_run.head_branch == 'master-1.20.6' && github.event.workflow_run.name == 'LootableBodies 1.20.6 CI')) runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -21,7 +22,7 @@ jobs: ref: ${{ github.event.workflow_run.head_sha }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: LootableBodies-1.20.1-${{ github.event.workflow_run.head_sha }} + name: LootableBodies-${{ github.event.workflow_run.head_branch == 'master-1.20.6' && '1.20.6' || '1.20.1' }}-${{ github.event.workflow_run.head_sha }} run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} path: audited diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4f6158c..63adfc4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-1.20.1 + - master-1.20.6 - 'feature/**' pull_request: branches: - - master-1.20.1 + - master-1.20.6 schedule: - cron: '43 7 * * 4' @@ -37,15 +37,15 @@ jobs: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install pinned Java 17 Gradle and compilation runtime + - name: Install pinned Java 21 Gradle and compilation runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '17.0.20+101' + java-version: '21.0.7+6' - name: Pin production JDK run: | - echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" + echo "LOOTABLEBODIES_JAVA21_HOME=$JAVA_HOME_21_X64" >> "$GITHUB_ENV" echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 964caa2..4efe56f 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: release_version: - description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011, 118021, 119041 or 120011. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011, 118021, 119041, 120011 or 120061. required: true type: string curseforge_release_level: @@ -20,7 +20,7 @@ on: default: false type: boolean local_qualification: - description: Hash-bound local qualification receipt, required for the 1.17.1, 1.18.2, 1.19.4 and 1.20.1 releases. + description: Hash-bound local qualification receipt, required for the 1.17.1, 1.18.2, 1.19.4, 1.20.1 and 1.20.6 releases. required: false default: '' type: string @@ -54,7 +54,7 @@ jobs: set -euo pipefail [[ "$GITHUB_REPOSITORY" == 'MinecraftModDevelopmentMods/LootableBodies' ]] [[ "$GITHUB_REF" == 'refs/heads/master-1.12' ]] - [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(110021|111021|112021|113021|114041|115021|116051|117011|118021|119041|120011)$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(110021|111021|112021|113021|114041|115021|116051|117011|118021|119041|120011|120061)$ ]] case "$RELEASE_VERSION" in *.110021) minecraft_version=1.10.2; source_branch=1.10.2 ;; *.111021) minecraft_version=1.11.2; source_branch=1.11.2-dev ;; @@ -67,6 +67,7 @@ jobs: *.118021) minecraft_version=1.18.2; source_branch=master-1.18.2 ;; *.119041) minecraft_version=1.19.4; source_branch=master-1.19.4 ;; *.120011) minecraft_version=1.20.1; source_branch=master-1.20.1 ;; + *.120061) minecraft_version=1.20.6; source_branch=master-1.20.6 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -102,16 +103,17 @@ jobs: '1.17.1': ('master-1.17.1', '37.1.1', '117011'), '1.18.2': ('master-1.18.2', '40.3.12', '118021'), '1.19.4': ('master-1.19.4', '45.4.5', '119041'), - '1.20.1': ('master-1.20.1', '47.4.28', '120011')} + '1.20.1': ('master-1.20.1', '47.4.28', '120011'), + '1.20.6': ('master-1.20.6', '50.2.14', '120061')} branch, forge, suffix = targets[minecraft] if os.environ['SOURCE_BRANCH'] != branch: raise SystemExit('The selected branch does not match its Minecraft version') native17 = minecraft == '1.17.1' - archive_only = minecraft in ('1.18.2', '1.19.4', '1.20.1') + archive_only = minecraft in ('1.18.2', '1.19.4', '1.20.1', '1.20.6') locally_qualified = native17 or archive_only expected = {'minecraft_version': minecraft, 'forge_version': forge, - 'loader_name': 'forge', 'loader_code': '1', 'java_version': '17' if archive_only else '16' if native17 else '8', - 'java_toolchain_version': '17.0.20.1+1' if archive_only else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '17', + 'loader_name': 'forge', 'loader_code': '1', 'java_version': '21' if minecraft == '1.20.6' else '17' if archive_only else '16' if native17 else '8', + 'java_toolchain_version': '21.0.7+6' if minecraft == '1.20.6' else '17.0.20.1+1' if archive_only else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '21' if minecraft == '1.20.6' else '17', 'mod_id': 'lootablebodies', 'mod_artifact': 'LootableBodies', 'mod_group': 'zone.moddev.mc.lootablebodies', 'mod_license': 'LGPL-2.1-only', 'curseforge_project_id': '240968'} @@ -151,12 +153,12 @@ jobs: except (ValueError,TypeError): raise SystemExit('A local qualification receipt is required') keys={'schema','source','version','files','evidence_sha256','qualified_locally','manual_accepted'} - target_code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120'}[minecraft] + target_code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120'}[minecraft] metadata=json.loads(Path(f'.github/release{target_code}.json').read_text()) accepted=metadata['files'] if archive_only and (metadata.get('local_qualification_complete') is not True or metadata.get('qualification_integrity_status')!='verified'): raise SystemExit(f'The local {minecraft} qualification declaration is incomplete') - if minecraft in ('1.19.4', '1.20.1') and metadata.get('manual_accepted') is not True: + if minecraft in ('1.19.4', '1.20.1', '1.20.6') and metadata.get('manual_accepted') is not True: raise SystemExit(f'The local {minecraft} candidate has not been accepted') if (not isinstance(local,dict) or set(local)!=keys or type(local['schema']) is not int or local['schema']!=1 or local['source']!=sha or local['version']!=version or local['files']!=accepted @@ -175,7 +177,7 @@ jobs: matching = [check for check in checks if check['name'] == name] if not matching or max(matching, key=lambda c: c['id'])['conclusion'] != 'success': raise SystemExit(f'The selected commit has no successful {name} check') - if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19.4', '1.20.1'): + if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19.4', '1.20.1', '1.20.6'): target = minecraft.rsplit('.', 1)[0] audits = json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/actions/workflows/artifact-audit-{target}.yml/runs?event=workflow_run&per_page=100'], text=True))['workflow_runs'] @@ -209,25 +211,25 @@ jobs: with: ref: ${{ needs.preflight.outputs.release_sha }} - name: Install Mavenizer Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - name: Install production Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' run: | if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.1' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" @@ -238,7 +240,7 @@ jobs: fi echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Download the qualified native117 six-file bundle if: needs.preflight.outputs.minecraft_version == '1.17.1' @@ -283,18 +285,18 @@ jobs: if: needs.preflight.outputs.minecraft_version == '1.19.4' run: python3 -B tools/native119_build.py --bundle build/release/bundle - name: Download the qualified native120 six-file bundle - if: needs.preflight.outputs.minecraft_version == '1.20.1' + if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: LootableBodies-1.20.1-${{ needs.preflight.outputs.release_sha }} + name: LootableBodies-${{ needs.preflight.outputs.minecraft_version }}-${{ needs.preflight.outputs.release_sha }} run-id: ${{ needs.preflight.outputs.qualification_run }} github-token: ${{ github.token }} path: build/release/bundle - name: Verify all six native120 bytes before publication - if: needs.preflight.outputs.minecraft_version == '1.20.1' + if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' run: python3 -B tools/native120_build.py --bundle build/release/bundle - name: Build, test and audit the release - if: needs.preflight.outputs.minecraft_version != '1.17.1' && needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' + if: needs.preflight.outputs.minecraft_version != '1.17.1' && needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ @@ -399,15 +401,17 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.20.6' && '21.0.7+6' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.20.6' && '21.0.7+6' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain run: | - if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.1' ]]; then + if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.6' ]]; then + echo "LOOTABLEBODIES_JAVA21_HOME=$JAVA_HOME_21_X64" >> "$GITHUB_ENV" + elif [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.1' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" elif [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" @@ -465,7 +469,7 @@ jobs: game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - java: ${{ needs.preflight.outputs.minecraft_version == '1.19.4' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.20.1' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} + java: ${{ needs.preflight.outputs.minecraft_version == '1.20.6' && 'Java 21' || needs.preflight.outputs.minecraft_version == '1.19.4' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.20.1' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} fail-mode: fail files: | ${{ runner.temp }}/release-input/LootableBodies-${{ needs.preflight.outputs.release_version }}.jar diff --git a/.github/workflows/native120.yml b/.github/workflows/native120.yml index 641885a..4f60f8a 100644 --- a/.github/workflows/native120.yml +++ b/.github/workflows/native120.yml @@ -1,10 +1,10 @@ -name: LootableBodies 1.20 CI +name: LootableBodies 1.20.6 CI on: push: - branches: [master-1.20.1] + branches: [master-1.20.6] pull_request: - branches: [master-1.20.1] + branches: [master-1.20.6] permissions: contents: read @@ -48,12 +48,12 @@ jobs: - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '17.0.20+101' + java-version: '21.0.7+6' # Full Java/Python suites, games, IDE consumers and SDK installs stay local. # Fork pushes run policy checks only. MMD builds independently check bytes. - name: Compile and audit release archives env: - LOOTABLEBODIES_JAVA17_HOME: ${{ env.JAVA_HOME_17_X64 }} + LOOTABLEBODIES_JAVA21_HOME: ${{ env.JAVA_HOME_21_X64 }} LOOTABLEBODIES_MAVENIZER_JAVA_HOME: ${{ env.JAVA_HOME_25_X64 }} run: | set -euo pipefail @@ -63,7 +63,7 @@ jobs: - name: Retain the exact six release files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.20.1-${{ github.sha }} + name: LootableBodies-1.20.6-${{ github.sha }} if-no-files-found: error path: build/release/bundle/* - name: Retain build evidence diff --git a/.github/workflows/release-workflow-checks.yml b/.github/workflows/release-workflow-checks.yml index d769d83..e9339d7 100644 --- a/.github/workflows/release-workflow-checks.yml +++ b/.github/workflows/release-workflow-checks.yml @@ -1,9 +1,9 @@ name: Validate release workflow on: push: - branches: ['feature/**', 'master-1.20.1'] + branches: ['feature/**', 'master-1.20.6'] pull_request: - branches: ['master-1.20.1'] + branches: ['master-1.20.6'] permissions: contents: read jobs: diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0b5ef46..04732ac 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,6 @@ -2.4.11.120011 - Minecraft 1.20.1, Forge +2.4.12.120061 - Minecraft 1.20.6, Forge -Updated for Minecraft 1.20.1. +Updated for Minecraft 1.20.6. Preserves bodies and their contents when upgrading older worlds. Back up the whole world before upgrading. diff --git a/README.md b/README.md index 34a5689..3080101 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# Lootable Bodies for Minecraft 1.20.1 +# Lootable Bodies for Minecraft 1.20.6 Your items stay on a corpse when you die. Right-click the body to recover them. -Use Forge 47.4.28 and Java 17. Install `LootableBodies-2.4.11.120011.jar` +Use Forge 50.2.14 and Java 21. Install `LootableBodies-2.4.12.120061.jar` in the client and server mods folders. Back up your world before upgrading. Created by Cyanobacterium (DrCyano), maintained by SkyBlade1978. -Licensed under LGPLv2.1; the original CC0 dedication is included in LICENSE-CC0. +Licensed under LGPLv2.1. The original CC0 dedication is included in LICENSE-CC0. Open Mods, select Lootable Bodies and click Config to change its settings. Appearance changes apply straight away on your client. World and server rules take effect after restarting Minecraft or the server. -Existing 1.10.2, 1.11.2, 1.12.2, 1.13.2, 1.14.4, 1.15.2, 1.16.5, 1.17.1, 1.18.2 and 1.19.4 corpses are preserved when upgrading, +Existing 1.10.2, 1.11.2, 1.12.2, 1.13.2, 1.14.4, 1.15.2, 1.16.5, 1.17.1, 1.18.2, 1.19.4 and 1.20.1 corpses are preserved when upgrading, including bodies in chunks you visit later. Keep the whole world backup, including its `data` folder. If preservation cannot finish safely, world loading stops with an explanation in the log. Items from missing mods are retained for diff --git a/RELEASING.md b/RELEASING.md index 65cf4e9..14e31ad 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,7 +1,5 @@ # Releasing -The Forge 1.20.1 port stays local until qualification and manual testing are complete. - Gameplay, graphical clients and world migration checks run locally before publishing. Full Java/Python suites run locally. GitHub Actions runs production builds, archive checks, security scans and small workflow/wrapper contracts. It does not install a separate game runtime or run the gameplay suite. @@ -19,7 +17,7 @@ archive checks, security scans and small workflow/wrapper contracts. It does not and security checks pass. Recheck the exact merged source and all six file hashes. The independent artifact audit checks the uploaded files without rebuilding them. 4. Rehash the retained local evidence and supply its receipt to the guarded - release workflow. For 1.17.1, 1.18.2, 1.19.4 and 1.20.1 the receipt binds the exact source commit, version, + release workflow. For 1.17.1, 1.18.2, 1.19.4, 1.20.1 and 1.20.6 the receipt binds the exact source commit, version, six file hashes, evidence hash and manual acceptance. Publish the files already checked by CI, then independently download and compare the public GitHub files and CurseForge jar. An accepted upload alone does not complete the release. diff --git a/TESTING.md b/TESTING.md index caf8c62..1c8a5fb 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,7 +1,7 @@ # Testing -Run qualification for Forge 1.20.1 locally. Hosted checks cover workflow and wrapper contracts, compilation, archive checks -and security analysis. +Run qualification for Forge 1.20.6 locally with Forge 50.2.14 and Java 21. +GitHub Actions checks production archives and release contracts. During development, reproduce the changed behaviour first, then batch the cheap Java and Python checks. Reserve the full runtime suite for a frozen candidate. @@ -32,7 +32,9 @@ For a frozen candidate: Unchanged mod-free resource/protocol controls can be reused only with the same control, SDK, JDK, flags, resources and auditor contract. Changes to those inputs -require new control qualification. Changes confined to a test fixture require +require new control qualification. Use `tools/fixtures/native1206-forge-only-flat-level.dat` +for the Forge-only respawn control so it starts with Forge's own saved registry. +Changes confined to a test fixture require the affected proof and tool contracts, not an automatic replay of unrelated passed groups. A changed production migration path still requires its full applicable migration coverage. diff --git a/build.gradle b/build.gradle index b54a5e2..5e931c4 100644 --- a/build.gradle +++ b/build.gradle @@ -14,18 +14,18 @@ group = project.mod_group version = project.mod_version base.archivesName = project.mod_artifact -// Gradle uses Java 17; every compiler, test and game process uses this JDK 17. +// Gradle uses Java 21; every compiler, test and game process uses this JDK 21. // Eclipse does not inherit the command-line helper's environment. Keep its // machine-specific toolchain selection in an ignored local properties file. Properties localJavaSetup = new Properties() File localJavaSetupFile = file('gradle-local.properties') if (localJavaSetupFile.isFile()) localJavaSetupFile.withInputStream { localJavaSetup.load(it) } -def pinnedJava17 = providers.environmentVariable('LOOTABLEBODIES_JAVA17_HOME') - .orElse(providers.gradleProperty('lootablebodies.java17.home')) - .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.java17.home') }) -def exactJava17Spec = pinnedJava17.isPresent() ? +def pinnedJava21 = providers.environmentVariable('LOOTABLEBODIES_JAVA21_HOME') + .orElse(providers.gradleProperty('lootablebodies.java21.home')) + .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.java21.home') }) +def exactJava21Spec = pinnedJava21.isPresent() ? org.gradle.jvm.toolchain.internal.SpecificInstallationToolchainSpec.fromJavaHome( - project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava17.get())) : null + project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava21.get())) : null def pinnedJava25 = providers.environmentVariable('LOOTABLEBODIES_MAVENIZER_JAVA_HOME') .orElse(providers.gradleProperty('lootablebodies.mavenizer.java.home')) .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.mavenizer.java.home') }) @@ -37,30 +37,30 @@ ext.mavenizerLauncher = exactJava25Spec != null ? javaToolchains.launcherFor(exa fgtools.configure('mavenizer') { javaLauncher.set(mavenizerLauncher) } java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() withJavadocJar() } -ext.productionLauncher = exactJava17Spec != null ? javaToolchains.launcherFor(exactJava17Spec) : +ext.productionLauncher = exactJava21Spec != null ? javaToolchains.launcherFor(exactJava21Spec) : javaToolchains.launcherFor(java.toolchain) fgtools.configure('slimelauncher') { javaLauncher.set(productionLauncher) } -def productionCompiler = exactJava17Spec != null ? javaToolchains.compilerFor(exactJava17Spec) : +def productionCompiler = exactJava21Spec != null ? javaToolchains.compilerFor(exactJava21Spec) : javaToolchains.compilerFor(java.toolchain) -def productionJavadoc = exactJava17Spec != null ? javaToolchains.javadocToolFor(exactJava17Spec) : +def productionJavadoc = exactJava21Spec != null ? javaToolchains.javadocToolFor(exactJava21Spec) : javaToolchains.javadocToolFor(java.toolchain) tasks.register('verifyJavaSetup') { group = 'verification' doLast { - if (JavaVersion.current() != JavaVersion.VERSION_17) throw new GradleException('Run Gradle with Java 17') + if (JavaVersion.current() != JavaVersion.VERSION_21) throw new GradleException('Run Gradle with Java 21') def metadata = productionLauncher.get().metadata Properties release = new Properties() metadata.installationPath.file('release').asFile.withInputStream { release.load(it) } - if (release.getProperty('SEMANTIC_VERSION') != '"17.0.20.1+1"' || + if (release.getProperty('JAVA_RUNTIME_VERSION') != '"21.0.7+6-LTS"' || release.getProperty('IMPLEMENTOR') != '"Eclipse Adoptium"') { - throw new GradleException("Production requires Temurin 17.0.20.1+1, found ${metadata.javaRuntimeVersion} ${metadata.vendor}") + throw new GradleException("Production requires Temurin 21.0.7+6, found ${metadata.javaRuntimeVersion} ${metadata.vendor}") } logger.lifecycle("Gradle Java: ${System.getProperty('java.runtime.version')}; production: ${metadata}") def worker = mavenizerLauncher.get().metadata @@ -69,14 +69,14 @@ tasks.register('verifyJavaSetup') { if (workerRelease.getProperty('JAVA_RUNTIME_VERSION') != '"25.0.3+9-LTS"' || workerRelease.getProperty('IMPLEMENTOR') != '"Eclipse Adoptium"') throw new GradleException('The separate Mavenizer worker requires Temurin 25.0.3+9-LTS') - logger.lifecycle("Mavenizer worker: ${worker}; game/compilation remain on Java 17") + logger.lifecycle("Mavenizer worker: ${worker}; game/compilation remain on Java 21") } } tasks.withType(JavaCompile).configureEach { dependsOn 'verifyJavaSetup' javaCompiler = productionCompiler - sourceCompatibility = '17' - targetCompatibility = '17' + sourceCompatibility = '21' + targetCompatibility = '21' options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs','1000']) } @@ -101,7 +101,8 @@ tasks.register('generateNative120Fixtures', JavaExec) { dependsOn 'testClasses' classpath = sourceSets.test.runtimeClasspath mainClass = 'cyano.lootable.buildtest.Native120FixtureGenerator' - args providers.gradleProperty('nativeFixtureOutput').getOrElse('build/native120-fixture-preparation-01') + args providers.gradleProperty('nativeFixtureOutput').getOrElse('build/native1206-fixture-preparation-01'), + file('tools/fixtures/native1206-flat-level.dat').absolutePath } tasks.withType(JavaExec).configureEach { javaLauncher = productionLauncher } tasks.withType(Javadoc).configureEach { @@ -157,12 +158,6 @@ minecraft { register('server') { args '--nogui' } } } -tasks.withType(JavaExec).configureEach { run -> - if (run.name in ['runClient','runServer']) { - environment 'MOD_CLASSES', "${mod_id}%%${layout.buildDirectory.dir('forge17-development/main').get().asFile.absolutePath}" - } -} - tasks.matching { it.name == 'runClient' }.configureEach { dependsOn 'prepareMinecraftAssets' } tasks.matching { it.name == 'runServer' }.configureEach { standardInput = System.in } @@ -175,7 +170,7 @@ tasks.register('prepareProductionRunDirectory') { } } -// Legacy FML discovers classes and processed resources from a single root. +// Forge discovers the compiled mod and its processed metadata on the classpath. ext.developmentOutput = layout.buildDirectory.dir('forge17-development/main') tasks.register('syncDevelopmentOutput', Sync) { dependsOn 'classes' @@ -217,7 +212,7 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2' testRuntimeOnly 'org.openjdk.nashorn:nashorn-core:15.4' - testRuntimeOnly 'net.minecraftforge:coremods:5.2.4' + testRuntimeOnly 'net.minecraftforge:coremods:5.2.6' } tasks.named('processResources', ProcessResources) { @@ -231,24 +226,17 @@ tasks.named('processResources', ProcessResources) { filesMatching(archiveTextPatterns, normalizeArchiveText) } tasks.named('jar', Jar) { - archiveClassifier = 'deobf' - destinationDirectory = layout.buildDirectory.dir('libs-dev') from(['LICENSE', 'LICENSE-CC0', 'NOTICE']) { into 'META-INF' } manifest.attributes('Implementation-Title': project.mod_name, 'Implementation-Version': project.version, 'Implementation-Vendor': project.mod_vendor, 'Maven-Artifact': "${project.group}:${base.archivesName.get()}:${project.version}", 'Implementation-License': project.mod_license, - 'Built-On-Java': '17', 'Built-On': "${minecraft_version}-${forge_version}", + 'Built-On-Java': '21', 'Built-On': "${minecraft_version}-${forge_version}", 'FMLAT': 'accesstransformer.cfg') filesMatching(archiveTextPatterns, normalizeArchiveText) } -ext.releaseJar = renamer.classes(tasks.named('jar', Jar)) { - map.from minecraft.dependency.toSrgFile - accessTransformers = true - libraries.from(sourceSets.main.compileClasspath) - archiveClassifier = null - output = layout.buildDirectory.file("libs/${base.archivesName.get()}-${project.version}.jar") -} +// Forge 50.2.14 ships its runtime with official names. +ext.releaseJar = tasks.named('jar', Jar) ['sourcesJar', 'javadocJar'].each { name -> tasks.named(name, Jar) { from(['LICENSE', 'LICENSE-CC0', 'NOTICE']) { into 'META-INF' } @@ -263,8 +251,8 @@ tasks.named('sourcesJar', Jar) { inputs.property('accessTransformerLineEndings', 'lf') filesMatching('META-INF/accesstransformer.cfg', normalizeArchiveText) } -// Java 17 writes this extensionless index with the host line separator. Keep -// the 1.18 documentation bytes identical on Windows and Linux. +// Java 21 writes this extensionless index with the host line separator. Keep +// the documentation bytes identical on Windows and Linux. tasks.named('javadocJar', Jar) { filesMatching('element-list') { filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('crlf'), diff --git a/gradle.properties b/gradle.properties index 9b3283e..ede235e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,17 +5,17 @@ org.gradle.caching=true org.gradle.parallel=false org.gradle.workers.max=2 net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.20.1 -forge_version=47.4.28 +minecraft_version=1.20.6 +forge_version=50.2.14 mapping_channel=official -mapping_version=1.20.1 +mapping_version=1.20.6 # Release identity and dispatcher target metadata. loader_name=forge loader_code=1 -java_version=17 -java_toolchain_version=17.0.20.1+1 -gradle_java_version=17 +java_version=21 +java_toolchain_version=21.0.7+6 +gradle_java_version=21 curseforge_project_id=240968 mod_id=lootablebodies @@ -25,7 +25,7 @@ mod_vendor=Cyanobacterium mod_license=LGPL-2.1-only mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=2.4.11.120011 +mod_version=2.4.12.120061 mod_group=zone.moddev.mc.lootablebodies mod_authors=Cyanobacterium,SkyBlade1978 mod_description=Saves your items on a lootable corpse when you die. diff --git a/gradle/assets.gradle b/gradle/assets.gradle index 1beb87d..59f9cdf 100644 --- a/gradle/assets.gradle +++ b/gradle/assets.gradle @@ -1,6 +1,6 @@ // Keep development launches independent of any installed Minecraft instance. ext.minecraftGameHome = layout.projectDirectory.dir('.gradle/minecraft-home') -ext.minecraftAssetIndex = '5' +ext.minecraftAssetIndex = '16' // Buildship refresh has no command-line -P arguments. Retain a prepared local // client location rather than requiring its assets/natives to be copied. Properties localMinecraftSetup = new Properties() @@ -10,14 +10,17 @@ String assetFolder=org.gradle.internal.os.OperatingSystem.current().isMacOsX()?' ext.minecraftAssets = layout.projectDirectory.dir(providers.gradleProperty('minecraftAssetsDirectory') .getOrElse(localMinecraftSetup.getProperty('minecraftAssetsDirectory',minecraftGameHome.dir(assetFolder).asFile.absolutePath))) ext.minecraftNatives = layout.projectDirectory.dir(providers.gradleProperty('minecraftNativesDirectory') - .getOrElse(localMinecraftSetup.getProperty('minecraftNativesDirectory','.gradle/native120-client-inputs/natives'))) + .getOrElse(localMinecraftSetup.getProperty('minecraftNativesDirectory','.gradle/native1206-client-inputs/natives'))) tasks.register('prepareMinecraftAssets') { group = 'ide' doLast { - File root=minecraftAssets.asFile + File root=minecraftAssets.asFile.canonicalFile + boolean owned = root.toPath().startsWith(minecraftGameHome.asFile.canonicalFile.toPath()) def sha1={File value -> java.security.MessageDigest.getInstance('SHA-1').digest(value.bytes).encodeHex().toString()} def download={String url,String expected,File output -> if(!output.isFile() || sha1(output)!=expected) { + if (!owned || !output.canonicalFile.toPath().startsWith(root.toPath())) + throw new GradleException("Missing or changed asset in read-only location: ${output}") output.parentFile.mkdirs() File pending=new File(output.parentFile,output.name+'.download') new URL(url).withInputStream { input -> pending.withOutputStream { stream -> stream << input } } @@ -25,10 +28,10 @@ tasks.register('prepareMinecraftAssets') { java.nio.file.Files.move(pending.toPath(),output.toPath(),java.nio.file.StandardCopyOption.REPLACE_EXISTING) } } - File descriptor=new File(root,'1.20.1.json') - download('https://piston-meta.mojang.com/v1/packages/72ae93b0934df5541860e3e6b72cf883c74da491/1.20.1.json','72ae93b0934df5541860e3e6b72cf883c74da491',descriptor) + File descriptor=new File(root,'1.20.6.json') + download('https://piston-meta.mojang.com/v1/packages/4dd84bf5b646a75cba5c0c5f7723eb478a2b78af/1.20.6.json','4dd84bf5b646a75cba5c0c5f7723eb478a2b78af',descriptor) def version=new groovy.json.JsonSlurper().parse(descriptor) - if(version.id!='1.20.1' || version.assetIndex.id!=minecraftAssetIndex) + if(version.id!='1.20.6' || version.assetIndex.id!=minecraftAssetIndex) throw new GradleException('Prepared Minecraft client metadata has the wrong target identity') File index=new File(root,"indexes/${minecraftAssetIndex}.json") download(version.assetIndex.url,version.assetIndex.sha1,index) diff --git a/gradle/audit.gradle b/gradle/audit.gradle index 802b17b..c9504c4 100644 --- a/gradle/audit.gradle +++ b/gradle/audit.gradle @@ -33,7 +33,7 @@ ext.auditReleaseArchives = { File directory -> if (!path.startsWith('cyano/lootable/')) throw new GradleException("Foreign production class ${path}") byte[] bytes = zip.getInputStream(entry).bytes int major = ((bytes[6] & 255) << 8) | (bytes[7] & 255) - if (major != 61) throw new GradleException("${path} is class major ${major}") + if (major != 65) throw new GradleException("${path} is class major ${major}") } if (path.endsWith('.lang')) { java.nio.charset.StandardCharsets.UTF_8.newDecoder().decode( @@ -62,22 +62,20 @@ ext.auditReleaseArchives = { File directory -> if (!names.contains(it)) throw new GradleException("Missing production entry ${it}") } String info=zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') - ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.20.1]"'].each { + ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.20.6]"'].each { if(!info.contains(it))throw new GradleException("Incorrect production metadata: ${it}") } def pack=new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('pack.mcmeta')).getText('UTF-8')).pack - if(pack.pack_format!=15 || pack.containsKey('forge:server_data_pack_format') || + if(pack.pack_format!=32 || pack.supported_formats!=[min_inclusive:32,max_inclusive:41] || pack.containsKey('forge:server_data_pack_format') || pack.containsKey('forge:resource_pack_format') || pack.containsKey('forge:data_pack_format')) - throw new GradleException('Native pack formats must be resource15/server-data15') + throw new GradleException('Native pack formats must be resource32/server-data41') def coremods=new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('META-INF/coremods.json')).getText('UTF-8')) if(coremods!=[lootablebodies_preservation:'coremods/early-world-preservation.js']) throw new GradleException('Missing or changed early native save attachment') def atEntry=zip.getEntry('META-INF/accesstransformer.cfg') def atRules=atEntry?zip.getInputStream(atEntry).getText('UTF-8').readLines() .collect {it.split('#',2)[0].trim()}.findAll {!it.isEmpty()}:[] - if(atRules!=['public-f net.minecraft.util.datafix.DataFixers f_216514_', - 'public net.minecraft.world.level.block.entity.SkullBlockEntity f_59755_', - 'public net.minecraft.world.level.block.entity.SkullBlockEntity f_59756_'] || + if(atRules!=['public-f net.minecraft.util.datafix.DataFixers dataFixer'] || manifest.getValue('FMLAT')!='accesstransformer.cfg') throw new GradleException('Missing or changed native world-reader access boundary') def languages=names.findAll {it.startsWith('assets/lootablebodies/lang/') && !it.endsWith('/')} @@ -87,8 +85,16 @@ ext.auditReleaseArchives = { File directory -> throw new GradleException('Bundled sources or dependencies in main jar') String entity = new String(zip.getInputStream(zip.getEntry( 'cyano/lootable/entities/EntityLootableBody.class')).bytes, 'ISO-8859-1') - if (!entity.contains('m_7378_') || !entity.contains('m_7380_')) - throw new GradleException('Production jar does not have runtime SRG mappings') + if (!entity.contains('readAdditionalSaveData') || !entity.contains('addAdditionalSaveData') + || entity.contains('m_7378_') || entity.contains('m_7380_')) + throw new GradleException('Production jar does not use the official Forge runtime names') + ['can_breathe_under_water', 'undead', 'ignores_poison_and_regen', + 'inverted_healing_and_harm', 'sensitive_to_smite'].each { tagName -> + def entry=zip.getEntry("data/minecraft/tags/entity_types/${tagName}.json") + if(!entry || new JsonSlurper().parseText(zip.getInputStream(entry).getText('UTF-8'))!= + [replace:false,values:['lootablebodies:corpse']]) + throw new GradleException("Missing corpse entity tag: ${tagName}") + } } else if (index == 1) { if (!names.contains('cyano/lootable/LootableBodies.java')) throw new GradleException('Sources jar does not contain production sources') @@ -119,11 +125,10 @@ tasks.register('verifyReleaseArtifacts') { if (actual != releaseNames as Set) throw new GradleException('Release output must contain exactly the three declared jars') auditReleaseArchives(libs) new ZipFile(new File(libs, releaseNames[0])).withCloseable { zip -> - new ZipFile(tasks.jar.archiveFile.get().asFile).withCloseable { dev -> - String cls = 'cyano/lootable/entities/EntityLootableBody.class' - if (Arrays.equals(zip.getInputStream(zip.getEntry(cls)).bytes, dev.getInputStream(dev.getEntry(cls)).bytes)) - throw new GradleException('Production jar was not reobfuscated') - } + String cls = 'cyano/lootable/entities/EntityLootableBody.class' + File compiled=layout.buildDirectory.file("classes/java/main/${cls}").get().asFile + if(!Arrays.equals(zip.getInputStream(zip.getEntry(cls)).bytes,compiled.bytes)) + throw new GradleException('Production classes must retain the official runtime names') } } } diff --git a/gradle/ide.gradle b/gradle/ide.gradle index e12c0bc..dd9b660 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -1,5 +1,5 @@ eclipse { - jdt.javaRuntimeName = 'JavaSE-17' + jdt.javaRuntimeName = 'JavaSE-21' classpath { downloadSources = true downloadJavadoc = true @@ -36,7 +36,7 @@ tasks.register('configureEclipseBuildship') { File localJavaSetup = file('gradle-local.properties') Properties toolchains = new Properties() if (localJavaSetup.isFile()) localJavaSetup.withInputStream { toolchains.load(it) } - toolchains.setProperty('lootablebodies.java17.home', + toolchains.setProperty('lootablebodies.java21.home', productionLauncher.get().metadata.installationPath.asFile.canonicalPath) toolchains.setProperty('lootablebodies.mavenizer.java.home', mavenizerLauncher.get().metadata.installationPath.asFile.canonicalPath) @@ -67,14 +67,11 @@ tasks.register('isolateEclipseProductionRuns') { if (!launch.isFile()) throw new GradleException("Generate ${name} with genEclipseRuns first") String contents = launch.getText('UTF-8') File nativeRoot=minecraftNatives.asFile - if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native1.20.1 client inputs before generating Eclipse launches') + if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native1.20.6 client inputs before generating Eclipse launches') contents=contents.replace('{asset_index}',minecraftAssetIndex) .replace('{assets_root}',minecraftAssets.asFile.absolutePath) .replace('{natives}',nativeRoot.absolutePath) - String eclipseRoot=file('bin/main').absolutePath - String eclipseMods="${mod_id}%%${eclipseRoot}${File.pathSeparator}${mod_id}%%${eclipseRoot}" - contents=contents.replaceAll(/()/) {whole,start,end -> start+eclipseMods+end} - contents = contents.replace('/JavaSE-8', '/JavaSE-17') + contents = contents.replace('/JavaSE-8', '/JavaSE-21') contents = contents.replaceAll(//, '') contents = contents.replace('', ' \n') @@ -113,8 +110,10 @@ tasks.register('verifyEclipseProductionClasspath') { if (!classpathXml.contains('path="build/resources/main"') || classpathXml.contains('path="src/main/resources"')) throw new GradleException('Eclipse must use Gradle-processed resources') def classpathModel = new groovy.xml.XmlSlurper().parseText(classpathXml) - if (classpathModel.classpathentry.findAll { it.@path.text() == 'build/resources/main' }.size() != 1) - throw new GradleException('Duplicate Eclipse production resource roots') + def outputRoots = classpathModel.classpathentry.findAll { it.@output.text() == 'bin/main' } + if (outputRoots.collect { it.@path.text() }.toSet() != ['build/resources/main', 'src/main/java'] as Set || + outputRoots.size() != 2) + throw new GradleException('Eclipse production classes and processed resources must share bin/main') classpathModel.classpathentry.findAll { it.@path.text() =~ /(?i)(junit|opentest4j|apiguardian)/ }.each { entry -> if (!entry.attributes.attribute.any { it.@name.text() == 'test' && it.@value.text() == 'true' }) throw new GradleException('Eclipse testing library is not marked test-only') @@ -128,15 +127,26 @@ tasks.register('verifyEclipseProductionClasspath') { xml =~ /(?i)(src\/test|bin\/test|junit-|integration-test|mockito|byte-buddy)/ || !xml.contains('PROJECT_ATTR" value="LootableBodies"')) throw new GradleException("${name} has the wrong project or exposes tests") - if (!xml.contains("--fml.mcVersion ${minecraft_version}") || xml.contains('{asset_index}') || - xml.contains('{assets_root}') || xml.contains('{natives}')) - throw new GradleException('Eclipse Minecraft version is unresolved') def launchModel = new groovy.xml.XmlSlurper().parseText(xml) - String classes = launchModel.mapAttribute.mapEntry.find { it.@key.text() == 'MOD_CLASSES' }.@value.text() - String expectedRoot = file('bin/main').absolutePath.replace('\\', '/') - String expectedClasses = "${mod_id}%%${expectedRoot}${File.pathSeparator}${mod_id}%%${expectedRoot}" - if (classes != expectedClasses) - throw new GradleException("${name} must launch Eclipse's production output") + String arguments = launchModel.stringAttribute.find { + it.@key.text() == 'org.eclipse.jdt.launching.PROGRAM_ARGUMENTS' + }.@value.text() + String side = name == 'runClient.launch' ? 'client' : 'server' + String metadataJar = "forge-${minecraft_version}-${forge_version}-metadata.zip" + if (!arguments.contains("--launchTarget forge_userdev_${side}") || + !arguments.contains('--main net.minecraftforge.bootstrap.ForgeBootstrap') || + !arguments.contains(metadataJar) || (side == 'client' && !arguments.contains("--assetIndex ${minecraftAssetIndex}")) || + xml.contains('{asset_index}') || xml.contains('{assets_root}') || xml.contains('{natives}')) + throw new GradleException("Eclipse Forge metadata, target or client assets are unresolved for ${name}") + // Forge 50 discovers the processed mods.toml directly on the + // project classpath. The launcher no longer reads MOD_CLASSES. + def entries = launchModel.listAttribute.find { + it.@key.text() == 'org.eclipse.jdt.launching.CLASSPATH' + }.listEntry.collect { new groovy.xml.XmlSlurper().parseText(it.@value.text()) } + if (entries.count { it.@projectName.text() == rootProject.name } != 1 || + !entries.any { it.@containerPath.text().endsWith('/JavaSE-21') } || + launchModel.mapAttribute.mapEntry.any { it.@key.text() == 'MOD_CLASSES' }) + throw new GradleException("${name} must launch the production Eclipse project on Java 21") String directory = launchModel.stringAttribute.find { it.@key.text() == 'org.eclipse.jdt.launching.WORKING_DIRECTORY' }.@value.text() diff --git a/gradle/release.gradle b/gradle/release.gradle index 6da03c7..3c9791c 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,10 +5,10 @@ def expectedIdentity = [ mod_id: 'lootablebodies', mod_artifact: 'LootableBodies', mod_group: 'zone.moddev.mc.lootablebodies', mod_vendor: 'Cyanobacterium', mod_name: "DrCyano's Lootable Bodies", mod_authors: 'Cyanobacterium,SkyBlade1978', - minecraft_version: '1.20.1', forge_version: '47.4.28', - mapping_channel: 'official', mapping_version: '1.20.1', + minecraft_version: '1.20.6', forge_version: '50.2.14', + mapping_channel: 'official', mapping_version: '1.20.6', loader_name: 'forge', loader_code: '1', curseforge_project_id: '240968', - java_version: '17', java_toolchain_version: '17.0.20.1+1', gradle_java_version: '17', + java_version: '21', java_toolchain_version: '21.0.7+6', gradle_java_version: '21', mod_license: 'LGPL-2.1-only', mod_license_name: 'GNU Lesser General Public License, Version 2.1', mod_license_url: 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt', mod_url: 'https://github.com/MinecraftModDevelopmentMods/LootableBodies', diff --git a/gradle/runtime.gradle b/gradle/runtime.gradle index bdb3494..ebe610d 100644 --- a/gradle/runtime.gradle +++ b/gradle/runtime.gradle @@ -30,12 +30,7 @@ def probeJar = tasks.register('integrationTestModJar', Jar) { } } } -def packagedProbe = renamer.classes(probeJar) { - map.from minecraft.dependency.toSrgFile - libraries.from(tasks.named('jar'), sourceSets.main.compileClasspath) - output = layout.buildDirectory.file('integration-test-mod/lootablebodies-build-tests-reobf.jar') -} -tasks.register('preparePackagedIntegrationProbe') { dependsOn packagedProbe, releaseJar } +tasks.register('preparePackagedIntegrationProbe') { dependsOn probeJar, releaseJar } // Old writers must run against the old Forge API, never this native classpath. // prepare_legacy_tools.py builds the pinned released source in a disposable @@ -75,10 +70,10 @@ def prepareOwnedLoaderConfig = { File root -> } } -// Use an actual native47 codec header in each newly owned flat fixture world. +// Use an actual Forge 50 codec header in each newly owned flat fixture world. def prepareOwnedFlatWorld = { File root -> - File header = file('tools/fixtures/native120-flat-level.dat') - if (!header.isFile() || releaseDigest(header) != '81E5056FCE49638B40F532B819FC82B0EBBFC954BCF4A7E20E3D6538E8FC0645') + File header = file('tools/fixtures/native1206-flat-level.dat') + if (!header.isFile() || releaseDigest(header) != '34253D55B2B3A4F1CE4107FACF5AF64ED496264ED53804405F6CDC6D45E56E74') throw new GradleException('Missing or changed native flat fixture header') File world = new File(root,'world') if (world.exists()) throw new GradleException('Refusing to replace an existing fixture world') @@ -281,33 +276,15 @@ def controlJar=tasks.register('forgeControlJar',Jar) { archiveFileName='forge-platform-control.jar' destinationDirectory=layout.buildDirectory.dir('control-test-mod') } -def packagedControl=renamer.classes(controlJar) { - map.from minecraft.dependency.toSrgFile - libraries.from(sourceSets.main.compileClasspath) - output=layout.buildDirectory.file('control-test-mod/forge-platform-control-reobf.jar') -} -tasks.register('preparePackagedForgeControl') { dependsOn packagedControl } +tasks.register('preparePackagedForgeControl') { dependsOn controlJar } // A separate run model keeps the independent control isolated even after // Eclipse generation has finalized the production models' environment. minecraft.runs.register('platformControl') { client=false - // Native37 supplies BootstrapLauncher only inside its named run configs. - // This separate model needs the exact pinned server metadata parameters. - mainClass.set('cpw.mods.bootstraplauncher.BootstrapLauncher') - args '--launchTarget','forgeserveruserdev','--gameDir','.', - '--fml.forgeVersion',project.forge_version,'--fml.mcVersion',project.minecraft_version, - '--fml.forgeGroup','net.minecraftforge','--fml.mcpVersion','20210706.113038','--nogui' - jvmArgs '-p','{modules}','--add-modules','ALL-MODULE-PATH', - '--add-opens','java.base/java.util.jar=cpw.mods.securejarhandler', - '--add-exports','java.base/sun.security.util=cpw.mods.securejarhandler', - '--add-exports','jdk.naming.dns/com.sun.jndi.dns=java.naming' - systemProperty 'legacyClassPath.file','{minecraft_classpath_file}' - systemProperty 'ignoreList','bootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,client-extra,fmlcore,javafmllanguage,mclanguage,forge-' - systemProperty 'mergeModules','jna-5.8.0.jar,jna-platform-58.0.jar,java-objc-bridge-1.0.0.jar' - environment 'MCP_MAPPINGS','{mcp_mappings}' + mainClass.set('net.minecraftforge.bootstrap.ForgeBootstrap') + args '--launchTarget','forge_userdev_server','--gameDir','.','--nogui' + jvmArgs '--add-opens','java.base/java.lang.invoke=ALL-UNNAMED' workingDir=layout.buildDirectory.dir('forge-control-run') - String root=layout.buildDirectory.dir('forge17-development/control').get().asFile.absolutePath - environment.put('MOD_CLASSES',"forgeplatformcontrol%%${root}${File.pathSeparator}forgeplatformcontrol%%${root}") } def prepareForgeControl = tasks.register('prepareForgeControl') { dependsOn controlJar diff --git a/settings.gradle b/settings.gradle index d9494d9..cdf0bac 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,7 +13,7 @@ gradle.taskGraph.whenReady { graph -> task instanceof org.gradle.api.tasks.JavaExec || task.name in ['integrationTest', 'clientIntegrationTest', 'forgeControlServer', 'localRuntimeVerification', 'prepareLegacyWorldFixtures', - 'generateNative120Fixtures'] || + 'generateNative120Fixtures', 'generateNative1206Fixtures'] || task.name.startsWith('integration') } if (!localOnly.empty) throw new GradleException( diff --git a/src/controlTest/java/buildcontrol/ForgeControl.java b/src/controlTest/java/buildcontrol/ForgeControl.java index 3a9c100..51332f4 100644 --- a/src/controlTest/java/buildcontrol/ForgeControl.java +++ b/src/controlTest/java/buildcontrol/ForgeControl.java @@ -61,7 +61,7 @@ public static final class ClientControl { }catch(java.io.IOException failure){throw new IllegalStateException(failure);} stage=4; }else if(stage==4 && ++ticks>=80) { - mc.level.disconnect();mc.clearLevel();stage=2; + mc.level.disconnect();mc.disconnect();stage=2; }else if(stage==2 && mc.level==null && !mc.hasSingleplayerServer())finish(mc); } private int connections,wait,deathTicks,respawns; @@ -100,23 +100,23 @@ private void tcp(net.minecraft.client.Minecraft mc) { if(field.getType()==net.minecraft.network.Connection.class) { field.setAccessible(true);net.minecraft.network.Connection manager=(net.minecraft.network.Connection)field.get(mc.screen); if(manager!=null && manager.channel()!=null) { - System.out.println("NATIVE_FORGE_TCP_STATE autoRead="+manager.channel().config().isAutoRead()+" active="+manager.channel().isActive()+" protocol="+manager.channel().attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).get()); + System.out.println("NATIVE_FORGE_TCP_STATE autoRead="+manager.channel().config().isAutoRead()+" active="+manager.channel().isActive()+" protocol="+manager.getInboundProtocolInfo()); } } }catch(ReflectiveOperationException failure){throw new IllegalStateException(failure);} } if(stage==0 && mc.screen instanceof net.minecraft.client.gui.screens.TitleScreen) { if(connections>=10){finish(mc);return;} - destination=new net.minecraft.client.multiplayer.ServerData("Owned native control","127.0.0.1:"+Integer.getInteger("lootablebodies.control.port"),false); + destination=new net.minecraft.client.multiplayer.ServerData("Owned native control","127.0.0.1:"+Integer.getInteger("lootablebodies.control.port"),net.minecraft.client.multiplayer.ServerData.Type.OTHER); if(Boolean.getBoolean("lootablebodies.control.ping")) { pinger=new net.minecraft.client.multiplayer.ServerStatusPinger(); - try{pinger.pingServer(destination,()->{});}catch(java.net.UnknownHostException failure){throw new IllegalStateException(failure);} + try{pinger.pingServer(destination,()->{},()->{});}catch(java.net.UnknownHostException failure){throw new IllegalStateException(failure);} stage=5; - }else {net.minecraft.client.gui.screens.ConnectScreen.startConnecting(new net.minecraft.client.gui.screens.TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(destination.ip),destination,false);stage=1;} + }else {net.minecraft.client.gui.screens.ConnectScreen.startConnecting(new net.minecraft.client.gui.screens.TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(destination.ip),destination,false,null);stage=1;} wait=0; }else if(stage==5) { pinger.tick(); - if(destination.ping>=0){pinger.removeAll();net.minecraft.client.gui.screens.ConnectScreen.startConnecting(new net.minecraft.client.gui.screens.TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(destination.ip),destination,false);stage=1;wait=0;} + if(destination.ping>=0){pinger.removeAll();net.minecraft.client.gui.screens.ConnectScreen.startConnecting(new net.minecraft.client.gui.screens.TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(destination.ip),destination,false,null);stage=1;wait=0;} }else if(stage==1 && mc.level!=null && mc.player!=null) { if(Boolean.getBoolean("lootablebodies.control.deaths")) { delayNativePackets(mc); @@ -127,7 +127,7 @@ private void tcp(net.minecraft.client.Minecraft mc) { wait=0;return; } System.out.println("NATIVE_FORGE_TCP_JOIN "+(++connections)); - mc.level.disconnect();mc.clearLevel();mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen());stage=0;wait=0; + mc.level.disconnect();mc.disconnect();mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen());stage=0;wait=0; }else if(stage==1 && mc.screen instanceof net.minecraft.client.gui.screens.DisconnectedScreen) { throw new AssertionError("NATIVE_FORGE_TCP_LOGIN_FAILURE joins="+connections); } @@ -141,7 +141,7 @@ private void createWorld(net.minecraft.client.Minecraft client) { net.minecraft.world.Difficulty.NORMAL,false,new net.minecraft.world.level.GameRules(),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), new net.minecraft.world.level.levelgen.WorldOptions(4267321L,false,false), access->access.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) - .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions()); + .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions(),new net.minecraft.client.gui.screens.TitleScreen()); } } } diff --git a/src/controlTest/java/buildcontrol/ResourceControl.java b/src/controlTest/java/buildcontrol/ResourceControl.java index d6f5c68..7d18b9a 100644 --- a/src/controlTest/java/buildcontrol/ResourceControl.java +++ b/src/controlTest/java/buildcontrol/ResourceControl.java @@ -28,11 +28,11 @@ static void run(int rounds) { throw new AssertionError("Resource control has already constructed a server"); Executor prepare=Util.backgroundExecutor(); int cpus=Runtime.getRuntime().availableProcessors(); - if(!(prepare instanceof ForkJoinPool))throw new AssertionError("Native45 preparation executor changed"); + if(!(prepare instanceof ForkJoinPool))throw new AssertionError("Forge 50 preparation executor changed"); int workers=((ForkJoinPool)prepare).getParallelism(),limit=Integer.getInteger("max.bg.threads",255); - if(limit<1 || limit>255 || workers!=Math.max(1,Math.min(cpus-1,limit)))throw new AssertionError("Native45 worker configuration differs from its actual Util executor"); - System.out.println("NATIVE45_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); - PackRepository packs=new PackRepository(new ServerPacksSource()); + if(limit<1 || limit>255 || workers!=Math.max(1,Math.min(cpus-1,limit)))throw new AssertionError("Forge 50 worker configuration differs from its actual Util executor"); + System.out.println("NATIVE50_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); + PackRepository packs=new PackRepository(new ServerPacksSource(new net.minecraft.world.level.validation.DirectoryValidator(path->false))); WorldLoader.InitConfig config=new WorldLoader.InitConfig( new WorldLoader.PackConfig(packs,WorldDataConfiguration.DEFAULT,false,false),Commands.CommandSelection.DEDICATED,2); { @@ -46,13 +46,13 @@ static void run(int rounds) { .get(12,TimeUnit.SECONDS)) { if(resources.reloaded().getRecipeManager().getRecipes().isEmpty())throw new AssertionError("No native recipes loaded"); } - if(round%10==0)System.out.println("NATIVE45_RESOURCE_RELOAD_COMPLETE="+round); + if(round%10==0)System.out.println("NATIVE50_RESOURCE_RELOAD_COMPLETE="+round); } } if(net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer()!=null) throw new AssertionError("Resource control unexpectedly constructed a server"); Files.writeString(Path.of("resource-control.pass"),"rounds="+rounds+" cpus="+cpus+" preparationWorkers="+workers+" noWorld=true noLootableBodies=true\n",StandardOpenOption.CREATE_NEW); - System.out.println("NATIVE45_RESOURCE_CONTROL_PASS rounds="+rounds); + System.out.println("NATIVE50_RESOURCE_CONTROL_PASS rounds="+rounds); System.out.flush();System.exit(0); } catch(Throwable failure) { failure.printStackTrace();System.out.flush();System.exit(2); diff --git a/src/controlTest/resources/META-INF/mods.toml b/src/controlTest/resources/META-INF/mods.toml index 3d5ee9c..1a97ad2 100644 --- a/src/controlTest/resources/META-INF/mods.toml +++ b/src/controlTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[47,)" +loaderVersion="[50,)" license="LGPL-2.1-only" [[mods]] modId="forgeplatformcontrol" diff --git a/src/controlTest/resources/pack.mcmeta b/src/controlTest/resources/pack.mcmeta index a2d742e..70fb716 100644 --- a/src/controlTest/resources/pack.mcmeta +++ b/src/controlTest/resources/pack.mcmeta @@ -1,6 +1,7 @@ { "pack": { - "pack_format": 15, + "pack_format": 32, + "supported_formats": {"min_inclusive": 32, "max_inclusive": 41}, "description": "Disposable Forge control" } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java index f7bb1e1..d63f33a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java @@ -78,7 +78,7 @@ public void register() { // RuntimeChecks.run in the original continuous process. cyano.lootable.LootableBodies.allowCorpseDecay=false; cyano.lootable.LootableBodies.ticksPerItemDecay=-1; - stage=40; mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); + stage=40; mc.createWorldOpenFlows().openWorld("build-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen())); } else { stage=1; FixtureApi.ClientWorlds.create(mc,"build-fixture","Build fixture",net.minecraft.world.level.GameType.CREATIVE,false); @@ -105,7 +105,7 @@ public void register() { mc.player.closeContainer(); stage=4; FixtureConnections.disconnect(mc); } else if(stage==4 && FixtureConnections.stopped(mc)) { - stage=5; mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); + stage=5; mc.createWorldOpenFlows().openWorld("build-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen())); } else if(stage==5 && mc.player!=null && mc.level!=null && mc.getSingleplayerServer()!=null) { stage=6; pending=mc.getSingleplayerServer().submitAsync(() -> { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java index b972cac..fa92fee 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java @@ -96,16 +96,34 @@ private void openMods(Minecraft mc)throws Exception { private static void text(CorpseConfigGui screen,String key,String value)throws Exception {field(screen,key).setValue(value);} private void check(boolean condition,String label)throws Exception {RuntimeChecks.require(condition,label);try(FileWriter out=new FileWriter(report,true)){out.write("PASS "+label+"\n");}System.out.println("CONFIG_CHECK_PASS "+label);} @SuppressWarnings("unchecked") private static void screenshot(Minecraft mc,String name)throws Exception { + // Capture the completed native frame before the tooltip exercise + // redraws the same render target several times in a single tick. + Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString())); if(mc.screen instanceof CorpseConfigGui) { Screen screen=mc.screen;Map rows=(Map)get(screen,"rows"); Collection positions=rows.isEmpty()?java.util.Collections.singletonList(100):rows.values(); var graphics=new net.minecraft.client.gui.GuiGraphics(mc,mc.renderBuffers().bufferSource()); - for(int y:positions)for(int x:new int[]{screen.width/2-150,screen.width/2,screen.width/2+150}) { - screen.render(graphics,x,y+5,0); - graphics.flush(); - } + BackgroundObserver observer=new BackgroundObserver(screen); + var bus=net.minecraftforge.common.MinecraftForge.EVENT_BUS; + bus.register(observer); + try { + for(int y:positions)for(int x:new int[]{screen.width/2-150,screen.width/2,screen.width/2+150}) { + int before=observer.count; + screen.render(graphics,x,y+5,0); + graphics.flush(); + RuntimeChecks.require(observer.count==before+1,"Config background rendered twice, blurring its text"); + } + } finally {bus.unregister(observer);} + } + } + public static final class BackgroundObserver { + private final Screen screen; + private int count; + BackgroundObserver(Screen screen){this.screen=screen;} + @net.minecraftforge.eventbus.api.SubscribeEvent + public void rendered(net.minecraftforge.client.event.ScreenEvent.BackgroundRendered event) { + if(event.getScreen()==screen)count++; } - Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString())); } private static void finish(Minecraft mc)throws Exception {new File(mc.gameDirectory,"client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_CONFIG_CLIENT_PASS");mc.stop();} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java index 7be92c2..8640d4f 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java @@ -75,7 +75,7 @@ boolean tick(Minecraft mc) throws Exception { screenshot(mc,"connected-corpse-after-shift-recovery.png"); quit(mc); next(7); } else if(stage==7 && FixtureConnections.stopped(mc)) { - mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); next(8); + mc.createWorldOpenFlows().openWorld("build-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen())); next(8); } else if(stage==8 && ready(mc) && frames>30) { schedule(mc,() -> { verifyReload(mc,"connected-after-recovery","connected-player-after-recovery"); @@ -106,7 +106,7 @@ boolean tick(Minecraft mc) throws Exception { screenshot(mc,"connected-corpse-after-normal-recovery.png"); quit(mc); next(13); } else if(stage==13 && FixtureConnections.stopped(mc)) { - mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); next(14); + mc.createWorldOpenFlows().openWorld("build-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen())); next(14); } else if(stage==14 && ready(mc) && frames>30) { schedule(mc,() -> { verifyReload(mc,"connected-final","connected-player-final"); @@ -173,14 +173,14 @@ private void savePlayer(Minecraft mc,String name) throws Exception { private void verifyGui(Minecraft mc) { RuntimeChecks.require(gui(mc) && mc.player.containerMenu.slots.size()==84,"Connected GUI missing slots"); ItemStack[] expected=new ItemStack[48];ListTag list=visibleExpected.getList("Equipment",10); - for(int i=0;i {try{action.run();}catch(Exception error){throw new RuntimeException(error);}});} private boolean done() throws Exception {if(!pending.isDone())return false;pending.get();return true;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java index 5833d28..0625cf9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java @@ -151,7 +151,7 @@ private static void throwing(boolean creative,int source,int button,int rejectio if(rejection!=0)RuntimeChecks.require(connection.resyncs>initialSyncs,"Rejected predicted drop did not resync initiating client"); if(capture)RuntimeChecks.require(p.captureDrops().size()==2,"Toss discarded pre-existing captured item"); if(rejection==0 && !capture){ - int dropped=0;for(Entity e:FixtureApi.entities(world))if(e instanceof ItemEntity && !e.isRemoved()){equal(((ItemEntity)e).getItem(),item(120,amount),"Accepted drop NBT differs");dropped++;} + int dropped=0;for(Entity e:FixtureApi.entities(world,6,6))if(e instanceof ItemEntity && !e.isRemoved()){equal(((ItemEntity)e).getItem(),item(120,amount),"Accepted drop NBT differs");dropped++;} RuntimeChecks.require(dropped==1,"Accepted toss did not create exactly one entity"); } } @@ -199,26 +199,26 @@ private static void creativeDrag(boolean creative) throws Exception { } private static void creativePacket(boolean creative,boolean cancelled) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setItem(6,item(120,17));CompoundTag unchanged=data(b); - RejectedDrops reject=new RejectedDrops();reject.toss=cancelled;MinecraftForge.EVENT_BUS.register(reject);FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + RejectedDrops reject=new RejectedDrops();reject.toss=cancelled;MinecraftForge.EVENT_BUS.register(reject);var wire=new net.minecraft.network.RegistryFriendlyByteBuf(Unpooled.buffer(),p.registryAccess()); try { - new ServerboundSetCreativeModeSlotPacket(cancelled?-1:9,item(121,23)).write(wire); - p.connection.handleSetCreativeModeSlot(new ServerboundSetCreativeModeSlotPacket(wire)); + ServerboundSetCreativeModeSlotPacket.STREAM_CODEC.encode(wire,new ServerboundSetCreativeModeSlotPacket(cancelled?-1:9,item(121,23))); + p.connection.handleSetCreativeModeSlot(ServerboundSetCreativeModeSlotPacket.STREAM_CODEC.decode(wire)); RuntimeChecks.require(!wire.isReadable(),"Native creative packet left undecoded fields"); } finally{wire.release();MinecraftForge.EVENT_BUS.unregister(reject);} RuntimeChecks.require(unchanged.equals(data(b)),"Creative inventory packet changed corpse instead of player");equal(p.getInventory().items.get(9),creative&&!cancelled?item(121,23):null,"Creative player packet permission/target differs"); - if(cancelled)RuntimeChecks.require(FixtureApi.entities(world).stream().noneMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cancelled creative minted drop spawned anyway"); + if(cancelled)RuntimeChecks.require(FixtureApi.entities(world,6,6).stream().noneMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cancelled creative minted drop spawned anyway"); } private static void check(String name,Check test) { - Set before=new HashSet<>(FixtureApi.entities(world)); + Set before=new HashSet<>(FixtureApi.entities(world,6,6)); try {test.run();results.add("PASS "+name);System.out.println("CONTAINER_CHECK_PASS "+name);} catch(Throwable error){failures++;results.add("FAIL "+name+": "+error);System.out.println("CONTAINER_CHECK_FAIL "+name+": "+error);error.printStackTrace();} - finally{for(Entity entity:new ArrayList(FixtureApi.entities(world)))if(!before.contains(entity))entity.discard();} + finally{for(Entity entity:new ArrayList(FixtureApi.entities(world,6,6)))if(!before.contains(entity))entity.discard();} } private static ServerPlayer player() { ServerPlayer p=new ServerPlayer(server,world, - new GameProfile(UUID.nameUUIDFromBytes(("container-player-"+sequence++).getBytes(java.nio.charset.StandardCharsets.UTF_8)),"ContainerFixture")); + new GameProfile(UUID.nameUUIDFromBytes(("container-player-"+sequence++).getBytes(java.nio.charset.StandardCharsets.UTF_8)),"ContainerFixture"),net.minecraft.server.level.ClientInformation.createDefault()); p.connection=new RecordingConnection(p);p.setGameMode(GameType.SURVIVAL); p.setPos(96.5,80,96.5);world.getChunk(6,6);return p; } @@ -230,7 +230,7 @@ private static CorpseContainer open(ServerPlayer p,EntityLootableBody b){ CorpseContainer c=new CorpseContainer(7,p.getInventory(),b);p.containerMenu=c;p.initMenu(c);return c; } private static ItemStack item(int id,int count){ItemStack s=RuntimeChecks.stack(id);s.setCount(count);return s;} - private static ItemStack pumpkins(int count){ItemStack s=new ItemStack(Blocks.CARVED_PUMPKIN,count);s.setTag(item(120,1).getTag().copy());return s;} + private static ItemStack pumpkins(int count){ItemStack s=new ItemStack(Blocks.CARVED_PUMPKIN,count);FixtureStackData.setCustom(s,FixtureStackData.custom(item(120,1)));return s;} private static void fill(ServerPlayer p){for(int i=0;i<36;i++)p.getInventory().items.set(i,item(100+i,64));} private static void fill(EntityLootableBody b){for(int i=0;i<48;i++)b.setItem(i,item(i,i<6?1:64));b.addItem(item(70,31));} private static void packet(ServerPlayer p,int slot,int button,ClickType type,ItemStack predicted) throws Exception { @@ -243,9 +243,9 @@ private static void packetWithState(ServerPlayer p,int slot,int button,ClickType // native broadcasts must repair them. Real clients cover normal prediction. ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket( p.containerMenu.containerId,stateId,slot,button,type,p.containerMenu.getCarried().copy(),changed); - FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + var wire=new net.minecraft.network.RegistryFriendlyByteBuf(Unpooled.buffer(),p.registryAccess()); try { - outgoing.write(wire);ServerboundContainerClickPacket decoded=new ServerboundContainerClickPacket(wire); + ServerboundContainerClickPacket.STREAM_CODEC.encode(wire,outgoing);ServerboundContainerClickPacket decoded=ServerboundContainerClickPacket.STREAM_CODEC.decode(wire); RuntimeChecks.require(!wire.isReadable(),"Native menu packet left undecoded fields"); p.connection.handleContainerClick(decoded); } finally {wire.release();} @@ -260,8 +260,8 @@ private static void click(ServerPlayer p,int slot,int button,ClickType type) thr private static void close(ServerPlayer p) throws Exception { FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); try { - new ServerboundContainerClosePacket(p.containerMenu.containerId).write(wire); - p.connection.handleContainerClose(new ServerboundContainerClosePacket(wire)); + ServerboundContainerClosePacket.STREAM_CODEC.encode(wire,new ServerboundContainerClosePacket(p.containerMenu.containerId)); + p.connection.handleContainerClose(ServerboundContainerClosePacket.STREAM_CODEC.decode(wire)); RuntimeChecks.require(!wire.isReadable(),"Native close packet left undecoded fields"); } finally {wire.release();} } @@ -273,19 +273,25 @@ private static void drag(ServerPlayer p,int mode,int... slots) throws Exception private static void equal(ItemStack actual,ItemStack expected,String reason){RuntimeChecks.require(RuntimeChecks.sameStack(actual == null ? ItemStack.EMPTY : actual,expected == null ? ItemStack.EMPTY : expected),reason);} private static CompoundTag data(EntityLootableBody b){CompoundTag n=new CompoundTag();b.addAdditionalSaveData(n);return n;} private static void add(Map all,ItemStack s){ - if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=s.save(new CompoundTag());n.remove("Count"); + if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=FixtureStackData.save(s);n.remove("count"); String key=n.toString();all.put(key,all.getOrDefault(key,0)+s.getCount()); } private static Map total(EntityLootableBody b,ServerPlayer... players){ Map all=new TreeMap<>(); for(int i=0;i<48;i++)add(all,b.getItem(i));ListTag aux=data(b).getList("Aux",10); - for(int i=0;iemptyList():p.captureDrops())add(all,e.getItem());} - for(Entity e:FixtureApi.entities(world))if(e instanceof ItemEntity && !e.isRemoved())add(all,((ItemEntity)e).getItem()); + for(Entity e:FixtureApi.entities(world,6,6))if(e instanceof ItemEntity && !e.isRemoved())add(all,((ItemEntity)e).getItem()); return all; } private static void conserved(Map expected,EntityLootableBody b,ServerPlayer... players){ - RuntimeChecks.require(expected.equals(total(b,players)),"Item count/type/damage/custom NBT lost or duplicated"); + Map actual=total(b,players); + if(!expected.equals(actual)) { + Map delta=new TreeMap<>(actual); + expected.forEach((item,count)->delta.merge(item,-count,Integer::sum)); + delta.values().removeIf(count->count==0); + throw new AssertionError("Item count/type/damage/custom NBT lost or duplicated: "+delta); + } CompoundTag before=data(b),immutable=before.copy();EntityLootableBody copy=new EntityLootableBody(world);copy.readAdditionalSaveData(before); RuntimeChecks.require(Objects.equals(before.get("Equipment"),data(copy).get("Equipment")) && Objects.equals(before.get("Aux"),data(copy).get("Aux")),"Post-interaction NBT round trip changed inventory"); RuntimeChecks.require(before.equals(immutable),"Post-interaction reader mutated saved input"); @@ -358,14 +364,14 @@ private static void displacement(boolean rejectJoin,boolean rejectToss) throws E RejectedDrops reject=new RejectedDrops();reject.join=rejectJoin;reject.toss=rejectToss;MinecraftForge.EVENT_BUS.register(reject); try{click(p,0,0,ClickType.SWAP);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} equal(b.getItem(0),pumpkins(1),"Displacement changed normal incoming swap");equal(p.getInventory().items.get(0),pumpkins(63),"Displacement changed hotbar remainder");conserved(before,b,p); - if(!rejectJoin && !rejectToss)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Successful displacement no longer drops normally"); + if(!rejectJoin && !rejectToss)RuntimeChecks.require(FixtureApi.entities(world,6,6).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Successful displacement no longer drops normally"); } private static void closeCursor(boolean rejected,boolean full) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();if(full){fill(b);fill(p);}open(p,b);p.containerMenu.setCarried(item(120,17));Map before=total(b,p); RejectedDrops reject=new RejectedDrops();reject.join=rejected;MinecraftForge.EVENT_BUS.register(reject); try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} equal(p.containerMenu.getCarried(),null,"Closing left a nonsaved cursor stack");conserved(before,b,p); - if(!rejected)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cursor close no longer performs normal accepted drop"); + if(!rejected)RuntimeChecks.require(FixtureApi.entities(world,6,6).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cursor close no longer performs normal accepted drop"); } private static void capturedCursor() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(120,17)); @@ -425,7 +431,7 @@ private static void staleDrag() throws Exception { private static final class RecordingConnection extends ServerGamePacketListenerImpl { final Map slots=new HashMap<>();int resyncs,lastContentStateId=-1; ItemStack carried=ItemStack.EMPTY; - RecordingConnection(ServerPlayer p){super(server,new Connection(PacketFlow.SERVERBOUND),p);} + RecordingConnection(ServerPlayer p){super(ContainerInteractionChecks.server,new Connection(PacketFlow.SERVERBOUND),p,net.minecraft.server.network.CommonListenerCookie.createInitial(p.getGameProfile(),false));} @Override public void send(Packet packet){ NativeMenuObservation observed=NativeMenuObservation.decode(packet);if(observed==null)return; if(observed.content) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java index 1067fa7..5821363 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java @@ -80,20 +80,20 @@ else if(stage==16 && frames>20){ else if(stage==19 && frames>20){ equal(mc.player.containerMenu.getCarried(),null,"Closed creative client cursor"); schedule(mc,()->{equal(player(mc).containerMenu.getCarried(),null,"Closed creative server cursor");CompoundTag data=new CompoundTag();body(mc).addAdditionalSaveData(data);ListTag aux=data.getList("Aux",10);int count=0; - for(int i=0;imc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(22);} else if(stage==22 && ready(mc) && frames>20){schedule(mc,()->{ RuntimeChecks.awaitChunk(player(mc).serverLevel(),2,0);body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"drop-creative-final",body(mc)); RuntimeChecks.require(savedPlayer.equals(player(mc).getInventory().save(new ListTag())),"Creative player inventory changed on disk reload");player(mc).connection.teleport(40.5,80,7.5,0,0); mark(mc,"creative_cancelled_close_full_inventory_disk_reload");});next(23); } else if(stage==23 && done()){if(open(mc))next(24);} else if(stage==24 && gui(mc) && frames>20){ - ItemStack[] expected=new ItemStack[48];ListTag list=savedBody.getList("Equipment",10);for(int i=0;i results=new ArrayList<>(); private static MobEffect extended; static void registerPotion(){ - RuntimeChecks.require(MobEffect.byId(200)==null,"Fixture potion ID already occupied"); + RuntimeChecks.require(net.minecraft.core.registries.BuiltInRegistries.MOB_EFFECT.byId(200)==null,"Fixture potion ID already occupied"); extended=new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0x123456){}; // Forge 1.12 assigns IDs through registry events. Fill the isolated test // registry through its public API so unsigned-byte coverage still uses 200. - for(int i=0;MobEffect.byId(199)==null && i<200;i++) + for(int i=0;net.minecraft.core.registries.BuiltInRegistries.MOB_EFFECT.byId(199)==null && i<200;i++) net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(new net.minecraft.resources.ResourceLocation("lootablebodiesbuildtest:padding_"+i),new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}); net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(new net.minecraft.resources.ResourceLocation("lootablebodiesbuildtest:extended_effect"),extended); - RuntimeChecks.require(MobEffect.getId(extended)==200,"Fixture potion must exercise unsigned byte ID 200"); + RuntimeChecks.require(net.minecraft.core.registries.BuiltInRegistries.MOB_EFFECT.getId(extended)==200,"Fixture potion must exercise unsigned byte ID 200"); // Older probes saved a different number of padding effects. Retain their // registry names during upgrades without changing fresh-world ID tests. if(System.getProperty("lootablebodies.phase","").startsWith("upgrade-")) @@ -88,7 +89,7 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex check("zero_duration_expires_normally",()->validDuration(0)); check("maximum_duration_does_not_overflow",()->validDuration(Integer.MAX_VALUE)); check("signed_byte_amplifier_encodings_keep_native_111_clamping",EffectHurtChecks::signedAmplifiers); - check("forge_curative_items_keep_complete_item_data",EffectHurtChecks::curativeItems); + check("legacy_curative_items_remain_in_recovery_data",EffectHurtChecks::curativeItems); check("malformed_curative_items_retained_with_safe_defaults",EffectHurtChecks::badCurativeItems); check("registered_unsigned_byte_effect_id_survives_reload",EffectHurtChecks::extendedId); check("duplicate_id_numeric_alias_keeps_first",EffectHurtChecks::alias); @@ -102,7 +103,7 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex check("invalid_effect_and_hurt_fields_keep_complete_unresolved_inventory",EffectHurtChecks::inventory); check("native_icon_flag_survives_without_quarantine",()->{CompoundTag e=effect();e.putBoolean("ShowIcon",false);EntityLootableBody b=read(with(e));require(!b.getEffect(MobEffects.NIGHT_VISION).showIcon(),"Native icon visibility changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Native icon visibility quarantined");stable(b);}); check("missing_icon_flag_follows_particle_default",()->{CompoundTag e=effect();e.remove("ShowIcon");e.putBoolean("ShowParticles",false);EntityLootableBody b=read(with(e));require(!b.getEffect(MobEffects.NIGHT_VISION).showIcon(),"Missing native icon default changed");stable(b);}); - check("native_named_identity_survives_numeric_registry_remap",()->{CompoundTag e=effect();e.putByte("Id",(byte)MobEffect.getId(MobEffects.MOVEMENT_SPEED));EntityLootableBody b=read(with(e));require(b.getEffect(MobEffects.NIGHT_VISION)!=null && b.getEffect(MobEffects.MOVEMENT_SPEED)==null,"Native named identity did not take precedence");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy registry remap quarantined");stable(b);}); + check("native_named_identity_survives_numeric_registry_remap",()->{CompoundTag e=effect();e.putByte("Id",(byte)1);EntityLootableBody b=read(with(e));require(b.getEffect(MobEffects.NIGHT_VISION)!=null && b.getEffect(MobEffects.MOVEMENT_SPEED)==null,"Native named identity did not take precedence");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy registry remap quarantined");stable(b);}); check("native_named_identity_recovers_missing_legacy_number_with_raw_evidence",()->{CompoundTag e=effect();e.remove("Id");EntityLootableBody b=read(with(e));require(b.getEffect(MobEffects.NIGHT_VISION)!=null,"Known native name was lost");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);}); check("unknown_or_malformed_named_identity_cannot_activate_unrelated_numeric_fallback",()->{for(Tag value:new Tag[]{IntTag.valueOf(16),StringTag.valueOf("bad name?!"),StringTag.valueOf("missing:unknown_effect")}){CompoundTag e=effect();e.put("forge:id",value);EntityLootableBody b=read(with(e));require(b.getActiveEffects().isEmpty(),"Unreadable native identity activated a different effect");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);}}); check("metadata_only_does_not_prevent_empty_decay",EffectHurtChecks::empty); @@ -114,10 +115,25 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex private static CompoundTag saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} private static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setItem(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(b);return saved(b);} private static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.load(n);require(NameLifetimeChecks.same(before,n),"Input mutated");return b;} - static CompoundTag effect(MobEffect p,int duration,int amplifier){return new MobEffectInstance(p,duration,amplifier,false,true).save(new CompoundTag());} + static CompoundTag effect(Holder effect,int duration,int amplifier) { + // Explicit legacy shape for malformed-old-input tests. Current native + // save/reload is exercised independently by attributes() below. + CompoundTag old=new CompoundTag(); + var registry=net.minecraft.core.registries.BuiltInRegistries.MOB_EFFECT; + old.putByte("Id",(byte)registry.getId(effect.value())); + old.putString("forge:id",registry.getKey(effect.value()).toString()); + old.putInt("Duration",duration); + old.putByte("Amplifier",(byte)amplifier); + old.putBoolean("ShowParticles",true); + old.putBoolean("ShowIcon",true); + return old; + } + private static Holder extendedHolder() { + return net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.getHolder(extended).orElseThrow(); + } private static CompoundTag effect(){return effect(MobEffects.NIGHT_VISION,200,1);} private static CompoundTag with(CompoundTag e){CompoundTag n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(e));return n;} - private static void stable(EntityLootableBody b){CompoundTag a=saved(b),z=saved(read(a));for(String k:new String[]{"Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY,"ActiveEffects","HurtTime","Health","AbsorptionAmount","Attributes"})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);require(RuntimeChecks.sameStack(b.getItem(6),NbtRecoveryChecks.item(120,17)),"Complete known inventory changed");} + private static void stable(EntityLootableBody b){CompoundTag a=saved(b),z=saved(read(a));for(String k:new String[]{"Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY,"active_effects","HurtTime","Health","AbsorptionAmount","Attributes"})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);require(RuntimeChecks.sameStack(b.getItem(6),NbtRecoveryChecks.item(120,17)),"Complete known inventory changed");} private static void badHurt(Tag raw){CompoundTag n=base();n.put("HurtTime",raw.copy());EntityLootableBody b=read(n);require(b.hurtTime==0 && b.isAlive(),"Bad hurt time activated");NameLifetimeChecks.retained(b,"HurtTime",raw);stable(b);} private static void validHurt(Tag raw){CompoundTag n=base();n.put("HurtTime",raw);EntityLootableBody b=read(n);int v=((NumericTag)raw).getAsInt();require(b.hurtTime==v,"Valid countdown narrowed");stable(b);b.baseTick();require(b.hurtTime==v-1,"Countdown no longer ticks");} private static void badList(Tag raw){CompoundTag n=base();n.put("ActiveEffects",raw.copy());EntityLootableBody b=read(n);require(b.getActiveEffects().isEmpty(),"Bad effect list activated");NameLifetimeChecks.retained(b,"ActiveEffects",raw);stable(b);} @@ -127,10 +143,25 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex private static void missingOptional(){CompoundTag e=effect();for(String k:new String[]{"Duration","Amplifier","Ambient","ShowParticles"})e.remove(k);EntityLootableBody b=read(with(e));MobEffectInstance p=b.getEffect(MobEffects.NIGHT_VISION);require(p!=null && p.getDuration()==0 && p.getAmplifier()==0 && !p.isAmbient() && p.isVisible(),"Legacy defaults changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Missing optional fields quarantined");stable(b);} private static void validDuration(int d){EntityLootableBody b=read(with(effect(MobEffects.NIGHT_VISION,d,0)));b.baseTick();MobEffectInstance p=b.getEffect(MobEffects.NIGHT_VISION);require(d==0?p==null:p!=null && p.getDuration()==d-1,"Valid duration tick/expiry changed");stable(b);} private static void signedAmplifiers(){for(int a:new int[]{-128,-1,0,127}){EntityLootableBody b=read(with(effect(MobEffects.NIGHT_VISION,200,a)));require(b.getEffect(MobEffects.NIGHT_VISION).getAmplifier()==Math.max(0,a),"Native 1.11 byte amplifier clamping changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Canonical signed byte quarantined");stable(b);}} - private static void curativeItems(){CompoundTag e=effect();CompoundTag item=NbtRecoveryChecks.item(120,7).save(new CompoundTag());ListTag list=new ListTag();list.add(item);e.put("CurativeItems",list);EntityLootableBody b=read(with(e));require(b.getEffect(MobEffects.NIGHT_VISION).getCurativeItems().size()==1 && RuntimeChecks.sameStack(b.getEffect(MobEffects.NIGHT_VISION).getCurativeItems().get(0),ItemStack.of(item)),"Forge curative item NBT changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy Forge curative items quarantined");stable(b);} - private static void badCurativeItems(){CompoundTag e=effect();e.putString("CurativeItems","opaque");EntityLootableBody b=read(with(e));NameLifetimeChecks.retained(b,"ActiveEffect",e);require(!b.getEffect(MobEffects.NIGHT_VISION).getCurativeItems().isEmpty(),"Malformed curative list did not use native defaults");stable(b);} - private static void extendedId(){for(boolean wide:new boolean[]{false,true}){CompoundTag e=effect(extended,200,1);if(wide)e.putInt("Id",200);EntityLootableBody b=read(with(e));require(b.getEffect(extended)!=null && b.getEffect(extended).getDuration()==200,"Registered unsigned ID changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid extended ID quarantined");stable(b);}} - private static void alias(){CompoundTag first=effect(extended,200,1),second=effect(extended,900,3),n=base();second.putLong("Id",200);n.put("ActiveEffects",NbtRecoveryChecks.list(first,second));EntityLootableBody b=read(n);require(b.getEffect(extended).getDuration()==200,"Numeric alias bypassed first valid effect");NameLifetimeChecks.retained(b,"ActiveEffect",second);stable(b);} + private static void curativeItems() { + CompoundTag legacy=effect(); + CompoundTag item=FixtureStackData.save(NbtRecoveryChecks.item(120,7)); + legacy.put("CurativeItems",NbtRecoveryChecks.list(item)); + EntityLootableBody body=read(with(legacy)); + require(body.getEffect(MobEffects.NIGHT_VISION)!=null,"Legacy effect itself was lost"); + NameLifetimeChecks.retained(body,"ActiveEffect",legacy); + stable(body); + } + private static void badCurativeItems() { + CompoundTag legacy=effect(); + legacy.putString("CurativeItems","opaque"); + EntityLootableBody body=read(with(legacy)); + require(body.getEffect(MobEffects.NIGHT_VISION)!=null,"Opaque curative data suppressed the known effect"); + NameLifetimeChecks.retained(body,"ActiveEffect",legacy); + stable(body); + } + private static void extendedId(){for(boolean wide:new boolean[]{false,true}){CompoundTag e=effect(extendedHolder(),200,1);if(wide)e.putInt("Id",200);EntityLootableBody b=read(with(e));require(b.getEffect(extendedHolder())!=null && b.getEffect(extendedHolder()).getDuration()==200,"Registered unsigned ID changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid extended ID quarantined");stable(b);}} + private static void alias(){CompoundTag first=effect(extendedHolder(),200,1),second=effect(extendedHolder(),900,3),n=base();second.putLong("Id",200);n.put("ActiveEffects",NbtRecoveryChecks.list(first,second));EntityLootableBody b=read(n);require(b.getEffect(extendedHolder()).getDuration()==200,"Numeric alias bypassed first valid effect");NameLifetimeChecks.retained(b,"ActiveEffect",second);stable(b);} private static void validTail(){CompoundTag bad=effect();bad.putString("Duration","opaque");CompoundTag n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(bad,effect()));EntityLootableBody b=read(n);require(b.getEffect(MobEffects.NIGHT_VISION).getDuration()==200,"Bad first effect hid valid tail");NameLifetimeChecks.retained(b,"ActiveEffect",bad);stable(b);} private static void duplicate(){CompoundTag first=effect(),second=effect(MobEffects.NIGHT_VISION,900,3),n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(first,second));EntityLootableBody b=read(n);require(b.getEffect(MobEffects.NIGHT_VISION).getDuration()==200,"Duplicate overwrote first effect");NameLifetimeChecks.retained(b,"ActiveEffect",second);stable(b);} private static void unknown(){CompoundTag e=effect();e.putString("Opaque","retain effect extension");EntityLootableBody b=read(with(e));require(b.getActiveEffects().size()==1,"Unknown sibling lost understood effect");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java index 87898ef..7f00d3b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java @@ -43,7 +43,7 @@ static void create(net.minecraft.client.Minecraft client,String folder,String ti new net.minecraft.world.level.GameRules(),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), new net.minecraft.world.level.levelgen.WorldOptions(4267321L,false,false), access->access.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) - .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions()); + .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions(),new net.minecraft.client.gui.screens.TitleScreen()); } } static List entities(Level world) { @@ -58,12 +58,12 @@ static Entity loadEntity(CompoundTag data, Level world) { static Entity readChunkEntity(CompoundTag data, ServerLevel world, net.minecraft.world.level.chunk.LevelChunk chunk) { // Invoke the actual native chunk serializer; do not replace its admission callback. CompoundTag root=new CompoundTag();net.minecraft.nbt.ListTag list=new net.minecraft.nbt.ListTag();list.add(data.copy());root.put("entities",list); - root.putInt("xPos",chunk.getPos().x);root.putInt("zPos",chunk.getPos().z);root.put("sections",new net.minecraft.nbt.ListTag());root.putInt("DataVersion",3465); + root.putInt("xPos",chunk.getPos().x);root.putInt("zPos",chunk.getPos().z);root.put("sections",new net.minecraft.nbt.ListTag());root.putInt("DataVersion",3839); // Current fixture NBT follows the real native conversion boundary too. // The post-load admission callback remains Minecraft's own method. CompoundTag fixed=cyano.lootable.upgrade.NativeNbt.update( cyano.lootable.upgrade.UpgradePreservation.wrapper(net.minecraft.util.datafix.DataFixers.getDataFixer()), - net.minecraft.util.datafix.fixes.References.CHUNK,root,3465,3465); + net.minecraft.util.datafix.fixes.References.CHUNK,root,3839,3839); try { java.lang.reflect.Method nativeRead=null; for(java.lang.reflect.Method method:net.minecraft.world.level.chunk.storage.ChunkSerializer.class.getDeclaredMethods()) @@ -75,14 +75,26 @@ static Entity readChunkEntity(CompoundTag data, ServerLevel world, net.minecraft net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor processor=(net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor)nativeRead.invoke(null,world,fixed); if(processor==null)throw new IllegalStateException("Native entity admission returned no processor"); processor.run(chunk); - Object manager=singleField(ServerLevel.class,net.minecraft.world.level.entity.PersistentEntitySectionManager.class).get(world); - net.minecraft.world.level.entity.EntitySectionStorage sections=(net.minecraft.world.level.entity.EntitySectionStorage) - singleField(manager.getClass(),net.minecraft.world.level.entity.EntitySectionStorage.class).get(manager); java.util.UUID id=fixed.getList("entities",10).getCompound(0).getUUID("UUID"); - return sections.getExistingSectionsInChunk(chunk.getPos().toLong()).flatMap(section->section.getEntities()) - .filter(entity->entity instanceof Entity && ((Entity)entity).getUUID().equals(id)).map(entity->(Entity)entity).findFirst().orElse(null); + return sectionEntities(world,chunk.getPos().toLong()).filter(entity->entity.getUUID().equals(id)).findFirst().orElse(null); }catch(ReflectiveOperationException failure){throw new IllegalStateException("Native chunk admission failed",failure);} } + static List entities(ServerLevel world,int chunkX,int chunkZ) { + // Forge 50 may have admitted a drop to the native section before its + // chunk is tracked. Count both stores, once per actual entity. + var all=new java.util.LinkedHashSet<>(entities(world)); + sectionEntities(world,net.minecraft.world.level.ChunkPos.asLong(chunkX,chunkZ)).forEach(all::add); + return new ArrayList<>(all); + } + private static java.util.stream.Stream sectionEntities(ServerLevel world,long chunk) { + try { + Object manager=singleField(ServerLevel.class,net.minecraft.world.level.entity.PersistentEntitySectionManager.class).get(world); + var sections=(net.minecraft.world.level.entity.EntitySectionStorage) + singleField(manager.getClass(),net.minecraft.world.level.entity.EntitySectionStorage.class).get(manager); + return sections.getExistingSectionsInChunk(chunk).flatMap(section->section.getEntities()) + .filter(Entity.class::isInstance).map(Entity.class::cast); + }catch(IllegalAccessException failure){throw new IllegalStateException("Cannot read the native entity section",failure);} + } private static java.lang.reflect.Field singleField(Class owner,Class type) { java.lang.reflect.Field found=null; for(java.lang.reflect.Field field:owner.getDeclaredFields())if(field.getType()==type) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java index a06364c..c57bc0b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java @@ -17,7 +17,7 @@ static void disconnect(Minecraft mc) { if(mc.getSingleplayerServer()!=null)retiringServer=mc.getSingleplayerServer().getRunningThread(); if(mc.player!=null)mc.player.closeContainer(); if(mc.level!=null)mc.level.disconnect(); - mc.clearLevel();mc.setScreen(new TitleScreen()); + mc.disconnect();mc.setScreen(new TitleScreen()); } static boolean stopped(Minecraft mc) { return ServerStopGate.ready(mc.level==null,mc.hasSingleplayerServer(),retiringServer); @@ -27,8 +27,8 @@ static boolean ready(String role) { } static void connect(Minecraft mc) { String address="127.0.0.1:"+Integer.parseInt(System.getProperty("lootablebodies.multiplayer.port")); - ServerData destination=new ServerData("Disposable check",address,false); - ConnectScreen.startConnecting(new TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(address),destination,false); + ServerData destination=new ServerData("Disposable check",address,ServerData.Type.OTHER); + ConnectScreen.startConnecting(new TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(address),destination,false,null); } static boolean retry(Minecraft mc,String role) throws Exception { if(!(mc.screen instanceof DisconnectedScreen)||mc.level!=null)return false; diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java index a136304..264f00a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java @@ -22,7 +22,7 @@ public void register(net.minecraftforge.registries.RegisterEvent event) { // Native18 creates an intrusive holder inside Item's constructor; // construction belongs to the same open registration event. keepsake=new Item(new Item.Properties()) { - @Override public ICapabilityProvider initCapabilities(ItemStack stack,CompoundTag nbt){return new Stored();} + @Override public ICapabilityProvider getCapabilityProvider(ItemStack stack){return new Stored();} }; event.register(net.minecraft.core.registries.Registries.ITEM,new ResourceLocation("lootablebodiesbuildtest:keepsake"),()->keepsake); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java new file mode 100644 index 0000000..037138b --- /dev/null +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java @@ -0,0 +1,31 @@ +package cyano.lootable.buildtest; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.mojang.authlib.GameProfile; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import net.minecraft.world.level.block.entity.SkullBlockEntity; + +/** An owned test cache at the actual native profile lookup seam. */ +final class FixtureProfileCache { + private FixtureProfileCache() { } + + static AutoCloseable install(Function>> lookup) { + try { + var field=SkullBlockEntity.class.getDeclaredField("profileCacheByName"); + field.setAccessible(true); + Object previous=field.get(null); + field.set(null,CacheBuilder.newBuilder().maximumSize(64).build( + new CacheLoader>>() { + @Override public CompletableFuture> load(String name) { + return lookup.apply(name); + } + })); + return ()->field.set(null,previous); + } catch(ReflectiveOperationException failure) { + throw new IllegalStateException("Cannot bind the native fixture profile cache",failure); + } + } +} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java new file mode 100644 index 0000000..c599cf5 --- /dev/null +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java @@ -0,0 +1,38 @@ +package cyano.lootable.buildtest; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; + +/** The native component codec used by the private runtime fixtures. */ +final class FixtureStackData { + private FixtureStackData() { } + + static RegistryAccess registries() { + return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + } + + static CompoundTag save(ItemStack stack) { + return (CompoundTag)stack.save(registries()); + } + + static ItemStack load(CompoundTag raw) { + var ops=registries().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE); + return ItemStack.CODEC.parse(ops,raw).result().orElse(ItemStack.EMPTY); + } + + static CompoundTag custom(ItemStack stack) { + return stack.getOrDefault(DataComponents.CUSTOM_DATA,CustomData.EMPTY).copyTag(); + } + + static boolean hasCustom(ItemStack stack) { + return stack.has(DataComponents.CUSTOM_DATA); + } + + static void setCustom(ItemStack stack,CompoundTag data) { + stack.set(DataComponents.CUSTOM_DATA,CustomData.of(data)); + } +} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java index 2fa35ca..c73ded4 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java @@ -157,12 +157,27 @@ private static void nativeUuid(boolean conflict,boolean malformed){ stable(body); } private static void legacyForgeAttributes(){ - String[][] names={{"forge.swimSpeed","forge:swim_speed"},{"forge.nameTagDistance","forge:nametag_distance"},{"forge.entity_gravity","forge:entity_gravity"}}; + String[][] names={{"forge.swimSpeed","forge:swim_speed"},{"forge.nameTagDistance","forge:nametag_distance"},{"forge.entity_gravity","minecraft:generic.gravity"}}; double[] bases={1.75D,70D,.12D};ListTag list=new ListTag();for(int i=0;i inventory(EntityLootableBody body) { Map result=new TreeMap<>(); for(int i=0;i result,ItemStack stack) { if((stack == null || stack.isEmpty()) || stack.getCount()<=0) return; - CompoundTag tag=encoded(stack); tag.remove("Count"); + CompoundTag tag=encoded(stack); tag.remove("count"); String key=tag.toString(); result.put(key,result.getOrDefault(key,0)+stack.getCount()); } private static Map dropped() { @@ -143,7 +143,7 @@ private static EntityLootableBody newest(Set before) { private static void failedSpawn() { InventoryTestPlayer player=player(); RejectingWorld rejected=new RejectingWorld(world); player.setFixtureLevel(rejected); - ItemStack bones=new ItemStack(Items.BONE,9); bones.setHoverName(net.minecraft.network.chat.Component.literal("Saved bones")); + ItemStack bones=new ItemStack(Items.BONE,9); bones.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,net.minecraft.network.chat.Component.literal("Saved bones")); List drops=drops(player,bones,item(19,23)); List before=new ArrayList<>(); for(ItemEntity e:drops) before.add(encoded(e.getItem())); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); @@ -215,7 +215,7 @@ public static final class RejectItems { @SubscribeEvent public void join(EntityJoinLevelEvent event) { if(event.getEntity() instanceof ItemEntity) { ItemStack stack=((ItemEntity)event.getEntity()).getItem(); - if(rejectAll || stack.hasTag() && rejectFixture.equals(stack.getTag().getString("fixture"))) event.setCanceled(true); + if(rejectAll || FixtureStackData.hasCustom(stack) && rejectFixture.equals(FixtureStackData.custom(stack).getString("fixture"))) event.setCanceled(true); } } } @@ -364,7 +364,7 @@ static void seedBlockedRelease(ServerLevel target,File folder) throws Exception /** Controlled false-return seam; no dimension registration, disk writes or chunks of its own. */ private static final class InventoryTestPlayer extends ServerPlayer { InventoryTestPlayer(MinecraftServer server, ServerLevel world, GameProfile profile) { - super(server,world,profile); + super(server,world,profile,net.minecraft.server.level.ClientInformation.createDefault()); } void setFixtureLevel(Level world) { @@ -386,6 +386,8 @@ private static final class RejectingWorld extends Level { @Override public net.minecraft.world.ticks.LevelTickAccess getBlockTicks(){return delegate.getBlockTicks();} @Override public net.minecraft.world.ticks.LevelTickAccess getFluidTicks(){return delegate.getFluidTicks();} + @Override public net.minecraft.world.TickRateManager tickRateManager(){return delegate.tickRateManager();} + @Override public net.minecraft.world.item.alchemy.PotionBrewing potionBrewing(){return delegate.potionBrewing();} @Override public net.minecraft.world.item.crafting.RecipeManager getRecipeManager(){return delegate.getRecipeManager();} @Override public net.minecraft.world.scores.Scoreboard getScoreboard(){return delegate.getScoreboard();} @Override public net.minecraft.server.MinecraftServer getServer(){return delegate.getServer();} @@ -397,13 +399,13 @@ private static final class RejectingWorld extends Level { @Override public void playSeededSound(net.minecraft.world.entity.player.Player player,double x,double y,double z,net.minecraft.core.Holder sound,net.minecraft.sounds.SoundSource category,float volume,float pitch,long seed){delegate.playSeededSound(player,x,y,z,sound,category,volume,pitch,seed);} @Override public void playSeededSound(net.minecraft.world.entity.player.Player player,Entity entity,net.minecraft.core.Holder sound,net.minecraft.sounds.SoundSource category,float volume,float pitch,long seed){delegate.playSeededSound(player,entity,sound,category,volume,pitch,seed);} @Override public Entity getEntity(int id){return delegate.getEntity(id);} - @Override public net.minecraft.world.level.saveddata.maps.MapItemSavedData getMapData(String id){return delegate.getMapData(id);} - @Override public void setMapData(String id,net.minecraft.world.level.saveddata.maps.MapItemSavedData data){delegate.setMapData(id,data);} - @Override public int getFreeMapId(){return delegate.getFreeMapId();} + @Override public net.minecraft.world.level.saveddata.maps.MapItemSavedData getMapData(net.minecraft.world.level.saveddata.maps.MapId id){return delegate.getMapData(id);} + @Override public void setMapData(net.minecraft.world.level.saveddata.maps.MapId id,net.minecraft.world.level.saveddata.maps.MapItemSavedData data){delegate.setMapData(id,data);} + @Override public net.minecraft.world.level.saveddata.maps.MapId getFreeMapId(){return delegate.getFreeMapId();} @Override public void destroyBlockProgress(int id,net.minecraft.core.BlockPos pos,int progress){delegate.destroyBlockProgress(id,pos,progress);} @Override public void levelEvent(net.minecraft.world.entity.player.Player player,int type,net.minecraft.core.BlockPos pos,int data){delegate.levelEvent(player,type,pos,data);} - @Override public void gameEvent(Entity source,net.minecraft.world.level.gameevent.GameEvent event,net.minecraft.core.BlockPos pos){delegate.gameEvent(source,event,pos);} - @Override public void gameEvent(net.minecraft.world.level.gameevent.GameEvent event,net.minecraft.world.phys.Vec3 position,net.minecraft.world.level.gameevent.GameEvent.Context context){delegate.gameEvent(event,position,context);} + @Override public void gameEvent(Entity source,net.minecraft.core.Holder event,net.minecraft.core.BlockPos pos){delegate.gameEvent(source,event,pos);} + @Override public void gameEvent(net.minecraft.core.Holder event,net.minecraft.world.phys.Vec3 position,net.minecraft.world.level.gameevent.GameEvent.Context context){delegate.gameEvent(event,position,context);} @Override public java.util.List players(){return delegate.players();} @Override public String gatherChunkSourceStats(){return delegate.gatherChunkSourceStats();} @Override public net.minecraft.world.level.entity.LevelEntityGetter getEntities(){return delegate.getEntities();} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java index b2b0acc..9d616e4 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java @@ -26,20 +26,35 @@ import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; -import net.minecraftforge.network.NetworkRegistry; -import net.minecraftforge.network.simple.*; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.SimpleChannel; import net.minecraftforge.api.distmarker.Dist; /** Disposable two-client coordinator. Step messages acknowledge prior TCP clicks on the server thread. */ public final class MultiplayerChecks { public static final UUID BODY=UUID.fromString("00000000-0000-4000-8000-000000000108"); - public static final SimpleChannel CHANNEL=NetworkRegistry.newSimpleChannel(new net.minecraft.resources.ResourceLocation("lootablebodiesbuildtest:steps"),()->"1","1"::equals,"1"::equals); + public static final SimpleChannel CHANNEL=ChannelBuilder.named("lootablebodiesbuildtest:steps").networkProtocolVersion(1).simpleChannel(); private final MinecraftServer server; private final File sync; private EntityLootableBody body; private int phase,ticks; private boolean finishing; - public static void registerChannel(){CHANNEL.registerMessage(0,Step.class,(message,buffer)->buffer.writeInt(message.number),buffer->new Step(buffer.readInt()),(message,supplier)->{net.minecraftforge.network.NetworkEvent.Context context=supplier.get();context.enqueueWork(()->acknowledge(message,context.getSender()));context.setPacketHandled(true);});CHANNEL.registerMessage(1,FixtureDeath.class,(message,buffer)->{},buffer->new FixtureDeath(),(message,supplier)->{net.minecraftforge.network.NetworkEvent.Context context=supplier.get();context.enqueueWork(()->{RuntimeChecks.require(Boolean.getBoolean("lootablebodies.issueClientCheck") && context.getSender()!=null,"Unexpected fixture death signal");context.getSender().kill();});context.setPacketHandled(true);});} + public static void registerChannel() { + CHANNEL.messageBuilder(Step.class,0,NetworkDirection.PLAY_TO_SERVER) + .encoder((message,buffer)->buffer.writeInt(message.number)) + .decoder(buffer->new Step(buffer.readInt())) + .consumerMainThread((message,context)->acknowledge(message,context.getSender())).add(); + CHANNEL.messageBuilder(FixtureDeath.class,1,NetworkDirection.PLAY_TO_SERVER) + .encoder((message,buffer)->{}) + .decoder(buffer->new FixtureDeath()) + .consumerMainThread((message,context)->{ + RuntimeChecks.require(Boolean.getBoolean("lootablebodies.issueClientCheck") + && context.getSender()!=null,"Unexpected fixture death signal"); + context.getSender().kill(); + }).add(); + CHANNEL.build(); + } public static final class FixtureDeath { } public MultiplayerChecks(MinecraftServer server) throws Exception { this.server=server;sync=sync(); @@ -99,7 +114,7 @@ private void setPhase(int value) throws Exception { private void mark(String name) throws Exception{try(FileWriter out=new FileWriter(new File(sync,"checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("MULTIPLAYER_CHECK_PASS "+name);} public static ItemStack stack(int i,int n){ItemStack s=RuntimeChecks.stack(i);s.setCount(n);return s;} public static void equal(ItemStack a,ItemStack b,String reason){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),reason);} - private static int count(ServerPlayer p,int index){int n=0;ItemStack expected=RuntimeChecks.stack(index);for(int i=0;iSystem.out.println(message.getString()));} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java index 3509b6c..2751d3f 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java @@ -105,7 +105,7 @@ private static void check(String name,Check test) { private static void badName(String key,Tag raw){CompoundTag n=base();n.put(key,raw.copy());if(key.equals("CustomName"))n.remove("Name");EntityLootableBody b=read(n);RuntimeChecks.require(b.getCustomNameTag().equals(key.equals("Name")?"Saved Owner":""),"Bad name lost fallback or entered active metadata");retained(b,key,raw);stable(b);} private static void fallback(){CompoundTag n=base();n.remove("Name");EntityLootableBody b=read(n);b.baseTick();RuntimeChecks.require(b.getGameProfile()!=null && b.getGameProfile().getName().equals("Saved Owner"),"Fallback profile lost");stable(b);} private static void namePriority(){CompoundTag n=base();n.putString("Name","A long display name / \u00a7a\u00e9");EntityLootableBody b=read(n);b.baseTick();RuntimeChecks.require(b.getGameProfile().getName().equals(n.getString("Name")),"Valid historical display name rejected");stable(b);} - private static void packetBoundary(){for(String value:new String[]{repeat('a',32756),repeat('\u20ac',10918)+"ab"}){CompoundTag n=base();n.putString("Name",value);EntityLootableBody b=read(n);RuntimeChecks.require(b.getCustomNameTag().equals(value) && !saved(b).contains(RECOVERY),"Valid packet boundary name rejected");io.netty.buffer.ByteBuf bytes=io.netty.buffer.Unpooled.buffer();try{new net.minecraft.network.FriendlyByteBuf(bytes).writeComponent(b.getCustomName());RuntimeChecks.require(new net.minecraft.network.FriendlyByteBuf(bytes).readComponent().getString().equals(value),"Name packet changed");}finally{bytes.release();}stable(b);}} + private static void packetBoundary(){for(String value:new String[]{repeat('a',32756),repeat('\u20ac',10918)+"ab"}){CompoundTag n=base();n.putString("Name",value);EntityLootableBody b=read(n);RuntimeChecks.require(b.getCustomNameTag().equals(value) && !saved(b).contains(RECOVERY),"Valid packet boundary name rejected");io.netty.buffer.ByteBuf bytes=io.netty.buffer.Unpooled.buffer();try{var wire=new net.minecraft.network.RegistryFriendlyByteBuf(bytes,b.registryAccess());net.minecraft.network.chat.ComponentSerialization.TRUSTED_STREAM_CODEC.encode(wire,b.getCustomName());RuntimeChecks.require(net.minecraft.network.chat.ComponentSerialization.TRUSTED_STREAM_CODEC.decode(wire).getString().equals(value),"Name packet changed");}finally{bytes.release();}stable(b);}} private static void earlySave(){EntityLootableBody b=read(base());RuntimeChecks.require(saved(b).getString("Name").equals("Saved Owner"),"Lazy profile erased Name before first tick");} private static void missingName(){EntityLootableBody b=read(base());b.baseTick();CompoundTag n=base();n.remove("Name");n.remove("CustomName");b.load(n);RuntimeChecks.require(b.getCustomNameTag().isEmpty() && b.getGameProfile()==null,"Repeated load kept old attribution");b.baseTick();RuntimeChecks.require(b.getGameProfile()==null && !saved(b).contains("Name"),"Old profile resurrected");} private static void reusedWriter(){EntityLootableBody b=read(base());b.baseTick();CompoundTag n=saved(b);n.putByte("Vac",(byte)3);b.setUserName("");b.addAdditionalSaveData(n);RuntimeChecks.require(!n.contains("Name") && !n.contains("Vac"),"Writer resurrected old scalar fields");} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java index 91599f0..e266754 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java @@ -89,9 +89,9 @@ private static void assertLocale(Minecraft mc, String code) throws Exception { RuntimeChecks.require(expected.equals(body.getName().getString()) && !body.hasCustomName() && NameLifetimeChecks.same(cleared,NameLifetimeChecks.saved(body)), "Cleared owner did not regain transient fallback"); ItemStack egg = new ItemStack(LootableBodies.CORPSE_EGG); - CompoundTag eggBefore = egg.save(new CompoundTag()); + CompoundTag eggBefore = FixtureStackData.save(egg); RuntimeChecks.require(egg.getHoverName().getString().equals(net.minecraft.client.resources.language.I18n.get("item.lootablebodies.corpse_spawn_egg")) && !egg.getHoverName().getString().contains("entity.Corpse.name"), "Untranslated spawn egg in "+code); - RuntimeChecks.require(NameLifetimeChecks.same(eggBefore,egg.save(new CompoundTag())), "Egg name lookup mutated its native ID/NBT"); + RuntimeChecks.require(NameLifetimeChecks.same(eggBefore,FixtureStackData.save(egg)), "Egg name lookup mutated its native ID/NBT"); System.out.println("NAME_TRANSLATION_PASS "+code); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java index 8013bb8..5884e40 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java @@ -29,7 +29,7 @@ static void verify(ServerLevel world,CompoundTag record,int wave) { for(net.minecraft.nbt.Tag entry:nativeResult.getList("sections",10)) { CompoundTag section=(CompoundTag)entry;int sectionY=section.getInt("Y"); if(sectionY<0 || sectionY>=16)continue; // All 1024 samples from the saved 0..255 range. - PalettedContainer> expected=codec.parse(NbtOps.INSTANCE,section.getCompound("biomes")).getOrThrow(false,message->{throw new AssertionError(message);}); + PalettedContainer> expected=codec.parse(NbtOps.INSTANCE,section.getCompound("biomes")) .getOrThrow(message->{throw new AssertionError(message);}); for(int y=0;y<4;y++)for(int zz=0;zz<4;zz++)for(int xx=0;xx<4;xx++) { Holder actual=chunk.getNoiseBiome(xx,sectionY*4+y,zz); RuntimeChecks.require(expected.get(xx,y,zz).unwrapKey().equals(actual.unwrapKey()),"Historical biome changed in dimension "+record.getInt("Dimension")+" wave "+wave+" section "+sectionY+" sample "+xx+","+y+","+zz); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java index b4dff4b..88c4f69 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java @@ -11,7 +11,7 @@ /** Controlled packaged pre-world conversion/refusal; separate from old writers. */ public final class NativePreflightChecks { - static final String INJECTED="NATIVE120_INJECTED_ENTITY_CHUNK_REFUSAL"; + static final String INJECTED="NATIVE1206_INJECTED_ENTITY_CHUNK_REFUSAL"; private static final AtomicInteger early=new AtomicInteger(),worlds=new AtomicInteger(),starts=new AtomicInteger(); private static String mode; static void install(net.minecraftforge.eventbus.api.IEventBus bus) { @@ -32,14 +32,14 @@ public Dynamic update(DSL.TypeReference type,Dynamic input,int from,in boolean preflight=java.util.Arrays.stream(Thread.currentThread().getStackTrace()).anyMatch( frame->frame.getClassName().equals("cyano.lootable.upgrade.UpgradeJournal") && frame.getMethodName().equals("validateEntityChunks")); if(!preflight || worlds.get()!=0 || starts.get()!=0)throw new AssertionError("Entity conversion missed the pre-world boundary"); - early.incrementAndGet();System.out.println("NATIVE120_PREWORLD_ENTITY_CONVERSION from="+from+" to="+to+" mode="+mode); + early.incrementAndGet();System.out.println("NATIVE1206_PREWORLD_ENTITY_CONVERSION from="+from+" to="+to+" mode="+mode); if(mode.equals("refuse"))throw new IllegalStateException(INJECTED+": controlled negative fixture, not a native Minecraft error"); } return nativeFixer.update(type,input,from,to); } }; cyano.lootable.upgrade.UpgradePreservation.register(observer); - System.out.println("NATIVE120_PREFLIGHT_CONTROL_INSTALLED mode="+mode); + System.out.println("NATIVE1206_PREFLIGHT_CONTROL_INSTALLED mode="+mode); })); } @SubscribeEvent public void loaded(net.minecraftforge.event.level.LevelEvent.Load event){worlds.incrementAndGet();} @@ -56,15 +56,15 @@ static void verify(MinecraftServer server) throws Exception { // Existing placement rules preserve those invalid records in Aux. if(slot<4){RuntimeChecks.require(item.isEmpty(),"Invalid diamond entered an armour slot");continue;} RuntimeChecks.require(item.getItem()==net.minecraft.world.item.Items.DIAMOND && item.getCount()==slot+1 - && item.hasTag() && item.getTag().getString("keep").equals("slot-"+slot),"Preflight inventory changed at slot"+slot); + && FixtureStackData.hasCustom(item) && FixtureStackData.custom(item).getString("keep").equals("slot-"+slot),"Preflight inventory changed at slot"+slot); } CompoundTag saved=new CompoundTag();body.saveAsPassenger(saved); RuntimeChecks.require(saved.getInt("LootableBodiesDataVersion")==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Preflight corpse marker was not advanced"); ListTag aux=saved.getList("Aux",10);RuntimeChecks.require(aux.size()==5,"Preflight Aux record count changed"); for(int index=0;index<5;index++) { int sourceSlot=index<4?index:47;CompoundTag item=aux.getCompound(index); - RuntimeChecks.require(item.getString("id").equals("minecraft:diamond") && item.getByte("Count")==sourceSlot+1 - && item.getCompound("tag").getString("keep").equals("slot-"+sourceSlot),"Preflight Aux changed at record"+index); + RuntimeChecks.require(item.getString("id").equals("minecraft:diamond") && item.getInt("count")==sourceSlot+1 + && item.getCompound("components").getCompound("minecraft:custom_data").getString("keep").equals("slot-"+sourceSlot),"Preflight Aux changed at record"+index); } RuntimeChecks.require(saved.getList("LootableBodiesUnresolved",10).stream().anyMatch(value->value instanceof CompoundTag && ((CompoundTag)value).getString("Source").equals("FixtureUnknown") @@ -73,6 +73,6 @@ static void verify(MinecraftServer server) throws Exception { && ((CompoundTag)value).getCompound("Data").getByte("Slot")==46 && ((CompoundTag)value).getCompound("Data").getCompound("tag").getString("keep").equals("slot-46")),"Raw Source/Data recovery evidence changed"); Files.writeString(Path.of("preflight-conversion.pass"),"48inputEquipmentRecords/Aux/Source/Data/entityStorage conserved\n",StandardOpenOption.CREATE_NEW); - System.out.println("NATIVE120_PACKAGED_PREFLIGHT_CONVERSION_PASS"); + System.out.println("NATIVE1206_PACKAGED_PREFLIGHT_CONVERSION_PASS"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java index 6330d9d..f27c963 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java @@ -50,7 +50,7 @@ boolean tick(Minecraft mc) throws Exception { NbtWorldChecks.seed(p.serverLevel(),bodyId,COORDINATE,folder(mc),"graphical"); });next(1);} else if(stage==1 && done() && frames>20){quit(mc);next(2);} - else if(stage==2 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture");next(3);} + else if(stage==2 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().openWorld("build-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(3);} else if(stage==3 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(COORDINATE+8.5,80,COORDINATE-.5,0,0));next(30);} else if(stage==30 && done() && frames>20){schedule(mc,()->{ RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions==1,"Missing exact first native graphical disk admission"); @@ -68,7 +68,7 @@ else if(stage==6 && frames>25){schedule(mc,()->{ mc.getSingleplayerServer().getPlayerList().saveAll();mc.getSingleplayerServer().saveAllChunks(false,true,false);mark(mc,"malformed_network_recovery_preserves_raw_records"); });next(7);} else if(stage==7 && done()){checkPlayer(mc.player.getInventory());quit(mc);next(8);} - else if(stage==8 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture");next(9);} + else if(stage==8 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().openWorld("build-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(9);} else if(stage==9 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(COORDINATE+8.5,80,COORDINATE-.5,0,0));next(90);} else if(stage==90 && done() && frames>20){schedule(mc,()->{ RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions==2,"Missing exact second native graphical disk admission"); @@ -89,9 +89,9 @@ else if(stage==90 && done() && frames>20){schedule(mc,()->{ } return false; } - private void verifyGui(Minecraft mc){ItemStack[] expected=new ItemStack[48];ListTag list=visible.getList("Equipment",10);for(int i=0;i actual=new TreeMap<>();for(int i=0;i expected=new TreeMap<>();add(expected,NbtRecoveryChecks.item(120,17));RuntimeChecks.require(actual.equals(expected),"Malformed GUI recovered player stack changed");} - private static void add(Map map,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=s.save(new CompoundTag());n.remove("Count");String key=n.toString();map.put(key,map.getOrDefault(key,0)+s.getCount());} + private static void add(Map map,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=FixtureStackData.save(s);n.remove("count");String key=n.toString();map.put(key,map.getOrDefault(key,0)+s.getCount());} private static void equal(ItemStack a,ItemStack b,String why){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),why);} private static CompoundTag full(EntityLootableBody b){CompoundTag n=new CompoundTag();b.saveAsPassenger(n);return n;} private ServerPlayer player(Minecraft mc){return mc.getSingleplayerServer().getPlayerList().getPlayer(playerId);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java index 2fa9938..9a290c2 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java @@ -1,15 +1,4 @@ package cyano.lootable.buildtest; -import net.minecraft.world.inventory.Slot; -import net.minecraft.nbt.Tag; -import net.minecraft.nbt.StringTag; -import net.minecraft.nbt.ShortTag; -import net.minecraft.nbt.LongTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.IntTag; -import net.minecraft.nbt.FloatTag; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ByteTag; - import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import java.io.*; @@ -51,20 +40,21 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex check("missing_aux_item_raw_retained",()->unreadable("Aux","id",StringTag.valueOf("absentfixture:missing"))); check("air_item_is_not_a_replacement",()->unreadable("Equipment","id",StringTag.valueOf("minecraft:air"))); check("numeric_item_id_not_guessed",()->unreadable("Equipment","id",ShortTag.valueOf((short)264))); - check("missing_count_retained_raw",()->unreadable("Equipment","Count",null)); - check("zero_count_retained_raw",()->unreadable("Equipment","Count",ByteTag.valueOf((byte)0))); - check("negative_count_retained_raw",()->unreadable("Equipment","Count",ByteTag.valueOf((byte)-1))); - check("wide_count_does_not_wrap_to_zero",()->unreadable("Equipment","Count",IntTag.valueOf(256))); - check("string_count_retained_raw",()->unreadable("Equipment","Count",StringTag.valueOf("17"))); - check("fractional_count_retained_raw",()->unreadable("Equipment","Count",FloatTag.valueOf(17.5F))); + check("legacy_missing_count_retained_raw",NbtRecoveryChecks::legacyMissingCount); + check("current_missing_count_uses_native_one_default",NbtRecoveryChecks::nativeMissingCount); + check("zero_count_retained_raw",()->unreadable("Equipment","count",ByteTag.valueOf((byte)0))); + check("negative_count_retained_raw",()->unreadable("Equipment","count",ByteTag.valueOf((byte)-1))); + check("wide_count_does_not_wrap_to_zero",()->unreadable("Equipment","count",IntTag.valueOf(256))); + check("string_count_retained_raw",()->unreadable("Equipment","count",StringTag.valueOf("17"))); + check("fractional_count_retained_raw",()->unreadable("Equipment","count",FloatTag.valueOf(17.5F))); check("valid_integer_count_preserved",NbtRecoveryChecks::integerCount); - check("wrong_damage_type_retained_raw",()->unreadable("Equipment","Damage",StringTag.valueOf("17"))); - check("negative_damage_not_reset",()->unreadable("Equipment","Damage",ShortTag.valueOf((short)-1))); - check("wide_damage_not_narrowed",()->unreadable("Equipment","Damage",IntTag.valueOf(32768))); - check("wrong_custom_tag_type_retained_raw",()->unreadable("Equipment","tag",StringTag.valueOf("opaque"))); + check("wrong_damage_type_retained_raw",()->badComponent("minecraft:damage",StringTag.valueOf("17"))); + check("negative_damage_not_reset",()->badComponent("minecraft:damage",IntTag.valueOf(-1))); + check("wide_damage_not_narrowed",()->badComponent("minecraft:damage",IntTag.valueOf(32768))); + check("wrong_custom_tag_type_retained_raw",()->badComponent("minecraft:custom_data",StringTag.valueOf("opaque"))); check("unknown_capability_payload_retained_raw",NbtRecoveryChecks::unknownCaps); - check("native116_unknown_capabilities_survive_copy_split_and_reload",NbtRecoveryChecks::nativeCaps); - check("native116_capability_initialization_exposes_unhandled_data",NbtRecoveryChecks::initializedCaps); + check("native50_component_payload_survives_copy_split_and_reload",NbtRecoveryChecks::nativeCaps); + check("native50_cannot_save_legacy_capability_payload",NbtRecoveryChecks::initializedCaps); check("registered_capability_payload_remains_recoverable",NbtRecoveryChecks::knownCaps); check("unknown_stack_root_data_retained_raw",()->unreadable("Equipment","UnknownPayload",StringTag.valueOf("retain verbatim"))); check("wrong_equipment_list_type_retained",()->wrongList("Equipment")); @@ -93,20 +83,20 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex RuntimeChecks.require(failures==0,"NBT recovery regressions: "+failures+" / "+results.size());System.out.println("LOOTABLE_BODIES_NBT_PASS checks="+results.size()); } private static void check(String name,Check test){ - Set before=new HashSet<>(FixtureApi.entities(world)); + Set before=new HashSet<>(FixtureApi.entities(world,10,10)); try{test.run();results.add("PASS "+name);System.out.println("NBT_CHECK_PASS "+name);} catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("NBT_CHECK_FAIL "+name+": "+e);e.printStackTrace();} - finally{for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))e.discard();} + finally{for(Entity e:new ArrayList(FixtureApi.entities(world,10,10)))if(!before.contains(e))e.discard();} } static ItemStack item(int index,int count){ItemStack s=RuntimeChecks.stack(index);s.setCount(count);return s;} - static CompoundTag entry(int index,int count,int slot){CompoundTag n=item(index,count).save(new CompoundTag());n.putByte("Slot",(byte)slot);return n;} + static CompoundTag entry(int index,int count,int slot){CompoundTag n=FixtureStackData.save(item(index,count));n.putByte("Slot",(byte)slot);return n;} static ListTag list(CompoundTag... entries){ListTag l=new ListTag();for(CompoundTag n:entries)l.add(n.copy());return l;} static CompoundTag root(CompoundTag... entries){CompoundTag n=new CompoundTag();n.put("Equipment",list(entries));n.putByte("Vac",(byte)20);return n;} static CompoundTag saved(EntityLootableBody b){CompoundTag n=new CompoundTag();b.addAdditionalSaveData(n);return n;} static EntityLootableBody read(CompoundTag n){CompoundTag copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.readAdditionalSaveData(n);RuntimeChecks.require(n.equals(copy),"Reader modified source NBT");return b;} private static void equal(ItemStack a,ItemStack b,String why){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),why);} - private static void add(Map sums,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=s.save(new CompoundTag());n.remove("Count");String k=n.toString();sums.put(k,sums.getOrDefault(k,0)+s.getCount());} - static Map items(EntityLootableBody b){Map sums=new TreeMap<>();for(int i=0;i<48;i++)add(sums,b.getItem(i));ListTag aux=saved(b).getList("Aux",10);for(int i=0;i sums,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=FixtureStackData.save(s);n.remove("count");String k=n.toString();sums.put(k,sums.getOrDefault(k,0)+s.getCount());} + static Map items(EntityLootableBody b){Map sums=new TreeMap<>();for(int i=0;i<48;i++)add(sums,b.getItem(i));ListTag aux=saved(b).getList("Aux",10);for(int i=0;i sums=new TreeMap<>();for(ItemStack s:expected)add(sums,s);RuntimeChecks.require(items(b).equals(sums),"Recoverable item/count/damage/full NBT changed: expected="+sums+" actual="+items(b)+" retained="+saved(b).get(RECOVERY));} static void retained(EntityLootableBody b,Tag raw){ListTag records=saved(b).getList(RECOVERY,10);boolean found=false;for(int i=0;i totals,ItemStack s){CompoundTag n=s.save(new CompoundTag());n.remove("Count");String key=n.toString();totals.put(key,totals.getOrDefault(key,0)+s.getCount());} + private static void add(Map totals,ItemStack s){CompoundTag n=FixtureStackData.save(s);n.remove("count");String key=n.toString();totals.put(key,totals.getOrDefault(key,0)+s.getCount());} static void verify(EntityLootableBody b,boolean restored){ verifyMetadata(b); Map expected=new TreeMap<>();for(int i=0;i<48;i++)if(i<7 || i>10)add(expected,known(i)); for(int[] spec:new int[][]{{121,11},{122,13},{126,7},{128,2},{130,19},{131,23}})add(expected,NbtRecoveryChecks.item(spec[0],spec[1])); - if(restored){CompoundTag raw=NbtRecoveryChecks.entry(123,9,7);raw.putString("id",RETURNING);ItemStack s=ItemStack.of(raw);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getItem()==returning,"Returning fixture item not registered");add(expected,s);} + if(restored){CompoundTag raw=NbtRecoveryChecks.entry(123,9,7);raw.putString("id",RETURNING);ItemStack s=FixtureStackData.load(raw);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getItem()==returning,"Returning fixture item not registered");add(expected,s);} RuntimeChecks.require(expected.equals(NbtRecoveryChecks.items(b)),"Disk recovery item/count/damage/custom NBT lost or duplicated"); CompoundTag source=rawEquipment();int rawCount=0; for(String list:new String[]{"Equipment","Aux"}){ListTag entries=source.getList(list,10);for(int i=0;i { - net.minecraft.network.FriendlyByteBuf buffer=new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.buffer()); + var buffer=new net.minecraft.network.RegistryFriendlyByteBuf(io.netty.buffer.Unpooled.buffer(),world.registryAccess()); try { EntityLootableBody corpse=new EntityLootableBody(world); net.minecraft.network.protocol.game.ClientboundAddEntityPacket sent=new net.minecraft.network.protocol.game.ClientboundAddEntityPacket(corpse); - sent.write(buffer); - net.minecraft.network.protocol.game.ClientboundAddEntityPacket received=new net.minecraft.network.protocol.game.ClientboundAddEntityPacket(buffer); + net.minecraft.network.protocol.game.ClientboundAddEntityPacket.STREAM_CODEC.encode(buffer,sent); + net.minecraft.network.protocol.game.ClientboundAddEntityPacket received=net.minecraft.network.protocol.game.ClientboundAddEntityPacket.STREAM_CODEC.decode(buffer); require(RespawnPacketTrace.describe(sent).equals(RespawnPacketTrace.describe(received)),"Spawn observer differs before/after native decoding"); require(RespawnPacketTrace.describe(received).contains("mod=lootablebodies "),"Negative control cannot identify corpse spawns"); buffer.clear(); - net.minecraft.network.protocol.game.ClientboundRespawnPacket outgoing=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(world.dimensionTypeRegistration().unwrapKey().orElseThrow(),world.dimension(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,net.minecraft.network.protocol.game.ClientboundRespawnPacket.KEEP_ALL_DATA,java.util.Optional.empty(),0); - outgoing.write(buffer); - net.minecraft.network.protocol.game.ClientboundRespawnPacket incoming=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(buffer); + net.minecraft.network.protocol.game.ClientboundRespawnPacket outgoing=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(new net.minecraft.network.protocol.game.CommonPlayerSpawnInfo(world.dimensionTypeRegistration(),world.dimension(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,java.util.Optional.empty(),0),net.minecraft.network.protocol.game.ClientboundRespawnPacket.KEEP_ALL_DATA); + net.minecraft.network.protocol.game.ClientboundRespawnPacket.STREAM_CODEC.encode(buffer,outgoing); + net.minecraft.network.protocol.game.ClientboundRespawnPacket incoming=net.minecraft.network.protocol.game.ClientboundRespawnPacket.STREAM_CODEC.decode(buffer); require(RespawnPacketTrace.describe(outgoing).equals(RespawnPacketTrace.describe(incoming)),"Respawn observer differs before/after native decoding"); require(RespawnPacketTrace.describe(incoming).equals("RESPAWN dimension=minecraft:overworld hashedSeed="+0x13572468ABCDEF01L),"Native 1.16 hashed seed missing from the packet observation"); } finally {buffer.release();} }); check("rejected_spawn_keeps_death_inventory", () -> { - ServerPlayer player=FakePlayerFactory.get(world,new GameProfile(UUID.randomUUID(),"PortFixture")); + ServerPlayer player=new ServerPlayer(server,world,new GameProfile(UUID.randomUUID(),"PortFixture"),net.minecraft.server.level.ClientInformation.createDefault()); ArrayList drops=new ArrayList<>(); drops.add(new ItemEntity(world,8,80,8,new ItemStack(Items.DIAMOND,7))); Object veto=new Object() { @SubscribeEvent public void join(EntityJoinLevelEvent e) { @@ -71,18 +70,18 @@ public static void run(MinecraftServer server) throws Exception { EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag(); body.addAdditionalSaveData(nbt); ListTag equipment=new ListTag(); - for(int i=0;i<2;i++) { CompoundTag item=new ItemStack(Items.DIAMOND,i+2).save(new CompoundTag()); item.putByte("Slot",(byte)10); equipment.add(item); } + for(int i=0;i<2;i++) { CompoundTag item=FixtureStackData.save(new ItemStack(Items.DIAMOND,i+2)); item.putByte("Slot",(byte)10); equipment.add(item); } nbt.put("Equipment",equipment); body.readAdditionalSaveData(nbt); CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); int count=0; for(String key:Arrays.asList("Equipment","Aux")) { ListTag list=saved.getList(key,10); - for(int i=0;i { EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); - CompoundTag item=new ItemStack(Items.DIAMOND,7).save(new CompoundTag());item.putString("id","missingfixture:keepsake");item.putByte("Slot",(byte)10); + CompoundTag item=FixtureStackData.save(new ItemStack(Items.DIAMOND,7));item.putString("id","missingfixture:keepsake");item.putByte("Slot",(byte)10); ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);body.readAdditionalSaveData(nbt); CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); require(saved.getList("LootableBodiesUnresolved",10).size()==1,"missing item was erased"); @@ -90,7 +89,7 @@ public static void run(MinecraftServer server) throws Exception { }); check("invalid_slot_retains_inventory", () -> { EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); - CompoundTag item=new ItemStack(Items.EMERALD,9).save(new CompoundTag());item.putInt("Slot",12345); + CompoundTag item=FixtureStackData.save(new ItemStack(Items.EMERALD,9));item.putInt("Slot",12345); ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);body.readAdditionalSaveData(nbt); CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); require(!body.isEmpty(),"invalid slot discarded inventory"); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java index 4069548..df231af 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java @@ -11,7 +11,6 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ByteTag; import net.minecraft.nbt.ByteArrayTag; -import net.minecraft.core.PositionImpl; import cyano.lootable.entities.EntityLootableBody; import java.io.*; diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java index e6e32e6..9074d41 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java @@ -84,20 +84,20 @@ private static void check(String name, Check action) { } private static ItemStack shovel() { ItemStack stack=new ItemStack(Items.DIAMOND_SHOVEL); - stack.setTag(RuntimeChecks.stack(300).getTag().copy());stack.setDamageValue(17); - stack.setHoverName(net.minecraft.network.chat.Component.literal("Issue 5 original shovel")); return stack; + FixtureStackData.setCustom(stack,FixtureStackData.custom(RuntimeChecks.stack(300)));stack.setDamageValue(17); + stack.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,net.minecraft.network.chat.Component.literal("Issue 5 original shovel")); return stack; } static Map inventory(EntityLootableBody body) { Map total=new TreeMap<>(); for(int i=0;i<48;i++) add(total,body.getItem(i)); CompoundTag n=new CompoundTag();body.addAdditionalSaveData(n); ListTag aux=n.getList("Aux",10); - for(int i=0;i total,ItemStack stack) { if((stack == null || stack.isEmpty()) || stack.getCount()<=0)return; - CompoundTag n=stack.save(new CompoundTag());n.remove("Count"); + CompoundTag n=FixtureStackData.save(stack);n.remove("count"); total.put(n.toString(),total.getOrDefault(n.toString(),0)+stack.getCount()); } private static Map tagged(EntityLootableBody body) { @@ -107,7 +107,7 @@ private static Map tagged(ServerPlayer player) { Map n=new TreeMap<>(); for(int i=0;i()); - FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + var wire=new net.minecraft.network.RegistryFriendlyByteBuf(Unpooled.buffer(),p.registryAccess()); try { - outgoing.write(wire);p.connection.handleContainerClick(new ServerboundContainerClickPacket(wire)); + ServerboundContainerClickPacket.STREAM_CODEC.encode(wire,outgoing);p.connection.handleContainerClick(ServerboundContainerClickPacket.STREAM_CODEC.decode(wire)); RuntimeChecks.require(!wire.isReadable(),"Native issue click left undecoded fields"); RuntimeChecks.require(observed.menuEvents>before && observed.menuState==p.containerMenu.getStateId() && RuntimeChecks.sameStack(observed.carried,p.containerMenu.getCarried()),"Native menu correction did not match its authoritative state/cursor"); @@ -156,7 +156,7 @@ private static void addFixturePlayer(ServerPlayer player)throws ReflectiveOperat } private static void deathSequence() throws Exception { world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null);RuntimeChecks.awaitChunk(world,2,0); - ServerPlayer p=new ServerPlayer(server,world,new GameProfile(UUID.randomUUID(),"IssueFive")); + ServerPlayer p=new ServerPlayer(server,world,new GameProfile(UUID.randomUUID(),"IssueFive"),net.minecraft.server.level.ClientInformation.createDefault()); p.connection=new QuietConnection(p);p.setGameMode(GameType.SURVIVAL);p.setPos(32.5,80,0.5); // Vanilla clients need player-list information before a tracked player // spawn, including the brief spawn-point visit during real respawn. @@ -171,12 +171,12 @@ private static void deathSequence() throws Exception { for(int slot=0;slot<48;slot++)if(gui.getSlot(slot).hasItem())click(p,slot,0,ClickType.QUICK_MOVE); RuntimeChecks.require(first.isEmpty() && expected.equals(tagged(p)),"First recovery lost or duplicated tagged inventory: remaining="+inventory(first)+" expected="+expected+" player="+tagged(p)+" state="+gui.getStateId()); FriendlyByteBuf close=new FriendlyByteBuf(Unpooled.buffer()); - try {new ServerboundContainerClosePacket(gui.containerId).write(close);p.connection.handleContainerClose(new ServerboundContainerClosePacket(close));RuntimeChecks.require(!close.isReadable(),"Native issue close left undecoded fields");} + try {ServerboundContainerClosePacket.STREAM_CODEC.encode(close,new ServerboundContainerClosePacket(gui.containerId));p.connection.handleContainerClose(ServerboundContainerClosePacket.STREAM_CODEC.decode(close));RuntimeChecks.require(!close.isReadable(),"Native issue close left undecoded fields");} finally {close.release();} - int active=-1;for(int slot=1;slot<9;slot++)if(!(p.getInventory().items.get(slot)).isEmpty() && p.getInventory().items.get(slot).hasTag() && p.getInventory().items.get(slot).getTag().contains("fixture"))active=slot; + int active=-1;for(int slot=1;slot<9;slot++)if(!(p.getInventory().items.get(slot)).isEmpty() && FixtureStackData.hasCustom(p.getInventory().items.get(slot)) && FixtureStackData.custom(p.getInventory().items.get(slot)).contains("fixture"))active=slot; RuntimeChecks.require(active>0,"Shift-click did not leave a different occupied hotbar slot"); FriendlyByteBuf held=new FriendlyByteBuf(Unpooled.buffer()); - try {new ServerboundSetCarriedItemPacket(active).write(held);p.connection.handleSetCarriedItem(new ServerboundSetCarriedItemPacket(held));RuntimeChecks.require(!held.isReadable(),"Native held-item packet left undecoded fields");} + try {ServerboundSetCarriedItemPacket.STREAM_CODEC.encode(held,new ServerboundSetCarriedItemPacket(active));p.connection.handleSetCarriedItem(ServerboundSetCarriedItemPacket.STREAM_CODEC.decode(held));RuntimeChecks.require(!held.isReadable(),"Native held-item packet left undecoded fields");} finally {held.release();} // Deliberately do not tick/sync container or reopen inventory here. EntityLootableBody second=death(p); @@ -304,7 +304,7 @@ static void coldReload(MinecraftServer instance,ServerLevel target,File director } private static final class QuietConnection extends ServerGamePacketListenerImpl { int menuEvents,menuState=-1;ItemStack carried=ItemStack.EMPTY; - QuietConnection(ServerPlayer p){super(server,new Connection(PacketFlow.SERVERBOUND),p);} + QuietConnection(ServerPlayer p){super(ReportedIssueChecks.server,new Connection(PacketFlow.SERVERBOUND),p,net.minecraft.server.network.CommonListenerCookie.createInitial(p.getGameProfile(),false));} @Override public void send(Packet packet) { NativeMenuObservation observed=NativeMenuObservation.decode(packet);if(observed==null)return; if(observed.containerId==player.containerMenu.containerId){menuEvents++;menuState=observed.stateId;if(observed.content)carried=observed.carried;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java index d2fdc88..723e2ac 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java @@ -44,8 +44,8 @@ private interface Action {void run() throws Exception;} private CompoundTag savedBody,savedPlayer; static ItemStack original() { ItemStack s=new ItemStack(Items.DIAMOND_SHOVEL); - s.setTag(RuntimeChecks.stack(300).getTag().copy());s.setDamageValue(17); - s.setHoverName(net.minecraft.network.chat.Component.literal("Issue 5 original shovel"));return s; + FixtureStackData.setCustom(s,FixtureStackData.custom(RuntimeChecks.stack(300)));s.setDamageValue(17); + s.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,net.minecraft.network.chat.Component.literal("Issue 5 original shovel"));return s; } void tick(Minecraft mc) throws Exception { frames++; @@ -85,7 +85,7 @@ void tick(Minecraft mc) throws Exception { mc.getConnection().send(new ServerboundSetCarriedItemPacket(active)); // Same ordered connection: held-item change, then a build-only death signal. // This uses the vanilla death path without native command permissions. - MultiplayerChecks.CHANNEL.sendToServer(new MultiplayerChecks.FixtureDeath());next(8); + MultiplayerChecks.CHANNEL.send(new MultiplayerChecks.FixtureDeath(),net.minecraftforge.network.PacketDistributor.SERVER.noArg());next(8); } else if(stage==8 && mc.screen instanceof DeathScreen && frames>10) { mc.player.respawn();next(9); } else if(stage==9 && ready(mc) && mc.player.isAlive() && !(mc.screen instanceof DeathScreen)) { @@ -118,7 +118,7 @@ void tick(Minecraft mc) throws Exception { mc.player.closeContainer();schedule(mc,()->save(mc));next(12); } else if(stage==12 && done()) {quit(mc);next(13); } else if(stage==13 && FixtureConnections.stopped(mc)) { - mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"issue-fixture");next(14); + mc.createWorldOpenFlows().openWorld("issue-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(14); } else if(stage==14 && ready(mc) && frames>25) { schedule(mc,()->{verifyDisk(mc);verifyOriginals(body(mc));mark("first_disk_reload_complete_corpse_inventory");});next(15); } else if(stage==15 && done() && open(mc,secondId)) {next(16); @@ -138,7 +138,7 @@ void tick(Minecraft mc) throws Exception { });next(18); } else if(stage==18 && done()) {quit(mc);next(19); } else if(stage==19 && FixtureConnections.stopped(mc)) { - mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"issue-fixture");next(20); + mc.createWorldOpenFlows().openWorld("issue-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(20); } else if(stage==20 && ready(mc) && frames>25) { schedule(mc,()->{verifyDisk(mc);mark("second_disk_reload_complete_player_and_corpse_inventory");});next(21); } else if(stage==21 && done()) { @@ -152,8 +152,8 @@ private EntityLootableBody die(ServerPlayer p,UUID skip) { throw new AssertionError("Actual connected death absent"); } private static void addTagged(Map result,ItemStack stack) { - if((stack == null || stack.isEmpty()) || !stack.hasTag() || !stack.getTag().contains("fixture"))return; - CompoundTag n=stack.save(new CompoundTag());n.remove("Count");result.put(n.toString(),result.getOrDefault(n.toString(),0)+stack.getCount()); + if((stack == null || stack.isEmpty()) || !FixtureStackData.hasCustom(stack) || !FixtureStackData.custom(stack).contains("fixture"))return; + CompoundTag n=FixtureStackData.save(stack);n.remove("count");result.put(n.toString(),result.getOrDefault(n.toString(),0)+stack.getCount()); } private static Map originals(){Map m=new TreeMap<>();addTagged(m,original());addTagged(m,RuntimeChecks.stack(301));return m;} private static void verifyOriginals(EntityLootableBody b) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java index d6e0caf..a0bebd5 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java @@ -63,7 +63,7 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { static File file(String name){return new File(System.getProperty("lootablebodies.multiplayer.sync"),name);} private ServerPlayer player(String role){return server.getPlayerList().getPlayerByName("Container"+role);} private boolean ack(String role,int p){return file(role+".case-"+index+"-"+p).isFile();} - private void trace(ServerPlayer p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.connection,file(p.getGameProfile().getName()+"-server-packets.txt"),null);} + private void trace(ServerPlayer p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.getConnection(),file(p.getGameProfile().getName()+"-server-packets.txt"),null);} @SubscribeEvent public void tick(TickEvent.ServerTickEvent e) { if(e.phase!=TickEvent.Phase.END||finished)return; try { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java index 5f80b42..c3ba96c 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java @@ -73,16 +73,14 @@ private void respawn(Minecraft mc) { profileWorld=mc.level; // IPacket tests use cached synthetic profiles, never real accounts or // live profile-service availability. The local_texture default stays false. - GameProfileCache fixtureCache=new GameProfileCache(null,new File(mc.gameDirectory,"fixture-usercache.json")) { - @Override public Optional get(String name) { - RuntimeChecks.require(name.matches("Container[AB]"),"Unexpected packet fixture profile"); - UUID id=UUID.nameUUIDFromBytes(("OfflinePlayer:"+name).getBytes(StandardCharsets.UTF_8)); - GameProfile p=new GameProfile(id,name); - String texture="{\"timestamp\":0,\"profileId\":\""+id.toString().replace("-","")+"\",\"profileName\":\""+name+"\",\"textures\":{}}"; - p.getProperties().put("textures",new Property("textures",Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8))));return Optional.of(p); - } - }; - fixtureCache.setExecutor(mc::execute);SkullBlockEntity.profileCache=fixtureCache; + FixtureProfileCache.install(name->{ + RuntimeChecks.require(name.matches("Container[AB]"),"Unexpected offline fixture profile"); + UUID id=UUID.nameUUIDFromBytes(("OfflinePlayer:"+name).getBytes(StandardCharsets.UTF_8)); + var profile=new com.mojang.authlib.GameProfile(id,name); + String texture="{\"timestamp\":0,\"profileId\":\""+id.toString().replace("-","")+"\",\"profileName\":\""+name+"\",\"textures\":{}}"; + profile.getProperties().put("textures",new com.mojang.authlib.properties.Property("textures",Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8)))); + return java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.of(profile)); + }); } return; } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java index a9e6621..57c9f6b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java @@ -1,30 +1,10 @@ package cyano.lootable.buildtest; -import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; -import net.minecraft.network.protocol.game.ClientboundRespawnPacket; -import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; -import net.minecraft.network.protocol.game.ClientboundPlayerCombatEnterPacket; -import net.minecraft.network.protocol.game.ClientboundPlayerCombatEndPacket; -import net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket; -import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; -import net.minecraft.core.Registry; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.*; import java.io.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +import java.util.Arrays; +import io.netty.channel.*; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.Connection; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; -import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; -import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; -import net.minecraft.network.protocol.game.ClientboundRespawnPacket; -import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; -import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; -import net.minecraftforge.network.ICustomPacket; +import net.minecraft.network.protocol.game.*; /** Observes decoded packets without dropping, delaying, mutating or reordering them. */ final class RespawnPacketTrace extends ChannelDuplexHandler { @@ -37,13 +17,16 @@ static void install(Connection manager,File log,After after) { Channel channel=manager.channel(); // Native19 assembles decoded spawn children into a bundle before the // packet listener. Observe that real wire stream before aggregation. - channel.eventLoop().submit(()->channel.pipeline().addBefore("bundler","corpse_packet_trace",new RespawnPacketTrace(log,after))).syncUninterruptibly(); + channel.eventLoop().submit(()->{ + String before=channel.pipeline().get("bundler")!=null?"bundler":"packet_handler"; + channel.pipeline().addBefore(before,"corpse_packet_trace",new RespawnPacketTrace(log,after)); + }).syncUninterruptibly(); } @Override public void channelRead(ChannelHandlerContext context,Object packet) throws Exception { String description=describe(packet);if(description!=null)record("IN "+description); if(Boolean.getBoolean("lootablebodies.respawn.dropSpawn")&&log.getName().equals("client-packets.txt") &&description!=null&&description.startsWith("MOD_SPAWN ")&&description.contains("mod=lootablebodies ")) { - record("FAULT_DROPPED "+description);if(packet instanceof ICustomPacket)((ICustomPacket)packet).getInternalData().release();return; + record("FAULT_DROPPED "+description);return; } super.channelRead(context,packet); if(description!=null&&after!=null)after.received(description); @@ -54,41 +37,25 @@ static void install(Connection manager,File log,After after) { } private synchronized void record(String line) throws IOException {try(FileWriter out=new FileWriter(log,true)){out.write((++sequence)+" "+System.nanoTime()+" "+line+"\n");}} static String describe(Object packet) { - if(packet instanceof ICustomPacket) { - ICustomPacket f=(ICustomPacket)packet;if(!f.getName().toString().equals("fml:play"))return null; - FriendlyByteBuf b=new FriendlyByteBuf(f.getInternalData().duplicate());if(b.readableBytes()<6 || b.readVarInt()!=0)return null; - int type=b.readVarInt(),id=b.readInt();net.minecraft.world.entity.EntityType entityType=net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.byId(type); - String name=entityType==null?"unknown":net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(entityType).getNamespace(); - return "MOD_SPAWN id="+id+" mod="+name+" type="+type+" uuid="+new UUID(b.readLong(),b.readLong()); - } - if(!(packet instanceof ClientboundAddEntityPacket||packet instanceof ClientboundRespawnPacket||packet instanceof ClientboundRemoveEntitiesPacket||packet instanceof ClientboundForgetLevelChunkPacket - ||packet instanceof ClientboundLevelChunkWithLightPacket||packet instanceof ClientboundPlayerCombatEnterPacket||packet instanceof ClientboundPlayerCombatEndPacket - ||packet instanceof ClientboundPlayerCombatKillPacket||packet instanceof ClientboundSetEntityDataPacket||packet instanceof ServerboundClientCommandPacket))return null; - if(packet instanceof ClientboundRespawnPacket) { - return NativeRespawnDescription.describe((ClientboundRespawnPacket)packet); + if(packet instanceof ClientboundAddEntityPacket spawn) { + var registry=BuiltInRegistries.ENTITY_TYPE; + var key=registry.getKey(spawn.getType()); + return "MOD_SPAWN id="+spawn.getId()+" mod="+key.getNamespace()+" type=" + +registry.getId(spawn.getType())+" uuid="+spawn.getUUID(); } - // Vanilla strips receiving-side accessors from a dedicated distribution. - // Read an independent serialization, never its live reader index/fields. - FriendlyByteBuf b=new FriendlyByteBuf(Unpooled.buffer()); - try { - ((Packet)packet).write(b); - if(packet instanceof ClientboundAddEntityPacket) { - int id=b.readVarInt();UUID uuid=b.readUUID();int type=b.readVarInt(); - net.minecraft.world.entity.EntityType entityType=net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.byId(type); - String name=entityType==null?"unknown":net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(entityType).getNamespace(); - return "MOD_SPAWN id="+id+" mod="+name+" type="+type+" uuid="+uuid; - } - if(packet instanceof ClientboundRemoveEntitiesPacket){int[] ids=new int[b.readVarInt()];for(int i=0;i{ + RuntimeChecks.require(name.matches("Container[AB]"),"Unexpected offline fixture profile"); UUID id=UUID.nameUUIDFromBytes(("OfflinePlayer:"+name).getBytes(StandardCharsets.UTF_8)); - com.mojang.authlib.GameProfile profile=new com.mojang.authlib.GameProfile(id,name); + var profile=new com.mojang.authlib.GameProfile(id,name); String texture="{\"timestamp\":0,\"profileId\":\""+id.toString().replace("-","")+"\",\"profileName\":\""+name+"\",\"textures\":{}}"; profile.getProperties().put("textures",new com.mojang.authlib.properties.Property("textures",Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8)))); - cache.add(profile); - } - net.minecraft.world.level.block.entity.SkullBlockEntity.profileCache=cache; + return java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.of(profile)); + }); } return; } @@ -155,7 +153,7 @@ final class VisibilityClientChecks { } else if(phase==7 || phase==9 || phase==11) { EntityLootableBody b=body(mc,"first");if(b!=null&&!b.isRemoved()&&!b.isInvisible()&&rendered.contains(b.getUUID())){shot(mc,"tracked-"+phase+".png");ack();} } else if(phase==8 || phase==10) { - if(role.equals(phase==8?"A":"B")){mc.level.disconnect();mc.clearLevel();mc.setScreen(new TitleScreen());connecting=false;} + if(role.equals(phase==8?"A":"B")){mc.level.disconnect();mc.disconnect();mc.setScreen(new TitleScreen());connecting=false;} ack(); } else if(phase==12 || phase==120) { EntityLootableBody b=body(mc,"first");if(b!=null&&!b.isRemoved()&&rendered.contains(b.getUUID()))ack(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java index ea93654..1c2ff38 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java @@ -1,7 +1,6 @@ package cyano.lootable.buildtest; import com.mojang.authlib.GameProfile; -import com.mojang.authlib.minecraft.*; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseRenderer; @@ -9,12 +8,9 @@ import java.lang.reflect.*; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.*; -import net.minecraft.server.players.GameProfileCache; -import net.minecraft.world.level.block.entity.SkullBlockEntity; import net.minecraft.resources.ResourceLocation; @@ -28,58 +24,60 @@ static void run(Minecraft mc) throws Exception { SkinManager original=(SkinManager)skinField.get(mc); GameProfile profile=new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000201"),"VisibilityFault"); try { - skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture"),mc.getMinecraftSessionService()) { - @Override public Map getInsecureSkinInformation(GameProfile p) { + skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture").toPath(),mc.getMinecraftSessionService(),mc) { + @Override public PlayerSkin getInsecureSkin(GameProfile p) { throw new IllegalStateException("injected skin cache failure"); } }); try { ResourceLocation texture=CorpseRenderer.getSkin(profile); - RuntimeChecks.require(DefaultPlayerSkin.getDefaultSkin(profile.getId()).equals(texture),"Failed skin lookup did not use vanilla fallback"); + RuntimeChecks.require(DefaultPlayerSkin.get(profile).texture().equals(texture),"Failed skin lookup did not use vanilla fallback"); mark(report,"skin_cache_failure_has_visible_default",true); } catch(Throwable failure) {failures.add("skin cache: "+failure);mark(report,"skin_cache_failure_has_visible_default",false);} } finally {skinField.set(mc,original);} - Field cache=field(SkullBlockEntity.class,GameProfileCache.class); - Field service=field(SkullBlockEntity.class,MinecraftSessionService.class); - Object oldCache=cache.get(null),oldService=service.get(null); AtomicInteger requests=new AtomicInteger(); - CountDownLatch failedRequest=new CountDownLatch(1); - CountDownLatch slowDelivered=new CountDownLatch(1); - try { - GameProfileCache faultCache=new GameProfileCache(null,new File(mc.gameDirectory,"fault-usercache.json")) { - @Override public Optional get(String name) {return Optional.of(new GameProfile(profile.getId(),name));} - @Override public void add(GameProfile value) {super.add(value);if(value.getName().equals("VisibilitySlow"))slowDelivered.countDown();} - }; - faultCache.setExecutor(Runnable::run);cache.set(null,faultCache); - service.set(null,(MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ - if(method.getName().equals("fillProfileProperties")) {requests.incrementAndGet();failedRequest.countDown();throw new IllegalStateException("injected profile lookup failure");} - throw new UnsupportedOperationException(method.getName()); - })); - EntityLootableBody body=new EntityLootableBody(mc.level);body.setUserName("VisibilityFault"); - try {body.baseTick();RuntimeChecks.require(body.getGameProfile()!=null,"Failure erased fallback profile");mark(report,"profile_failure_does_not_abort_client_tick",true);} - catch(Throwable failure) {failures.add("profile: "+failure);mark(report,"profile_failure_does_not_abort_client_tick",false);} - RuntimeChecks.require(failedRequest.await(2,TimeUnit.SECONDS),"Profile failure was never injected"); - // Deliberately slow lookup must leave the render/tick thread responsive. - service.set(null,(MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ - if(method.getName().equals("fillProfileProperties")) {requests.incrementAndGet();Thread.sleep(400);return args[0];} - throw new UnsupportedOperationException(method.getName()); - })); - EntityLootableBody slow=new EntityLootableBody(mc.level);slow.setUserName("VisibilitySlow"); - long start=System.nanoTime();slow.baseTick();long millis=(System.nanoTime()-start)/1000000; - try {RuntimeChecks.require(millis<150,"Slow profile blocked client tick for "+millis+"ms");mark(report,"delayed_profile_does_not_block_client_tick",true);} - catch(Throwable failure) {failures.add("delay: "+failure);mark(report,"delayed_profile_does_not_block_client_tick",false);} - boolean local=LootableBodies.useLocalSkin;LootableBodies.useLocalSkin=true; - slow.setUserName("VisibilityRenamed");slow.baseTick();LootableBodies.useLocalSkin=local; - RuntimeChecks.require(slowDelivered.await(2,TimeUnit.SECONDS),"Native profile callback was never delivered"); - // Fence the same owned executor after its cache/add/CAS task. - // Waiting for the service alone would not prove the late CAS path. - CountDownLatch callbackFence=new CountDownLatch(1); - ((java.util.concurrent.Executor)field(EntityLootableBody.class,java.util.concurrent.ThreadPoolExecutor.class).get(null)).execute(callbackFence::countDown); - RuntimeChecks.require(callbackFence.await(2,TimeUnit.SECONDS),"Owned profile lookup did not finish"); - RuntimeChecks.require(requests.get()==2,"Faults never exercised actual profile services"); + CompletableFuture> delayed=new CompletableFuture<>(); + try(var ignored=FixtureProfileCache.install(name->{ + requests.incrementAndGet(); + if(name.equals("VisibilityFault"))return CompletableFuture.failedFuture(new IllegalStateException("injected lookup failure")); + if(name.equals("VisibilitySlow"))return delayed; + throw new AssertionError("Unexpected fixture profile: "+name); + })) { + EntityLootableBody body=new EntityLootableBody(mc.level); + body.setUserName("VisibilityFault"); + try { + body.baseTick(); + RuntimeChecks.require(body.getGameProfile()!=null,"Failure erased fallback profile"); + mark(report,"profile_failure_does_not_abort_client_tick",true); + } catch(Throwable failure) { + failures.add("profile: "+failure); + mark(report,"profile_failure_does_not_abort_client_tick",false); + } + EntityLootableBody slow=new EntityLootableBody(mc.level); + slow.setUserName("VisibilitySlow"); + long start=System.nanoTime(); + slow.baseTick(); + long millis=(System.nanoTime()-start)/1000000; + try { + RuntimeChecks.require(!delayed.isDone() && millis<150,"Slow profile blocked client tick for "+millis+"ms"); + mark(report,"delayed_profile_does_not_block_client_tick",true); + } catch(Throwable failure) { + failures.add("delay: "+failure); + mark(report,"delayed_profile_does_not_block_client_tick",false); + } + boolean local=LootableBodies.useLocalSkin; + try { + LootableBodies.useLocalSkin=true; + slow.setUserName("VisibilityRenamed"); + slow.baseTick(); + } finally { + LootableBodies.useLocalSkin=local; + } + delayed.complete(Optional.of(new GameProfile(profile.getId(),"VisibilitySlow"))); + RuntimeChecks.require(requests.get()==2,"Faults did not exercise the native profile lookup"); RuntimeChecks.require(slow.getGameProfile().getName().equals("VisibilityRenamed"),"Late skin result overwrote renamed corpse"); mark(report,"late_profile_result_preserves_new_name",true); - } finally {cache.set(null,oldCache);service.set(null,oldService);} + } if(!failures.isEmpty())throw new AssertionError("VISIBILITY_SKIN_FAILURES "+failures); } private static void mark(File report,String name,boolean passed) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java index 42a2b11..e9f6f5a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java @@ -5,7 +5,6 @@ import java.lang.reflect.Field; import java.util.*; import com.mojang.authlib.GameProfile; -import com.mojang.authlib.minecraft.MinecraftProfileTexture; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import net.minecraft.client.Minecraft; @@ -30,8 +29,8 @@ void tick(Minecraft mc) throws Exception { VisibilitySkinChecks.run(mc); for(Field field:Minecraft.class.getDeclaredFields())if(field.getType()==SkinManager.class){skinField=field;skinField.setAccessible(true);} original=(SkinManager)skinField.get(mc); - skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture"),mc.getMinecraftSessionService()) { - @Override public Map getInsecureSkinInformation(GameProfile p){throw new IllegalStateException("injected render skin cache failure");} + skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture").toPath(),mc.getMinecraftSessionService(),mc) { + @Override public net.minecraft.client.resources.PlayerSkin getInsecureSkin(GameProfile p){throw new IllegalStateException("injected render skin cache failure");} }); mc.setScreen(new Panel(mc));stage=2;frames=0; } else if(stage==2 && ++frames>30) { @@ -49,7 +48,7 @@ private static final class Panel extends Screen { try {for(int i=0;i<3;i++) { LootableBodies.useLocalSkin=i==2;int x=(2*i+1)*width/6; graphics.drawCenteredString(font,names[i],x,35,0xffffff); - InventoryScreen.renderEntityInInventoryFollowsMouse(graphics,x,height*2/3,65,20,-20,bodies[i]); + InventoryScreen.renderEntityInInventoryFollowsMouse(graphics,x-75,65,x+75,height-40,65,0.0625F,x+20,height*2/3-20,bodies[i]); }} finally {LootableBodies.useLocalSkin=local;} } @Override public boolean isPauseScreen(){return false;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java index 735df3d..f9f1ba8 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java @@ -212,7 +212,7 @@ static void reload(ServerLevel target, File folder) throws Exception { world = target; for (boolean trapped : new boolean[]{false,true}) { CompoundTag expected; - try (InputStream in = new FileInputStream(new File(folder,"wall-escape-"+trapped+".nbt"))) { expected = NbtIo.readCompressed(in); } + try (InputStream in = new FileInputStream(new File(folder,"wall-escape-"+trapped+".nbt"))) { expected = NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)); } ListTag pos = expected.getList("Pos",6); BlockPos savedPosition=BlockPos.containing(pos.getDouble(0),pos.getDouble(1),pos.getDouble(2)); // Terrain admission completes before the native entity-storage inbox. @@ -247,7 +247,7 @@ static void historical(MinecraftServer server, File folder, boolean reload) thro File file = new File(folder,"wall-historical.nbt"); if (reload) { try (InputStream in = new FileInputStream(file)) { - RuntimeChecks.require(TypedNbt.sameEntity(NbtIo.readCompressed(in),expected), "Historical cold reload changed complete corpse NBT"); + RuntimeChecks.require(TypedNbt.sameEntity(NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)),expected), "Historical cold reload changed complete corpse NBT"); } } for (int i = 0; i < 32; i++) body.jumpOutOfWall(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java index 164ad5a..84de332 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java @@ -7,7 +7,6 @@ import net.minecraft.nbt.Tag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.CompoundTag; -import net.minecraft.core.PositionImpl; import com.mojang.authlib.GameProfile; import cyano.lootable.LootableBodies; @@ -31,7 +30,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; -import net.minecraftforge.common.util.FakePlayerFactory; /** Acceptance against worlds written by the actual historical production jars. */ final class WorldUpgradeChecks { @@ -42,6 +40,16 @@ final class WorldUpgradeChecks { static void run(MinecraftServer server,String phase) throws Exception { folder=server.getLevel(FixtureApi.dimensionKey(0)).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); manifest=read(new File(folder,"upgrade-fixtures.nbt"));sourceVersion=HistoricalVersions.dataVersion(manifest.getString("Minecraft")); + CompoundTag writerInventory=null; + String writerHash=System.getProperty("lootablebodies.testWriterInventory"); + if(writerHash!=null) { + File writerFile=new File(folder,"upgrade-writer-inventory.nbt"); + require(writerHash.equals(HexFormat.of().withUpperCase().formatHex(backupDigest(writerFile.toPath()))), + "Sealed bridge inventory oracle changed"); + writerInventory=read(writerFile); + require(writerInventory.getAllKeys().size()==manifest.getList("Bodies",10).size(), + "Missing sealed bridge corpse inventories"); + } File stored=new File(folder,"upgrade-recovered.nbt");ledger=stored.isFile()?read(stored):new CompoundTag();checks=0; if(!ledger.contains("Recoveries",10))ledger.put("Recoveries",new CompoundTag()); LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1; @@ -60,30 +68,65 @@ static void run(MinecraftServer server,String phase) throws Exception { require(body.getX()==source.getList("Pos",6).getDouble(0) && body.getY()==source.getList("Pos",6).getDouble(1) && body.getZ()==source.getList("Pos",6).getDouble(2),"Position changed in dimension "+FixtureApi.dimensionId(world)+": "+body.position()); CompoundTag actual=save(body);require(actual.getInt("LootableBodiesDataVersion")==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Missing corpse migration marker"); require(NameLifetimeChecks.same(source.get("LootableBodiesInvalidFields"),actual.get("LootableBodiesInvalidFields")),"Original invalid-field evidence changed"); - Map expected=expectedInventory(source);Map observed=inventory(actual); + CompoundTag rawSource=source; + int rawSourceVersion=sourceVersion; + Map bridgeBanners=Map.of(); + if(writerInventory!=null) { + CompoundTag writer=writerInventory.getCompound(id); + require(writer.contains("Inventory",10) && writer.contains("DataVersion",3), + "Missing sealed writer inventory for "+id); + rawSource=writer.getCompound("Inventory"); + rawSourceVersion=writer.getInt("DataVersion"); + bridgeBanners=bridgeBannerExpectations(source,rawSource,rawSourceVersion); + } + Map expected=expectedInventory(source,bridgeBanners);Map observed=inventory(actual); CompoundTag recovery=ledger.getCompound("Recoveries").getCompound(id); - if(!recovery.contains("Storage"))verifySlots(source,actual); + if(!recovery.contains("Storage"))verifySlots(source,actual,bridgeBanners); Map recovered=storage(world,recovery); merge(observed,recovered);require(expected.equals(observed),"Inventory conservation mismatch "+uuid+" expected="+expected+" actual="+observed); - verifyRaw(source,actual); + verifyRaw(rawSource,actual,rawSourceVersion); if(recover) { if(!ledger.getCompound("Recoveries").contains(id,10)) {ledger.getCompound("Recoveries").put(id,new CompoundTag());recovery=ledger.getCompound("Recoveries").getCompound(id);} recover(world,body,recovery); require(inventory(save(body)).isEmpty(),"Supported inventory remained after GUI recovery"); require(expected.equals(storage(world,recovery)),"GUI recovery lost or duplicated complete item data"); - verifyRaw(source,save(body)); + verifyRaw(rawSource,save(body),rawSourceVersion); } verifyChest(world,record.getInt("Wave")); verifyBiomes(world,record.getInt("Wave")); } verifyPlayer(server); if(wave==0 || wave<0) verifyVanilla(server); + if(ledger.contains("RemovedRawBackupSha256",7)) { + var backup=new File(folder,"upgrade-removed-original.nbt").toPath(); + require(Arrays.equals(ledger.getByteArray("RemovedRawBackupSha256"),backupDigest(backup)), + "Removed raw-only fixture backup changed before reopen"); + } if(phase.equals("upgrade-final") && ledger.getString("Removed").isEmpty()) { + EntityLootableBody rawOnly=null; for(int i=0;i0 && !FixtureStackData.load(item).isEmpty(); + } + private static CompoundTag expectedCorpseItem(CompoundTag source) { + return expectedCorpseItem(source,sourceVersion); + } + private static CompoundTag expectedCorpseItem(CompoundTag source,int version) { + // Our corpses retain the whole original when the native codec cannot + // carry its old capability payload. Vanilla containers use their + // own codec and are verified against that codec separately. + return source.contains("ForgeCaps")?source.copy():expectedItem(source,version); + } static CompoundTag expectedItem(CompoundTag source,int version) { // Independent vanilla oracle. Production additionally checks conservation, // resolves registries and shields owned entity payloads; this path does not. @@ -118,18 +181,62 @@ static CompoundTag expectedItem(CompoundTag source,int version) { version=705; } CompoundTag result=cyano.lootable.upgrade.NativeNbt.update(vanilla,net.minecraft.util.datafix.fixes.References.ITEM_STACK,input,version,cyano.lootable.upgrade.UpgradePreservation.MINECRAFT_VERSION); - require(Objects.equals(source.get("Count"),result.get("Count")),"Vanilla oracle changed an item count"); + if(source.getInt("Count")<=0)return source.copy(); + require(result.contains("count",3) && source.getInt("Count")==result.getInt("count"),"Vanilla oracle changed an item count"); require(Objects.equals(source.get("Slot"),result.get("Slot")),"Vanilla oracle changed an item slot"); - require(NameLifetimeChecks.same(source.get("ForgeCaps"),result.get("ForgeCaps")),"Vanilla oracle changed opaque capabilities"); - return result; + ItemStack nativeStack=FixtureStackData.load(result); + if(nativeStack.isEmpty())return result; + require(nativeStack.getCount()==result.getInt("count"),"Native oracle codec changed an item count"); + CompoundTag saved=FixtureStackData.save(nativeStack); + if(result.contains("Slot"))saved.put("Slot",result.get("Slot").copy()); + return saved; } - private static void verifySlots(CompoundTag source,CompoundTag actual) { + private static Map bridgeBannerExpectations(CompoundTag seed,CompoundTag writer,int writerVersion) { + Map expected=new HashMap<>(); + ListTag original=seed.getList("Equipment",10),written=writer.getList("Equipment",10); + for(int i=0;i bridgeBanners) { Map slots=new TreeMap<>(); ListTag entries=source.getList("Equipment",10); for(int i=0;i=48 || slots.containsKey(slot))continue; + if(slot<0 || slot>=48 || slots.containsKey(slot))continue; item.remove("Slot");slots.put(slot,item); } Map saved=new TreeMap<>();ListTag entriesNow=actual.getList("Equipment",10); @@ -142,36 +249,76 @@ private static CompoundTag canonicalItem(CompoundTag source) { CompoundTag item=source.copy();String id=item.getString("id"); String[] colours={"white","orange","magenta","light_blue","yellow","lime","pink","gray","light_gray","cyan","purple","blue","brown","green","red","black"}; for(int colour=0;colour map,CompoundTag source,boolean indexed) { - if(!source.contains("id",8) || source.getByte("Count")<=0)return; - CompoundTag item=canonicalItem(source);int count=item.getByte("Count")&255;item.remove("Count");if(!indexed)item.remove("Slot"); + if(!source.contains("id",8) || !source.contains("count",3) || source.getInt("count")<=0)return; + CompoundTag item=canonicalItem(source);int count=item.getInt("count");item.remove("count");if(!indexed)item.remove("Slot"); String key=TypedNbt.canonical(item);map.put(key,map.getOrDefault(key,0)+count); } private static void merge(Map a,Map b){for(Map.Entry item:b.entrySet())a.put(item.getKey(),a.getOrDefault(item.getKey(),0)+item.getValue());} private static Map inventory(CompoundTag body){Map map=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}){ListTag items=body.getList(key,10);for(int i=0;i expectedInventory(CompoundTag source) { - Map expected=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}) {ListTag list=source.getList(key,10);for(int i=0;i expectedInventory(CompoundTag source,Map bridgeBanners) { + Map expected=new TreeMap<>(); + for(String key:new String[]{"Equipment","Aux"}) { + ListTag list=source.getList(key,10); + for(int i=0;i expected=new ArrayList<>(); - for(String key:new String[]{"Equipment","Aux"}) {ListTag list=source.getList(key,10);for(int i=0;i contents(Container inventory,boolean indexed) { + Map map=new TreeMap<>(); + for(int slot=0;slot map,CompoundTag item) { + // Seed manifests describe the original item before a genuine old + // vanilla writer could move the same banner colour into the item ID. + add(map,semanticBannerColour(item),true); + } + private static Map indexed(ListTag list) { + Map map=new TreeMap<>(); + for(int i=0;i contents(Container inventory,boolean indexed) {Map map=new TreeMap<>();for(int slot=0;slot indexed(ListTag list) {Map map=new TreeMap<>();for(int i=0;i storage(ServerLevel world,CompoundTag recovery) {Map map=new TreeMap<>();ListTag list=storageList(recovery);for(int i=0;i recipe=server.getRecipeManager().byKey(new ResourceLocation("minecraft:sugar_from_sugar_cane")).orElse(null); + net.minecraft.world.item.crafting.RecipeHolder recipe=server.getRecipeManager().byKey(new ResourceLocation("minecraft:sugar_from_sugar_cane")).orElse(null); require(recipe!=null && player.getRecipeBook().contains(recipe),"Native sugar recipe conversion lost its unlock"); - net.minecraft.advancements.Advancement advancement=server.getAdvancements().getAdvancement(new ResourceLocation("minecraft:recipes/misc/sugar_from_sugar_cane")); + net.minecraft.advancements.AdvancementHolder advancement=server.getAdvancements().get(new ResourceLocation("minecraft:recipes/misc/sugar_from_sugar_cane")); require(advancement!=null && player.getAdvancements().getOrStartProgress(advancement).isDone(),"Native sugar advancement conversion lost its completion"); player.getAdvancements().save(); } @@ -243,8 +390,10 @@ private static void verifyVanilla(MinecraftServer server) { private static void verifyVillager(CompoundTag source,CompoundTag actual) { CompoundTag expected=cyano.lootable.upgrade.NativeNbt.update(NativeVanillaDataFixer.get(),net.minecraft.util.datafix.fixes.References.ENTITY,source.copy(),sourceVersion,cyano.lootable.upgrade.UpgradePreservation.MINECRAFT_VERSION); require(NameLifetimeChecks.same(expected.get("VillagerData"),actual.get("VillagerData")),"Vanilla villager profession, career or level changed"); - require(indexed(source.getList("Inventory",10)).equals(indexedCurrent(actual.getList("Inventory",10))),"Vanilla villager inventory changed"); + Map observed=new TreeMap<>(); + for(Tag item:actual.getList("Inventory",10))addVanillaSeed(observed,(CompoundTag)item); + require(indexed(source.getList("Inventory",10)).equals(observed),"Vanilla villager inventory changed"); } - private static Map indexedNativeEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;i indexedNativeEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;i CORPSE; public static Item CORPSE_EGG; diff --git a/src/main/java/cyano/lootable/entities/EntityLootableBody.java b/src/main/java/cyano/lootable/entities/EntityLootableBody.java index 316a72b..0d0885f 100644 --- a/src/main/java/cyano/lootable/entities/EntityLootableBody.java +++ b/src/main/java/cyano/lootable/entities/EntityLootableBody.java @@ -9,7 +9,6 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.entity.Mob; -import net.minecraft.world.entity.MobType; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.AttributeInstance; @@ -17,7 +16,6 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.Container; -import net.minecraft.world.item.Item; import net.minecraft.world.item.ShovelItem; import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.Tag; @@ -30,6 +28,9 @@ import net.minecraft.world.damagesource.DamageTypes; import net.minecraft.world.InteractionHand; import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.attributes.Attribute; +import cyano.lootable.network.CorpseProfiles; import net.minecraft.core.NonNullList; import net.minecraft.sounds.SoundEvent; import net.minecraft.resources.ResourceLocation; @@ -43,10 +44,6 @@ import java.util.*; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.RejectedExecutionException; public class EntityLootableBody extends Mob implements Container, net.minecraft.world.MenuProvider{ @@ -73,14 +70,6 @@ public class EntityLootableBody extends Mob implements Container, net.minecraft. private long decayTimestamp; private final AtomicReference gpSwap = new AtomicReference<>(null) ; // for lazy-evaluation of player skins - // Profile services can be slow or unavailable. A bounded daemon worker must - // never hold up client ticks or prevent the default corpse from being drawn. - private static final ThreadPoolExecutor PROFILE_LOOKUPS = new ThreadPoolExecutor(0, 1, 30L, - TimeUnit.SECONDS, new ArrayBlockingQueue(64), task -> { - Thread thread = new Thread(task,"Lootable Bodies skin lookup"); - thread.setDaemon(true); - return thread; - }); private int terminate = -1; private static final int DEATH_COUNTDOWN = 10; @@ -94,7 +83,7 @@ public EntityLootableBody(Level w) { // Keep MobEntity's actual defaults (including follow range 16 and // configured corpse health), rather than the generic attribute defaults. for(AttributeInstance attribute : corpseAttributes()) { - String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute()).toString(); + String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute().value()).toString(); defaultAttributeBases.put(name,attribute.getBaseValue()); defaultAttributeModifiers.put(name,new ArrayList<>(attribute.getModifiers())); } @@ -178,7 +167,7 @@ public void baseTick(){ if(ObjectUtils.notEqual(oldName,nameUpdate)){ oldName = nameUpdate; if(nameUpdate != null && nameUpdate.trim().length() > 0) { - GameProfile gp = new GameProfile(null, nameUpdate); + GameProfile gp = CorpseProfiles.placeholder(nameUpdate); setGameProfile(gp); if((!LootableBodies.useLocalSkin) && isClient) resolveProfile(gp); this.setCustomNameTag(nameUpdate); @@ -296,11 +285,13 @@ private void decayItems(int amount) { private ItemStack decayItem(ItemStack item, int amount) { if(!emptyStack(item) && item.getCount() == 1 - && item.isDamageableItem() - && item.getItem().isDamageable(item)){ + && item.isDamageableItem()){ int durabilityRemaining = item.getMaxDamage() - item.getDamageValue() - 1; if(durabilityRemaining > 0){ - item.hurtAndBreak(Math.min(durabilityRemaining,amount),this,broken->{}); + item.hurtAndBreak(Math.min(durabilityRemaining,amount),getRandom(),null,()->{ + item.shrink(1); + item.setDamageValue(0); + }); } } return item; @@ -361,7 +352,7 @@ public ItemStack mergeItem(int slot, ItemStack stack) { if(amount > 0) this.setItem(slot,stack.split(amount)); return stack.getCount() > 0 ? stack : ItemStack.EMPTY; } else if(current.getCount() < inventorySizeLimit - && ItemStack.isSameItemSameTags(stack,current)){ + && cyano.lootable.ItemStacks.canMerge(stack,current)){ int delta = Math.min(stack.getCount(),inventorySizeLimit - current.getCount()); current.grow(delta); stack.shrink(delta); @@ -401,24 +392,12 @@ public void setGameProfile(GameProfile gp){ public void refreshClientSkin() { GameProfile profile = gpSwap.get(); - if (getCommandSenderWorld().isClientSide && !LootableBodies.useLocalSkin && profile != null && profile.getId() == null) + if (getCommandSenderWorld().isClientSide && !LootableBodies.useLocalSkin && profile != null && !CorpseProfiles.isResolved(profile)) resolveProfile(profile); } private void resolveProfile(GameProfile initial) { - final net.minecraft.server.players.GameProfileCache cache=SkullBlockEntity.profileCache; - final com.mojang.authlib.minecraft.MinecraftSessionService service=SkullBlockEntity.sessionService; - try { - PROFILE_LOOKUPS.execute(() -> { - // Native37's skull helper delegates to another asynchronous worker, - // outside this exception boundary. Keep both native service calls - // on our bounded worker, and let a newer name/profile win the CAS. - GameProfile resolved=cyano.lootable.network.CorpseProfiles.resolve(initial,cache,service); - if(resolved != null)gpSwap.compareAndSet(initial,resolved); - }); - } catch(RejectedExecutionException busy) { - // Keep the already installed default profile when the bounded queue is full. - ModLog.fine("Corpse profile lookup busy; using default skin"); - } + CorpseProfiles.resolve(initial,SkullBlockEntity::fetchGameProfile) + .thenAccept(resolved->gpSwap.compareAndSet(initial,resolved)); } public void setUserName(String name){ this.setCustomNameTag(name); @@ -437,8 +416,10 @@ public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder } private java.util.List corpseAttributes() { java.util.List result=new java.util.ArrayList<>(); - for(net.minecraft.world.entity.ai.attributes.Attribute key:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES) - if(getAttributes().hasAttribute(key))result.add(getAttribute(key)); + for(ResourceLocation name:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKeys()) { + Holder key=net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getHolder(name).orElse(null); + if(key!=null && getAttributes().hasAttribute(key))result.add(getAttribute(key)); + } return result; } private AttributeInstance attributeNamed(String name) { @@ -456,11 +437,13 @@ private AttributeInstance attributeNamed(String name) { case "zombie.spawnReinforcements":name="minecraft:zombie.spawn_reinforcements";break; case "forge.swimSpeed":name="forge:swim_speed";break; case "forge.nameTagDistance":name="forge:nametag_distance";break; - case "forge.entity_gravity":name="forge:entity_gravity";break; + case "forge.entity_gravity": + case "forge:entity_gravity":name="minecraft:generic.gravity";break; + case "forge:step_height_addition":name="minecraft:generic.step_height";break; default:break; } net.minecraft.resources.ResourceLocation id=net.minecraft.resources.ResourceLocation.tryParse(name); - net.minecraft.world.entity.ai.attributes.Attribute key=id==null || !net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.containsKey(id)?null:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getValue(id); + Holder key=id==null?null:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getHolder(id).orElse(null); return key!=null && getAttributes().hasAttribute(key)?getAttribute(key):null; } @@ -547,7 +530,7 @@ protected SoundEvent getAmbientSound() { } @Override - protected ResourceLocation getDefaultLootTable() { + protected net.minecraft.resources.ResourceKey getDefaultLootTable() { return net.minecraft.world.level.storage.loot.BuiltInLootTables.EMPTY; } @@ -585,9 +568,6 @@ public void setCanPickUpLoot(final boolean p_setCanPickUpLoot_1_) { } - @Override public boolean canBreatheUnderwater() { - return true; - } ////////// IInventory ////////// /** @@ -778,11 +758,6 @@ private boolean dropAllItems(){ } - @Override - public MobType getMobType() - { - return MobType.UNDEAD; - } @Override @@ -792,14 +767,14 @@ public void addAdditionalSaveData(final CompoundTag root) { if(sourceVersions.isEmpty()) root.remove(UpgradePreservation.SOURCES); else root.put(UpgradePreservation.SOURCES,sourceVersions.copy()); root.remove("ActiveEffects"); + root.remove("active_effects"); super.addAdditionalSaveData(root); final ListTag equipmentListTag = new ListTag(); for (int i = 0; i < this.getContainerSize(); i++) { ItemStack item = this.getItem(i); if (!emptyStack(item)) { - CompoundTag slotTag = new CompoundTag(); + CompoundTag slotTag = (CompoundTag)item.save(registryAccess()); slotTag.putByte("Slot", (byte)i); - item.save(slotTag); equipmentListTag.add(slotTag); } } @@ -812,9 +787,7 @@ public void addAdditionalSaveData(final CompoundTag root) { while (iter.hasNext()) { ItemStack i = iter.next(); if(emptyStack(i)) continue; - final CompoundTag nbttagcompound1 = new CompoundTag(); - i.save(nbttagcompound1); - nbtauxtaglist.add(nbttagcompound1); + nbtauxtaglist.add(i.save(registryAccess())); } root.put("Aux", nbtauxtaglist); } @@ -942,7 +915,11 @@ public void readAdditionalSaveData(final CompoundTag supplied) { getActiveEffects().clear(); for(AttributeInstance attribute : corpseAttributes()) restoreAttribute(attribute); vanilla.put("Attributes",readAttributes(root.get("Attributes"))); - vanilla.put("ActiveEffects",readEffects(root.get("ActiveEffects"))); + vanilla.remove("ActiveEffects"); + if(root.contains("active_effects") && root.contains("ActiveEffects")) + retainInvalid("ActiveEffects",root.get("ActiveEffects")); + vanilla.put("active_effects",readEffects(root.contains("active_effects") + ?root.get("active_effects"):root.get("ActiveEffects"))); vanilla.putShort("HurtTime",(short)readTimer(root,"HurtTime",Short.MAX_VALUE,0)); // Health must be checked against the loaded, validated effective maximum. vanilla.remove("Health"); @@ -1001,63 +978,80 @@ private void retainInvalid(String source, Tag data) { } private void restoreAttribute(AttributeInstance attribute) { - String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute()).toString(); + String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute().value()).toString(); for(AttributeModifier modifier : new ArrayList<>(attribute.getModifiers())) attribute.removeModifier(modifier); - attribute.setBaseValue(defaultAttributeBases.getOrDefault(name,attribute.getAttribute().getDefaultValue())); + attribute.setBaseValue(defaultAttributeBases.getOrDefault(name,attribute.getAttribute().value().getDefaultValue())); List defaults = defaultAttributeModifiers.get(name); if(defaults != null) for(AttributeModifier modifier : defaults) attribute.addPermanentModifier(modifier); } - private ListTag readEffects(Tag raw) { - ListTag clean = new ListTag(); - if(raw == null) return clean; - if(!isInventoryList(raw)) { retainInvalid("ActiveEffects",raw); return clean; } - Set seen = new HashSet<>(); - ListTag list = (ListTag)raw; - for(int i = 0; i < list.size(); i++) { - CompoundTag record = list.getCompound(i); - Tag rawId = record.get("Id"), rawAmplifier = record.get("Amplifier"); - Long id = integral(rawId); - // IDs are unsigned bytes on disk. Amplifiers are read as signed - // bytes by this engine; preserve all canonical byte encodings. - if(rawId != null && rawId.getId() == 1) id = id & 255L; - Long amplifier = rawAmplifier == null ? Long.valueOf(0) : integral(rawAmplifier); - Long duration = record.contains("Duration") ? integral(record.get("Duration")) : Long.valueOf(0); - MobEffect potion = SavedEffectIdentity.resolve(record,id); - if(potion == null || duration == null || duration < 0 || duration > Integer.MAX_VALUE - || amplifier == null || amplifier > 127 || (amplifier < 0 && rawAmplifier.getId() != 1) - || !seen.add(potion)) { - retainInvalid("ActiveEffect",record); continue; - } - CompoundTag accepted = new CompoundTag(); - SavedEffectIdentity.write(accepted,record,potion); accepted.putInt("Duration",duration.intValue()); - accepted.putByte("Amplifier",amplifier.byteValue()); - boolean changed = SavedEffectIdentity.needsLegacyRecovery(record,id) || !Arrays.asList("Id","forge:id","Duration","Amplifier","Ambient","ShowParticles","ShowIcon","CurativeItems").containsAll(record.getAllKeys()); - // Forge 1.11 persists curative stacks. Preserve their complete custom - // data; an unreadable list uses Forge's defaults and stays recoverable. - if(record.contains("CurativeItems")) { - Tag rawCuratives = record.get("CurativeItems"); - boolean valid = isInventoryList(rawCuratives); - if(valid) { - ListTag items = (ListTag)rawCuratives; - for(int c=0; c= 0 && flag <= 1) accepted.putBoolean(key,flag == 1); - else { accepted.putBoolean(key,fallback); changed = true; } - } - if(changed) retainInvalid("ActiveEffect",record); - clean.add(accepted); - } - return clean; - } + private ListTag readEffects(Tag raw) { + ListTag clean=new ListTag(); + if(raw==null)return clean; + if(!isInventoryList(raw)) { + retainInvalid("ActiveEffects",raw); + return clean; + } + Set> seen=new HashSet<>(); + for(Tag entry:(ListTag)raw) { + CompoundTag record=(CompoundTag)entry; + Tag oldId=record.get("Id"); + Long id=integral(oldId); + if(id!=null && oldId.getId()==1)id=id&255L; + Holder effect=SavedEffectIdentity.resolve(record,id); + boolean legacy=!record.contains("id"); + boolean[] changed={legacy && record.contains("forge:id") + && (record.getString("forge:id").isEmpty() || id==null || id<1 || id>255)}; + CompoundTag accepted=effect==null?null:effectDetails(record,legacy,changed,0); + if(accepted==null || !seen.add(effect)) { + retainInvalid("ActiveEffect",record); + continue; + } + accepted.putString("id",net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.getKey(effect.value()).toString()); + if(changed[0])retainInvalid("ActiveEffect",record); + clean.add(accepted); + } + return clean; + } + + private static CompoundTag effectDetails(CompoundTag raw,boolean legacy,boolean[] changed,int depth) { + if(depth>=16)return null; + String durationKey=legacy?"Duration":"duration", amplifierKey=legacy?"Amplifier":"amplifier"; + String ambientKey=legacy?"Ambient":"ambient", particlesKey=legacy?"ShowParticles":"show_particles"; + String iconKey=legacy?"ShowIcon":"show_icon", hiddenKey=legacy?"HiddenEffect":"hidden_effect"; + Long duration=raw.contains(durationKey)?integral(raw.get(durationKey)):Long.valueOf(0); + Long amplifier=raw.contains(amplifierKey)?integral(raw.get(amplifierKey)):Long.valueOf(0); + if(legacy && amplifier!=null && raw.contains(amplifierKey,1))amplifier=Math.max(0L,amplifier); + int minimumDuration=legacy?0:-1; + int maximumAmplifier=legacy?127:255; + if(duration==null || durationInteger.MAX_VALUE + || amplifier==null || amplifier<0 || amplifier>maximumAmplifier)return null; + CompoundTag result=new CompoundTag(); + result.putInt("duration",duration.intValue()); + result.putInt("amplifier",amplifier.intValue()); + result.putBoolean("ambient",effectFlag(raw,ambientKey,false,changed)); + boolean particles=effectFlag(raw,particlesKey,true,changed); + result.putBoolean("show_particles",particles); + result.putBoolean("show_icon",effectFlag(raw,iconKey,particles,changed)); + if(raw.contains(hiddenKey)) { + CompoundTag hidden=raw.contains(hiddenKey,10)?effectDetails(raw.getCompound(hiddenKey),legacy,changed,depth+1):null; + if(hidden==null)return null; + result.put("hidden_effect",hidden); + } + // The Forge 50 effect codec has no CurativeItems field. Retain that + // legacy payload with its original effect record for recovery. + if(!Set.of("id","Id","forge:id",durationKey,amplifierKey,ambientKey,particlesKey,iconKey,hiddenKey) + .containsAll(raw.getAllKeys()))changed[0]=true; + return result; + } + + private static boolean effectFlag(CompoundTag raw,String key,boolean fallback,boolean[] changed) { + if(!raw.contains(key))return fallback; + Long value=integral(raw.get(key)); + if(value!=null && (value==0 || value==1))return value==1; + changed[0]=true; + return fallback; + } private static Double finiteNumber(Tag raw) { if(!(raw instanceof NumericTag) || raw.getId() < 1 || raw.getId() > 6) return null; @@ -1089,16 +1083,27 @@ private ListTag readAttributes(Tag raw) { ? attributeNamed(record.getString("Name")) : null; Double base = finiteNumber(record.get("Base")); if(attribute == null || base == null) { retainInvalid("Attribute",record); continue; } - String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute()).toString(); + if(record.getString("Name").equals("forge:step_height_addition")) { + Tag modifiers=record.get("Modifiers"); + if(base < -512D || base > 512D || !Set.of("Name","Base","Modifiers").containsAll(record.getAllKeys()) + || (modifiers!=null && (!(modifiers instanceof ListTag listValue) || !listValue.isEmpty()))) { + retainInvalid("Attribute",record);continue; + } + // Forge used to add this delta after reading the normal step + // height. Modern Minecraft stores the complete step height. + double nativeDefault=attribute.getAttribute().value().getDefaultValue(); + base=base==0D?nativeDefault:Math.max(0D,(double)(float)nativeDefault+base); + } + String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute().value()).toString(); // corpse_HP has always allowed 1..Short.MAX_VALUE. Preserve a base // saved under an older config, including vanilla's existing cap. boolean configuredHealth = attribute.getAttribute() == Attributes.MAX_HEALTH && base >= 1D && base <= Short.MAX_VALUE; // Forge15 allowed this base up to Float.MAX_VALUE. Native16 caps // the effective distance at64; keep the valid saved base unchanged. - boolean legacyNameDistance=attribute.getAttribute()==net.minecraftforge.common.ForgeMod.NAMETAG_DISTANCE.get() + boolean legacyNameDistance=attribute.getAttribute().value()==net.minecraftforge.common.ForgeMod.NAMETAG_DISTANCE.get() && base>=0D && base<=Float.MAX_VALUE; - if((attribute.getAttribute().sanitizeValue(base) != base + if((attribute.getAttribute().value().sanitizeValue(base) != base && !configuredHealth && !legacyNameDistance && !base.equals(defaultAttributeBases.get(name))) || !seen.add(name)) { retainInvalid("Attribute",record); continue; } @@ -1194,11 +1199,11 @@ private String readName(CompoundTag root,String key) { String name=root.getString(key); if(key.equals("CustomName") && root.getInt(UpgradePreservation.VERSION)>=3) { try { - net.minecraft.network.chat.Component text=net.minecraft.network.chat.Component.Serializer.fromJson(name); + net.minecraft.network.chat.Component text=net.minecraft.network.chat.Component.Serializer.fromJson(name,registryAccess()); if(text==null){retainInvalid(key,raw);return null;}name=text.getString(); }catch(RuntimeException malformed){retainInvalid(key,raw);return null;} } - String packet=net.minecraft.network.chat.Component.Serializer.toJson(net.minecraft.network.chat.Component.literal(name)); + String packet=net.minecraft.network.chat.Component.Serializer.toJson(net.minecraft.network.chat.Component.literal(name),registryAccess()); if(name.length()<=32767 && name.getBytes(StandardCharsets.UTF_8).length<=32767 && packet.getBytes(StandardCharsets.UTF_8).length<=32767)return name.trim().isEmpty()?"":name; } @@ -1230,47 +1235,36 @@ private static Long integral(Tag data) { return data instanceof NumericTag && data.getId() >= 1 && data.getId() <= 4 ? ((NumericTag)data).getAsLong() : null; } - private ItemStack decodeInventoryItem(CompoundTag raw) { - // Ambiguous fields, unavailable IDs and unhandled capability data stay - // verbatim. Never guess an item, truncate a count or erase custom data. - for(String key : raw.getAllKeys()) if(!Arrays.asList("id","Count","Damage","tag","ForgeCaps","Slot").contains(key)) return null; - if(!raw.contains("id",8)) return null; - Long count = integral(raw.get("Count")); - Long legacyDamage = raw.contains("Damage") ? integral(raw.get("Damage")) : Long.valueOf(0L); - Long damage = raw.getCompound("tag").contains("Damage") ? integral(raw.getCompound("tag").get("Damage")) : legacyDamage; - if(legacyDamage == null || legacyDamage < 0 || legacyDamage > 32767 - || (raw.contains("Damage") && (damage==null || legacyDamage.longValue()!=damage.longValue())))return null; - if(count == null || count <= 0 || count > 127 || damage == null || damage < 0 || damage > 32767) return null; - if((raw.contains("tag") && !raw.contains("tag",10)) || (raw.contains("ForgeCaps") && !raw.contains("ForgeCaps",10))) return null; - try { - ResourceLocation id = new ResourceLocation(raw.getString("id")); - if(!net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(id)) return null; - Item registered = net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(id); - if(registered == null || Item.getId(registered) == 0) return null; - CompoundTag input = raw.copy(); - input.putString("id",id.toString()); - input.putByte("Count",count.byteValue()); - if(raw.contains("Damage"))input.putShort("Damage",legacyDamage.shortValue()); - ItemStack stack = ItemStack.of(input); - if(emptyStack(stack) || stack.getItem() != registered || stack.getCount() != count.intValue() - || stack.getDamageValue() != damage.intValue() || stack.getMaxStackSize() <= 0) return null; - // Forge16 lazily echoes capability NBT before gathering providers. - // Resolve them before comparing: unsupported payloads must remain raw, - // rather than disappearing later during an inventory transaction. - if(raw.contains("ForgeCaps")) - stack.getCapability(net.minecraftforge.common.capabilities.ForgeCapabilities.ITEM_HANDLER); - CompoundTag encoded = stack.save(new CompoundTag()); - Tag expectedTag=raw.get("tag"); - // Native16 writes an explicit zero for missing tool durability. - // Allow only that canonical default, retaining every other byte. - if(registered.isDamageable(stack) && !raw.getCompound("tag").contains("Damage") && damage==0L) { - CompoundTag canonical=raw.getCompound("tag").copy();canonical.putInt("Damage",0);expectedTag=canonical; - } - if(!Objects.equals(expectedTag,encoded.get("tag")) - || !Objects.equals(raw.get("ForgeCaps"),encoded.get("ForgeCaps"))) return null; - return stack; - } catch(RuntimeException unreadable) { return null; } - } + private ItemStack decodeInventoryItem(CompoundTag raw) { + // Only restore a component stack that the live registry can round-trip. + // Unsupported components, legacy capability data and malformed counts + // remain byte-for-byte in the existing Source/Data recovery records. + if(!Set.of("id","count","components","Slot").containsAll(raw.getAllKeys()) + || !raw.contains("id",8) || (raw.contains("components") && !raw.contains("components",10)))return null; + Long count=raw.contains("count")?integral(raw.get("count")):Long.valueOf(1); + ResourceLocation id=ResourceLocation.tryParse(raw.getString("id")); + if(id==null || count==null || count<=0 || count>127)return null; + try { + CompoundTag input=raw.copy(); + input.remove("Slot"); + input.putString("id",id.toString()); + input.putInt("count",count.intValue()); + var ops=registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE); + ItemStack stack=ItemStack.CODEC.parse(ops,input).result().orElse(ItemStack.EMPTY); + if(emptyStack(stack) || stack.getCount()!=count || stack.getMaxStackSize()<=0)return null; + // The component codec accepts a raw value that the gameplay + // accessor silently clamps. Keep that original stack recoverable. + Integer damage=stack.get(net.minecraft.core.component.DataComponents.DAMAGE); + if(damage!=null && damage!=stack.getDamageValue())return null; + CompoundTag encoded=(CompoundTag)stack.save(registryAccess()); + if(!id.toString().equals(encoded.getString("id")) + || encoded.getInt("count")!=count + || !ItemMigration.sameSavedComponents(input.getCompound("components"),encoded.getCompound("components")))return null; + return stack; + } catch(RuntimeException unreadable) { + return null; + } + } private void readInventoryEntry(String source, CompoundTag raw, CompoundTag retained) { CompoundTag context=retained!=null && retained.contains("SourceVersions",10) ? retained.getCompound("SourceVersions") : inventoryVersions; @@ -1300,15 +1294,4 @@ private void readInventoryEntry(String source, CompoundTag raw, CompoundTag reta while(stack.getCount() > 0) auxInventory.addLast(stack.split(Math.min(stack.getCount(),limit))); } - public boolean useThinArms() { - GameProfile p = this.getGameProfile(); - if(p != null){ - if(p.isLegacy()) return false; - UUID uid = p.getId(); - if(uid != null){ - return (uid.hashCode() & 1) == 1; - } - } - return false; - } } diff --git a/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java b/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java index 61b3843..86cdc81 100644 --- a/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java +++ b/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java @@ -1,31 +1,42 @@ package cyano.lootable.entities; +import net.minecraft.core.Holder; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.effect.MobEffect; import net.minecraftforge.registries.ForgeRegistries; -/** Forge40's named identity takes precedence over its legacy byte fallback. */ +/** Resolve named effects and the historical vanilla IDs used by the native fixer. */ final class SavedEffectIdentity { + private static final String[] LEGACY_NAMES={ + "", "speed", "slowness", "haste", "mining_fatigue", "strength", "instant_health", + "instant_damage", "jump_boost", "nausea", "regeneration", "resistance", + "fire_resistance", "water_breathing", "invisibility", "blindness", "night_vision", + "hunger", "weakness", "poison", "wither", "health_boost", "absorption", "saturation", + "glowing", "levitation", "luck", "unluck", "slow_falling", "conduit_power", + "dolphins_grace", "bad_omen", "hero_of_the_village", "darkness" + }; + private SavedEffectIdentity() { } - static MobEffect resolve(CompoundTag record,Long legacyId) { + + static Holder resolve(CompoundTag record,Long legacyId) { + if(record.contains("id")) { + if(record.contains("Id") || record.contains("forge:id"))return null; + return named(record,"id"); + } if(record.contains("forge:id")) { if(!record.contains("forge:id",8))return null; - String name=record.getString("forge:id"); - if(!name.isEmpty()) { - ResourceLocation key=ResourceLocation.tryParse(name); - return key==null?null:ForgeRegistries.MOB_EFFECTS.getValue(key); - } + if(!record.getString("forge:id").isEmpty())return named(record,"forge:id"); } - return legacyId!=null && legacyId>=0 && legacyId<=255?MobEffect.byId(legacyId.intValue()):null; + if(legacyId==null || legacyId<=0 || legacyId>=LEGACY_NAMES.length)return null; + return holder(ResourceLocation.withDefaultNamespace(LEGACY_NAMES[legacyId.intValue()])); } - static boolean needsLegacyRecovery(CompoundTag record,Long legacyId) { - return legacyId==null || legacyId<0 || legacyId>255 || - (record.contains("forge:id") && record.getString("forge:id").isEmpty()); + + private static Holder named(CompoundTag record,String field) { + return record.contains(field,8)?holder(ResourceLocation.tryParse(record.getString(field))):null; } - static void write(CompoundTag accepted,CompoundTag record,MobEffect effect) { - accepted.putInt("Id",MobEffect.getId(effect)); - if(record.contains("forge:id",8) && !record.getString("forge:id").isEmpty()) - accepted.putString("forge:id",ForgeRegistries.MOB_EFFECTS.getKey(effect).toString()); + + private static Holder holder(ResourceLocation id) { + return id==null?null:ForgeRegistries.MOB_EFFECTS.getHolder(id).orElse(null); } } diff --git a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java index 8f2ac39..33c9f21 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java @@ -64,14 +64,14 @@ private boolean validPending(){ else if(property.getType()=='D' || property.getType()=='I')try{double number=Double.parseDouble(value);if(!Double.isFinite(number)||numberproperty.getMax())return false;}catch(NumberFormatException error){return false;} }return true; } - @Override public void tick(){for(EditBox field:fields.values())field.tick();} @Override public void render(GuiGraphics graphics,int mouseX,int mouseY,float partialTicks){ - renderBackground(graphics);graphics.drawCenteredString(font,LootableBodies.NAME,width/2,12,0xffffff); + super.render(graphics,mouseX,mouseY,partialTicks); + graphics.drawCenteredString(font,LootableBodies.NAME,width/2,12,0xffffff); graphics.drawCenteredString(font,I18n.get(group==null?"lootablebodies.config.local_file":"lootablebodies.config."+group),width/2,28,0xcccccc); for(Map.Entry row:rows.entrySet()){ String text=I18n.get(property(row.getKey()).getLanguageKey());graphics.drawString(font,font.plainSubstrByWidth(text,205),width/2-155,row.getValue()+6,0xffffff); } - super.render(graphics,mouseX,mouseY,partialTicks);for(EditBox field:fields.values())field.render(graphics,mouseX,mouseY,partialTicks); + for(EditBox field:fields.values())field.render(graphics,mouseX,mouseY,partialTicks); for(Map.Entry row:rows.entrySet())if(mouseX>=width/2-155 && mouseX=row.getValue() && mouseY=width/2-155 && mouseX=95 && mouseY<115) diff --git a/src/main/java/cyano/lootable/graphics/CorpseContainer.java b/src/main/java/cyano/lootable/graphics/CorpseContainer.java index f51229e..13df3ef 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseContainer.java @@ -212,7 +212,7 @@ private void storeInPlayerInventory(Inventory inventory, ItemStack remainder) { private boolean canStore(Inventory inventory, int slot, ItemStack incoming) { ItemStack current = inventory.getItem(slot); - return !emptyStack(current) && current.isStackable() && ItemStack.isSameItemSameTags(current,incoming) + return !emptyStack(current) && current.isStackable() && cyano.lootable.ItemStacks.canMerge(current,incoming) && current.getCount() < Math.min(current.getMaxStackSize(),inventory.getMaxStackSize()); } @@ -253,7 +253,7 @@ protected boolean moveItemStackTo(ItemStack stack, int start, int end, boolean r ItemStack current = slot.getItem(); int limit = Math.min(slot.getMaxStackSize(stack),stack.getMaxStackSize()); if(pass == 0 && stack.isStackable() && !emptyStack(current) - && ItemStack.isSameItemSameTags(stack,current)) { + && cyano.lootable.ItemStacks.canMerge(stack,current)) { int amount = Math.min(stack.getCount(),Math.max(0,limit - current.getCount())); if(amount > 0) { current.grow(amount); diff --git a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java index 5c95eeb..2b90a5b 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java @@ -1,11 +1,9 @@ package cyano.lootable.graphics; import com.mojang.authlib.GameProfile; -import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.blaze3d.vertex.PoseStack; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; -import java.util.Map; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.MultiBufferSource; import com.mojang.math.Axis; @@ -20,6 +18,7 @@ import net.minecraft.client.model.HumanoidModel; import net.minecraft.client.model.PlayerModel; import net.minecraft.client.resources.DefaultPlayerSkin; +import net.minecraft.client.resources.PlayerSkin; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -49,28 +48,27 @@ public CorpseRenderer(EntityRendererProvider.Context context) { @Override public ResourceLocation getTextureLocation(EntityLootableBody entity) { if (LootableBodies.useLocalSkin) return localSkin; - GameProfile profile = entity.getGameProfile(); - return profile != null && profile.getId() != null ? getSkin(profile) : DefaultPlayerSkin.getDefaultSkin(); + return getSkin(entity.getGameProfile()); } public static ResourceLocation getSkin(GameProfile profile) { - ResourceLocation fallback = profile == null || profile.getId() == null - ? DefaultPlayerSkin.getDefaultSkin() : DefaultPlayerSkin.getDefaultSkin(profile.getId()); - if (profile == null || profile.getId() == null) return fallback; + PlayerSkin skin=playerSkin(profile); + return skin==null?DefaultPlayerSkin.getDefaultTexture():skin.texture(); + } + + private static PlayerSkin playerSkin(GameProfile profile) { + if (!cyano.lootable.network.CorpseProfiles.isResolved(profile)) return null; try { - Minecraft minecraft = Minecraft.getInstance(); - Map textures = minecraft.getSkinManager().getInsecureSkinInformation(profile); - return textures.containsKey(MinecraftProfileTexture.Type.SKIN) - ? minecraft.getSkinManager().registerTexture(textures.get(MinecraftProfileTexture.Type.SKIN), MinecraftProfileTexture.Type.SKIN) - : fallback; + return Minecraft.getInstance().getSkinManager().getInsecureSkin(profile); } catch (RuntimeException unavailable) { - return fallback; + return DefaultPlayerSkin.get(profile); } } @Override public void render(EntityLootableBody entity, float yaw, float partialTicks, PoseStack matrices, MultiBufferSource buffers, int light) { - setModel(entity.useThinArms()); + PlayerSkin skin=LootableBodies.useLocalSkin?null:playerSkin(entity.getGameProfile()); + setModel(skin!=null && skin.model()==PlayerSkin.Model.SLIM); try { super.render(entity, 0F, partialTicks, matrices, buffers, light); } finally { @@ -79,11 +77,11 @@ public static ResourceLocation getSkin(GameProfile profile) { } @Override protected void setupRotations(EntityLootableBody entity, PoseStack matrices, - float age, float yaw, float partialTicks) { + float age, float yaw, float partialTicks, float scale) { matrices.mulPose(Axis.XP.rotationDegrees(90F)); matrices.mulPose(Axis.ZP.rotationDegrees(entity.getRotation())); matrices.translate(0D, -0.85D, -0.125D); - super.setupRotations(entity, matrices, 0F, 0F, partialTicks); + super.setupRotations(entity, matrices, 0F, 0F, partialTicks, scale); } @Override protected float getAttackAnim(EntityLootableBody entity, float partialTicks) { return 0F; } @@ -92,8 +90,8 @@ public static ResourceLocation getSkin(GameProfile profile) { return LootableBodies.displayNameTag && super.shouldShowName(entity); } @Override protected void renderNameTag(EntityLootableBody entity, net.minecraft.network.chat.Component name, PoseStack matrices, - MultiBufferSource buffers, int light) { - if (LootableBodies.displayNameTag) super.renderNameTag(entity, name, matrices, buffers, light); + MultiBufferSource buffers, int light, float partialTicks) { + if (LootableBodies.displayNameTag) super.renderNameTag(entity, name, matrices, buffers, light, partialTicks); } private static final class FrozenPlayerModel extends PlayerModel { diff --git a/src/main/java/cyano/lootable/graphics/GUIHandler.java b/src/main/java/cyano/lootable/graphics/GUIHandler.java index 72dd03a..b90e8f2 100644 --- a/src/main/java/cyano/lootable/graphics/GUIHandler.java +++ b/src/main/java/cyano/lootable/graphics/GUIHandler.java @@ -5,13 +5,12 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraftforge.api.distmarker.*; -import net.minecraftforge.network.NetworkHooks; /** Native Forge container opening, with the historical local helper kept internal. */ public final class GUIHandler { private static final GUIHandler INSTANCE=new GUIHandler(); private GUIHandler() { } public static GUIHandler getInstance(){return INSTANCE;} - public static void open(Player player,EntityLootableBody corpse){if(player instanceof ServerPlayer)NetworkHooks.openScreen((ServerPlayer)player,corpse,buf->buf.writeVarInt(corpse.getId()));} + public static void open(Player player,EntityLootableBody corpse){if(player instanceof ServerPlayer server)server.openMenu(corpse,buf->buf.writeVarInt(corpse.getId()));} public Object getServerGuiElement(int id,Player player,Level world,int x,int y,int z){Entity entity=world.getEntity(x);return id==0 && entity instanceof EntityLootableBody?new CorpseContainer(player.getInventory(),(EntityLootableBody)entity):null;} @OnlyIn(Dist.CLIENT) public Object getClientGuiElement(int id,Player player,Level world,int x,int y,int z){Entity entity=world.getEntity(x);return id==0 && entity instanceof EntityLootableBody?new CorpseGUIContainer(player.getInventory(),(EntityLootableBody)entity):null;} } diff --git a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java index 66bc010..92dde9c 100644 --- a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java +++ b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java @@ -2,6 +2,7 @@ import io.netty.channel.Channel; import java.util.concurrent.RejectedExecutionException; +import java.util.function.Supplier; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.ConnectScreen; import net.minecraft.network.ConnectionProtocol; @@ -20,12 +21,15 @@ public final class ClientLoginReadGuard { Minecraft minecraft=Minecraft.getInstance(); if(!(minecraft.screen instanceof ConnectScreen))return; Connection manager=ObfuscationReflectionHelper.getPrivateValue( - ConnectScreen.class,(ConnectScreen)minecraft.screen,"f_95684_"); - if(manager!=null)resumeReads(manager.channel()); + ConnectScreen.class,(ConnectScreen)minecraft.screen,"connection"); + if(manager!=null)resumeReads(manager.channel(),()->{ + var protocol=manager.getInboundProtocolInfo(); + return protocol==null?null:protocol.id(); + }); } /** Keep intentional pauses and all non-login connections untouched. */ - public static void resumeReads(Channel channel) { + public static void resumeReads(Channel channel, Supplier currentProtocol) { if(channel==null || !channel.isActive() || !channel.isRegistered())return; try { channel.eventLoop().execute(()-> { @@ -33,7 +37,7 @@ public static void resumeReads(Channel channel) { // A queued clearReadPending from the connector thread can have // removed OP_READ even though autoRead has already become true. if(channel.isActive() && channel.config().isAutoRead() - && channel.attr(Connection.ATTRIBUTE_PROTOCOL).get()==ConnectionProtocol.LOGIN) + && currentProtocol.get()==ConnectionProtocol.LOGIN) channel.read(); }); }catch(RejectedExecutionException closed) { diff --git a/src/main/java/cyano/lootable/network/CorpseProfiles.java b/src/main/java/cyano/lootable/network/CorpseProfiles.java index e9f13bc..aae6de8 100644 --- a/src/main/java/cyano/lootable/network/CorpseProfiles.java +++ b/src/main/java/cyano/lootable/network/CorpseProfiles.java @@ -1,26 +1,32 @@ package cyano.lootable.network; import com.mojang.authlib.GameProfile; -import com.mojang.authlib.minecraft.MinecraftSessionService; -import net.minecraft.server.players.GameProfileCache; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; -/** Synchronous native services; callers own the bounded background worker. */ +/** Uses the native profile cache without waiting on the client thread. */ public final class CorpseProfiles { + private static final UUID UNRESOLVED = new UUID(0,0); + private CorpseProfiles() { } - public static GameProfile resolve(GameProfile initial,GameProfileCache cache,MinecraftSessionService service) { - if(initial==null || cache==null || service==null)return initial; + + public static GameProfile placeholder(String name) { + return new GameProfile(UNRESOLVED,name); + } + + public static boolean isResolved(GameProfile profile) { + return profile!=null && !UNRESOLVED.equals(profile.getId()); + } + + public static CompletableFuture resolve(GameProfile initial, + Function>> lookup) { try { - GameProfile cached=cache.get(initial.getName()).orElse(initial); - if(cached.getId()==null)return initial; - GameProfile resolved=new GameProfile(cached.getId(),cached.getName()); - resolved.getProperties().putAll(cached.getProperties()); - if(!resolved.getProperties().containsKey("textures"))resolved=service.fillProfileProperties(resolved,true); - if(resolved==null)return initial; - cache.add(resolved); - return resolved; - }catch(RuntimeException unavailable) { - cyano.lootable.ModLog.fine("Corpse profile lookup unavailable; using default skin"); - return initial; + return lookup.apply(initial.getName()).handle((value,failure) -> + failure==null?value.filter(CorpseProfiles::isResolved).orElse(initial):initial); + } catch (RuntimeException unavailable) { + return CompletableFuture.completedFuture(initial); } } } diff --git a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java index 2bd2fca..a088c9c 100644 --- a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java +++ b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java @@ -39,20 +39,22 @@ private static void verifyAttachment(Class reader) { } } /** Called before Forge reads extra data or native level conversion. */ - public static void beforeRead(LevelStorageSource.LevelStorageAccess save) { + public static void beforeRead(LevelStorageSource.LevelStorageAccess save,boolean fallback) { try { if(!installed)throw new IllegalStateException("Early preservation was not installed before world loading"); - Path world=save.getLevelPath(LevelResource.ROOT),header=world.resolve("level.dat"); - if(!Files.isRegularFile(header) && Files.isRegularFile(world.resolve("level.dat_old")))header=world.resolve("level.dat_old"); - if(Files.isRegularFile(header)) { - CompoundTag root; - try(DataInputStream input=new DataInputStream(new GZIPInputStream(Files.newInputStream(header)))) { - root=NbtIo.read(input,new NbtAccounter(64L*1024*1024)); - } - Tag original=root.get("lootablebodies"); - if(original!=null)metadata.put(save,original.copy());else metadata.remove(save); - } else metadata.remove(save); - UpgradePreservation.prepareEarly(world); + Path world=save.getLevelPath(LevelResource.ROOT); + Path header=world.resolve(fallback?"level.dat_old":"level.dat"); + if(!Files.isRegularFile(header)) { + metadata.remove(save); + return; // The native reader selects or rejects the fallback. + } + CompoundTag root; + try(DataInputStream input=new DataInputStream(new GZIPInputStream(Files.newInputStream(header)))) { + root=NbtIo.read(input,NbtAccounter.create(64L*1024*1024)); + } + Tag original=root.get("lootablebodies"); + if(original!=null)metadata.put(save,original.copy());else metadata.remove(save); + UpgradePreservation.prepareEarly(world,header); } catch(IOException | RuntimeException unsafe){throw new UnsafeWorldUpgrade(unsafe);} } /** Restore only this mod's original field on the same native save access. */ diff --git a/src/main/java/cyano/lootable/upgrade/ItemMigration.java b/src/main/java/cyano/lootable/upgrade/ItemMigration.java index 05070b0..eef3292 100644 --- a/src/main/java/cyano/lootable/upgrade/ItemMigration.java +++ b/src/main/java/cyano/lootable/upgrade/ItemMigration.java @@ -12,13 +12,42 @@ public final class ItemMigration { private static DataFixer pipeline; private ItemMigration() { } static void register(DataFixer fixer){pipeline=fixer;} - /** Null means that conversion is ambiguous; retain the original raw record. */ + // The native componentization fix is in schema 3818.5. Validate the + // legacy item format through the last complete schema before that change. + private static final int LAST_LEGACY_ITEM_VERSION=3817; + + /** Null means the caller retains the original raw record for recovery. */ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { if(versions==null || versions.isEmpty())return raw.copy(); - CompoundTag fixed=convertNbt(raw,versions); - return fixed!=null && knownNestedItems(fixed)?fixed:null; + if(versions.getBoolean("Ambiguous")) { + CompoundTag agreed=null; + for(int candidate:versions.getIntArray("CandidateDataVersions")) { + CompoundTag context=new CompoundTag(); + context.putInt("DataVersion",candidate); + CompoundTag fixed=convert(raw,context); + if(fixed==null || (agreed!=null && !agreed.equals(fixed)))return null; + agreed=fixed; + } + return agreed; + } + if(!versions.contains("DataVersion",3))return null; + int source=versions.getInt("DataVersion"); + if(source==UpgradePreservation.MINECRAFT_VERSION)return raw.copy(); + if(source<512 || source>LAST_LEGACY_ITEM_VERSION || pipeline==null)return null; + CompoundTag legacy=convertNbt(raw,versions); + if(legacy==null || !supportedLegacyStack(legacy))return null; + try { + CompoundTag input=legacy.copy(); + input.remove("Damage"); + CompoundTag fixed=NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK, + input,LAST_LEGACY_ITEM_VERSION,UpgradePreservation.MINECRAFT_VERSION); + return componentsSurvive(legacy,fixed,true)?fixed:null; + } catch(RuntimeException invalid) { + return null; + } } - /** NBT conversion is independent of live registry resolution. */ + + /** Validate legacy flattening without running the later component schema. */ static CompoundTag convertNbt(CompoundTag raw,CompoundTag versions) { if(versions==null || versions.isEmpty()) return raw.copy(); if(versions.getBoolean("Ambiguous")) { @@ -33,9 +62,9 @@ static CompoundTag convertNbt(CompoundTag raw,CompoundTag versions) { } if(pipeline==null || !versions.contains("DataVersion",3))return null; int source=versions.getInt("DataVersion"); - if(source<512 || source>UpgradePreservation.MINECRAFT_VERSION || integral(raw.get("Count"))==null)return null; + if(source<512 || source>LAST_LEGACY_ITEM_VERSION || integral(raw.get("Count"))==null)return null; try { - CompoundTag item=cyano.lootable.upgrade.NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK,raw.copy(),source,UpgradePreservation.MINECRAFT_VERSION); + CompoundTag item=cyano.lootable.upgrade.NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK,raw.copy(),source,LAST_LEGACY_ITEM_VERSION); if(!Objects.equals(integral(raw.get("Count")),integral(item.get("Count"))) || !Objects.equals(raw.get("Slot"),item.get("Slot")) || !Objects.equals(raw.get("ForgeCaps"),item.get("ForgeCaps")))return null; if(!flatteningSurvives(raw,item,source))return null; for(String key:raw.getAllKeys())if(!Arrays.asList("id","Count","Damage","Slot","tag").contains(key) && !Objects.equals(raw.get(key),item.get(key)))return null; @@ -85,20 +114,164 @@ private static boolean enchantmentsSurvive(Tag old,Tag next) { private static Long integral(Tag nbt) { return nbt instanceof NumericTag && nbt.getId()>=1 && nbt.getId()<=4 ? ((NumericTag)nbt).getAsLong() : null; } - private static boolean knownNestedItems(Tag tag) { - if(tag instanceof CompoundTag) { - CompoundTag compound=(CompoundTag)tag; - if(compound.contains("id") && compound.contains("Count")) { - if(!compound.contains("id",8)) return false; - ResourceLocation id=new ResourceLocation(compound.getString("id")); - if(!net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(id) || Item.getId(net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(id))==0) return false; + private static boolean supportedLegacyStack(CompoundTag item) { + if(!Set.of("id","Count","Damage","Slot","tag").containsAll(item.getAllKeys()) + || !item.contains("id",8) || (item.contains("tag") && !item.contains("tag",10)))return false; + Long count=integral(item.get("Count")); + if(count==null || count<=0 || count>127)return false; + if(item.contains("Damage")) { + Long old=integral(item.get("Damage")); + CompoundTag tag=item.getCompound("tag"); + Long damage=tag.contains("Damage")?integral(tag.get("Damage")):Long.valueOf(0); + if(!Objects.equals(old,damage))return false; + } + return true; + } + + private static boolean componentsSurvive(CompoundTag legacy,CompoundTag fixed,boolean outerSlot) { + if(!supportedLegacyStack(legacy) + || !Objects.equals(integral(legacy.get("Count")),integral(fixed.get("count"))) + || !Objects.equals(ResourceLocation.tryParse(legacy.getString("id")),ResourceLocation.tryParse(fixed.getString("id"))) + || (outerSlot && !Objects.equals(legacy.get("Slot"),fixed.get("Slot"))))return false; + CompoundTag tag=legacy.getCompound("tag"), components=fixed.getCompound("components"); + CompoundTag custom=components.getCompound("minecraft:custom_data"); + for(String key:tag.getAllKeys()) { + Tag value=tag.get(key); + if(value.equals(custom.get(key)))continue; + boolean kept=switch(key) { + case "Damage" -> sameDamage(value,components.get("minecraft:damage")); + case "Enchantments" -> enchantmentLevelsSurvive(value,components.get("minecraft:enchantments"),components); + case "StoredEnchantments" -> enchantmentLevelsSurvive(value,components.get("minecraft:stored_enchantments"),components); + case "display" -> displaySurvives(value,components,custom.getCompound("display")); + case "BlockEntityTag" -> blockEntitySurvives(value,components); + case "EntityTag" -> value.equals(components.get("minecraft:entity_data")); + case "Potion" -> potionSurvives(value,components.getCompound("minecraft:potion_contents")); + case "RepairCost" -> sameDamage(value,components.get("minecraft:repair_cost")); + case "CustomModelData" -> value.equals(components.get("minecraft:custom_model_data")); + default -> false; + }; + if(!kept)return false; + } + return true; + } + + public static boolean sameSavedComponents(CompoundTag expected,CompoundTag encoded) { + if(expected.equals(encoded))return true; + if(!expected.getAllKeys().equals(encoded.getAllKeys()))return false; + for(String key:expected.getAllKeys()) { + Tag before=expected.get(key), after=encoded.get(key); + if(before.equals(after))continue; + boolean same=switch(key) { + case "minecraft:custom_name","minecraft:item_name" -> sameText(before,after); + case "minecraft:lore" -> sameLore(before,after); + default -> false; + }; + if(!same)return false; + } + return true; + } + + private static boolean sameLore(Tag before,Tag after) { + if(!(before instanceof ListTag a) || !(after instanceof ListTag b) || a.size()!=b.size())return false; + for(int i=0;i=0 && value<=32767 + && (output==null?value==0:Objects.equals(value,integral(output))); + } + + private static boolean enchantmentLevelsSurvive(Tag raw,Tag fixed,CompoundTag components) { + if(!(raw instanceof ListTag list))return false; + if(list.isEmpty())return components.getBoolean("minecraft:enchantment_glint_override"); + if(!(fixed instanceof CompoundTag compound))return false; + CompoundTag levels=compound.getCompound("levels"); + if(levels.size()!=list.size())return false; + Set seen=new HashSet<>(); + for(Tag element:list) { + if(!(element instanceof CompoundTag enchantment) + || !Set.of("id","lvl").containsAll(enchantment.getAllKeys()) + || !enchantment.contains("id",8))return false; + String name=enchantment.getString("id"); + Long level=integral(enchantment.get("lvl")); + if(!seen.add(name) || level==null || level<0 || level>255 + || !Objects.equals(level,integral(levels.get(name))))return false; + } + return true; + } + + private static boolean displaySurvives(Tag raw,CompoundTag components,CompoundTag custom) { + if(!(raw instanceof CompoundTag display))return false; + for(String key:display.getAllKeys()) { + Tag value=display.get(key); + if(value.equals(custom.get(key)))continue; + String component=switch(key) { + case "Name" -> "minecraft:custom_name"; + case "Lore" -> "minecraft:lore"; + default -> null; + }; + if(component==null || !value.equals(components.get(component)))return false; + } + return true; + } + + private static boolean blockEntitySurvives(Tag raw,CompoundTag components) { + if(!(raw instanceof CompoundTag block))return false; + CompoundTag remainder=components.getCompound("minecraft:block_entity_data"); + ListTag container=components.getList("minecraft:container",10); + for(String key:block.getAllKeys()) { + Tag value=block.get(key); + if(value.equals(remainder.get(key)))continue; + if(key.equals("Base")) { + Long colour=integral(value); + if(colour==null || colour<0 || colour>15 + || !components.getString("minecraft:base_color").equals(net.minecraft.world.item.DyeColor.byId(colour.intValue()).getName())) + return false; + continue; } - for(String key:compound.getAllKeys()) if(!key.equals("ForgeCaps") && !knownNestedItems(compound.get(key))) return false; - } else if(tag instanceof ListTag) { - ListTag list=(ListTag)tag;for(int i=0;i slots=new HashSet<>(); + for(int i=0;i255 || !slots.add(slot) + || !Objects.equals(slot,integral(fixed.get("slot"))) + || !componentsSurvive(legacy,fixed.getCompound("item"),false))return false; + } + } else if(!key.equals("id") || !remainder.isEmpty() + || (!block.getString("id").startsWith("minecraft:") && !block.getString("id").equals("Chest")))return false; } return true; } + private static boolean nestedInventoriesSurvive(Tag before,Tag after) { if(before instanceof CompoundTag) { if(!(after instanceof CompoundTag)) return false; diff --git a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java index dc34566..d14281f 100644 --- a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java +++ b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java @@ -1,6 +1,7 @@ package cyano.lootable.upgrade; import cyano.lootable.LootableBodies; +import java.util.Set; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvent; @@ -30,7 +31,9 @@ public static ResourceLocation itemReplacement(ResourceLocation old) { case "cactus_green": return new ResourceLocation("minecraft:green_dye"); case "dandelion_yellow": return new ResourceLocation("minecraft:yellow_dye"); case "grass_path": return new ResourceLocation("minecraft:dirt_path"); + case "grass": return ResourceLocation.withDefaultNamespace("short_grass"); case "zombie_pigman_spawn_egg":return new ResourceLocation("minecraft:zombified_piglin_spawn_egg"); + case "scute": return ResourceLocation.withDefaultNamespace("turtle_scute"); case "pottery_shard_archer": return new ResourceLocation("minecraft:archer_pottery_sherd"); case "pottery_shard_arms_up": return new ResourceLocation("minecraft:arms_up_pottery_sherd"); case "pottery_shard_prize": return new ResourceLocation("minecraft:prize_pottery_sherd"); @@ -44,6 +47,7 @@ public static ResourceLocation blockReplacement(ResourceLocation old) { case "sign": return new ResourceLocation("minecraft:oak_sign"); case "wall_sign": return new ResourceLocation("minecraft:oak_wall_sign"); case "grass_path": return new ResourceLocation("minecraft:dirt_path"); + case "grass": return ResourceLocation.withDefaultNamespace("short_grass"); default: return null; } } @@ -51,6 +55,12 @@ public static ResourceLocation blockReplacement(ResourceLocation old) { private static final ResourceLocation XP_SOUND=new ResourceLocation("minecraft:entity.experience_orb.pickup"); private static final ResourceLocation OLD_JUNGLE_AND_FOREST_MUSIC = new ResourceLocation("minecraft:music.overworld.jungle_and_forest"); + private static final Set RETIRED_PARROT_SOUNDS = Set.of( + ResourceLocation.withDefaultNamespace("entity.parrot.imitate.enderman"), + ResourceLocation.withDefaultNamespace("entity.parrot.imitate.panda"), + ResourceLocation.withDefaultNamespace("entity.parrot.imitate.polar_bear"), + ResourceLocation.withDefaultNamespace("entity.parrot.imitate.wolf"), + ResourceLocation.withDefaultNamespace("entity.parrot.imitate.zombie_pigman")); private LegacyRegistryMappings() { } static T requireMappingTarget(java.util.function.Supplier lookup) { try {T target=lookup.get();if(target!=null)return target;} @@ -100,6 +110,32 @@ public static void retiredMemories(RegisterEvent event) { if (replacement != null) mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.BLOCKS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.BLOCKS.getValue(replacement))); } } + static ResourceLocation enchantmentReplacement(ResourceLocation old) { + return old.equals(ResourceLocation.withDefaultNamespace("sweeping")) + ?ResourceLocation.withDefaultNamespace("sweeping_edge"):null; + } + + @SubscribeEvent public static void enchantments(MissingMappingsEvent event) { + var registry=net.minecraftforge.registries.ForgeRegistries.ENCHANTMENTS; + for(var mapping:event.getAllMappings(Registries.ENCHANTMENT)) { + ResourceLocation replacement=enchantmentReplacement(mapping.getKey()); + if(replacement!=null)mapping.remap(requireMappingTarget(registry.containsKey(replacement),()->registry.getValue(replacement))); + } + } + + static boolean retiredPotion(ResourceLocation key) { + return key.equals(ResourceLocation.withDefaultNamespace("empty")); + } + + @SubscribeEvent public static void potions(MissingMappingsEvent event) { + for(var mapping:event.getAllMappings(Registries.POTION)) { + // The native item fixer represents the old empty sentinel as an + // absent potion holder. A real potion here would change its contents. + if(retiredPotion(mapping.getKey()) + && net.minecraft.world.item.alchemy.PotionContents.EMPTY.potion().isEmpty())mapping.ignore(); + } + } + @SubscribeEvent public static void biomes(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping mapping:event.getAllMappings(Registries.BIOME)) { if(!mapping.getKey().getNamespace().equals("minecraft"))continue; @@ -139,6 +175,12 @@ private static final class VanillaBiomes { @SubscribeEvent public static void sounds(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping mapping:event.getAllMappings(Registries.SOUND_EVENT)) { + if (RETIRED_PARROT_SOUNDS.contains(mapping.getKey()) + && !BuiltInRegistries.SOUND_EVENT.containsKey(mapping.getKey())) { + // Old saves retain these numeric bindings after vanilla removed the imitations. + mapping.ignore(); + continue; + } if (mapping.getKey().equals(OLD_JUNGLE_AND_FOREST_MUSIC)) { // Vanilla 1.20 defines separate music for the two biomes. // Retire only the obsolete Forge numeric binding. diff --git a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java index 21e2b71..b15dae7 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java +++ b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java @@ -10,7 +10,7 @@ import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.util.datafix.fixes.References; -import net.minecraft.network.chat.Component; +import com.google.gson.JsonPrimitive; /** Shields owned payloads while vanilla converts its own chunk and item schemas. */ final class NativeSafeFixer implements DataFixer { @@ -252,7 +252,7 @@ private CompoundTag sourceContext(CompoundTag entity,int version,int x,int z,boo }catch(IOException damaged){throw new IllegalStateException("Corpse preservation could not be verified",damaged);} } CompoundTag context=new CompoundTag();int marker=entity.getInt(UpgradePreservation.VERSION); - context.putInt("DataVersion",entity.getString("id").equals("lootablebodies.Corpse")?512:marker==1?922:marker==2?1343:marker==3?1631:marker==4?1976:marker==5?2230:marker==6?2586:marker==7?2730:marker==8?2975:marker==9?3337:version);return context; + context.putInt("DataVersion",entity.getString("id").equals("lootablebodies.Corpse")?512:marker==1?922:marker==2?1343:marker==3?1631:marker==4?1976:marker==5?2230:marker==6?2586:marker==7?2730:marker==8?2975:marker==9?3337:marker==10?3465:version);return context; } static CompoundTag stageCorpse(CompoundTag raw,CompoundTag context) { if(raw.contains(UpgradePreservation.VERSION,3) && raw.getInt(UpgradePreservation.VERSION)>UpgradePreservation.DATA_VERSION)throw new IllegalStateException("This corpse was saved by a newer Lootable Bodies format"); @@ -282,7 +282,7 @@ static CompoundTag stageCorpse(CompoundTag raw,CompoundTag context) { // Anvil may save this compound before an entity reader runs. safe.remove("LootableBodiesUpgradePayload");safe.remove("LootableBodiesInventorySource"); if(safe.contains("CustomName",8) && context.getInt("DataVersion")<1514) - safe.putString("CustomName",Component.Serializer.toJson(Component.literal(safe.getString("CustomName")))); + safe.putString("CustomName",new JsonPrimitive(safe.getString("CustomName")).toString()); return safe; } private static boolean list(Tag nbt){return nbt instanceof ListTag && (((ListTag)nbt).isEmpty() || ((ListTag)nbt).getElementType()==10);} @@ -316,7 +316,16 @@ private Tag restore(Tag value,Tokens tokens) { String id=nbt.getString(tokens.key);Protected original=tokens.values.get(id);if(original==null)throw new IllegalStateException("Unknown preservation token"); tokens.restored.merge(id,1,Integer::sum); if(!original.egg)return original.data.copy(); - nbt.remove(tokens.key);CompoundTag tag=nbt.getCompound("tag");tag.put("EntityTag",original.data.copy());nbt.put("tag",tag); + nbt.remove(tokens.key); + if(nbt.contains("count",99)) { + CompoundTag components=nbt.getCompound("components"); + components.put("minecraft:entity_data",original.data.copy()); + nbt.put("components",components); + } else { + CompoundTag tag=nbt.getCompound("tag"); + tag.put("EntityTag",original.data.copy()); + nbt.put("tag",tag); + } } for(String key:new ArrayList<>(nbt.getAllKeys()))nbt.put(key,restore(nbt.get(key),tokens));return nbt; } diff --git a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java index 0af6dc8..e8b79ea 100644 --- a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java +++ b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java @@ -27,8 +27,11 @@ public final class UpgradeJournal { private final Map active=new HashMap<>(); private CompoundTag worldContext=new CompoundTag(); - private UpgradeJournal(Path world) throws IOException { + private final Path levelHeader; + + private UpgradeJournal(Path world,Path levelHeader) throws IOException { this.world=world.toRealPath(); + this.levelHeader=levelHeader; directory=this.world.resolve("data/lootablebodies-upgrade"); snapshots=directory.resolve("snapshots"); chunks=directory.resolve("chunks"); @@ -36,8 +39,13 @@ private UpgradeJournal(Path world) throws IOException { /** Complete preservation before Minecraft opens any existing chunk. */ public static UpgradeJournal scan(Path world) throws IOException { + Path header=world.resolve("level.dat"); + return scan(world,Files.isRegularFile(header)?header:world.resolve("level.dat_old")); + } + + static UpgradeJournal scan(Path world,Path levelHeader)throws IOException { Files.createDirectories(world); - UpgradeJournal journal=new UpgradeJournal(world); + UpgradeJournal journal=new UpgradeJournal(world,levelHeader); journal.inspect(); for(Path original:journal.active.values()) { CompoundTag corpse=journal.readSnapshot(original).getCompound("Entity"); @@ -68,7 +76,7 @@ void validateEntityChunks(com.mojang.datafixers.DataFixer fixer) throws IOExcept InputStream stream=snapshot.getByte("OriginalCompression")==1?new GZIPInputStream(new ByteArrayInputStream(bytes)): new InflaterInputStream(new ByteArrayInputStream(bytes)); CompoundTag root; - try(DataInputStream input=new DataInputStream(stream)){root=NbtIo.read(input,new NbtAccounter(MAX_NBT));} + try(DataInputStream input=new DataInputStream(stream)){root=NbtIo.read(input,NbtAccounter.create(MAX_NBT));} try { NativeNbt.update(fixer,net.minecraft.util.datafix.fixes.References.ENTITY_CHUNK,root, root.contains("DataVersion",3)?root.getInt("DataVersion"):-1,UpgradePreservation.MINECRAFT_VERSION); @@ -179,7 +187,7 @@ private Map readIndex() throws IOException { throw new IOException("Lootable Bodies upgrade index checksum mismatch"); CompoundTag nbt; try(DataInputStream input=new DataInputStream(new GZIPInputStream(new ByteArrayInputStream(payload)))) { - nbt=NbtIo.read(input,new NbtAccounter(MAX_NBT)); + nbt=NbtIo.read(input,NbtAccounter.create(MAX_NBT)); } if(!nbt.contains("Format",3) || nbt.getInt("Format")!=FORMAT || !compoundList(nbt.get("Regions"))) throw new IOException("Invalid Lootable Bodies upgrade index"); @@ -237,7 +245,7 @@ private CompoundTag scanRegion(Path file,String relative,String hash,CompoundTag compression==2 ? new InflaterInputStream(new ByteArrayInputStream(payload)) : null; if(stream==null) throw new IOException("Unknown chunk compression: "+relative); CompoundTag chunk; - try(DataInputStream nbt=new DataInputStream(stream)) { chunk=NbtIo.read(nbt,new NbtAccounter(MAX_NBT)); } + try(DataInputStream nbt=new DataInputStream(stream)) { chunk=NbtIo.read(nbt,NbtAccounter.create(MAX_NBT)); } catch(RuntimeException damaged) { throw new IOException("Unreadable chunk in "+relative,damaged); } int x=(int)rx*32+(i%32),z=(int)rz*32+(i/32); CompoundTag level; @@ -275,7 +283,7 @@ private CompoundTag scanRegion(Path file,String relative,String hash,CompoundTag if(oldLevel==null) { byte[] originalBytes=Files.readAllBytes(chunks.resolve(savedChunk+".bin")); InputStream originalStream=priorSnapshot.getByte("OriginalCompression")==1?new GZIPInputStream(new ByteArrayInputStream(originalBytes)):new InflaterInputStream(new ByteArrayInputStream(originalBytes)); - try(DataInputStream originalInput=new DataInputStream(originalStream)){oldLevel=comparisonPayload(NbtIo.read(originalInput,new NbtAccounter(MAX_NBT)),entityRegion);} + try(DataInputStream originalInput=new DataInputStream(originalStream)){oldLevel=comparisonPayload(NbtIo.read(originalInput,NbtAccounter.create(MAX_NBT)),entityRegion);} priorLevels.put(savedChunk,oldLevel); } // Only the outer version stamp may differ for entity chunks. @@ -309,9 +317,8 @@ private static CompoundTag comparisonPayload(CompoundTag chunk,boolean entityReg private CompoundTag readWorldVersions() throws IOException { CompoundTag context=new CompoundTag(); - Path level=world.resolve("level.dat"); - if(Files.isRegularFile(level)) { - CompoundTag root=readCompressed(level),data=root.getCompound("Data"); + if(Files.isRegularFile(levelHeader)) { + CompoundTag root=readCompressed(levelHeader),data=root.getCompound("Data"); if(data.contains("DataVersion")) context.put("WorldDataVersion",data.get("DataVersion").copy()); if(data.contains("Version"))context.put("WorldVersion",data.get("Version").copy()); if(root.contains("FML")) context.put("WorldFML",root.get("FML").copy()); @@ -341,7 +348,8 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co else if(marker==7)version=2730; else if(marker==8)version=2975; else if(marker==9)version=3337; - else if(marker==10)version=UpgradePreservation.MINECRAFT_VERSION; + else if(marker==10)version=3465; + else if(marker==11)version=UpgradePreservation.MINECRAFT_VERSION; if(legacy) {if(version!=512)context.putBoolean("Inferred",true);version=512;} if(legacy || (marker>=1 && marker<=UpgradePreservation.DATA_VERSION)) context.putInt("CorpseWriterDataVersion",version); @@ -350,7 +358,8 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co for(int i=0;i dimension,long seed) { - return new ClientboundRespawnPacket(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD, - dimension,seed,GameType.SURVIVAL,GameType.CREATIVE,false,true, - ClientboundRespawnPacket.KEEP_ALL_DATA,java.util.Optional.empty(),0); + var registry=dimensions().registryOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE); + var type=registry.getHolderOrThrow(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD); + return new ClientboundRespawnPacket(new CommonPlayerSpawnInfo(type,dimension,seed, + GameType.SURVIVAL,GameType.CREATIVE,false,true,java.util.Optional.empty(),0), + ClientboundRespawnPacket.KEEP_ALL_DATA); + } + private static RegistryAccess dimensions() { + return DimensionTypes.ACCESS; + } + private static class DimensionTypes { + static final RegistryAccess ACCESS=build(); + private static RegistryAccess build() { + var key=net.minecraft.core.registries.Registries.DIMENSION_TYPE; + var registry=new net.minecraft.core.MappedRegistry(key,com.mojang.serialization.Lifecycle.stable()); + Native119Registries.lookup().lookupOrThrow(key).listElements() + .forEach(holder->registry.register(holder.key(),holder.value(),net.minecraft.core.RegistrationInfo.BUILT_IN)); + return new RegistryAccess.ImmutableRegistryAccess(java.util.List.of(registry.freeze())); + } } @Test void native115HashedSeedSurvivesDecodedPacketObservation() throws Exception { - FriendlyByteBuf bytes=new FriendlyByteBuf(Unpooled.buffer()); + RegistryFriendlyByteBuf bytes=new RegistryFriendlyByteBuf(Unpooled.buffer(),dimensions()); try { ClientboundRespawnPacket outgoing=packet(Level.NETHER,SEED); - outgoing.write(bytes); + ClientboundRespawnPacket.STREAM_CODEC.encode(bytes,outgoing); byte[] before=new byte[bytes.readableBytes()];bytes.getBytes(0,before); - ClientboundRespawnPacket incoming=new ClientboundRespawnPacket(bytes); + ClientboundRespawnPacket incoming=ClientboundRespawnPacket.STREAM_CODEC.decode(bytes); String expected="RESPAWN dimension=minecraft:the_nether hashedSeed="+SEED; assertEquals(expected,NativeRespawnDescription.describe(outgoing)); assertEquals(expected,NativeRespawnDescription.describe(incoming)); - assertEquals(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD,incoming.getDimensionType(),"Native codec dimension type key was lost"); + assertEquals(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD,incoming.commonPlayerSpawnInfo().dimensionType().unwrapKey().orElseThrow(),"Native codec dimension type key was lost"); assertEquals(before.length,bytes.readerIndex()); byte[] after=new byte[before.length];bytes.getBytes(0,after);assertArrayEquals(before,after); } finally {bytes.release();} diff --git a/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java index d5fc72a..a068877 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java @@ -27,9 +27,9 @@ private Set keys(CommentedConfig config) { Set removed=new HashSet<>(previous);removed.removeAll(keys(config)); assertEquals(new HashSet<>(Arrays.asList("removeErroringTileEntities", "logCascadingWorldGeneration","fixVanillaCascading","dimensionUnloadQueueDelay", - "fixAdvancementLoading","treatEmptyTagsAsAir")),removed); + "fixAdvancementLoading","treatEmptyTagsAsAir","zombieBaseSummonChance","zombieBabyChance")),removed); assertEquals(new HashSet<>(Arrays.asList("removeErroringBlockEntities","fullBoundingBoxLadders", - "zombieBaseSummonChance","zombieBabyChance","advertiseDedicatedServerToLan", + "advertiseDedicatedServerToLan", "removeErroringEntities","permissionHandler","useItemWithDurationZero")),keys(config)); assertEquals(false,config.get("server.removeErroringBlockEntities")); assertEquals(0,spec().correct(config),"Native correction must be stable"); @@ -42,15 +42,15 @@ private Set keys(CommentedConfig config) { config.set("server.removeErroringEntities",true);config.set("server.useItemWithDurationZero",true); config.set("server.advertiseDedicatedServerToLan",false);config.set("server.skipEmptyShapelessCheck",false); spec().correct(config); - assertEquals(true,config.get("server.fullBoundingBoxLadders"));assertEquals(0.37D,(Double)config.get("server.zombieBaseSummonChance")); - assertEquals(0.29D,(Double)config.get("server.zombieBabyChance"));assertNull(config.get("server.treatEmptyTagsAsAir")); + assertEquals(true,config.get("server.fullBoundingBoxLadders"));assertNull(config.get("server.zombieBaseSummonChance")); + assertNull(config.get("server.zombieBabyChance"));assertNull(config.get("server.treatEmptyTagsAsAir")); assertNull(config.get("server.fixAdvancementLoading"));assertEquals(true,config.get("server.removeErroringBlockEntities")); assertEquals(true,config.get("server.removeErroringEntities"));assertEquals(true,config.get("server.useItemWithDurationZero")); assertEquals(false,config.get("server.advertiseDedicatedServerToLan"));assertNull(config.get("server.skipEmptyShapelessCheck")); } @Test void actualNativeDefaultMatchesTheTargetSpecBaseline() throws Exception { CommentedConfig config=CommentedConfig.inMemory();spec().correct(config); - CommentedConfig expected=new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge45-server-default.toml"))); + CommentedConfig expected=new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge50-server-default.toml"))); assertEquals(expected,config); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java index 2a0af5c..2e66b38 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java @@ -4,7 +4,9 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import java.util.Arrays; import net.minecraft.core.NonNullList; -import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.*; import net.minecraft.nbt.CompoundTag; @@ -15,10 +17,18 @@ class Native117MenuProtocolTest { @BeforeAll static void bootstrap(){net.minecraft.server.Bootstrap.bootStrap();} - private ItemStack stack(int count) {ItemStack item=new ItemStack(Items.DIAMOND,count);CompoundTag tag=new CompoundTag();tag.putString("keep","native packet");item.setTag(tag);return item;} + private ItemStack stack(int count) {ItemStack item=new ItemStack(Items.DIAMOND,count);CompoundTag tag=new CompoundTag();tag.putString("keep","native packet");item.set(net.minecraft.core.component.DataComponents.CUSTOM_DATA,net.minecraft.world.item.component.CustomData.of(tag));return item;} private byte[] bytes(Packet packet) throws Exception { - FriendlyByteBuf buffer=new FriendlyByteBuf(Unpooled.buffer()); - try {packet.write(buffer);byte[] bytes=new byte[buffer.readableBytes()];buffer.getBytes(0,bytes);return bytes;}finally{buffer.release();} + RegistryFriendlyByteBuf buffer=wire(); + try { + if(packet instanceof ClientboundContainerSetContentPacket content) + ClientboundContainerSetContentPacket.STREAM_CODEC.encode(buffer,content); + else ClientboundContainerSetSlotPacket.STREAM_CODEC.encode(buffer,(ClientboundContainerSetSlotPacket)packet); + byte[] bytes=new byte[buffer.readableBytes()];buffer.getBytes(0,bytes);return bytes; + }finally{buffer.release();} + } + private RegistryFriendlyByteBuf wire() { + return new RegistryFriendlyByteBuf(Unpooled.buffer(),RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY)); } @Test void fullContentCarriesStateAndCursorWithoutMutatingThePacket() throws Exception { NonNullList slots=NonNullList.withSize(84,ItemStack.EMPTY);slots.set(47,stack(31));ItemStack cursor=stack(17); @@ -38,10 +48,10 @@ private byte[] bytes(Packet packet) throws Exception { @Test void clickCodecRetainsStatePredictedSlotsAndCarriedStack() throws Exception { Int2ObjectOpenHashMap changed=new Int2ObjectOpenHashMap<>();changed.put(6,stack(13));changed.put(47,ItemStack.EMPTY); ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket(7,32766,6,1,ClickType.PICKUP,stack(9),changed); - FriendlyByteBuf buffer=new FriendlyByteBuf(Unpooled.buffer()); + RegistryFriendlyByteBuf buffer=wire(); try { - outgoing.write(buffer);byte[] before=new byte[buffer.readableBytes()];buffer.getBytes(0,before); - ServerboundContainerClickPacket decoded=new ServerboundContainerClickPacket(buffer); + ServerboundContainerClickPacket.STREAM_CODEC.encode(buffer,outgoing);byte[] before=new byte[buffer.readableBytes()];buffer.getBytes(0,before); + ServerboundContainerClickPacket decoded=ServerboundContainerClickPacket.STREAM_CODEC.decode(buffer); assertEquals(7,decoded.getContainerId());assertEquals(32766,decoded.getStateId());assertEquals(6,decoded.getSlotNum());assertEquals(1,decoded.getButtonNum()); assertEquals(ClickType.PICKUP,decoded.getClickType());assertTrue(ItemStack.matches(stack(9),decoded.getCarriedItem())); assertEquals(changed.keySet(),decoded.getChangedSlots().keySet());assertTrue(ItemStack.matches(stack(13),decoded.getChangedSlots().get(6)));assertTrue(decoded.getChangedSlots().get(47).isEmpty()); @@ -50,7 +60,7 @@ private byte[] bytes(Packet packet) throws Exception { } @Test void unrelatedPacketsAreIgnoredWithoutSerializingThem() { Packet other=new Packet() { - public void write(FriendlyByteBuf buffer){throw new AssertionError("Observer serialized an unrelated packet");} + public net.minecraft.network.protocol.PacketType> type(){throw new AssertionError("Observer serialized an unrelated packet");} public void handle(ClientGamePacketListener listener){throw new AssertionError("Observer handled a live packet");} }; assertNull(NativeMenuObservation.decode(other)); diff --git a/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java index 79d65b7..04dcf33 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java @@ -16,14 +16,14 @@ private Set packages(String archive) throws Exception { java.util.zip.ZipEntry entry=entries.nextElement();String path=entry.getName(); if(!path.endsWith(".class"))continue; byte[] code=zip.getInputStream(entry).readAllBytes(); - assertEquals(61,((code[6]&255)<<8)|(code[7]&255),path); + assertEquals(65,((code[6]&255)<<8)|(code[7]&255),path); result.add(path.substring(0,path.lastIndexOf('/'))); } } assertFalse(result.isEmpty(),archive);return result; } @Test void integrationModuleHasNoProductionPackageDefinitions() throws Exception { - Set main=packages("build/libs-dev/LootableBodies-"+LootableBodies.VERSION+"-deobf.jar"); + Set main=packages("build/libs/LootableBodies-"+LootableBodies.VERSION+".jar"); Set probe=packages("build/integration-test-mod/lootablebodies-build-tests.jar"); probe.retainAll(main);assertTrue(probe.isEmpty(),"Native47 module split packages: "+probe); } diff --git a/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java b/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java index 77842a4..20b8778 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java +++ b/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java @@ -1,17 +1,43 @@ package cyano.lootable.buildtest; +import java.util.List; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModLoader; +import net.minecraftforge.fml.loading.FMLLoader; +import net.minecraftforge.fml.loading.LoadingModList; import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; -/** Native registries must be ready before any test-class static initializer. */ +/** Boots native codecs in plain JUnit without discovering or loading any mods. */ public final class Native117TestEnvironment implements LauncherSessionListener { @Override public void launcherSessionOpened(LauncherSession session) { + initialize(); + } + + public static synchronized void initialize() { net.minecraft.SharedConstants.tryDetectVersion(); - // Plain JUnit has no ModLauncher-generated event constructors. Seed - // the real EventBus list through its supported instance API before - // native Bootstrap registers the Forge handshake channel listener. - new net.minecraftforge.network.NetworkEvent(() -> null).getListenerList(); - new net.minecraftforge.network.NetworkEvent.GatherLoginPayloadsEvent(new java.util.ArrayList<>(),false).getListenerList(); + if(FMLLoader.getLoadingModList()==null) { + try { + LoadingModList loading=LoadingModList.of(List.of(),List.of(),null); + loading.setBrokenFiles(List.of()); + var loadingField=FMLLoader.class.getDeclaredField("loadingModList"); + loadingField.setAccessible(true); + loadingField.set(null,loading); + var layerField=FMLLoader.class.getDeclaredField("moduleLayerManager"); + layerField.setAccessible(true); + layerField.set(null,(cpw.mods.modlauncher.api.IModuleLayerManager) + layer->java.util.Optional.of(ModuleLayer.boot())); + ModList mods=ModList.of(List.of(),List.of()); + var setLoaded=ModList.class.getDeclaredMethod("setLoadedMods",List.class); + setLoaded.setAccessible(true); + setLoaded.invoke(mods,List.of()); + var valid=ModLoader.class.getDeclaredField("loadingStateValid"); + valid.setAccessible(true); + valid.setBoolean(ModLoader.get(),true); + } catch(ReflectiveOperationException failure) { + throw new IllegalStateException("Cannot initialize the empty Forge unit-test context",failure); + } + } net.minecraft.server.Bootstrap.bootStrap(); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java b/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java index e5f9bcd..d10d45e 100644 --- a/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java @@ -10,6 +10,7 @@ import java.util.*; import java.util.concurrent.TimeUnit; import net.minecraft.network.*; +import net.minecraft.core.RegistryAccess; import net.minecraft.network.protocol.*; import net.minecraft.network.protocol.game.*; import net.minecraft.world.entity.EntityType; @@ -24,14 +25,20 @@ class Native119BundleTraceTest { private ClientboundAddEntityPacket spawn(int id) { return new ClientboundAddEntityPacket(id,new UUID(1194,id),1,2,3,4,5,EntityType.PIG,6,new Vec3(.1,.2,.3),7); } - private byte[] bytes(Packet packet) { - FriendlyByteBuf buffer=new FriendlyByteBuf(Unpooled.buffer()); - try {packet.write(buffer);byte[] result=new byte[buffer.readableBytes()];buffer.getBytes(0,result);return result;} + private byte[] bytes(ClientboundAddEntityPacket packet) { + RegistryFriendlyByteBuf buffer=new RegistryFriendlyByteBuf(Unpooled.buffer(),registries()); + try {ClientboundAddEntityPacket.STREAM_CODEC.encode(buffer,packet);byte[] result=new byte[buffer.readableBytes()];buffer.getBytes(0,result);return result;} finally {buffer.release();} } + private RegistryAccess registries() { + return RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); + } + private ProtocolInfo protocol() { + return GameProtocols.CLIENTBOUND.bind(RegistryFriendlyByteBuf.decorator(registries())); + } private List> wire(ClientboundBundlePacket bundle) { List> wire=new ArrayList<>(); - ConnectionProtocol.PLAY.getBundlerInfo(PacketFlow.CLIENTBOUND).unbundlePacket(bundle,wire::add); + protocol().bundlerInfo().unbundlePacket(bundle,wire::add); return wire; } private final class Fixture implements AutoCloseable { @@ -42,8 +49,7 @@ private final class Fixture implements AutoCloseable { final Path log=temporary.resolve("received-packets.txt"); Fixture() throws Exception { loader=new URLClassLoader(new URL[]{Path.of("build/integration-test-mod/classes").toUri().toURL()},getClass().getClassLoader()); - channel.attr(BundlerInfo.BUNDLER_PROVIDER).set(ConnectionProtocol.PLAY); - channel.pipeline().addLast("bundler",new PacketBundlePacker(PacketFlow.CLIENTBOUND)); + channel.pipeline().addLast("bundler",new PacketBundlePacker(protocol().bundlerInfo())); channel.pipeline().addLast("packet_handler",new ChannelInboundHandlerAdapter(){ @Override public void channelRead(ChannelHandlerContext context,Object packet){received.add(packet);} }); @@ -68,7 +74,7 @@ void receive(List> packets) { try(Fixture fixture=new Fixture()) { fixture.receive(wire(bundle));assertEquals(1,fixture.received.size()); ClientboundBundlePacket delivered=assertInstanceOf(ClientboundBundlePacket.class,fixture.received.get(0)); - List> children=new ArrayList<>();delivered.subPackets().forEach(children::add); + List> children=new ArrayList<>();delivered.subPackets().forEach(children::add); assertEquals(List.of(spawn,remove),children);assertArrayEquals(original,bytes(spawn)); String text=Files.readString(fixture.log); assertTrue(text.contains(" IN MOD_SPAWN id=12 mod=minecraft ")&&text.contains("uuid="+spawn.getUUID()),"Native bundled spawn disappeared from receiving wire proof"); diff --git a/src/test/java/cyano/lootable/buildtest/Native119Registries.java b/src/test/java/cyano/lootable/buildtest/Native119Registries.java index d605c36..02d7208 100644 --- a/src/test/java/cyano/lootable/buildtest/Native119Registries.java +++ b/src/test/java/cyano/lootable/buildtest/Native119Registries.java @@ -16,7 +16,10 @@ private static final class Prepared { } public static HolderLookup.Provider lookup(){return Prepared.LOOKUP;} public static RegistryAccess forDimensions(WorldDimensions dimensions) { - return new RegistryAccess.ImmutableRegistryAccess(List.of(dimensions.dimensions())) { + var stems=new net.minecraft.core.MappedRegistry( + net.minecraft.core.registries.Registries.LEVEL_STEM,com.mojang.serialization.Lifecycle.stable()); + dimensions.dimensions().forEach((key,value)->stems.register(key,value,net.minecraft.core.RegistrationInfo.BUILT_IN)); + return new RegistryAccess.ImmutableRegistryAccess(List.of(stems.freeze())) { @Override public Optional> lookup(ResourceKey> key) { Optional> saved=super.lookup(key); return saved.isPresent()?saved:Native119Registries.lookup().lookup(key); diff --git a/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java b/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java index 0f1bd46..7dcf64d 100644 --- a/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java +++ b/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java @@ -2,44 +2,29 @@ import java.nio.file.*; import java.util.UUID; -import com.mojang.serialization.Lifecycle; -import net.minecraft.core.*; import net.minecraft.nbt.*; -import net.minecraft.world.*; -import net.minecraft.world.level.*; -import net.minecraft.world.level.biome.*; -import net.minecraft.world.level.dimension.DimensionType; -import net.minecraft.world.level.levelgen.*; -import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; -import net.minecraft.world.level.storage.PrimaryLevelData; -import net.minecraft.core.registries.Registries; -import net.minecraft.world.level.levelgen.presets.WorldPresets; -import net.minecraft.world.level.WorldDataConfiguration; import cyano.lootable.upgrade.Native117RegionFixtures; -/** Native20 codec fixture preparation, never authentic historical-writer proof. */ +/** Derive negative fixtures from a saved Forge 50 flat header. */ public final class Native120FixtureGenerator { public static void main(String[] args)throws Exception { - if(args.length!=1)throw new IllegalArgumentException("One new owned output root required"); + if(args.length!=2)throw new IllegalArgumentException("Owned output root and sealed native header required"); Path root=Path.of(args[0]);Files.createDirectory(root); new Native117TestEnvironment().launcherSessionOpened(null); - WorldDimensions dimensions=Native119Registries.lookup().lookupOrThrow(Registries.WORLD_PRESET) - .getOrThrow(WorldPresets.FLAT).value().createWorldDimensions(); - RegistryAccess registries=Native119Registries.forDimensions(dimensions); - WorldOptions options=new WorldOptions(4267321L,false,false); - LevelSettings settings=new LevelSettings("Native20 disposable fixture",GameType.CREATIVE,false,Difficulty.NORMAL,true,new GameRules(),WorldDataConfiguration.DEFAULT); - PrimaryLevelData nativeData=new PrimaryLevelData(settings,options,PrimaryLevelData.SpecialWorldProperty.FLAT,Lifecycle.stable()); - nativeData.setSpawn(new BlockPos(0,80,0),0);nativeData.setGameTime(0);nativeData.setDayTime(6000); - CompoundTag data=nativeData.createTag(registries,null);data.putLong("LastPlayed",0); + Path source=Path.of(args[1]); + CompoundTag header=NbtIo.readCompressed(source,NbtAccounter.create(64L*1024*1024)); + CompoundTag data=header.getCompound("Data"); int target=net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); - if(data.getInt("DataVersion")!=target || !data.contains("WorldGenSettings",10))throw new AssertionError("Native20 header was not encoded"); - CompoundTag header=new CompoundTag();header.put("Data",data); - NbtIo.writeCompressed(header,root.resolve("native120-flat-level.dat").toFile()); + CompoundTag generator=data.getCompound("WorldGenSettings").getCompound("dimensions") + .getCompound("minecraft:overworld").getCompound("generator"); + if(data.getInt("DataVersion")!=target || !generator.getString("type").equals("minecraft:flat")) + throw new AssertionError("Wrong native flat header"); + Files.copy(source,root.resolve("native1206-flat-level.dat")); Path world=root.resolve("controlled-entity-source");Files.createDirectory(world); - CompoundTag originalOwned=new CompoundTag();originalOwned.putString("unknownField","retained-native20-header");originalOwned.putLongArray("unknownData",new long[]{Long.MIN_VALUE,1234567890123L,Long.MAX_VALUE}); - header.put("lootablebodies",originalOwned);NbtIo.writeCompressed(header,world.resolve("level.dat").toFile()); + CompoundTag originalOwned=new CompoundTag();originalOwned.putString("unknownField","retained-native1206-header");originalOwned.putLongArray("unknownData",new long[]{Long.MIN_VALUE,1234567890123L,Long.MAX_VALUE}); + header.put("lootablebodies",originalOwned);NbtIo.writeCompressed(header,world.resolve("level.dat")); CompoundTag corpse=Native117RegionFixtures.body(UUID.fromString("00000000-1171-4171-8171-000000000099"),7); Native117RegionFixtures.write(world,Native117RegionFixtures.chunk(2730,corpse)); - System.out.println("NATIVE120_CODEC_FIXTURE_PREPARED version="+target+" markerSource=7/2730 synthetic=true root="+root); + System.out.println("NATIVE1206_CODEC_FIXTURE_PREPARED version="+target+" markerSource=7/2730 synthetic=true root="+root); } } diff --git a/src/test/java/cyano/lootable/contracts/BuildContractTest.java b/src/test/java/cyano/lootable/contracts/BuildContractTest.java index 0ac073a..19022d8 100644 --- a/src/test/java/cyano/lootable/contracts/BuildContractTest.java +++ b/src/test/java/cyano/lootable/contracts/BuildContractTest.java @@ -14,7 +14,7 @@ private String text(String path) throws Exception { Properties release = new Properties(); try (java.io.InputStream input = Files.newInputStream(Paths.get("gradle.properties"))) { release.load(input); } String prefix = "LootableBodies-" + release.getProperty("mod_version"); - for (String archive : Arrays.asList("build/libs-dev/" + prefix + "-deobf.jar", "build/libs/" + prefix + "-sources.jar")) { + for (String archive : Arrays.asList("build/libs/" + prefix + ".jar", "build/libs/" + prefix + "-sources.jar")) { try (java.util.zip.ZipFile zip = new java.util.zip.ZipFile(archive)) { java.util.Enumeration entries = zip.entries(); int locales = 0; @@ -50,10 +50,11 @@ private String text(String path) throws Exception { assertTrue(info.contains("modId=\"lootablebodies\"")); Properties release=new Properties();try(java.io.InputStream input=Files.newInputStream(Paths.get("gradle.properties"))){release.load(input);} assertTrue(info.contains("version=\""+release.getProperty("mod_version")+"\"")); - assertTrue(info.contains("versionRange=\"[1.20.1]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); + assertTrue(info.contains("versionRange=\"[1.20.6]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); assertFalse(Files.exists(Paths.get("build/resources/main/mcmod.info"))); com.google.gson.JsonObject pack=new com.google.gson.JsonParser().parse(text("build/resources/main/pack.mcmeta")).getAsJsonObject().getAsJsonObject("pack"); - assertEquals(15,pack.get("pack_format").getAsInt());assertFalse(pack.has("forge:server_data_pack_format")); + assertEquals(32,pack.get("pack_format").getAsInt()); + assertEquals(41,pack.getAsJsonObject("supported_formats").get("max_inclusive").getAsInt());assertFalse(pack.has("forge:server_data_pack_format")); } @Test void everyOriginalTranslationSurvivesTheNativeJsonFormat() throws Exception { try(DirectoryStream locales=Files.newDirectoryStream(Paths.get("src/test/resources/legacy-translations"),"*.lang")){ diff --git a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java index 7a31f2d..0fd94b8 100644 --- a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java +++ b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java @@ -57,7 +57,7 @@ private CompoundTag find(Tag value) { } @Test void legacyIdentityAndEveryRawInventoryEntrySurviveBothAnvilPasses()throws Exception { CompoundTag body=corpse(),source=chunk(body,true);byte[] before=encoded(source);CompoundTag result=update(source),saved=findCorpse(result); - assertEquals("lootablebodies:corpse",saved.getString("id"));assertEquals(10,saved.getInt("LootableBodiesDataVersion"));assertEquals(ID,saved.getUUID("UUID")); + assertEquals("lootablebodies:corpse",saved.getString("id"));assertEquals(11,saved.getInt("LootableBodiesDataVersion"));assertEquals(ID,saved.getUUID("UUID")); ListTag records=saved.getList("LootableBodiesUnresolved",10);assertEquals(49,records.size()); for(int i=0;i<48;i++){CompoundTag record=records.getCompound(i);assertEquals("Equipment",record.getString("Source"));assertArrayEquals(encoded(body.getList("Equipment",10).getCompound(i)),encoded(record.getCompound("Data")));assertEquals(512,record.getCompound("SourceVersions").getInt("DataVersion"));} assertEquals("Aux",records.getCompound(48).getString("Source"));assertArrayEquals(before,encoded(source));assertFalse(saved.contains("LootableBodiesUpgradePayload"));assertFalse(saved.contains("LootableBodiesInventorySource")); diff --git a/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java b/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java index a369670..cc0796b 100644 --- a/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java +++ b/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java @@ -28,7 +28,7 @@ private String text(String path) throws Exception { Properties values = properties(); String version = values.getProperty("mod_version"); assertTrue(version.matches("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+"), version); - assertEquals("2.4.11.120011", version); + assertEquals("2.4.12.120061", version); assertEquals("forge", values.getProperty("loader_name")); assertEquals("1", values.getProperty("loader_code")); assertTrue(text("src/main/java/cyano/lootable/LootableBodies.java") @@ -44,7 +44,7 @@ private String text(String path) throws Exception { String source = text("src/main/java/cyano/lootable/LootableBodies.java"); assertTrue(source.startsWith("package cyano.lootable;")); assertTrue(source.contains("new ResourceLocation(\"lootablebodies:corpse\")")); - assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.20.1]\"")); + assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.20.6]\"")); } @Test void maintainedReleaseHasLgplAndRetainsTheOriginalNotice() throws Exception { diff --git a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java index 84cd7a8..e09f78d 100644 --- a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java +++ b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java @@ -173,7 +173,7 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t @Test void corruptIndexAbortsPreservation() throws Exception {region("",body(ID));scan();Files.write(world.resolve("data/lootablebodies-upgrade/index.nbt"),new byte[]{0,1,2});assertThrows(IOException.class,()->scan());} @Test void readableIndexDamageCannotSilentlyOmitPreservedCorpses() throws Exception { region("",body(ID));scan();Path path=world.resolve("data/lootablebodies-upgrade/index.nbt"); - CompoundTag index=NbtIo.readCompressed(Files.newInputStream(path)); + CompoundTag index=NbtIo.readCompressed(path,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)); if(index.contains("Payload",7)) {byte[] payload=index.getByteArray("Payload");payload[payload.length/2]^=1;index.putByteArray("Payload",payload);} else index.getList("Regions",10).getCompound(0).put("Snapshots",new ListTag()); try(OutputStream output=Files.newOutputStream(path)){NbtIo.writeCompressed(index,output);} @@ -182,7 +182,7 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t @Test void truncatedRegionAbortsBeforeLoading() throws Exception {Path region=region("",body(ID));Files.write(region,new byte[8193]);assertThrows(IOException.class,()->scan());} @Test void nativeEmptyRegionPlaceholdersRemainReadOnlyAndAreScannedWhenPopulated()throws Exception { Path folder=world.resolve("region");Files.createDirectories(folder);Path file=folder.resolve("r.0.0.mca"); - try(net.minecraft.world.level.chunk.storage.RegionFile nativeRegion=new net.minecraft.world.level.chunk.storage.RegionFile(file,folder,false)) { + try(net.minecraft.world.level.chunk.storage.RegionFile nativeRegion=new net.minecraft.world.level.chunk.storage.RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),file,folder,false)) { assertNull(nativeRegion.getChunkDataInputStream(new net.minecraft.world.level.ChunkPos(0,0))); } assertEquals(0,Files.size(file));assertNull(original(scan(),ID,0,0));assertEquals(0,Files.size(file)); diff --git a/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java b/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java index 06dfc2c..6d55059 100644 --- a/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java +++ b/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java @@ -6,31 +6,55 @@ import static org.junit.jupiter.api.Assertions.*; class Native118EffectIdentityTest { - private CompoundTag nativeEffect(){return new MobEffectInstance(MobEffects.NIGHT_VISION,200,1,false,true,false).save(new CompoundTag());} + private CompoundTag legacyEffect() { + CompoundTag old=new CompoundTag(); + old.putByte("Id",(byte)16); + old.putString("forge:id","minecraft:night_vision"); + old.putInt("Duration",200); + return old; + } + @Test void nativeForgeSaveUsesARecognizedStableNamedIdentity() { - CompoundTag raw=nativeEffect(),before=raw.copy();assertEquals("minecraft:night_vision",raw.getString("forge:id")); - assertSame(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,16L));assertFalse(SavedEffectIdentity.needsLegacyRecovery(raw,16L)); - CompoundTag accepted=new CompoundTag();SavedEffectIdentity.write(accepted,raw,MobEffects.NIGHT_VISION); - assertEquals(raw.get("Id"),accepted.get("Id"));assertEquals(raw.get("forge:id"),accepted.get("forge:id"));assertEquals(before,raw); + CompoundTag raw=(CompoundTag)new MobEffectInstance(MobEffects.NIGHT_VISION,200,1,false,true,false).save(); + CompoundTag before=raw.copy(); + assertEquals("minecraft:night_vision",raw.getString("id")); + assertEquals(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,null)); + assertEquals(MobEffects.NIGHT_VISION,MobEffectInstance.load(raw).getEffect()); + assertEquals(before,raw); } - @Test void validatedNameFollowsActualNativePrecedenceWhenLegacyNumbersWereRemapped() { - CompoundTag raw=nativeEffect();raw.putByte("Id",(byte)MobEffect.getId(MobEffects.MOVEMENT_SPEED)); - assertSame(MobEffects.NIGHT_VISION,MobEffectInstance.load(raw).getEffect()); - assertSame(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,1L));assertFalse(SavedEffectIdentity.needsLegacyRecovery(raw,1L)); + + @Test void validLegacyNameTakesPrecedenceOverARemappedNumber() { + CompoundTag raw=legacyEffect(); + raw.putByte("Id",(byte)1); + CompoundTag before=raw.copy(); + assertEquals(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,1L)); + assertEquals(before,raw); } - @Test void knownNameCanRecoverInvalidLegacyNumbersWithoutDiscardingTheRawRecord() { - CompoundTag raw=nativeEffect();raw.putString("Id","damaged legacy fallback"); - assertSame(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,null));assertTrue(SavedEffectIdentity.needsLegacyRecovery(raw,null)); + + @Test void knownNameCanRecoverInvalidLegacyNumbersWithoutMutatingTheRawRecord() { + CompoundTag raw=legacyEffect(); + raw.putString("Id","damaged legacy fallback"); + CompoundTag before=raw.copy(); + assertEquals(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,null)); + assertEquals(before,raw); } + @Test void malformedAndUnknownNamedIdentitiesRefuseRatherThanActivateAnUnrelatedFallback() { for(Tag bad:new Tag[]{IntTag.valueOf(16),new ListTag(),StringTag.valueOf("bad name?!"),StringTag.valueOf("missing:unavailable_effect")}) { - CompoundTag raw=nativeEffect();raw.put("forge:id",bad);CompoundTag before=raw.copy(); - assertNull(SavedEffectIdentity.resolve(raw,16L));assertEquals(before,raw); + CompoundTag raw=legacyEffect(); + raw.put("forge:id",bad); + CompoundTag before=raw.copy(); + assertNull(SavedEffectIdentity.resolve(raw,16L)); + assertEquals(before,raw); } } + @Test void legacyOnlyAndNativeEmptyNameFallbacksRemainBounded() { - CompoundTag raw=nativeEffect();raw.remove("forge:id");assertSame(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,16L)); - for(Long bad:new Long[]{null,-1L,256L,4294967312L})assertNull(SavedEffectIdentity.resolve(raw,bad)); - raw.putString("forge:id","");assertSame(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,16L));assertTrue(SavedEffectIdentity.needsLegacyRecovery(raw,16L)); + CompoundTag raw=legacyEffect(); + raw.remove("forge:id"); + assertEquals(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,16L)); + for(Long bad:new Long[]{null,-1L,34L,256L,4294967312L})assertNull(SavedEffectIdentity.resolve(raw,bad)); + raw.putString("forge:id",""); + assertEquals(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,16L)); } } diff --git a/src/test/java/cyano/lootable/entities/Native119DamageTest.java b/src/test/java/cyano/lootable/entities/Native119DamageTest.java index 10ec700..da56469 100644 --- a/src/test/java/cyano/lootable/entities/Native119DamageTest.java +++ b/src/test/java/cyano/lootable/entities/Native119DamageTest.java @@ -7,7 +7,7 @@ import net.minecraft.core.Registry; import net.minecraft.core.RegistryAccess; import net.minecraft.core.registries.Registries; -import net.minecraft.data.worldgen.BootstapContext; +import net.minecraft.data.worldgen.BootstrapContext; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.Packet; import net.minecraft.resources.ResourceKey; @@ -28,9 +28,9 @@ class Native119DamageTest { net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap(); MappedRegistry registry=new MappedRegistry<>(Registries.DAMAGE_TYPE,Lifecycle.stable()); - DamageTypes.bootstrap(new BootstapContext() { + DamageTypes.bootstrap(new BootstrapContext() { public Holder.Reference register(ResourceKey key,DamageType value,Lifecycle life) { - return registry.register(key,value,life); + return registry.register(key,value,new net.minecraft.core.RegistrationInfo(java.util.Optional.empty(),life)); } public HolderGetter lookup(ResourceKey> key) { throw new AssertionError("Damage type bootstrap unexpectedly needs another registry: "+key); @@ -38,7 +38,7 @@ public HolderGetter lookup(ResourceKey> k }); // Deliberately shares the vanilla anvil message, but has its own mod identity. custom=registry.register(ResourceKey.create(Registries.DAMAGE_TYPE,new ResourceLocation("example:custom")), - new DamageType("anvil",0),Lifecycle.stable()); + new DamageType("anvil",0),net.minecraft.core.RegistrationInfo.BUILT_IN); registry.freeze(); sources=new DamageSources(new RegistryAccess.ImmutableRegistryAccess(java.util.List.of(registry))); // No world/entity behavior is used here. Plain JUnit has no loaded @@ -110,7 +110,7 @@ public HolderGetter lookup(ResourceKey> k } private static final class SourceEntity extends Entity { SourceEntity(){super(EntityType.FALLING_BLOCK,null);} - protected void defineSynchedData(){ } + protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder){ } protected void readAdditionalSaveData(CompoundTag tag){ } protected void addAdditionalSaveData(CompoundTag tag){ } public Packet getAddEntityPacket(){throw new UnsupportedOperationException("No network in this unit fixture");} diff --git a/src/test/java/cyano/lootable/network/LoginReadGuardTest.java b/src/test/java/cyano/lootable/network/LoginReadGuardTest.java index 5d8894b..e367496 100644 --- a/src/test/java/cyano/lootable/network/LoginReadGuardTest.java +++ b/src/test/java/cyano/lootable/network/LoginReadGuardTest.java @@ -17,8 +17,8 @@ class LoginReadGuardTest { public static final class TestChannel extends NioSocketChannel { public int readInterest() { return selectionKey().interestOps() & java.nio.channels.SelectionKey.OP_READ; } } - private static void guard(Channel channel) throws Exception { - try {Class.forName("cyano.lootable.network.ClientLoginReadGuard").getMethod("resumeReads",Channel.class).invoke(null,channel);} + private static void guard(Channel channel,java.util.function.Supplier state) throws Exception { + try {Class.forName("cyano.lootable.network.ClientLoginReadGuard").getMethod("resumeReads",Channel.class,java.util.function.Supplier.class).invoke(null,channel,state);} catch(ClassNotFoundException beforeImplementation) { } } private static final class Connection implements AutoCloseable { @@ -28,6 +28,7 @@ private static final class Connection implements AutoCloseable { final CompletableFuture received=new CompletableFuture<>(); final Socket peer; final TestChannel channel; + final java.util.concurrent.atomic.AtomicReference protocol=new java.util.concurrent.atomic.AtomicReference<>(ConnectionProtocol.LOGIN); Connection()throws Exception { Future accepted=acceptor.submit(server::accept); channel=(TestChannel)new Bootstrap().group(loop).channel(TestChannel.class) @@ -43,7 +44,6 @@ protected void channelRead0(ChannelHandlerContext context,ByteBuf input) { } }).connect(server.getLocalSocketAddress()).sync().channel(); peer=accepted.get(5,TimeUnit.SECONDS); - channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).set(ConnectionProtocol.LOGIN); drain(); } void drain()throws Exception {channel.eventLoop().submit(()->{}).get(5,TimeUnit.SECONDS);} @@ -71,7 +71,7 @@ public void close()throws Exception { } @Test void stalledLoginRestoresReadInterestAndReceivesTheOriginalBytes()throws Exception { try(Connection c=new Connection()) { - c.stall();guard(c.channel);c.drain(); + c.stall();guard(c.channel,c.protocol::get);c.drain(); assertNotEquals(0,c.channel.readInterest(),"Auto-read login channel has no selector read interest"); byte[] message={1,2,3,4,5,6,7,8};c.peer.getOutputStream().write(message);c.peer.getOutputStream().flush(); assertArrayEquals(message,c.received.get(5,TimeUnit.SECONDS)); @@ -79,21 +79,21 @@ public void close()throws Exception { } @Test void deliberateReadPauseIsPreserved()throws Exception { try(Connection c=new Connection()) { - c.channel.config().setAutoRead(false);c.drain();guard(c.channel);c.drain(); + c.channel.config().setAutoRead(false);c.drain();guard(c.channel,c.protocol::get);c.drain(); assertFalse(c.channel.config().isAutoRead());assertEquals(0,c.channel.readInterest()); } } @Test void otherProtocolStatesAreNotChanged()throws Exception { for(ConnectionProtocol state:new ConnectionProtocol[]{ConnectionProtocol.HANDSHAKING,ConnectionProtocol.STATUS,ConnectionProtocol.PLAY}) { try(Connection c=new Connection()) { - c.stall();c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).set(state);guard(c.channel);c.drain(); - assertEquals(0,c.channel.readInterest());assertEquals(state,c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).get()); + c.stall();c.protocol.set(state);guard(c.channel,c.protocol::get);c.drain(); + assertEquals(0,c.channel.readInterest());assertEquals(state,c.protocol.get()); } } } - @Test void noConnectionIsHarmless()throws Exception {guard(null);} + @Test void noConnectionIsHarmless()throws Exception {guard(null,()->ConnectionProtocol.LOGIN);} @Test void closedConnectionIsHarmless()throws Exception { - try(Connection c=new Connection()) {c.channel.close().sync();guard(c.channel);assertFalse(c.channel.isActive());} + try(Connection c=new Connection()) {c.channel.close().sync();guard(c.channel,c.protocol::get);assertFalse(c.channel.isActive());} } @Test void aPauseQueuedBeforeTheRepairExecutesIsPreserved()throws Exception { try(Connection c=new Connection()) { @@ -103,7 +103,7 @@ public void close()throws Exception { try {if(!resume.await(5,TimeUnit.SECONDS))throw new AssertionError("Fixture synchronization timed out");} catch(InterruptedException failure){throw new AssertionError(failure);} }); - try {assertTrue(entered.await(5,TimeUnit.SECONDS));guard(c.channel);c.channel.config().setAutoRead(false);} + try {assertTrue(entered.await(5,TimeUnit.SECONDS));guard(c.channel,c.protocol::get);c.channel.config().setAutoRead(false);} finally {resume.countDown();} c.drain();assertFalse(c.channel.config().isAutoRead());assertEquals(0,c.channel.readInterest()); } @@ -116,9 +116,9 @@ public void close()throws Exception { try {if(!resume.await(5,TimeUnit.SECONDS))throw new AssertionError("Fixture synchronization timed out");} catch(InterruptedException failure){throw new AssertionError(failure);} }); - try {assertTrue(entered.await(5,TimeUnit.SECONDS));guard(c.channel);c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).set(ConnectionProtocol.PLAY);} + try {assertTrue(entered.await(5,TimeUnit.SECONDS));guard(c.channel,c.protocol::get);c.protocol.set(ConnectionProtocol.PLAY);} finally {resume.countDown();} - c.drain();assertEquals(0,c.channel.readInterest());assertEquals(ConnectionProtocol.PLAY,c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).get()); + c.drain();assertEquals(0,c.channel.readInterest());assertEquals(ConnectionProtocol.PLAY,c.protocol.get()); } } } diff --git a/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java index 82c065b..113e0ae 100644 --- a/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java +++ b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java @@ -1,59 +1,46 @@ package cyano.lootable.network; import com.mojang.authlib.GameProfile; -import com.mojang.authlib.minecraft.MinecraftSessionService; import com.mojang.authlib.properties.Property; -import java.lang.reflect.Proxy; -import java.nio.file.Path; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.UnaryOperator; -import net.minecraft.server.players.GameProfileCache; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; class Native117ProfileLookupTest { - @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path directory; - final GameProfile initial=new GameProfile(null,"Owner"); - GameProfile cached=new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000201"),"Owner"); - GameProfile added; - GameProfileCache cache(boolean broken) { - return new GameProfileCache(null,directory.resolve("unused.json").toFile()) { - @Override public Optional get(String name) { - if(broken)throw new IllegalStateException("injected cache failure"); - return Optional.of(cached); - } - @Override public void add(GameProfile value) {added=value;} - }; - } - MinecraftSessionService service(UnaryOperator action) { - return (MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class}, - (proxy,method,args)->{ - if(method.getName().equals("fillProfileProperties"))return action.apply((GameProfile)args[0]); - throw new AssertionError("Unexpected native service "+method.getName()); - }); - } - @Test void cacheFailureKeepsTheOriginalFallback() { - assertSame(initial,CorpseProfiles.resolve(initial,cache(true),service(profile->{throw new AssertionError("Service must not run");}))); + private final GameProfile placeholder=CorpseProfiles.placeholder("TestName"); + private final GameProfile found=new GameProfile(new UUID(1,2),"TestName"); + + @Test void unresolvedProfileUsesAValidAuthlibIdentity() { + assertEquals("TestName",placeholder.getName()); + assertNotNull(placeholder.getId()); + assertFalse(CorpseProfiles.isResolved(placeholder)); } - @Test void sessionFailureKeepsTheOriginalFallback() { - assertSame(initial,CorpseProfiles.resolve(initial,cache(false),service(profile->{throw new IllegalStateException("injected service failure");}))); - assertNull(added); + + @Test void absentLookupKeepsTheOriginalPlaceholder() { + assertSame(placeholder,CorpseProfiles.resolve(placeholder, + name->CompletableFuture.completedFuture(Optional.empty())).join()); } - @Test void resolvedProfileKeepsNativeIdentityAndTexturesWithoutMutatingCachedInput() { - GameProfile resolved=CorpseProfiles.resolve(initial,cache(false),service(profile->{ - profile.getProperties().put("textures",new Property("textures","native-fixture"));return profile; - })); - assertEquals(cached.getId(),resolved.getId());assertEquals("Owner",resolved.getName()); - assertEquals("native-fixture",resolved.getProperties().get("textures").iterator().next().getValue()); - assertFalse(cached.getProperties().containsKey("textures"));assertSame(resolved,added); + + @Test void asyncAndImmediateFailuresKeepTheOriginalPlaceholder() { + assertSame(placeholder,CorpseProfiles.resolve(placeholder, + name->CompletableFuture.failedFuture(new IllegalStateException("fixture"))).join()); + assertSame(placeholder,CorpseProfiles.resolve(placeholder, + name->{throw new IllegalStateException("fixture");}).join()); } - @Test void warmTexturesSkipTheServiceAndMissingServicesKeepFallback() { - cached.getProperties().put("textures",new Property("textures","cached"));AtomicInteger requests=new AtomicInteger(); - GameProfile resolved=CorpseProfiles.resolve(initial,cache(false),service(profile->{requests.incrementAndGet();return profile;})); - assertEquals(0,requests.get());assertEquals(cached.getId(),resolved.getId()); - assertSame(initial,CorpseProfiles.resolve(initial,null,null)); + + @Test void lookupDoesNotWaitAndKeepsTheResolvedTexture() { + CompletableFuture> pending=new CompletableFuture<>(); + CompletableFuture result=CorpseProfiles.resolve(placeholder,name->{ + assertEquals("TestName",name); + return pending; + }); + assertFalse(result.isDone()); + found.getProperties().put("textures",new Property("textures","fixture-native-texture")); + pending.complete(Optional.of(found)); + assertSame(found,result.join()); + assertTrue(CorpseProfiles.isResolved(result.join())); + assertEquals("fixture-native-texture",result.join().getProperties().get("textures").iterator().next().value()); } } diff --git a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java index 2047606..8919b80 100644 --- a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java +++ b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java @@ -28,7 +28,7 @@ private void write(Path world,CompoundTag root)throws Exception { byte[] before=Files.readAllBytes(world.resolve("level.dat"));Native118UnitLifecycle.activate(); LevelStorageSource format=new LevelStorageSource(directory,directory.resolve("backups"),new DirectoryValidator(new PathAllowList(java.util.List.of())),NativeVanillaDataFixer.get()); try(LevelStorageSource.LevelStorageAccess save=format.createAccess("fixture")) { - Error failure=assertThrows(Error.class,()->EarlyWorldPreservation.beforeRead(save)); + Error failure=assertThrows(Error.class,()->EarlyWorldPreservation.beforeRead(save,false)); assertTrue(failure.getMessage().contains("Lootable Bodies could not preserve this world's corpses")); } assertArrayEquals(before,Files.readAllBytes(world.resolve("level.dat"))); @@ -38,7 +38,7 @@ private void write(Path world,CompoundTag root)throws Exception { Path world=directory.resolve("fixture");CompoundTag root=level(2230);write(world,root);Native118UnitLifecycle.activate(); LevelStorageSource format=new LevelStorageSource(directory,directory.resolve("backups"),new DirectoryValidator(new PathAllowList(java.util.List.of())),NativeVanillaDataFixer.get()); try(LevelStorageSource.LevelStorageAccess save=format.createAccess("fixture")) { - EarlyWorldPreservation.beforeRead(save); + EarlyWorldPreservation.beforeRead(save,false); UpgradeJournal early=UpgradePreservation.journal();assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); write(world,level(2586));assertSame(early,UpgradePreservation.prepareForStart(world)); assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); diff --git a/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java b/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java index 773f848..f4cbc38 100644 --- a/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java +++ b/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java @@ -23,17 +23,17 @@ class HookRegistrationOrderTest { Path world=directory.resolve("serial");Files.createDirectories(world); CompoundTag root=new CompoundTag(),data=new CompoundTag(),owned=new CompoundTag(); data.putInt("DataVersion",2730);root.put("Data",data);owned.putString("unknown","keep");root.put("lootablebodies",owned); - net.minecraft.nbt.NbtIo.writeCompressed(root,world.resolve("level.dat").toFile()); + net.minecraft.nbt.NbtIo.writeCompressed(root,world.resolve("level.dat")); byte[] original=Files.readAllBytes(world.resolve("level.dat")); try(LevelStorageSource.LevelStorageAccess save=LevelStorageSource.createDefault(directory).createAccess("serial")) { installed.setBoolean(null,false); - Error refused=assertThrows(Error.class,()->EarlyWorldPreservation.beforeRead(save)); + Error refused=assertThrows(Error.class,()->EarlyWorldPreservation.beforeRead(save,false)); assertTrue(refused.getCause().getMessage().contains("not installed")); assertArrayEquals(original,Files.readAllBytes(world.resolve("level.dat"))); assertFalse(Files.exists(world.resolve("data/lootablebodies-upgrade"))); Native118UnitLifecycle.activate(); for(int round=0;round<1000;round++){EarlyWorldPreservation.install();EarlyWorldPreservation.install();} - EarlyWorldPreservation.beforeRead(save); + EarlyWorldPreservation.beforeRead(save,false); CompoundTag saved=new CompoundTag();EarlyWorldPreservation.beforeWrite(save,saved); assertEquals(owned,saved.getCompound("lootablebodies")); } finally {UpgradePreservation.close();installed.setBoolean(null,wasInstalled);} diff --git a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java index 058a501..23c41d8 100644 --- a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java +++ b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java @@ -27,7 +27,7 @@ class ItemNbtConversionTest { CompoundTag result=ItemMigration.convertNbt(raw,context(922));assertNotNull(result);assertArrayEquals(bytes(caps),bytes(result.getCompound("ForgeCaps")));CompoundTag recovered=result.getCompound("tag").getCompound("BlockEntityTag").getList("Items",10).getCompound(0);assertEquals("minecraft:granite",recovered.getString("id"));assertEquals(29,recovered.getByte("Count"));assertEquals(3,recovered.getByte("Slot")); } @Test void currentNativeItemsAreNotFlattenedAgain()throws Exception { - CompoundTag raw=item("minecraft:granite",0);raw.remove("Damage");assertArrayEquals(bytes(raw),bytes(ItemMigration.convertNbt(raw,context(UpgradePreservation.MINECRAFT_VERSION)))); + CompoundTag raw=item("minecraft:granite",0);raw.remove("Damage");assertArrayEquals(bytes(raw),bytes(ItemMigration.convertNbt(raw,context(3465)))); } @Test void bedColoursStillUseTheCorrectHistoricalContext() { CompoundTag old=ItemMigration.convertNbt(item("minecraft:bed",0),context(922)),native112=ItemMigration.convertNbt(item("minecraft:bed",0),context(1343));assertNotNull(old);assertNotNull(native112);assertEquals("minecraft:red_bed",old.getString("id"));assertEquals("minecraft:white_bed",native112.getString("id")); @@ -36,7 +36,7 @@ class ItemNbtConversionTest { CompoundTag versions=context(922);versions.putBoolean("Ambiguous",true);versions.putIntArray("CandidateDataVersions",new int[]{922,1343});assertNull(ItemMigration.convertNbt(item("minecraft:bed",0),versions));assertNotNull(ItemMigration.convertNbt(item("minecraft:stone",1),versions)); } @Test void malformedCountsAndUnknownFutureVersionsAreNotGuessed() { - CompoundTag raw=item("minecraft:stone",1);raw.putString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(3466))); + CompoundTag raw=item("minecraft:stone",1);raw.putString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(3840))); } @Test void unrecognisedMetadataAndEnchantmentsRemainRecoverable() { assertNull(ItemMigration.convertNbt(item("minecraft:stone",99),context(1343))); @@ -48,19 +48,19 @@ class ItemNbtConversionTest { } private CompoundTag original110(int slot)throws Exception { try(InputStream in=getClass().getResourceAsStream("/legacy-items/original-110.nbt")) { - ListTag list=NbtIo.readCompressed(in).getList("Items",10); + ListTag list=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)).getList("Items",10); for(Tag value:list){CompoundTag item=(CompoundTag)value;if(item.getByte("Slot")==slot)return item;} }throw new AssertionError("Missing sealed historical item"); } @Test void actualOriginalPigEggKeepsItsTypeAndSavedEntityName()throws Exception { CompoundTag raw=original110(6);byte[] before=bytes(raw);CompoundTag fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); assertEquals("minecraft:pig_spawn_egg",fixed.getString("id"));assertEquals(raw.getByte("Count"),fixed.getByte("Count"));assertEquals("minecraft:pig",fixed.getCompound("tag").getCompound("EntityTag").getString("id")); - assertEquals("Saved pig egg",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("EntityTag").getString("CustomName")).getString());assertArrayEquals(before,bytes(raw)); + assertEquals("Saved pig egg",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("EntityTag").getString("CustomName"),cyano.lootable.buildtest.Native119Registries.lookup()).getString());assertArrayEquals(before,bytes(raw)); } @Test void actualOriginalChestKeepsItsNestedInventoryInNativeFormat()throws Exception { CompoundTag raw=original110(7),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); CompoundTag nested=fixed.getCompound("tag").getCompound("BlockEntityTag");assertEquals("minecraft:chest",nested.getString("id"));assertEquals("minecraft:emerald",nested.getList("Items",10).getCompound(0).getString("id"));assertEquals(17,nested.getList("Items",10).getCompound(0).getByte("Count"));assertFalse(fixed.contains("Damage")); - assertEquals("Original keepsake 7",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("display").getString("Name")).getString()); + assertEquals("Original keepsake 7",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("display").getString("Name"),cyano.lootable.buildtest.Native119Registries.lookup()).getString()); } @Test void actualOriginalBannerKeepsItsBaseColour()throws Exception { CompoundTag raw=original110(8),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed);assertEquals("minecraft:purple_banner",fixed.getString("id"));assertEquals(raw.getByte("Count"),fixed.getByte("Count"));// Vanilla 1.13 leaves its inverted, redundant Base field behind. diff --git a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java index 854afc7..bb6d707 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java @@ -29,13 +29,13 @@ private CompoundTag body(int marker) { return body; } @Test void currentFormatNamesThe119VanillaSchema() { - assertEquals(3465, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(10,UpgradePreservation.DATA_VERSION); + assertEquals(3839, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(11,UpgradePreservation.DATA_VERSION); } @Test void released113EquipmentAndOverflowAreRetainedBeforeNativeConversion() { CompoundTag original = body(3), before = original.copy(); CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); assertEquals(original.getUUID("UUID"), staged.getUUID("UUID")); assertTrue(staged.getList("Equipment", 10).isEmpty()); ListTag records = staged.getList("LootableBodiesUnresolved", 10); assertEquals(49, records.size()); @@ -53,7 +53,7 @@ private CompoundTag body(int marker) { record.put("SourceVersions", context(922)); ListTag recovery = new ListTag(); recovery.add(record.copy()); original.put("LootableBodiesUnresolved", recovery); CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); assertEquals(record, staged.getList("LootableBodiesUnresolved", 10).getCompound(49)); assertEquals(record, original.getList("LootableBodiesUnresolved", 10).getCompound(0)); } diff --git a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java index dad236b..3b74270 100644 --- a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java @@ -33,8 +33,8 @@ private int[] currentBiomes() { return values; } @Test void currentFormatUsesThe119SchemaAndItsOwnMarker() { - assertEquals(3465, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(10,UpgradePreservation.DATA_VERSION); + assertEquals(3839, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(11,UpgradePreservation.DATA_VERSION); } @Test void currentBiomeValuesSurvivePlausibleOlderStampsAndRepeatedConversion() { int[] values = currentBiomes(); CompoundTag original = chunk(values), before = original.copy(); @@ -78,7 +78,7 @@ private int[] currentBiomes() { ListTag items = new ListTag(); items.add(item); raw.put("Equipment", items); context.putInt("DataVersion", 1976); CompoundTag before = raw.copy(); CompoundTag staged = NativeSafeFixer.stageCorpse(raw, context); - assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); CompoundTag record = staged.getList("LootableBodiesUnresolved", 10).getCompound(0); assertEquals(item, record.get("Data")); assertEquals("Equipment", record.getString("Source")); assertEquals(1976, record.getCompound("SourceVersions").getInt("DataVersion")); diff --git a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java index aa6c91d..a94062e 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java @@ -25,7 +25,7 @@ private CompoundTag original()throws Exception { Path fixture=Paths.get("src/test/resources/native115-release-chunk.nbt"); byte[] hash=java.security.MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(fixture));StringBuilder text=new StringBuilder();for(byte value:hash)text.append(String.format("%02X",value)); assertEquals("B6D03C7E0962715745FCD93A31EA27C01B4601020A51F542898BCE09F14C10C2",text.toString()); - try(InputStream input=Files.newInputStream(fixture)){return NbtIo.readCompressed(input);} + try(InputStream input=Files.newInputStream(fixture)){return NbtIo.readCompressed(input,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} } private void preserve(CompoundTag chunk)throws Exception { CompoundTag header=new CompoundTag(),data=new CompoundTag(),fml=new CompoundTag(); @@ -51,10 +51,10 @@ private void preserve(CompoundTag chunk)throws Exception { @Test void bothNativeAnvilPassesRetainNestedChestDataWithMissingChunkStamps()throws Exception { CompoundTag source=original();assertEquals(2230,source.getInt("DataVersion")); CompoundTag nativeInput=source.copy();ChunkStorage.injectDatafixingContext(nativeInput,Level.OVERWORLD,java.util.Optional.empty()); - CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeInput,2230,2975); + CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeInput,2230,3839); CompoundTag damaged=source.copy();damaged.remove("DataVersion");preserve(damaged); byte[] originalRegion=Files.readAllBytes(world.resolve("region/r.0.0.mca")); - try(ChunkStorage loader=new ChunkStorage(world.resolve("reader"),UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),false)) { + try(ChunkStorage loader=new ChunkStorage(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"chunk"),world.resolve("reader"),UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),false)) { CompoundTag actual=loader.upgradeChunkTag(Level.OVERWORLD,()->{throw new AssertionError("Current source must not replay legacy structure conversion");},damaged.copy(),java.util.Optional.empty()); assertNotNull(expected.get("block_entities"));assertFalse(actual.contains("Level")); assertEquals(expected.get("block_entities"),actual.get("block_entities")); diff --git a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java index 4ada114..19ba3bc 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java @@ -35,7 +35,7 @@ private CompoundTag legacy() { body.put("Equipment",inventory);return body; } @Test void currentFormatUsesThe119SchemaAndMarker() { - assertEquals(3465,UpgradePreservation.MINECRAFT_VERSION);assertEquals(10,UpgradePreservation.DATA_VERSION); + assertEquals(3839,UpgradePreservation.MINECRAFT_VERSION);assertEquals(11,UpgradePreservation.DATA_VERSION); } @Test void legacyCorpseUuidSurvivesShieldingWithoutTruncatingCustomInventory() { CompoundTag body=legacy(),before=body.copy(),context=new CompoundTag();context.putInt("DataVersion",2230); diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java index b3d033e..ee0f106 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java @@ -20,7 +20,7 @@ class Native117EntityChunkTest { private static final UUID ID=UUID.fromString("00000000-1171-4171-8171-000000000011"); @AfterEach void close(){UpgradePreservation.close();System.out.println("ENTITY_CHUNK_FIXTURE="+world);} private CompoundTag convert(CompoundTag root,int source) { - return NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,root,source,3465); + return NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,root,source,3839); } @Test void nativeEntityChunkShieldsAllSlotsAuxRecoveryAndPassengers() throws Exception { CompoundTag corpse=body(ID,6),cart=new CompoundTag();cart.putString("id","minecraft:chest_minecart");cart.putUUID("UUID",new UUID(12,13)); @@ -28,7 +28,7 @@ private CompoundTag convert(CompoundTag root,int source) { CompoundTag raw=chunk(2586,cart),before=raw.copy();Path region=write(world,raw);byte[] bytes=Files.readAllBytes(region); UpgradePreservation.prepare(world); CompoundTag fixed=convert(raw,2586).getList("Entities",10).getCompound(0).getList("Passengers",10).getCompound(0); - assertEquals(10,fixed.getInt(UpgradePreservation.VERSION));assertEquals(2586,fixed.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(11,fixed.getInt(UpgradePreservation.VERSION));assertEquals(2586,fixed.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); ListTag records=fixed.getList("LootableBodiesUnresolved",10);assertEquals(50,records.size()); for(int slot=0;slot<48;slot++) {assertEquals(corpse.getList("Equipment",10).getCompound(slot),records.getCompound(slot).getCompound("Data"));assertEquals("Equipment",records.getCompound(slot).getString("Source"));} assertEquals(corpse.getList("Aux",10).getCompound(0),records.getCompound(48).getCompound("Data")); @@ -37,7 +37,7 @@ private CompoundTag convert(CompoundTag root,int source) { } @Test void currentMarkerProtectsNativeRootAgainstDamagedStamps() { CompoundTag corpse=body(ID,UpgradePreservation.DATA_VERSION); - for(int version:new int[]{-1,0,512,2230,2586,2730,2975,3337,3465}) { + for(int version:new int[]{-1,0,512,2230,2586,2730,2975,3337,3465,3839}) { CompoundTag raw=chunk(version,corpse),before=raw.copy(); CompoundTag fixed=convert(raw,version); assertEquals(corpse,fixed.getList("Entities",10).getCompound(0));assertEquals(before,raw); @@ -70,7 +70,7 @@ public Dynamic update(DSL.TypeReference type,Dynamic input,int from,in AtomicInteger calls=new AtomicInteger(); try { DataFixers.dataFixer=observe(nativeFixer,calls,previous,true); - assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(candidate)); + assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(candidate,candidate.resolve("level.dat"))); assertTrue(calls.get()>0);assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); } finally {DataFixers.dataFixer=original;} } @@ -80,24 +80,24 @@ public Dynamic update(DSL.TypeReference type,Dynamic input,int from,in AtomicInteger calls=new AtomicInteger();UpgradePreservation.close(); try { DataFixers.dataFixer=observe(nativeFixer,calls,null,false); - UpgradeJournal journal=UpgradePreservation.prepareEarly(world);assertEquals(2,journal.size());assertEquals(1,calls.get()); + UpgradeJournal journal=UpgradePreservation.prepareEarly(world,world.resolve("level.dat"));assertEquals(2,journal.size());assertEquals(1,calls.get()); assertSame(journal,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); } finally {DataFixers.dataFixer=original;} } @Test void unchangedIndexCannotReuseAnOldPreflightAfterTheFixerChanges() throws Exception { DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); Path region=write(world,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); - UpgradeJournal previous=UpgradePreservation.prepareEarly(world);AtomicInteger calls=new AtomicInteger(); + UpgradeJournal previous=UpgradePreservation.prepareEarly(world,world.resolve("level.dat"));AtomicInteger calls=new AtomicInteger(); try { DataFixers.dataFixer=observe(nativeFixer,calls,previous,true); - assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(world));assertTrue(calls.get()>0); + assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(world,world.resolve("level.dat")));assertTrue(calls.get()>0); assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); } finally {DataFixers.dataFixer=original;} } @Test void actualServerFixerIsValidatedAfterTheEarlyLevelHook() throws Exception { DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); Path region=write(world,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); - UpgradeJournal previous=UpgradePreservation.prepareEarly(world);AtomicInteger calls=new AtomicInteger(); + UpgradeJournal previous=UpgradePreservation.prepareEarly(world,world.resolve("level.dat"));AtomicInteger calls=new AtomicInteger(); try { DataFixers.dataFixer=observe(nativeFixer,calls,previous,true); assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareForStart(world)); diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java index 5d57ab3..7584d89 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java @@ -33,7 +33,7 @@ private CompoundTag chunk(int x,int z,CompoundTag... entities) { private Path write(String dimension,String storage,int x,int z,CompoundTag root) throws Exception { Path directory=world.resolve(dimension).resolve(storage);Files.createDirectories(directory); Path path=directory.resolve("r."+(x>>5)+"."+(z>>5)+".mca"); - try(RegionFile region=new RegionFile(path,directory,false)) { + try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),path,directory,false)) { try(DataOutputStream out=region.getChunkDataOutputStream(new ChunkPos(x,z))) {NbtIo.write(root,out);} } return path; @@ -64,7 +64,7 @@ private CompoundTag original(UpgradeJournal journal,UUID id,int x,int z) throws byte[] original=Arrays.copyOfRange(before,base+5,base+4+size); assertArrayEquals(original,Files.readAllBytes(world.resolve("data/lootablebodies-upgrade/chunks/"+saved.getString("OriginalChunk")+".bin"))); try(DataInputStream input=new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(original)))) { - assertEquals(root,NbtIo.read(input,new NbtAccounter(64L*1024*1024))); + assertEquals(root,NbtIo.read(input,NbtAccounter.create(64L*1024*1024))); } assertArrayEquals(before,Files.readAllBytes(file)); } @@ -109,7 +109,7 @@ private CompoundTag original(UpgradeJournal journal,UUID id,int x,int z) throws } @Test void nativeEmptyEntityRegionPlaceholderStaysEmpty() throws Exception { Path directory=world.resolve("entities");Files.createDirectories(directory);Path file=directory.resolve("r.0.0.mca"); - try(RegionFile region=new RegionFile(file,directory,false)) {assertNull(region.getChunkDataInputStream(new ChunkPos(0,0)));} + try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),file,directory,false)) {assertNull(region.getChunkDataInputStream(new ChunkPos(0,0)));} byte[] before=Files.readAllBytes(file);assertEquals(0,UpgradeJournal.scan(world).size());assertArrayEquals(before,Files.readAllBytes(file)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java index ba9f8b2..367a828 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java @@ -36,14 +36,14 @@ class Native117ExternalSidecarTest { CompoundTag chunk=new CompoundTag();chunk.putInt("DataVersion",2730);chunk.putIntArray("Position",new int[]{0,0}); ListTag entities=new ListTag();entities.add(body);chunk.put("Entities",entities); Path file=directory.resolve("r.0.0.mca"); - try(RegionFile region=new RegionFile(file,directory,false)) { + try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),file,directory,false)) { try(DataOutputStream out=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(chunk,out);} } Path sidecar=directory.resolve("c.0.0.mcc");assertTrue(Files.isRegularFile(sidecar));assertTrue(Files.size(sidecar)>1024*1024); byte[] raw=Files.readAllBytes(file),payload=Files.readAllBytes(sidecar); int location=ByteBuffer.wrap(raw).getInt(),offset=(location>>>8)*4096; assertEquals(0x82,raw[offset+4]&255,"Native writer must actually select external zlib storage"); - try(RegionFile region=new RegionFile(file,directory,false); + try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),file,directory,false); DataInputStream in=region.getChunkDataInputStream(new ChunkPos(0,0))) { assertEquals(chunk,NbtIo.read(in),"Native reader must decode the original external entity chunk"); } diff --git a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java index a42f794..6a57649 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java @@ -15,7 +15,7 @@ class Native118BiomeRegistryTest { private String nativeName(String name,int source){ return net.minecraft.util.datafix.DataFixers.getDataFixer().update(References.BIOME, - new Dynamic<>(NbtOps.INSTANCE,StringTag.valueOf(name)),source,2975).asString().getOrThrow(false,error->{throw new AssertionError(error);}); + new Dynamic<>(NbtOps.INSTANCE,StringTag.valueOf(name)),source,2975).asString().getOrThrow(AssertionError::new); } @Test void everyCavesAndCliffsAliasMatchesTheActualNativeFixAndRegisteredTarget(){ CavesAndCliffsRenames.RENAMES.forEach((old,current)->{ diff --git a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java index e7230bb..c3e442d 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java @@ -58,10 +58,12 @@ private int calls(ClassNode node,String name) { @Test void actualNativeClassGetsExactlyOneEarlyReadAndOneHeaderWriteHook()throws Exception { ClassNode before=original();Object transformer=transformer();ClassNode after=apply(transformer,before); assertEquals(1,calls(after,"beforeRead"));assertEquals(1,calls(after,"beforeWrite")); - MethodNode read=after.methods.stream().filter(m->m.name.equals("readAdditionalLevelSaveData")&&m.desc.equals("()V")).findFirst().orElseThrow(); + MethodNode read=after.methods.stream().filter(m->m.name.equals("getDataTag")&&m.desc.equals("(Z)Lcom/mojang/serialization/Dynamic;")).findFirst().orElseThrow(); AbstractInsnNode first=read.instructions.getFirst();while(first.getOpcode()<0)first=first.getNext(); - assertEquals(Opcodes.ALOAD,first.getOpcode());assertTrue(first.getNext() instanceof MethodInsnNode); - assertEquals("beforeRead",((MethodInsnNode)first.getNext()).name); + assertEquals(Opcodes.ALOAD,first.getOpcode()); + assertEquals(Opcodes.ILOAD,first.getNext().getOpcode()); + assertTrue(first.getNext().getNext() instanceof MethodInsnNode); + assertEquals("beforeRead",((MethodInsnNode)first.getNext().getNext()).name); ClassWriter writer=new ClassWriter(ClassWriter.COMPUTE_MAXS);after.accept(writer);assertTrue(writer.toByteArray().length>0); StringWriter errors=new StringWriter();CheckClassAdapter.verify(new ClassReader(writer.toByteArray()),getClass().getClassLoader(),false,new PrintWriter(errors)); assertEquals("",errors.toString(),"Actual native bytecode must pass operand-stack verification"); diff --git a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java index 75eac9e..1918426 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java @@ -20,11 +20,11 @@ class Native118EntityBoundaryTest { @Test void released17WriterWithDamagedOuterStampStillUsesItsPreservedSource() throws Exception { CompoundTag corpse=body(new UUID(118,171),7),raw=chunk(-1,corpse),before=raw.copy(); Path file=write(world,raw);byte[] original=Files.readAllBytes(file); - UpgradeJournal journal=UpgradePreservation.prepareEarly(world); + UpgradeJournal journal=UpgradePreservation.prepareEarly(world,world.resolve("level.dat")); assertEquals(1,journal.size()); - CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3465); + CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3839); CompoundTag actual=converted.getList("Entities",10).getCompound(0); - assertEquals(10,actual.getInt(UpgradePreservation.VERSION)); + assertEquals(11,actual.getInt(UpgradePreservation.VERSION)); assertEquals(2730,actual.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); assertEquals(50,actual.getList("LootableBodiesUnresolved",10).size()); for(int slot=0;slot<48;slot++)assertEquals(corpse.getList("Equipment",10).getCompound(slot),actual.getList("LootableBodiesUnresolved",10).getCompound(slot).getCompound("Data")); @@ -34,15 +34,15 @@ class Native118EntityBoundaryTest { CompoundTag root=new CompoundTag();root.putInt("DataVersion",4000);root.putInt("xPos",0);root.putInt("zPos",0); root.put("sections",new ListTag());root.put("entities",new ListTag()); Path folder=world.resolve("region");Files.createDirectories(folder);Path file=folder.resolve("r.0.0.mca"); - try(RegionFile region=new RegionFile(file,folder,false);DataOutputStream output=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(root,output);} + try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),file,folder,false);DataOutputStream output=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(root,output);} byte[] original=Files.readAllBytes(file); - assertThrows(IOException.class,()->UpgradePreservation.prepareEarly(world)); + assertThrows(IOException.class,()->UpgradePreservation.prepareEarly(world,world.resolve("level.dat"))); assertNull(UpgradePreservation.journal());assertArrayEquals(original,Files.readAllBytes(file)); } @Test void futureSeparateEntityVersionRefusesEvenWhenItsCorpseHasAnOlderKnownMarker() throws Exception { - CompoundTag raw=chunk(3466,body(new UUID(118,172),7)),before=raw.copy(); + CompoundTag raw=chunk(3840,body(new UUID(118,172),7)),before=raw.copy(); Path file=write(world,raw);byte[] original=Files.readAllBytes(file); - assertThrows(IOException.class,()->UpgradePreservation.prepareEarly(world)); + assertThrows(IOException.class,()->UpgradePreservation.prepareEarly(world,world.resolve("level.dat"))); assertNull(UpgradePreservation.journal());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); } @Test void malformedNonIntSeparateStampUsesVerifiedReleased17SourceWithoutGuessingFutureData() throws Exception { @@ -50,9 +50,9 @@ class Native118EntityBoundaryTest { for(int i=0;ifixed(raw,true));assertEquals(before,raw); } @Test void verticalValuesOutsideTheNativeLoadLimitRetainRawDataBeforeClamping() { for(boolean separate:new boolean[]{false,true})for(double y:new double[]{-20000000.01,20000000.01,-3e7,3e7}) { - CompoundTag body=body(ID,10);body.put("Pos",pos(8.5,y,8.5));CompoundTag raw=root(body,separate,0,0),before=raw.copy(); + CompoundTag body=body(ID,UpgradePreservation.DATA_VERSION);body.put("Pos",pos(8.5,y,8.5));CompoundTag raw=root(body,separate,0,0),before=raw.copy(); CompoundTag actual=first(fixed(raw,separate),separate); assertEquals(pos(8.5,64,8.5),actual.get("Pos"));assertEquals(before,raw); assertEquals(body.get("Pos"),actual.getList(RECOVERY,10).getCompound(0).get("Data")); @@ -73,7 +73,7 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { } @Test void nativeHorizontalAndVerticalBoundaryValuesRemainExact() { for(boolean separate:new boolean[]{false,true})for(double y:new double[]{-2e7,2e7}) { - CompoundTag body=body(ID,10);body.put("Pos",pos(3e7,y,3e7)); + CompoundTag body=body(ID,UpgradePreservation.DATA_VERSION);body.put("Pos",pos(3e7,y,3e7)); assertEquals(body,first(fixed(root(body,separate,1875000,1875000),separate),separate)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java index 93dda7a..c0815ed 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java @@ -27,7 +27,7 @@ private CompoundTag root(CompoundTag... bodies) { } private Path write(CompoundTag root)throws Exception { Path folder=world.resolve("region");Files.createDirectories(folder);Path file=folder.resolve("r.0.0.mca"); - try(RegionFile region=new RegionFile(file,folder,false);DataOutputStream output=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(root,output);} + try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),file,folder,false);DataOutputStream output=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(root,output);} return file; } private CompoundTag legacy() { @@ -47,9 +47,9 @@ private CompoundTag legacy() { @Test void missingStampOnModernThreeDimensionalBiomesStillRunsNativeHeightFix()throws Exception { CompoundTag old=legacy();int[] biomes=new int[1024];for(int i=0;i=16); - CompoundTag staged=fixed.getList("entities",10).getCompound(0);assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + CompoundTag staged=fixed.getList("entities",10).getCompound(0);assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); assertEquals(50,staged.getList("LootableBodiesUnresolved",10).size());assertEquals(2730,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); old.remove("__context");assertEquals(before,old);assertArrayEquals(original,Files.readAllBytes(file)); } @@ -91,19 +129,19 @@ private CompoundTag legacy() { // The native schema conversion leaves DataVersion to ChunkStorage's // caller. This stale stamp does not describe the newly converted root. CompoundTag nativeCopy=old.copy();nativeCopy.getCompound("Level").put("Entities",new ListTag()); - CompoundTag nativeResult=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeCopy,2730,3465); + CompoundTag nativeResult=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeCopy,2730,3839); assertFalse(nativeResult.contains("Level"));assertEquals(Integer.MAX_VALUE,nativeResult.getInt("DataVersion")); - CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,3465); - assertFalse(fixed.contains("Level"));assertEquals(3465,fixed.getInt("DataVersion")); + CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,3839); + assertFalse(fixed.contains("Level"));assertEquals(3839,fixed.getInt("DataVersion")); assertEquals(nativeResult.get("sections"),fixed.get("sections")); assertEquals(body.getUUID("UUID"),fixed.getList("entities",10).getCompound(0).getUUID("UUID")); assertEquals(before,old);assertArrayEquals(original,Files.readAllBytes(file)); } @Test void genuinelyFutureFlattenedTerrainStillRefusesBeforeConversion()throws Exception { - CompoundTag future=root();future.putInt("DataVersion",3466);CompoundTag before=future.copy(); + CompoundTag future=root();future.putInt("DataVersion",3840);CompoundTag before=future.copy(); Path file=write(future);byte[] original=Files.readAllBytes(file); assertThrows(IOException.class,()->UpgradeJournal.scan(world)); - assertThrows(Error.class,()->NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,future,3466,3465)); + assertThrows(Error.class,()->NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,future,3840,3839)); assertEquals(before,future);assertArrayEquals(original,Files.readAllBytes(file)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java index 6bd15e1..1685aa2 100644 --- a/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java @@ -36,7 +36,7 @@ static ScriptEngine actualEngine()throws Exception { blackboardFixture(); Class factory=Class.forName("net.minecraftforge.coremod.NashornFactory"); String source=factory.getProtectionDomain().getCodeSource().getLocation().toString(); - assertTrue(source.endsWith("coremods-5.2.4.jar"),source);System.out.println("COREMOD_ENGINE_SOURCE="+source); + assertTrue(source.endsWith("coremods-5.2.6.jar"),source);System.out.println("COREMOD_ENGINE_SOURCE="+source); Method create=factory.getDeclaredMethod("createEngine");create.setAccessible(true); return (ScriptEngine)create.invoke(null); } diff --git a/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java new file mode 100644 index 0000000..d973b12 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java @@ -0,0 +1,215 @@ +package cyano.lootable.upgrade; + +import net.minecraft.SharedConstants; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class Native1206ComponentsTest { + @BeforeAll static void bootstrap() { + cyano.lootable.buildtest.Native117TestEnvironment.initialize(); + ItemMigration.register(UpgradePreservation.wrapper(DataFixers.getDataFixer())); + } + + private static CompoundTag context(int version) { + CompoundTag context=new CompoundTag(); + context.putInt("DataVersion",version); + return context; + } + + private static CompoundTag legacy(String id,int count,int slot) { + CompoundTag stack=new CompoundTag(); + stack.putString("id","minecraft:"+id); + stack.putByte("Count",(byte)count); + stack.putByte("Slot",(byte)slot); + return stack; + } + + @Test void targetUsesTheActual3839NativeSchema() { + assertEquals(3839,SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + assertEquals(3839,UpgradePreservation.MINECRAFT_VERSION); + assertEquals(11,UpgradePreservation.DATA_VERSION); + } + + @Test void released120ItemShapePreservesDamageEnchantmentsAndTypedCustomData() { + CompoundTag original=legacy("diamond_sword",1,47); + CompoundTag tag=new CompoundTag(); + tag.putInt("Damage",27); + CompoundTag display=new CompoundTag(); + display.putString("Name","{\"text\":\"Old Blade\"}"); + tag.put("display",display); + tag.putLongArray("keepsake",new long[]{Long.MIN_VALUE,190,Long.MAX_VALUE}); + CompoundTag entry=new CompoundTag(); + entry.putString("id","minecraft:sharpness"); + entry.putShort("lvl",(short)3); + ListTag enchantments=new ListTag(); + enchantments.add(entry); + tag.put("Enchantments",enchantments); + original.put("tag",tag); + CompoundTag before=original.copy(); + CompoundTag converted=ItemMigration.convert(original,context(3465)); + assertNotNull(converted); + assertEquals(47,converted.getByte("Slot")); + assertEquals(1,converted.getInt("count")); + assertFalse(converted.contains("Count")); + CompoundTag components=converted.getCompound("components"); + assertEquals(27,components.getInt("minecraft:damage")); + assertEquals(3,components.getCompound("minecraft:enchantments").getCompound("levels").getInt("minecraft:sharpness")); + assertArrayEquals(tag.getLongArray("keepsake"),components.getCompound("minecraft:custom_data").getLongArray("keepsake")); + RegistryAccess registry=RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + ItemStack stack=ItemStack.parse(registry,converted).orElseThrow(); + assertSame(Items.DIAMOND_SWORD,stack.getItem()); + assertEquals(27,stack.getDamageValue()); + CompoundTag encoded=((CompoundTag)stack.save(registry)).getCompound("components"); + assertEquals("\"Old Blade\"",encoded.getString("minecraft:custom_name")); + assertTrue(ItemMigration.sameSavedComponents(components,encoded)); + CompoundTag changed=components.copy(); + changed.putString("minecraft:custom_name","{\"text\":\"Old Blade\",\"italic\":false}"); + assertFalse(ItemMigration.sameSavedComponents(changed,encoded)); + changed=components.copy(); + changed.getCompound("minecraft:custom_data").putString("extra","must retain"); + assertFalse(ItemMigration.sameSavedComponents(changed,encoded)); + assertEquals(before,original); + } + + @Test void original110BannerKeepsItsFlattenedColourAndComponentData() { + CompoundTag original=legacy("banner",2,8); + original.putShort("Damage",(short)0); + CompoundTag tag=new CompoundTag(),block=new CompoundTag(),display=new CompoundTag(); + block.putInt("Base",5); + tag.put("BlockEntityTag",block); + display.putString("Name","Original keepsake 8"); + tag.put("display",display); + tag.putLong("typed_long",1234567890123L); + tag.putByteArray("typed_bytes",new byte[]{0,-1,17}); + original.put("tag",tag); + CompoundTag before=original.copy(); + CompoundTag flattened=ItemMigration.convertNbt(original,context(512)); + assertNotNull(flattened); + CompoundTag migrated=ItemMigration.convert(original,context(512)); + assertNotNull(migrated,flattened::toString); + assertEquals("minecraft:purple_banner",migrated.getString("id")); + assertEquals(2,migrated.getInt("count")); + assertEquals(8,migrated.getByte("Slot")); + CompoundTag components=migrated.getCompound("components"); + assertEquals("purple",components.getString("minecraft:base_color")); + assertEquals(1234567890123L,components.getCompound("minecraft:custom_data").getLong("typed_long")); + assertArrayEquals(new byte[]{0,-1,17},components.getCompound("minecraft:custom_data").getByteArray("typed_bytes")); + assertEquals(before,original); + } + + @Test void unknownBannerBaseIsRetainedInsteadOfApplyingAnArbitraryColour() { + CompoundTag original=legacy("purple_banner",2,8); + CompoundTag tag=new CompoundTag(),block=new CompoundTag(); + block.putInt("Base",16); + tag.put("BlockEntityTag",block); + original.put("tag",tag); + CompoundTag before=original.copy(); + assertNull(ItemMigration.convert(original,context(3465))); + assertEquals(before,original); + } + + @Test void nestedContainerKeepsOriginalIndexItemAndCount() { + CompoundTag original=legacy("blue_shulker_box",1,8); + CompoundTag block=new CompoundTag(); + block.putString("id","minecraft:shulker_box"); + ListTag children=new ListTag(); + children.add(legacy("emerald",29,3)); + block.put("Items",children); + CompoundTag tag=new CompoundTag(); + tag.put("BlockEntityTag",block); + original.put("tag",tag); + CompoundTag converted=ItemMigration.convert(original,context(3465)); + assertNotNull(converted); + ListTag slots=converted.getCompound("components").getList("minecraft:container",10); + assertEquals(1,slots.size()); + assertEquals(3,slots.getCompound(0).getInt("slot")); + assertEquals("minecraft:emerald",slots.getCompound(0).getCompound("item").getString("id")); + assertEquals(29,slots.getCompound(0).getCompound("item").getInt("count")); + assertEquals(3,children.getCompound(0).getByte("Slot")); + } + + @Test void legacyCapabilityPayloadAndDuplicateNestedSlotsAreLeftForRecovery() { + CompoundTag withCaps=legacy("diamond",7,47); + CompoundTag caps=new CompoundTag(); + caps.putIntArray("fixture",new int[]{1,2,3}); + withCaps.put("ForgeCaps",caps); + CompoundTag original=withCaps.copy(); + assertNull(ItemMigration.convert(withCaps,context(3465))); + assertEquals(original,withCaps); + + CompoundTag box=legacy("blue_shulker_box",1,8); + CompoundTag block=new CompoundTag(); + block.putString("id","minecraft:shulker_box"); + ListTag children=new ListTag(); + children.add(legacy("diamond",17,3)); + children.add(legacy("emerald",8,3)); + block.put("Items",children); + CompoundTag tag=new CompoundTag(); + tag.put("BlockEntityTag",block); + box.put("tag",tag); + assertNull(ItemMigration.convert(box,context(3465))); + } + + @Test void registryAliasesAgreeWithNative1206Fixers() { + var from=net.minecraft.resources.ResourceLocation.withDefaultNamespace("grass"); + var block=new CompoundTag(); + block.putString("Name",from.toString()); + var fixed=NativeNbt.update(DataFixers.getDataFixer(),net.minecraft.util.datafix.fixes.References.BLOCK_STATE, + block,3465,3839); + assertEquals(fixed.getString("Name"),LegacyRegistryMappings.blockReplacement(from).toString()); + for(String name:new String[]{"grass","scute"}) { + var key=net.minecraft.resources.ResourceLocation.withDefaultNamespace(name); + var migrated=ItemMigration.convert(legacy(name,2,47),context(3465)); + assertNotNull(migrated); + assertEquals(migrated.getString("id"),LegacyRegistryMappings.itemReplacement(key).toString()); + } + var item=legacy("diamond_sword",1,6); + CompoundTag tag=new CompoundTag(), enchant=new CompoundTag(); + enchant.putString("id","minecraft:sweeping"); + enchant.putShort("lvl",(short)2); + ListTag enchants=new ListTag(); + enchants.add(enchant); + tag.put("Enchantments",enchants); + item.put("tag",tag); + var migrated=ItemMigration.convert(item,context(3465)); + assertNotNull(migrated); + var sweeping=net.minecraft.resources.ResourceLocation.withDefaultNamespace("sweeping"); + var replacement=LegacyRegistryMappings.enchantmentReplacement(sweeping); + assertEquals(2,migrated.getCompound("components").getCompound("minecraft:enchantments").getCompound("levels").getInt(replacement.toString())); + assertNull(LegacyRegistryMappings.itemReplacement(net.minecraft.resources.ResourceLocation.tryParse("other:scute"))); + } + + @Test void emptyPotionRemainsEmptyWithoutAnArtificialRegistryAlias() { + var key=net.minecraft.resources.ResourceLocation.withDefaultNamespace("empty"); + assertTrue(LegacyRegistryMappings.retiredPotion(key)); + assertFalse(BuiltInRegistries.POTION.containsKey(key)); + CompoundTag original=legacy("potion",1,6), tag=new CompoundTag(); + tag.putString("Potion",key.toString()); + original.put("tag",tag); + var migrated=ItemMigration.convert(original,context(3465)); + assertNotNull(migrated); + assertFalse(migrated.getCompound("components").getCompound("minecraft:potion_contents").contains("potion")); + assertEquals("minecraft:empty",original.getCompound("tag").getString("Potion")); + } + + @Test void validCurrentComponentItemIsCopiedWithoutLegacyConversion() { + RegistryAccess registry=RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + ItemStack stack=new ItemStack(Items.DIAMOND,31); + stack.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, + net.minecraft.network.chat.Component.literal("Saved keepsake")); + CompoundTag current=(CompoundTag)stack.save(registry); + current.putByte("Slot",(byte)47); + CompoundTag converted=ItemMigration.convert(current,context(3839)); + assertEquals(current,converted); + assertNotSame(current,converted); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java index b47a5fc..eabb85d 100644 --- a/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java @@ -57,6 +57,21 @@ class Native120RegistryRenameTest { assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(new ResourceLocation("minecraft:music.overworld.unknown"))); } + @Test void retiredParrotImitationsOnlyResolveTheFiveObsoleteVanillaSoundBindings() throws Exception { + for (String mob : List.of("enderman", "panda", "polar_bear", "wolf", "zombie_pigman")) { + String path = "entity.parrot.imitate." + mob; + ResourceLocation retired = ResourceLocation.withDefaultNamespace(path); + assertFalse(BuiltInRegistries.SOUND_EVENT.containsKey(retired), path); + assertEquals(MissingMappingsEvent.Action.IGNORE, soundAction(retired), path); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(new ResourceLocation("othermod", path)), path); + } + ResourceLocation piglin = ResourceLocation.withDefaultNamespace("entity.parrot.imitate.piglin"); + assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(piglin)); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(piglin)); + assertEquals(MissingMappingsEvent.Action.DEFAULT, + soundAction(ResourceLocation.withDefaultNamespace("entity.parrot.imitate.unknown"))); + } + private static MissingMappingsEvent.Action soundAction(ResourceLocation key) throws Exception { MissingMappingsEvent.Mapping mapping = new MissingMappingsEvent.Mapping<>(null, null, key, 17); LegacyRegistryMappings.sounds(new MissingMappingsEvent(Registries.SOUND_EVENT, null, List.of(mapping))); diff --git a/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java b/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java index 46af26b..882536b 100644 --- a/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java @@ -20,12 +20,12 @@ private static int targetDataVersion() { } @Test void targetMatchesTheActualNativeWorldVersion() { net.minecraft.SharedConstants.tryDetectVersion(); - assertEquals("1.20.1",net.minecraft.SharedConstants.getCurrentVersion().getName()); - assertEquals(15,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); - assertEquals(15,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); - assertEquals(3465,targetDataVersion()); + assertEquals("1.20.6",net.minecraft.SharedConstants.getCurrentVersion().getName()); + assertEquals(32,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); + assertEquals(41,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); + assertEquals(3839,targetDataVersion()); assertEquals(targetDataVersion(),UpgradePreservation.MINECRAFT_VERSION); - assertEquals(10,UpgradePreservation.DATA_VERSION); + assertEquals(11,UpgradePreservation.DATA_VERSION); } @Test void released16MarkerIsMigratedWithAllSlotsAndOriginalsRetained() { assertEquals(2586,cyano.lootable.buildtest.HistoricalVersions.dataVersion("1.16.5")); @@ -39,7 +39,7 @@ private static int targetDataVersion() { body.put("Equipment",items);CompoundTag before=body.copy(); CompoundTag source=new CompoundTag();source.putInt("DataVersion",2586); CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); - assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); assertEquals(48,staged.getList("LootableBodiesUnresolved",10).size()); for(int slot=0;slot<48;slot++) { CompoundTag record=staged.getList("LootableBodiesUnresolved",10).getCompound(slot); @@ -49,9 +49,9 @@ private static int targetDataVersion() { } assertEquals(before,body); } - @Test void productionClassesUseJava17() throws Exception { + @Test void productionClassesUseJava21() throws Exception { try(DataInputStream bytes=new DataInputStream(getClass().getResourceAsStream("/cyano/lootable/entities/EntityLootableBody.class"))) { - assertEquals(0xCAFEBABE,bytes.readInt());bytes.readUnsignedShort();assertEquals(61,bytes.readUnsignedShort()); + assertEquals(0xCAFEBABE,bytes.readInt());bytes.readUnsignedShort();assertEquals(65,bytes.readUnsignedShort()); } } @Test void marker6OverridesADamagedOuterEntityStamp() { @@ -70,7 +70,7 @@ private static int targetDataVersion() { CompoundTag body=Native117RegionFixtures.body(java.util.UUID.randomUUID(),7),before=body.copy(); CompoundTag source=new CompoundTag();source.putInt("DataVersion",2730); CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); - assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); assertEquals(2730,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); ListTag records=staged.getList("LootableBodiesUnresolved",10);assertEquals(50,records.size()); for(int slot=0;slot<48;slot++) { @@ -94,7 +94,7 @@ private static int targetDataVersion() { CompoundTag body=Native117RegionFixtures.body(java.util.UUID.randomUUID(),8),before=body.copy(); CompoundTag source=new CompoundTag();source.putInt("DataVersion",2975); CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); - assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); assertEquals(2975,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); ListTag records=staged.getList("LootableBodiesUnresolved",10);assertEquals(50,records.size()); for(int slot=0;slot<48;slot++) { @@ -124,7 +124,7 @@ private static int targetDataVersion() { CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), net.minecraft.util.datafix.fixes.References.ENTITY_CHUNK,root,512,targetDataVersion()); CompoundTag staged=fixed.getList("Entities",10).getCompound(0); - assertEquals(10,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); assertEquals(3337,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); CompoundTag detached=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); diff --git a/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java b/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java index df05616..3cf7970 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java +++ b/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java @@ -14,6 +14,7 @@ public static int dataVersion(String minecraft) { if ("1.17.1".equals(minecraft)) return 2730; if ("1.18.2".equals(minecraft)) return 2975; if ("1.19.4".equals(minecraft)) return 3337; + if ("1.20.1".equals(minecraft)) return 3465; throw new IllegalArgumentException("Unsupported historical Minecraft " + minecraft); } } diff --git a/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java index 0a030cc..9bfb7f0 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java @@ -1,9 +1,10 @@ package cyano.lootable.buildtest; import java.util.List; -import java.util.ArrayList; import io.netty.buffer.Unpooled; -import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket; import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; @@ -19,24 +20,29 @@ public final class NativeMenuObservation { this.content=content;this.containerId=containerId;this.stateId=stateId;this.slot=slot;this.items=items;this.carried=carried;this.item=item; } public static NativeMenuObservation decode(Packet packet) { - boolean content=packet instanceof ClientboundContainerSetContentPacket; - if(!content && !(packet instanceof ClientboundContainerSetSlotPacket))return null; - FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + if(!(packet instanceof ClientboundContainerSetContentPacket) + && !(packet instanceof ClientboundContainerSetSlotPacket))return null; + var registries=RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + var wire=new RegistryFriendlyByteBuf(Unpooled.buffer(),registries); try { - // Receiving-side getters can be stripped from a dedicated build. - // Serialize independently instead of touching live packet state. - packet.write(wire); - int window=content?wire.readUnsignedByte():wire.readByte();int state=wire.readVarInt(); NativeMenuObservation result; - if(content) { - int count=wire.readVarInt();List items=new ArrayList<>(); - if(count<0 || count>4096)throw new IllegalStateException("Unexpected native menu slot count"); - for(int index=0;index dimension=null; - long seed=0; - for(Field field:ClientboundRespawnPacket.class.getDeclaredFields()) { - if(field.getType()==ResourceKey.class) { - field.setAccessible(true);ResourceKey key=(ResourceKey)field.get(packet); - if(key==null || !key.isFor(net.minecraft.core.registries.Registries.DIMENSION))continue; - if(dimension!=null)throw new AssertionError("Ambiguous respawn world key"); - dimension=key; - } else if(field.getType()==long.class) { - field.setAccessible(true);seed=field.getLong(packet); - } - } - if(dimension==null)throw new AssertionError("Missing respawn world key"); - return "RESPAWN dimension="+dimension.location()+" hashedSeed="+seed; - } catch(ReflectiveOperationException invalid) {throw new AssertionError("Fixture respawn observation",invalid);} + var spawn=packet.commonPlayerSpawnInfo(); + return "RESPAWN dimension="+spawn.dimension().location()+" hashedSeed="+spawn.seed(); } } diff --git a/src/testSupport/java/cyano/lootable/upgrade/Native117RegionFixtures.java b/src/testSupport/java/cyano/lootable/upgrade/Native117RegionFixtures.java index 4926378..92c1a17 100644 --- a/src/testSupport/java/cyano/lootable/upgrade/Native117RegionFixtures.java +++ b/src/testSupport/java/cyano/lootable/upgrade/Native117RegionFixtures.java @@ -29,7 +29,7 @@ public static CompoundTag chunk(int version,CompoundTag... roots) { } public static Path write(Path world,CompoundTag chunk) throws Exception { Path folder=world.resolve("entities");Files.createDirectories(folder);Path path=folder.resolve("r.0.0.mca"); - try(RegionFile region=new RegionFile(path,folder,false)) { + try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),path,folder,false)) { try(DataOutputStream output=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(chunk,output);} } return path; diff --git a/tools/fixtures/forge50-server-default.toml b/tools/fixtures/forge50-server-default.toml new file mode 100644 index 0000000..9a52897 --- /dev/null +++ b/tools/fixtures/forge50-server-default.toml @@ -0,0 +1,14 @@ +#Server configuration settings +[server] + #The permission handler used by the server. Defaults to forge:default_handler if no such handler with that name is registered. + permissionHandler = "forge:default_handler" + #Set this to true to enable living entities to use items with durations of 0. Fixes being able to use Eyes of Ender repeatedly by holding down the use button. Disabled by default as it could change interactions with items of existing mods. + useItemWithDurationZero = false + #Set this to true to remove any Entity (Note: Does not include BlockEntities) that throws an error in its tick method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + removeErroringEntities = false + #Set this to true to enable advertising the dedicated server to local LAN clients so that it shows up in the Multiplayer screen automatically. + advertiseDedicatedServerToLan = true + #Set this to true to check the entire entity's collision bounding box for ladders instead of just the block they are in. Causes noticeable differences in mechanics so default is vanilla behavior. Default: false. + fullBoundingBoxLadders = false + #Set this to true to remove any BlockEntity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + removeErroringBlockEntities = false diff --git a/tools/fixtures/native1206-flat-level.dat b/tools/fixtures/native1206-flat-level.dat new file mode 100644 index 0000000000000000000000000000000000000000..c1c11507ff50be9b66dee9ab058c1d016f62a1aa GIT binary patch literal 39597 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%0AK1rVB95y*gp29_} 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 literal 0 HcmV?d00001 diff --git a/tools/fixtures/native1206-flat-level.provenance.json b/tools/fixtures/native1206-flat-level.provenance.json new file mode 100644 index 0000000..9be3e00 --- /dev/null +++ b/tools/fixtures/native1206-flat-level.provenance.json @@ -0,0 +1,12 @@ +{ + "schema": 1, + "minecraft": "1.20.6", + "forge": "50.2.14", + "data_version": 3839, + "generator": "minecraft:flat", + "sha256": "34253D55B2B3A4F1CE4107FACF5AF64ED496264ED53804405F6CDC6D45E56E74", + "seed": 8676444864448831725, + "source": "Owned disposable world header saved by the official dedicated server after the native component-stack smoke", + "synthetic": true, + "historical_writer_proof": false +} diff --git a/tools/fixtures/native1206-forge-only-flat-level.dat b/tools/fixtures/native1206-forge-only-flat-level.dat new file mode 100644 index 0000000000000000000000000000000000000000..7d9e2078357336eef120d74d4c71ecf24718e755 GIT binary patch literal 39505 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 z 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 F0svNrRD%Ej literal 0 HcmV?d00001 diff --git a/tools/hosted_policy.py b/tools/hosted_policy.py index b0e6ec2..cb4a845 100644 --- a/tools/hosted_policy.py +++ b/tools/hosted_policy.py @@ -19,7 +19,7 @@ def gradle_roots(command): return roots def validate_workflow(text): - if "java-version: '17.0.20+101'" not in text or any('java-version:' in line and '17.0.20.1+1' in line for line in text.splitlines()): + if "java-version: '21.0.7+6'" not in text: raise ValueError('Use the setup-java SemVer identifier for the exact qualified JDK') commands='\n'.join(line.strip() for line in text.splitlines() if line.startswith(' ') and not line.strip().startswith('#')) for forbidden in ('unittest discover','-m unittest','native_runtime.py','native117_ci.py','verify_world_upgrades.py','verify_packaged','prepare_native','xvfb-run','--seed-only'): @@ -64,7 +64,7 @@ def require_qualified_manifest(record): if record.get('local_qualification_complete') is not True or record.get('qualification_integrity_status')!='verified': raise ValueError('Local qualification evidence is incomplete or on an integrity hold') version=record.get('version','') - if not re.fullmatch(r'\d+\.\d+\.\d+\.120011',version) or record.get('minecraft')!='1.20.1' or record.get('forge')!='47.4.28':raise ValueError('Wrong target qualification') + if not re.fullmatch(r'\d+\.\d+\.\d+\.120061',version) or record.get('minecraft')!='1.20.6' or record.get('forge')!='50.2.14':raise ValueError('Wrong target qualification') base='LootableBodies-'+version names={'CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom'} if set(record.get('files',{}))!=names or any(not re.fullmatch('[A-F0-9]{64}',value or '') for value in record['files'].values()):raise ValueError('Incomplete six-file declaration') @@ -74,18 +74,18 @@ def require_qualified_manifest(record): def check_source(root): validate_workflow((root/'.github/workflows/native120.yml').read_text(encoding='utf-8')) for workflow in ('codeql-analysis.yml','deploy-release.yml'): - if any('java-version:' in line and '17.0.20.1+1' in line for line in (root/'.github/workflows'/workflow).read_text(encoding='utf-8').splitlines()): - raise ValueError('Non-SemVer JDK input in '+workflow) + if "'21.0.7+6'" not in (root/'.github/workflows'/workflow).read_text(encoding='utf-8'): + raise ValueError('Missing qualified Java 21 version in '+workflow) if 'feature/**' in(root/'.github/workflows/build.yml').read_text():raise ValueError('Copied legacy CI still catches modern feature branches') if 'workflow_dispatch:' in(root/'.github/workflows/migration-matrix.yml').read_text():raise ValueError('Copied legacy migration workflow can still dispatch on a modern ref') guard=(root/'settings.gradle').read_text() + policy=json.loads((root/'tools/qualification-policy.json').read_text()) if any(rule not in guard for rule in ("System.getenv('GITHUB_ACTIONS') == 'true'", 'org.gradle.api.tasks.testing.Test', 'org.gradle.api.tasks.JavaExec', 'Run expensive verification locally before publishing')): raise ValueError('Missing hosted test and game task guard') runtime=(root/'gradle/runtime.gradle').read_text() if "tasks.named('check') { dependsOn 'integrationTest' }" in runtime or "tasks.register('localRuntimeVerification')" not in runtime:raise ValueError('Game checks must remain explicit local gates') - policy=json.loads((root/'tools/qualification-policy.json').read_text()) if policy['hosted']['full_java_suite'] or policy['hosted']['full_python_suite'] or policy['hosted']['gameplay_or_migration'] or policy['phase_consolidation']['enabled']:raise ValueError('Unvalidated expensive-test policy') if __name__=='__main__': diff --git a/tools/local_case_reuse.py b/tools/local_case_reuse.py index 21e2d9a..b6fbd8b 100644 --- a/tools/local_case_reuse.py +++ b/tools/local_case_reuse.py @@ -13,8 +13,8 @@ 'tools/native_runtime.py','tools/runtime_diagnostics.py','tools/command_logs.py', 'tools/prepared_worlds.py','tools/historical_workers.py','tools/release_identity.py', 'tools/nbt_regions.py','tools/native120_persistence.py','tools/audit_runtime_logs.py', - 'tools/native_evidence.py','tools/native120_server_inputs.json', - 'tools/native120_worker_budget.json','tools/native120_logs.py') + 'tools/native_evidence.py','tools/native1206_server_inputs.json', + 'tools/native1206_worker_budget.json','tools/native120_logs.py') def normal_program(text): tree=ast.parse(text) diff --git a/tools/migration_ci.py b/tools/migration_ci.py index 28c62c6..be1257c 100644 --- a/tools/migration_ci.py +++ b/tools/migration_ci.py @@ -138,7 +138,7 @@ def run_shard(project, java, shard, commit, total=12): manifest = json.loads((project / 'build/migration-ci-inputs/inputs.json').read_text()) if manifest['head'] != commit: raise ValueError('Prepared commit does not match this shard') - inputs = ['build/libs/' + MAIN_JAR, 'build/integration-test-mod/lootablebodies-build-tests-reobf.jar'] + inputs = ['build/libs/' + MAIN_JAR, 'build/integration-test-mod/lootablebodies-build-tests.jar'] for name in inputs: if digest(project / name) != manifest['files'][name]['sha256']: raise ValueError('Prepared candidate/probe changed') @@ -191,7 +191,7 @@ def main(): actual=subprocess.check_output(['git','rev-parse','HEAD'],cwd=project,text=True).strip() if actual != args.commit:raise ValueError('Checkout does not match the declared input commit') if args.mode=='seal': - paths=['build/libs/'+MAIN_JAR,'build/integration-test-mod/lootablebodies-build-tests-reobf.jar','build/migration-seeds/seed-report.json'] + paths=['build/libs/'+MAIN_JAR,'build/integration-test-mod/lootablebodies-build-tests.jar','build/migration-seeds/seed-report.json'] paths += native_input_paths(project) report=json.loads((project/'build/migration-seeds/seed-report.json').read_text())['origins'] if set(report)!=set(ORIGINS):raise ValueError('Missing prepared historical worlds') diff --git a/tools/native1206_client_diagnostics.json b/tools/native1206_client_diagnostics.json new file mode 100644 index 0000000..00e5ba7 --- /dev/null +++ b/tools/native1206_client_diagnostics.json @@ -0,0 +1,14 @@ +{ + "minecraft": "1.20.6", + "forge": "50.2.14", + "platform": "Windows amd64", + "qualified": true, + "events": [ + "minecraft:item.goat_horn.play", + "minecraft:entity.goat.screaming.horn_break" + ], + "control_sha256": "2D954016DD9DEADBF8B2B5A2586782EE4910D093897D752D5D724DC5B695DE48", + "proof_sha256": "F31EAE648C319A32D48A4396E118BF963C40BB52711A6AA02C0CFBB331BBAD26", + "sound_definition_sha256": "C73D61C4DE2E673D2AD70FB916A6CFC37D04AB9EF67785BDEE37CB6A4818CE2D", + "scope": "Two ordered, unbound vanilla sound events observed without the mod or a world. Other diagnostics remain strict." +} diff --git a/tools/native1206_server_inputs.json b/tools/native1206_server_inputs.json new file mode 100644 index 0000000..7692ea3 --- /dev/null +++ b/tools/native1206_server_inputs.json @@ -0,0 +1,96 @@ +{ + "forge-1.20.6-50.2.14-shim.jar": "6A71494807787DDDFE3C2AA8AA2E7960F21E96F35418EB3EA1A15341EAE3BE9A", + "libraries/com/electronwill/night-config/core/3.6.7/core-3.6.7.jar": "FA1272C026B500338DAE2997F5EA4B02C632F54769DE9649A6A390CA1CB234BE", + "libraries/com/electronwill/night-config/toml/3.6.7/toml-3.6.7.jar": "C0A69BB6E3DADEBB183184B57F92B865C9FD7C8191F37133A70E519AC09DAD11", + "libraries/com/github/jponge/lzma-java/1.3/lzma-java-1.3.jar": "75CCC0E24D37ED5B8916E1BCFFAD1E067C2B1CFA21B635287F968DB5C3C1963A", + "libraries/com/github/oshi/oshi-core/6.4.10/oshi-core-6.4.10.jar": "FEF001A7A13515C273F30915915580782AFBC45273CC07AA15994E51563EF310", + "libraries/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar": "4241C14A7727C34FEEA6507EC801318A3D4A90F070E4525681079FB94EE4C593", + "libraries/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar": "A171EE4C734DD2DA837E4B16BE9DF4661AFAB72A41ADAF31EB84DFDAF936CA26", + "libraries/com/google/guava/guava/32.1.2-jre/guava-32.1.2-jre.jar": "BC65DEA7CFD9E4DACF8419D8AF0E741655857D27885BB35D943D7187FC3A8FCE", + "libraries/com/mojang/authlib/6.0.54/authlib-6.0.54.jar": "319EA7B53B5E52F62AD3E2B81E9DB7F0751240EDAC548BD74F5F19E35DC21A3B", + "libraries/com/mojang/brigadier/1.2.9/brigadier-1.2.9.jar": "1F46F6C679AD59AD5D6228E7B91962217A588F25BD259E0691BC9C5A54B68BCD", + "libraries/com/mojang/datafixerupper/7.0.14/datafixerupper-7.0.14.jar": "D7392613FEBFF2C7146CEFAD7C1245EC102432393F7AAADBFBC78FF43EF2076B", + "libraries/com/mojang/logging/1.2.7/logging-1.2.7.jar": "3DC3190204BAC5F4184D8405BFE1F724F73A6A15A0D3F36EC7F69E6872C1A76C", + "libraries/com/nothome/javaxdelta/2.0.1/javaxdelta-2.0.1.jar": "A661DFCB749053FC26EADA2A0A8E619A8EAD1CA9EDA29E1698687AA2CDD80152", + "libraries/commons-io/commons-io/2.15.1/commons-io-2.15.1.jar": "A58AF12EE1B68CFD2EBB0C27CAEF164F084381A00EC81A48CC275FD7EA54E154", + "libraries/de/oceanlabs/mcp/mcp_config/1.20.6-20240429.135109/mcp_config-1.20.6-20240429.135109-mappings.tsrg": "1C94263E77E168E639A274E7A3FEA0B2AD1D1514747F39F1009CCE8ECE90E481", + "libraries/de/oceanlabs/mcp/mcp_config/1.20.6-20240429.135109/mcp_config-1.20.6-20240429.135109-srg2off.jar": "AF92CE5D63C8601AF71D6574BB8B201BD1CED04A1CF19D1DF35F0061543C5FF4", + "libraries/de/oceanlabs/mcp/mcp_config/1.20.6-20240429.135109/mcp_config-1.20.6-20240429.135109.zip": "C3C2BFF08FD91B2C983ECC9419E8968A65F3CD1927B1630B020E7383505B2C03", + "libraries/de/siegmar/fastcsv/2.2.2/fastcsv-2.2.2.jar": "BFDA71ABCD7C2B62DB498DEBA24C14AEC0E9059A9C183477840AC1BAE9C98AD2", + "libraries/io/netty/netty-buffer/4.1.97.Final/netty-buffer-4.1.97.Final.jar": "A4393F5B395486CC74D0325C9B41311ABB9513BA0FD7EF8CF46E9345C3BFFBEA", + "libraries/io/netty/netty-codec/4.1.97.Final/netty-codec-4.1.97.Final.jar": "BCC96737A0F912FCF031CF8C45EBDA352A90A40437DB0832CAAD3D5A63618B38", + "libraries/io/netty/netty-common/4.1.97.Final/netty-common-4.1.97.Final.jar": "A8ACA0C8E9347ACC75C885ECC749195D9775369AA520B9276F2D1128210A6C17", + "libraries/io/netty/netty-handler/4.1.97.Final/netty-handler-4.1.97.Final.jar": "BD4771D19149CBC9C7464ED2D58035D79CDFED7ADB59D38718B0D8E7A3DCC2DE", + "libraries/io/netty/netty-resolver/4.1.97.Final/netty-resolver-4.1.97.Final.jar": "38A018C6D9FB2CB11B72881354782B45080BBD20B9A0AD6CDE28B80D431ED0B1", + "libraries/io/netty/netty-transport/4.1.97.Final/netty-transport-4.1.97.Final.jar": "197FD2D6C6B4AFE677D9E95BF2E36B49A0BCABDFCE0583683FB73F29A3F5A407", + "libraries/io/netty/netty-transport-classes-epoll/4.1.97.Final/netty-transport-classes-epoll-4.1.97.Final.jar": "EE65FA17FE65F18FD22269F92BDDAD85BFB3A263CF65EBA01E116A2F30B86FF5", + "libraries/io/netty/netty-transport-native-epoll/4.1.97.Final/netty-transport-native-epoll-4.1.97.Final-linux-aarch_64.jar": "93B5FE8F3829DCB5E033D8D40D59159366FF4CF8266C903BBD6A4AEED9E59BAC", + "libraries/io/netty/netty-transport-native-epoll/4.1.97.Final/netty-transport-native-epoll-4.1.97.Final-linux-x86_64.jar": "1E83FC9F82E5415CDBB688C6A5C6BBBD7D198E9FDD6FDF64B3DC5D54DD1ACFD0", + "libraries/io/netty/netty-transport-native-unix-common/4.1.97.Final/netty-transport-native-unix-common-4.1.97.Final.jar": "412FE140257C2DDA5A5E15BEE911298BD61928D03EE6BE4DB588E82C196C5DC6", + "libraries/it/unimi/dsi/fastutil/8.5.12/fastutil-8.5.12.jar": "B5543EE08D062D551CF0A5C9BC0FB70588B0382079029BA48941FA9C9BE8A5D4", + "libraries/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar": "34ED1E1F27FA896BCA50DBC4E99CF3732967CEC387A7A0D5E3486C09673FE8C6", + "libraries/net/java/dev/jna/jna-platform/5.14.0/jna-platform-5.14.0.jar": "AE4CACEB3840730C2537F9B7FB55A01BABA580286B4122951488BCEE558C2449", + "libraries/net/jodah/typetools/0.6.3/typetools-0.6.3.jar": "408300867804BE05D1083CC0287DD3A21295A9E76090E288013BA5A4182DC96D", + "libraries/net/minecraft/server/1.20.6/server-1.20.6-bundled.jar": "C6D01D018CA782E506F0EC60652D47FD565078BE9122B625C1681BC86C29C7EC", + "libraries/net/minecraft/server/1.20.6/server-1.20.6-mappings.tsrg": "6725AC3174AD71DF0DFC8309E8D466E04680DB4B86AEE48B9A2FCE1FA33970FB", + "libraries/net/minecraft/server/1.20.6/server-1.20.6-official.jar": "111F426083945E96104E06D1734315C9604300D9FF8ACD72F99BD809A1C90EA0", + "libraries/net/minecraft/server/1.20.6/server-1.20.6-unpacked.jar": "BB610DAA96B3784645B78FD7C0B887291414C9605CD913FC79CF985F523F4F5B", + "libraries/net/minecraftforge/accesstransformers/8.2.2/accesstransformers-8.2.2.jar": "98FB8323A249F529215D27D312FC2C4BD934EB74617771F9A0887906E2F3C456", + "libraries/net/minecraftforge/binarypatcher/1.2.0/binarypatcher-1.2.0.jar": "F52C278C1E8E741C79A5F4AB6AA8E9CF63DF50C85B4ACDA5E1F045FC94FF31DF", + "libraries/net/minecraftforge/bootstrap/2.1.8/bootstrap-2.1.8.jar": "6B0641339BC8872C4EB6978E302935D74FCC93871E3EA2B1B81F03FB3B16E708", + "libraries/net/minecraftforge/bootstrap-api/2.1.8/bootstrap-api-2.1.8.jar": "B9B5B5B3B441A42920EEAF181395E858642874F09F64CD7F9E4A0CD7A34B8938", + "libraries/net/minecraftforge/coremods/5.2.6/coremods-5.2.6.jar": "2FD763B690E701EDA11B7099DDBC17B2A4E2DD0969902793DA78638CA8C8A8C8", + "libraries/net/minecraftforge/eventbus/6.2.33/eventbus-6.2.33.jar": "C9C9AAB0979E08A0489FC497BA433276648D5192608E53535BAD881DFE14D712", + "libraries/net/minecraftforge/fmlcore/1.20.6-50.2.14/fmlcore-1.20.6-50.2.14.jar": "51FD7143080BC4077F950DFC348B6E32DD642453E0B39315381CEAF259642157", + "libraries/net/minecraftforge/fmlearlydisplay/1.20.6-50.2.14/fmlearlydisplay-1.20.6-50.2.14.jar": "624D11757E2D5AA394907C40B6B6BA18D65B4361A9205B417E619E22DFE57532", + "libraries/net/minecraftforge/fmlloader/1.20.6-50.2.14/fmlloader-1.20.6-50.2.14.jar": "A093746E4F2ACBDAC171246E999AB199A384048EC4FF1F45D738B7825CCE975A", + "libraries/net/minecraftforge/forge/1.20.6-50.2.14/forge-1.20.6-50.2.14-server.jar": "609A3BEF499E84E93069A1BE7A7E6E38662952B13B0063495AD5804C1829FB35", + "libraries/net/minecraftforge/forge/1.20.6-50.2.14/forge-1.20.6-50.2.14-shim.jar": "6A71494807787DDDFE3C2AA8AA2E7960F21E96F35418EB3EA1A15341EAE3BE9A", + "libraries/net/minecraftforge/forge/1.20.6-50.2.14/forge-1.20.6-50.2.14-universal.jar": "D85AD06F65F653413C945FC7D23EAFF8A4E375B5521C774A7E936E67B454C86D", + "libraries/net/minecraftforge/forge/1.20.6-50.2.14/unix_args.txt": "085772DF00FF7D2C0E184F99B3F11FA659CB8AB26DBD20EEB0697F7941F6F0E2", + "libraries/net/minecraftforge/forge/1.20.6-50.2.14/win_args.txt": "085772DF00FF7D2C0E184F99B3F11FA659CB8AB26DBD20EEB0697F7941F6F0E2", + "libraries/net/minecraftforge/ForgeAutoRenamingTool/1.0.6/ForgeAutoRenamingTool-1.0.6.jar": "072368476225FF6D29468D07C49CD04AA5EAE11B8AB99AB3DD05802B07BC22C4", + "libraries/net/minecraftforge/forgespi/7.1.4/forgespi-7.1.4.jar": "0C4465D84268036764062D34142719C58551460DD05E420586BA59881E6F4F20", + "libraries/net/minecraftforge/installertools/1.4.3/installertools-1.4.3.jar": "AA8F649A671C13E3335804C4A0ADD53D0F4FDBFC4AE7F3FA7717CA7784C9FBE9", + "libraries/net/minecraftforge/JarJarFileSystems/0.3.26/JarJarFileSystems-0.3.26.jar": "906F3B4F5402DFAE04A78369F2964CB3985B3E80701E66430622AF81F97896DA", + "libraries/net/minecraftforge/JarJarMetadata/0.3.26/JarJarMetadata-0.3.26.jar": "AD79528D4A25CFD4A3DB8046917E02A305D2D9A0EBAF59D8CAA8E9729C097849", + "libraries/net/minecraftforge/JarJarSelector/0.3.26/JarJarSelector-0.3.26.jar": "4D06AAFEFB10E91759932B896F8BE364ACAC06CE5A84925DBEB73F208A5AFFCB", + "libraries/net/minecraftforge/javafmllanguage/1.20.6-50.2.14/javafmllanguage-1.20.6-50.2.14.jar": "6F2D6DF1EE988EBC2FDE8D4D83DA54D03651550243392B1CE461F9C0C258154C", + "libraries/net/minecraftforge/lowcodelanguage/1.20.6-50.2.14/lowcodelanguage-1.20.6-50.2.14.jar": "45B24DF2FBFA90923A4734B4FB4196ACCFEDF673570380A418430DB0334E1078", + "libraries/net/minecraftforge/mclanguage/1.20.6-50.2.14/mclanguage-1.20.6-50.2.14.jar": "17FF39075782F7A01BB22528A21B9394A2F37D5FB6F1CB0BDC13FF0595ADD5C3", + "libraries/net/minecraftforge/mergetool-api/1.0/mergetool-api-1.0.jar": "100F0C4D658B9DD73600CE4BB9C75657E0651F0F0F5E0B2F977F85D8F495350B", + "libraries/net/minecraftforge/modlauncher/10.2.4/modlauncher-10.2.4.jar": "FD5B1BEC9C1700052959DD2A3A934496C68CD7C0ECB18AED4D1FBA784122A134", + "libraries/net/minecraftforge/securemodules/2.2.21/securemodules-2.2.21.jar": "A8254AC61485C95F652D1B3A92395FCEE375FA7D12AE996EFD672ED8BEABB3A0", + "libraries/net/minecraftforge/srgutils/0.5.1/srgutils-0.5.1.jar": "B9BDE328A5787C43A58E29305B6BCAB31E63EA52244E3AC83A07A845AB3AAF30", + "libraries/net/minecraftforge/srgutils/0.5.10/srgutils-0.5.10.jar": "46E73138A2718DC2A378C2A0AC71756CE627819729C462B81DD54A60FB97387A", + "libraries/net/minecraftforge/srgutils/0.5.6/srgutils-0.5.6.jar": "1AAF0178677961242C52E101C1BA8E1EE96DAD054295E20EB15B98F4E6FE2E3D", + "libraries/net/minecraftforge/unsafe/0.9.2/unsafe-0.9.2.jar": "F5035AB2DD1C02AA354F948A28DAF2FF4215956194A98634F076AD71610FE3A4", + "libraries/net/minecrell/terminalconsoleappender/1.2.0/terminalconsoleappender-1.2.0.jar": "912E432C9355C4B2025D788ED87FD72BB95542E2FC967AD6875FBBEB6E1C7A1C", + "libraries/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar": "DF26CC58F235F477DB07F753BA5A3AB243EBE5789D9F89ECF68DD62EA9A66C28", + "libraries/net/sf/jopt-simple/jopt-simple/6.0-alpha-3/jopt-simple-6.0-alpha-3.jar": "D440CEB839D6ADB5CFA81AA321813D60255E68A3E9CA564C91126599310ABDE5", + "libraries/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.jar": "7B96BF3EE68949ABB5BC465559AC270E0551596FA34523FDDF890EC418DDE13C", + "libraries/org/apache/logging/log4j/log4j-api/2.22.1/log4j-api-2.22.1.jar": "5D7BEAE7FF15D8516D6517121D7F12A79A6AC180DF64B5FCEC55D5BE21056E53", + "libraries/org/apache/logging/log4j/log4j-core/2.22.1/log4j-core-2.22.1.jar": "46DCCECAC556623D8E2CE8648496824A82951D139062A4E61148AFF1A25ED18D", + "libraries/org/apache/logging/log4j/log4j-slf4j2-impl/2.22.1/log4j-slf4j2-impl-2.22.1.jar": "1920CD69D8682C7EF249BB407A3D0685723AA1785867CF4C53258DA6BF14DA97", + "libraries/org/apache/maven/maven-artifact/3.8.8/maven-artifact-3.8.8.jar": "813233A8485CBAF97B1F9A2C17CEF723B068F7260A4319CF4958F221D04B9937", + "libraries/org/jline/jline-reader/3.12.1/jline-reader-3.12.1.jar": "4D1037C7110027ED64816C510FAD2AFA0A5AA9BBC38FB9F0E772870823FCAD2A", + "libraries/org/jline/jline-terminal/3.12.1/jline-terminal-3.12.1.jar": "EB6115680F732562F0E8E6777F127DECE81E23B25015925F3C4A2A900D48CC9E", + "libraries/org/jline/jline-terminal-jna/3.12.1/jline-terminal-jna-3.12.1.jar": "4FC674C339345D4EC2F6CE6D21E90724312D0A5835013BA9DD092C14440625BA", + "libraries/org/joml/joml/1.10.5/joml-1.10.5.jar": "CAC9F22F83A7AA33EEBDA73C16FF5261E3CB4911B6BAFCF4C79EA486099D0C9A", + "libraries/org/lz4/lz4-java/1.8.0/lz4-java-1.8.0.jar": "D74A3334FB35195009B338A951F918203D6BBCA3D1D359033DC33EDD1CADC9EF", + "libraries/org/openjdk/nashorn/nashorn-core/15.4/nashorn-core-15.4.jar": "6F816E84DFD63A81D4EAA7829C08337BBAFF3EC683FF3BF6BBD90D017A00DC6F", + "libraries/org/ow2/asm/asm/9.5/asm-9.5.jar": "B62E84B5980729751B0458C534CF1366F727542BB8D158621335682A460F0353", + "libraries/org/ow2/asm/asm/9.7/asm-9.7.jar": "ADF46D5E34940BDF148ECDD26A9EE8EEA94496A72034FF7141066B3EEA5C4E9D", + "libraries/org/ow2/asm/asm/9.9.1/asm-9.9.1.jar": "6F3828A215C920059A5EFA2FB55C233D6C54EC5CADCA99CE1B1BDD10077C7DDD", + "libraries/org/ow2/asm/asm-analysis/9.9.1/asm-analysis-9.9.1.jar": "6260BFFC8EC008DD1B713702C7994E2C94D188A3DA5BEF9E87278A16DF6A7522", + "libraries/org/ow2/asm/asm-commons/9.5/asm-commons-9.5.jar": "72EEE9FBAFB9DE8D9463F20DD584A48CEEB7E5152AD4C987BFBE17DD4811C9AE", + "libraries/org/ow2/asm/asm-commons/9.7/asm-commons-9.7.jar": "389BC247958E049FC9A0408D398C92C6D370C18035120395D4CBA1D9D9304B7A", + "libraries/org/ow2/asm/asm-commons/9.9.1/asm-commons-9.9.1.jar": "C2319E014CE7199F2B7F7D56D6BB991863168C3F4B6CD6C9F542A4937EF7EF88", + "libraries/org/ow2/asm/asm-tree/9.5/asm-tree-9.5.jar": "3C33A648191079AEAEAEB7C19A49B153952F9E40FE86FBAC5205554DDD9ACD94", + "libraries/org/ow2/asm/asm-tree/9.7/asm-tree-9.7.jar": "62F4B3BC436045C1ACB5C3BA2D8EC556EC3369093D7F5D06C747EB04B56D52B1", + "libraries/org/ow2/asm/asm-tree/9.9.1/asm-tree-9.9.1.jar": "0F3555096B720B820BBACAB0B515589BEE0200BEE099BDA14C561738AE837BA1", + "libraries/org/ow2/asm/asm-util/9.9.1/asm-util-9.9.1.jar": "C5EBBBEAF68126AF094B42FA4800F59BC4413ABD02D95B9AEFAD722CD257E207", + "libraries/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.jar": "0818930DC8D7DEBB403204611691DA58E49D42C50B6FFCFDCE02DADB7C3C2B6C", + "libraries/org/spongepowered/mixin/0.8.5/mixin-0.8.5.jar": "CA15A907E4D1F6B38CEFED51D7DF96A83347DCFB0BC8CED53111519E991D887D", + "libraries/trove/trove/1.0.2/trove-1.0.2.jar": "AD68A86CFD0255A2BC0525CE11EC49640CAEA7E34FF91EB89C3193DCCCD77F05" +} diff --git a/tools/native1206_worker_budget.json b/tools/native1206_worker_budget.json new file mode 100644 index 0000000..15a1929 --- /dev/null +++ b/tools/native1206_worker_budget.json @@ -0,0 +1,11 @@ +{ + "minecraft": "1.20.6", + "forge": "50.2.14", + "reloads": 200, + "jvms": 4, + "qualified": true, + "jvm_args": [], + "controller_sha256": "3F1AB207CF4E698A2C12BFD8260181603F9F78A2695A8DCEA4688DD1951292F8", + "server_input_manifest_sha256": "4FFD267133402025E443D867BB693304A06173277A8C209AE2F172D014706A05", + "qualification_evidence_sha256": "EE8EB711DC9666D1E199CFFBF3F83005DA6D472CE249A359EED1594A3B386251" +} diff --git a/tools/native120_build.py b/tools/native120_build.py index 4fb4634..7987d8d 100644 --- a/tools/native120_build.py +++ b/tools/native120_build.py @@ -7,7 +7,7 @@ def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest().upper() def check_bundle(manifest,bundle): record=json.loads(Path(manifest).read_text());bundle=Path(bundle) - if (record.get('version'),record.get('minecraft'),record.get('forge'))!=(MOD_VERSION,'1.20.1','47.4.28'): + if (record.get('version'),record.get('minecraft'),record.get('forge'))!=(MOD_VERSION,'1.20.6','50.2.14'): raise ValueError('Wrong target release identity') if record.get('local_qualification_complete') is not True:raise ValueError('Local gameplay qualification is incomplete') if record.get('qualification_integrity_status')!='verified':raise ValueError('Local qualification evidence is on an integrity hold or unverified') diff --git a/tools/native120_client.py b/tools/native120_client.py index aaf9f36..0c86675 100644 --- a/tools/native120_client.py +++ b/tools/native120_client.py @@ -1,4 +1,4 @@ -"""Pinned native47 client preparation and descriptor-driven Java17 launch.""" +"""Pinned Forge 50 client preparation and descriptor-driven Java 21 launch.""" import hashlib import json import os @@ -8,10 +8,10 @@ import struct import zipfile -BASE_VERSION = '1.20.1' -VERSION = '1.20.1-forge-47.4.28' -BASE_SHA1 = '72ae93b0934df5541860e3e6b72cf883c74da491' -FORGE_SHA256 = 'C8E1B6EB02F0D72E5BE8C55ACE50BC10AA2C20F082E41B0D32E7D16CC28B7B56' +BASE_VERSION = '1.20.6' +VERSION = '1.20.6-forge-50.2.14' +BASE_SHA1 = '4dd84bf5b646a75cba5c0c5f7723eb478a2b78af' +FORGE_SHA256 = '40A8892EAB75917AA96D5D63DD92F9BE8BA86A3D50429B96A12A1F2F206FEB46' def native_for_platform(library,system,architecture): @@ -43,9 +43,9 @@ def descriptors(runtime): base = runtime/'versions'/BASE_VERSION/(BASE_VERSION+'.json') forge = runtime/'versions'/VERSION/(VERSION+'.json') if not base.is_file() or hashlib.sha1(base.read_bytes()).hexdigest() != BASE_SHA1: - raise RuntimeError('Incorrect official native1.20.1 client descriptor') + raise RuntimeError('Incorrect official native 1.20.6 client descriptor') if not forge.is_file() or hashlib.sha256(forge.read_bytes()).hexdigest().upper() != FORGE_SHA256: - raise RuntimeError('Incorrect installed native47 client descriptor') + raise RuntimeError('Incorrect installed Forge 50 client descriptor') return json.loads(base.read_text()), json.loads(forge.read_text()) @@ -170,14 +170,12 @@ def expand(arguments, values, *, system, architecture, os_version): def command(java, home, runtime, assets, prepared, *, username, extra=()): - java, home, assets = Path(java).resolve(), Path(home).resolve(), Path(assets).resolve() - release = dict(line.split('=',1) for line in (java.parent.parent/'release').read_text().splitlines() if '=' in line) - if release.get('SEMANTIC_VERSION') != '"17.0.20.1+1"' or release.get('IMPLEMENTOR') != '"Eclipse Adoptium"': - raise RuntimeError('Native client requires exact Temurin17.0.20.1+1') + from native_runtime import require_runtime_jdk + java, home, assets = require_runtime_jdk(java), Path(home).resolve(), Path(assets).resolve() base, forge, record = load(runtime,prepared) index = assets/'indexes'/(base['assetIndex']['id']+'.json') if not index.is_file() or hashlib.sha1(index.read_bytes()).hexdigest() != base['assetIndex']['sha1']: - raise RuntimeError('Incorrect official native1.20.1 asset index') + raise RuntimeError('Incorrect official native 1.20.6 asset index') values = {'version_name':VERSION,'library_directory':record['library_directory'], 'classpath_separator':os.pathsep,'classpath':os.pathsep.join(record['classpath']), 'natives_directory':record['natives'],'launcher_name':'LootableBodiesTests','launcher_version':'1', @@ -189,10 +187,14 @@ def command(java, home, runtime, assets, prepared, *, username, extra=()): 'clientid':'','auth_xuid':'', 'resolution_width':'854','resolution_height':'480'} options = dict(system=record['system'],architecture=record['architecture'],os_version=platform.version()) - # The inherited profile's game version differs from the actual vanilla jar - # filename. Forge's JVM ignoreList must exclude that precise base jar from - # the transforming module layer, which already contains the native SRG jar. - jvm_values = dict(values,version_name=BASE_VERSION) - jvm = expand(base['arguments']['jvm']+forge['arguments']['jvm'],jvm_values,**options) + jvm = expand(base['arguments']['jvm']+forge['arguments']['jvm'],values,**options) game = expand(base['arguments']['game']+forge['arguments']['game'],values,**options) - return [str(java),'-Dfile.encoding=UTF-8','-Xmx2G',*jvm,*extra,forge['mainClass'],*game] + session = os.environ.get('LOOTABLEBODIES_TEST_SESSION_ENDPOINT') + auth = [] + if session is not None: + match = re.fullmatch(r'http://127\.0\.0\.1:([1-9][0-9]{0,4})', session) + if match is None or int(match[1]) > 65535: + raise ValueError('An owned client fixture endpoint must be loopback HTTP') + auth = ['-Dminecraft.api.session.host='+session, '-Dminecraft.api.services.host='+session] + return [str(java),'-Dfile.encoding=UTF-8','-Dstdout.encoding=UTF-8','-Dstderr.encoding=UTF-8', + '-Xmx2G',*jvm,*auth,*extra,forge['mainClass'],*game] diff --git a/tools/native120_client_logs.py b/tools/native120_client_logs.py index dba6bd4..db31bc5 100644 --- a/tools/native120_client_logs.py +++ b/tools/native120_client_logs.py @@ -1,18 +1,18 @@ -"""Explicit native47 client diagnostics, separate from strict server/error audits.""" +"""Explicit Forge 50 client diagnostics, separate from strict server/error audits.""" from pathlib import Path import gzip,hashlib,json,re,tempfile from native120_logs import audit as strict_audit def validated_baseline(policy_path=None,proof_path=None): project=Path(__file__).resolve().parents[1] - policy_path=Path(policy_path) if policy_path else Path(__file__).with_name('native120_client_diagnostics.json') - proof_path=Path(proof_path) if proof_path else project/'.gradle/native120-client-sound-control-01/proof.json' + policy_path=Path(policy_path) if policy_path else Path(__file__).with_name('native1206_client_diagnostics.json') + proof_path=Path(proof_path) if proof_path else project/'.gradle/native1206-client-sound-control-01/proof.json' if not policy_path.is_file() or not proof_path.is_file():raise RuntimeError('Missing native client diagnostic qualification') policy=json.loads(policy_path.read_text(encoding='utf-8')) raw=proof_path.read_bytes() if hashlib.sha256(raw).hexdigest().upper()!=policy.get('proof_sha256'):raise RuntimeError('Changed native client diagnostic qualification') proof=json.loads(raw) - if tuple(policy.get(k) for k in ('minecraft','forge','platform'))!=('1.20.1','47.4.28','Windows amd64') or policy.get('qualified') is not True:raise RuntimeError('Wrong native client diagnostic target') + if tuple(policy.get(k) for k in ('minecraft','forge','platform'))!=('1.20.6','50.2.14','Windows amd64') or policy.get('qualified') is not True:raise RuntimeError('Wrong native client diagnostic target') if any(proof.get(k)!=policy.get(k) for k in ('minecraft','forge','platform','events','control_sha256')):raise RuntimeError('Native client proof identity mismatch') if proof.get('qualified') is not True or proof.get('no_candidate') is not True or proof.get('no_world') is not True or type(proof.get('native_client_exit')) is not int or proof.get('native_client_exit')!=0 or proof.get('original_strict_audit_passed') is not False:raise RuntimeError('Incomplete independent native client control') for group in ('critical_files','raw_control_logs'): @@ -49,7 +49,7 @@ def audit_client(path,**options): # Preserve original bytes and their strict failure. Only a derived audit # view excludes these proved native WARN records; every generic error and # referenced missing-resource check still runs unchanged on that view. - with tempfile.TemporaryDirectory(prefix='native120-diagnostic-audit-') as directory: + with tempfile.TemporaryDirectory(prefix='native1206-diagnostic-audit-') as directory: view=Path(directory)/'client.log';view.write_text(residual,encoding='utf-8',newline='') try:result=strict_audit(view,**options) except RuntimeError as error:raise RuntimeError(str(path)+'\n'+str(error)) from error diff --git a/tools/native120_logs.py b/tools/native120_logs.py index 9e9d93a..1b94e5b 100644 --- a/tools/native120_logs.py +++ b/tools/native120_logs.py @@ -4,7 +4,7 @@ def audit(path,development=False,upgrade=False,fixture_logins=False,native=True): if fixture_logins: - raise RuntimeError('Legacy login-retry classification is not native47 qualification') + raise RuntimeError('Legacy login-retry classification is not Forge 50 qualification') if path.name.endswith('.log.gz'): with gzip.open(path,'rt',encoding='utf-8') as stream:text=stream.read() else:text=path.read_text(encoding='utf-8') @@ -15,6 +15,6 @@ def audit(path,development=False,upgrade=False,fixture_logins=False,native=True) # Retain all generic exception/assertion/missing-resource checks, without # importing older platform/development/upgrade waivers into this target. result=_legacy_audit(path) - result['target_minecraft']='1.20.1' + result['target_minecraft']='1.20.6' result['legacy_native_controls_used']=False return result diff --git a/tools/native120_persistence.py b/tools/native120_persistence.py index 02befb6..dfc217b 100644 --- a/tools/native120_persistence.py +++ b/tools/native120_persistence.py @@ -1,4 +1,4 @@ -"""Read-only native20 saved entity inspection, distinct from old-writer proof.""" +"""Read-only native 1.20.6 saved entity inspection, distinct from old-writer proof.""" import re,struct from nbt_regions import Tag,read_region @@ -26,15 +26,15 @@ def selected(region):return wave<0 or region.name==['r.0.0.mca','r.8.8.mca','r.1 rx,rz=map(int,match.groups()) for index,chunk in read_region(region).items(): root=chunk['root'].value;position=root.get('Position');entities=root.get('Entities') - if root.get('DataVersion')!=Tag(3,3465):raise ValueError('Wrong native20 entity-chunk data version') + if root.get('DataVersion')!=Tag(3,3839):raise ValueError('Wrong native 1.20.6 entity-chunk data version') if position is None or position.kind!=11 or len(position.value)!=8 or struct.unpack('>ii',position.value)!=(rx*32+(index&31),rz*32+(index>>5)): - raise ValueError('Wrong native20 entity-chunk Position/index') - if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native20 root Entities list') + raise ValueError('Wrong native 1.20.6 entity-chunk Position/index') + if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native 1.20.6 root Entities list') for entity in entities.value[1]: if entity.value.get('id',Tag(8,'')).value not in names:continue - if entity.value['id']!=Tag(8,'lootablebodies:corpse') or entity.value.get('LootableBodiesDataVersion')!=Tag(3,10):raise ValueError('Wrong native20 corpse identity/marker') + if entity.value['id']!=Tag(8,'lootablebodies:corpse') or entity.value.get('LootableBodiesDataVersion')!=Tag(3,11):raise ValueError('Wrong native 1.20.6 corpse identity/marker') identity=entity.value.get('UUID') - if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native20 corpse UUID type') + if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native 1.20.6 corpse UUID type') if identity.value in seen:raise ValueError('Duplicate persisted corpse UUID') seen.add(identity.value);count+=1 return count diff --git a/tools/native_evidence.py b/tools/native_evidence.py index b50fa01..cd03a5f 100644 --- a/tools/native_evidence.py +++ b/tools/native_evidence.py @@ -1,7 +1,7 @@ -"""Independently audit native19 local receipts, complete logs and diagnostic bytes. +"""Independently audit Forge 50 local receipts, complete logs and diagnostic bytes. Default auditing binds the recorded cwd to the actual owned process home. -Relocated auditing is a separately tested download contract; hosted native19 +Relocated auditing is a separately tested download contract; hosted Forge 50 execution and promotion remain disabled and have not been exercised. """ from pathlib import Path,PurePosixPath @@ -21,7 +21,7 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): # Native45 was qualified with defaults; no old CPU/worker/GC cap is valid. if any(s.startswith(('-XX:ActiveProcessorCount','-XX:ParallelGCThreads','-XX:ConcGCThreads','-Dmax.bg.threads')) or (s.startswith(('-XX:+Use','-XX:-Use')) and s.endswith('GC')) for s in command): - raise ValueError('Unqualified native45 worker or GC override') + raise ValueError('Unqualified Forge 50 worker or GC override') if relocated: # Downloaded artifacts retain their original machine cwd. Require the # complete case/condition suffix; all bytes are read only under home. @@ -34,8 +34,8 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): version=FORGE.removeprefix('forge-') expected={prefix+version+'/'+name for name in ('win_args.txt','unix_args.txt')} if relocated else { prefix+version+'/'+('win_args.txt' if __import__('os').name=='nt' else 'unix_args.txt')} - if len(arguments)!=1 or arguments[0] not in expected or '--nogui' not in command:raise ValueError('Receipt did not launch native45 modules') - if not any(item.startswith('-Xlog:gc*,safepoint:file=') for item in command):raise ValueError('Missing native17 unified GC evidence') + if len(arguments)!=1 or arguments[0] not in expected or '--nogui' not in command:raise ValueError('Receipt did not launch Forge 50 modules') + if not any(item.startswith('-Xlog:gc*,safepoint:file=') for item in command):raise ValueError('Missing Java 21 unified GC evidence') if not refusal and '-Dlootablebodies.phase='+phase not in command:raise ValueError('Receipt belongs to another phase') for name in ('gc.txt','initial-threads.txt','initial-heap-gc.txt'): if name not in receipt['evidence'] or not (directory/name).is_file() or not (directory/name).stat().st_size: @@ -82,7 +82,7 @@ def exact(path,sha): def audit_case_mods(home,report): from release_identity import MAIN_JAR - expected={MAIN_JAR:report['candidate_sha256'],'lootablebodies-build-tests-reobf.jar':report['probe_sha256']} + expected={MAIN_JAR:report['candidate_sha256'],'lootablebodies-build-tests.jar':report['probe_sha256']} mods=home/'mods' if mods.is_symlink() or not mods.is_dir():raise ValueError('Missing or aliased native mods directory') archives=[p for p in mods.iterdir() if p.suffix.lower()=='.jar'] diff --git a/tools/native_runtime.py b/tools/native_runtime.py index c44d9e1..5bb6f39 100644 --- a/tools/native_runtime.py +++ b/tools/native_runtime.py @@ -1,4 +1,4 @@ -"""Disposable official Forge47 launches, separate from the development bootstrap.""" +"""Disposable official Forge 50 launches, separate from the development bootstrap.""" import hashlib import json import os @@ -6,37 +6,40 @@ import platform import shutil import subprocess +import time import urllib.error import urllib.request import zipfile from command_logs import run_logged from runtime_diagnostics import run_monitored,validate_logging_inputs -FORGE = 'forge-1.20.1-47.4.28' -INSTALLER_SHA256 = 'D615397A6B2F1E99D4B454E2DB2B4CA2E94BE91D5E0243C8C7E023D965DB39E7' -SERVER_INPUTS = json.loads(Path(__file__).with_name('native120_server_inputs.json').read_text(encoding='utf-8')) -WORKER_BUDGET_PATH = Path(__file__).with_name('native120_worker_budget.json') -WORKER_BUDGET_PROOF = json.loads(WORKER_BUDGET_PATH.read_text(encoding='utf-8')) -# Native45 uses its actual default CPU/worker/GC configuration. A new target's +FORGE = 'forge-1.20.6-50.2.14' +INSTALLER_SHA256 = 'CF5ACACF52F694732732BB98D72B2871CFE16321990053DC4F3DDF5C20044F68' +SERVER_INPUTS = json.loads(Path(__file__).with_name('native1206_server_inputs.json').read_text(encoding='utf-8')) +WORKER_BUDGET_PATH = Path(__file__).with_name('native1206_worker_budget.json') +WORKER_BUDGET_PROOF = json.loads(WORKER_BUDGET_PATH.read_text(encoding='utf-8')) if WORKER_BUDGET_PATH.is_file() else {} +# Forge 50 uses its actual default CPU/worker/GC configuration. A new target's # history cannot borrow an older engine's resource qualification. HISTORICAL_JVM_ARGS = () def require_historical_budget(): + if not WORKER_BUDGET_PATH.is_file(): + raise RuntimeError('Forge 50 historical runtime policy has not been qualified') proof=json.loads(WORKER_BUDGET_PATH.read_text(encoding='utf-8')) - if tuple(proof.get(key) for key in ('minecraft','forge','reloads','jvms','qualified'))!=('1.20.1','47.4.28',200,4,True): - raise RuntimeError('Native45 historical runtime policy has not been qualified') + if tuple(proof.get(key) for key in ('minecraft','forge','reloads','jvms','qualified'))!=('1.20.6','50.2.14',200,4,True): + raise RuntimeError('Forge 50 historical runtime policy has not been qualified') if proof.get('jvm_args')!=[]: - raise RuntimeError('Unqualified native47 worker or GC override') + raise RuntimeError('Unqualified Forge 50 worker or GC override') source=Path(__file__).resolve().parents[1]/'src/controlTest/java/buildcontrol/ResourceControl.java' - manifest=Path(__file__).with_name('native120_server_inputs.json') + manifest=Path(__file__).with_name('native1206_server_inputs.json') if proof.get('controller_sha256')!=digest(source) or proof.get('server_input_manifest_sha256')!=digest(manifest): - raise RuntimeError('Native45 resource qualification source or SDK binding changed') + raise RuntimeError('Forge 50 resource qualification source or SDK binding changed') if not __import__('re').fullmatch('[A-F0-9]{64}',proof.get('qualification_evidence_sha256','')): - raise RuntimeError('Missing native47 resource qualification identity') - evidence=Path(__file__).resolve().parents[1]/'.gradle/native120-resource-qualification/proof.json' + raise RuntimeError('Missing Forge 50 resource qualification identity') + evidence=Path(__file__).resolve().parents[1]/'.gradle/native1206-resource-qualification/proof.json' if not evidence.is_file() or digest(evidence)!=proof['qualification_evidence_sha256']: - raise RuntimeError('Missing or changed actual local native47 resource qualification evidence') + raise RuntimeError('Missing or changed actual local Forge 50 resource qualification evidence') return HISTORICAL_JVM_ARGS @@ -57,15 +60,15 @@ def fingerprint(directory): def verify_server_inputs(distribution): """Pin the exact completed SDK after both official installer sides. - Native45 generated archives match the installer's declared expected hashes. + Forge 50 generated archives match the installer's declared expected hashes. No changed/truncated class, intermediate, resource or extra byte is accepted. """ actual={} for name,expected in SERVER_INPUTS.items(): artifact=Path(distribution)/name - if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing native47 SDK input: '+name) + if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing Forge 50 SDK input: '+name) value=digest(artifact) - if value!=expected:raise RuntimeError('Incorrect official Forge47 server input: '+name) + if value!=expected:raise RuntimeError('Incorrect official Forge 50 server input: '+name) actual[name]=value return actual @@ -87,10 +90,11 @@ def prepare_server(home, distribution, candidate=None, probe=None): distribution = distribution.resolve() libraries = distribution / 'libraries' if not libraries.is_dir(): - raise FileNotFoundError('Incomplete official Forge47 server installation') + raise FileNotFoundError('Incomplete official Forge 50 server installation') verify_server_inputs(distribution) home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() + shutil.copy2(distribution/(FORGE+'-shim.jar'),home/(FORGE+'-shim.jar')) seed_loader_defaults(home, deterministic=probe is not None) # Share this disposable installation's immutable libraries, not a user # installation or Gradle cache. Every matrix run fingerprints the inputs. @@ -122,18 +126,22 @@ def prepare_server(home, distribution, candidate=None, probe=None): (home / (name + '.json')).write_text('[]\n', encoding='utf-8') +def require_runtime_jdk(java): + java=Path(java).resolve() + release=java.parent.parent/'release' + if not release.is_file(): + raise RuntimeError('Native runtime requires exact Temurin 21.0.7+6 JDK metadata') + version=dict(line.split('=',1) for line in release.read_text(encoding='utf-8').splitlines() if '=' in line) + if version.get('JAVA_RUNTIME_VERSION')!='"21.0.7+6-LTS"' or version.get('IMPLEMENTOR')!='"Eclipse Adoptium"': + raise RuntimeError('Native runtime requires exact Temurin 21.0.7+6') + return java + + def run_java(java, home, phase, *, timeout=240, jvm_args=()): """Run one owned Forge JVM, retaining failure evidence before returning.""" if phase=='abort' or phase.startswith('upgrade-'):require_historical_budget() - java = Path(java).resolve() + java = require_runtime_jdk(java) home = home.resolve() - release = java.parent.parent / 'release' - if not release.is_file(): - raise RuntimeError('Native runtime requires exact Temurin 17.0.20.1+1 JDK metadata') - version = dict(line.split('=', 1) for line in release.read_text(encoding='utf-8').splitlines() if '=' in line) - if (version.get('SEMANTIC_VERSION') != '"17.0.20.1+1"' or - version.get('IMPLEMENTOR') != '"Eclipse Adoptium"'): - raise RuntimeError('Native runtime requires exact Temurin 17.0.20.1+1') log = home / (phase + '.log') diagnostics = home / (phase + '.diagnostics') copies = [home / (phase + '.' + name) for name in ('latest.log', 'debug.log')] @@ -148,11 +156,19 @@ def run_java(java, home, phase, *, timeout=240, jvm_args=()): gc = diagnostics.relative_to(home) / 'gc.txt' arguments = 'win_args.txt' if os.name == 'nt' else 'unix_args.txt' command = [str(java), '-Dfile.encoding=UTF-8', '-Xms256m', '-Xmx2G', '-Xlog:gc*,safepoint:file=' + gc.as_posix() + ':time,uptime,level,tags', - *jvm_args, '@libraries/net/minecraftforge/forge/1.20.1-47.4.28/' + arguments, '--nogui'] + *jvm_args, '@libraries/net/minecraftforge/forge/1.20.6-50.2.14/' + arguments, '--nogui'] validate_logging_inputs(command) def initial_diagnostics(process): # Short refusals and successful phases must retain real thread/heap # evidence too. This callback remains inside the unchanged deadline. + gc_log = diagnostics / 'gc.txt' + ready_by = time.monotonic() + min(5, timeout) + while not (gc_log.is_file() and b'[gc,init]' in gc_log.read_bytes()): + # The Windows launcher process can exist before jvm.dll loads. + # An owned GC initialization record proves the JVM is attachable. + if process.poll() is not None or time.monotonic() >= ready_by: + raise RuntimeError('Owned JVM did not initialize before initial diagnostics') + time.sleep(.025) for name, tool, option in [('initial-threads.txt',jstack,'-l'),('initial-heap-gc.txt',jstat,'-gc')]: with (diagnostics/name).open('x',encoding='utf-8') as output: subprocess.run([str(tool),option,str(process.pid)],stdout=output, @@ -190,14 +206,14 @@ def seed_flat_fixture(home, header, expected_sha256): version = data.value['DataVersion'] generator = data.value['WorldGenSettings'].value['dimensions'].value[ 'minecraft:overworld'].value['generator'].value['type'] - if version.kind != 3 or version.value != 3465 or generator.kind != 8 or generator.value != 'minecraft:flat': + if version.kind != 3 or version.value != 3839 or generator.kind != 8 or generator.value != 'minecraft:flat': raise ValueError('Wrong native version or generator') except (AttributeError, KeyError, ValueError) as error: - raise RuntimeError('Expected an explicit native3465 flat codec header') from error + raise RuntimeError('Expected an explicit native3839 flat codec header') from error world.mkdir() shutil.copy2(header, world/'level.dat') (home/'native-flat-header-input.json').write_text(json.dumps({ - 'sha256':expected_sha256, 'native_data_version':3465, 'generator':'minecraft:flat', + 'sha256':expected_sha256, 'native_data_version':3839, 'generator':'minecraft:flat', 'only_header_copied':True}, indent=2)+'\n', encoding='utf-8') @@ -233,7 +249,7 @@ def _download(artifact, destination): destination.parent.mkdir(parents=True, exist_ok=True) for address in (artifact['url'], artifact['url'] + '?source=lootablebodies-runtime'): try: - request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesRuntimeTests/2.4.9.118021'}) + request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesRuntimeTests/2.4.12.120061'}) with urllib.request.urlopen(request, timeout=60) as response: content = response.read() if hashlib.sha1(content).hexdigest() != expected: @@ -265,11 +281,10 @@ def validate_client_paths(paths): import re for path in paths: value=str(path).replace('\\','/').lower();name=path.name.lower() - # Forge 47's pinned descriptor includes the three dist-marker classes. - # Reject the merge tool itself and any changed or aliased API archive. - if (value.endswith('/net/minecraftforge/mergetool/1.1.5/mergetool-1.1.5-api.jar') + # Forge 50's exact descriptor requires this dist-marker API archive. + if (value.endswith('/net/minecraftforge/mergetool-api/1.0/mergetool-api-1.0.jar') and not path.is_symlink() and path.is_file() - and digest(path) == '39D9A3D9277C1402B8E7B97F917CE278241CF1796312AEC85B87F19A376DD99A'): + and digest(path) == '100F0C4D658B9DD73600CE4BB9C75657E0651F0F0F5E0B2F977F85D8F495350B'): continue if any(word in name for word in ('junit','mockito','opentest4j','slime-launcher','legacydev','mergetool','launchwrapper')) or '/mavenizer/' in value or re.match(r'asm(?:-debug-all|-commons|-tree|-util|-analysis)?-[0-5][.-]',name) or re.match(r'lwjgl(?:_util|-platform)?-2[.-]',name): raise RuntimeError('Development, testing or incompatible legacy library in packaged client: '+str(path)) @@ -282,7 +297,7 @@ def client_inputs(runtime, prepared): def prepare_assets(runtime, cache=None): - raise RuntimeError('Use pinned native1.20.1 assets; inherited cache-copy hydration is disabled') + raise RuntimeError('Use pinned native 1.20.6 assets; inherited cache-copy hydration is disabled') def prepare_client(home, candidate=None, probe=None): @@ -301,20 +316,20 @@ def prepare_client(home, candidate=None, probe=None): def client_command(java, home, runtime, assets, *, username='LootableTest', extra=(), prepared=None): from native120_client import command if prepared is None: - prepared=Path(__file__).resolve().parents[1]/'.gradle/native120-client-inputs' + prepared=Path(__file__).resolve().parents[1]/'.gradle/native1206-client-inputs' return command(java,home,runtime,assets,prepared,username=username,extra=extra) def install(java, home, client=False): - """Install native47 locally; its installer owns the server downloads.""" + """Install Forge 50 locally; its installer owns the server downloads.""" from prepare_migration_inputs import fetch home=home.resolve();home.mkdir(parents=True,exist_ok=False) installer=home/(FORGE+'-installer.jar') - fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.1-47.4.28/'+installer.name,installer,INSTALLER_SHA256) + fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.6-50.2.14/'+installer.name,installer,INSTALLER_SHA256) if client: from native120_client import BASE_VERSION,BASE_SHA1 base=home/'versions'/BASE_VERSION/(BASE_VERSION+'.json') - _download({'sha1':BASE_SHA1,'url':'https://piston-meta.mojang.com/v1/packages/'+BASE_SHA1+'/1.20.1.json'},base) + _download({'sha1':BASE_SHA1,'url':'https://piston-meta.mojang.com/v1/packages/'+BASE_SHA1+'/1.20.6.json'},base) metadata=json.loads(base.read_text());_download(metadata['downloads']['client'],base.with_suffix('.jar')) (home/'launcher_profiles.json').write_text('{"profiles":{}}\n') run_logged([str(java),'-Djava.awt.headless=true','-jar',installer.name, diff --git a/tools/owned_process.py b/tools/owned_process.py index e410ebf..b150d43 100644 --- a/tools/owned_process.py +++ b/tools/owned_process.py @@ -82,10 +82,8 @@ def native_process(command,*,cwd,log,timeout,console_input=False): if encodings and encodings!=['-Dfile.encoding=UTF-8']:raise ValueError('Owned runtime requires one explicit UTF-8 encoding') if not encodings:command.insert(1,'-Dfile.encoding=UTF-8') validate_logging_inputs(command) - java=Path(command[0]).resolve();metadata=java.parent.parent/'release' - values=dict(line.split('=',1) for line in metadata.read_text().splitlines() if '=' in line) - if values.get('SEMANTIC_VERSION')!='"17.0.20.1+1"' or values.get('IMPLEMENTOR')!='"Eclipse Adoptium"': - raise RuntimeError('Group runtime requires exact Temurin17.0.20.1+1') + from native_runtime import require_runtime_jdk + java=require_runtime_jdk(command[0]) suffix='.exe' if java.suffix=='.exe' else '' stack=java.with_name('jstack'+suffix);metric=java.with_name('jstat'+suffix) if not stack.is_file() or not metric.is_file():raise FileNotFoundError('Missing pinned native JDK diagnostics') diff --git a/tools/qualification-policy.json b/tools/qualification-policy.json index 9452814..6627cd5 100644 --- a/tools/qualification-policy.json +++ b/tools/qualification-policy.json @@ -1,7 +1,7 @@ { "schema": 1, - "target": "1.20.1", - "status": "Gameplay and migration qualify locally. Hosted checks validate policy, production archives and security.", + "target": "1.20.6", + "status": "Forge 1.20.6 qualifies locally. Hosted checks verify production archives and release contracts.", "hosted": { "allowed": [ "policy", diff --git a/tools/test_historical_runtime_budget.py b/tools/test_historical_runtime_budget.py index e0facf0..d9749ac 100644 --- a/tools/test_historical_runtime_budget.py +++ b/tools/test_historical_runtime_budget.py @@ -71,7 +71,7 @@ def test_forged_flag_or_stale_source_binding_cannot_authorize_history(self): import json with tempfile.TemporaryDirectory() as directory: policy=Path(directory)/'policy.json' - base={'minecraft':'1.20.1','forge':'47.4.28','reloads':200,'jvms':4,'qualified':True,'jvm_args':[]} + base={'minecraft':'1.20.6','forge':'50.2.14','reloads':200,'jvms':4,'qualified':True,'jvm_args':[]} for fields in ({'jvm_args':['-Dmax.bg.threads=1']},{'controller_sha256':'0'*64}): policy.write_text(json.dumps(dict(base,**fields))) with patch.object(native_runtime,'WORKER_BUDGET_PATH',policy),patch.object(native_runtime,'run_monitored') as launch: diff --git a/tools/test_hosted_policy.py b/tools/test_hosted_policy.py index 6d1a1d6..fff379e 100644 --- a/tools/test_hosted_policy.py +++ b/tools/test_hosted_policy.py @@ -6,7 +6,7 @@ class HostedPolicyTests(unittest.TestCase): def test_raw_jdk_version_is_rejected_before_java_preparation(self): text=(Path(__file__).resolve().parents[1]/'.github/workflows/native120.yml').read_text() - with self.assertRaises(ValueError):validate_workflow(text.replace('17.0.20+101','17.0.20.1+1')) + with self.assertRaises(ValueError):validate_workflow(text.replace('21.0.7+6','21.0.7+99')) def test_hosted_roots_allow_archive_builds_and_reject_every_test_or_game_entry(self): base='./gradlew assemble verifyProductionRuntimeClasspath prepareReleaseBundle ' @@ -29,17 +29,17 @@ def test_missing_fork_guard_or_pipe_failure_guard_is_rejected(self): with self.subTest(removed=removed),self.assertRaises(ValueError):validate_workflow(text.replace(removed,'')) def test_manifest_boolean_cannot_override_an_integrity_hold(self): - version='2.4.11.120011';base='LootableBodies-'+version + version='2.4.11.120061';base='LootableBodies-'+version names=['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom'] - good={'version':version,'minecraft':'1.20.1','forge':'47.4.28','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in names}} + good={'version':version,'minecraft':'1.20.6','forge':'50.2.14','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in names}} require_qualified_manifest(good) for field,value in [('qualification_integrity_status','blocked_receipt_mismatch'),('qualification_integrity_status',None),('local_qualification_complete',False),('minecraft','1.17.1')]: changed=dict(good);changed[field]=value with self.subTest(field=field,value=value),self.assertRaises(ValueError):require_qualified_manifest(changed) def test_missing_or_extra_file_declaration_is_fatal(self): - version='2.4.11.120011';base='LootableBodies-'+version - good={'version':version,'minecraft':'1.20.1','forge':'47.4.28','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in ['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom']}} + version='2.4.11.120061';base='LootableBodies-'+version + good={'version':version,'minecraft':'1.20.6','forge':'50.2.14','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in ['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom']}} for extra in (False,True): changed=json.loads(json.dumps(good)) if extra:changed['files']['extra.jar']='B'*64 diff --git a/tools/test_native120_build.py b/tools/test_native120_build.py index 1c9803d..eec2812 100644 --- a/tools/test_native120_build.py +++ b/tools/test_native120_build.py @@ -10,7 +10,7 @@ def test_missing_changed_extra_and_unqualified_inputs_are_fatal(self): root=Path(directory);bundle=root/'bundle';bundle.mkdir();manifest=root/'manifest.json' names=['CHANGELOG.txt','SHA256SUMS',MAIN_JAR,MAIN_JAR[:-4]+'-sources.jar',MAIN_JAR[:-4]+'-javadoc.jar',MAIN_JAR[:-4]+'.pom'] for name in names:(bundle/name).write_bytes(name.encode()) - record={'version':MOD_VERSION,'minecraft':'1.20.1','forge':'47.4.28','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{p.name:digest(p) for p in bundle.iterdir()}} + record={'version':MOD_VERSION,'minecraft':'1.20.6','forge':'50.2.14','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{p.name:digest(p) for p in bundle.iterdir()}} manifest.write_text(json.dumps(record));self.assertEqual(record['files'],check_bundle(manifest,bundle)) for mutation in ['unqualified','integrity-hold','wrong-target','missing-file','changed-file','extra-file','missing-identity']: with self.subTest(mutation=mutation): diff --git a/tools/test_native120_client_contracts.py b/tools/test_native120_client_contracts.py index 10e4efe..2624da9 100644 --- a/tools/test_native120_client_contracts.py +++ b/tools/test_native120_client_contracts.py @@ -27,20 +27,32 @@ def test_unknown_placeholder_fails_before_a_process_can_be_started(self): with self.assertRaises(KeyError): native120_client.expand(['${unrecognised}'],{},system='windows',architecture='amd64',os_version='10.0') - def test_jvm_filters_the_actual_base_jar_while_game_keeps_its_forge_profile_version(self): + def test_forge_bootstrap_uses_installed_client_descriptor_and_exact_java21(self): with tempfile.TemporaryDirectory() as directory: root=Path(directory);java=root/'jdk/bin/java.exe';java.parent.mkdir(parents=True) - (java.parent.parent/'release').write_text('SEMANTIC_VERSION="17.0.20.1+1"\nIMPLEMENTOR="Eclipse Adoptium"\n') - index=root/'assets/indexes/5.json';index.parent.mkdir(parents=True);index.write_bytes(b'{}') - base={'assetIndex':{'id':'5','sha1':hashlib.sha1(b'{}').hexdigest()},'arguments':{'jvm':['-cp','${classpath}'],'game':['--version','${version_name}','--accessToken','${auth_access_token}','--clientId','${clientid}','--xuid','${auth_xuid}']}} - forge={'arguments':{'jvm':['-DignoreList=forge-,${version_name}.jar'],'game':['--launchTarget','forgeclient']},'mainClass':'cpw.mods.bootstraplauncher.BootstrapLauncher'} - record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'1.20.1.jar')],'natives':str(root/'natives'),'system':'windows','architecture':'amd64'} + (java.parent.parent/'release').write_text('JAVA_RUNTIME_VERSION="21.0.7+6-LTS"\nIMPLEMENTOR="Eclipse Adoptium"\n') + index=root/'assets/indexes/16.json';index.parent.mkdir(parents=True);index.write_bytes(b'{}') + base={'assetIndex':{'id':'16','sha1':hashlib.sha1(b'{}').hexdigest()},'arguments':{'jvm':['-cp','${classpath}'],'game':['--version','${version_name}','--accessToken','${auth_access_token}','--clientId','${clientid}','--xuid','${auth_xuid}']}} + forge={'arguments':{'jvm':['-Djava.net.preferIPv6Addresses=system'],'game':['--launchTarget','forge_client']},'mainClass':'net.minecraftforge.bootstrap.ForgeBootstrap'} + record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'1.20.6.jar')],'natives':str(root/'natives'),'system':'windows','architecture':'amd64'} with patch('native120_client.load',return_value=(base,forge,record)): result=native120_client.command(java,root/'home',root/'sdk',root/'assets',root/'prepared',username='Fixture') - self.assertIn('-DignoreList=forge-,1.20.1.jar',result) - self.assertEqual('1.20.1-forge-47.4.28',result[result.index('--version')+1]) + with patch.dict('os.environ',{'LOOTABLEBODIES_TEST_SESSION_ENDPOINT':'http://127.0.0.1:51927'}): + owned=native120_client.command(java,root/'home',root/'sdk',root/'assets',root/'prepared',username='Fixture') + with patch.dict('os.environ',{'LOOTABLEBODIES_TEST_SESSION_ENDPOINT':'https://example.invalid'}), self.assertRaisesRegex(ValueError,'loopback'): + native120_client.command(java,root/'home',root/'sdk',root/'assets',root/'prepared',username='Fixture') + self.assertIn('-Djava.net.preferIPv6Addresses=system',result) + self.assertIn(str(root/'1.20.6.jar'),result) + self.assertIn('net.minecraftforge.bootstrap.ForgeBootstrap',result) + self.assertEqual('forge_client',result[result.index('--launchTarget')+1]) + self.assertFalse(any('ignoreList' in arg for arg in result)) + self.assertIn('-Dstdout.encoding=UTF-8',result) + self.assertIn('-Dstderr.encoding=UTF-8',result) + self.assertEqual('1.20.6-forge-50.2.14',result[result.index('--version')+1]) self.assertEqual('0',result[result.index('--accessToken')+1]) self.assertEqual('',result[result.index('--clientId')+1]);self.assertEqual('',result[result.index('--xuid')+1]) + self.assertIn('-Dminecraft.api.session.host=http://127.0.0.1:51927',owned) + self.assertIn('-Dminecraft.api.services.host=http://127.0.0.1:51927',owned) def test_unsealed_classpath_and_changed_native_bytes_are_rejected(self): with tempfile.TemporaryDirectory() as directory: diff --git a/tools/test_native120_client_logs.py b/tools/test_native120_client_logs.py index 7241bf3..3c31b48 100644 --- a/tools/test_native120_client_logs.py +++ b/tools/test_native120_client_logs.py @@ -1,4 +1,4 @@ -"""Synthetic diagnostic counterexamples; independent Forge 47 is separate proof.""" +"""Synthetic diagnostic counterexamples; independent Forge 50 is separate proof.""" from pathlib import Path import hashlib,json,tempfile,unittest from unittest.mock import patch @@ -38,9 +38,9 @@ def test_actual_proof_reader_rejects_changed_resource_proof_target_and_control(s with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as directory: root=Path(directory);definition=root/'sounds.json';definition.write_text('{}');critical=root/'control.fixture';critical.write_text('synthetic independent control input');rawlog=root/'native.log';rawlog.write_text('synthetic native fixture') sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest().upper() - proof={'minecraft':'1.20.1','forge':'47.4.28','platform':'Windows amd64','qualified':True,'no_candidate':True,'no_world':True,'native_client_exit':0,'events':EVENTS,'control_sha256':'C'*64,'sound_definition':str(definition),'critical_files':{str(definition):sha(definition),str(critical):sha(critical)},'raw_control_logs':{str(rawlog):sha(rawlog)},'original_strict_audit_passed':False} + proof={'minecraft':'1.20.6','forge':'50.2.14','platform':'Windows amd64','qualified':True,'no_candidate':True,'no_world':True,'native_client_exit':0,'events':EVENTS,'control_sha256':'C'*64,'sound_definition':str(definition),'critical_files':{str(definition):sha(definition),str(critical):sha(critical)},'raw_control_logs':{str(rawlog):sha(rawlog)},'original_strict_audit_passed':False} saved=root/'proof.json';saved.write_text(json.dumps(proof)) - policy={'minecraft':'1.20.1','forge':'47.4.28','platform':'Windows amd64','qualified':True,'proof_sha256':sha(saved),'events':EVENTS,'control_sha256':'C'*64,'sound_definition_sha256':sha(definition)} + policy={'minecraft':'1.20.6','forge':'50.2.14','platform':'Windows amd64','qualified':True,'proof_sha256':sha(saved),'events':EVENTS,'control_sha256':'C'*64,'sound_definition_sha256':sha(definition)} if mutation=='resource':definition.write_text('{"item.goat_horn.play":{}}') elif mutation=='proof':saved.write_text('{}') elif mutation=='missing':critical.unlink() diff --git a/tools/test_native120_flat_header.py b/tools/test_native120_flat_header.py index 9e93ffd..47bfd53 100644 --- a/tools/test_native120_flat_header.py +++ b/tools/test_native120_flat_header.py @@ -5,10 +5,11 @@ import unittest from native_runtime import seed_flat_fixture from nbt_regions import Tag,encode +from verify_native_respawn_control import require_forge_only_header class Native120FlatHeaderTests(unittest.TestCase): - def header(self,root,version=3465,generator='minecraft:flat'): + def header(self,root,version=3839,generator='minecraft:flat'): settings=Tag(10,{'dimensions':Tag(10,{'minecraft:overworld':Tag(10,{ 'generator':Tag(10,{'type':Tag(8,generator)})})})}) data=encode('',Tag(10,{'Data':Tag(10,{'DataVersion':Tag(3,version),'WorldGenSettings':settings})})) @@ -22,7 +23,7 @@ def test_only_the_explicit_sealed_header_is_copied(self): self.assertEqual(before,(home/'world/level.dat').read_bytes());self.assertEqual(before,header.read_bytes()) self.assertEqual(['level.dat'],[p.name for p in (home/'world').iterdir()]) def test_wrong_hash_target_or_generator_refuses_before_creating_world(self): - for version,generator,bad_hash in [(3465,'minecraft:flat',True),(2586,'minecraft:flat',False),(2730,'minecraft:flat',False),(2975,'minecraft:flat',False),(3465,'minecraft:noise',False)]: + for version,generator,bad_hash in [(3839,'minecraft:flat',True),(3465,'minecraft:flat',False),(2586,'minecraft:flat',False),(2730,'minecraft:flat',False),(2975,'minecraft:flat',False),(3839,'minecraft:noise',False)]: with self.subTest(version=version,generator=generator,bad_hash=bad_hash),tempfile.TemporaryDirectory() as directory: root=Path(directory);header,digest=self.header(root,version,generator);home=self.home(root) with self.assertRaises(RuntimeError):seed_flat_fixture(home,header,'0'*64 if bad_hash else digest) @@ -35,12 +36,21 @@ def test_existing_world_is_never_overwritten(self): self.assertEqual(b'existing world bytes',protected.read_bytes()) def test_actual_native20_codec_header_is_a_sealed_current_target_input(self): - header=Path(__file__).parent/'fixtures/native120-flat-level.dat' - expected='81E5056FCE49638B40F532B819FC82B0EBBFC954BCF4A7E20E3D6538E8FC0645' + header=Path(__file__).parent/'fixtures/native1206-flat-level.dat' + expected='34253D55B2B3A4F1CE4107FACF5AF64ED496264ED53804405F6CDC6D45E56E74' self.assertEqual(expected,hashlib.sha256(header.read_bytes()).hexdigest().upper()) with tempfile.TemporaryDirectory() as directory: home=self.home(Path(directory));seed_flat_fixture(home,header,expected) self.assertEqual(header.read_bytes(),(home/'world/level.dat').read_bytes()) + def test_forge_only_control_rejects_native_header_with_the_mod_installed(self): + fixtures=Path(__file__).parent/'fixtures' + forge_only=fixtures/'native1206-forge-only-flat-level.dat' + expected='8A67C3D55353C3F330899626E224F6FFC448A8A53FFA1858A275FA1A76314CE2' + self.assertEqual(expected,hashlib.sha256(forge_only.read_bytes()).hexdigest().upper()) + require_forge_only_header(forge_only) + with self.assertRaisesRegex(ValueError,'Forge-only saved mod list'): + require_forge_only_header(fixtures/'native1206-flat-level.dat') + if __name__=='__main__':unittest.main() diff --git a/tools/test_native120_infrastructure.py b/tools/test_native120_infrastructure.py index 0bc602e..290dfb6 100644 --- a/tools/test_native120_infrastructure.py +++ b/tools/test_native120_infrastructure.py @@ -5,18 +5,18 @@ class Native120InfrastructureTest(unittest.TestCase): def test_native_distribution_and_every_server_input_use_the_target(self): - self.assertEqual('forge-1.20.1-47.4.28',runtime.FORGE) - self.assertEqual('D615397A6B2F1E99D4B454E2DB2B4CA2E94BE91D5E0243C8C7E023D965DB39E7',runtime.INSTALLER_SHA256) - self.assertEqual(107,len(runtime.SERVER_INPUTS)) - self.assertEqual('993C0EE3F6C3A4E6CF47119A929F490C5B59A8E8CA0A4F3624E1FDB80508F26C', - runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.20.1-47.4.28/forge-1.20.1-47.4.28-server.jar']) - self.assertEqual('292607A1120C035382974FE03D00126942E5B2C8DB2762553FAB9137751B83E1', - runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.20.1-47.4.28/win_args.txt']) + self.assertEqual('forge-1.20.6-50.2.14',runtime.FORGE) + self.assertEqual('CF5ACACF52F694732732BB98D72B2871CFE16321990053DC4F3DDF5C20044F68',runtime.INSTALLER_SHA256) + self.assertEqual(94,len(runtime.SERVER_INPUTS)) + self.assertEqual('609A3BEF499E84E93069A1BE7A7E6E38662952B13B0063495AD5804C1829FB35', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.20.6-50.2.14/forge-1.20.6-50.2.14-server.jar']) + self.assertEqual('085772DF00FF7D2C0E184F99B3F11FA659CB8AB26DBD20EEB0697F7941F6F0E2', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.20.6-50.2.14/win_args.txt']) self.assertFalse(any('1.16.5' in path for path in runtime.SERVER_INPUTS)) def test_development_assets_use_the_native_index(self): text=(Path(__file__).resolve().parents[1]/'gradle/assets.gradle').read_text() - self.assertIn("minecraftAssetIndex = '5'",text) - self.assertIn("1.20.1.json",text) + self.assertIn("minecraftAssetIndex = '16'",text) + self.assertIn("1.20.6.json",text) self.assertNotIn('e9d0adb8f642abe422909ede50f651b2b58a3573',text) def test_every_released115_history_extends_the_complete_earlier_matrix(self): from verify_world_upgrades import ORIGINS,PHASES diff --git a/tools/test_native120_launch.py b/tools/test_native120_launch.py index 11fc83c..9407c9b 100644 --- a/tools/test_native120_launch.py +++ b/tools/test_native120_launch.py @@ -11,19 +11,19 @@ class Native120LaunchTests(unittest.TestCase): def test_posix_native_diagnostics_fixture_uses_the_posix_tools(self): with patch.object(native_runtime,'os',SimpleNamespace(name='posix')): - self.test_packaged_server_uses_installer_module_arguments_and_java17_gc() + self.test_packaged_server_uses_installer_module_arguments_and_java21_gc() - def jdk(self, root, version='17.0.20.1+1'): + def jdk(self, root, version='21.0.7+6-LTS'): suffix='.exe' if native_runtime.os.name=='nt' else '' java = root/('jdk/bin/java'+suffix) java.parent.mkdir(parents=True) for name in ('java', 'jstack', 'jstat'): (java.parent/(name+suffix)).touch() (java.parent.parent/'release').write_text( - 'SEMANTIC_VERSION="'+version+'"\nIMPLEMENTOR="Eclipse Adoptium"\n') + 'JAVA_RUNTIME_VERSION="'+version+'"\nIMPLEMENTOR="Eclipse Adoptium"\n') return java - def test_packaged_server_uses_installer_module_arguments_and_java17_gc(self): + def test_packaged_server_uses_installer_module_arguments_and_java21_gc(self): with tempfile.TemporaryDirectory() as directory: home=Path(directory); java=self.jdk(home) with patch('native_runtime.run_monitored', return_value=subprocess.CompletedProcess([],0)) as run: @@ -31,7 +31,7 @@ def test_packaged_server_uses_installer_module_arguments_and_java17_gc(self): command=run.call_args.args[0] self.assertNotIn('-jar',command) self.assertIn('-Xmx2G',command) - self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/1.20.1-47.4.28/') for arg in command)) + self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/1.20.6-50.2.14/') for arg in command)) gc=next((arg for arg in command if arg.startswith('-Xlog:')),None) self.assertIsNotNone(gc) self.assertIn('gc*,safepoint',gc) @@ -45,23 +45,46 @@ def test_runtime_rejects_a_different_jdk_before_launch(self): with tempfile.TemporaryDirectory() as directory: home=Path(directory); java=self.jdk(home,'17.0.1+12') with patch('native_runtime.run_monitored') as run: - with self.assertRaisesRegex(RuntimeError,'17.0.20.1'): + with self.assertRaisesRegex(RuntimeError,'21.0.7'): native_runtime.run_java(java,home,'fixture') run.assert_not_called() - def test_server_inputs_pin_native47_instead_of_the_old_launcher(self): - self.assertEqual('forge-1.20.1-47.4.28',native_runtime.FORGE) - for path in ('libraries/net/minecraftforge/forge/1.20.1-47.4.28/win_args.txt', - 'libraries/net/minecraftforge/forge/1.20.1-47.4.28/unix_args.txt', - 'libraries/net/minecraftforge/forge/1.20.1-47.4.28/forge-1.20.1-47.4.28-server.jar'): + def test_startup_diagnostics_wait_for_the_owned_jvm_to_load(self): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory) + java=self.jdk(home) + gc=home/'fixture.diagnostics/gc.txt' + started=[] + def launch(command, **options): + self.assertEqual(240,options['timeout']) + options['diagnostics'].mkdir() + options['process_started'](SimpleNamespace(pid=1234,poll=lambda:None)) + return subprocess.CompletedProcess(command,0) + def boot(_): + gc.write_text('[0.010s][info][gc,init] Version: 21.0.7+6-LTS\n') + def attach(command, **options): + self.assertTrue(gc.is_file(),'Attach ran before the owned JVM initialized GC logging') + self.assertEqual('1234',command[-1]) + started.append(Path(command[0]).stem) + options['stdout'].write('attached\n') + with patch('native_runtime.run_monitored',side_effect=launch), \ + patch('time.sleep',side_effect=boot),patch('native_runtime.subprocess.run',side_effect=attach): + native_runtime.run_java(java,home,'fixture',timeout=240) + self.assertEqual(['jstack','jstat'],started) + + def test_server_inputs_pin_native50_instead_of_the_old_launcher(self): + self.assertEqual('forge-1.20.6-50.2.14',native_runtime.FORGE) + for path in ('libraries/net/minecraftforge/forge/1.20.6-50.2.14/win_args.txt', + 'libraries/net/minecraftforge/forge/1.20.6-50.2.14/unix_args.txt', + 'libraries/net/minecraftforge/forge/1.20.6-50.2.14/forge-1.20.6-50.2.14-server.jar'): self.assertIn(path,native_runtime.SERVER_INPUTS) self.assertFalse(any('1.16.5' in path for path in native_runtime.SERVER_INPUTS)) - def test_native47_historical_workers_have_their_own_fresh_qualification_binding(self): + def test_native50_historical_workers_have_their_own_fresh_qualification_binding(self): proof=native_runtime.WORKER_BUDGET_PROOF - self.assertEqual(('1.20.1','47.4.28',200,4,True),tuple(proof[key] for key in + self.assertEqual(('1.20.6','50.2.14',200,4,True),tuple(proof[key] for key in ('minecraft','forge','reloads','jvms','qualified'))) - self.assertTrue(proof['no_candidate'] and proof['no_worlds'] and proof['source_and_sdk_seals_unchanged']) + self.assertIn('server_input_manifest_sha256',proof) self.assertRegex(proof['qualification_evidence_sha256'],r'^[A-F0-9]{64}$') source=Path(__file__).resolve().parents[1]/'src/controlTest/java/buildcontrol/ResourceControl.java' self.assertEqual(hashlib.sha256(source.read_bytes()).hexdigest().upper(),proof['controller_sha256']) @@ -72,9 +95,12 @@ def test_owned_server_does_not_trigger_native_bare_json_flat_settings_fallback(s with tempfile.TemporaryDirectory() as directory: root=Path(directory); sdk=root/'sdk';(sdk/'libraries').mkdir(parents=True) artifact=sdk/'libraries/pinned-fixture';artifact.write_bytes(b'owned-sdk-input') - pins={'libraries/pinned-fixture':hashlib.sha256(artifact.read_bytes()).hexdigest().upper()} + shim=sdk/(native_runtime.FORGE+'-shim.jar');shim.write_bytes(b'owned-shim') + pins={'libraries/pinned-fixture':hashlib.sha256(artifact.read_bytes()).hexdigest().upper(), + shim.name:hashlib.sha256(shim.read_bytes()).hexdigest().upper()} with patch.object(native_runtime,'SERVER_INPUTS',pins),patch('native_runtime.subprocess.run'): native_runtime.prepare_server(root/'home',sdk) + self.assertEqual(shim.read_bytes(),(root/'home'/shim.name).read_bytes()) properties=(root/'home/server.properties').read_text() self.assertIn('level-type=DEFAULT\n',properties) self.assertNotIn('level-type=FLAT',properties) diff --git a/tools/test_native120_persistence.py b/tools/test_native120_persistence.py index 60a5d68..27277a9 100644 --- a/tools/test_native120_persistence.py +++ b/tools/test_native120_persistence.py @@ -6,8 +6,8 @@ from verify_world_upgrades import patch_stamps class Native120PersistenceTest(unittest.TestCase): - def body(self,number=1,marker=10):return Tag(10,{'id':Tag(8,'lootablebodies:corpse'),'LootableBodiesDataVersion':Tag(3,marker),'UUID':Tag(11,struct.pack('>iiii',0,0,0,number))}) - def chunk(self,bodies,version=3465,position=(8,8)): + def body(self,number=1,marker=11):return Tag(10,{'id':Tag(8,'lootablebodies:corpse'),'LootableBodiesDataVersion':Tag(3,marker),'UUID':Tag(11,struct.pack('>iiii',0,0,0,number))}) + def chunk(self,bodies,version=3839,position=(8,8)): return {'name':'','root':Tag(10,{'DataVersion':Tag(3,version),'Position':Tag(11,struct.pack('>ii',*position)),'Entities':Tag(9,(10,bodies))}),'compression':2,'timestamp':bytes(4)} def write(self,world,chunk,dimension=''): path=world/dimension/'entities/r.0.0.mca';path.parent.mkdir(parents=True,exist_ok=True);write_disposable_region(path,{264:chunk});return path @@ -16,7 +16,7 @@ def test_native20_saved_entities_count_all_dimensions_without_mutation(self): world=Path(directory);paths=[self.write(world,self.chunk([self.body(i+1)]),dim) for i,dim in enumerate(['','DIM-1','DIM1'])] before=[p.read_bytes() for p in paths];self.assertEqual(3,count_persisted(world,0));self.assertEqual(before,[p.read_bytes() for p in paths]) def test_previous_marker_native_version_or_wrong_entity_chunk_position_is_fatal(self): - for marker,version,position in [(9,3465,(8,8)),(10,3337,(8,8)),(10,3465,(9,8))]: + for marker,version,position in [(10,3839,(8,8)),(11,3465,(8,8)),(11,3839,(9,8))]: with self.subTest(marker=marker,version=version,position=position),tempfile.TemporaryDirectory() as directory: world=Path(directory);self.write(world,self.chunk([self.body(marker=marker)],version,position)) with self.assertRaises(ValueError):count_persisted(world,0) @@ -60,7 +60,7 @@ def test_valid_future_entity_fixture_preserves_original_and_rejects_wrong_source else: mutation=inject_future_entity_version(world) self.assertEqual(original,(home/'future-entity-original.mca').read_bytes()) - self.assertEqual(Tag(3,3466),read_region(path)[264]['root'].value['DataVersion']) + self.assertEqual(Tag(3,3840),read_region(path)[264]['root'].value['DataVersion']) self.assertEqual(8,mutation['corpse_marker']) if __name__=='__main__':unittest.main() diff --git a/tools/test_native47_platform_config.py b/tools/test_native47_platform_config.py index 8682222..c8f4770 100644 --- a/tools/test_native47_platform_config.py +++ b/tools/test_native47_platform_config.py @@ -1,7 +1,7 @@ import tempfile,unittest from pathlib import Path from native_runtime import fingerprint -from verify_upgrade_abort import allowed_native47_platform_additions +from verify_upgrade_abort import allowed_forge_platform_additions class Native47ConfigTest(unittest.TestCase): def target(self,root,text=None): @@ -11,12 +11,12 @@ def old(self):return (Path(__file__).parent/'fixtures/forge36-server-default.tom def test_exact_actual_native_default_addition(self): with tempfile.TemporaryDirectory() as directory: world=Path(directory);self.target(world);after=fingerprint(world) - self.assertEqual(['serverconfig/forge-server.toml'],allowed_native47_platform_additions({},after,world)) + self.assertEqual(['serverconfig/forge-server.toml'],allowed_forge_platform_additions({},after,world,forge=47)) def test_exact_actual_native_spec_retirement_preserves_supported_choices(self): with tempfile.TemporaryDirectory() as directory: world=Path(directory);path=self.target(world,self.old().replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true'));original=path.read_bytes();before=fingerprint(world) path.write_bytes((Path(__file__).parent/'fixtures/forge47-server-default.toml').read_bytes().replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true')) - self.assertEqual(['serverconfig/forge-server.toml'],allowed_native47_platform_additions(before,fingerprint(world),world,original)) + self.assertEqual(['serverconfig/forge-server.toml'],allowed_forge_platform_additions(before,fingerprint(world),world,original,forge=47)) def test_supported_mutation_unknown_retirement_or_wrong_type_cannot_pass(self): for change in ['value','unknown','type']: with self.subTest(change=change),tempfile.TemporaryDirectory() as directory: @@ -26,6 +26,19 @@ def test_supported_mutation_unknown_retirement_or_wrong_type_cannot_pass(self): path=self.target(world,original);before=fingerprint(world);target=(Path(__file__).parent/'fixtures/forge47-server-default.toml').read_bytes() if change=='value':target=target.replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true') path.write_bytes(target) - self.assertEqual([],allowed_native47_platform_additions(before,fingerprint(world),world,original)) + self.assertEqual([],allowed_forge_platform_additions(before,fingerprint(world),world,original,forge=47)) + + def test_forge50_retires_only_proved_native_options_and_keeps_supported_settings(self): + fixture=Path(__file__).parent/'fixtures' + for unknown in (False,True): + with self.subTest(unknown=unknown),tempfile.TemporaryDirectory() as directory: + world=Path(directory) + original=(fixture/'forge47-server-default.toml').read_bytes().replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true') + if unknown:original+=b'unknownFlag = true\n' + path=self.target(world,original) + before=fingerprint(world) + path.write_bytes((fixture/'forge50-server-default.toml').read_bytes().replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true')) + expected=[] if unknown else ['serverconfig/forge-server.toml'] + self.assertEqual(expected,allowed_forge_platform_additions(before,fingerprint(world),world,original,forge=50)) if __name__=='__main__':unittest.main() diff --git a/tools/test_native_evidence.py b/tools/test_native_evidence.py index 53576e4..b7fc54d 100644 --- a/tools/test_native_evidence.py +++ b/tools/test_native_evidence.py @@ -14,7 +14,7 @@ def fixture(self,home): record={'timed_out':False,'returncode':0,'pid':123,'timeout':240,'cwd':str(home), 'command':['fixture-java','-Dlootablebodies.phase='+phase, '-Xlog:gc*,safepoint:file=upgrade-near.diagnostics/gc.txt:time,uptime,level,tags', - '@libraries/net/minecraftforge/forge/1.20.1-47.4.28/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], + '@libraries/net/minecraftforge/forge/1.20.6-50.2.14/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], 'evidence':{name:digest(directory/name) for name in ['gc.txt','initial-threads.txt','initial-heap-gc.txt']},'captures':[],'metrics':[]} (directory/'result.json').write_text(json.dumps(record));return directory,record def test_complete_receipt_retains_all_logs_and_rehashes_every_native_file(self): @@ -48,7 +48,7 @@ def test_relocated_artifact_requires_the_actual_case_suffix(self): def test_native_process_must_have_exact_qualified_mod_copies(self): from release_identity import MAIN_JAR with tempfile.TemporaryDirectory() as directory: - home=Path(directory);mods=home/'mods';mods.mkdir();main=mods/MAIN_JAR;probe=mods/'lootablebodies-build-tests-reobf.jar' + home=Path(directory);mods=home/'mods';mods.mkdir();main=mods/MAIN_JAR;probe=mods/'lootablebodies-build-tests.jar' main.write_bytes(b'synthetic candidate');probe.write_bytes(b'synthetic probe') report={'candidate_sha256':digest(main),'probe_sha256':digest(probe)};self.assertEqual(2,len(audit_case_mods(home,report))) main.write_bytes(b'changed') @@ -64,7 +64,7 @@ def test_downloaded_linux_receipt_is_audited_independently_of_auditor_os(self): record['cwd']='/original/runner/build/migration-results/case' record['command']=[part.replace('/win_args.txt','/unix_args.txt') for part in record['command']] (folder/'result.json').write_text(json.dumps(record));audit_phase(home,'upgrade-near',relocated=True) - record['command'].append('@libraries/net/minecraftforge/forge/1.20.1-47.4.28/win_args.txt') + record['command'].append('@libraries/net/minecraftforge/forge/1.20.6-50.2.14/win_args.txt') (folder/'result.json').write_text(json.dumps(record)) with self.assertRaises(ValueError):audit_phase(home,'upgrade-near',relocated=True) @@ -72,7 +72,7 @@ def test_mod_aliases_cannot_hide_an_extra_loader_archive(self): from unittest.mock import patch from release_identity import MAIN_JAR with tempfile.TemporaryDirectory() as directory: - home=Path(directory);mods=home/'mods';mods.mkdir();main=mods/MAIN_JAR;probe=mods/'lootablebodies-build-tests-reobf.jar' + home=Path(directory);mods=home/'mods';mods.mkdir();main=mods/MAIN_JAR;probe=mods/'lootablebodies-build-tests.jar' main.write_bytes(b'synthetic candidate');probe.write_bytes(b'synthetic probe') report={'candidate_sha256':digest(main),'probe_sha256':digest(probe)} (mods/'unexpected.jar').write_bytes(b'extra') diff --git a/tools/test_native_server_preparation.py b/tools/test_native_server_preparation.py index 343f738..34e8f4c 100644 --- a/tools/test_native_server_preparation.py +++ b/tools/test_native_server_preparation.py @@ -6,7 +6,7 @@ class VanillaServerPreparationTests(unittest.TestCase): def fixture(self,home,payload=b'exact installed native input'): - relative='libraries/net/minecraft/server/1.20.1/server-1.20.1.jar' + relative='libraries/net/minecraft/server/1.20.6/server-1.20.6.jar' expected=hashlib.sha256(b'exact installed native input').hexdigest().upper() def installed(command,**options): self.assertIn('--installServer',command) @@ -25,7 +25,7 @@ def test_wrong_installed_vanilla_bytes_refuse_before_gameplay_without_installer_ with tempfile.TemporaryDirectory() as directory: home=Path(directory)/'new-sdk';pins,installed=self.fixture(home,b'changed bytes') with patch('prepare_migration_inputs.fetch'),patch.object(runtime,'_download') as prefetch,patch.object(runtime,'run_logged',side_effect=installed) as installer,patch.object(runtime,'SERVER_INPUTS',pins): - with self.assertRaisesRegex(RuntimeError,'Incorrect official Forge47 server input'):runtime.install(Path('fixture-java'),home) + with self.assertRaisesRegex(RuntimeError,'Incorrect official Forge 50 server input'):runtime.install(Path('fixture-java'),home) prefetch.assert_not_called();self.assertEqual(1,installer.call_count) self.assertFalse((home/'runtime-inputs.json').exists()) def test_missing_installed_input_is_fatal(self): diff --git a/tools/test_owned_process.py b/tools/test_owned_process.py index 1f08701..f98cbe1 100644 --- a/tools/test_owned_process.py +++ b/tools/test_owned_process.py @@ -113,11 +113,11 @@ def initial(process):raise RuntimeError('injected initial diagnostic failure') self.assertIsNotNone(receipt['returncode']);self.assertNotEqual(0,receipt['returncode']);self.assertFalse(receipt['timed_out']) def test_previous_runtime_and_wrong_patch_refuse_before_group_launch(self): - for version,vendor in [('16.0.2+7','Eclipse Foundation'),('17.0.20+8','Eclipse Adoptium')]: + for version,vendor in [('16.0.2+7','Eclipse Foundation'),('17.0.20.1+1','Eclipse Adoptium'),('21.0.8+9-LTS','Eclipse Adoptium'),('21.0.7+6-LTS','Other Vendor')]: with self.subTest(version=version),tempfile.TemporaryDirectory() as directory: root=Path(directory);java=root/'jdk/bin/java.exe';java.parent.mkdir(parents=True) - (java.parent.parent/'release').write_text('SEMANTIC_VERSION="'+version+'"\nIMPLEMENTOR="'+vendor+'"\n') - with patch('owned_process.OwnedProcess') as launch,self.assertRaisesRegex(RuntimeError,'17.0.20.1'): + (java.parent.parent/'release').write_text('JAVA_RUNTIME_VERSION="'+version+'"\nIMPLEMENTOR="'+vendor+'"\n') + with patch('owned_process.OwnedProcess') as launch,self.assertRaisesRegex(RuntimeError,'21.0.7'): native_process([str(java)],cwd=root,log=root/'runtime.log',timeout=5) launch.assert_not_called() diff --git a/tools/verify_multiplayer.py b/tools/verify_multiplayer.py index e02d12d..fd80d15 100644 --- a/tools/verify_multiplayer.py +++ b/tools/verify_multiplayer.py @@ -27,7 +27,7 @@ def reload_server(java, server, logging): def client_command(project,java,distribution,root,cache,shared,port,role,hidden_agent=None,probe=None,prepared=None,assets=None): - prepare_client(root,project/'build/libs'/MAIN,probe or project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar') + prepare_client(root,project/'build/libs'/MAIN,probe or project/'build/integration-test-mod/lootablebodies-build-tests.jar') (root/'config').mkdir(exist_ok=True);(root/'config/lootablebodies.cfg').write_text('options {\n B:local_texture=true\n}\n') extra=['-Dlootablebodies.multiplayer.role='+role,'-Dlootablebodies.multiplayer.sync='+str(shared), '-Dlootablebodies.multiplayer.port='+str(port), @@ -51,7 +51,7 @@ def main(): java=args.java.resolve();distribution=args.forge_home.resolve();root=args.output.resolve();root.mkdir(parents=True,exist_ok=False) shared=root/'sync';shared.mkdir();server=root/'server' jar=project/'build/libs'/MAIN;before=digest(jar) - prepare(server,distribution,jar,project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar') + prepare(server,distribution,jar,project/'build/integration-test-mod/lootablebodies-build-tests.jar') seed_flat_fixture(server,args.flat_header,args.flat_header_sha256) with socket.socket() as reservation: reservation.bind(('127.0.0.1',0));port=reservation.getsockname()[1] diff --git a/tools/verify_native_respawn_control.py b/tools/verify_native_respawn_control.py index 665b1a6..cfab1a9 100644 --- a/tools/verify_native_respawn_control.py +++ b/tools/verify_native_respawn_control.py @@ -1,10 +1,11 @@ -"""Exercise native45 delayed respawns/FIFO packets without Lootable Bodies.""" -import argparse,json,socket,subprocess,time,os,re +"""Exercise Forge 50 delayed respawns and FIFO packets without Lootable Bodies.""" +import argparse,gzip,json,socket,subprocess,time,os,re from pathlib import Path from native120_logs import audit from native120_client_logs import audit_client from audit_runtime_logs import audit as legacy_audit, forge36_dead_player_movement_blocks from native_runtime import prepare_server,prepare_client,client_command,project_assets,FORGE,digest,seed_flat_fixture +from nbt_regions import decode from owned_process import native_process def control_error_counts(path): @@ -15,6 +16,18 @@ def control_error_counts(path): return count,report['known_forge36_dead_player_movement_failures']-count +def require_forge_only_header(header): + """Reject saved mod registries before the isolated control starts.""" + _, root = decode(gzip.decompress(header.read_bytes())) + try: + kind, entries = root.value['fml'].value['LoadingModList'].value + mods = [(entry.value['ModId'].value, entry.value['ModVersion'].value) for entry in entries] + except (AttributeError, KeyError, TypeError, ValueError) as error: + raise ValueError('Expected a Forge-only saved mod list') from error + if kind != 10 or sorted(mods) != [('forge', '50.2.14'), ('minecraft', '1.20.6')]: + raise ValueError('Expected a Forge-only saved mod list') + + def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--java',type=Path,required=True) @@ -27,8 +40,9 @@ def main(): parser.add_argument('--hidden-agent',type=Path) parser.add_argument('--output',type=Path,required=True) args=parser.parse_args();project=Path(__file__).resolve().parents[1] + require_forge_only_header(args.flat_header) root=args.output.resolve();root.mkdir(parents=True,exist_ok=False) - probe=project/'build/control-test-mod/forge-platform-control-reobf.jar' + probe=project/'build/control-test-mod/forge-platform-control.jar' prepare_server(root/'server',args.forge_home.resolve(),probe=probe) seed_flat_fixture(root/'server',args.flat_header,args.flat_header_sha256) prepare_client(root/'client',probe=probe) @@ -79,9 +93,10 @@ def start(name): server=(root/'server.log').read_text(errors='replace') batches=[int(value) for value in re.findall(r'NATIVE_FIFO_DELAY_RELEASE packets=([0-9]+)',client)] if server.count('NATIVE_DEATH ')!=8 or len(batches)!=8 or sum(batches)<=0: - raise RuntimeError('Native45 control did not exercise all eight real delayed respawn/FIFO actions') - result={'lootable_bodies_installed':False,'respawns':8,'native45_delayed_fifo_batches':batches, - 'legacy36_error_matcher_used':False,'native45_complete_logs_audited':True,'no_unclassified_diagnostics':True, + raise RuntimeError('Forge 50 control did not exercise all eight real delayed respawn/FIFO actions') + result={'lootable_bodies_installed':False,'respawns':8,'forge50_delayed_fifo_batches':batches, + 'flat_header_sha256':digest(args.flat_header), + 'legacy36_error_matcher_used':False,'forge50_complete_logs_audited':True,'no_unclassified_diagnostics':True, 'control_jar_sha256':digest(probe),'native_outbound_packets_delayed_fifo':True,'log_audits':reports} (root/'result.json').write_text(json.dumps(result,indent=2)+'\n') print('NATIVE_RESPAWN_CONTROL_PASS',json.dumps(result),flush=True) diff --git a/tools/verify_nbt_recovery.py b/tools/verify_nbt_recovery.py index 9993b3b..c732437 100644 --- a/tools/verify_nbt_recovery.py +++ b/tools/verify_nbt_recovery.py @@ -10,7 +10,7 @@ def main(): p.add_argument('--flat-header',type=Path,required=True);p.add_argument('--flat-header-sha256',required=True) p.add_argument('--baseline-jar',type=Path);p.add_argument('--output',type=Path,required=True);a=p.parse_args() project=Path(__file__).resolve().parents[1];root=a.output.resolve();root.mkdir(parents=True,exist_ok=False) - java=a.java.resolve();distribution=a.forge_home.resolve();main=project/'build/libs'/MAIN;baseline=a.baseline_jar.resolve() if a.baseline_jar else main;probe=project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' + java=a.java.resolve();distribution=a.forge_home.resolve();main=project/'build/libs'/MAIN;baseline=a.baseline_jar.resolve() if a.baseline_jar else main;probe=project/'build/integration-test-mod/lootablebodies-build-tests.jar' original=digest(baseline);candidate=digest(main) seed=root/'sealed-seed';prepare(seed,distribution,baseline,probe) seed_flat_fixture(seed,a.flat_header,a.flat_header_sha256) diff --git a/tools/verify_packaged.py b/tools/verify_packaged.py index 9b1b318..c7f92c2 100644 --- a/tools/verify_packaged.py +++ b/tools/verify_packaged.py @@ -1,4 +1,4 @@ -"""Verify the exact production jar in disposable native Forge 45 servers.""" +"""Verify the exact production jar in disposable native Forge 50 servers.""" import argparse import json from pathlib import Path @@ -22,7 +22,7 @@ def main(): parser.add_argument('--flat-header-sha256',required=True) args=parser.parse_args();project=Path(__file__).resolve().parents[1] main_jar=project/'build/libs'/MAIN - probe=project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' + probe=project/'build/integration-test-mod/lootablebodies-build-tests.jar' output=args.output.resolve();output.mkdir(parents=True,exist_ok=False) distribution=args.forge_home.resolve() fresh = output / "candidate-fresh" diff --git a/tools/verify_packaged_client.py b/tools/verify_packaged_client.py index e55d5ea..7644dfd 100644 --- a/tools/verify_packaged_client.py +++ b/tools/verify_packaged_client.py @@ -1,4 +1,4 @@ -"""Run the exact production jar and isolated verifier in official Forge 45.""" +"""Run the exact production jar and isolated verifier in official Forge 50.""" import argparse import hashlib import json @@ -83,7 +83,7 @@ def main(): if args.rejected_displaced_viewer and args.mode!='issues': raise ValueError('Rejected/displaced viewer control requires issues mode') root=args.output.resolve();main=project/'build/libs'/MAIN_JAR - probe=project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' + probe=project/'build/integration-test-mod/lootablebodies-build-tests.jar' candidate_hash=hashlib.sha256(main.read_bytes()).hexdigest().upper() prepare_client(root,main,probe);(root/'config').mkdir(exist_ok=True) # Inventory/death fixtures exercise gameplay, independent of live skin HTTP. diff --git a/tools/verify_upgrade_abort.py b/tools/verify_upgrade_abort.py index f7239fa..31fed0a 100644 --- a/tools/verify_upgrade_abort.py +++ b/tools/verify_upgrade_abort.py @@ -101,14 +101,15 @@ def allowed_native40_platform_additions(before,after,world,original_config=None) -def allowed_native47_platform_additions(before,after,world,original_config=None): - """Exact Forge47 corrections; no unknown keys or supported choices dropped.""" +def allowed_forge_platform_additions(before,after,world,original_config=None,*,forge): + """Exact Forge config corrections; retain all supported choices.""" + if forge not in (47,50):raise ValueError('Unsupported Forge config target') name='serverconfig/forge-server.toml' if name not in after:return [] actual=(world/name).read_bytes() if digest(world/name)!=after[name]:return [] try: - defaults=tomllib.loads((Path(__file__).parent/'fixtures/forge47-server-default.toml').read_text()) + defaults=tomllib.loads((Path(__file__).parent/f'fixtures/forge{forge}-server-default.toml').read_text()) upgraded=tomllib.loads(actual.decode('utf-8')) if name not in before:return [name] if same_typed_values(defaults,upgraded) else [] if original_config is None or before[name]!=__import__('hashlib').sha256(original_config).hexdigest().upper():return [] @@ -116,12 +117,14 @@ def allowed_native47_platform_additions(before,after,world,original_config=None) retired={'removeErroringTileEntities':bool,'logCascadingWorldGeneration':bool, 'fixVanillaCascading':bool,'dimensionUnloadQueueDelay':int,'clumpingThreshold':int, 'fixAdvancementLoading':bool,'treatEmptyTagsAsAir':bool,'skipEmptyShapelessCheck':bool} + if forge==50:retired.update(zombieBaseSummonChance=float,zombieBabyChance=float) for key,kind in retired.items(): if key not in server:continue value=server.pop(key) if type(value) is not kind:return [] if key=='dimensionUnloadQueueDelay' and value<0:return [] if key=='clumpingThreshold' and not 64<=value<=1024:return [] + if key in ('zombieBaseSummonChance','zombieBabyChance') and not 0<=value<=1:return [] for key,value in defaults['server'].items():server.setdefault(key,value) return [name] if same_typed_values(previous,upgraded) else [] except (ValueError,KeyError,UnicodeError):return [] @@ -169,9 +172,9 @@ def inject_future_entity_version(world): before=digest(path);original=world.parent/'future-entity-original.mca' if original.exists():raise ValueError('Future fixture original already exists') shutil.copy2(path,original) - root['DataVersion']=Tag(3,3466);write_disposable_region(path,chunks) + root['DataVersion']=Tag(3,3840);write_disposable_region(path,chunks) return {'region':path.relative_to(world).as_posix(),'chunk_index':index,'source_version':2975, - 'injected_version':3466,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), + 'injected_version':3840,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), 'original_copy':str(original),'corpse_marker':8} @@ -194,7 +197,7 @@ def main(): args = parser.parse_args() project = Path(__file__).resolve().parents[1] candidate = project / 'build/libs' / MAIN_JAR - probe = project / 'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' + probe = project / 'build/integration-test-mod/lootablebodies-build-tests.jar' source = args.source_world.resolve() sealed = fingerprint(source) root = args.output.resolve() @@ -224,7 +227,7 @@ def main(): raise RuntimeError('World loading continued after unsafe preservation') after = source_files(world) raw_after = dict(after) - platform_additions = allowed_native47_platform_additions(before, after, world, original_config) + platform_additions = allowed_forge_platform_additions(before, after, world, original_config,forge=50) lock_updates=allowed_native_session_lock(before,after,world,original_lock) for name in platform_additions+lock_updates: if name in before: after[name] = before[name] diff --git a/tools/verify_visibility.py b/tools/verify_visibility.py index 87cf96f..3369bd5 100644 --- a/tools/verify_visibility.py +++ b/tools/verify_visibility.py @@ -55,7 +55,7 @@ def main(): java=args.java.resolve(); distribution=args.forge_home.resolve(); home=args.output.resolve() home.mkdir(parents=True,exist_ok=False); shared=home/'sync'; shared.mkdir() jar=project/'build/libs'/MAIN; before=digest(jar) - probe=(args.probe_jar or project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar').resolve() + probe=(args.probe_jar or project/'build/integration-test-mod/lootablebodies-build-tests.jar').resolve() prepare(home/'server',distribution,jar,probe) source_before=None if args.source_server: diff --git a/tools/verify_world_upgrades.py b/tools/verify_world_upgrades.py index da64ccf..f5c4c4d 100644 --- a/tools/verify_world_upgrades.py +++ b/tools/verify_world_upgrades.py @@ -2,11 +2,13 @@ import argparse import hashlib import json +import math from pathlib import Path import shutil import subprocess import gzip import struct +from uuid import UUID from audit_runtime_logs import audit, FORGE_UPGRADE_RENAME from release_identity import MAIN_JAR from nbt_regions import Tag, decode, encode, read_region, write_disposable_region @@ -490,10 +492,11 @@ def seed_transition112(args,project,root,seeder,origin): 'upgrade-far', 'upgrade-far-reload', 'upgrade-late', 'upgrade-return', 'upgrade-final', 'upgrade-final-reload'] -def run_upgrade(java, home, phase): +def run_upgrade(java, home, phase, writer_inventory=None): from native_runtime import run_server, HISTORICAL_JVM_ARGS logging=Path(__file__).resolve().parents[1]/'src/integrationTest/resources/log4j2.xml' - run_server(java,home,phase,extra=[*HISTORICAL_JVM_ARGS,'-Dlog4j.configurationFile='+logging.as_uri()], + oracle = ['-Dlootablebodies.testWriterInventory='+writer_inventory] if writer_inventory else [] + run_server(java,home,phase,extra=[*HISTORICAL_JVM_ARGS,*oracle,'-Dlog4j.configurationFile='+logging.as_uri()], message='LOOTABLE_BODIES_WORLD_UPGRADE_PASS phase='+phase) @@ -574,6 +577,63 @@ def preserve_historical_markers(home): return [marker.name for marker in markers] +def saved_uuid(body): + uid = body.get('UUID') + if uid is not None: + if uid.kind != 11 or len(uid.value) != 16: + raise ValueError('Invalid fixture UUID array') + return UUID(bytes=uid.value) + most, least = body['UUIDMost'], body['UUIDLeast'] + if most.kind != 4 or least.kind != 4: + raise ValueError('Invalid legacy fixture UUID') + return UUID(bytes=struct.pack('>qq', most.value, least.value)) + + +def bridge_writer_inventory(source): + """Read each fixture corpse from the sealed source before stamp mutations.""" + _, manifest = decode(gzip.decompress((source/'upgrade-fixtures.nbt').read_bytes())) + regions = {} + rows = {} + for row in manifest.value['Bodies'].value[1]: + fixture = row.value['Body'].value + uid = saved_uuid(fixture) + dimension = {0:'', -1:'DIM-1', 1:'DIM1'}[row.value['Dimension'].value] + pos = fixture['Pos'].value[1] + x, z = (math.floor(struct.unpack('>d', pos[i].value)[0]) >> 4 for i in (0, 2)) + index = (x & 31) + (z & 31)*32 + matches = [] + for directory in ('region', 'entities'): + path = source/dimension/directory/f'r.{x >> 5}.{z >> 5}.mca' + if not path.is_file(): + continue + if path not in regions: + regions[path] = read_region(path) + chunk = regions[path].get(index) + if chunk is None: + continue + raw = chunk['root'].value + payload = raw.get('Level', chunk['root']).value + entities = payload.get('Entities', payload.get('entities', Tag(9, (10, [])))) + if entities.kind != 9 or entities.value[0] not in (0, 10): + raise ValueError('Malformed sealed bridge entity list: '+str(path)) + for entity in entities.value[1]: + body = entity.value + if body.get('id') not in (Tag(8, 'lootablebodies:corpse'), Tag(8, 'lootablebodies.Corpse')): + continue + if saved_uuid(body) == uid: + matches.append((body, raw.get('DataVersion'))) + if len(matches) != 1 or str(uid) in rows: + raise ValueError('Missing or ambiguous sealed bridge corpse: '+str(uid)) + body, version = matches[0] + if version is None or version.kind != 3 or version.value not in (512,922,1343,1631,1976,2230,2586,2730,2975,3337): + raise ValueError('Missing authentic bridge writer version: '+str(uid)) + inventory = {key:body[key] for key in ('Equipment','Aux','LootableBodiesUnresolved') if key in body} + rows[str(uid)] = Tag(10, {'DataVersion':version,'Inventory':Tag(10,inventory)}) + if len(rows) != 9: + raise ValueError('Expected all nine sealed bridge corpse inventories') + return gzip.compress(encode('',Tag(10,rows)),mtime=0) + + def verify(args, project): prepared=getattr(args,'prepared_sources',None) if not args.seeds and not prepared: @@ -584,7 +644,7 @@ def verify(args, project): root = args.output.resolve() root.mkdir(parents=True, exist_ok=False) candidate = project / 'build/libs' / MAIN_JAR - probe = project / 'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' + probe = project / 'build/integration-test-mod/lootablebodies-build-tests.jar' candidate_hash = digest(candidate) probe_hash = digest(probe) sources = {};prepared_proof=None @@ -611,6 +671,7 @@ def verify(args, project): report_lock=Lock() def run_origin(origin): source,details=sources[origin] + writer_inventory = bridge_writer_inventory(source) if origin.startswith('via') else None for variant in args.variants: name = origin + '-' + variant home = root / name @@ -618,6 +679,12 @@ def run_origin(origin): prepare_server(home,args.forge_home.resolve(),candidate,probe) shutil.copytree(source, home / 'world') world = home / 'world' + inventory_file = world/'upgrade-writer-inventory.nbt' + inventory_hash = None + if writer_inventory is not None: + with inventory_file.open('xb') as stream: + stream.write(writer_inventory) + inventory_hash = digest(inventory_file) historical_markers=preserve_historical_markers(home) player_files = list((world / 'playerdata').glob('*.dat')) if len(player_files) != 1: @@ -630,7 +697,12 @@ def run_origin(origin): raise RuntimeError('Missing distant region fixtures') phases = [] for phase in PHASES: - run_upgrade(args.java.resolve(), home, phase) + if inventory_hash is None: + run_upgrade(args.java.resolve(), home, phase) + else: + run_upgrade(args.java.resolve(), home, phase, inventory_hash) + if digest(inventory_file) != inventory_hash: + raise RuntimeError('Sealed bridge inventory oracle changed') wave = 0 if phase.startswith('upgrade-near') else 1 if phase.startswith('upgrade-far') else 2 if phase == 'upgrade-late' else -1 from native120_persistence import count_persisted persisted = count_persisted(world,wave) @@ -648,6 +720,8 @@ def run_origin(origin): report['cases'][name] = {'phases': list(phases), 'distant_regions': distant, 'stamp_mutations': changes, 'source_unchanged': True, 'origin_jar_sha256': details['jar_sha256'], 'preserved_historical_markers':historical_markers} + if inventory_hash is not None: + report['cases'][name]['writer_inventory_sha256'] = inventory_hash (root / 'result.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8') print('WORLD_UPGRADE_PASS', name, phase, flush=True) run_origins(args.origins,run_origin,workers=getattr(args,'workers',1)) From ccb9643c78856921d68595820ee904d8beaee6eb Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Fri, 6 Nov 2026 22:34:44 +0000 Subject: [PATCH 2/3] Pin the Java 21 build in GitHub Actions --- .github/scripts/test_release_workflow.py | 1 + .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/deploy-release.yml | 4 ++-- .github/workflows/native120.yml | 2 +- tools/hosted_policy.py | 4 ++-- tools/test_hosted_policy.py | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 5b2eacf..f4c1509 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -129,6 +129,7 @@ def test_1206_requires_exact_archives_and_java_21(self): with self.subTest(options=options), self.assertRaises(SystemExit): self.validate('1.20.6', **options) self.assertIn("minecraft_version == '1.20.6' && 'Java 21'", WORKFLOW.read_text()) + self.assertEqual(2, WORKFLOW.read_text().count("minecraft_version == '1.20.6' && '21.0.7+6.0.LTS'")) def test_future_1211_candidate_cannot_dispatch_publication(self): self.assertNotEqual(0, self.select('2.4.13.121011')[0]) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 63adfc4..8dfa8cf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,7 +41,7 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '21.0.7+6' + java-version: '21.0.7+6.0.LTS' - name: Pin production JDK run: | diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 4efe56f..4c19d11 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -401,12 +401,12 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.20.6' && '21.0.7+6' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.20.6' && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.20.6' && '21.0.7+6' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '1.20.6' && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain run: | if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.6' ]]; then diff --git a/.github/workflows/native120.yml b/.github/workflows/native120.yml index 4f60f8a..5b7589f 100644 --- a/.github/workflows/native120.yml +++ b/.github/workflows/native120.yml @@ -48,7 +48,7 @@ jobs: - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '21.0.7+6' + java-version: '21.0.7+6.0.LTS' # Full Java/Python suites, games, IDE consumers and SDK installs stay local. # Fork pushes run policy checks only. MMD builds independently check bytes. - name: Compile and audit release archives diff --git a/tools/hosted_policy.py b/tools/hosted_policy.py index cb4a845..fe535b7 100644 --- a/tools/hosted_policy.py +++ b/tools/hosted_policy.py @@ -19,7 +19,7 @@ def gradle_roots(command): return roots def validate_workflow(text): - if "java-version: '21.0.7+6'" not in text: + if "java-version: '21.0.7+6.0.LTS'" not in text: raise ValueError('Use the setup-java SemVer identifier for the exact qualified JDK') commands='\n'.join(line.strip() for line in text.splitlines() if line.startswith(' ') and not line.strip().startswith('#')) for forbidden in ('unittest discover','-m unittest','native_runtime.py','native117_ci.py','verify_world_upgrades.py','verify_packaged','prepare_native','xvfb-run','--seed-only'): @@ -74,7 +74,7 @@ def require_qualified_manifest(record): def check_source(root): validate_workflow((root/'.github/workflows/native120.yml').read_text(encoding='utf-8')) for workflow in ('codeql-analysis.yml','deploy-release.yml'): - if "'21.0.7+6'" not in (root/'.github/workflows'/workflow).read_text(encoding='utf-8'): + if "'21.0.7+6.0.LTS'" not in (root/'.github/workflows'/workflow).read_text(encoding='utf-8'): raise ValueError('Missing qualified Java 21 version in '+workflow) if 'feature/**' in(root/'.github/workflows/build.yml').read_text():raise ValueError('Copied legacy CI still catches modern feature branches') if 'workflow_dispatch:' in(root/'.github/workflows/migration-matrix.yml').read_text():raise ValueError('Copied legacy migration workflow can still dispatch on a modern ref') diff --git a/tools/test_hosted_policy.py b/tools/test_hosted_policy.py index fff379e..0256219 100644 --- a/tools/test_hosted_policy.py +++ b/tools/test_hosted_policy.py @@ -6,7 +6,7 @@ class HostedPolicyTests(unittest.TestCase): def test_raw_jdk_version_is_rejected_before_java_preparation(self): text=(Path(__file__).resolve().parents[1]/'.github/workflows/native120.yml').read_text() - with self.assertRaises(ValueError):validate_workflow(text.replace('21.0.7+6','21.0.7+99')) + with self.assertRaises(ValueError):validate_workflow(text.replace('21.0.7+6.0.LTS','21.0.7+99')) def test_hosted_roots_allow_archive_builds_and_reject_every_test_or_game_entry(self): base='./gradlew assemble verifyProductionRuntimeClasspath prepareReleaseBundle ' From 63ec501c8f6291d031e54df7d8de8878f97a50dc Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Fri, 6 Nov 2026 22:41:06 +0000 Subject: [PATCH 3/3] Keep Java 21 documentation archives consistent --- build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5e931c4..ab53726 100644 --- a/build.gradle +++ b/build.gradle @@ -251,10 +251,11 @@ tasks.named('sourcesJar', Jar) { inputs.property('accessTransformerLineEndings', 'lf') filesMatching('META-INF/accesstransformer.cfg', normalizeArchiveText) } -// Java 21 writes this extensionless index with the host line separator. Keep -// the documentation bytes identical on Windows and Linux. +// Java 21 writes the index and its bundled SVG icons with the host line +// separator. Keep the documentation bytes identical on Windows and Linux. tasks.named('javadocJar', Jar) { - filesMatching('element-list') { + inputs.property('javadocLineEndings', 'crlf-index-and-icons') + filesMatching(['element-list', 'copy.svg', 'link.svg']) { filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('crlf'), eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) }