diff --git a/.github/release120.json b/.github/release120.json index 673815d..30cbd45 100644 --- a/.github/release120.json +++ b/.github/release120.json @@ -1,16 +1,16 @@ { - "version": "2.4.12.120061", - "minecraft": "1.20.6", - "forge": "50.2.14", + "version": "2.4.13.121011", + "minecraft": "1.21.1", + "forge": "52.1.20", "local_qualification_complete": true, "qualification_integrity_status": "verified", "manual_accepted": true, "files": { - "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" + "CHANGELOG.txt": "A7A09C8F1C6CEC52E4861B97AE8D4637C45054931F2D8D07ADC60F503E3561F3", + "LootableBodies-2.4.13.121011-javadoc.jar": "4DA01E1F17CA8D17F41DA5BE15C6513359B64598167A467096E6E5992AA1270B", + "LootableBodies-2.4.13.121011-sources.jar": "EAFC9F737A70846A99A96D625C38485ED82248D74E7D907D62FF02F63622BEDD", + "LootableBodies-2.4.13.121011.jar": "F24183DF61AB677017915CE0D0B17816D6D6A6C095BC6FBF6E40070E33DC1845", + "LootableBodies-2.4.13.121011.pom": "4FAC990CA9B74FB3AEB8B734EFBEE8461D84682C33ACCB95B8DD4A7E0443E272", + "SHA256SUMS": "147108294CEAA421090E75CFBDBC537E7F6E087DB116A1638783D65C89739486" } } diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index f4c1509..05fb4fe 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -72,13 +72,14 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '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'), - '1.20.6': ('120061', 'master-1.20.6', '50.2.14')}[minecraft] + '1.20.6': ('120061', 'master-1.20.6', '50.2.14'), + '1.21.1': ('121011', 'master-1.21.1', '52.1.20')}[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='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', + java_version='21' if minecraft in ('1.20.6','1.21.1') 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 in ('1.20.6','1.21.1') 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 in ('1.20.6','1.21.1') 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: @@ -88,13 +89,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','1.20.6') and names is None:checks=CORE118 + if minecraft in ('1.18.2','1.19.4','1.20.1','1.20.6','1.21.1') 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','1.20.6':'120'}[minecraft] + code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120','1.21.1':'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]}) @@ -128,11 +129,22 @@ def test_1206_requires_exact_archives_and_java_21(self): {'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()) - self.assertEqual(2, WORKFLOW.read_text().count("minecraft_version == '1.20.6' && '21.0.7+6.0.LTS'")) + self.assertIn("(needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1') && 'Java 21'", WORKFLOW.read_text()) + self.assertEqual(2, WORKFLOW.read_text().count("(needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1') && '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]) + def test_1211_requires_exact_archives_and_java_21(self): + code, values = self.select('2.4.13.121011') + self.assertEqual(0, code) + self.assertEqual({'minecraft_version': '1.21.1', 'source_branch': 'master-1.21.1'}, values) + self.validate('1.21.1') + for options in ({'mutation': {'java_version': '17'}}, {'audit_success': False}, + {'qualification': {'head_branch': 'master-1.20.6'}}, + {'local_receipt': {'manual_accepted': False}}): + with self.subTest(options=options), self.assertRaises(SystemExit): + self.validate('1.21.1', **options) + + def test_future_12111_candidate_cannot_dispatch_publication(self): + self.assertNotEqual(0, self.select('2.4.14.121111')[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.21.yml b/.github/workflows/artifact-audit-1.21.yml new file mode 100644 index 0000000..6835f5c --- /dev/null +++ b/.github/workflows/artifact-audit-1.21.yml @@ -0,0 +1,48 @@ +name: Audit LootableBodies 1.21 artifacts +run-name: Audit 1.21.1 artifacts from ${{ github.event.workflow_run.head_sha }} +on: + workflow_run: + workflows: [LootableBodies 1.21.1 CI] + types: [completed] +permissions: + contents: read + actions: read +jobs: + audit: + 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.21.1' && github.event.workflow_run.name == 'LootableBodies 1.21.1 CI' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.21.1-${{ github.event.workflow_run.head_sha }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + path: audited + - name: Independently verify every locally qualified byte + env: + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + python3 -B tools/native120_build.py --bundle audited + python3 - <<'PY' + import json,os + from pathlib import Path + accepted=json.loads(Path('.github/release120.json').read_text()) + Path('independent-audit.json').write_text(json.dumps({'source':os.environ['SOURCE_SHA'],'files':accepted['files'],'all_six_exact':True,'identical_to_locally_audited_bundle':True},indent=2)+'\n') + PY + # Exact hashes reuse the local classfile/licence/identity/mapping audit. + # This independent receipt is a byte match, not a fresh gameplay run or + # another Gradle/Forge preparation. The build job audits new archives. + - name: Retain independent audit evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native120-independent-audit-${{ github.event.workflow_run.head_sha }} + if-no-files-found: error + path: independent-audit.json diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8dfa8cf..614f249 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.6 + - master-1.21.1 - 'feature/**' pull_request: branches: - - master-1.20.6 + - master-1.21.1 schedule: - cron: '43 7 * * 4' diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 4c19d11..f6317e6 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -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, 1.20.1 and 1.20.6 releases. + description: Hash-bound local qualification receipt, required for the 1.17.1, 1.18.2, 1.19.4, 1.20.1, 1.20.6 and 1.21.1 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|120061)$ ]] + [[ "$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|121011)$ ]] 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 ;; @@ -68,6 +68,7 @@ jobs: *.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 ;; + *.121011) minecraft_version=1.21.1; source_branch=master-1.21.1 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -104,16 +105,17 @@ jobs: '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.6': ('master-1.20.6', '50.2.14', '120061')} + '1.20.6': ('master-1.20.6', '50.2.14', '120061'), + '1.21.1': ('master-1.21.1', '52.1.20', '121011')} 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', '1.20.6') + archive_only = minecraft in ('1.18.2', '1.19.4', '1.20.1', '1.20.6', '1.21.1') locally_qualified = native17 or archive_only expected = {'minecraft_version': minecraft, 'forge_version': forge, - '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', + 'loader_name': 'forge', 'loader_code': '1', 'java_version': '21' if minecraft in ('1.20.6', '1.21.1') else '17' if archive_only else '16' if native17 else '8', + 'java_toolchain_version': '21.0.7+6' if minecraft in ('1.20.6', '1.21.1') 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 in ('1.20.6', '1.21.1') else '17', 'mod_id': 'lootablebodies', 'mod_artifact': 'LootableBodies', 'mod_group': 'zone.moddev.mc.lootablebodies', 'mod_license': 'LGPL-2.1-only', 'curseforge_project_id': '240968'} @@ -153,7 +155,7 @@ 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','1.20.6':'120'}[minecraft] + target_code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120','1.21.1':'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'): @@ -177,7 +179,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', '1.20.6'): + 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', '1.21.1'): 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'] @@ -211,25 +213,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' && needs.preflight.outputs.minecraft_version != '1.20.6' + 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' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' 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' && needs.preflight.outputs.minecraft_version != '1.20.6' + 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' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' 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' && needs.preflight.outputs.minecraft_version != '1.20.6' + 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' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' 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' && needs.preflight.outputs.minecraft_version != '1.20.6' + 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' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' 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" @@ -240,7 +242,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' && needs.preflight.outputs.minecraft_version != '1.20.6' + 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' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Download the qualified native117 six-file bundle if: needs.preflight.outputs.minecraft_version == '1.17.1' @@ -285,7 +287,7 @@ 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' || needs.preflight.outputs.minecraft_version == '1.20.6' + if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.1' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: LootableBodies-${{ needs.preflight.outputs.minecraft_version }}-${{ needs.preflight.outputs.release_sha }} @@ -293,10 +295,10 @@ jobs: 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' || needs.preflight.outputs.minecraft_version == '1.20.6' + if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.1' 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' && needs.preflight.outputs.minecraft_version != '1.20.6' + 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' && needs.preflight.outputs.minecraft_version != '1.21.1' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ @@ -401,15 +403,15 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - 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' }} + java-version: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1') && '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.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' }} + java-version: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1') && '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 + if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.6' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.21.1' ]]; 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" @@ -469,7 +471,7 @@ jobs: game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - 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' }} + java: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1') && '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 5b7589f..62d4840 100644 --- a/.github/workflows/native120.yml +++ b/.github/workflows/native120.yml @@ -1,10 +1,10 @@ -name: LootableBodies 1.20.6 CI +name: LootableBodies 1.21.1 CI on: push: - branches: [master-1.20.6] + branches: [master-1.21.1] pull_request: - branches: [master-1.20.6] + branches: [master-1.21.1] permissions: contents: read @@ -63,7 +63,7 @@ jobs: - name: Retain the exact six release files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.20.6-${{ github.sha }} + name: LootableBodies-1.21.1-${{ 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 e9339d7..3d45efd 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.6'] + branches: ['feature/**', 'master-1.21.1'] pull_request: - branches: ['master-1.20.6'] + branches: ['master-1.21.1'] permissions: contents: read jobs: @@ -25,7 +25,7 @@ jobs: run: >- "$RUNNER_TEMP/actionlint/actionlint" -shellcheck= -pyflakes= .github/workflows/deploy-release.yml .github/workflows/release-workflow-checks.yml .github/workflows/artifact-audit-1.13.yml .github/workflows/artifact-audit-1.14.yml .github/workflows/artifact-audit-1.15.yml .github/workflows/artifact-audit-1.16.yml .github/workflows/artifact-audit-1.17.yml - .github/workflows/native120.yml .github/workflows/artifact-audit-1.20.yml .github/workflows/codeql-analysis.yml + .github/workflows/native120.yml .github/workflows/artifact-audit-1.20.yml .github/workflows/artifact-audit-1.21.yml .github/workflows/codeql-analysis.yml - name: Test version selection and publication checks run: python3 .github/scripts/test_release_workflow.py -v diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 04732ac..c019ce5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,6 @@ -2.4.12.120061 - Minecraft 1.20.6, Forge +2.4.13.121011 - Minecraft 1.21.1, Forge -Updated for Minecraft 1.20.6. +Updated for Minecraft 1.21.1. 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 3080101..5c3aef1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lootable Bodies for Minecraft 1.20.6 +# Lootable Bodies for Minecraft 1.21.1 Your items stay on a corpse when you die. Right-click the body to recover them. -Use Forge 50.2.14 and Java 21. Install `LootableBodies-2.4.12.120061.jar` +Use Forge 52.1.20 and Java 21. Install `LootableBodies-2.4.13.121011.jar` in the client and server mods folders. Back up your world before upgrading. Created by Cyanobacterium (DrCyano), maintained by SkyBlade1978. @@ -12,7 +12,7 @@ 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, 1.19.4 and 1.20.1 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, 1.20.1 and 1.20.6 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 14e31ad..b6a7060 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -17,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, 1.20.1 and 1.20.6 the receipt binds the exact source commit, version, + release workflow. For 1.17.1, 1.18.2, 1.19.4, 1.20.1, 1.20.6 and 1.21.1 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 1c8a5fb..ee7e300 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,7 +1,7 @@ # Testing -Run qualification for Forge 1.20.6 locally with Forge 50.2.14 and Java 21. -GitHub Actions checks production archives and release contracts. +Run qualification for Forge 1.21.1 locally with Forge 52.1.20 and Java 21. +Qualification runs locally on this branch. During development, reproduce the changed behaviour first, then batch the cheap Java and Python checks. Reserve the full runtime suite for a frozen candidate. diff --git a/build.gradle b/build.gradle index ab53726..8f2e5e1 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,6 @@ plugins { id 'idea' id 'maven-publish' id 'net.minecraftforge.renamer' version '1.1.5' - id 'net.minecraftforge.accesstransformers' version '2.0.0' id 'net.minecraftforge.gradle' version '7.0.34' } @@ -101,8 +100,8 @@ tasks.register('generateNative120Fixtures', JavaExec) { dependsOn 'testClasses' classpath = sourceSets.test.runtimeClasspath mainClass = 'cyano.lootable.buildtest.Native120FixtureGenerator' - args providers.gradleProperty('nativeFixtureOutput').getOrElse('build/native1206-fixture-preparation-01'), - file('tools/fixtures/native1206-flat-level.dat').absolutePath + args providers.gradleProperty('nativeFixtureOutput').getOrElse('build/native121-fixture-preparation-01'), + file('tools/fixtures/native121-flat-level.dat').absolutePath } tasks.withType(JavaExec).configureEach { javaLauncher = productionLauncher } tasks.withType(Javadoc).configureEach { @@ -145,7 +144,6 @@ apply from: 'gradle/assets.gradle' minecraft { mappings channel: project.mapping_channel, version: project.mapping_version - accessTransformer = 'META-INF/accesstransformer.cfg' runs { configureEach { workingDir.convention layout.projectDirectory.dir( @@ -231,11 +229,10 @@ tasks.named('jar', Jar) { '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': '21', 'Built-On': "${minecraft_version}-${forge_version}", - 'FMLAT': 'accesstransformer.cfg') + 'Built-On-Java': '21', 'Built-On': "${minecraft_version}-${forge_version}") filesMatching(archiveTextPatterns, normalizeArchiveText) } -// Forge 50.2.14 ships its runtime with official names. +// Forge 52.1.20 ships its runtime with official names. ext.releaseJar = tasks.named('jar', Jar) ['sourcesJar', 'javadocJar'].each { name -> tasks.named(name, Jar) { @@ -247,10 +244,6 @@ ext.releaseJar = tasks.named('jar', Jar) filesMatching(archiveTextPatterns, normalizeArchiveText) } } -tasks.named('sourcesJar', Jar) { - inputs.property('accessTransformerLineEndings', 'lf') - filesMatching('META-INF/accesstransformer.cfg', normalizeArchiveText) -} // 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) { @@ -286,7 +279,7 @@ tasks.register('verifyProductionRuntimeClasspath') { } tasks.named('check') { dependsOn 'verifyProductionRuntimeClasspath' } -// Publication is restricted to the confirmed MMD release workflow. +// Publication is disabled during local qualification. // Renamer registers every auxiliary probe with assemble. Production archives // have their own explicit inputs; probes are built by their verification gates. diff --git a/gradle.properties b/gradle.properties index ede235e..c3f385c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,10 +5,10 @@ org.gradle.caching=true org.gradle.parallel=false org.gradle.workers.max=2 net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.20.6 -forge_version=50.2.14 +minecraft_version=1.21.1 +forge_version=52.1.20 mapping_channel=official -mapping_version=1.20.6 +mapping_version=1.21.1 # Release identity and dispatcher target metadata. loader_name=forge @@ -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.12.120061 +mod_version=2.4.13.121011 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 59f9cdf..fc2aa3e 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 = '16' +ext.minecraftAssetIndex = '17' // 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,7 +10,7 @@ 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/native1206-client-inputs/natives'))) + .getOrElse(localMinecraftSetup.getProperty('minecraftNativesDirectory','.gradle/native121-client-inputs/natives'))) tasks.register('prepareMinecraftAssets') { group = 'ide' doLast { @@ -28,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.6.json') - download('https://piston-meta.mojang.com/v1/packages/4dd84bf5b646a75cba5c0c5f7723eb478a2b78af/1.20.6.json','4dd84bf5b646a75cba5c0c5f7723eb478a2b78af',descriptor) + File descriptor=new File(root,'1.21.1.json') + download('https://piston-meta.mojang.com/v1/packages/82ba6501da174a665c1787775f946487422f2432/1.21.1.json','82ba6501da174a665c1787775f946487422f2432',descriptor) def version=new groovy.json.JsonSlurper().parse(descriptor) - if(version.id!='1.20.6' || version.assetIndex.id!=minecraftAssetIndex) + if(version.id!='1.21.1' || 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 c9504c4..2305cc9 100644 --- a/gradle/audit.gradle +++ b/gradle/audit.gradle @@ -62,22 +62,22 @@ 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.6]"'].each { + ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.21.1]"'].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!=32 || pack.supported_formats!=[min_inclusive:32,max_inclusive:41] || pack.containsKey('forge:server_data_pack_format') || + if(pack.pack_format!=34 || pack.supported_formats!=[min_inclusive:34,max_inclusive:48] || 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 resource32/server-data41') + throw new GradleException('Native pack formats must be resource34/server-data48') 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 dataFixer'] || - manifest.getValue('FMLAT')!='accesstransformer.cfg') - throw new GradleException('Missing or changed native world-reader access boundary') + String hooks=zip.getInputStream(zip.getEntry('coremods/early-world-preservation.js')).getText('UTF-8') + if (!hooks.contains('lootablebodies_fixer') || !hooks.contains('getDataFixer') || + manifest.getValue('FMLAT')!=null) + throw new GradleException('Missing or changed native data fixer attachment') + if (!hooks.contains('lootablebodies_dedicated_read') || !hooks.contains('readDedicatedWorld')) + throw new GradleException('Missing dedicated world refusal guard') def languages=names.findAll {it.startsWith('assets/lootablebodies/lang/') && !it.endsWith('/')} if(languages.size()!=14 || languages.any {!it.endsWith('.json') || it!=it.toLowerCase(Locale.ROOT)}) throw new GradleException('Incorrect native language resources') @@ -90,7 +90,7 @@ ext.auditReleaseArchives = { File directory -> 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") + def entry=zip.getEntry("data/minecraft/tags/entity_type/${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}") @@ -98,9 +98,6 @@ ext.auditReleaseArchives = { File directory -> } else if (index == 1) { if (!names.contains('cyano/lootable/LootableBodies.java')) throw new GradleException('Sources jar does not contain production sources') - def entry = zip.getEntry('META-INF/accesstransformer.cfg') - if (!entry || zip.getInputStream(entry).getText('UTF-8').contains('\r')) - throw new GradleException('Source access transformer must use LF line endings') } else if (index == 2) { if (!names.contains('index.html')) throw new GradleException('Javadoc jar has no index') File jdk = tasks.named('javadoc', Javadoc).get().javadocTool.get().metadata.installationPath.asFile diff --git a/gradle/ide.gradle b/gradle/ide.gradle index dd9b660..b557349 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -67,7 +67,7 @@ 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.6 client inputs before generating Eclipse launches') + if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native 1.21.1 client inputs before generating Eclipse launches') contents=contents.replace('{asset_index}',minecraftAssetIndex) .replace('{assets_root}',minecraftAssets.asFile.absolutePath) .replace('{natives}',nativeRoot.absolutePath) @@ -138,7 +138,7 @@ tasks.register('verifyEclipseProductionClasspath') { !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 + // Forge 52 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' diff --git a/gradle/release.gradle b/gradle/release.gradle index 3c9791c..db16061 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,8 +5,8 @@ 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.6', forge_version: '50.2.14', - mapping_channel: 'official', mapping_version: '1.20.6', + minecraft_version: '1.21.1', forge_version: '52.1.20', + mapping_channel: 'official', mapping_version: '1.21.1', loader_name: 'forge', loader_code: '1', curseforge_project_id: '240968', 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', @@ -37,7 +37,7 @@ tasks.register('verifyReleaseConfiguration') { doLast { String source = file('src/main/java/cyano/lootable/LootableBodies.java').getText('UTF-8') if (!source.contains("VERSION = \"${project.version}\"") || !source.contains('MODID = "lootablebodies"') || - !source.contains('@Mod(LootableBodies.MODID)') || !source.contains('new ResourceLocation("lootablebodies:corpse")')) + !source.contains('@Mod(LootableBodies.MODID)') || !source.contains('ResourceLocation.fromNamespaceAndPath(MODID,"corpse")')) throw new GradleException('Java release version or persistent identity differs from release metadata') if (!file('README.md').getText('UTF-8').contains(releaseNames[0]) || !file('CHANGELOG.txt').getText('UTF-8').contains(project.version.toString())) diff --git a/gradle/runtime.gradle b/gradle/runtime.gradle index ebe610d..6b6de69 100644 --- a/gradle/runtime.gradle +++ b/gradle/runtime.gradle @@ -70,10 +70,10 @@ def prepareOwnedLoaderConfig = { File root -> } } -// Use an actual Forge 50 codec header in each newly owned flat fixture world. +// Use an actual Forge 52 codec header in each newly owned flat fixture world. def prepareOwnedFlatWorld = { File root -> - File header = file('tools/fixtures/native1206-flat-level.dat') - if (!header.isFile() || releaseDigest(header) != '34253D55B2B3A4F1CE4107FACF5AF64ED496264ED53804405F6CDC6D45E56E74') + File header = file('tools/fixtures/native121-flat-level.dat') + if (!header.isFile() || releaseDigest(header) != 'C85D57B04D1309304A2B28992E5B45B4E695039BE162682255A0CCCCB3EE30CD') 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') diff --git a/settings.gradle b/settings.gradle index cdf0bac..f478699 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,23 +3,8 @@ plugins { } rootProject.name = 'LootableBodies' -// Release uploads remain restricted by the confirmed-controller guard in gradle/release.gradle. -gradle.taskGraph.whenReady { graph -> - // Hosted jobs compile/audit artifacts; all test JVMs and game tasks are local. - // This also rejects an accidental build/check dependency on a costly task. - if (System.getenv('GITHUB_ACTIONS') == 'true') { - def localOnly = graph.allTasks.findAll { task -> - task instanceof org.gradle.api.tasks.testing.Test || - task instanceof org.gradle.api.tasks.JavaExec || - task.name in ['integrationTest', 'clientIntegrationTest', 'forgeControlServer', - 'localRuntimeVerification', 'prepareLegacyWorldFixtures', - 'generateNative120Fixtures', 'generateNative1206Fixtures'] || - task.name.startsWith('integration') - } - if (!localOnly.empty) throw new GradleException( - 'Run expensive verification locally before publishing: ' + localOnly*.path.join(', ')) - } +// Local qualification and manual testing come before hosted checks. +if (System.getenv('GITHUB_ACTIONS') == 'true' && + System.getenv('GITHUB_REPOSITORY') != 'MinecraftModDevelopmentMods/LootableBodies') { + throw new GradleException('Hosted builds are limited to the MMD repository') } - -// Publication remains restricted to the confirmed MMD release controller in -// gradle/release.gradle; local Maven and unconfirmed uploads are rejected there. diff --git a/src/controlTest/java/buildcontrol/ResourceControl.java b/src/controlTest/java/buildcontrol/ResourceControl.java index 7d18b9a..30541ef 100644 --- a/src/controlTest/java/buildcontrol/ResourceControl.java +++ b/src/controlTest/java/buildcontrol/ResourceControl.java @@ -28,10 +28,10 @@ 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("Forge 50 preparation executor changed"); + if(!(prepare instanceof ForkJoinPool))throw new AssertionError("Forge 52 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("Forge 50 worker configuration differs from its actual Util executor"); - System.out.println("NATIVE50_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); + if(limit<1 || limit>255 || workers!=Math.max(1,Math.min(cpus-1,limit)))throw new AssertionError("Forge 52 worker configuration differs from its actual Util executor"); + System.out.println("NATIVE52_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("NATIVE50_RESOURCE_RELOAD_COMPLETE="+round); + if(round%10==0)System.out.println("NATIVE52_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("NATIVE50_RESOURCE_CONTROL_PASS rounds="+rounds); + System.out.println("NATIVE52_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 1a97ad2..552d69d 100644 --- a/src/controlTest/resources/META-INF/mods.toml +++ b/src/controlTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[50,)" +loaderVersion="[52,)" license="LGPL-2.1-only" [[mods]] modId="forgeplatformcontrol" diff --git a/src/controlTest/resources/pack.mcmeta b/src/controlTest/resources/pack.mcmeta index 70fb716..694a10a 100644 --- a/src/controlTest/resources/pack.mcmeta +++ b/src/controlTest/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { - "pack_format": 32, - "supported_formats": {"min_inclusive": 32, "max_inclusive": 41}, + "pack_format": 34, + "supported_formats": {"min_inclusive": 34, "max_inclusive": 48}, "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 d63f33a..b3b849a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java @@ -21,6 +21,11 @@ /** Automated real-client world, packet interaction, GUI render and reload check. */ public final class ClientChecks { + static net.minecraft.core.HolderLookup.Provider fixtureRegistries() { + var level=net.minecraft.client.Minecraft.getInstance().level; + return level==null?null:level.registryAccess(); + } + private int stage, ticks, frames; private int reportedStage=-1; private Future pending; diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java index 0625cf9..94ad6e2 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java @@ -433,7 +433,7 @@ private static final class RecordingConnection extends ServerGamePacketListenerI ItemStack carried=ItemStack.EMPTY; 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; + NativeMenuObservation observed=NativeMenuObservation.decode(packet,player.registryAccess());if(observed==null)return; if(observed.content) { slots.clear();for(int index=0;index extendedHolder() { } 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,"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 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);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java index 7f00d3b..a755c86 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java @@ -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",3839); + root.putInt("xPos",chunk.getPos().x);root.putInt("zPos",chunk.getPos().z);root.put("sections",new net.minecraft.nbt.ListTag());root.putInt("DataVersion",3955); // 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,3839,3839); + net.minecraft.util.datafix.fixes.References.CHUNK,root,3955,3955); try { java.lang.reflect.Method nativeRead=null; for(java.lang.reflect.Method method:net.minecraft.world.level.chunk.storage.ChunkSerializer.class.getDeclaredMethods()) diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java index 264f00a..2ffcfea 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java @@ -24,7 +24,7 @@ public void register(net.minecraftforge.registries.RegisterEvent event) { keepsake=new Item(new Item.Properties()) { @Override public ICapabilityProvider getCapabilityProvider(ItemStack stack){return new Stored();} }; - event.register(net.minecraft.core.registries.Registries.ITEM,new ResourceLocation("lootablebodiesbuildtest:keepsake"),()->keepsake); + event.register(net.minecraft.core.registries.Registries.ITEM,ResourceLocation.parse("lootablebodiesbuildtest:keepsake"),()->keepsake); } } private static final class Stored implements ICapabilitySerializable { @@ -32,7 +32,7 @@ private static final class Stored implements ICapabilitySerializable capability,Direction side){return false;} public net.minecraftforge.common.util.LazyOptional getCapability(Capability capability,Direction side){return net.minecraftforge.common.util.LazyOptional.empty();} - public CompoundTag serializeNBT(){return data.copy();} - public void deserializeNBT(CompoundTag nbt){data=nbt.copy();} + public CompoundTag serializeNBT(net.minecraft.core.HolderLookup.Provider registries){return data.copy();} + public void deserializeNBT(net.minecraft.core.HolderLookup.Provider registries,CompoundTag nbt){data=nbt.copy();} } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java index c599cf5..61752cd 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java @@ -1,8 +1,6 @@ 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; @@ -11,8 +9,14 @@ final class FixtureStackData { private FixtureStackData() { } - static RegistryAccess registries() { - return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + static net.minecraft.core.HolderLookup.Provider registries() { + var server=net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(); + if(server!=null && server.isSameThread())return server.registryAccess(); + var client=net.minecraftforge.fml.DistExecutor.unsafeCallWhenOn( + net.minecraftforge.api.distmarker.Dist.CLIENT,()->ClientChecks::fixtureRegistries); + if(client!=null)return client; + if(server!=null)return server.registryAccess(); + return net.minecraft.data.registries.VanillaRegistries.createLookup(); } static CompoundTag save(ItemStack stack) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java index c73ded4..54ebb5a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java @@ -107,7 +107,7 @@ private static void check(String name,Check test){ catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("HEALTH_ATTRIBUTE_FAIL "+name+": "+e);e.printStackTrace();} finally{LootableBodies.corpseHP=hp;for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))e.discard();} } - static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setUserName("HealthFixture");b.setItem(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(b);return NameLifetimeChecks.saved(b);} + static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setUserName("HealthFixture");b.setItem(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(b);CompoundTag legacy=NameLifetimeChecks.saved(b);legacy.remove("attributes");return legacy;} static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.load(n);RuntimeChecks.require(NameLifetimeChecks.same(before,n),"Input NBT mutated");return b;} static CompoundTag attribute(String name,double base){CompoundTag n=new CompoundTag();n.putString("Name",name);n.putDouble("Base",base);return n;} private static void legacyUuid(CompoundTag n,UUID id){n.remove("UUID");n.putLong("UUIDMost",id.getMostSignificantBits());n.putLong("UUIDLeast",id.getLeastSignificantBits());} @@ -115,7 +115,7 @@ private static void check(String name,Check test){ static CompoundTag with(CompoundTag a){CompoundTag n=base();n.put("Attributes",NbtRecoveryChecks.list(a));return n;} private static void value(double a,double b,String why){RuntimeChecks.require(Double.isFinite(a) && Math.abs(a-b)<1e-6,why+" expected="+b+" actual="+a);} private static void inventoryKept(EntityLootableBody b){RuntimeChecks.require(RuntimeChecks.sameStack(b.getItem(6),NbtRecoveryChecks.item(120,17)),"Complete recoverable inventory changed");} - private static void stable(EntityLootableBody b){CompoundTag a=NameLifetimeChecks.saved(b),z=NameLifetimeChecks.saved(read(a));for(String key:new String[]{"Health","AbsorptionAmount","Attributes","Equipment","Aux",NameLifetimeChecks.RECOVERY,NbtRecoveryChecks.RECOVERY})RuntimeChecks.require(NameLifetimeChecks.same(a.get(key),z.get(key)),"Round trip changed "+key);inventoryKept(b);} + private static void stable(EntityLootableBody b){CompoundTag a=NameLifetimeChecks.saved(b),z=NameLifetimeChecks.saved(read(a));for(String key:new String[]{"Health","AbsorptionAmount","attributes","Equipment","Aux",NameLifetimeChecks.RECOVERY,NbtRecoveryChecks.RECOVERY})RuntimeChecks.require(NameLifetimeChecks.same(a.get(key),z.get(key)),"Round trip changed "+key);inventoryKept(b);} private static void retained(EntityLootableBody b,String key,Tag raw){NameLifetimeChecks.retained(b,key,raw);} private static void badScalar(String key,Tag raw){CompoundTag n=base();n.put(key,raw.copy());EntityLootableBody b=read(n);value(key.equals("Health")?b.getHealth():b.getAbsorptionAmount(),key.equals("Health")?20:0,"Bad scalar did not use safe default");RuntimeChecks.require(b.isAlive(),"Malformed scalar made corpse inaccessible");retained(b,key,raw);stable(b);} private static void numeric(int type){Tag[] values={ByteTag.valueOf((byte)1),ShortTag.valueOf((short)1),IntTag.valueOf(1),LongTag.valueOf(1),FloatTag.valueOf(1),DoubleTag.valueOf(1)};CompoundTag n=base();n.put("Health",values[type]);n.put("AbsorptionAmount",values[type].copy());EntityLootableBody b=read(n);value(b.getHealth(),1,"Valid Health changed");value(b.getAbsorptionAmount(),1,"Valid absorption changed");stable(b);} @@ -143,7 +143,7 @@ private static void check(String name,Check test){ private static void pendingRemoval(){CompoundTag n=base();n.putFloat("Health",0);EntityLootableBody b=read(n);b.baseTick();n=base();n.putDouble("Health",Double.NaN);b.load(n);b.baseTick();RuntimeChecks.require(b.isAlive(),"Repaired owner still dead");inventoryKept(b);stable(b);n=base();n.putFloat("Health",3.5F);b.discard();RuntimeChecks.require(b.isRemoved(),"Native removal fixture was not applied");b.load(n);RuntimeChecks.require(b.isAlive(),"Reused entity kept stale dead flag");value(b.getHealth(),3.5,"Reset healed valid wounded corpse");} private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);n.putDouble("Health",Double.NaN);CompoundTag a=attribute("generic.maxHealth",Double.NaN);n.put("Attributes",NbtRecoveryChecks.list(a,attribute("generic.followRange",24)));EntityLootableBody b=read(n);RuntimeChecks.require(b.isAlive(),"Invalid health made recoverable inventory inaccessible");NbtRecoveryChecks.retained(b,raw);retained(b,"Attribute",a);retained(b,"Health",n.get("Health"));stable(b);stable(read(NameLifetimeChecks.saved(b)));} private static void empty(){CompoundTag n=base();n.put("Equipment",new ListTag());n.putDouble("Health",Double.NaN);EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Health evidence counted as inventory");retained(b,"Health",n.get("Health"));} - private static void healthy(){CompoundTag n=base();EntityLootableBody b=read(n);RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy save gained recovery records");stable(b);} + private static void healthy(){CompoundTag nativeSave=NameLifetimeChecks.saved(read(base()));EntityLootableBody b=read(nativeSave);RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy save gained recovery records");stable(b);} private static void nativeUuid(boolean conflict,boolean malformed){ CompoundTag modifier=modifier(5,0);modifier.remove("UUIDMost");modifier.remove("UUIDLeast"); @@ -161,7 +161,7 @@ private static void legacyForgeAttributes(){ double[] bases={1.75D,70D,.12D};ListTag list=new ListTag();for(int i=0;i drops(ServerPlayer player,ItemStack... stacks) { return result; } private static void handle(PlayerDeathEventHandler handler,ServerPlayer player,List drops) { - handler.entityDropEvent(new LivingDropsEvent(player,player.damageSources().fellOutOfWorld(),drops,0,false)); + handler.entityDropEvent(new LivingDropsEvent(player,player.damageSources().fellOutOfWorld(),drops,false)); } private static CompoundTag data(EntityLootableBody body) { CompoundTag result=new CompoundTag(); body.addAdditionalSaveData(result); return result; @@ -128,7 +128,7 @@ private static Map inventory(EntityLootableBody body) { } private static void add(Map result,ItemStack stack) { if((stack == null || stack.isEmpty()) || stack.getCount()<=0) return; - CompoundTag tag=encoded(stack); tag.remove("count"); + CompoundTag tag=encoded(stack.copyWithCount(1)); tag.remove("count"); String key=tag.toString(); result.put(key,result.getOrDefault(key,0)+stack.getCount()); } private static Map dropped() { @@ -204,7 +204,7 @@ private static void cacheLifecycle() throws Exception { RuntimeChecks.require(!cache(handler).containsKey(player),"Cancelled death cached equipment"); handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().fellOutOfWorld())); List original=drops(player,item(19,23)); - LivingDropsEvent canceledDrops=new LivingDropsEvent(player,player.damageSources().fellOutOfWorld(),original,0,false); + LivingDropsEvent canceledDrops=new LivingDropsEvent(player,player.damageSources().fellOutOfWorld(),original,false); canceledDrops.setCanceled(true);handler.entityDropEvent(canceledDrops); RuntimeChecks.require(!cache(handler).containsKey(player) && original.size()==1 && RuntimeChecks.sameStack(original.get(0).getItem(),item(19,23)),"Cancelled drops retained stale cache or changed inventory"); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java index e266754..150d6b7 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java @@ -51,7 +51,7 @@ static void englishContract(Minecraft mc) throws Exception { private static String translation(Minecraft mc, String code) throws Exception { com.google.gson.JsonObject values; - try(Reader in=new InputStreamReader(mc.getResourceManager().getResource(new ResourceLocation("lootablebodies","lang/"+code+".json")).orElseThrow().open(),StandardCharsets.UTF_8)) { + try(Reader in=new InputStreamReader(mc.getResourceManager().getResource(ResourceLocation.fromNamespaceAndPath("lootablebodies","lang/"+code+".json")).orElseThrow().open(),StandardCharsets.UTF_8)) { values=new com.google.gson.JsonParser().parse(in).getAsJsonObject(); } String expected=values.get("entity.lootablebodies.Corpse.name").getAsString(); @@ -71,7 +71,7 @@ private static void assertLocale(Minecraft mc, String code) throws Exception { retained.putString("Source","FutureFixture"); retained.putString("Data","retain verbatim"); data.put("LootableBodiesUnresolved",NbtRecoveryChecks.list(retained)); body = (EntityLootableBody)FixtureApi.loadEntity(data,mc.level); - RuntimeChecks.require(body != null && new ResourceLocation("lootablebodies:corpse").equals(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(body.getType())), "Registry/display identity changed"); + RuntimeChecks.require(body != null && ResourceLocation.parse("lootablebodies:corpse").equals(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(body.getType())), "Registry/display identity changed"); CompoundTag before = NameLifetimeChecks.saved(body); RuntimeChecks.require(expected.equals(body.getName().getString()), "Untranslated nameless corpse in "+code+": "+body.getName().getString()); RuntimeChecks.require(expected.equals(body.getDisplayName().getString()), "Wrong rendered name in "+code); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java index 88c4f69..0ee8a29 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java @@ -11,9 +11,10 @@ /** Controlled packaged pre-world conversion/refusal; separate from old writers. */ public final class NativePreflightChecks { - static final String INJECTED="NATIVE1206_INJECTED_ENTITY_CHUNK_REFUSAL"; + static final String INJECTED="NATIVE121_INJECTED_ENTITY_CHUNK_REFUSAL"; private static final AtomicInteger early=new AtomicInteger(),worlds=new AtomicInteger(),starts=new AtomicInteger(); private static String mode; + private static boolean installed; static void install(net.minecraftforge.eventbus.api.IEventBus bus) { mode=System.getProperty("lootablebodies.nativePreflight",""); if(mode.isEmpty())return; @@ -24,23 +25,28 @@ static void install(net.minecraftforge.eventbus.api.IEventBus bus) { catch(Exception failure){failure.printStackTrace();} },"PreflightControlReceipt")); bus.addListener((net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent event)->event.enqueueWork(()->{ - DataFixer nativeFixer=NativeVanillaDataFixer.get(); - DataFixer observer=new DataFixer() { + installed=true; + System.out.println("NATIVE121_PREFLIGHT_CONTROL_INSTALLED mode="+mode); + })); + } + /** Called only from the disposable probe's transformer on the early-reader path. */ + public static DataFixer observe(DataFixer delegate) { + if(mode==null || mode.isEmpty())return delegate; + RuntimeChecks.require(installed,"Preflight observation was not installed before reading the world"); + DataFixer nativeFixer=NativeVanillaDataFixer.unwrap(delegate); + return new DataFixer() { public Schema getSchema(int version){return nativeFixer.getSchema(version);} public Dynamic update(DSL.TypeReference type,Dynamic input,int from,int to) { if(type.typeName().equals("entity_chunk") && net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer()==null) { 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("NATIVE1206_PREWORLD_ENTITY_CONVERSION from="+from+" to="+to+" mode="+mode); + early.incrementAndGet();System.out.println("NATIVE121_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("NATIVE1206_PREFLIGHT_CONTROL_INSTALLED mode="+mode); - })); + }; } @SubscribeEvent public void loaded(net.minecraftforge.event.level.LevelEvent.Load event){worlds.incrementAndGet();} @SubscribeEvent public void starting(net.minecraftforge.event.server.ServerAboutToStartEvent event){starts.incrementAndGet();} @@ -73,6 +79,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("NATIVE1206_PACKAGED_PREFLIGHT_CONVERSION_PASS"); + System.out.println("NATIVE121_PACKAGED_PREFLIGHT_CONVERSION_PASS"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java index 9a290c2..ca41fc9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java @@ -89,13 +89,13 @@ private static void check(String name,Check test){ 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=FixtureStackData.save(item(index,count));n.putByte("Slot",(byte)slot);return n;} + static CompoundTag entry(int index,int count,int slot){CompoundTag n=FixtureStackData.save(item(index,1));n.putInt("count",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=FixtureStackData.save(s);n.remove("count");String k=n.toString();sums.put(k,sums.getOrDefault(k,0)+s.getCount());} + private static void add(Map sums,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=FixtureStackData.save(s.copyWithCount(1));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 packet) { - NativeMenuObservation observed=NativeMenuObservation.decode(packet);if(observed==null)return; + NativeMenuObservation observed=NativeMenuObservation.decode(packet,player.registryAccess());if(observed==null)return; if(observed.containerId==player.containerMenu.containerId){menuEvents++;menuState=observed.stateId;if(observed.content)carried=observed.carried;} else if(observed.containerId==-1)carried=observed.item; } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java index a0bebd5..c79976e 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java @@ -50,7 +50,7 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { fixture=new RespawnPacketChecks(server); // Cold software-rendered clients may need more than 500 server ticks // to reach their first login. Exercise the actual fixture tick path. - for(int tick=0;tick<1000;tick++)fixture.tick(new TickEvent.ServerTickEvent(TickEvent.Phase.END,()->true,server)); + for(int tick=0;tick<1000;tick++)fixture.tick(new TickEvent.ServerTickEvent.Post(()->true,server)); RuntimeChecks.require(!fixture.finished&&fixture.index==0&&fixture.phase==0&&fixture.phaseTicks==0, "Client startup consumed the death/respawn case deadline"); System.out.println("RESPAWN_STARTUP_CLOCK_PASS waiting_ticks=1000 case_ticks=0"); @@ -103,14 +103,20 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { RuntimeChecks.require(observerBody!=null,"No observer death corpse"); } } else if(phase==2&&a!=null&&a!=dead&&a.isAlive()&&(!simultaneous||(b!=null&&b!=observerDead&&b.isAlive()))) { - RuntimeChecks.require(body.isAlive()&&VisibilityChecks.has(body,520+index),"Server corpse/inventory lost during respawn"); if(a.level().dimension()!=body.level().dimension())transfer(a,body.level().dimension()); a.connection.teleport(body.getX(),body.getY(),body.getZ()-2.5,0,25); if(b.level().dimension()!=body.level().dimension())transfer(b,body.level().dimension()); b.connection.teleport(body.getX()+1.5,body.getY(),body.getZ()-2.5,0,25); - // With no players left, Forge may unload/reload the Nether between - // death and return. Saved UUIDs survive; runtime entity IDs do not. - body=live(body);if(simultaneous)observerBody=live(observerBody);state(3); + // Forge may unload the death chunk while both clients respawn. + // Resolve its entity inbox before checking the live UUID and inventory. + body=live(body); + RuntimeChecks.require(body.isAlive()&&VisibilityChecks.has(body,520+index),"Server corpse/inventory lost during respawn"); + if(simultaneous) { + observerBody=live(observerBody); + RuntimeChecks.require(observerBody.isAlive()&&VisibilityChecks.has(observerBody,620+index), + "Observer corpse/inventory lost during respawn"); + } + state(3); } else if(phase==3) { RuntimeChecks.require(a!=null&&b!=null&&a.isAlive()&&b.isAlive(),"Unexpected extra player death during the accepted respawn observation"); // In a cold restart, many ordinary saved bodies can collide and @@ -152,7 +158,7 @@ private void writeTracked(ServerPlayer player,String role) throws Exception { } private EntityLootableBody live(EntityLootableBody previous) { ServerLevel w=server.getLevel(previous.level().dimension()); - w.getChunk((int)Math.floor(previous.getX()/16),(int)Math.floor(previous.getZ()/16)); + RuntimeChecks.awaitChunk(w,previous.blockPosition().getX()>>4,previous.blockPosition().getZ()>>4); return RuntimeChecks.find(w,previous.getUUID()); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java index 4523f07..a720c7a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java @@ -96,9 +96,9 @@ static boolean sameStack(ItemStack first,ItemStack second) { public static void run(MinecraftServer server, String phase) throws Exception { ServerLevel world = server.getLevel(FixtureApi.dimensionKey(0)); File folder = world.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); - require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.12.120061"), "identity/version"); + require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.13.121011"), "identity/version"); require(net.minecraftforge.fml.ModList.get().getModContainerById("lootablebodies").get().getModInfo().getVersion().toString().equals(phase.equals("seed") ? "2.4.0" : phase.equals("issues") ? - System.getProperty("lootablebodies.expectedIssueVersion","2.4.12.120061") : "2.4.12.120061"), "loaded release version"); + System.getProperty("lootablebodies.expectedIssueVersion","2.4.13.121011") : "2.4.13.121011"), "loaded release version"); if (!defaultsChecked) require(LootableBodies.addBonesToCorpse && LootableBodies.displayNameTag && !LootableBodies.hurtByEnvironment && !LootableBodies.hurtByAttacks && !LootableBodies.hurtByMisc && LootableBodies.corpseHP == 20 && LootableBodies.ticksPerItemDecay == 6000 && LootableBodies.corpseDecayTime == 72000 && @@ -216,7 +216,8 @@ static ItemStack stack(int index) { contents.add(FixtureStackData.save(new ItemStack(Items.DIAMOND,3))); nested.put("contents", contents); custom.put("nested", nested); FixtureStackData.setCustom(stack,custom);if(index<=5)stack.setDamageValue(index<4?index+2:index==4?17:9); stack.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,net.minecraft.network.chat.Component.literal("Fixture " + index)); - if (index == 4) stack.enchant(net.minecraft.world.item.enchantment.Enchantments.UNBREAKING, 2); + if (index == 4) stack.enchant(FixtureStackData.registries() + .lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT).getOrThrow(Enchantments.UNBREAKING), 2); return stack; } static net.minecraft.world.level.chunk.LevelChunk awaitChunk(ServerLevel world,int x,int z) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java index 6836b5c..f5273bd 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java @@ -123,11 +123,11 @@ private static Object mappingField(net.minecraftforge.registries.MissingMappings } private static void soundMapping() throws Exception { net.minecraftforge.registries.IForgeRegistry registry=net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS; - net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("minecraft:entity.experience_orb.touch"),196); - net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("absentfixture:other_sound"),197); + net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.parse("minecraft:entity.experience_orb.touch"),196); + net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.parse("absentfixture:other_sound"),197); net.minecraftforge.registries.MissingMappingsEvent event=new net.minecraftforge.registries.MissingMappingsEvent(registry.getRegistryKey(),registry,Arrays.asList(old,other)); net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(event); - RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(new net.minecraft.resources.ResourceLocation("minecraft:entity.experience_orb.pickup")),"Known vanilla sound rename not remapped"); + RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(net.minecraft.resources.ResourceLocation.parse("minecraft:entity.experience_orb.pickup")),"Known vanilla sound rename not remapped"); RuntimeChecks.require(mappingField(other,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.DEFAULT,"Unrelated missing mappings must retain Forge's normal handling"); } private static void nativeRenames() { @@ -139,18 +139,18 @@ private static void nativeRenames() { rename(net.minecraftforge.registries.ForgeRegistries.BLOCKS,"wall_sign","oak_wall_sign"); } private static void rename(net.minecraftforge.registries.IForgeRegistry registry,String oldName,String current) { - net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("minecraft",oldName),30000); - net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("absentfixture",oldName),30001); + net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("minecraft",oldName),30000); + net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("absentfixture",oldName),30001); net.minecraftforge.registries.MissingMappingsEvent event=new net.minecraftforge.registries.MissingMappingsEvent(registry.getRegistryKey(),registry,Arrays.asList(old,other)); net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(event); - RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(new net.minecraft.resources.ResourceLocation("minecraft",current)),"Native registry rename event not remapped: "+oldName); + RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("minecraft",current)),"Native registry rename event not remapped: "+oldName); RuntimeChecks.require(mappingField(other,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.DEFAULT,"Foreign registry rename was consumed"); } private static void legacyProfessions() { - net.minecraft.resources.ResourceLocation name=new net.minecraft.resources.ResourceLocation("minecraft:villagerprofessions"); + net.minecraft.resources.ResourceLocation name=net.minecraft.resources.ResourceLocation.parse("minecraft:villagerprofessions"); net.minecraftforge.registries.IForgeRegistry registry=net.minecraftforge.registries.RegistryManager.ACTIVE.getRegistry(name); RuntimeChecks.require(registry!=null && registry.getKeys().size()==6,"Retired vanilla profession registry was not preserved"); - RuntimeChecks.require(!registry.containsKey(new net.minecraft.resources.ResourceLocation("absentfixture:profession")),"Unknown professions must retain native missing-mapping handling"); + RuntimeChecks.require(!registry.containsKey(net.minecraft.resources.ResourceLocation.parse("absentfixture:profession")),"Unknown professions must retain native missing-mapping handling"); RuntimeChecks.require(!net.minecraftforge.registries.RegistryManager.ACTIVE.takeSnapshot(true).containsKey(name),"Retired registry must not be written into new saves"); } private static void bed(int source,int marker,int damage) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java index 84de332..686809a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java @@ -360,9 +360,9 @@ private static void verifyPlayer(MinecraftServer server) throws Exception { require(player.getUUID().equals(id),"Player UUID changed"); if(manifest.contains("Recipe",8)) { require(HistoricalUnlocks.sugarPair(manifest.getString("Recipe"),manifest.getString("Advancement")),"Unexpected historical recipe/advancement fixture"); - net.minecraft.world.item.crafting.RecipeHolder recipe=server.getRecipeManager().byKey(new ResourceLocation("minecraft:sugar_from_sugar_cane")).orElse(null); + net.minecraft.world.item.crafting.RecipeHolder recipe=server.getRecipeManager().byKey(ResourceLocation.parse("minecraft:sugar_from_sugar_cane")).orElse(null); require(recipe!=null && player.getRecipeBook().contains(recipe),"Native sugar recipe conversion lost its unlock"); - net.minecraft.advancements.AdvancementHolder advancement=server.getAdvancements().get(new ResourceLocation("minecraft:recipes/misc/sugar_from_sugar_cane")); + net.minecraft.advancements.AdvancementHolder advancement=server.getAdvancements().get(ResourceLocation.parse("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(); } diff --git a/src/integrationTest/resources/META-INF/coremods.json b/src/integrationTest/resources/META-INF/coremods.json new file mode 100644 index 0000000..447235e --- /dev/null +++ b/src/integrationTest/resources/META-INF/coremods.json @@ -0,0 +1,3 @@ +{ + "lootablebodies_observe_preflight": "coremods/observe-preflight.js" +} diff --git a/src/integrationTest/resources/META-INF/mods.toml b/src/integrationTest/resources/META-INF/mods.toml index dcdcf58..0f59bbf 100644 --- a/src/integrationTest/resources/META-INF/mods.toml +++ b/src/integrationTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[50,)" +loaderVersion="[52,)" license="LGPL-2.1-only" [[mods]] modId="lootablebodiesbuildtest" @@ -8,6 +8,6 @@ displayName="Disposable build checks" [[dependencies.lootablebodiesbuildtest]] modId="lootablebodies" mandatory=true -versionRange="[2.4.12.120061]" +versionRange="[2.4.13.121011]" ordering="AFTER" side="BOTH" diff --git a/src/integrationTest/resources/coremods/observe-preflight.js b/src/integrationTest/resources/coremods/observe-preflight.js new file mode 100644 index 0000000..0430f75 --- /dev/null +++ b/src/integrationTest/resources/coremods/observe-preflight.js @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LGPL-2.1-only +function initializeCoreMod() { + var Opcodes = Java.type('org.objectweb.asm.Opcodes'); + var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); + var preservation = 'cyano/lootable/upgrade/UpgradePreservation'; + var observer = 'cyano/lootable/buildtest/NativePreflightChecks'; + var fixer = 'Lcom/mojang/datafixers/DataFixer;'; + return { + lootablebodies_observe_preflight: { + target: {type: 'CLASS', name: preservation.replace(/\//g, '.')}, + transformer: function(node) { + if (String(node.name) !== preservation) throw new Error('Unexpected preflight class'); + var accessor = null, target = null; + for (var m = 0; m < node.methods.size(); m++) { + var method = node.methods.get(m); + if (String(method.name) !== 'prepareEarly' || String(method.desc) !== + '(Ljava/nio/file/Path;Ljava/nio/file/Path;)Lcyano/lootable/upgrade/UpgradeJournal;') continue; + for (var insn = method.instructions.getFirst(); insn !== null; insn = insn.getNext()) { + if (!(insn instanceof MethodInsnNode) || insn.getOpcode() !== Opcodes.INVOKESTATIC || + String(insn.owner) !== 'net/minecraft/util/datafix/DataFixers' || + String(insn.name) !== 'getDataFixer' || String(insn.desc) !== '()' + fixer) continue; + if (accessor !== null) throw new Error('Ambiguous preflight data fixer lookup'); + accessor = insn; + target = method; + } + } + if (accessor === null) throw new Error('Missing preflight data fixer lookup'); + target.instructions.insert(accessor, new MethodInsnNode( + Opcodes.INVOKESTATIC, observer, 'observe', '(' + fixer + ')' + fixer, false)); + return node; + } + } + }; +} diff --git a/src/integrationTest/resources/pack.mcmeta b/src/integrationTest/resources/pack.mcmeta index 076b13b..80a1320 100644 --- a/src/integrationTest/resources/pack.mcmeta +++ b/src/integrationTest/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { - "pack_format": 32, - "supported_formats": {"min_inclusive": 32, "max_inclusive": 41}, + "pack_format": 34, + "supported_formats": {"min_inclusive": 34, "max_inclusive": 48}, "description": "Disposable build checks" } } diff --git a/src/main/java/cyano/lootable/ItemStacks.java b/src/main/java/cyano/lootable/ItemStacks.java index 8e8756b..6af2405 100644 --- a/src/main/java/cyano/lootable/ItemStacks.java +++ b/src/main/java/cyano/lootable/ItemStacks.java @@ -4,7 +4,7 @@ public final class ItemStacks { private ItemStacks() { } public static boolean canMerge(ItemStack first, ItemStack second) { - return ItemStack.isSameItemSameComponents(first,second) && first.areCapsCompatible(second); + return ItemStack.isSameItemSameComponents(first,second); } public static boolean emptyStack(ItemStack stack) { return stack == null || stack.isEmpty(); } } diff --git a/src/main/java/cyano/lootable/LootableBodies.java b/src/main/java/cyano/lootable/LootableBodies.java index 6f1bee4..68082f5 100644 --- a/src/main/java/cyano/lootable/LootableBodies.java +++ b/src/main/java/cyano/lootable/LootableBodies.java @@ -27,8 +27,8 @@ public final class LootableBodies { public static final String MODID = "lootablebodies"; public static final String NAME = "DrCyano's Lootable Bodies"; - public static final String VERSION = "2.4.12.120061"; - public static final ResourceLocation CORPSE_ID = new ResourceLocation("lootablebodies:corpse"); + public static final String VERSION = "2.4.13.121011"; + public static final ResourceLocation CORPSE_ID = ResourceLocation.fromNamespaceAndPath(MODID,"corpse"); public static EntityType CORPSE; public static Item CORPSE_EGG; public static net.minecraft.world.inventory.MenuType CORPSE_CONTAINER; @@ -85,7 +85,7 @@ private void registerEntities(RegisterEvent event) { event.register(Registries.ENTITY_TYPE,CORPSE_ID,LootableBodies::corpseType); } private void registerItems(RegisterEvent event) { - event.register(Registries.ITEM,new ResourceLocation(MODID,"corpse_spawn_egg"),()-> { + event.register(Registries.ITEM,ResourceLocation.fromNamespaceAndPath(MODID,"corpse_spawn_egg"),()-> { CORPSE_EGG=new SpawnEggItem(corpseType(),1,1,new Item.Properties()); return CORPSE_EGG; }); @@ -105,7 +105,7 @@ private void registerContainers(RegisterEvent event) { File level=event.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); try { UpgradePreservation.register(event.getServer().getFixerUpper()); - int count=UpgradePreservation.prepareForStart(level.toPath()).size(); + int count=UpgradePreservation.prepareForStart(level.toPath(),event.getServer().getFixerUpper()).size(); ChunkVersionContext.installServer(event.getServer()); ModLog.info("Lootable Bodies preserved %s existing corpses before world loading",count); } catch(java.io.IOException unsafe) { diff --git a/src/main/java/cyano/lootable/entities/EntityLootableBody.java b/src/main/java/cyano/lootable/entities/EntityLootableBody.java index 0d0885f..bb07ea0 100644 --- a/src/main/java/cyano/lootable/entities/EntityLootableBody.java +++ b/src/main/java/cyano/lootable/entities/EntityLootableBody.java @@ -287,11 +287,8 @@ private ItemStack decayItem(ItemStack item, int amount) { && item.getCount() == 1 && item.isDamageableItem()){ int durabilityRemaining = item.getMaxDamage() - item.getDamageValue() - 1; - if(durabilityRemaining > 0){ - item.hurtAndBreak(Math.min(durabilityRemaining,amount),getRandom(),null,()->{ - item.shrink(1); - item.setDamageValue(0); - }); + if(durabilityRemaining > 0 && level() instanceof net.minecraft.server.level.ServerLevel server){ + item.hurtAndBreak(Math.min(durabilityRemaining,amount),server,null,broken -> {}); } } return item; @@ -302,7 +299,8 @@ public void addItem(ItemStack stack) { stack = mergeItem(slot,stack); if(emptyStack(stack)) return; } - auxInventory.addLast(stack); + int limit=Math.max(1,Math.min(stack.getMaxStackSize(),getMaxStackSize())); + while(!emptyStack(stack))auxInventory.addLast(stack.split(limit)); setChanged(); } @@ -519,10 +517,15 @@ public void playAmbientSound() { @Override - public int getExperienceReward() { + protected int getBaseExperienceReward() { return 0; } + @Override + protected void dropExperience(net.minecraft.world.entity.Entity attacker) { + // Enchantments can add experience even when the base reward is zero. + } + @Override protected SoundEvent getAmbientSound() { @@ -541,7 +544,7 @@ protected void dropFromLootTable(DamageSource source,boolean recentlyHit) { } @Override - protected void dropCustomDeathLoot(DamageSource source, int lootingModifier, boolean wasRecentlyHit) { + protected void dropCustomDeathLoot(net.minecraft.server.level.ServerLevel level, DamageSource source, boolean wasRecentlyHit) { // Corpse equipment belongs to the inventory release path, not mob loot. } @@ -552,7 +555,7 @@ public net.minecraft.world.entity.LivingEntity getControllingPassenger() { @Override - public boolean canBeLeashed(Player player) { + public boolean canBeLeashed() { return false; // leashing not allowed } @@ -914,7 +917,11 @@ public void readAdditionalSaveData(final CompoundTag supplied) { // attributes and absorption are restored below, just as on a fresh entity. getActiveEffects().clear(); for(AttributeInstance attribute : corpseAttributes()) restoreAttribute(attribute); - vanilla.put("Attributes",readAttributes(root.get("Attributes"))); + boolean legacyAttributes=!root.contains("attributes"); + Tag rawAttributes=root.get(legacyAttributes?"Attributes":"attributes"); + if(!legacyAttributes && root.contains("Attributes"))retainInvalid("Attributes",root.get("Attributes")); + vanilla.remove("Attributes"); + vanilla.put("attributes",readAttributes(rawAttributes,legacyAttributes)); vanilla.remove("ActiveEffects"); if(root.contains("active_effects") && root.contains("ActiveEffects")) retainInvalid("ActiveEffects",root.get("ActiveEffects")); @@ -929,7 +936,7 @@ public void readAdditionalSaveData(final CompoundTag supplied) { for(AttributeInstance attribute : corpseAttributes()) { if(!Double.isFinite(attribute.getValue())) { // Also protect against engine ordering/parent-attribute arithmetic. - if(root.contains("Attributes")) retainInvalid("Attributes",root.get("Attributes")); + if(rawAttributes!=null) retainInvalid("Attributes",rawAttributes); restoreAttribute(attribute); } } @@ -1071,21 +1078,22 @@ private float readLivingFloat(CompoundTag root, String key, float fallback, floa return fallback; } - private ListTag readAttributes(Tag raw) { + private ListTag readAttributes(Tag raw,boolean legacy) { ListTag clean = new ListTag(); if(raw == null) return clean; if(!isInventoryList(raw)) { retainInvalid("Attributes",raw); return clean; } Set seen = new HashSet<>(); + String nameKey=legacy?"Name":"id", baseKey=legacy?"Base":"base", modifiersKey=legacy?"Modifiers":"modifiers"; ListTag list = (ListTag)raw; for(int i = 0; i < list.size(); i++) { CompoundTag record = list.getCompound(i); - AttributeInstance attribute = record.contains("Name",8) - ? attributeNamed(record.getString("Name")) : null; - Double base = finiteNumber(record.get("Base")); + AttributeInstance attribute = record.contains(nameKey,8) + ? attributeNamed(record.getString(nameKey)) : null; + Double base = finiteNumber(record.get(baseKey)); if(attribute == null || base == null) { retainInvalid("Attribute",record); continue; } - 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()) + if(legacy && record.getString(nameKey).equals("forge:step_height_addition")) { + Tag modifiers=record.get(modifiersKey); + if(base < -512D || base > 512D || !Set.of(nameKey,baseKey,modifiersKey).containsAll(record.getAllKeys()) || (modifiers!=null && (!(modifiers instanceof ListTag listValue) || !listValue.isEmpty()))) { retainInvalid("Attribute",record);continue; } @@ -1107,36 +1115,39 @@ private ListTag readAttributes(Tag raw) { && !configuredHealth && !legacyNameDistance && !base.equals(defaultAttributeBases.get(name))) || !seen.add(name)) { retainInvalid("Attribute",record); continue; } - boolean changed = !Arrays.asList("Name","Base","Modifiers").containsAll(record.getAllKeys()); + boolean changed = !Set.of(nameKey,baseKey,modifiersKey).containsAll(record.getAllKeys()); CompoundTag accepted = new CompoundTag(); - accepted.putString("Name",name); accepted.putDouble("Base",base); - Tag rawModifiers = record.get("Modifiers"); + accepted.putString("id",name); accepted.putDouble("base",base); + Tag rawModifiers = record.get(modifiersKey); if(rawModifiers != null) { ListTag modifiers = new ListTag(); if(!isInventoryList(rawModifiers)) changed = true; else { - Set ids = new HashSet<>(); + Set ids = new HashSet<>(); ListTag entries = (ListTag)rawModifiers; for(int m = 0; m < entries.size(); m++) { CompoundTag entry = entries.getCompound(m); - Long operation = integral(entry.get("Operation")); - UUID id=attributeUuid(entry); - Double amount = finiteNumber(entry.get("Amount")); - if(!entry.contains("Name",8) || entry.getString("Name").isEmpty() || operation == null - || operation < 0 || operation > 2 || id == null || amount == null) { + String operation=legacy?legacyOperation(entry):entry.getString("operation"); + ResourceLocation id=legacy?legacyModifier(entry): + entry.contains("id",8)?ResourceLocation.tryParse(entry.getString("id")):null; + Double amount = finiteNumber(entry.get(legacy?"Amount":"amount")); + if(operation==null || !Set.of("add_value","add_multiplied_base","add_multiplied_total").contains(operation) + || id == null || id.getPath().isEmpty() || amount == null) { changed = true; continue; } if(ids.contains(id)) { changed = true; continue; } CompoundTag modifier = new CompoundTag(); - modifier.putString("Name",entry.getString("Name")); modifier.putDouble("Amount",amount); - modifier.putInt("Operation",operation.intValue()); modifier.putUUID("UUID",id); + modifier.putString("id",id.toString()); modifier.putDouble("amount",amount); + modifier.putString("operation",operation); ListTag candidate = modifiers.copy(); candidate.add(modifier); if(!finiteAttributeValue(base,candidate)) { changed = true; continue; } ids.add(id); modifiers.add(modifier); - if(!Arrays.asList("Name","Amount","Operation","UUIDMost","UUIDLeast","UUID").containsAll(entry.getAllKeys())) changed = true; + Set fields=legacy?Set.of("Name","Amount","Operation","UUIDMost","UUIDLeast","UUID"): + Set.of("id","amount","operation"); + if(!fields.containsAll(entry.getAllKeys())) changed = true; } } - accepted.put("Modifiers",modifiers); + accepted.put("modifiers",modifiers); } if(changed) retainInvalid("Attribute",record); clean.add(accepted); @@ -1144,6 +1155,22 @@ private ListTag readAttributes(Tag raw) { return clean; } + private static ResourceLocation legacyModifier(CompoundTag entry) { + UUID uuid=attributeUuid(entry); + String name=entry.getString("Name"); + return uuid==null || !entry.contains("Name",8) || name.isEmpty()?null:ItemMigration.legacyModifierId(uuid,name); + } + + private static String legacyOperation(CompoundTag entry) { + Long operation=integral(entry.get("Operation")); + if(operation==null || operation<0 || operation>2)return null; + return switch(operation.intValue()) { + case 0 -> "add_value"; + case 1 -> "add_multiplied_base"; + default -> "add_multiplied_total"; + }; + } + private static UUID attributeUuid(CompoundTag entry) { UUID current=null,legacy=null; if(entry.contains("UUID")) { @@ -1161,18 +1188,18 @@ private static boolean finiteAttributeValue(double base, ListTag modifiers) { double value = base; for(int i = 0; i < modifiers.size(); i++) { CompoundTag m = modifiers.getCompound(i); - if(m.getInt("Operation") == 0) value += m.getDouble("Amount"); + if(m.getString("operation").equals("add_value")) value += m.getDouble("amount"); } if(!Double.isFinite(value)) return false; double additive = value; for(int i = 0; i < modifiers.size(); i++) { CompoundTag m = modifiers.getCompound(i); - if(m.getInt("Operation") == 1) value += additive * m.getDouble("Amount"); + if(m.getString("operation").equals("add_multiplied_base")) value += additive * m.getDouble("amount"); } if(!Double.isFinite(value)) return false; for(int i = 0; i < modifiers.size(); i++) { CompoundTag m = modifiers.getCompound(i); - if(m.getInt("Operation") == 2) value *= 1D + m.getDouble("Amount"); + if(m.getString("operation").equals("add_multiplied_total")) value *= 1D + m.getDouble("amount"); } return Double.isFinite(value); } @@ -1248,18 +1275,21 @@ private ItemStack decodeInventoryItem(CompoundTag raw) { CompoundTag input=raw.copy(); input.remove("Slot"); input.putString("id",id.toString()); - input.putInt("count",count.intValue()); + // The native codec rejects counts above 99. Decode the identity + // and components first, then split the saved count into legal stacks. + input.putInt("count",1); 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; + if(emptyStack(stack) || stack.getCount()!=1 || 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 + || encoded.getInt("count")!=1 || !ItemMigration.sameSavedComponents(input.getCompound("components"),encoded.getCompound("components")))return null; + stack.setCount(count.intValue()); return stack; } catch(RuntimeException unreadable) { return null; diff --git a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java index 9837167..a4d1b6b 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java @@ -15,7 +15,7 @@ @OnlyIn(Dist.CLIENT) public class CorpseGUIContainer extends net.minecraft.client.gui.screens.inventory.AbstractContainerScreen{ - private final ResourceLocation image = new ResourceLocation(LootableBodies.MODID+":textures/gui/corpse.png"); + private final ResourceLocation image = ResourceLocation.parse(LootableBodies.MODID+":textures/gui/corpse.png"); public CorpseGUIContainer(Inventory playerItems, Container entity) { this(new CorpseContainer(playerItems,entity),playerItems,entity instanceof net.minecraft.world.entity.Entity?((net.minecraft.world.entity.Entity)entity).getDisplayName():Component.literal(LootableBodies.NAME)); diff --git a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java index 2b90a5b..0dd9853 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java @@ -26,7 +26,7 @@ /** Corpse renderer, originally created by Chris on 4/10/2016. */ @OnlyIn(Dist.CLIENT) public class CorpseRenderer extends LivingEntityRenderer> { - private static final ResourceLocation localSkin = new ResourceLocation(LootableBodies.MODID, + private static final ResourceLocation localSkin = ResourceLocation.fromNamespaceAndPath(LootableBodies.MODID, "textures/entity/corpse/corpse.png"); protected final PlayerModel thickArmsModel; protected final PlayerModel thinArmsModel; diff --git a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java index a088c9c..64f1858 100644 --- a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java +++ b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import com.mojang.serialization.Dynamic; +import cyano.lootable.ModLog; import java.io.DataInputStream; import java.io.IOException; import java.nio.file.Files; @@ -14,6 +16,8 @@ import net.minecraft.nbt.Tag; import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelStorageSource; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; /** Preserve old chunks before Forge rewrites the level header on startup. */ public final class EarlyWorldPreservation { @@ -23,6 +27,8 @@ private EarlyWorldPreservation(){ } public static synchronized void install() { if(installed)return; verifyAttachment(LevelStorageSource.LevelStorageAccess.class); + if(!(net.minecraft.util.datafix.DataFixers.getDataFixer() instanceof NativeSafeFixer)) + throw new IllegalStateException("Native data fixer preservation could not be attached. World loading has been stopped."); installed=true; } private static void verifyAttachment(Class reader) { @@ -57,6 +63,23 @@ public static void beforeRead(LevelStorageSource.LevelStorageAccess save,boolean UpgradePreservation.prepareEarly(world,header); } catch(IOException | RuntimeException unsafe){throw new UnsafeWorldUpgrade(unsafe);} } + /** The dedicated startup entry point has no shutdown hook before world construction. */ + public static Dynamic readDedicatedWorld(LevelStorageSource.LevelStorageAccess save,boolean fallback) throws IOException { + try { + return fallback ? save.getDataTagFallback() : save.getDataTag(); + } catch(UnsafeWorldUpgrade unsafe) { + metadata.remove(save); + try { + save.close(); + } catch(IOException closeFailure) { + unsafe.addSuppressed(closeFailure); + } + ModLog.log(Level.ERROR,unsafe,unsafe.getMessage()); + LogManager.shutdown(); + System.exit(1); + throw unsafe; + } + } /** Restore only this mod's original field on the same native save access. */ public static void beforeWrite(LevelStorageSource.LevelStorageAccess save,CompoundTag root) { Tag original=metadata.get(save);if(original!=null)root.put("lootablebodies",original.copy()); diff --git a/src/main/java/cyano/lootable/upgrade/ItemMigration.java b/src/main/java/cyano/lootable/upgrade/ItemMigration.java index eef3292..8c36c30 100644 --- a/src/main/java/cyano/lootable/upgrade/ItemMigration.java +++ b/src/main/java/cyano/lootable/upgrade/ItemMigration.java @@ -15,6 +15,7 @@ private ItemMigration() { } // 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; + private static final int RELEASED_COMPONENT_VERSION=3839; /** Null means the caller retains the original raw record for recovery. */ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { @@ -33,6 +34,7 @@ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { if(!versions.contains("DataVersion",3))return null; int source=versions.getInt("DataVersion"); if(source==UpgradePreservation.MINECRAFT_VERSION)return raw.copy(); + if(source==RELEASED_COMPONENT_VERSION)return convertReleasedComponents(raw); if(source<512 || source>LAST_LEGACY_ITEM_VERSION || pipeline==null)return null; CompoundTag legacy=convertNbt(raw,versions); if(legacy==null || !supportedLegacyStack(legacy))return null; @@ -47,6 +49,102 @@ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { } } + private static CompoundTag convertReleasedComponents(CompoundTag raw) { + if(pipeline==null || !Set.of("id","count","components","Slot").containsAll(raw.getAllKeys()) + || !raw.contains("id",8) || ResourceLocation.tryParse(raw.getString("id"))==null + || (raw.contains("components") && !raw.contains("components",10)))return null; + Long count=raw.contains("count")?integral(raw.get("count")):Long.valueOf(1); + if(count==null || count<=0 || count>127)return null; + try { + CompoundTag fixed=NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK, + raw.copy(),RELEASED_COMPONENT_VERSION,UpgradePreservation.MINECRAFT_VERSION); + return componentStackSurvives(raw,fixed)?fixed:null; + } catch(RuntimeException invalid) { + return null; + } + } + + /** Resolve a legacy modifier name and UUID through the native 3945 mapping. */ + public static ResourceLocation legacyModifierId(UUID uuid,String name) { + CompoundTag modifier=new CompoundTag(); + modifier.putUUID("uuid",uuid); + modifier.putString("name",name); + ListTag modifiers=new ListTag(); + modifiers.add(modifier); + CompoundTag attributes=new CompoundTag(), components=new CompoundTag(), carrier=new CompoundTag(); + attributes.put("modifiers",modifiers); + components.put("minecraft:attribute_modifiers",attributes); + carrier.putString("id","minecraft:stick"); + carrier.putInt("count",1); + carrier.put("components",components); + CompoundTag converted=convertReleasedComponents(carrier); + return converted==null?null:ResourceLocation.tryParse(converted.getCompound("components") + .getCompound("minecraft:attribute_modifiers").getList("modifiers",10).getCompound(0).getString("id")); + } + + private static boolean componentStackSurvives(CompoundTag before,CompoundTag after) { + if(before.equals(after))return true; + if(!equalExcept(before,after,"components") + || !before.contains("components",10) || !after.contains("components",10))return false; + CompoundTag old=before.getCompound("components"), fixed=after.getCompound("components"); + if(!old.getAllKeys().equals(fixed.getAllKeys()))return false; + for(String key:old.getAllKeys()) { + Tag previous=old.get(key), current=fixed.get(key); + if(previous.equals(current))continue; + boolean preserved=switch(key) { + case "minecraft:attribute_modifiers" -> previous instanceof CompoundTag a && current instanceof CompoundTag b + && modifiersSurvive(a,b); + case "minecraft:container" -> componentItemsSurvive(previous,current,true); + case "minecraft:bundle_contents","minecraft:charged_projectiles" -> componentItemsSurvive(previous,current,false); + case "minecraft:food" -> previous instanceof CompoundTag a && current instanceof CompoundTag b + && equalExcept(a,b,"using_converts_to") && a.contains("using_converts_to",10) + && b.contains("using_converts_to",10) + && componentStackSurvives(a.getCompound("using_converts_to"),b.getCompound("using_converts_to")); + default -> false; + }; + if(!preserved)return false; + } + return true; + } + + private static boolean equalExcept(CompoundTag before,CompoundTag after,String... fields) { + CompoundTag old=before.copy(), fixed=after.copy(); + for(String field:fields) { + old.remove(field); + fixed.remove(field); + } + return old.equals(fixed); + } + + private static boolean componentItemsSurvive(Tag previous,Tag current,boolean indexed) { + if(!(previous instanceof ListTag before) || !(current instanceof ListTag after) || before.size()!=after.size())return false; + for(int i=0;i identities=new HashSet<>(); + for(int i=0;i builder = new RegistryBuilder() - .setName(new ResourceLocation("minecraft:villagerprofessions")) + .setName(ResourceLocation.parse("minecraft:villagerprofessions")) .setIDRange(0, 32767) .disableSaving().disableSync(); // These are the six actual 1.10-1.13 vanilla entries, in original ID order. // Unknown mod professions remain missing and retain Forge's normal gate. event.create(builder, registry -> { for (String name : new String[]{"farmer", "librarian", "priest", "smith", "butcher", "nitwit"}) - registry.register(new ResourceLocation("minecraft", name),new Entry()); + registry.register(ResourceLocation.fromNamespaceAndPath("minecraft", name),new Entry()); }); } diff --git a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java index d14281f..f5a0776 100644 --- a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java +++ b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java @@ -21,40 +21,40 @@ public static ResourceLocation biomeReplacement(ResourceLocation old) { if(name.equals("minecraft:nether"))name="minecraft:nether_wastes"; name=net.minecraft.util.datafix.fixes.CavesAndCliffsRenames.RENAMES.getOrDefault(name,name); if(name.equals("minecraft:deep_warm_ocean"))name="minecraft:warm_ocean"; - return name.equals(old.toString())?null:new ResourceLocation(name); + return name.equals(old.toString())?null:ResourceLocation.parse(name); } public static ResourceLocation itemReplacement(ResourceLocation old) { if (!old.getNamespace().equals("minecraft")) return null; switch (old.getPath()) { - case "sign": return new ResourceLocation("minecraft:oak_sign"); - case "rose_red": return new ResourceLocation("minecraft:red_dye"); - 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 "sign": return ResourceLocation.parse("minecraft:oak_sign"); + case "rose_red": return ResourceLocation.parse("minecraft:red_dye"); + case "cactus_green": return ResourceLocation.parse("minecraft:green_dye"); + case "dandelion_yellow": return ResourceLocation.parse("minecraft:yellow_dye"); + case "grass_path": return ResourceLocation.parse("minecraft:dirt_path"); case "grass": return ResourceLocation.withDefaultNamespace("short_grass"); - case "zombie_pigman_spawn_egg":return new ResourceLocation("minecraft:zombified_piglin_spawn_egg"); + case "zombie_pigman_spawn_egg":return ResourceLocation.parse("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"); - case "pottery_shard_skull": return new ResourceLocation("minecraft:skull_pottery_sherd"); + case "pottery_shard_archer": return ResourceLocation.parse("minecraft:archer_pottery_sherd"); + case "pottery_shard_arms_up": return ResourceLocation.parse("minecraft:arms_up_pottery_sherd"); + case "pottery_shard_prize": return ResourceLocation.parse("minecraft:prize_pottery_sherd"); + case "pottery_shard_skull": return ResourceLocation.parse("minecraft:skull_pottery_sherd"); default: return null; } } public static ResourceLocation blockReplacement(ResourceLocation old) { if (!old.getNamespace().equals("minecraft")) return null; switch (old.getPath()) { - 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 "sign": return ResourceLocation.parse("minecraft:oak_sign"); + case "wall_sign": return ResourceLocation.parse("minecraft:oak_wall_sign"); + case "grass_path": return ResourceLocation.parse("minecraft:dirt_path"); case "grass": return ResourceLocation.withDefaultNamespace("short_grass"); default: return null; } } - private static final ResourceLocation OLD_XP_SOUND=new ResourceLocation("minecraft:entity.experience_orb.touch"); - private static final ResourceLocation XP_SOUND=new ResourceLocation("minecraft:entity.experience_orb.pickup"); + private static final ResourceLocation OLD_XP_SOUND=ResourceLocation.parse("minecraft:entity.experience_orb.touch"); + private static final ResourceLocation XP_SOUND=ResourceLocation.parse("minecraft:entity.experience_orb.pickup"); private static final ResourceLocation OLD_JUNGLE_AND_FOREST_MUSIC = - new ResourceLocation("minecraft:music.overworld.jungle_and_forest"); + ResourceLocation.parse("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"), @@ -77,24 +77,26 @@ private static final class UnsafeRegistryUpgrade extends Error { } public static ResourceLocation entityReplacement(ResourceLocation old) { - return old.equals(new ResourceLocation("minecraft:zombie_pigman"))?new ResourceLocation("minecraft:zombified_piglin"):null; + return old.equals(ResourceLocation.parse("minecraft:zombie_pigman"))?ResourceLocation.parse("minecraft:zombified_piglin"):null; } public static ResourceLocation soundReplacement(ResourceLocation old) { if(old.equals(OLD_XP_SOUND))return XP_SOUND; if(!old.getNamespace().equals("minecraft"))return null; - if(old.getPath().equals("item.sweet_berries.pick_from_bush"))return new ResourceLocation("minecraft:block.sweet_berry_bush.pick_berries"); - if(old.getPath().equals("music.nether"))return new ResourceLocation("minecraft:music.nether.nether_wastes"); + if(old.getPath().equals("item.sweet_berries.pick_from_bush"))return ResourceLocation.parse("minecraft:block.sweet_berry_bush.pick_berries"); + if(old.getPath().equals("music.nether"))return ResourceLocation.parse("minecraft:music.nether.nether_wastes"); + if(old.getPath().equals("block.trial_spawner.ambient_charged"))return ResourceLocation.withDefaultNamespace("block.trial_spawner.ambient_ominous"); + if(old.getPath().equals("block.trial_spawner.charge_activate"))return ResourceLocation.withDefaultNamespace("block.trial_spawner.ominous_activate"); if (old.getPath().equals("item.brush.brushing")) - return new ResourceLocation("minecraft:item.brush.brushing.generic"); + return ResourceLocation.parse("minecraft:item.brush.brushing.generic"); if (old.getPath().equals("item.brush.brush_sand_completed")) - return new ResourceLocation("minecraft:item.brush.brushing.sand.complete"); + return ResourceLocation.parse("minecraft:item.brush.brushing.sand.complete"); for(String suffix:new String[]{"ambient","angry","death","hurt"}) - if(old.getPath().equals("entity.zombie_pigman."+suffix))return new ResourceLocation("minecraft:entity.zombified_piglin."+suffix); + if(old.getPath().equals("entity.zombie_pigman."+suffix))return ResourceLocation.parse("minecraft:entity.zombified_piglin."+suffix); return null; } /** This old Long memory had no serializer or saved values in vanilla1.15. */ public static void retiredMemories(RegisterEvent event) { - event.register(Registries.MEMORY_MODULE_TYPE,new ResourceLocation("minecraft:golem_last_seen_time"), + event.register(Registries.MEMORY_MODULE_TYPE,ResourceLocation.parse("minecraft:golem_last_seen_time"), ()->new net.minecraft.world.entity.ai.memory.MemoryModuleType(java.util.Optional.empty())); } @@ -116,13 +118,34 @@ static ResourceLocation enchantmentReplacement(ResourceLocation old) { } @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))); + if(replacement==null)continue; + // The old numeric snapshot does not own the 1.21 data-pack + // registry. Native item conversion renames the actual enchantment. + if(VanillaData.LOOKUP.lookupOrThrow(Registries.ENCHANTMENT) + .get(net.minecraft.resources.ResourceKey.create(Registries.ENCHANTMENT,replacement)).isEmpty()) + throw new UnsafeRegistryUpgrade(null); + mapping.ignore(); + } + } + private static final class VanillaData { + static final net.minecraft.core.HolderLookup.Provider LOOKUP=net.minecraft.data.registries.VanillaRegistries.createLookup(); + } + + @SubscribeEvent public static void paintings(MissingMappingsEvent event) { + for(var mapping:event.getAllMappings(Registries.PAINTING_VARIANT)) { + if(vanillaPaintingVariant(mapping.getKey()))mapping.ignore(); } } + static boolean vanillaPaintingVariant(ResourceLocation key) { + // A named vanilla painting now lives in the data-pack registry. + // Retire only a numeric binding whose exact name the native data owns. + return key.getNamespace().equals("minecraft") && VanillaData.LOOKUP.lookupOrThrow(Registries.PAINTING_VARIANT) + .get(net.minecraft.resources.ResourceKey.create(Registries.PAINTING_VARIANT,key)).isPresent(); + } + static boolean retiredPotion(ResourceLocation key) { return key.equals(ResourceLocation.withDefaultNamespace("empty")); } @@ -144,21 +167,18 @@ static boolean retiredPotion(ResourceLocation key) { // Native19 chunk palettes use names in the data-pack registry, not // the old Forge numeric biome snapshot. Resolve only keys proved // by the actual vanilla bootstrap; vanilla still fixes chunk data. - if(VanillaBiomes.LOOKUP.get(net.minecraft.resources.ResourceKey.create(Registries.BIOME,current)).isPresent())mapping.ignore(); + if(VanillaData.LOOKUP.lookupOrThrow(Registries.BIOME) + .get(net.minecraft.resources.ResourceKey.create(Registries.BIOME,current)).isPresent())mapping.ignore(); else if(replacement!=null)throw new UnsafeRegistryUpgrade(null); } } - private static final class VanillaBiomes { - static final net.minecraft.core.HolderLookup.RegistryLookup LOOKUP= - net.minecraft.data.registries.VanillaRegistries.createLookup().lookupOrThrow(Registries.BIOME); - } @SubscribeEvent public static void retiredPointsOfInterest(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping mapping: event.getAllMappings(Registries.POINT_OF_INTEREST_TYPE)) { // Native PoiTypeRemoveFix3107 removes exactly these unpopulated // vanilla records. Resolve their retired numeric bindings only. - if(mapping.getKey().equals(new ResourceLocation("minecraft:unemployed")) - || mapping.getKey().equals(new ResourceLocation("minecraft:nitwit")))mapping.ignore(); + if(mapping.getKey().equals(ResourceLocation.parse("minecraft:unemployed")) + || mapping.getKey().equals(ResourceLocation.parse("minecraft:nitwit")))mapping.ignore(); } } @SubscribeEvent public static void entities(MissingMappingsEvent event) { @@ -169,8 +189,8 @@ private static final class VanillaBiomes { } @SubscribeEvent public static void memories(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping> mapping:event.>getAllMappings(Registries.MEMORY_MODULE_TYPE)) - if(mapping.getKey().equals(new ResourceLocation("minecraft:opened_doors"))) - mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.containsKey(new ResourceLocation("minecraft:doors_to_close")),()->net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.getValue(new ResourceLocation("minecraft:doors_to_close")))); + if(mapping.getKey().equals(ResourceLocation.parse("minecraft:opened_doors"))) + mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.containsKey(ResourceLocation.parse("minecraft:doors_to_close")),()->net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.getValue(ResourceLocation.parse("minecraft:doors_to_close")))); } @SubscribeEvent public static void sounds(MissingMappingsEvent event) { @@ -184,8 +204,8 @@ private static final class VanillaBiomes { 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. - if (!BuiltInRegistries.SOUND_EVENT.containsKey(new ResourceLocation("minecraft:music.overworld.jungle")) - || !BuiltInRegistries.SOUND_EVENT.containsKey(new ResourceLocation("minecraft:music.overworld.forest"))) + if (!BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.jungle")) + || !BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.forest"))) throw new UnsafeRegistryUpgrade(null); mapping.ignore(); continue; diff --git a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java index b15dae7..fa66cb3 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java +++ b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java @@ -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:marker==10?3465: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:marker==11?3839: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"); diff --git a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java index e8b79ea..cecf39b 100644 --- a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java +++ b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java @@ -349,7 +349,8 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co else if(marker==8)version=2975; else if(marker==9)version=3337; else if(marker==10)version=3465; - else if(marker==11)version=UpgradePreservation.MINECRAFT_VERSION; + else if(marker==11)version=3839; + else if(marker==12)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); @@ -358,7 +359,8 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co for(int i=0;i32)throw new IOException("Recursive native converter during entity preflight"); @@ -52,7 +50,15 @@ private static void validateEntityChunks(UpgradeJournal complete) throws IOExcep } public static synchronized void close(){journal=null;earlyWorld=null;} static UpgradeJournal journal(){return journal;} - public static DataFixer wrapper(DataFixer delegate){return delegate instanceof NativeSafeFixer?delegate:new NativeSafeFixer(delegate,null);} + private static DataFixer nativeWrapper; + + /** Called by the native accessor before save readers retain their fixer. */ + public static synchronized DataFixer wrapper(DataFixer delegate) { + if(delegate instanceof NativeSafeFixer)return delegate; + if(!(nativeWrapper instanceof NativeSafeFixer safe) || safe.delegate!=delegate) + nativeWrapper=new NativeSafeFixer(delegate,null); + return nativeWrapper; + } public static CompoundTag chunkContext(CompoundTag chunk){return chunk;} public static void clearChunkContext(CompoundTag chunk){chunk.remove("LootableBodiesChunkContext");} public static CompoundTag restore(CompoundTag input) { @@ -66,7 +72,7 @@ static int chunkSourceVersion(CompoundTag versions) { for(int m=0;m 1) throw new Error('Ambiguous native data fixer return'); + if (wraps === 0) + accessor.instructions.insertBefore(returns[0], new MethodInsnNode( + Opcodes.INVOKESTATIC, wrapper, 'wrapper', wrapperDesc, false)); + return node; + } + }, lootablebodies_preservation: { target: {type: 'CLASS', name: save.replace(/\//g, '.')}, transformer: function(node) { diff --git a/src/main/resources/data/minecraft/tags/entity_types/can_breathe_under_water.json b/src/main/resources/data/minecraft/tags/entity_type/can_breathe_under_water.json similarity index 100% rename from src/main/resources/data/minecraft/tags/entity_types/can_breathe_under_water.json rename to src/main/resources/data/minecraft/tags/entity_type/can_breathe_under_water.json diff --git a/src/main/resources/data/minecraft/tags/entity_types/ignores_poison_and_regen.json b/src/main/resources/data/minecraft/tags/entity_type/ignores_poison_and_regen.json similarity index 100% rename from src/main/resources/data/minecraft/tags/entity_types/ignores_poison_and_regen.json rename to src/main/resources/data/minecraft/tags/entity_type/ignores_poison_and_regen.json diff --git a/src/main/resources/data/minecraft/tags/entity_types/inverted_healing_and_harm.json b/src/main/resources/data/minecraft/tags/entity_type/inverted_healing_and_harm.json similarity index 100% rename from src/main/resources/data/minecraft/tags/entity_types/inverted_healing_and_harm.json rename to src/main/resources/data/minecraft/tags/entity_type/inverted_healing_and_harm.json diff --git a/src/main/resources/data/minecraft/tags/entity_types/sensitive_to_smite.json b/src/main/resources/data/minecraft/tags/entity_type/sensitive_to_smite.json similarity index 100% rename from src/main/resources/data/minecraft/tags/entity_types/sensitive_to_smite.json rename to src/main/resources/data/minecraft/tags/entity_type/sensitive_to_smite.json diff --git a/src/main/resources/data/minecraft/tags/entity_types/undead.json b/src/main/resources/data/minecraft/tags/entity_type/undead.json similarity index 100% rename from src/main/resources/data/minecraft/tags/entity_types/undead.json rename to src/main/resources/data/minecraft/tags/entity_type/undead.json diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index d8341cc..943efa0 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,7 @@ { "pack": { - "pack_format": 32, - "supported_formats": {"min_inclusive": 32, "max_inclusive": 41}, + "pack_format": 34, + "supported_formats": {"min_inclusive": 34, "max_inclusive": 48}, "description": "Lootable Bodies resources" } } diff --git a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java index d820439..0956e92 100644 --- a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java @@ -66,7 +66,7 @@ private static RegistryAccess build() { NativeRespawnDescription.describe(packet(Level.NETHER,SEED))); } @Test void observationKeepsUnknownDimensionKeysAndEveryDecodedField() throws Exception { - ResourceKey key=ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,new net.minecraft.resources.ResourceLocation("fixture:unhandled")); + ResourceKey key=ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,net.minecraft.resources.ResourceLocation.parse("fixture:unhandled")); ClientboundRespawnPacket packet=packet(key,SEED); java.util.Map before=new java.util.HashMap<>(); for(Field field:ClientboundRespawnPacket.class.getDeclaredFields()){field.setAccessible(true);before.put(field,field.get(packet));} diff --git a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java index 2e66b38..cee99fc 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java @@ -28,19 +28,22 @@ private byte[] bytes(Packet packet) throws Exception { }finally{buffer.release();} } private RegistryFriendlyByteBuf wire() { - return new RegistryFriendlyByteBuf(Unpooled.buffer(),RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY)); + return new RegistryFriendlyByteBuf(Unpooled.buffer(),registries()); + } + private RegistryAccess registries() { + return 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); ClientboundContainerSetContentPacket packet=new ClientboundContainerSetContentPacket(7,321,slots,cursor);byte[] before=bytes(packet); - NativeMenuObservation observed=NativeMenuObservation.decode(packet);assertNotNull(observed);assertTrue(observed.content); + NativeMenuObservation observed=NativeMenuObservation.decode(packet,registries());assertNotNull(observed);assertTrue(observed.content); assertEquals(7,observed.containerId);assertEquals(321,observed.stateId);assertEquals(84,observed.items.size()); assertTrue(ItemStack.matches(slots.get(47),observed.items.get(47)));assertTrue(ItemStack.matches(cursor,observed.carried));assertArrayEquals(before,bytes(packet)); } @Test void slotAndCursorCorrectionsUseTheirActualNativeWindowAndState() throws Exception { for(int window:new int[]{7,-1,-2}) { ClientboundContainerSetSlotPacket packet=new ClientboundContainerSetSlotPacket(window,123,window==-1?-1:47,stack(19));byte[] before=bytes(packet); - NativeMenuObservation observed=NativeMenuObservation.decode(packet);assertNotNull(observed);assertFalse(observed.content); + NativeMenuObservation observed=NativeMenuObservation.decode(packet,registries());assertNotNull(observed);assertFalse(observed.content); assertEquals(window,observed.containerId);assertEquals(123,observed.stateId);assertEquals(window==-1?-1:47,observed.slot); assertTrue(ItemStack.matches(stack(19),observed.item));assertArrayEquals(before,bytes(packet)); } @@ -63,6 +66,6 @@ private RegistryFriendlyByteBuf wire() { 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)); + assertNull(NativeMenuObservation.decode(other,registries())); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java b/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java index d10d45e..90d1ddf 100644 --- a/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java @@ -34,7 +34,7 @@ private RegistryAccess registries() { return RegistryAccess.fromRegistryOfRegistries(net.minecraft.core.registries.BuiltInRegistries.REGISTRY); } private ProtocolInfo protocol() { - return GameProtocols.CLIENTBOUND.bind(RegistryFriendlyByteBuf.decorator(registries())); + return GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(registries())); } private List> wire(ClientboundBundlePacket bundle) { List> wire=new ArrayList<>(); diff --git a/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java b/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java index 7dcf64d..60276de 100644 --- a/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java +++ b/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java @@ -19,7 +19,7 @@ public static void main(String[] args)throws Exception { .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")); + Files.copy(source,root.resolve("native121-flat-level.dat")); Path world=root.resolve("controlled-entity-source");Files.createDirectory(world); 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")); diff --git a/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java b/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java index 03083e7..938a4e4 100644 --- a/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java +++ b/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java @@ -10,14 +10,14 @@ class NativeVanillaDataFixerTest { assertSame(vanilla,NativeVanillaDataFixer.unwrap(UpgradePreservation.wrapper(vanilla))); assertSame(vanilla,NativeVanillaDataFixer.unwrap(vanilla)); } - @Test void capturedVanillaOracleStaysIndependentWhenASyntheticCounterWrapsTheGlobalReader(){ + @Test void capturedVanillaOracleStaysIndependentWhenASyntheticCounterIsRegisteredForConversion(){ DataFixer nativeOracle=NativeVanillaDataFixer.get();DataFixer previous=net.minecraft.util.datafix.DataFixers.getDataFixer(); try { - net.minecraft.util.datafix.DataFixers.dataFixer=new DataFixer(){ + UpgradePreservation.register(new DataFixer(){ public com.mojang.datafixers.schemas.Schema getSchema(int key){throw new AssertionError("Counter is not an oracle");} public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic input,int source,int target){throw new AssertionError("Counter is not an oracle");} - }; + }); assertSame(nativeOracle,NativeVanillaDataFixer.get()); - }finally{net.minecraft.util.datafix.DataFixers.dataFixer=previous;} + }finally{UpgradePreservation.register(previous);} } } diff --git a/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java b/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java index bbbe77d..c23d7b3 100644 --- a/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java +++ b/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java @@ -65,6 +65,25 @@ private CompoundTag entity(boolean reverse) { assertFalse(TypedNbt.same(before,other));assertTrue(TypedNbt.sameEntity(before,other)); assertTrue(TypedNbt.same(before,copy));assertTrue(TypedNbt.same(other,entity(true))); } + @Test void native121AttributeOrderMayChangeButTypedValuesMayNot() { + CompoundTag left=entity(false),right=entity(true); + for(CompoundTag body : new CompoundTag[]{left,right}) { + ListTag attributes=body.getList("Attributes",10); + for(int i=0;i 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 0fd94b8..70379e6 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(11,saved.getInt("LootableBodiesDataVersion"));assertEquals(ID,saved.getUUID("UUID")); + assertEquals("lootablebodies:corpse",saved.getString("id"));assertEquals(12,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 cc0796b..a14061f 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.12.120061", version); + assertEquals("2.4.13.121011", version); assertEquals("forge", values.getProperty("loader_name")); assertEquals("1", values.getProperty("loader_code")); assertTrue(text("src/main/java/cyano/lootable/LootableBodies.java") @@ -43,8 +43,8 @@ private String text(String path) throws Exception { assertEquals("240968", values.getProperty("curseforge_project_id")); 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.6]\"")); + assertTrue(source.contains("ResourceLocation.fromNamespaceAndPath(MODID,\"corpse\")")); + assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.21.1]\"")); } @Test void maintainedReleaseHasLgplAndRetainsTheOriginalNotice() throws Exception { diff --git a/src/test/java/cyano/lootable/entities/Native119DamageTest.java b/src/test/java/cyano/lootable/entities/Native119DamageTest.java index da56469..c8e9302 100644 --- a/src/test/java/cyano/lootable/entities/Native119DamageTest.java +++ b/src/test/java/cyano/lootable/entities/Native119DamageTest.java @@ -37,7 +37,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")), + custom=registry.register(ResourceKey.create(Registries.DAMAGE_TYPE,ResourceLocation.parse("example:custom")), new DamageType("anvil",0),net.minecraft.core.RegistrationInfo.BUILT_IN); registry.freeze(); sources=new DamageSources(new RegistryAccess.ImmutableRegistryAccess(java.util.List.of(registry))); diff --git a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java index 8919b80..fa61d4a 100644 --- a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java +++ b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java @@ -40,7 +40,7 @@ private void write(Path world,CompoundTag root)throws Exception { try(LevelStorageSource.LevelStorageAccess save=format.createAccess("fixture")) { EarlyWorldPreservation.beforeRead(save,false); UpgradeJournal early=UpgradePreservation.journal();assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); - write(world,level(2586));assertSame(early,UpgradePreservation.prepareForStart(world)); + write(world,level(2586));assertSame(early,UpgradePreservation.prepareForStart(world,net.minecraft.util.datafix.DataFixers.getDataFixer())); assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); CompoundTag output=new CompoundTag();EarlyWorldPreservation.beforeWrite(save,output); assertEquals(root.get("lootablebodies"),output.get("lootablebodies")); diff --git a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java index 23c41d8..96821bf 100644 --- a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java +++ b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java @@ -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(3840))); + CompoundTag raw=item("minecraft:stone",1);raw.putString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(3956))); } @Test void unrecognisedMetadataAndEnchantmentsRemainRecoverable() { assertNull(ItemMigration.convertNbt(item("minecraft:stone",99),context(1343))); diff --git a/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java index 4d3290e..ce5039f 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java @@ -11,8 +11,8 @@ class Native114RegistryRenameTest { @Test void itemRegistryBridgesAgreeWithVanillasActualItemConversion() { for (String[] names : new String[][]{{"sign","oak_sign"},{"rose_red","red_dye"}, {"cactus_green","green_dye"},{"dandelion_yellow","yellow_dye"}}) { - ResourceLocation old = new ResourceLocation("minecraft", names[0]); - ResourceLocation current = new ResourceLocation("minecraft", names[1]); + ResourceLocation old = ResourceLocation.fromNamespaceAndPath("minecraft", names[0]); + ResourceLocation current = ResourceLocation.fromNamespaceAndPath("minecraft", names[1]); assertEquals(current, LegacyRegistryMappings.itemReplacement(old)); CompoundTag input = new CompoundTag(); input.putString("id", old.toString()); input.putByte("Count", (byte)11); CompoundTag converted = NativeNbt.update(DataFixers.getDataFixer(), References.ITEM_STACK, input, 1631, 1976); @@ -20,11 +20,11 @@ class Native114RegistryRenameTest { } } @Test void onlyTheTwoRetiredVanillaSignBlocksAreBridged() { - assertEquals(new ResourceLocation("minecraft:oak_sign"), LegacyRegistryMappings.blockReplacement(new ResourceLocation("minecraft:sign"))); - assertEquals(new ResourceLocation("minecraft:oak_wall_sign"), LegacyRegistryMappings.blockReplacement(new ResourceLocation("minecraft:wall_sign"))); + assertEquals(ResourceLocation.parse("minecraft:oak_sign"), LegacyRegistryMappings.blockReplacement(ResourceLocation.parse("minecraft:sign"))); + assertEquals(ResourceLocation.parse("minecraft:oak_wall_sign"), LegacyRegistryMappings.blockReplacement(ResourceLocation.parse("minecraft:wall_sign"))); for (String name : new String[]{"othermod:sign", "minecraft:unknown", "minecraft:oak_sign"}) { - assertNull(LegacyRegistryMappings.itemReplacement(new ResourceLocation(name))); - assertNull(LegacyRegistryMappings.blockReplacement(new ResourceLocation(name))); + assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse(name))); + assertNull(LegacyRegistryMappings.blockReplacement(ResourceLocation.parse(name))); } } } diff --git a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java index bb6d707..de033b7 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(3839, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(11,UpgradePreservation.DATA_VERSION); + assertEquals(3955, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(12,UpgradePreservation.DATA_VERSION); } @Test void released113EquipmentAndOverflowAreRetainedBeforeNativeConversion() { CompoundTag original = body(3), before = original.copy(); CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(11,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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(11,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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 3b74270..7a872f8 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(3839, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(11,UpgradePreservation.DATA_VERSION); + assertEquals(3955, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(12,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(11,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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 a94062e..8531a6f 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java @@ -51,7 +51,7 @@ 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,3839); + CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeInput,2230,3955); 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(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"chunk"),world.resolve("reader"),UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),false)) { diff --git a/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java index 88ca03e..79ac1d6 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java @@ -7,22 +7,22 @@ import static org.junit.jupiter.api.Assertions.*; class Native116RegistryRenameTest { @Test void zombiePigmanMappingsAgreeWithNativeEntityAndItemFixes(){ - ResourceLocation old=new ResourceLocation("minecraft:zombie_pigman"),current=new ResourceLocation("minecraft:zombified_piglin"); + ResourceLocation old=ResourceLocation.parse("minecraft:zombie_pigman"),current=ResourceLocation.parse("minecraft:zombified_piglin"); assertEquals(current,LegacyRegistryMappings.entityReplacement(old)); - assertEquals(new ResourceLocation("minecraft:zombified_piglin_spawn_egg"),LegacyRegistryMappings.itemReplacement(new ResourceLocation("minecraft:zombie_pigman_spawn_egg"))); + assertEquals(ResourceLocation.parse("minecraft:zombified_piglin_spawn_egg"),LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("minecraft:zombie_pigman_spawn_egg"))); CompoundTag entity=new CompoundTag();entity.putString("id",old.toString()); assertEquals(current.toString(),NativeNbt.update(DataFixers.getDataFixer(),References.ENTITY_TREE,entity,2230,2586).getString("id")); CompoundTag item=new CompoundTag();item.putString("id","minecraft:zombie_pigman_spawn_egg");item.putByte("Count",(byte)7); assertEquals("minecraft:zombified_piglin_spawn_egg",NativeNbt.update(DataFixers.getDataFixer(),References.ITEM_STACK,item,2230,2586).getString("id")); } @Test void onlyExactRetiredVanillaSoundsAndBiomesAreRenamed(){ - for(String sound:new String[]{"ambient","angry","death","hurt"})assertEquals(new ResourceLocation("minecraft:entity.zombified_piglin."+sound),LegacyRegistryMappings.soundReplacement(new ResourceLocation("minecraft:entity.zombie_pigman."+sound))); - assertEquals(new ResourceLocation("minecraft:music.nether.nether_wastes"),LegacyRegistryMappings.soundReplacement(new ResourceLocation("minecraft:music.nether"))); - for(String key:new String[]{"othermod:zombie_pigman","minecraft:unrecognised","minecraft:zombified_piglin"})assertNull(LegacyRegistryMappings.entityReplacement(new ResourceLocation(key))); - assertNull(LegacyRegistryMappings.soundReplacement(new ResourceLocation("othermod:music.nether"))); + for(String sound:new String[]{"ambient","angry","death","hurt"})assertEquals(ResourceLocation.parse("minecraft:entity.zombified_piglin."+sound),LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:entity.zombie_pigman."+sound))); + assertEquals(ResourceLocation.parse("minecraft:music.nether.nether_wastes"),LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:music.nether"))); + for(String key:new String[]{"othermod:zombie_pigman","minecraft:unrecognised","minecraft:zombified_piglin"})assertNull(LegacyRegistryMappings.entityReplacement(ResourceLocation.parse(key))); + assertNull(LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("othermod:music.nether"))); } @Test void nativeBiomesUseTheirOwnDynamicRegistry() { - assertEquals(new ResourceLocation("minecraft:worldgen/biome"),net.minecraftforge.registries.ForgeRegistries.Keys.BIOMES.location()); + assertEquals(ResourceLocation.parse("minecraft:worldgen/biome"),net.minecraftforge.registries.ForgeRegistries.Keys.BIOMES.location()); } @Test void failedMappingsEscapeTheNativeLevelReaderFallback() { Error absent=assertThrows(Error.class,()->LegacyRegistryMappings.requireMappingTarget(()->null)); diff --git a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java index 19ba3bc..f88b7b1 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java @@ -11,6 +11,7 @@ import net.minecraft.nbt.Tag; import net.minecraft.nbt.TagParser; import net.minecraft.util.datafix.DataFixers; +import com.mojang.datafixers.DataFixer; import net.minecraft.util.datafix.fixes.References; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -35,7 +36,7 @@ private CompoundTag legacy() { body.put("Equipment",inventory);return body; } @Test void currentFormatUsesThe119SchemaAndMarker() { - assertEquals(3839,UpgradePreservation.MINECRAFT_VERSION);assertEquals(11,UpgradePreservation.DATA_VERSION); + assertEquals(3955,UpgradePreservation.MINECRAFT_VERSION);assertEquals(12,UpgradePreservation.DATA_VERSION); } @Test void legacyCorpseUuidSurvivesShieldingWithoutTruncatingCustomInventory() { CompoundTag body=legacy(),before=body.copy(),context=new CompoundTag();context.putInt("DataVersion",2230); @@ -78,10 +79,11 @@ private CompoundTag legacy() { References.LEVEL,root,-1,2586); assertEquals(before.get("WorldGenSettings"),fixed.get("WorldGenSettings"));assertEquals(before,root); } - @Test void registrationProtectsTheNativeLevelReaderBeforeServerCreation() throws Exception { - UpgradePreservation.register(DataFixers.getDataFixer()); + @Test void nativeAccessorWrapperProtectsLevelSettingsBeforeServerCreation() throws Exception { + DataFixer nativeFixer=DataFixers.getDataFixer(); + UpgradePreservation.register(nativeFixer); CompoundTag raw=settings(),before=raw.copy(); - CompoundTag fixed=NativeNbt.update(DataFixers.getDataFixer(), + CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(nativeFixer), References.WORLD_GEN_SETTINGS,raw,-1,2586); assertEquals(before,fixed,"The world reader obtains its fixer before server-about-to-start"); assertEquals(before,raw); diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java index ee0f106..47104de 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,3839); + return NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,root,source,3955); } @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(11,fixed.getInt(UpgradePreservation.VERSION));assertEquals(2586,fixed.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(12,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,3839}) { + for(int version:new int[]{-1,0,512,2230,2586,2730,2975,3337,3465,3839,3955}) { CompoundTag raw=chunk(version,corpse),before=raw.copy(); CompoundTag fixed=convert(raw,version); assertEquals(corpse,fixed.getList("Entities",10).getCompound(0));assertEquals(before,raw); @@ -64,44 +64,36 @@ public Dynamic update(DSL.TypeReference type,Dynamic input,int from,in }; } @Test void startupRefusesBeforeWorldConstructionWithoutChangingGlobalOrOriginals() throws Exception { - DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); + DataFixer nativeFixer=NativeVanillaDataFixer.get(); UpgradeJournal previous=UpgradePreservation.prepare(world.resolve("previous")); Path candidate=world.resolve("candidate"),region=write(candidate,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); AtomicInteger calls=new AtomicInteger(); - try { - DataFixers.dataFixer=observe(nativeFixer,calls,previous,true); - 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;} + DataFixer fixer=observe(nativeFixer,calls,previous,true); + assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(candidate,candidate.resolve("level.dat"),fixer)); + assertTrue(calls.get()>0);assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); } @Test void successfulPreflightConvertsOneImmutableCopyPerChunkNotPerBody() throws Exception { - DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); + DataFixer nativeFixer=NativeVanillaDataFixer.get(); Path region=write(world,chunk(2586,body(ID,6),body(new UUID(14,15),6)));byte[] bytes=Files.readAllBytes(region); AtomicInteger calls=new AtomicInteger();UpgradePreservation.close(); - try { - DataFixers.dataFixer=observe(nativeFixer,calls,null,false); - 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;} + DataFixer fixer=observe(nativeFixer,calls,null,false); + UpgradeJournal journal=UpgradePreservation.prepareEarly(world,world.resolve("level.dat"),fixer);assertEquals(2,journal.size());assertEquals(1,calls.get()); + assertSame(journal,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); } @Test void unchangedIndexCannotReuseAnOldPreflightAfterTheFixerChanges() throws Exception { - DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); + DataFixer nativeFixer=NativeVanillaDataFixer.get(); Path region=write(world,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); 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,world.resolve("level.dat")));assertTrue(calls.get()>0); - assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); - } finally {DataFixers.dataFixer=original;} + DataFixer fixer=observe(nativeFixer,calls,previous,true); + assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(world,world.resolve("level.dat"),fixer));assertTrue(calls.get()>0); + assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); } @Test void actualServerFixerIsValidatedAfterTheEarlyLevelHook() throws Exception { - DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); + DataFixer nativeFixer=NativeVanillaDataFixer.get(); Path region=write(world,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); 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)); - assertTrue(calls.get()>0);assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); - } finally {DataFixers.dataFixer=original;} + DataFixer fixer=observe(nativeFixer,calls,previous,true); + assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareForStart(world,fixer)); + assertTrue(calls.get()>0);assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java index 48a4f7e..352512d 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java @@ -6,23 +6,23 @@ import static org.junit.jupiter.api.Assertions.*; class Native117RegistryRenameTest { - final ResourceLocation oldPath=new ResourceLocation("minecraft:grass_path"); - final ResourceLocation dirtPath=new ResourceLocation("minecraft:dirt_path"); + final ResourceLocation oldPath=ResourceLocation.parse("minecraft:grass_path"); + final ResourceLocation dirtPath=ResourceLocation.parse("minecraft:dirt_path"); @Test void grassPathBlockUsesTheActualNativeDirtPath() { assertEquals(dirtPath,LegacyRegistryMappings.blockReplacement(oldPath)); assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH,net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(dirtPath)); - assertNull(LegacyRegistryMappings.blockReplacement(new ResourceLocation("anothermod:grass_path"))); + assertNull(LegacyRegistryMappings.blockReplacement(ResourceLocation.parse("anothermod:grass_path"))); } @Test void grassPathItemUsesTheActualNativeDirtPathItem() { assertEquals(dirtPath,LegacyRegistryMappings.itemReplacement(oldPath)); assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH.asItem(),net.minecraft.core.registries.BuiltInRegistries.ITEM.get(dirtPath)); - assertNull(LegacyRegistryMappings.itemReplacement(new ResourceLocation("anothermod:grass_path"))); + assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("anothermod:grass_path"))); } @Test void berryPickingSoundUsesTheActualNativeBlockSound() { - ResourceLocation old=new ResourceLocation("minecraft:item.sweet_berries.pick_from_bush"); - ResourceLocation current=new ResourceLocation("minecraft:block.sweet_berry_bush.pick_berries"); + ResourceLocation old=ResourceLocation.parse("minecraft:item.sweet_berries.pick_from_bush"); + ResourceLocation current=ResourceLocation.parse("minecraft:block.sweet_berry_bush.pick_berries"); assertEquals(current,LegacyRegistryMappings.soundReplacement(old)); assertSame(net.minecraft.sounds.SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES,net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.get(current)); - assertNull(LegacyRegistryMappings.soundReplacement(new ResourceLocation("anothermod:item.sweet_berries.pick_from_bush"))); + assertNull(LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("anothermod:item.sweet_berries.pick_from_bush"))); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java index 6a57649..0fdf49e 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java @@ -20,28 +20,28 @@ private String nativeName(String name,int source){ @Test void everyCavesAndCliffsAliasMatchesTheActualNativeFixAndRegisteredTarget(){ CavesAndCliffsRenames.RENAMES.forEach((old,current)->{ assertEquals(current,nativeName(old,2730),old); - ResourceLocation target=new ResourceLocation(current); + ResourceLocation target=ResourceLocation.parse(current); assertTrue(cyano.lootable.buildtest.Native119Registries.lookup().lookupOrThrow(Registries.BIOME) .get(net.minecraft.resources.ResourceKey.create(Registries.BIOME,target)).isPresent(),current); - assertEquals(target,LegacyRegistryMappings.biomeReplacement(new ResourceLocation(old)),old); + assertEquals(target,LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(old)),old); }); } @Test void earlierNativeBiomeRenamesChainIntoTheCurrentNativeBiomeNames(){ BiomeFix.BIOMES.forEach((old,renamed)->{ String current=CavesAndCliffsRenames.RENAMES.getOrDefault(renamed,renamed); if(current.equals("minecraft:deep_warm_ocean"))current="minecraft:warm_ocean"; - ResourceLocation target=new ResourceLocation(current); + ResourceLocation target=ResourceLocation.parse(current); assertTrue(cyano.lootable.buildtest.Native119Registries.lookup().lookupOrThrow(Registries.BIOME) .get(net.minecraft.resources.ResourceKey.create(Registries.BIOME,target)).isPresent(),current); - assertEquals(target,LegacyRegistryMappings.biomeReplacement(new ResourceLocation(old)),old); + assertEquals(target,LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(old)),old); }); } @Test void replacedNetherAndDeepWarmOceanFollowTheirSeparateActualNativeRules(){ for(String old:new String[]{"minecraft:nether","minecraft:deep_warm_ocean"}) - assertEquals(new ResourceLocation(nativeName(old,2230)),LegacyRegistryMappings.biomeReplacement(new ResourceLocation(old))); + assertEquals(ResourceLocation.parse(nativeName(old,2230)),LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(old))); } @Test void healthyUnknownAndOtherModNamesDoNotReceiveGuessedReplacements(){ for(String name:new String[]{"minecraft:plains","minecraft:forest","minecraft:unknown_saved_biome","anothermod:wooded_hills"}) - assertNull(LegacyRegistryMappings.biomeReplacement(new ResourceLocation(name)),name); + assertNull(LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(name)),name); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java index 1918426..3fa52a0 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java @@ -22,9 +22,9 @@ class Native118EntityBoundaryTest { Path file=write(world,raw);byte[] original=Files.readAllBytes(file); 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,3839); + CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3955); CompoundTag actual=converted.getList("Entities",10).getCompound(0); - assertEquals(11,actual.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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")); @@ -40,7 +40,7 @@ class Native118EntityBoundaryTest { assertNull(UpgradePreservation.journal());assertArrayEquals(original,Files.readAllBytes(file)); } @Test void futureSeparateEntityVersionRefusesEvenWhenItsCorpseHasAnOlderKnownMarker() throws Exception { - CompoundTag raw=chunk(3840,body(new UUID(118,172),7)),before=raw.copy(); + CompoundTag raw=chunk(3956,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,world.resolve("level.dat"))); assertNull(UpgradePreservation.journal());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); @@ -51,8 +51,8 @@ class Native118EntityBoundaryTest { UpgradePreservation.close();Path folder=world.resolve("typed-"+i); CompoundTag raw=chunk(2730,body(new UUID(118,173+i),7));raw.put("DataVersion",stamps[i]);CompoundTag before=raw.copy(); Path file=write(folder,raw);byte[] original=Files.readAllBytes(file);UpgradePreservation.prepareEarly(folder,folder.resolve("level.dat")); - CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3839).getList("Entities",10).getCompound(0); - assertEquals(11,converted.getInt(UpgradePreservation.VERSION));assertEquals(2730,converted.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3955).getList("Entities",10).getCompound(0); + assertEquals(12,converted.getInt(UpgradePreservation.VERSION));assertEquals(2730,converted.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); assertEquals(50,converted.getList("LootableBodiesUnresolved",10).size());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java b/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java index e223028..79bdcc6 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java @@ -17,10 +17,10 @@ private ListTag pos(double x,double y,double z) { } private CompoundTag fixed(CompoundTag raw,boolean separate) { return NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), - separate?References.ENTITY_CHUNK:References.CHUNK,raw,3839,3839); + separate?References.ENTITY_CHUNK:References.CHUNK,raw,3955,3955); } private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { - CompoundTag root=chunk(3839,body); + CompoundTag root=chunk(3955,body); if(separate)root.putIntArray("Position",new int[]{x,z}); else { root.putInt("xPos",x);root.putInt("zPos",z);root.put("sections",new ListTag());root.put("entities",root.get("Entities")); diff --git a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java index c0815ed..91b6ce8 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java @@ -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(11,staged.getInt(UpgradePreservation.VERSION)); + CompoundTag staged=fixed.getList("entities",10).getCompound(0);assertEquals(12,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)); } @@ -129,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,3839); + CompoundTag nativeResult=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeCopy,2730,3955); assertFalse(nativeResult.contains("Level"));assertEquals(Integer.MAX_VALUE,nativeResult.getInt("DataVersion")); - CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,3839); - assertFalse(fixed.contains("Level"));assertEquals(3839,fixed.getInt("DataVersion")); + CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,3955); + assertFalse(fixed.contains("Level"));assertEquals(3955,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",3840);CompoundTag before=future.copy(); + CompoundTag future=root();future.putInt("DataVersion",3956);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,3840,3839)); + assertThrows(Error.class,()->NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,future,3956,3955)); 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 1685aa2..395cf28 100644 --- a/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java @@ -40,7 +40,7 @@ static ScriptEngine actualEngine()throws Exception { Method create=factory.getDeclaredMethod("createEngine");create.setAccessible(true); return (ScriptEngine)create.invoke(null); } - private Object actualTransformer()throws Exception { + private Object actualTransformer(String className)throws Exception { blackboardFixture();Class type=Class.forName("net.minecraftforge.coremod.CoreModEngine");Object engine=type.getConstructor().newInstance(); Method load=java.util.Arrays.stream(type.getDeclaredMethods()).filter(m->m.getName().equals("loadCoreMod")).findFirst().orElseThrow();load.setAccessible(true); Class api=load.getParameterTypes()[0];java.util.List readers=new java.util.ArrayList<>(); @@ -56,12 +56,22 @@ private Object actualTransformer()throws Exception { }); try { load.invoke(engine,file);List transforms=(List)type.getMethod("initializeCoreMods").invoke(engine); - assertEquals(1,transforms.size(),"Actual native engine must install the shipped transformer");return transforms.get(0); + assertEquals(3,transforms.size(),"Actual native engine must install the three shipped transformers"); + return transforms.stream().map(item->(cpw.mods.modlauncher.api.ITransformer)item) + .filter(item->item.targets().stream().anyMatch(target->target.getClassName().replace('/','.').equals(className))) + .findFirst().orElseThrow(); } finally {for(var reader:readers)reader.close();} } - private ClassNode nativeReader()throws Exception { + private static final String READER="net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess"; + private static final String FIXER="net.minecraft.util.datafix.DataFixers"; + private static final String DEDICATED_MAIN="net.minecraft.server.Main"; + + private ClassNode nativeClass(String name)throws Exception { ClassNode node=new ClassNode(Opcodes.ASM9); - try(var input=getClass().getClassLoader().getResourceAsStream("net/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess.class")) {assertNotNull(input);new ClassReader(input).accept(node,0);} + try(var input=getClass().getClassLoader().getResourceAsStream(name.replace('.','/')+".class")) { + assertNotNull(input); + new ClassReader(input).accept(node,0); + } return node; } private ClassNode transform(Object transformer,ClassNode node)throws Exception { @@ -84,16 +94,50 @@ private void verify(Class reader)throws Exception { assertNotNull(((Invocable)engine).invokeFunction("initializeCoreMod")); } @Test void nativeEngineTransformsActualReaderAndItsAttachmentIsVerifiable()throws Exception { - ClassNode transformed=transform(actualTransformer(),nativeReader()); + ClassNode transformed=transform(actualTransformer(READER),nativeClass(READER)); verify(define(transformed)); } + @Test void nativeEngineWrapsTheDataFixerAccessorExactlyOnce()throws Exception { + Object transformer=actualTransformer(FIXER); + ClassNode result=transform(transformer,nativeClass(FIXER)); + result=transform(transformer,result); + MethodNode accessor=result.methods.stream().filter(method->method.name.equals("getDataFixer") + && method.desc.equals("()Lcom/mojang/datafixers/DataFixer;")).findFirst().orElseThrow(); + int calls=0; + for(AbstractInsnNode instruction:accessor.instructions) { + if(instruction instanceof MethodInsnNode invoke && invoke.owner.equals("cyano/lootable/upgrade/UpgradePreservation")) { + assertEquals("wrapper",invoke.name); + assertEquals("(Lcom/mojang/datafixers/DataFixer;)Lcom/mojang/datafixers/DataFixer;",invoke.desc); + assertEquals(Opcodes.ARETURN,invoke.getNext().getOpcode()); + calls++; + } + } + assertEquals(1,calls); + } + @Test void nativeDedicatedReaderGuardsBothHeaderReads()throws Exception { + Object transformer=actualTransformer(DEDICATED_MAIN); + ClassNode result=transform(transformer,nativeClass(DEDICATED_MAIN)); + result=transform(transformer,result); + MethodNode main=result.methods.stream().filter(method->method.name.equals("main") + && method.desc.equals("([Ljava/lang/String;)V")).findFirst().orElseThrow(); + java.util.ArrayList guardedReads=new java.util.ArrayList<>(); + for(AbstractInsnNode instruction:main.instructions) { + if(instruction instanceof MethodInsnNode invoke && invoke.owner.equals("cyano/lootable/upgrade/EarlyWorldPreservation")) { + assertEquals("readDedicatedWorld",invoke.name); + assertEquals(Opcodes.INVOKESTATIC,invoke.getOpcode()); + guardedReads.add(invoke.getPrevious().getOpcode()); + } + } + assertEquals(List.of(Opcodes.ICONST_0,Opcodes.ICONST_1),guardedReads); + define(result); + } @Test void serialSetupRefusesAnUntransformedReaderBeforeAnyWorldCanOpen()throws Exception { Field installed=EarlyWorldPreservation.class.getDeclaredField("installed");installed.setAccessible(true);boolean before=installed.getBoolean(null); try {installed.setBoolean(null,false);assertThrows(IllegalStateException.class,EarlyWorldPreservation::install);assertFalse(installed.getBoolean(null));} finally {installed.setBoolean(null,before);} } @Test void nativeWrapperCannotTurnAFailedTransformIntoAnAcceptedAttachment()throws Exception { - ClassNode node=nativeReader();Object transformer=actualTransformer(); + ClassNode node=nativeClass(READER);Object transformer=actualTransformer(READER); for(MethodNode method:node.methods)for(AbstractInsnNode i=method.instructions.getFirst();i!=null;){AbstractInsnNode next=i.getNext();if(i instanceof MethodInsnNode&&((MethodInsnNode)i).owner.equals("net/minecraftforge/common/ForgeHooks")&&((MethodInsnNode)i).name.equals("writeAdditionalLevelSaveData")){method.instructions.insertBefore(i,new InsnNode(Opcodes.POP2));method.instructions.remove(i);}i=next;} ClassNode returned=transform(transformer,node); InvocationTargetException refused=assertThrows(InvocationTargetException.class,()->verify(define(returned))); diff --git a/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java b/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java index c222a61..e69b65d 100644 --- a/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java @@ -16,7 +16,7 @@ /** Actual Forge45 mapping actions and native PoI data fixes, without a world. */ class Native119RegistrySnapshotTest { private MissingMappingsEvent.Action action(ResourceKey> registry,String name)throws Exception { - MissingMappingsEvent.Mapping mapping=new MissingMappingsEvent.Mapping<>(null,null,new ResourceLocation(name),17); + MissingMappingsEvent.Mapping mapping=new MissingMappingsEvent.Mapping<>(null,null,ResourceLocation.parse(name),17); MissingMappingsEvent event=new MissingMappingsEvent(registry,null,List.of(mapping)); for(java.lang.reflect.Method method:LegacyRegistryMappings.class.getDeclaredMethods()) if(method.isAnnotationPresent(SubscribeEvent.class) && Arrays.equals(method.getParameterTypes(),new Class[]{MissingMappingsEvent.class}))method.invoke(null,event); diff --git a/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java index d973b12..f80c635 100644 --- a/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java @@ -1,7 +1,7 @@ package cyano.lootable.upgrade; import net.minecraft.SharedConstants; -import net.minecraft.core.RegistryAccess; +import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -33,10 +33,10 @@ private static CompoundTag legacy(String id,int count,int slot) { return stack; } - @Test void targetUsesTheActual3839NativeSchema() { - assertEquals(3839,SharedConstants.getCurrentVersion().getDataVersion().getVersion()); - assertEquals(3839,UpgradePreservation.MINECRAFT_VERSION); - assertEquals(11,UpgradePreservation.DATA_VERSION); + @Test void targetUsesTheActual3955NativeSchema() { + assertEquals(3955,SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + assertEquals(3955,UpgradePreservation.MINECRAFT_VERSION); + assertEquals(12,UpgradePreservation.DATA_VERSION); } @Test void released120ItemShapePreservesDamageEnchantmentsAndTypedCustomData() { @@ -64,7 +64,7 @@ private static CompoundTag legacy(String id,int count,int slot) { 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); + HolderLookup.Provider registry=net.minecraft.data.registries.VanillaRegistries.createLookup(); ItemStack stack=ItemStack.parse(registry,converted).orElseThrow(); assertSame(Items.DIAMOND_SWORD,stack.getItem()); assertEquals(27,stack.getDamageValue()); @@ -164,7 +164,7 @@ private static CompoundTag legacy(String id,int count,int slot) { 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); + block,3465,3955); assertEquals(fixed.getString("Name"),LegacyRegistryMappings.blockReplacement(from).toString()); for(String name:new String[]{"grass","scute"}) { var key=net.minecraft.resources.ResourceLocation.withDefaultNamespace(name); @@ -202,14 +202,83 @@ private static CompoundTag legacy(String id,int count,int slot) { } @Test void validCurrentComponentItemIsCopiedWithoutLegacyConversion() { - RegistryAccess registry=RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + HolderLookup.Provider registry=net.minecraft.data.registries.VanillaRegistries.createLookup(); 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)); + CompoundTag converted=ItemMigration.convert(current,context(3955)); assertEquals(current,converted); assertNotSame(current,converted); } + + @Test void released1206ModifiersKeepTheirValuesAndSlotThroughNativeConversion() { + CompoundTag old=releasedModifierStack(); + CompoundTag original=old.copy(); + CompoundTag migrated=ItemMigration.convert(old,context(3839)); + assertNotNull(migrated); + assertEquals(47,migrated.getByte("Slot")); + CompoundTag modifier=migrated.getCompound("components").getCompound("minecraft:attribute_modifiers") + .getList("modifiers",10).getCompound(0); + assertEquals("minecraft:00000000-0000-0065-0000-0000000000ca",modifier.getString("id")); + assertEquals(2.75,modifier.getDouble("amount")); + assertEquals("add_value",modifier.getString("operation")); + assertEquals("mainhand",modifier.getString("slot")); + assertFalse(modifier.contains("uuid")); + assertEquals(original,old); + HolderLookup.Provider registry=net.minecraft.data.registries.VanillaRegistries.createLookup(); + assertSame(Items.DIAMOND_SWORD,ItemStack.parse(registry,migrated).orElseThrow().getItem()); + } + + @Test void legacyEntityModifiersUseTheNativeUuidAndNameMapping() { + assertEquals("minecraft:effect.speed",ItemMigration.legacyModifierId( + java.util.UUID.fromString("91aeaa56-376b-4498-935b-2f7f68070635"),"Old speed").toString()); + assertEquals("minecraft:00000000-0000-0065-0000-0000000000ca", + ItemMigration.legacyModifierId(new java.util.UUID(101,202),"Old modifier").toString()); + } + + @Test void released1206NestedStackKeepsContainerIndexAndRejectsCollidingModifierIds() { + CompoundTag sword=releasedModifierStack(); + sword.remove("Slot"); + CompoundTag box=new CompoundTag(),components=new CompoundTag(),entry=new CompoundTag(); + box.putString("id","minecraft:blue_shulker_box"); + box.putInt("count",1); + entry.putInt("slot",19); + entry.put("item",sword); + ListTag contents=new ListTag(); + contents.add(entry); + components.put("minecraft:container",contents); + box.put("components",components); + CompoundTag upgraded=ItemMigration.convert(box,context(3839)); + assertNotNull(upgraded); + CompoundTag migrated=upgraded.getCompound("components").getList("minecraft:container",10).getCompound(0); + assertEquals(19,migrated.getInt("slot")); + assertEquals("minecraft:diamond_sword",migrated.getCompound("item").getString("id")); + ListTag modifiers=sword.getCompound("components").getCompound("minecraft:attribute_modifiers").getList("modifiers",10); + modifiers.add(modifiers.getCompound(0).copy()); + CompoundTag original=box.copy(); + assertNull(ItemMigration.convert(box,context(3839))); + assertEquals(original,box); + } + + private static CompoundTag releasedModifierStack() { + CompoundTag item=new CompoundTag(),components=new CompoundTag(),attributes=new CompoundTag(),modifier=new CompoundTag(); + item.putString("id","minecraft:diamond_sword"); + item.putInt("count",1); + item.putByte("Slot",(byte)47); + modifier.putUUID("uuid",new java.util.UUID(101,202)); + modifier.putString("name","Old modifier"); + modifier.putString("type","minecraft:generic.attack_damage"); + modifier.putDouble("amount",2.75); + modifier.putString("operation","add_value"); + modifier.putString("slot","mainhand"); + ListTag list=new ListTag(); + list.add(modifier); + attributes.put("modifiers",list); + attributes.putBoolean("show_in_tooltip",false); + components.put("minecraft:attribute_modifiers",attributes); + item.put("components",components); + return item; + } } diff --git a/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java index eabb85d..d60650d 100644 --- a/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java @@ -17,8 +17,8 @@ class Native120RegistryRenameTest { @Test void released119PotteryItemsMatchTheVanillaItemStackConversion() { for (String design : List.of("archer", "arms_up", "prize", "skull")) { - ResourceLocation old = new ResourceLocation("minecraft:pottery_shard_" + design); - ResourceLocation replacement = new ResourceLocation("minecraft:" + design + "_pottery_sherd"); + ResourceLocation old = ResourceLocation.parse("minecraft:pottery_shard_" + design); + ResourceLocation replacement = ResourceLocation.parse("minecraft:" + design + "_pottery_sherd"); CompoundTag item = new CompoundTag(); item.putString("id", old.toString()); item.putByte("Count", (byte) 3); @@ -33,28 +33,28 @@ class Native120RegistryRenameTest { assertTrue(BuiltInRegistries.ITEM.containsKey(replacement)); assertEquals(replacement, LegacyRegistryMappings.itemReplacement(old)); } - assertNull(LegacyRegistryMappings.itemReplacement(new ResourceLocation("anothermod:pottery_shard_archer"))); - assertNull(LegacyRegistryMappings.itemReplacement(new ResourceLocation("minecraft:pottery_shard_unknown"))); + assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("anothermod:pottery_shard_archer"))); + assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("minecraft:pottery_shard_unknown"))); } @Test void released119BrushSoundsUseTheNative120Registrations() { - ResourceLocation brushing = new ResourceLocation("minecraft:item.brush.brushing.generic"); - ResourceLocation complete = new ResourceLocation("minecraft:item.brush.brushing.sand.complete"); + ResourceLocation brushing = ResourceLocation.parse("minecraft:item.brush.brushing.generic"); + ResourceLocation complete = ResourceLocation.parse("minecraft:item.brush.brushing.sand.complete"); assertSame(SoundEvents.BRUSH_GENERIC, BuiltInRegistries.SOUND_EVENT.get(brushing)); assertSame(SoundEvents.BRUSH_SAND_COMPLETED, BuiltInRegistries.SOUND_EVENT.get(complete)); - assertEquals(brushing, LegacyRegistryMappings.soundReplacement(new ResourceLocation("minecraft:item.brush.brushing"))); - assertEquals(complete, LegacyRegistryMappings.soundReplacement(new ResourceLocation("minecraft:item.brush.brush_sand_completed"))); - assertNull(LegacyRegistryMappings.soundReplacement(new ResourceLocation("anothermod:item.brush.brushing"))); + assertEquals(brushing, LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:item.brush.brushing"))); + assertEquals(complete, LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:item.brush.brush_sand_completed"))); + assertNull(LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("anothermod:item.brush.brushing"))); } @Test void splitBiomeMusicOnlyResolvesItsRetiredNumericSoundBinding() throws Exception { - ResourceLocation retired = new ResourceLocation("minecraft:music.overworld.jungle_and_forest"); + ResourceLocation retired = ResourceLocation.parse("minecraft:music.overworld.jungle_and_forest"); assertFalse(BuiltInRegistries.SOUND_EVENT.containsKey(retired)); - assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(new ResourceLocation("minecraft:music.overworld.jungle"))); - assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(new ResourceLocation("minecraft:music.overworld.forest"))); + assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.jungle"))); + assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.forest"))); assertEquals(MissingMappingsEvent.Action.IGNORE, soundAction(retired)); - assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(new ResourceLocation("anothermod:music.overworld.jungle_and_forest"))); - assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(new ResourceLocation("minecraft:music.overworld.unknown"))); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(ResourceLocation.parse("anothermod:music.overworld.jungle_and_forest"))); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(ResourceLocation.parse("minecraft:music.overworld.unknown"))); } @Test void retiredParrotImitationsOnlyResolveTheFiveObsoleteVanillaSoundBindings() throws Exception { @@ -63,7 +63,7 @@ class Native120RegistryRenameTest { 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); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(ResourceLocation.fromNamespaceAndPath("othermod", path)), path); } ResourceLocation piglin = ResourceLocation.withDefaultNamespace("entity.parrot.imitate.piglin"); assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(piglin)); diff --git a/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java b/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java index 882536b..338c0d5 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.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("1.21.1",net.minecraft.SharedConstants.getCurrentVersion().getName()); + assertEquals(34,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); + assertEquals(48,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); + assertEquals(3955,targetDataVersion()); assertEquals(targetDataVersion(),UpgradePreservation.MINECRAFT_VERSION); - assertEquals(11,UpgradePreservation.DATA_VERSION); + assertEquals(12,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(11,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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); @@ -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(11,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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(11,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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(11,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(12,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/NativeMenuObservation.java b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java index 9bfb7f0..d87b59b 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java @@ -4,7 +4,6 @@ import io.netty.buffer.Unpooled; 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,10 +18,9 @@ public final class NativeMenuObservation { NativeMenuObservation(boolean content,int containerId,int stateId,int slot,List items,ItemStack carried,ItemStack item) { 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) { + public static NativeMenuObservation decode(Packet packet,RegistryAccess registries) { if(!(packet instanceof ClientboundContainerSetContentPacket) && !(packet instanceof ClientboundContainerSetSlotPacket))return null; - var registries=RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); var wire=new RegistryFriendlyByteBuf(Unpooled.buffer(),registries); try { NativeMenuObservation result; diff --git a/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java b/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java index d880bd8..3961f50 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java +++ b/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java @@ -15,22 +15,27 @@ private TypedNbt() { } public static boolean same(Tag left, Tag right) { return canonical(left).equals(canonical(right)); } - /** Native Attributes is a map; inventory and recovery lists stay ordered. */ + /** The native attribute map is unordered; inventory and recovery lists stay ordered. */ public static boolean sameEntity(CompoundTag left,CompoundTag right) { return same(entityCopy(left),entityCopy(right)); } private static CompoundTag entityCopy(CompoundTag input) { CompoundTag copy=input.copy(); - if(!copy.contains("Attributes",9))return copy; - ListTag attributes=copy.getList("Attributes",10); - if(attributes.size()!=((ListTag)copy.get("Attributes")).size())return copy; + sortAttributes(copy,"Attributes","Name"); + sortAttributes(copy,"attributes","id"); + return copy; + } + private static void sortAttributes(CompoundTag copy,String key,String identity) { + if(!(copy.get(key) instanceof ListTag raw))return; + ListTag attributes=copy.getList(key,10); + if(attributes.size()!=raw.size())return; java.util.List entries=new java.util.ArrayList<>(); for(Tag value:attributes) { - if(!(value instanceof CompoundTag) || !((CompoundTag)value).contains("Name",8))return copy; + if(!(value instanceof CompoundTag compound) || !compound.contains(identity,8))return; entries.add(value); } entries.sort(java.util.Comparator.comparing(TypedNbt::canonical)); - ListTag sorted=new ListTag();entries.forEach(sorted::add);copy.put("Attributes",sorted);return copy; + ListTag sorted=new ListTag();entries.forEach(sorted::add);copy.put(key,sorted); } public static String canonical(Tag tag) { if(tag==null)return "null"; diff --git a/tools/fixtures/native121-flat-level.dat b/tools/fixtures/native121-flat-level.dat new file mode 100644 index 0000000..4927242 Binary files /dev/null and b/tools/fixtures/native121-flat-level.dat differ diff --git a/tools/fixtures/native121-flat-level.provenance.json b/tools/fixtures/native121-flat-level.provenance.json new file mode 100644 index 0000000..3aa9c35 --- /dev/null +++ b/tools/fixtures/native121-flat-level.provenance.json @@ -0,0 +1,11 @@ +{ + "minecraft": "1.21.1", + "forge": "52.1.20", + "java": "21.0.7+6", + "data_version": 3955, + "generator": "minecraft:flat", + "sha256": "C85D57B04D1309304A2B28992E5B45B4E695039BE162682255A0CCCCB3EE30CD", + "candidate_sha256": "995425C67D6CD62C5BE6C70937BD6F8E1EDA05EC931810EE087A44CCCF09D310", + "generated_by_native_server": true, + "stdout_sha256": "E83C11EEFDE3DBCAB3CFEC7E881270FE0F59F0990F25C3B90B9B7DB3B79C7590" +} diff --git a/tools/fixtures/native121-forge-only-flat-level.dat b/tools/fixtures/native121-forge-only-flat-level.dat new file mode 100644 index 0000000..69e7a9f Binary files /dev/null and b/tools/fixtures/native121-forge-only-flat-level.dat differ diff --git a/tools/hosted_policy.py b/tools/hosted_policy.py index fe535b7..9dbd24a 100644 --- a/tools/hosted_policy.py +++ b/tools/hosted_policy.py @@ -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+\.120061',version) or record.get('minecraft')!='1.20.6' or record.get('forge')!='50.2.14':raise ValueError('Wrong target qualification') + if not re.fullmatch(r'\d+\.\d+\.\d+\.121011',version) or record.get('minecraft')!='1.21.1' or record.get('forge')!='52.1.20':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') @@ -80,10 +80,11 @@ def check_source(root): 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') + if policy['target']!='1.21.1' or any(rule not in guard for rule in ( + "System.getenv('GITHUB_ACTIONS') == 'true'", + "System.getenv('GITHUB_REPOSITORY') != 'MinecraftModDevelopmentMods/LootableBodies'", + 'Hosted builds are limited to the MMD repository')): + raise ValueError('Missing MMD-only hosted build 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') 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') diff --git a/tools/local_case_reuse.py b/tools/local_case_reuse.py index b6fbd8b..61407b2 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/native1206_server_inputs.json', - 'tools/native1206_worker_budget.json','tools/native120_logs.py') + 'tools/native_evidence.py','tools/native121_server_inputs.json', + 'tools/native121_worker_budget.json','tools/native120_logs.py') def normal_program(text): tree=ast.parse(text) diff --git a/tools/native120_build.py b/tools/native120_build.py index 7987d8d..cd5f58b 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.6','50.2.14'): + if (record.get('version'),record.get('minecraft'),record.get('forge'))!=(MOD_VERSION,'1.21.1','52.1.20'): 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 0c86675..1eefa1c 100644 --- a/tools/native120_client.py +++ b/tools/native120_client.py @@ -1,4 +1,4 @@ -"""Pinned Forge 50 client preparation and descriptor-driven Java 21 launch.""" +"""Pinned Forge 52 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.6' -VERSION = '1.20.6-forge-50.2.14' -BASE_SHA1 = '4dd84bf5b646a75cba5c0c5f7723eb478a2b78af' -FORGE_SHA256 = '40A8892EAB75917AA96D5D63DD92F9BE8BA86A3D50429B96A12A1F2F206FEB46' +BASE_VERSION = '1.21.1' +VERSION = '1.21.1-forge-52.1.20' +BASE_SHA1 = '82ba6501da174a665c1787775f946487422f2432' +FORGE_SHA256 = '359AF0F59286C8680B64CED0C830BBCF5F66A0DAB2CE6A27BB6C2030AC2D5193' 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 native 1.20.6 client descriptor') + raise RuntimeError('Incorrect official native 1.21.1 client descriptor') if not forge.is_file() or hashlib.sha256(forge.read_bytes()).hexdigest().upper() != FORGE_SHA256: - raise RuntimeError('Incorrect installed Forge 50 client descriptor') + raise RuntimeError('Incorrect installed Forge 52 client descriptor') return json.loads(base.read_text()), json.loads(forge.read_text()) @@ -175,7 +175,7 @@ def command(java, home, runtime, assets, prepared, *, username, extra=()): 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 native 1.20.6 asset index') + raise RuntimeError('Incorrect official native 1.21.1 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', diff --git a/tools/native120_client_logs.py b/tools/native120_client_logs.py index db31bc5..5caf86d 100644 --- a/tools/native120_client_logs.py +++ b/tools/native120_client_logs.py @@ -1,18 +1,18 @@ -"""Explicit Forge 50 client diagnostics, separate from strict server/error audits.""" +"""Explicit Forge 52 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('native1206_client_diagnostics.json') - proof_path=Path(proof_path) if proof_path else project/'.gradle/native1206-client-sound-control-01/proof.json' + policy_path=Path(policy_path) if policy_path else Path(__file__).with_name('native121_client_diagnostics.json') + proof_path=Path(proof_path) if proof_path else project/'.gradle/native121-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.6','50.2.14','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.21.1','52.1.20','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='native1206-diagnostic-audit-') as directory: + with tempfile.TemporaryDirectory(prefix='native121-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 1b94e5b..b9bc4e0 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 Forge 50 qualification') + raise RuntimeError('Legacy login-retry classification is not Forge 52 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.6' + result['target_minecraft']='1.21.1' result['legacy_native_controls_used']=False return result diff --git a/tools/native120_persistence.py b/tools/native120_persistence.py index dfc217b..cfcb264 100644 --- a/tools/native120_persistence.py +++ b/tools/native120_persistence.py @@ -1,4 +1,4 @@ -"""Read-only native 1.20.6 saved entity inspection, distinct from old-writer proof.""" +"""Read-only native 1.21.1 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,3839):raise ValueError('Wrong native 1.20.6 entity-chunk data version') + if root.get('DataVersion')!=Tag(3,3955):raise ValueError('Wrong native 1.21.1 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 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') + raise ValueError('Wrong native 1.21.1 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.21.1 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,11):raise ValueError('Wrong native 1.20.6 corpse identity/marker') + if entity.value['id']!=Tag(8,'lootablebodies:corpse') or entity.value.get('LootableBodiesDataVersion')!=Tag(3,12):raise ValueError('Wrong native 1.21.1 corpse identity/marker') identity=entity.value.get('UUID') - 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 is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native 1.21.1 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/native121_client_diagnostics.json b/tools/native121_client_diagnostics.json new file mode 100644 index 0000000..4e44e68 --- /dev/null +++ b/tools/native121_client_diagnostics.json @@ -0,0 +1,14 @@ +{ + "minecraft": "1.21.1", + "forge": "52.1.20", + "platform": "Windows amd64", + "qualified": true, + "events": [ + "minecraft:item.goat_horn.play", + "minecraft:entity.goat.screaming.horn_break" + ], + "control_sha256": "99557854F30962C100A7DC4A85B37C5E3469A7693CD230EC691C01527CD16140", + "proof_sha256": "41FCEB9CCF951425819B20A9B8AEEB7137BEA911351BC1704D1F7A4BB82D09BA", + "sound_definition_sha256": "51B23584D267BA37D3A1BAFD4F24D3DDA07672AB91672EA6F7A1BC86067A1A6B", + "scope": "Two ordered, unbound vanilla sound events observed without the mod or a world. Other diagnostics remain strict." +} diff --git a/tools/native121_server_inputs.json b/tools/native121_server_inputs.json new file mode 100644 index 0000000..e598dc1 --- /dev/null +++ b/tools/native121_server_inputs.json @@ -0,0 +1,95 @@ +{ + "libraries/com/electronwill/night-config/core/3.7.4/core-3.7.4.jar": "CA45C88482C0F909810889DF14A05350BF8AC7B82C57EBBBAF32AACE8DDB6B52", + "libraries/com/electronwill/night-config/toml/3.7.4/toml-3.7.4.jar": "835783CB048326B02DD701E04A7EDAF141068DA1E4554A1F1D37466BB4D7810B", + "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.3.10/brigadier-1.3.10.jar": "C8EE4136E474AC7723CA2B432EC8D1A2BC88EF7D1EC57C314BA9E33CDC83DD75", + "libraries/com/mojang/datafixerupper/8.0.16/datafixerupper-8.0.16.jar": "FFC138BC2596C291781B0D5E211CCAC51F0F2345F27FC2742F335CEDF7E2870D", + "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.21.1-20240808.132146/mcp_config-1.21.1-20240808.132146-srg2off.jar": "95A92957E05314DBDB01EC69D74DCF0CA651C5CE3CABD78A52900956D15FC9C9", + "libraries/de/oceanlabs/mcp/mcp_config/1.21.1-20240808.132146/mcp_config-1.21.1-20240808.132146.zip": "7AAA35810C5D3D831D4DB986B622F763D3BE631D6C66DBBA1F8EE3E7F4C87BBB", + "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.21.1/server-1.21.1-bundled.jar": "E3BC55693E93CDA0188F2E60AEA28113FC647C5E85A15FA3D1B347349231B4BB", + "libraries/net/minecraft/server/1.21.1/server-1.21.1-mappings.tsrg": "2B850FF97C0BC8C5052C7B2AB51E3CE06714698ADEC9D36CAA02A29E17BFCE02", + "libraries/net/minecraft/server/1.21.1/server-1.21.1-official.jar": "11E94FE3E52746E49465C1F4082C57F5F2751EF90993DCA14158962E512510FF", + "libraries/net/minecraft/server/1.21.1/server-1.21.1-unpacked.jar": "C301DE10F575027D13EAC18C7F34409D60648CF56A35D566AA1F530FF617840A", + "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.21.1-52.1.20/fmlcore-1.21.1-52.1.20.jar": "5D66AD04E28AE7E9DD6750674E0EF952B872C85840EFF69B5FAE31B948E16577", + "libraries/net/minecraftforge/fmlearlydisplay/1.21.1-52.1.20/fmlearlydisplay-1.21.1-52.1.20.jar": "2F7904EDA0F150B7C5DEFD0A2C0686287B63C278E175C7D1BB702C7AD6FA750A", + "libraries/net/minecraftforge/fmlloader/1.21.1-52.1.20/fmlloader-1.21.1-52.1.20.jar": "B0DD692FA04C77A878B12751DFA028A21367FB603ABDBFEB8802F123DAFEB765", + "libraries/net/minecraftforge/forge/1.21.1-52.1.20/forge-1.21.1-52.1.20-server.jar": "A19B7DC205719132139D03492BBFAE51DCF58C8F65E2044935CF9C4DA9C0E5A2", + "libraries/net/minecraftforge/forge/1.21.1-52.1.20/forge-1.21.1-52.1.20-shim.jar": "6A910737E45BBB05F02B3D95576E263F9D83C1E425AA821ADBD1F29A02E9595C", + "libraries/net/minecraftforge/forge/1.21.1-52.1.20/forge-1.21.1-52.1.20-universal.jar": "AAB8963864486381C1DB99E2508E57BACA3914C17B9DED66D264D9C5289E864C", + "libraries/net/minecraftforge/forge/1.21.1-52.1.20/unix_args.txt": "8E1A1872D63BA80249626637B70043C864053A41338E18D7F04163902FFFD53E", + "libraries/net/minecraftforge/forge/1.21.1-52.1.20/win_args.txt": "8E1A1872D63BA80249626637B70043C864053A41338E18D7F04163902FFFD53E", + "libraries/net/minecraftforge/ForgeAutoRenamingTool/1.0.6/ForgeAutoRenamingTool-1.0.6.jar": "072368476225FF6D29468D07C49CD04AA5EAE11B8AB99AB3DD05802B07BC22C4", + "libraries/net/minecraftforge/forgespi/7.1.5/forgespi-7.1.5.jar": "6DC1DF5E18479DCE2D7D65A8B706E22F70448EE79FA9123825B459B458DABCB7", + "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.21.1-52.1.20/javafmllanguage-1.21.1-52.1.20.jar": "06B3AAA52701F604FC49E267B14AD7737D9EC7D0B1621AD00CC98ED199827CB6", + "libraries/net/minecraftforge/lowcodelanguage/1.21.1-52.1.20/lowcodelanguage-1.21.1-52.1.20.jar": "495125DFFEB1190A94C0392FD555CA12EAFD3151410CDA9934DC6BED4F2E01EE", + "libraries/net/minecraftforge/mclanguage/1.21.1-52.1.20/mclanguage-1.21.1-52.1.20.jar": "C14449CDDE1346680FB72410744582C87C9BC79BC2AA53BC728B1DFEC51E1FA2", + "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.24/securemodules-2.2.24.jar": "40CB1FA55F48E3812EB3F823019CF906D9995BC4C9015F3F8F28471315C2A656", + "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.25.1/jline-reader-3.25.1.jar": "26333A275DE502ADF1DD9E6EA50AA0B4021412C71490DF9ED5E88A648886EE89", + "libraries/org/jline/jline-terminal/3.25.1/jline-terminal-3.25.1.jar": "C0F5D70901255DA66A94E59778B265D19F9308342578E34C88FC92D1B0C65FEF", + "libraries/org/jline/jline-terminal-jna/3.25.1/jline-terminal-jna-3.25.1.jar": "58CA9D719C373206AF15775EE3CD5F268136EA0D0C4E009C3E96A6D4612D5C66", + "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.7/mixin-0.8.7.jar": "87AAA7EE080465ED7EA13616679534ABCDBFDD636B4752D0EFF0A00017E235C0", + "libraries/trove/trove/1.0.2/trove-1.0.2.jar": "AD68A86CFD0255A2BC0525CE11EC49640CAEA7E34FF91EB89C3193DCCCD77F05", + "forge-1.21.1-52.1.20-shim.jar": "6A910737E45BBB05F02B3D95576E263F9D83C1E425AA821ADBD1F29A02E9595C" +} diff --git a/tools/native121_worker_budget.json b/tools/native121_worker_budget.json new file mode 100644 index 0000000..3bd9304 --- /dev/null +++ b/tools/native121_worker_budget.json @@ -0,0 +1,11 @@ +{ + "minecraft": "1.21.1", + "forge": "52.1.20", + "reloads": 200, + "jvms": 4, + "qualified": true, + "jvm_args": [], + "controller_sha256": "EDA2C8CB7983A3986274809265CA1DCD131ED1A78967FD2E4B392CFF5D05C842", + "server_input_manifest_sha256": "1FE3E789952A0B43B178A7B51178F331117035870103596C62F144E97AECDD0B", + "qualification_evidence_sha256": "3264929DA220F9E21840DE9D43C3C04E3827C27F4372C6D7076D9B54F16F61B7" +} diff --git a/tools/native_evidence.py b/tools/native_evidence.py index cd03a5f..be23576 100644 --- a/tools/native_evidence.py +++ b/tools/native_evidence.py @@ -1,7 +1,7 @@ -"""Independently audit Forge 50 local receipts, complete logs and diagnostic bytes. +"""Independently audit Forge 52 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 Forge 50 +Relocated auditing is a separately tested download contract; hosted Forge 52 execution and promotion remain disabled and have not been exercised. """ from pathlib import Path,PurePosixPath @@ -16,12 +16,12 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): if receipt['timed_out'] or type(receipt['returncode']) is not int or receipt['pid']<=0: raise ValueError('Incomplete or timed-out native JVM receipt') if receipt['timeout']!=(180 if refusal else 240):raise ValueError('Changed native deadline') - if (receipt['returncode']==0)==refusal:raise ValueError('Unexpected native JVM exit') + if receipt['returncode']!=(1 if refusal else 0):raise ValueError('Unexpected native JVM exit') command=receipt['command'] - # Native45 was qualified with defaults; no old CPU/worker/GC cap is valid. + # Forge 52 was qualified with its default worker and GC settings. 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 Forge 50 worker or GC override') + raise ValueError('Unqualified Forge 52 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,7 +34,7 @@ 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 Forge 50 modules') + if len(arguments)!=1 or arguments[0] not in expected or '--nogui' not in command:raise ValueError('Receipt did not launch Forge 52 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'): diff --git a/tools/native_runtime.py b/tools/native_runtime.py index 5bb6f39..19d1bb1 100644 --- a/tools/native_runtime.py +++ b/tools/native_runtime.py @@ -1,4 +1,4 @@ -"""Disposable official Forge 50 launches, separate from the development bootstrap.""" +"""Disposable official Forge 52 launches, separate from the development bootstrap.""" import hashlib import json import os @@ -13,33 +13,33 @@ from command_logs import run_logged from runtime_diagnostics import run_monitored,validate_logging_inputs -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') +FORGE = 'forge-1.21.1-52.1.20' +INSTALLER_SHA256 = '6B83521A70AAF5371AF8C4C9F97D930D51EC4266AC6EC3BC975182FBA2EF9F62' +SERVER_INPUTS = json.loads(Path(__file__).with_name('native121_server_inputs.json').read_text(encoding='utf-8')) +WORKER_BUDGET_PATH = Path(__file__).with_name('native121_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 +# Forge 52 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') + raise RuntimeError('Forge 52 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.6','50.2.14',200,4,True): - raise RuntimeError('Forge 50 historical runtime policy has not been qualified') + if tuple(proof.get(key) for key in ('minecraft','forge','reloads','jvms','qualified'))!=('1.21.1','52.1.20',200,4,True): + raise RuntimeError('Forge 52 historical runtime policy has not been qualified') if proof.get('jvm_args')!=[]: - raise RuntimeError('Unqualified Forge 50 worker or GC override') + raise RuntimeError('Unqualified Forge 52 worker or GC override') source=Path(__file__).resolve().parents[1]/'src/controlTest/java/buildcontrol/ResourceControl.java' - manifest=Path(__file__).with_name('native1206_server_inputs.json') + manifest=Path(__file__).with_name('native121_server_inputs.json') if proof.get('controller_sha256')!=digest(source) or proof.get('server_input_manifest_sha256')!=digest(manifest): - raise RuntimeError('Forge 50 resource qualification source or SDK binding changed') + raise RuntimeError('Forge 52 resource qualification source or SDK binding changed') if not __import__('re').fullmatch('[A-F0-9]{64}',proof.get('qualification_evidence_sha256','')): - raise RuntimeError('Missing Forge 50 resource qualification identity') - evidence=Path(__file__).resolve().parents[1]/'.gradle/native1206-resource-qualification/proof.json' + raise RuntimeError('Missing Forge 52 resource qualification identity') + evidence=Path(__file__).resolve().parents[1]/'.gradle/native121-resource-qualification/proof.json' if not evidence.is_file() or digest(evidence)!=proof['qualification_evidence_sha256']: - raise RuntimeError('Missing or changed actual local Forge 50 resource qualification evidence') + raise RuntimeError('Missing or changed actual local Forge 52 resource qualification evidence') return HISTORICAL_JVM_ARGS @@ -60,15 +60,15 @@ def fingerprint(directory): def verify_server_inputs(distribution): """Pin the exact completed SDK after both official installer sides. - Forge 50 generated archives match the installer's declared expected hashes. + Forge 52 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 Forge 50 SDK input: '+name) + if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing Forge 52 SDK input: '+name) value=digest(artifact) - if value!=expected:raise RuntimeError('Incorrect official Forge 50 server input: '+name) + if value!=expected:raise RuntimeError('Incorrect official Forge 52 server input: '+name) actual[name]=value return actual @@ -90,7 +90,7 @@ def prepare_server(home, distribution, candidate=None, probe=None): distribution = distribution.resolve() libraries = distribution / 'libraries' if not libraries.is_dir(): - raise FileNotFoundError('Incomplete official Forge 50 server installation') + raise FileNotFoundError('Incomplete official Forge 52 server installation') verify_server_inputs(distribution) home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() @@ -156,7 +156,7 @@ 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.6-50.2.14/' + arguments, '--nogui'] + *jvm_args, '@libraries/net/minecraftforge/forge/1.21.1-52.1.20/' + arguments, '--nogui'] validate_logging_inputs(command) def initial_diagnostics(process): # Short refusals and successful phases must retain real thread/heap @@ -206,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 != 3839 or generator.kind != 8 or generator.value != 'minecraft:flat': + if version.kind != 3 or version.value != 3955 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 native3839 flat codec header') from error + raise RuntimeError('Expected an explicit native3955 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':3839, 'generator':'minecraft:flat', + 'sha256':expected_sha256, 'native_data_version':3955, 'generator':'minecraft:flat', 'only_header_copied':True}, indent=2)+'\n', encoding='utf-8') @@ -249,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.12.120061'}) + request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesRuntimeTests/2.4.13.121011'}) with urllib.request.urlopen(request, timeout=60) as response: content = response.read() if hashlib.sha1(content).hexdigest() != expected: @@ -281,7 +281,7 @@ def validate_client_paths(paths): import re for path in paths: value=str(path).replace('\\','/').lower();name=path.name.lower() - # Forge 50's exact descriptor requires this dist-marker API archive. + # Forge 52'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) == '100F0C4D658B9DD73600CE4BB9C75657E0651F0F0F5E0B2F977F85D8F495350B'): @@ -297,7 +297,7 @@ def client_inputs(runtime, prepared): def prepare_assets(runtime, cache=None): - raise RuntimeError('Use pinned native 1.20.6 assets; inherited cache-copy hydration is disabled') + raise RuntimeError('Use pinned native 1.21.1 assets; inherited cache-copy hydration is disabled') def prepare_client(home, candidate=None, probe=None): @@ -316,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/native1206-client-inputs' + prepared=Path(__file__).resolve().parents[1]/'.gradle/native121-client-inputs' return command(java,home,runtime,assets,prepared,username=username,extra=extra) def install(java, home, client=False): - """Install Forge 50 locally; its installer owns the server downloads.""" + """Install Forge 52 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.6-50.2.14/'+installer.name,installer,INSTALLER_SHA256) + fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.21.1-52.1.20/'+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.6.json'},base) + _download({'sha1':BASE_SHA1,'url':'https://piston-meta.mojang.com/v1/packages/'+BASE_SHA1+'/1.21.1.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/qualification-policy.json b/tools/qualification-policy.json index 6627cd5..1963dac 100644 --- a/tools/qualification-policy.json +++ b/tools/qualification-policy.json @@ -1,7 +1,7 @@ { "schema": 1, - "target": "1.20.6", - "status": "Forge 1.20.6 qualifies locally. Hosted checks verify production archives and release contracts.", + "target": "1.21.1", + "status": "Forge 1.21.1 stays local until qualification and manual testing are complete.", "hosted": { "allowed": [ "policy", diff --git a/tools/test_historical_runtime_budget.py b/tools/test_historical_runtime_budget.py index d9749ac..2b8aac6 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.6','forge':'50.2.14','reloads':200,'jvms':4,'qualified':True,'jvm_args':[]} + base={'minecraft':'1.21.1','forge':'52.1.20','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 0256219..3d0b06b 100644 --- a/tools/test_hosted_policy.py +++ b/tools/test_hosted_policy.py @@ -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.120061';base='LootableBodies-'+version + version='2.4.11.121011';base='LootableBodies-'+version names=['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom'] - 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}} + good={'version':version,'minecraft':'1.21.1','forge':'52.1.20','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.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']}} + version='2.4.11.121011';base='LootableBodies-'+version + good={'version':version,'minecraft':'1.21.1','forge':'52.1.20','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 eec2812..c4909dc 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.6','forge':'50.2.14','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{p.name:digest(p) for p in bundle.iterdir()}} + record={'version':MOD_VERSION,'minecraft':'1.21.1','forge':'52.1.20','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 2624da9..b74e321 100644 --- a/tools/test_native120_client_contracts.py +++ b/tools/test_native120_client_contracts.py @@ -34,7 +34,7 @@ def test_forge_bootstrap_uses_installed_client_descriptor_and_exact_java21(self) 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'} + record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'1.21.1.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') with patch.dict('os.environ',{'LOOTABLEBODIES_TEST_SESSION_ENDPOINT':'http://127.0.0.1:51927'}): @@ -42,13 +42,13 @@ def test_forge_bootstrap_uses_installed_client_descriptor_and_exact_java21(self) 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(str(root/'1.21.1.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('1.21.1-forge-52.1.20',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) diff --git a/tools/test_native120_client_logs.py b/tools/test_native120_client_logs.py index 3c31b48..6e04cee 100644 --- a/tools/test_native120_client_logs.py +++ b/tools/test_native120_client_logs.py @@ -1,4 +1,4 @@ -"""Synthetic diagnostic counterexamples; independent Forge 50 is separate proof.""" +"""Synthetic diagnostic counterexamples; independent Forge 52 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.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} + proof={'minecraft':'1.21.1','forge':'52.1.20','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.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)} + policy={'minecraft':'1.21.1','forge':'52.1.20','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 47bfd53..12e3a25 100644 --- a/tools/test_native120_flat_header.py +++ b/tools/test_native120_flat_header.py @@ -9,7 +9,7 @@ class Native120FlatHeaderTests(unittest.TestCase): - def header(self,root,version=3839,generator='minecraft:flat'): + def header(self,root,version=3955,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})})) @@ -23,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 [(3839,'minecraft:flat',True),(3465,'minecraft:flat',False),(2586,'minecraft:flat',False),(2730,'minecraft:flat',False),(2975,'minecraft:flat',False),(3839,'minecraft:noise',False)]: + for version,generator,bad_hash in [(3955,'minecraft:flat',True),(3839,'minecraft:flat',False),(3465,'minecraft:flat',False),(2586,'minecraft:flat',False),(2730,'minecraft:flat',False),(2975,'minecraft:flat',False),(3955,'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) @@ -36,8 +36,8 @@ 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/native1206-flat-level.dat' - expected='34253D55B2B3A4F1CE4107FACF5AF64ED496264ED53804405F6CDC6D45E56E74' + header=Path(__file__).parent/'fixtures/native121-flat-level.dat' + expected='C85D57B04D1309304A2B28992E5B45B4E695039BE162682255A0CCCCB3EE30CD' 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) @@ -45,12 +45,12 @@ def test_actual_native20_codec_header_is_a_sealed_current_target_input(self): 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' + forge_only=fixtures/'native121-forge-only-flat-level.dat' + expected='62253943F2A307C158BC1CE29E2430BE253C3F727A4587FEDDBE279DD3C057D4' 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') + require_forge_only_header(fixtures/'native121-flat-level.dat') if __name__=='__main__':unittest.main() diff --git a/tools/test_native120_infrastructure.py b/tools/test_native120_infrastructure.py index 290dfb6..34cbe7e 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.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.assertEqual('forge-1.21.1-52.1.20',runtime.FORGE) + self.assertEqual('6B83521A70AAF5371AF8C4C9F97D930D51EC4266AC6EC3BC975182FBA2EF9F62',runtime.INSTALLER_SHA256) + self.assertEqual(93,len(runtime.SERVER_INPUTS)) + self.assertEqual('A19B7DC205719132139D03492BBFAE51DCF58C8F65E2044935CF9C4DA9C0E5A2', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.21.1-52.1.20/forge-1.21.1-52.1.20-server.jar']) + self.assertEqual('8E1A1872D63BA80249626637B70043C864053A41338E18D7F04163902FFFD53E', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.21.1-52.1.20/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 = '16'",text) - self.assertIn("1.20.6.json",text) + self.assertIn("minecraftAssetIndex = '17'",text) + self.assertIn("1.21.1.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 9407c9b..f64c8fe 100644 --- a/tools/test_native120_launch.py +++ b/tools/test_native120_launch.py @@ -31,7 +31,7 @@ def test_packaged_server_uses_installer_module_arguments_and_java21_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.6-50.2.14/') for arg in command)) + self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/1.21.1-52.1.20/') for arg in command)) gc=next((arg for arg in command if arg.startswith('-Xlog:')),None) self.assertIsNotNone(gc) self.assertIn('gc*,safepoint',gc) @@ -73,16 +73,16 @@ def attach(command, **options): 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.assertEqual('forge-1.21.1-52.1.20',native_runtime.FORGE) + for path in ('libraries/net/minecraftforge/forge/1.21.1-52.1.20/win_args.txt', + 'libraries/net/minecraftforge/forge/1.21.1-52.1.20/unix_args.txt', + 'libraries/net/minecraftforge/forge/1.21.1-52.1.20/forge-1.21.1-52.1.20-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_native50_historical_workers_have_their_own_fresh_qualification_binding(self): proof=native_runtime.WORKER_BUDGET_PROOF - self.assertEqual(('1.20.6','50.2.14',200,4,True),tuple(proof[key] for key in + self.assertEqual(('1.21.1','52.1.20',200,4,True),tuple(proof[key] for key in ('minecraft','forge','reloads','jvms','qualified'))) self.assertIn('server_input_manifest_sha256',proof) self.assertRegex(proof['qualification_evidence_sha256'],r'^[A-F0-9]{64}$') diff --git a/tools/test_native120_persistence.py b/tools/test_native120_persistence.py index 27277a9..9b9fc42 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=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)): + def body(self,number=1,marker=12):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=3955,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 [(10,3839,(8,8)),(11,3465,(8,8)),(11,3839,(9,8))]: + for marker,version,position in [(11,3955,(8,8)),(12,3839,(8,8)),(12,3955,(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,3840),read_region(path)[264]['root'].value['DataVersion']) + self.assertEqual(Tag(3,3956),read_region(path)[264]['root'].value['DataVersion']) self.assertEqual(8,mutation['corpse_marker']) if __name__=='__main__':unittest.main() diff --git a/tools/test_native_evidence.py b/tools/test_native_evidence.py index b7fc54d..bef5029 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.6-50.2.14/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], + '@libraries/net/minecraftforge/forge/1.21.1-52.1.20/'+('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): @@ -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.6-50.2.14/win_args.txt') + record['command'].append('@libraries/net/minecraftforge/forge/1.21.1-52.1.20/win_args.txt') (folder/'result.json').write_text(json.dumps(record)) with self.assertRaises(ValueError):audit_phase(home,'upgrade-near',relocated=True) diff --git a/tools/test_native_server_preparation.py b/tools/test_native_server_preparation.py index 34e8f4c..f26106e 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.6/server-1.20.6.jar' + relative='libraries/net/minecraft/server/1.21.1/server-1.21.1.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 Forge 50 server input'):runtime.install(Path('fixture-java'),home) + with self.assertRaisesRegex(RuntimeError,'Incorrect official Forge 52 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/verify_native_respawn_control.py b/tools/verify_native_respawn_control.py index cfab1a9..c710d75 100644 --- a/tools/verify_native_respawn_control.py +++ b/tools/verify_native_respawn_control.py @@ -1,4 +1,4 @@ -"""Exercise Forge 50 delayed respawns and FIFO packets without Lootable Bodies.""" +"""Exercise Forge 52 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 @@ -24,7 +24,7 @@ def require_forge_only_header(header): 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')]: + if kind != 10 or sorted(mods) != [('forge', '52.1.20'), ('minecraft', '1.21.1')]: raise ValueError('Expected a Forge-only saved mod list') @@ -93,10 +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('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, + raise RuntimeError('Forge 52 control did not exercise all eight real delayed respawn/FIFO actions') + result={'lootable_bodies_installed':False,'respawns':8,'forge52_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, + 'legacy36_error_matcher_used':False,'forge52_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_packaged.py b/tools/verify_packaged.py index c7f92c2..a138ca1 100644 --- a/tools/verify_packaged.py +++ b/tools/verify_packaged.py @@ -1,4 +1,4 @@ -"""Verify the exact production jar in disposable native Forge 50 servers.""" +"""Verify the exact production jar in disposable native Forge 52 servers.""" import argparse import json from pathlib import Path diff --git a/tools/verify_packaged_client.py b/tools/verify_packaged_client.py index 7644dfd..5b1346b 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 50.""" +"""Run the exact production jar and isolated verifier in official Forge 52.""" import argparse import hashlib import json diff --git a/tools/verify_upgrade_abort.py b/tools/verify_upgrade_abort.py index 31fed0a..3731dc0 100644 --- a/tools/verify_upgrade_abort.py +++ b/tools/verify_upgrade_abort.py @@ -8,11 +8,6 @@ from release_identity import MAIN_JAR -def source_files(world): - return {name: value for name, value in fingerprint(world).items() - if not name.startswith('data/lootablebodies-upgrade')} - - def same_typed_values(expected, actual): if type(expected) is not type(actual): return False @@ -172,9 +167,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,3840);write_disposable_region(path,chunks) + root['DataVersion']=Tag(3,3956);write_disposable_region(path,chunks) return {'region':path.relative_to(world).as_posix(),'chunk_index':index,'source_version':2975, - 'injected_version':3840,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), + 'injected_version':3956,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), 'original_copy':str(original),'corpse_marker':8} @@ -186,6 +181,50 @@ def run_abort(java, home): '-Dlog4j.configurationFile='+logging.as_uri()]) +def verified_future_journal(world, before, after): + """The future-version check runs after the immutable backup is written.""" + from gzip import decompress + from hashlib import sha256 + import re + from nbt_regions import Tag, decode + + prefix = 'data/lootablebodies-upgrade/' + index = prefix + 'index.nbt' + added = set(after) - set(before) + journal = {name for name in added if name.startswith(prefix)} + for name in journal - {index}: + match = re.fullmatch(prefix + r'(chunks|snapshots)/([a-f0-9]{64})\.(bin|nbt)', name) + if not match or (match[1], match[3]) not in {('chunks', 'bin'), ('snapshots', 'nbt')}: + raise ValueError('Unexpected recovery file after refusal: ' + name) + if after[name].lower() != match[2]: + raise ValueError('Recovery content hash mismatch after refusal: ' + name) + if index not in after or not journal: + raise ValueError('Future-version refusal did not retain its recovery backup') + envelope = decode(decompress((world / index).read_bytes()))[1].value + payload = envelope['Payload'] + if (envelope.get('Format') != Tag(3, 2) or payload.kind != 7 + or envelope.get('Digest') != Tag(8, sha256(payload.value).hexdigest())): + raise ValueError('Recovery index checksum mismatch after refusal') + record = decode(decompress(payload.value))[1].value + if record.get('Format') != Tag(3, 2) or record['Regions'].value[0] != 10: + raise ValueError('Invalid recovery index after refusal') + snapshots = {prefix + 'snapshots/' + item.value for region in record['Regions'].value[1] + for item in region.value['Snapshots'].value[1]} + new_snapshots = {name for name in journal if name.startswith(prefix + 'snapshots/')} + if not new_snapshots or not new_snapshots <= snapshots: + raise ValueError('Recovery index omitted a new original') + referenced = set() + for name in new_snapshots: + snapshot = decode(decompress((world / name).read_bytes()))[1].value + chunk = prefix + 'chunks/' + snapshot['OriginalChunk'].value + '.bin' + if chunk not in after or after[chunk].lower() != snapshot['OriginalChunk'].value: + raise ValueError('Recovery original chunk missing or changed: ' + chunk) + referenced.add(chunk) + if not (journal - new_snapshots - {index}) <= referenced: + raise ValueError('Recovery journal contains an unreferenced new chunk') + return sorted(journal | {index}) + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--java', type=Path, required=True) @@ -211,12 +250,12 @@ def main(): world = home / 'world' mutation=inject_future_entity_version(world) if case=='future-entity-version' else None if mutation is None:inject_unsafe_journal(world, case) - before = source_files(world) - config = world/'serverconfig/forge-server.toml' - original_config = config.read_bytes() if config.is_file() else None + before = fingerprint(world) lock=world/'session.lock';original_lock=lock.read_bytes() if lock.is_file() else None log = home / 'abort.log' result = run_abort(args.java.resolve(), home) + if result.returncode != 1: + raise RuntimeError('Dedicated preservation refusal did not exit with status 1: ' + str(log)) text = log.read_text(encoding='utf-8') if "Lootable Bodies could not preserve this world's corpses" not in text: raise RuntimeError('Missing explicit preservation abort: ' + str(log)) @@ -225,34 +264,33 @@ def main(): if any(token in text for token in ['Preparing start region', 'Preparing spawn area', 'LOOTABLE_BODIES_RUNTIME_PASS', 'Done (']): raise RuntimeError('World loading continued after unsafe preservation') - after = source_files(world) + after = fingerprint(world) raw_after = dict(after) - 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: + recovery_updates = verified_future_journal(world, before, after) if mutation else [] + for name in lock_updates + recovery_updates: if name in before: after[name] = before[name] else: del after[name] source_after = fingerprint(source) comparison = { 'before': before, 'after': raw_after, - 'allowed_stock_config_files': platform_additions, + 'allowed_stock_config_files': [], 'allowed_native_session_lock_files': lock_updates, + 'verified_recovery_files': recovery_updates, 'original_session_lock_hex':original_lock.hex() if original_lock is not None else None, 'world_added': sorted(set(after) - set(before)), 'world_removed': sorted(set(before) - set(after)), 'world_changed': sorted(name for name in set(before) & set(after) if before[name] != after[name]), 'sealed_source_unchanged': source_after == sealed, - 'original_forge_config': original_config.decode('utf-8', errors='replace') if original_config else None, - 'resulting_forge_config': config.read_text(encoding='utf-8') if config.is_file() else None, } (home / 'comparison.json').write_text(json.dumps(comparison, indent=2) + '\n', encoding='utf-8') if before != after or source_after != sealed: raise RuntimeError('A failed preflight changed source world data: ' + str(home / 'comparison.json')) report['cases'][case] = {'exit': result.returncode, 'source_unchanged': True, 'world_data_unchanged': True, 'world_loading_started': False, - 'stock_forge_config_created': [name for name in platform_additions if name not in before], - 'stock_forge_config_updated': [name for name in platform_additions if name in before]} + 'stock_forge_config_created': [], 'stock_forge_config_updated': []} report['cases'][case]['native_session_lock_updates']=lock_updates + report['cases'][case]['verified_recovery_files']=recovery_updates if mutation:report['cases'][case]['fixture_mutation']=mutation print('UPGRADE_ABORT_PASS', case, flush=True) (root / 'result.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8')