diff --git a/.github/release119.json b/.github/release119.json new file mode 100644 index 0000000..178568a --- /dev/null +++ b/.github/release119.json @@ -0,0 +1,16 @@ +{ + "version": "2.4.10.119041", + "minecraft": "1.19.4", + "forge": "45.4.5", + "local_qualification_complete": true, + "qualification_integrity_status": "verified", + "manual_accepted": true, + "files": { + "CHANGELOG.txt": "8DA85041587BCA549FCBEC71C365A2E7978CBDA28F6B2607001A02803128F773", + "LootableBodies-2.4.10.119041-javadoc.jar": "9E4D8D968A22E6DD3483532C07A221B5558BC728AF8A6D01CC87CACF001A4ECD", + "LootableBodies-2.4.10.119041-sources.jar": "EA61261C2E07066D56C6E8E588EDCDF2D1F983F0C51A4106DF523AB97064BBF5", + "LootableBodies-2.4.10.119041.jar": "6341FD1976BC195269F61C58885530CCDA0557873FB300E01EEAD657B5975014", + "LootableBodies-2.4.10.119041.pom": "16F2729DB3D499A22C84FDE9B863AA4A18CFD3BB61611EDC051FD4A70E3445AE", + "SHA256SUMS": "4F486F56B58ECD1C4655E8326398C695995C7A5054793B4BADC5CE317E2DC528" + } +} diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index ad9fc4f..794f71b 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -69,13 +69,14 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.15.2': ('115021', 'master-1.15.2', '31.2.57'), '1.16.5': ('116051', 'master-1.16.5', '36.2.42'), '1.17.1': ('117011', 'master-1.17.1', '37.1.1'), - '1.18.2': ('118021', 'master-1.18.2', '40.3.12')}[minecraft] + '1.18.2': ('118021', 'master-1.18.2', '40.3.12'), + '1.19.4': ('119041', 'master-1.19.4', '45.4.5')}[minecraft] version = '2.4.2.' + suffix files={name:'C'*64 for name in ['main.jar','sources.jar','javadoc.jar','release.pom','CHANGELOG.txt','SHA256SUMS']} receipt={'schema':1,'source':SHA,'version':version,'files':files,'evidence_sha256':'F'*64,'qualified_locally':True,'manual_accepted':True} if local_receipt is not None:receipt=local_receipt values = dict(minecraft_version=minecraft, forge_version=forge, loader_name='forge', loader_code='1', - java_version='17' if minecraft=='1.18.2' else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='17.0.20.1+1' if minecraft=='1.18.2' else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='17', mod_id='lootablebodies', + java_version='17' if minecraft in ('1.18.2','1.19.4') else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='17.0.20.1+1' if minecraft in ('1.18.2','1.19.4') else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='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: @@ -85,13 +86,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=='1.18.2' and names is None:checks=CORE118 + if minecraft in ('1.18.2','1.19.4') 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']): - code='118' if minecraft=='1.18.2' else '117' + if any('/actions/workflows/native'+code+'.yml/' in args[-1] for code in ['117','118','119']): + code={'1.17.1':'117','1.18.2':'118','1.19.4':'119'}[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]}) @@ -111,12 +112,12 @@ def existing(args, **options): 'MINECRAFT_VERSION': minecraft, 'SOURCE_BRANCH': branch, 'GITHUB_OUTPUT': str(Path(directory) / 'output'), 'LOCAL_QUALIFICATION':json.dumps(receipt) if receipt else ''}), \ - patch('pathlib.Path.read_text', side_effect=['\n'.join(k+'='+v for k,v in values.items()),json.dumps({'files':files,'local_qualification_complete':True,'qualification_integrity_status':'verified'})]), \ + patch('pathlib.Path.read_text', side_effect=['\n'.join(k+'='+v for k,v in values.items()),json.dumps({'files':files,'local_qualification_complete':True,'qualification_integrity_status':'verified','manual_accepted':True})]), \ patch('subprocess.check_output', side_effect=command), patch('subprocess.run', side_effect=existing): exec(compile(code, 'release-preflight', 'exec'), {}) - def test_future_119_candidate_cannot_dispatch_publication(self): - code, values = self.select('2.4.10.119041') + def test_future_120_candidate_cannot_dispatch_publication(self): + code, values = self.select('2.4.11.120011') self.assertNotEqual(0, code) self.assertEqual({}, values) @@ -315,7 +316,25 @@ def test_118_setup_java_notation_keeps_the_qualified_runtime(self): pins=[line for line in text.splitlines() if 'java-version:' in line and '17.0.20' in line] self.assertEqual(4,len(pins)) self.assertTrue(all('17.0.20+101' in line and '17.0.20.1+1' not in line for line in pins)) - self.assertIn("'17.0.20.1+1' if native18",script('Validate identity, exact commit and successful checks')) + self.assertIn("'17.0.20.1+1' if archive_only",script('Validate identity, exact commit and successful checks')) + + def test_119_requires_manual_acceptance_and_exact_artifacts(self): + code, values = self.select('2.4.10.119041') + self.assertEqual(0, code) + self.assertEqual('master-1.19.4', values['source_branch']) + self.validate('1.19.4') + for missing in CORE118: + with self.subTest(missing=missing), self.assertRaises(SystemExit): + self.validate('1.19.4', names=[name for name in CORE118 if name != missing]) + for kwargs in [{'audit_success':False}, {'local_receipt':{}}, + {'qualification':{'path':'.github/workflows/native118.yml'}}, + {'mutation':{'java_version':'8'}}]: + with self.subTest(kwargs=kwargs), self.assertRaises(SystemExit): + self.validate('1.19.4', **kwargs) + step = WORKFLOW.read_text().split('- name: Verify all six native119 bytes before publication', 1)[1].split(' - name:', 1)[0] + self.assertIn('python3 -B tools/native119_build.py --bundle build/release/bundle', step) + self.assertNotIn('gradlew', step) + self.assertIn("minecraft_version == '1.19.4' && 'Java 17'", WORKFLOW.read_text()) def test_identity_changes_are_rejected(self): for mutation in [{'mod_id': 'other'}, {'minecraft_version': '1.10.2'}, {'mod_license': 'CC0-1.0'}]: diff --git a/.github/workflows/artifact-audit-1.18.yml b/.github/workflows/artifact-audit-1.19.yml similarity index 78% rename from .github/workflows/artifact-audit-1.18.yml rename to .github/workflows/artifact-audit-1.19.yml index e14f0b5..81be46b 100644 --- a/.github/workflows/artifact-audit-1.18.yml +++ b/.github/workflows/artifact-audit-1.19.yml @@ -1,8 +1,8 @@ -name: Audit LootableBodies 1.18 artifacts -run-name: Audit 1.18.2 artifacts from ${{ github.event.workflow_run.head_sha }} +name: Audit LootableBodies 1.19 artifacts +run-name: Audit 1.19.4 artifacts from ${{ github.event.workflow_run.head_sha }} on: workflow_run: - workflows: [LootableBodies 1.18 CI] + workflows: [LootableBodies 1.19 CI] types: [completed] permissions: contents: read @@ -12,7 +12,7 @@ jobs: if: >- github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && - github.event.workflow_run.head_branch == 'master-1.18.2' + github.event.workflow_run.head_branch == 'master-1.19.4' runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -21,7 +21,7 @@ jobs: ref: ${{ github.event.workflow_run.head_sha }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: LootableBodies-1.18.2-${{ github.event.workflow_run.head_sha }} + name: LootableBodies-1.19.4-${{ github.event.workflow_run.head_sha }} run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} path: audited @@ -29,11 +29,11 @@ jobs: env: SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} run: | - python3 -B tools/native118_build.py --bundle audited + python3 -B tools/native119_build.py --bundle audited python3 - <<'PY' import json,os from pathlib import Path - accepted=json.loads(Path('.github/release118.json').read_text()) + accepted=json.loads(Path('.github/release119.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 class61/licence/identity/mapping audit. @@ -43,6 +43,6 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: native118-independent-audit-${{ github.event.workflow_run.head_sha }} + name: native119-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 904436c..28f6b84 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-1.18.2 + - master-1.19.4 - 'feature/**' pull_request: branches: - - master-1.18.2 + - master-1.19.4 schedule: - cron: '43 7 * * 4' @@ -17,7 +17,7 @@ permissions: security-events: write concurrency: - group: lootablebodies-1.18-codeql-${{ github.ref }} + group: lootablebodies-1.19-codeql-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 7af606f..e8f824f 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: release_version: - description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011 or 118021. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011, 118021 or 119041. required: true type: string curseforge_release_level: @@ -20,7 +20,7 @@ on: default: false type: boolean local_qualification: - description: Hash-bound local qualification receipt, required for the 1.17.1 and 1.18.2 releases. + description: Hash-bound local qualification receipt, required for the 1.17.1, 1.18.2 and 1.19.4 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)$ ]] + [[ "$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)$ ]] 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 ;; @@ -65,6 +65,7 @@ jobs: *.116051) minecraft_version=1.16.5; source_branch=master-1.16.5 ;; *.117011) minecraft_version=1.17.1; source_branch=master-1.17.1 ;; *.118021) minecraft_version=1.18.2; source_branch=master-1.18.2 ;; + *.119041) minecraft_version=1.19.4; source_branch=master-1.19.4 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -98,16 +99,17 @@ jobs: '1.15.2': ('master-1.15.2', '31.2.57', '115021'), '1.16.5': ('master-1.16.5', '36.2.42', '116051'), '1.17.1': ('master-1.17.1', '37.1.1', '117011'), - '1.18.2': ('master-1.18.2', '40.3.12', '118021')} + '1.18.2': ('master-1.18.2', '40.3.12', '118021'), + '1.19.4': ('master-1.19.4', '45.4.5', '119041')} 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' - native18 = minecraft == '1.18.2' - locally_qualified = native17 or native18 + archive_only = minecraft in ('1.18.2', '1.19.4') + locally_qualified = native17 or archive_only expected = {'minecraft_version': minecraft, 'forge_version': forge, - 'loader_name': 'forge', 'loader_code': '1', 'java_version': '17' if native18 else '16' if native17 else '8', - 'java_toolchain_version': '17.0.20.1+1' if native18 else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '17', + 'loader_name': 'forge', 'loader_code': '1', 'java_version': '17' if archive_only else '16' if native17 else '8', + 'java_toolchain_version': '17.0.20.1+1' if archive_only else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '17', 'mod_id': 'lootablebodies', 'mod_artifact': 'LootableBodies', 'mod_group': 'zone.moddev.mc.lootablebodies', 'mod_license': 'LGPL-2.1-only', 'curseforge_project_id': '240968'} @@ -137,7 +139,7 @@ jobs: qualification_run = '' if locally_qualified: required = {'Build, test, and audit', 'Analyze Java', 'Validation', 'Validate release workflow'} - if native18: + if archive_only: required = {'Check hosted work policy', 'Build and audit locally qualified archives', 'Analyze Java', 'Validation', 'Validate release workflow'} # The authorised local controller rehashes actual gameplay proof # before dispatch. Hosted CI verifies its exact source/artifact @@ -147,11 +149,13 @@ 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='118' if native18 else '117' + target_code={'1.17.1':'117','1.18.2':'118','1.19.4':'119'}[minecraft] metadata=json.loads(Path(f'.github/release{target_code}.json').read_text()) accepted=metadata['files'] - if native18 and (metadata.get('local_qualification_complete') is not True or metadata.get('qualification_integrity_status')!='verified'): - raise SystemExit('The local 1.18.2 qualification declaration is incomplete') + if archive_only and (metadata.get('local_qualification_complete') is not True or metadata.get('qualification_integrity_status')!='verified'): + raise SystemExit(f'The local {minecraft} qualification declaration is incomplete') + if minecraft == '1.19.4' and metadata.get('manual_accepted') is not True: + raise SystemExit('The local 1.19.4 candidate has not been accepted') if (not isinstance(local,dict) or set(local)!=keys or type(local['schema']) is not int or local['schema']!=1 or local['source']!=sha or local['version']!=version or local['files']!=accepted or local['qualified_locally'] is not True or local['manual_accepted'] is not True @@ -169,7 +173,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'): + 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'): 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'] @@ -203,27 +207,27 @@ jobs: with: ref: ${{ needs.preflight.outputs.release_sha }} - name: Install Mavenizer Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' 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' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' 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.17.1' && '16.0.2+7' || '8.0.502+7' }} + 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.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || '17.0.1+12' }} + 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' || '17.0.1+12' }} - name: Pin the production toolchain - if: needs.preflight.outputs.minecraft_version != '1.18.2' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' run: | - if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.18.2' ]; then + if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" elif [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" @@ -232,7 +236,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' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Download the qualified native117 six-file bundle if: needs.preflight.outputs.minecraft_version == '1.17.1' @@ -265,8 +269,19 @@ jobs: - name: Verify all six native118 bytes before publication if: needs.preflight.outputs.minecraft_version == '1.18.2' run: python3 -B tools/native118_build.py --bundle build/release/bundle + - name: Download the qualified native119 six-file bundle + if: needs.preflight.outputs.minecraft_version == '1.19.4' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.19.4-${{ needs.preflight.outputs.release_sha }} + run-id: ${{ needs.preflight.outputs.qualification_run }} + github-token: ${{ github.token }} + path: build/release/bundle + - name: Verify all six native119 bytes before publication + if: needs.preflight.outputs.minecraft_version == '1.19.4' + run: python3 -B tools/native119_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' + 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' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ @@ -371,15 +386,15 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + 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.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || '17.0.1+12' }} + 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' || '17.0.1+12' }} - name: Pin the production toolchain run: | - if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.18.2' ]; then + if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" elif [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" @@ -437,7 +452,7 @@ jobs: game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - java: ${{ needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} + java: ${{ needs.preflight.outputs.minecraft_version == '1.19.4' && '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/native118.yml b/.github/workflows/native119.yml similarity index 82% rename from .github/workflows/native118.yml rename to .github/workflows/native119.yml index fba2536..f167dc0 100644 --- a/.github/workflows/native118.yml +++ b/.github/workflows/native119.yml @@ -1,16 +1,16 @@ -name: LootableBodies 1.18 CI +name: LootableBodies 1.19 CI on: push: - branches: [master-1.18.2] + branches: [master-1.19.4] pull_request: - branches: [master-1.18.2] + branches: [master-1.19.4] permissions: contents: read concurrency: - group: native118-${{ github.workflow }}-${{ github.ref }} + group: native119-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: @@ -40,7 +40,7 @@ jobs: - name: Require local qualification before build preparation run: | set -euo pipefail - python3 -B tools/hosted_policy.py --require-qualified-manifest 2>&1 | tee "$RUNNER_TEMP/native118-build.log" + python3 -B tools/hosted_policy.py --require-qualified-manifest 2>&1 | tee "$RUNNER_TEMP/native119-build.log" - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin @@ -58,21 +58,21 @@ jobs: run: | set -euo pipefail chmod +x gradlew - ./gradlew assemble verifyProductionRuntimeClasspath prepareReleaseBundle --no-daemon --no-build-cache --stacktrace 2>&1 | tee -a "$RUNNER_TEMP/native118-build.log" - python3 -B tools/native118_build.py 2>&1 | tee -a "$RUNNER_TEMP/native118-build.log" + ./gradlew assemble verifyProductionRuntimeClasspath prepareReleaseBundle --no-daemon --no-build-cache --stacktrace 2>&1 | tee -a "$RUNNER_TEMP/native119-build.log" + python3 -B tools/native119_build.py 2>&1 | tee -a "$RUNNER_TEMP/native119-build.log" - name: Retain the exact six release files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.18.2-${{ github.sha }} + name: LootableBodies-1.19.4-${{ github.sha }} if-no-files-found: error path: build/release/bundle/* - name: Retain build evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: native118-fast-${{ github.sha }} + name: native119-fast-${{ github.sha }} if-no-files-found: error path: | build/reports/** build/release/bundle/* - ${{ runner.temp }}/native118-build.log + ${{ runner.temp }}/native119-build.log diff --git a/.github/workflows/release-workflow-checks.yml b/.github/workflows/release-workflow-checks.yml index 0589017..79b185f 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.18.2'] + branches: ['feature/**', 'master-1.19.4'] pull_request: - branches: ['master-1.18.2'] + branches: ['master-1.19.4'] 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/native118.yml .github/workflows/artifact-audit-1.18.yml .github/workflows/codeql-analysis.yml + .github/workflows/native119.yml .github/workflows/artifact-audit-1.19.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 cc6cad2..905f4c3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,6 @@ -2.4.9.118021 - Minecraft 1.18.2, Forge +2.4.10.119041 - Minecraft 1.19.4, Forge -Updated for Minecraft 1.18.2. +Updated for Minecraft 1.19.4. 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 f54d434..1f1d8f9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lootable Bodies for Minecraft 1.18.2 +# Lootable Bodies for Minecraft 1.19.4 Your items stay on a corpse when you die. Right-click the body to recover them. -Use Forge 40.3.12 and Java 17. Install `LootableBodies-2.4.9.118021.jar` +Use Forge 45.4.5 and Java 17. Install `LootableBodies-2.4.10.119041.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 and 1.17.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 and 1.18.2 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/build.gradle b/build.gradle index 85d628f..d4e4a67 100644 --- a/build.gradle +++ b/build.gradle @@ -94,14 +94,14 @@ tasks.register('nativeBoundaryTest', Test) { description = 'Runs native conversion, storage and early-read regressions without launching Minecraft.' testClassesDirs = sourceSets.test.output.classesDirs classpath = sourceSets.test.runtimeClasspath - include '**/Native117*Test.class', '**/Native118*Test.class', '**/EarlyWorldPreservationTest.class', '**/HookRegistrationOrderTest.class' + include '**/Native117*Test.class', '**/Native118*Test.class', '**/Native119*Test.class', '**/EarlyWorldPreservationTest.class', '**/HookRegistrationOrderTest.class' } -tasks.register('generateNative118Fixtures', JavaExec) { +tasks.register('generateNative119Fixtures', JavaExec) { group = 'verification' dependsOn 'testClasses' classpath = sourceSets.test.runtimeClasspath - mainClass = 'cyano.lootable.buildtest.Native118FixtureGenerator' - args providers.gradleProperty('nativeFixtureOutput').getOrElse('build/native118-fixture-preparation-01') + mainClass = 'cyano.lootable.buildtest.Native119FixtureGenerator' + args providers.gradleProperty('nativeFixtureOutput').getOrElse('build/native119-fixture-preparation-01') } tasks.withType(JavaExec).configureEach { javaLauncher = productionLauncher } tasks.withType(Javadoc).configureEach { @@ -217,7 +217,7 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2' testRuntimeOnly 'org.openjdk.nashorn:nashorn-core:15.4' - testRuntimeOnly 'net.minecraftforge:coremods:5.2.5' + testRuntimeOnly 'net.minecraftforge:coremods:5.2.4' } tasks.named('processResources', ProcessResources) { diff --git a/gradle.properties b/gradle.properties index 6538a42..a6815c1 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.18.2 -forge_version=40.3.12 +minecraft_version=1.19.4 +forge_version=45.4.5 mapping_channel=official -mapping_version=1.18.2 +mapping_version=1.19.4 # Release identity and dispatcher target metadata. loader_name=forge @@ -25,12 +25,10 @@ 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.9.118021 +mod_version=2.4.10.119041 mod_group=zone.moddev.mc.lootablebodies mod_authors=Cyanobacterium,SkyBlade1978 mod_description=Saves your items on a lootable corpse when you die. mod_url=https://github.com/MinecraftModDevelopmentMods/LootableBodies mod_scm_connection=scm:git:https://github.com/MinecraftModDevelopmentMods/LootableBodies.git mod_scm_developer_connection=scm:git:ssh://git@github.com/MinecraftModDevelopmentMods/LootableBodies.git - -mcp_version=20220404.173914 diff --git a/gradle/assets.gradle b/gradle/assets.gradle index c09a051..b9398b9 100644 --- a/gradle/assets.gradle +++ b/gradle/assets.gradle @@ -1,5 +1,6 @@ // Keep development launches independent of any installed Minecraft instance. ext.minecraftGameHome = layout.projectDirectory.dir('.gradle/minecraft-home') +ext.minecraftAssetIndex = '3' // 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() @@ -9,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/native118-client-inputs/natives'))) + .getOrElse(localMinecraftSetup.getProperty('minecraftNativesDirectory','.gradle/native119-client-inputs/natives'))) tasks.register('prepareMinecraftAssets') { group = 'ide' doLast { @@ -24,10 +25,12 @@ tasks.register('prepareMinecraftAssets') { java.nio.file.Files.move(pending.toPath(),output.toPath(),java.nio.file.StandardCopyOption.REPLACE_EXISTING) } } - File descriptor=new File(root,'1.18.2.json') - download('https://piston-meta.mojang.com/v1/packages/334b33fcba3c9be4b7514624c965256535bd7eba/1.18.2.json','334b33fcba3c9be4b7514624c965256535bd7eba',descriptor) + File descriptor=new File(root,'1.19.4.json') + download('https://piston-meta.mojang.com/v1/packages/4fc30c189872faebae2617aba033cc17fec5899f/1.19.4.json','4fc30c189872faebae2617aba033cc17fec5899f',descriptor) def version=new groovy.json.JsonSlurper().parse(descriptor) - File index=new File(root,'indexes/1.18.json') + if(version.id!='1.19.4' || 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) Set hashes=new groovy.json.JsonSlurper().parse(index).objects.values().collect {it.hash} as Set hashes.each {hash -> diff --git a/gradle/audit.gradle b/gradle/audit.gradle index 87ccdaf..f382fa8 100644 --- a/gradle/audit.gradle +++ b/gradle/audit.gradle @@ -62,19 +62,20 @@ ext.auditReleaseArchives = { File directory -> if (!names.contains(it)) throw new GradleException("Missing production entry ${it}") } String info=zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') - ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.18.2]"'].each { + ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.19.4]"'].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!=9 || pack['forge:resource_pack_format']!=8 || pack['forge:data_pack_format']!=9) - throw new GradleException('Native pack formats must be pack9/resource8/data9') + if(pack.pack_format!=13 || pack['forge:server_data_pack_format']!=12 || + pack.containsKey('forge:resource_pack_format') || pack.containsKey('forge:data_pack_format')) + throw new GradleException('Native pack formats must be resource13/server-data12') def coremods=new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('META-INF/coremods.json')).getText('UTF-8')) if(coremods!=[lootablebodies_preservation:'coremods/early-world-preservation.js']) throw new GradleException('Missing or changed early native save attachment') def atEntry=zip.getEntry('META-INF/accesstransformer.cfg') def atRules=atEntry?zip.getInputStream(atEntry).getText('UTF-8').readLines() .collect {it.split('#',2)[0].trim()}.findAll {!it.isEmpty()}:[] - if(atRules!=['public-f net.minecraft.util.datafix.DataFixers f_14510_', + if(atRules!=['public-f net.minecraft.util.datafix.DataFixers f_216514_', 'public net.minecraft.world.level.block.entity.SkullBlockEntity f_59755_', 'public net.minecraft.world.level.block.entity.SkullBlockEntity f_59756_'] || manifest.getValue('FMLAT')!='accesstransformer.cfg') diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 1d816b1..8077f6d 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -67,8 +67,8 @@ 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.18 client inputs before generating Eclipse launches') - contents=contents.replace('{asset_index}','1.18') + if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native1.19.4 client inputs before generating Eclipse launches') + contents=contents.replace('{asset_index}',minecraftAssetIndex) .replace('{assets_root}',minecraftAssets.asFile.absolutePath) .replace('{natives}',nativeRoot.absolutePath) String eclipseRoot=file('bin/main').absolutePath diff --git a/gradle/native118-writer.init.gradle b/gradle/native118-writer.init.gradle new file mode 100644 index 0000000..9864444 --- /dev/null +++ b/gradle/native118-writer.init.gradle @@ -0,0 +1,26 @@ +// Isolated writer compiles against public18's API and exact Java17 toolchain. +gradle.projectsEvaluated { + def p=gradle.rootProject + File writerRoot=p.file(p.property('historicalWriterSource')) + def classes=p.layout.buildDirectory.dir('native118-seed/classes') + def compile=p.tasks.register('compileNative118Seed',JavaCompile) { + dependsOn 'classes' + source p.fileTree(new File(writerRoot,'src/nativeSeed118/java')) + classpath=p.files(p.sourceSets.main.output,p.sourceSets.main.compileClasspath) + destinationDirectory=classes + options.compilerArgs.add('-proc:none') + } + def jar=p.tasks.register('native118SeedJar',Jar) { + dependsOn compile + archiveFileName='lootablebodies-native118-world-fixtures.jar' + destinationDirectory=p.layout.buildDirectory.dir('native118-seed') + from classes + from new File(writerRoot,'src/nativeSeed118/resources') + } + def packaged=p.renamer.classes(jar) { + map.from p.minecraft.dependency.toSrgFile + libraries.from(p.tasks.named('jar'),p.sourceSets.main.compileClasspath) + output=p.layout.buildDirectory.file('native118-seed/lootablebodies-native118-world-fixtures-reobf.jar') + } + p.tasks.register('prepareNative118WorldFixtures') {dependsOn packaged} +} diff --git a/gradle/release.gradle b/gradle/release.gradle index 79a65f2..8fcdc5d 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.18.2', forge_version: '40.3.12', - mapping_channel: 'official', mapping_version: '1.18.2', + minecraft_version: '1.19.4', forge_version: '45.4.5', + mapping_channel: 'official', mapping_version: '1.19.4', loader_name: 'forge', loader_code: '1', curseforge_project_id: '240968', java_version: '17', java_toolchain_version: '17.0.20.1+1', gradle_java_version: '17', mod_license: 'LGPL-2.1-only', mod_license_name: 'GNU Lesser General Public License, Version 2.1', diff --git a/gradle/runtime.gradle b/gradle/runtime.gradle index d2bf7e1..942162d 100644 --- a/gradle/runtime.gradle +++ b/gradle/runtime.gradle @@ -75,10 +75,10 @@ def prepareOwnedLoaderConfig = { File root -> } } -// Use an actual native40 codec header in each newly owned flat fixture world. +// Use an actual native45 codec header in each newly owned flat fixture world. def prepareOwnedFlatWorld = { File root -> - File header = file('tools/fixtures/native118-flat-level.dat') - if (!header.isFile() || releaseDigest(header) != 'A806A2751860CDB3D8566510D6A714435F36A6182C026825943BCC24838AC031') + File header = file('tools/fixtures/native119-flat-level.dat') + if (!header.isFile() || releaseDigest(header) != '9792ED9B0BF8A00ACB6BFFAEB50DC8B9C462CD9B6C96A9E6ED5CA65CFD534A55') 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/src/controlTest/java/buildcontrol/ForgeControl.java b/src/controlTest/java/buildcontrol/ForgeControl.java index 4eed2bf..672390e 100644 --- a/src/controlTest/java/buildcontrol/ForgeControl.java +++ b/src/controlTest/java/buildcontrol/ForgeControl.java @@ -31,7 +31,7 @@ private void loadComplete(net.minecraftforge.fml.event.lifecycle.FMLLoadComplete if(++checkTicks==1) { p.setGameMode(net.minecraft.world.level.GameType.SURVIVAL); double x=checkDeaths%2==0?96.5:-.4,z=checkDeaths%2==0?96.5:-.4; - for(int a=-6;a<=6;a++)for(int b=-6;b<=6;b++)p.level.setBlockAndUpdate(new net.minecraft.core.BlockPos(x+a,80,z+b),net.minecraft.world.level.block.Blocks.OBSIDIAN.defaultBlockState()); + for(int a=-6;a<=6;a++)for(int b=-6;b<=6;b++)p.level.setBlockAndUpdate(net.minecraft.core.BlockPos.containing(x+a,80,z+b),net.minecraft.world.level.block.Blocks.OBSIDIAN.defaultBlockState()); p.setRespawnPosition(p.level.dimension(),new net.minecraft.core.BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),0F,true,false); p.connection.teleport(x,81,z,0,0); } @@ -92,7 +92,7 @@ private void delayNativePackets(net.minecraft.client.Minecraft mc) { private net.minecraft.client.multiplayer.ServerStatusPinger pinger; private net.minecraft.client.multiplayer.ServerData destination; private void tcp(net.minecraft.client.Minecraft mc) { - mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30; + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30); if(++wait>1600)throw new AssertionError("Native Forge control login timed out"); if(stage==1 && wait%40==0 && mc.screen instanceof net.minecraft.client.gui.screens.ConnectScreen) { try { @@ -136,14 +136,12 @@ private void finish(net.minecraft.client.Minecraft mc) { System.out.println("FORGE_PLATFORM_CONTROL_PASS");mc.stop();stage=3; } private void createWorld(net.minecraft.client.Minecraft client) { - net.minecraft.core.RegistryAccess.Frozen registries=net.minecraft.core.RegistryAccess.BUILTIN.get(); - net.minecraft.core.Registry types=registries.registryOrThrow(net.minecraft.core.Registry.DIMENSION_TYPE_REGISTRY); - net.minecraft.core.Registry biomes=registries.registryOrThrow(net.minecraft.core.Registry.BIOME_REGISTRY); - net.minecraft.world.level.levelgen.WorldGenSettings settings=new net.minecraft.world.level.levelgen.WorldGenSettings(4267321L,false,false, - net.minecraft.world.level.levelgen.WorldGenSettings.withOverworld(types, - net.minecraft.world.level.dimension.DimensionType.defaultDimensions(registries,4267321L), - new net.minecraft.world.level.levelgen.FlatLevelSource(registries.registryOrThrow(net.minecraft.core.Registry.STRUCTURE_SET_REGISTRY),net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings.getDefault(biomes,registries.registryOrThrow(net.minecraft.core.Registry.STRUCTURE_SET_REGISTRY))))); - client.createLevel("platform-fixture",new net.minecraft.world.level.LevelSettings("Platform fixture",net.minecraft.world.level.GameType.CREATIVE,false,net.minecraft.world.Difficulty.NORMAL,false,new net.minecraft.world.level.GameRules(),net.minecraft.world.level.DataPackConfig.DEFAULT),registries,settings); + client.createWorldOpenFlows().createFreshLevel("platform-fixture", + new net.minecraft.world.level.LevelSettings("Platform fixture",net.minecraft.world.level.GameType.CREATIVE,false, + net.minecraft.world.Difficulty.NORMAL,false,new net.minecraft.world.level.GameRules(),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), + new net.minecraft.world.level.levelgen.WorldOptions(4267321L,false,false), + access->access.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) + .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions()); } } } diff --git a/src/controlTest/java/buildcontrol/ResourceControl.java b/src/controlTest/java/buildcontrol/ResourceControl.java index 339cf68..d6f5c68 100644 --- a/src/controlTest/java/buildcontrol/ResourceControl.java +++ b/src/controlTest/java/buildcontrol/ResourceControl.java @@ -7,11 +7,14 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ReloadableServerResources; +import net.minecraft.server.WorldLoader; +import net.minecraft.server.packs.resources.CloseableResourceManager; import net.minecraft.server.packs.resources.MultiPackResourceManager; import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.repository.PackRepository; import net.minecraft.server.packs.repository.ServerPacksSource; import net.minecraft.world.level.DataPackConfig; +import net.minecraft.world.level.WorldDataConfiguration; /** Actual packaged native resource loading, without Lootable Bodies or a world. */ final class ResourceControl { @@ -25,31 +28,37 @@ 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("Native40 preparation executor changed"); + if(!(prepare instanceof ForkJoinPool))throw new AssertionError("Native45 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("Native40 worker configuration differs from its actual Util executor"); - System.out.println("NATIVE40_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); - RegistryAccess.Frozen registries=RegistryAccess.BUILTIN.get(); - try(PackRepository packs=new PackRepository(PackType.SERVER_DATA,new ServerPacksSource())) { - MinecraftServer.configurePackRepository(packs,DataPackConfig.DEFAULT,false); + if(limit<1 || limit>255 || workers!=Math.max(1,Math.min(cpus-1,limit)))throw new AssertionError("Native45 worker configuration differs from its actual Util executor"); + System.out.println("NATIVE45_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); + PackRepository packs=new PackRepository(new ServerPacksSource()); + WorldLoader.InitConfig config=new WorldLoader.InitConfig( + new WorldLoader.PackConfig(packs,WorldDataConfiguration.DEFAULT,false,false),Commands.CommandSelection.DEDICATED,2); + { for(int round=1;round<=rounds;round++) { // These are Main's real resources and native executors. No // custom listeners, fake exception, HashSet or future edits. - try(MultiPackResourceManager manager=new MultiPackResourceManager(PackType.SERVER_DATA,packs.openAllSelected())) { - ReloadableServerResources resources=ReloadableServerResources.loadResources(manager,registries, - Commands.CommandSelection.DEDICATED,2,prepare,Runnable::run).get(12,TimeUnit.SECONDS); - if(resources.getRecipeManager().getRecipes().isEmpty())throw new AssertionError("No native recipes loaded"); + try(Prepared resources=Util.blockUntilDone(apply->WorldLoader.load(config, + context->new WorldLoader.DataLoadOutput<>(Boolean.TRUE,context.datapackDimensions()), + (manager,reloaded,registries,cookie)->new Prepared(manager,reloaded),prepare,apply) + .orTimeout(12,TimeUnit.SECONDS)) + .get(12,TimeUnit.SECONDS)) { + if(resources.reloaded().getRecipeManager().getRecipes().isEmpty())throw new AssertionError("No native recipes loaded"); } - if(round%10==0)System.out.println("NATIVE40_RESOURCE_RELOAD_COMPLETE="+round); + if(round%10==0)System.out.println("NATIVE45_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("NATIVE40_RESOURCE_CONTROL_PASS rounds="+rounds); + System.out.println("NATIVE45_RESOURCE_CONTROL_PASS rounds="+rounds); System.out.flush();System.exit(0); } catch(Throwable failure) { failure.printStackTrace();System.out.flush();System.exit(2); } } + private record Prepared(CloseableResourceManager manager,ReloadableServerResources reloaded) implements AutoCloseable { + public void close(){manager.close();} + } } diff --git a/src/controlTest/resources/META-INF/mods.toml b/src/controlTest/resources/META-INF/mods.toml index adf81ae..ccaaaa4 100644 --- a/src/controlTest/resources/META-INF/mods.toml +++ b/src/controlTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[40,)" +loaderVersion="[45,)" license="LGPL-2.1-only" [[mods]] modId="forgeplatformcontrol" diff --git a/src/controlTest/resources/pack.mcmeta b/src/controlTest/resources/pack.mcmeta index 4202da0..ae97e7a 100644 --- a/src/controlTest/resources/pack.mcmeta +++ b/src/controlTest/resources/pack.mcmeta @@ -1 +1 @@ -{"pack":{"pack_format":9,"forge:resource_pack_format":8,"forge:data_pack_format":9,"description":"Disposable Forge control"}} +{"pack":{"pack_format":13,"forge:server_data_pack_format":12,"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 877673f..8f5f7f5 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java @@ -31,10 +31,10 @@ public final class ClientChecks { private final VisibilitySkinClientChecks visibilitySkins = new VisibilitySkinClientChecks(); private final ConfigClientChecks configScreen = new ConfigClientChecks(); private final NamelessCorpseClientChecks namelessNames = new NamelessCorpseClientChecks(); - @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseClickedEvent.Pre event) { + @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre event) { event.setCanceled(true); } - @SubscribeEvent public void keyboard(net.minecraftforge.client.event.ScreenEvent.KeyboardKeyPressedEvent.Pre event) { + @SubscribeEvent public void keyboard(net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre event) { event.setCanceled(true); } @SubscribeEvent public void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Color event) { @@ -67,8 +67,8 @@ public void register() { if(Boolean.getBoolean("lootablebodies.issueClientCheck")) {reportedIssues.tick(mc);return;} if(stage==0 && mc.screen instanceof TitleScreen) { mc.options.pauseOnLostFocus=false; - mc.options.framerateLimit=30; - mc.options.renderDistance=4; + mc.options.framerateLimit().set(30); + mc.options.renderDistance().set(4); if(Boolean.getBoolean("lootablebodies.clientResumeDrops")) { RuntimeChecks.require(!Boolean.getBoolean("lootablebodies.clientStopBeforeDrops") && new File(mc.gameDirectory,"client-connected.pass").isFile(),"Missing connected phase for graphical continuation"); @@ -78,7 +78,7 @@ public void register() { // RuntimeChecks.run in the original continuous process. cyano.lootable.LootableBodies.allowCorpseDecay=false; cyano.lootable.LootableBodies.ticksPerItemDecay=-1; - stage=40; mc.loadLevel("build-fixture"); + stage=40; mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); } else { stage=1; FixtureApi.ClientWorlds.create(mc,"build-fixture","Build fixture",net.minecraft.world.level.GameType.CREATIVE,false); @@ -105,7 +105,7 @@ public void register() { mc.player.closeContainer(); stage=4; FixtureConnections.disconnect(mc); } else if(stage==4 && FixtureConnections.stopped(mc)) { - stage=5; mc.loadLevel("build-fixture"); + stage=5; mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); } else if(stage==5 && mc.player!=null && mc.level!=null && mc.getSingleplayerServer()!=null) { stage=6; pending=mc.getSingleplayerServer().submitAsync(() -> { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java index a4913a1..43901c7 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java @@ -34,7 +34,7 @@ void tick(Minecraft mc)throws Exception { if(++frames<20)return;frames=0; if(report==null)report=new File(mc.gameDirectory,"config-checks.txt"); if(stage==0 && mc.screen instanceof TitleScreen) { - mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); if(Boolean.getBoolean("lootablebodies.configUiReload")) { check(LootableBodies.corpseHP==31 && LootableBodies.corpseDecayTime==200 && LootableBodies.ticksPerItemDecay==0,"pending_rules_apply_after_process_restart"); check(!LootableBodies.useLocalSkin && LootableBodies.displayNameTag,"appearance_survives_restart");new File(mc.gameDirectory,"config-restart.pass").createNewFile();finish(mc);stage=5;return; @@ -88,7 +88,7 @@ private void openMods(Minecraft mc)throws Exception { private void openConfig(Minecraft mc)throws Exception {click(mods,20);check(mc.screen instanceof CorpseConfigGui,"mods_button_opens_regular_config");root=(CorpseConfigGui)mc.screen;} private static Object get(Object target,String name)throws Exception {for(Class type=target.getClass();type!=null;type=type.getSuperclass())try{Field f=type.getDeclaredField(name);f.setAccessible(true);return f.get(target);}catch(NoSuchFieldException absent){}throw new NoSuchFieldException(name);} private static Button button(Screen screen,int id)throws Exception {if(screen instanceof ModListScreen && id==20)return (Button)get(screen,"configButton");for(Object value:screen.children())if(value instanceof CorpseConfigGui.SettingButton && ((CorpseConfigGui.SettingButton)value).id==id)return (CorpseConfigGui.SettingButton)value;throw new AssertionError("Missing GUI button "+id);} - private static void click(Screen screen,int id)throws Exception {Button button=button(screen,id);RuntimeChecks.require(button.active,"Disabled GUI button "+id);button.onClick(button.x+1,button.y+1);} + private static void click(Screen screen,int id)throws Exception {Button button=button(screen,id);RuntimeChecks.require(button.active,"Disabled GUI button "+id);button.onClick(button.getX()+1,button.getY()+1);} @SuppressWarnings("unchecked") private static EditBox field(CorpseConfigGui screen,String key)throws Exception { for(int attempt=0;attempt<10;attempt++){EditBox field=((Map)get(screen,"fields")).get(key);if(field!=null)return field;int page=(Integer)get(screen,"page");try{click(screen,4);}catch(AssertionError end){while((Integer)get(screen,"page")>0)click(screen,3);}if(page==(Integer)get(screen,"page") && attempt>0)break;} throw new AssertionError("Missing config text field "+key); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java index 2e764f5..ed578dd 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java @@ -75,7 +75,7 @@ boolean tick(Minecraft mc) throws Exception { screenshot(mc,"connected-corpse-after-shift-recovery.png"); quit(mc); next(7); } else if(stage==7 && FixtureConnections.stopped(mc)) { - mc.loadLevel("build-fixture"); next(8); + mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); next(8); } else if(stage==8 && ready(mc) && frames>30) { schedule(mc,() -> { verifyReload(mc,"connected-after-recovery","connected-player-after-recovery"); @@ -106,7 +106,7 @@ boolean tick(Minecraft mc) throws Exception { screenshot(mc,"connected-corpse-after-normal-recovery.png"); quit(mc); next(13); } else if(stage==13 && FixtureConnections.stopped(mc)) { - mc.loadLevel("build-fixture"); next(14); + mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture"); next(14); } else if(stage==14 && ready(mc) && frames>30) { schedule(mc,() -> { verifyReload(mc,"connected-final","connected-player-final"); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java index 03ead60..5833d28 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java @@ -37,7 +37,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.GameType; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.event.entity.item.ItemTossEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -214,7 +214,7 @@ private static void check(String name,Check test) { Set before=new HashSet<>(FixtureApi.entities(world)); try {test.run();results.add("PASS "+name);System.out.println("CONTAINER_CHECK_PASS "+name);} catch(Throwable error){failures++;results.add("FAIL "+name+": "+error);System.out.println("CONTAINER_CHECK_FAIL "+name+": "+error);error.printStackTrace();} - finally{for(Entity entity:new ArrayList(FixtureApi.entities(world)))if(!before.contains(entity))world.removeEntity(entity);} + finally{for(Entity entity:new ArrayList(FixtureApi.entities(world)))if(!before.contains(entity))entity.discard();} } private static ServerPlayer player() { ServerPlayer p=new ServerPlayer(server,world, @@ -350,7 +350,7 @@ private static void fullCorpse() throws Exception { } public static final class RejectedDrops { boolean join,toss; - @SubscribeEvent public void join(EntityJoinWorldEvent e){if(join && e.getEntity() instanceof ItemEntity)e.setCanceled(true);} + @SubscribeEvent public void join(EntityJoinLevelEvent e){if(join && e.getEntity() instanceof ItemEntity)e.setCanceled(true);} @SubscribeEvent public void toss(ItemTossEvent e){if(toss)e.setCanceled(true);} } private static void displacement(boolean rejectJoin,boolean rejectToss) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java index af09b9d..692682d 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java @@ -86,7 +86,7 @@ else if(stage==19 && frames>20){ CompoundTag expected=new CompoundTag();expected.put("Inventory",savedPlayer.copy());try(FileOutputStream out=new FileOutputStream(new File(folder(mc),"drop-creative-player.nbt"))){NbtIo.writeCompressed(expected,out);} mc.getSingleplayerServer().getPlayerList().saveAll();mc.getSingleplayerServer().saveAllChunks(false,true,false);});next(20); } else if(stage==20 && done()){quit(mc);next(21);} - else if(stage==21 && FixtureConnections.stopped(mc)){mc.loadLevel("build-fixture");next(22);} + else if(stage==21 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture");next(22);} else if(stage==22 && ready(mc) && frames>20){schedule(mc,()->{ RuntimeChecks.awaitChunk(player(mc).getLevel(),2,0);body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"drop-creative-final",body(mc)); RuntimeChecks.require(savedPlayer.equals(player(mc).getInventory().save(new ListTag())),"Creative player inventory changed on disk reload");player(mc).connection.teleport(40.5,80,7.5,0,0); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java index 7e66155..44a3d50 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java @@ -52,8 +52,8 @@ static void registerPotion(){ // Forge 1.12 assigns IDs through registry events. Fill the isolated test // registry through its public API so unsigned-byte coverage still uses 200. for(int i=0;MobEffect.byId(199)==null && i<200;i++) - net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}.setRegistryName("lootablebodiesbuildtest:padding_"+i)); - net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(extended.setRegistryName("lootablebodiesbuildtest:extended_effect")); + net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(new net.minecraft.resources.ResourceLocation("lootablebodiesbuildtest:padding_"+i),new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}); + net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(new net.minecraft.resources.ResourceLocation("lootablebodiesbuildtest:extended_effect"),extended); RuntimeChecks.require(MobEffect.getId(extended)==200,"Fixture potion must exercise unsigned byte ID 200"); // Older probes saved a different number of padding effects. Retain their // registry names during upgrades without changing fresh-world ID tests. @@ -61,7 +61,7 @@ static void registerPotion(){ for(int i=0;i<200;i++) { net.minecraft.resources.ResourceLocation name=new net.minecraft.resources.ResourceLocation("lootablebodiesbuildtest:padding_"+i); if(!net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.containsKey(name)) - net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}.setRegistryName(name)); + net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(name,new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}); } } static void run(MinecraftServer server,ServerLevel target,File folder) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java index e95bfcd..c0f3898 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java @@ -38,14 +38,12 @@ static net.minecraft.world.level.storage.PlayerDataStorage playerData(MinecraftS @OnlyIn(Dist.CLIENT) static final class ClientWorlds { static void create(net.minecraft.client.Minecraft client,String folder,String title,net.minecraft.world.level.GameType mode,boolean commands) { - net.minecraft.core.RegistryAccess.Frozen registries=net.minecraft.core.RegistryAccess.BUILTIN.get(); - net.minecraft.core.Registry types=registries.registryOrThrow(net.minecraft.core.Registry.DIMENSION_TYPE_REGISTRY); - net.minecraft.core.Registry biomes=registries.registryOrThrow(net.minecraft.core.Registry.BIOME_REGISTRY); - net.minecraft.world.level.levelgen.WorldGenSettings settings=new net.minecraft.world.level.levelgen.WorldGenSettings(4267321L,false,false, - net.minecraft.world.level.levelgen.WorldGenSettings.withOverworld(types, - net.minecraft.world.level.dimension.DimensionType.defaultDimensions(registries,4267321L), - new net.minecraft.world.level.levelgen.FlatLevelSource(registries.registryOrThrow(net.minecraft.core.Registry.STRUCTURE_SET_REGISTRY),net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings.getDefault(biomes,registries.registryOrThrow(net.minecraft.core.Registry.STRUCTURE_SET_REGISTRY))))); - client.createLevel(folder,new net.minecraft.world.level.LevelSettings(title,mode,false,net.minecraft.world.Difficulty.NORMAL,commands,new net.minecraft.world.level.GameRules(),net.minecraft.world.level.DataPackConfig.DEFAULT),registries,settings); + client.createWorldOpenFlows().createFreshLevel(folder, + new net.minecraft.world.level.LevelSettings(title,mode,false,net.minecraft.world.Difficulty.NORMAL,commands, + new net.minecraft.world.level.GameRules(),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), + new net.minecraft.world.level.levelgen.WorldOptions(4267321L,false,false), + access->access.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) + .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions()); } } static List entities(Level world) { @@ -60,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",2975); + root.putInt("xPos",chunk.getPos().x);root.putInt("zPos",chunk.getPos().z);root.put("sections",new net.minecraft.nbt.ListTag());root.putInt("DataVersion",3337); // 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,2975,2975); + net.minecraft.util.datafix.fixes.References.CHUNK,root,3337,3337); 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 eb1fcc2..a136304 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java @@ -17,13 +17,14 @@ public static void register(net.minecraftforge.eventbus.api.IEventBus bus) { } public static final class RegistryItems { @net.minecraftforge.eventbus.api.SubscribeEvent - public void register(net.minecraftforge.event.RegistryEvent.Register event) { + public void register(net.minecraftforge.registries.RegisterEvent event) { + if(!event.getRegistryKey().equals(net.minecraft.core.registries.Registries.ITEM))return; // Native18 creates an intrusive holder inside Item's constructor; // construction belongs to the same open registration event. keepsake=new Item(new Item.Properties()) { @Override public ICapabilityProvider initCapabilities(ItemStack stack,CompoundTag nbt){return new Stored();} - }.setRegistryName("lootablebodiesbuildtest:keepsake"); - event.getRegistry().register(keepsake); + }; + event.register(net.minecraft.core.registries.Registries.ITEM,new ResourceLocation("lootablebodiesbuildtest:keepsake"),()->keepsake); } } private static final class Stored implements ICapabilitySerializable { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java index 4b1bfb9..2fa35ca 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java @@ -105,7 +105,7 @@ private static void check(String name,Check test){ float hp=LootableBodies.corpseHP;Set before=new HashSet<>(FixtureApi.entities(world)); try{test.run();results.add("PASS "+name);System.out.println("HEALTH_ATTRIBUTE_PASS "+name);} 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))world.removeEntity(e);} + 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 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;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/InventorySafetyChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/InventorySafetyChecks.java index 96db3e4..a733318 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/InventorySafetyChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/InventorySafetyChecks.java @@ -4,7 +4,7 @@ import net.minecraft.world.SimpleContainer; import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.minecraft.server.level.ServerLevel; -import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.Component; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.CompoundTag; @@ -32,7 +32,7 @@ import net.minecraft.world.level.chunk.ChunkSource; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.event.entity.living.*; import net.minecraftforge.eventbus.api.*; @@ -87,7 +87,7 @@ private static void check(String name,Check check) { world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(Boolean.parseBoolean(keep),null); world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).set(Boolean.parseBoolean(mobLoot),null); for(Entity entity:new ArrayList(FixtureApi.entities(world))) - if(!before.contains(entity)) world.removeEntity(entity); + if(!before.contains(entity)) entity.discard(); } } private static ServerPlayer player() { @@ -113,7 +113,7 @@ private static List drops(ServerPlayer player,ItemStack... stacks) { return result; } private static void handle(PlayerDeathEventHandler handler,ServerPlayer player,List drops) { - handler.entityDropEvent(new LivingDropsEvent(player,DamageSource.OUT_OF_WORLD,drops,0,false)); + handler.entityDropEvent(new LivingDropsEvent(player,player.damageSources().outOfWorld(),drops,0,false)); } private static CompoundTag data(EntityLootableBody body) { CompoundTag result=new CompoundTag(); body.addAdditionalSaveData(result); return result; @@ -143,11 +143,11 @@ private static EntityLootableBody newest(Set before) { private static void failedSpawn() { ServerPlayer player=player(); RejectingWorld rejected=new RejectingWorld(world); player.level=rejected; - ItemStack bones=new ItemStack(Items.BONE,9); bones.setHoverName(new net.minecraft.network.chat.TextComponent("Saved bones")); + ItemStack bones=new ItemStack(Items.BONE,9); bones.setHoverName(net.minecraft.network.chat.Component.literal("Saved bones")); List drops=drops(player,bones,item(19,23)); List before=new ArrayList<>(); for(ItemEntity e:drops) before.add(encoded(e.getItem())); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); - handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); + handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().outOfWorld())); handle(handler,player,drops); RuntimeChecks.require(rejected.attempts==1,"Failed spawn branch was not reached"); RuntimeChecks.require(drops.size()==before.size(),"Rejected corpse consumed vanilla death drops"); @@ -155,7 +155,7 @@ private static void failedSpawn() { } public static final class RejectCorpse { int attempts; - @SubscribeEvent public void join(EntityJoinWorldEvent event) { + @SubscribeEvent public void join(EntityJoinLevelEvent event) { if(event.getEntity() instanceof EntityLootableBody) { attempts++; event.setCanceled(true); } } } @@ -164,7 +164,7 @@ private static void cancelledSpawn() { Map expected=new TreeMap<>(); add(expected,player.getInventory().items.get(0));add(expected,player.getInventory().items.get(9)); Set before=new HashSet<>(FixtureApi.entities(world)); RejectCorpse reject=new RejectCorpse(); MinecraftForge.EVENT_BUS.register(reject); - try { player.die(DamageSource.OUT_OF_WORLD); } finally { MinecraftForge.EVENT_BUS.unregister(reject); } + try { player.die(player.damageSources().outOfWorld()); } finally { MinecraftForge.EVENT_BUS.unregister(reject); } RuntimeChecks.require(reject.attempts==1,"Corpse cancellation not exercised"); for(Entity entity:FixtureApi.entities(world)) RuntimeChecks.require(before.contains(entity) || !(entity instanceof EntityLootableBody),"Cancelled corpse spawn bypassed Forge cancellation"); RuntimeChecks.require(expected.equals(dropped()),"Cancelled corpse did not fall back to exact vanilla drops"); @@ -172,10 +172,10 @@ private static void cancelledSpawn() { private static void staleEquipment() { ServerPlayer player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); ItemStack previous=item(0,1); player.setItemSlot(EquipmentSlot.HEAD,previous); - handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); + handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().outOfWorld())); handle(handler,player,drops(player,previous)); ItemStack current=item(1,1); player.setItemSlot(EquipmentSlot.HEAD,current); - handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); + handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().outOfWorld())); Map expected=new TreeMap<>(); add(expected,current);add(expected,previous); Set before=new HashSet<>(FixtureApi.entities(world)); handle(handler,player,drops(player,current,previous)); @@ -192,19 +192,19 @@ private static void staleEquipment() { private static void cacheLifecycle() throws Exception { ServerPlayer player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); player.setItemSlot(EquipmentSlot.HEAD,item(0,1)); - handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); + handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().outOfWorld())); handle(handler,player,drops(player,player.getItemBySlot(EquipmentSlot.HEAD))); RuntimeChecks.require(!cache(handler).containsKey(player),"Equipment cache retains completed death"); world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(true,null); - handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); + handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().outOfWorld())); RuntimeChecks.require(!cache(handler).containsKey(player),"keepInventory death cached equipment"); world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); - LivingDeathEvent canceled=new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD); canceled.setCanceled(true); + LivingDeathEvent canceled=new LivingDeathEvent(player,player.damageSources().outOfWorld()); canceled.setCanceled(true); handler.entityDeathEvent(canceled); RuntimeChecks.require(!cache(handler).containsKey(player),"Cancelled death cached equipment"); - handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); + handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().outOfWorld())); List original=drops(player,item(19,23)); - LivingDropsEvent canceledDrops=new LivingDropsEvent(player,DamageSource.OUT_OF_WORLD,original,0,false); + LivingDropsEvent canceledDrops=new LivingDropsEvent(player,player.damageSources().outOfWorld(),original,0,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"); @@ -212,7 +212,7 @@ private static void cacheLifecycle() throws Exception { public static final class RejectItems { boolean rejectAll; String rejectFixture; - @SubscribeEvent public void join(EntityJoinWorldEvent event) { + @SubscribeEvent public void join(EntityJoinLevelEvent event) { if(event.getEntity() instanceof ItemEntity) { ItemStack stack=((ItemEntity)event.getEntity()).getItem(); if(rejectAll || stack.hasTag() && rejectFixture.equals(stack.getTag().getString("fixture"))) event.setCanceled(true); @@ -365,10 +365,11 @@ static void seedBlockedRelease(ServerLevel target,File folder) throws Exception private static final class RejectingWorld extends Level { private final ServerLevel delegate; int attempts; RejectingWorld(ServerLevel target) { - super((net.minecraft.world.level.storage.WritableLevelData)target.getLevelData(),target.dimension(),target.dimensionTypeRegistration(),()->net.minecraft.util.profiling.InactiveProfiler.INSTANCE,false,false,target.getSeed()); + super((net.minecraft.world.level.storage.WritableLevelData)target.getLevelData(),target.dimension(),target.registryAccess(),target.dimensionTypeRegistration(),()->net.minecraft.util.profiling.InactiveProfiler.INSTANCE,false,false,target.getSeed(),target.getServer().getMaxChainedNeighborUpdates()); delegate=target; } @Override public net.minecraft.core.RegistryAccess registryAccess(){return delegate.registryAccess();} + @Override public net.minecraft.world.flag.FeatureFlagSet enabledFeatures(){return delegate.enabledFeatures();} @Override public float getShade(net.minecraft.core.Direction direction,boolean shade){return delegate.getShade(direction,shade);} @Override public ChunkSource getChunkSource(){return delegate.getChunkSource();} @Override public net.minecraft.core.Holder getUncachedNoiseBiome(int x,int y,int z){return delegate.getUncachedNoiseBiome(x,y,z);} @@ -383,6 +384,8 @@ private static final class RejectingWorld extends Level { @Override public void sendBlockUpdated(net.minecraft.core.BlockPos pos,net.minecraft.world.level.block.state.BlockState oldState,net.minecraft.world.level.block.state.BlockState state,int flags){delegate.sendBlockUpdated(pos,oldState,state,flags);} @Override public void playSound(net.minecraft.world.entity.player.Player player,double x,double y,double z,net.minecraft.sounds.SoundEvent sound,net.minecraft.sounds.SoundSource category,float volume,float pitch){delegate.playSound(player,x,y,z,sound,category,volume,pitch);} @Override public void playSound(net.minecraft.world.entity.player.Player player,Entity entity,net.minecraft.sounds.SoundEvent sound,net.minecraft.sounds.SoundSource category,float volume,float pitch){delegate.playSound(player,entity,sound,category,volume,pitch);} + @Override public void playSeededSound(net.minecraft.world.entity.player.Player player,double x,double y,double z,net.minecraft.core.Holder sound,net.minecraft.sounds.SoundSource category,float volume,float pitch,long seed){delegate.playSeededSound(player,x,y,z,sound,category,volume,pitch,seed);} + @Override public void playSeededSound(net.minecraft.world.entity.player.Player player,Entity entity,net.minecraft.core.Holder sound,net.minecraft.sounds.SoundSource category,float volume,float pitch,long seed){delegate.playSeededSound(player,entity,sound,category,volume,pitch,seed);} @Override public Entity getEntity(int id){return delegate.getEntity(id);} @Override public net.minecraft.world.level.saveddata.maps.MapItemSavedData getMapData(String id){return delegate.getMapData(id);} @Override public void setMapData(String id,net.minecraft.world.level.saveddata.maps.MapItemSavedData data){delegate.setMapData(id,data);} @@ -390,6 +393,7 @@ private static final class RejectingWorld extends Level { @Override public void destroyBlockProgress(int id,net.minecraft.core.BlockPos pos,int progress){delegate.destroyBlockProgress(id,pos,progress);} @Override public void levelEvent(net.minecraft.world.entity.player.Player player,int type,net.minecraft.core.BlockPos pos,int data){delegate.levelEvent(player,type,pos,data);} @Override public void gameEvent(Entity source,net.minecraft.world.level.gameevent.GameEvent event,net.minecraft.core.BlockPos pos){delegate.gameEvent(source,event,pos);} + @Override public void gameEvent(net.minecraft.world.level.gameevent.GameEvent event,net.minecraft.world.phys.Vec3 position,net.minecraft.world.level.gameevent.GameEvent.Context context){delegate.gameEvent(event,position,context);} @Override public java.util.List players(){return delegate.players();} @Override public String gatherChunkSourceStats(){return delegate.gatherChunkSourceStats();} @Override public net.minecraft.world.level.entity.LevelEntityGetter getEntities(){return delegate.getEntities();} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java index 8aa6279..8c1bcc1 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java @@ -27,8 +27,8 @@ final class MultiplayerClientChecks { private boolean finished; private int lastPhase=-1,frames,ticks; void register(){if(role!=null&&!Boolean.getBoolean("lootablebodies.visibility")&&!Boolean.getBoolean("lootablebodies.respawn")){net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);MinecraftForge.EVENT_BUS.register(this);}} - @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseClickedEvent.Pre e){e.setCanceled(true);} - @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyboardKeyPressedEvent.Pre e){e.setCanceled(true);} + @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre e){e.setCanceled(true);} + @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre e){e.setCanceled(true);} @SubscribeEvent public void tick(TickEvent.ClientTickEvent e){ if(e.phase!=TickEvent.Phase.END)return;Minecraft mc=Minecraft.getInstance(); if(mc.isWindowActive())mc.mouseHandler.releaseMouse();net.minecraft.client.KeyMapping.releaseAll(); @@ -38,7 +38,7 @@ final class MultiplayerClientChecks { if(new File(MultiplayerChecks.sync(),"abort").exists())throw new AssertionError("Multiplayer runner abort"); if(FixtureConnections.retry(mc,role))connecting=false; if(!connecting && FixtureConnections.ready(role) && mc.screen instanceof TitleScreen){ - connecting=true;mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + connecting=true;mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); FixtureConnections.connect(mc); } if(mc.level==null || mc.player==null)return; diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java index b7e3dc4..3509b6c 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java @@ -94,7 +94,7 @@ private static void check(String name,Check test) { Set before=new HashSet<>(FixtureApi.entities(world)); try{((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(10000);LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1;test.run();results.add("PASS "+name);System.out.println("NAME_LIFETIME_PASS "+name);} catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("NAME_LIFETIME_FAIL "+name+": "+e);e.printStackTrace();} - finally{((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(clock);LootableBodies.ticksPerItemDecay=rate;LootableBodies.corpseDecayTime=life;LootableBodies.allowCorpseDecay=decay;LootableBodies.decayOnlyWhenEmpty=empty;for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))world.removeEntity(e);} + finally{((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(clock);LootableBodies.ticksPerItemDecay=rate;LootableBodies.corpseDecayTime=life;LootableBodies.allowCorpseDecay=decay;LootableBodies.decayOnlyWhenEmpty=empty;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.setCustomNameTag("Saved Owner");b.setItem(6,NbtRecoveryChecks.item(120,17));CompoundTag n=new CompoundTag();b.saveAsPassenger(n);n.putString("Name","Saved Owner");n.putByte("Vac",(byte)20);return n;} static EntityLootableBody read(CompoundTag n){CompoundTag copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.load(n);RuntimeChecks.require(same(n,copy),"Input NBT mutated");return b;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java index 485ff9a..91599f0 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java @@ -37,7 +37,7 @@ final class NamelessCorpseClientChecks { private static final UUID BODY = UUID.fromString("00000000-0000-4000-8000-000000000119"); private int stage, frames, locale; private Future pending; - private LanguageInfo originalLanguage; + private String originalLanguage; private boolean originalUnicode, originalBidi; private boolean renderedBody; @@ -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")).getInputStream(),StandardCharsets.UTF_8)) { + try(Reader in=new InputStreamReader(mc.getResourceManager().getResource(new ResourceLocation("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(body.getType().getRegistryName()), "Registry/display identity changed"); + RuntimeChecks.require(body != null && new ResourceLocation("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); @@ -97,9 +97,9 @@ private static void assertLocale(Minecraft mc, String code) throws Exception { void tick(Minecraft mc) throws Exception { if(stage==0 && mc.screen instanceof TitleScreen) { - mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); originalLanguage=mc.getLanguageManager().getSelected(); - originalUnicode=mc.options.forceUnicodeFont; originalBidi=mc.font.isBidirectional(); + originalUnicode=mc.options.forceUnicodeFont().get(); originalBidi=mc.font.isBidirectional(); MinecraftForge.EVENT_BUS.register(this); FixtureApi.ClientWorlds.create(mc,"name-fixture","Name fixture",net.minecraft.world.level.GameType.CREATIVE,false);stage=1; } else if(stage==1 && mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null) { @@ -125,7 +125,7 @@ void tick(Minecraft mc) throws Exception { String code=LOCALES[locale]; if(frames++==0) { renderedBody=false; - mc.getLanguageManager().setSelected(new LanguageInfo(code,code,code,false)); + mc.getLanguageManager().setSelected(code); mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager()); // Native16 LanguageMap controls bidirectional text after resource reload. diff --git a/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java index 2420d09..316a592 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java @@ -16,14 +16,15 @@ final class Native118BiomeChecks { private Native118BiomeChecks() { } static void verify(ServerLevel world,CompoundTag record,int wave) { + if(record.contains("Sections",9)) {verifyNative18Sections(world,record,wave);return;} int x=record.getInt("ChunkX"),z=record.getInt("ChunkZ"); CompoundTag root=new CompoundTag(),level=new CompoundTag();root.putInt("DataVersion",2730);root.put("Level",level); level.putInt("xPos",x);level.putInt("zPos",z);level.putString("Status","full");level.put("Sections",new ListTag()); level.putIntArray("Biomes",HistoricalBiomes.nativeValues(record.getIntArray("Data"))); ChunkStorage.injectDatafixingContext(root,world.dimension(),Optional.empty()); - CompoundTag nativeResult=cyano.lootable.upgrade.NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,root,2730,2975); - Registry registry=world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY); - com.mojang.serialization.Codec>> codec=PalettedContainer.codec(registry.asHolderIdMap(),registry.holderByNameCodec(),PalettedContainer.Strategy.SECTION_BIOMES,registry.getHolderOrThrow(Biomes.PLAINS)); + CompoundTag nativeResult=cyano.lootable.upgrade.NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,root,2730,3337); + Registry registry=world.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.BIOME); + com.mojang.serialization.Codec>> codec=PalettedContainer.codecRW(registry.asHolderIdMap(),registry.holderByNameCodec(),PalettedContainer.Strategy.SECTION_BIOMES,registry.getHolderOrThrow(Biomes.PLAINS)); LevelChunk chunk=world.getChunk(x,z);int samples=0; for(net.minecraft.nbt.Tag entry:nativeResult.getList("sections",10)) { CompoundTag section=(CompoundTag)entry;int sectionY=section.getInt("Y"); @@ -37,4 +38,23 @@ static void verify(ServerLevel world,CompoundTag record,int wave) { } RuntimeChecks.require(samples==1024,"Native biome conversion omitted saved quart samples"); } + private static void verifyNative18Sections(ServerLevel world,CompoundTag record,int wave) { + LevelChunk chunk=world.getChunk(record.getInt("ChunkX"),record.getInt("ChunkZ")); + ListTag sections=record.getList("Sections",10);java.util.Set seen=new java.util.HashSet<>();int samples=0; + RuntimeChecks.require(sections.size()==chunk.getSectionsCount(),"Native18 saved biome section coverage changed"); + for(net.minecraft.nbt.Tag entry:sections) { + CompoundTag section=(CompoundTag)entry;int y=section.getInt("Y"); + RuntimeChecks.require(section.contains("Y",3) && seen.add(y) && y>=chunk.getMinSection() && y Dynamic update(DSL.TypeReference type,Dynamic input,int from,in boolean preflight=java.util.Arrays.stream(Thread.currentThread().getStackTrace()).anyMatch( frame->frame.getClassName().equals("cyano.lootable.upgrade.UpgradeJournal") && frame.getMethodName().equals("validateEntityChunks")); if(!preflight || worlds.get()!=0 || starts.get()!=0)throw new AssertionError("Entity conversion missed the pre-world boundary"); - early.incrementAndGet();System.out.println("NATIVE117_PREWORLD_ENTITY_CONVERSION from="+from+" to="+to+" mode="+mode); + early.incrementAndGet();System.out.println("NATIVE119_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("NATIVE117_PREFLIGHT_CONTROL_INSTALLED mode="+mode); + System.out.println("NATIVE119_PREFLIGHT_CONTROL_INSTALLED mode="+mode); })); } - @SubscribeEvent public void loaded(net.minecraftforge.event.world.WorldEvent.Load event){worlds.incrementAndGet();} + @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();} static void verify(MinecraftServer server) throws Exception { RuntimeChecks.require(mode.equals("convert") && early.get()>0,"No actual pre-world entity conversion observed"); @@ -59,7 +59,7 @@ static void verify(MinecraftServer server) throws Exception { && item.hasTag() && item.getTag().getString("keep").equals("slot-"+slot),"Preflight inventory changed at slot"+slot); } CompoundTag saved=new CompoundTag();body.saveAsPassenger(saved); - RuntimeChecks.require(saved.getInt("LootableBodiesDataVersion")==8,"Preflight corpse marker was not advanced"); + RuntimeChecks.require(saved.getInt("LootableBodiesDataVersion")==9,"Preflight corpse marker was not advanced"); ListTag aux=saved.getList("Aux",10);RuntimeChecks.require(aux.size()==5,"Preflight Aux record count changed"); for(int index=0;index<5;index++) { int sourceSlot=index<4?index:47;CompoundTag item=aux.getCompound(index); @@ -73,6 +73,6 @@ static void verify(MinecraftServer server) throws Exception { && ((CompoundTag)value).getCompound("Data").getByte("Slot")==46 && ((CompoundTag)value).getCompound("Data").getCompound("tag").getString("keep").equals("slot-46")),"Raw Source/Data recovery evidence changed"); Files.writeString(Path.of("preflight-conversion.pass"),"48inputEquipmentRecords/Aux/Source/Data/entityStorage conserved\n",StandardOpenOption.CREATE_NEW); - System.out.println("NATIVE117_PACKAGED_PREFLIGHT_CONVERSION_PASS"); + System.out.println("NATIVE119_PACKAGED_PREFLIGHT_CONVERSION_PASS"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java index a3133b3..c7d2556 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java @@ -50,7 +50,7 @@ boolean tick(Minecraft mc) throws Exception { NbtWorldChecks.seed(p.getLevel(),bodyId,COORDINATE,folder(mc),"graphical"); });next(1);} else if(stage==1 && done() && frames>20){quit(mc);next(2);} - else if(stage==2 && FixtureConnections.stopped(mc)){mc.loadLevel("build-fixture");next(3);} + else if(stage==2 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture");next(3);} else if(stage==3 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(COORDINATE+8.5,80,COORDINATE-.5,0,0));next(30);} else if(stage==30 && done() && frames>20){schedule(mc,()->{ RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions==1,"Missing exact first native graphical disk admission"); @@ -68,7 +68,7 @@ else if(stage==6 && frames>25){schedule(mc,()->{ mc.getSingleplayerServer().getPlayerList().saveAll();mc.getSingleplayerServer().saveAllChunks(false,true,false);mark(mc,"malformed_network_recovery_preserves_raw_records"); });next(7);} else if(stage==7 && done()){checkPlayer(mc.player.getInventory());quit(mc);next(8);} - else if(stage==8 && FixtureConnections.stopped(mc)){mc.loadLevel("build-fixture");next(9);} + else if(stage==8 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"build-fixture");next(9);} else if(stage==9 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(COORDINATE+8.5,80,COORDINATE-.5,0,0));next(90);} else if(stage==90 && done() && frames>20){schedule(mc,()->{ RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions==2,"Missing exact second native graphical disk admission"); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java index 4079846..e50a6e1 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java @@ -96,7 +96,7 @@ private static void check(String name,Check test){ Set before=new HashSet<>(FixtureApi.entities(world)); try{test.run();results.add("PASS "+name);System.out.println("NBT_CHECK_PASS "+name);} catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("NBT_CHECK_FAIL "+name+": "+e);e.printStackTrace();} - finally{for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))world.removeEntity(e);} + finally{for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))e.discard();} } static ItemStack item(int index,int count){ItemStack s=RuntimeChecks.stack(index);s.setCount(count);return s;} static CompoundTag entry(int index,int count,int slot){CompoundTag n=item(index,count).save(new CompoundTag());n.putByte("Slot",(byte)slot);return n;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtWorldChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtWorldChecks.java index ad0c454..3ebb257 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtWorldChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtWorldChecks.java @@ -37,7 +37,7 @@ static void verifyGraphicalAdmission(Entity entity){ static void registerItem(){ String phase=System.getProperty("lootablebodies.phase",""); if(phase.startsWith("nbt-return") || phase.startsWith("upgrade-return") || phase.startsWith("upgrade-final")) { - returning=new Item(new Item.Properties()).setRegistryName(new ResourceLocation(RETURNING));net.minecraftforge.registries.ForgeRegistries.ITEMS.register(returning); + returning=new Item(new Item.Properties());net.minecraftforge.registries.ForgeRegistries.ITEMS.register(new ResourceLocation(RETURNING),returning); } } static void run(MinecraftServer server,String phase) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java index 9249c00..dbdf5b2 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java @@ -22,7 +22,7 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayerFactory; import com.mojang.authlib.GameProfile; -import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.event.entity.living.LivingDropsEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -42,13 +42,13 @@ public static void run(MinecraftServer server) throws Exception { net.minecraft.network.FriendlyByteBuf buffer=new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.buffer()); try { EntityLootableBody corpse=new EntityLootableBody(world); - net.minecraft.network.protocol.game.ClientboundAddMobPacket sent=new net.minecraft.network.protocol.game.ClientboundAddMobPacket(corpse); + net.minecraft.network.protocol.game.ClientboundAddEntityPacket sent=new net.minecraft.network.protocol.game.ClientboundAddEntityPacket(corpse); sent.write(buffer); - net.minecraft.network.protocol.game.ClientboundAddMobPacket received=new net.minecraft.network.protocol.game.ClientboundAddMobPacket(buffer); + net.minecraft.network.protocol.game.ClientboundAddEntityPacket received=new net.minecraft.network.protocol.game.ClientboundAddEntityPacket(buffer); require(RespawnPacketTrace.describe(sent).equals(RespawnPacketTrace.describe(received)),"Spawn observer differs before/after native decoding"); require(RespawnPacketTrace.describe(received).contains("mod=lootablebodies "),"Negative control cannot identify corpse spawns"); buffer.clear(); - net.minecraft.network.protocol.game.ClientboundRespawnPacket outgoing=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(world.dimensionTypeRegistration(),world.dimension(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,true); + net.minecraft.network.protocol.game.ClientboundRespawnPacket outgoing=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(world.dimensionTypeRegistration().unwrapKey().orElseThrow(),world.dimension(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,net.minecraft.network.protocol.game.ClientboundRespawnPacket.KEEP_ALL_DATA,java.util.Optional.empty()); outgoing.write(buffer); net.minecraft.network.protocol.game.ClientboundRespawnPacket incoming=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(buffer); require(RespawnPacketTrace.describe(outgoing).equals(RespawnPacketTrace.describe(incoming)),"Respawn observer differs before/after native decoding"); @@ -59,11 +59,11 @@ public static void run(MinecraftServer server) throws Exception { ServerPlayer player=FakePlayerFactory.get(world,new GameProfile(UUID.randomUUID(),"PortFixture")); ArrayList drops=new ArrayList<>(); drops.add(new ItemEntity(world,8,80,8,new ItemStack(Items.DIAMOND,7))); - Object veto=new Object() { @SubscribeEvent public void join(EntityJoinWorldEvent e) { + Object veto=new Object() { @SubscribeEvent public void join(EntityJoinLevelEvent e) { if(e.getEntity() instanceof EntityLootableBody) e.setCanceled(true); }}; MinecraftForge.EVENT_BUS.register(veto); - try { new PlayerDeathEventHandler().entityDropEvent(new LivingDropsEvent(player,DamageSource.GENERIC,drops,0,false)); } + try { new PlayerDeathEventHandler().entityDropEvent(new LivingDropsEvent(player,player.damageSources().generic(),drops,0,false)); } finally { MinecraftForge.EVENT_BUS.unregister(veto); } require(drops.size()==1 && drops.get(0).getItem().getCount()==7,"original drops were discarded"); }); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java index e3ec7f2..3fecf5b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java @@ -6,8 +6,8 @@ import net.minecraft.network.protocol.game.ServerboundContainerClosePacket; import net.minecraft.network.protocol.game.ServerboundContainerClickPacket; import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; -import net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket; -import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.chat.Component; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.CompoundTag; @@ -78,14 +78,14 @@ private static void check(String name, Check action) { finally { for(Entity entity:new ArrayList(FixtureApi.entities(world))) if(!before.contains(entity)) { if(entity instanceof ServerPlayer) server.getPlayerList().remove((ServerPlayer)entity); - else world.removeEntity(entity); + else entity.discard(); } } } private static ItemStack shovel() { ItemStack stack=new ItemStack(Items.DIAMOND_SHOVEL); stack.setTag(RuntimeChecks.stack(300).getTag().copy());stack.setDamageValue(17); - stack.setHoverName(new net.minecraft.network.chat.TextComponent("Issue 5 original shovel")); return stack; + stack.setHoverName(net.minecraft.network.chat.Component.literal("Issue 5 original shovel")); return stack; } static Map inventory(EntityLootableBody body) { Map total=new TreeMap<>(); @@ -138,15 +138,30 @@ private static void click(ServerPlayer p,int slot,int button,ClickType type) thr && RuntimeChecks.sameStack(observed.carried,p.containerMenu.getCarried()),"Native menu correction did not match its authoritative state/cursor"); } finally {wire.release();} } + private static void addFixturePlayer(ServerPlayer player)throws ReflectiveOperationException { + if(!(player.connection instanceof QuietConnection))throw new AssertionError("Quiet fixture bookkeeping received a real connection"); + // Forge40's removed addPlayer method only appended this list. Preserve + // that fixture behavior; real TCP joins use the ordinary native login. + java.util.List players=null; + for(java.lang.reflect.Field field:net.minecraft.server.players.PlayerList.class.getDeclaredFields()) { + if(field.getType()!=java.util.List.class || !field.getGenericType().getTypeName().contains(ServerPlayer.class.getName()))continue; + field.setAccessible(true);Object value=field.get(server.getPlayerList()); + if(value==server.getPlayerList().getPlayers())continue; + if(players!=null)throw new AssertionError("Ambiguous native fixture player list"); + @SuppressWarnings("unchecked") java.util.List found=(java.util.List)value;players=found; + } + if(players==null || players.contains(player))throw new AssertionError("Missing or duplicate native fixture player list entry"); + players.add(player); + RuntimeChecks.require(server.getPlayerList().getPlayers().contains(player),"Fixture player list observation"); + } private static void deathSequence() throws Exception { world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null);RuntimeChecks.awaitChunk(world,2,0); ServerPlayer p=new ServerPlayer(server,world,new GameProfile(UUID.randomUUID(),"IssueFive")); p.connection=new QuietConnection(p);p.setGameMode(GameType.SURVIVAL);p.setPos(32.5,80,0.5); // Vanilla clients need player-list information before a tracked player // spawn, including the brief spawn-point visit during real respawn. - server.getPlayerList().broadcastAll(new net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket( - net.minecraft.network.protocol.game.ClientboundPlayerInfoPacket.Action.ADD_PLAYER,p)); - RuntimeChecks.require(world.addFreshEntity(p),"Fixture player spawn");server.getPlayerList().addPlayer(p); + server.getPlayerList().broadcastAll(net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(java.util.List.of(p))); + RuntimeChecks.require(world.addFreshEntity(p),"Fixture player spawn");addFixturePlayer(p); p.getInventory().selected=4;p.getInventory().items.set(4,shovel());p.getInventory().items.set(9,RuntimeChecks.stack(301)); Map expected=tagged(p); EntityLootableBody first=death(p);RuntimeChecks.require(expected.equals(tagged(first)),"First death lost inventory"); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java index fa2a277..b3912fa 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java @@ -3,7 +3,7 @@ import net.minecraft.client.gui.screens.DeathScreen; import net.minecraft.world.SimpleContainer; import net.minecraft.server.level.ServerLevel; -import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.Component; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.CompoundTag; @@ -45,13 +45,13 @@ private interface Action {void run() throws Exception;} static ItemStack original() { ItemStack s=new ItemStack(Items.DIAMOND_SHOVEL); s.setTag(RuntimeChecks.stack(300).getTag().copy());s.setDamageValue(17); - s.setHoverName(new net.minecraft.network.chat.TextComponent("Issue 5 original shovel"));return s; + s.setHoverName(net.minecraft.network.chat.Component.literal("Issue 5 original shovel"));return s; } void tick(Minecraft mc) throws Exception { frames++; if(stage==0 && mc.screen instanceof TitleScreen) { report=new File(mc.gameDirectory,"issue5-client-checks.txt");RuntimeChecks.require(!report.exists(),"Stale issue5 report"); - mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); FixtureApi.ClientWorlds.create(mc,"issue-fixture","Issue fixture",net.minecraft.world.level.GameType.SURVIVAL,true);next(1); } else if(stage==1 && ready(mc)) { schedule(mc,()->{ @@ -118,7 +118,7 @@ void tick(Minecraft mc) throws Exception { mc.player.closeContainer();schedule(mc,()->save(mc));next(12); } else if(stage==12 && done()) {quit(mc);next(13); } else if(stage==13 && FixtureConnections.stopped(mc)) { - mc.loadLevel("issue-fixture");next(14); + mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"issue-fixture");next(14); } else if(stage==14 && ready(mc) && frames>25) { schedule(mc,()->{verifyDisk(mc);verifyOriginals(body(mc));mark("first_disk_reload_complete_corpse_inventory");});next(15); } else if(stage==15 && done() && open(mc,secondId)) {next(16); @@ -138,7 +138,7 @@ void tick(Minecraft mc) throws Exception { });next(18); } else if(stage==18 && done()) {quit(mc);next(19); } else if(stage==19 && FixtureConnections.stopped(mc)) { - mc.loadLevel("issue-fixture");next(20); + mc.createWorldOpenFlows().loadLevel(new net.minecraft.client.gui.screens.TitleScreen(),"issue-fixture");next(20); } else if(stage==20 && ready(mc) && frames>25) { schedule(mc,()->{verifyDisk(mc);mark("second_disk_reload_complete_player_and_corpse_inventory");});next(21); } else if(stage==21 && done()) { @@ -196,7 +196,7 @@ private boolean open(Minecraft mc,UUID id) throws Exception { // platform. Give this disposable fixture a stable place // beside the actual corpse before sending normal clicks. for(int dx=-1;dx<=1;dx++)for(int dz=-1;dz<=1;dz++) - p.level.setBlockAndUpdate(new BlockPos(x+dx,y-1,z+dz),Blocks.STONE.defaultBlockState()); + p.level.setBlockAndUpdate(BlockPos.containing(x+dx,y-1,z+dz),Blocks.STONE.defaultBlockState()); p.setDeltaMovement(0,0,0); p.connection.teleport(x,y,z,0,0); System.out.println("ISSUE5_VIEWER_APPROACH corpse="+id); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java index 63b4066..104d027 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)); + for(int tick=0;tick<1000;tick++)fixture.tick(new TickEvent.ServerTickEvent(TickEvent.Phase.END,()->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"); @@ -63,7 +63,7 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { static File file(String name){return new File(System.getProperty("lootablebodies.multiplayer.sync"),name);} private ServerPlayer player(String role){return server.getPlayerList().getPlayerByName("Container"+role);} private boolean ack(String role,int p){return file(role+".case-"+index+"-"+p).isFile();} - private void trace(ServerPlayer p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.getConnection(),file(p.getGameProfile().getName()+"-server-packets.txt"),null);} + private void trace(ServerPlayer p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.connection,file(p.getGameProfile().getName()+"-server-packets.txt"),null);} @SubscribeEvent public void tick(TickEvent.ServerTickEvent e) { if(e.phase!=TickEvent.Phase.END||finished)return; try { @@ -82,7 +82,7 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { if(a.level.dimension()!=dim)transfer(a,dim); if(b.level.dimension()!=dim)transfer(b,dim); double x=kind==6?-.4:96.5,z=kind==6?-.4:96.5; - FixtureArena.prepare(new BlockPos(x,79,z),w::setBlockAndUpdate); + FixtureArena.prepare(BlockPos.containing(x,79,z),w::setBlockAndUpdate); FixtureArena.prepare(new BlockPos(160,79,96),w::setBlockAndUpdate); w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null);w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); a.setGameMode(GameType.SURVIVAL);b.setGameMode(GameType.SURVIVAL);a.getInventory().clearContent();b.getInventory().clearContent(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java index aeb3d15..5f80b42 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java @@ -3,7 +3,7 @@ import net.minecraft.client.gui.screens.DeathScreen; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; -import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.Component; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -41,8 +41,8 @@ final class RespawnPacketClientChecks { private File observations; private Level profileWorld; void register(){if(Boolean.getBoolean("lootablebodies.respawn")&&role!=null){net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);MinecraftForge.EVENT_BUS.register(this);}} - @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseClickedEvent.Pre e){e.setCanceled(true);} - @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyboardKeyPressedEvent.Pre e){e.setCanceled(true);} + @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre e){e.setCanceled(true);} + @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre e){e.setCanceled(true);} private void respawn(Minecraft mc) { if((!role.equals("A")&&!Boolean.getBoolean("lootablebodies.respawn.simultaneous"))||mc.player==null)return; if(mc.screen instanceof DeathScreen) { @@ -96,7 +96,7 @@ private void respawn(Minecraft mc) { uuid=state[2].equals("none")?null:UUID.fromString(state[2]);entityId=Integer.parseInt(state[3]);frames++; if(FixtureConnections.retry(mc,role))connecting=false; if(!connecting&&FixtureConnections.ready(role)&&mc.screen instanceof TitleScreen) { - connecting=true;mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + connecting=true;mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); FixtureConnections.connect(mc); } if(mc.level==null||mc.player==null)return; @@ -135,7 +135,7 @@ else if(phase==1&&frames>20){ // Close the transport before clearing the world. The next main // loop drains already queued destroy packets against that world; // clearing it here races vanilla 1.11's unguarded packet handler. - mc.getConnection().getConnection().disconnect(new net.minecraft.network.chat.TextComponent("Disposable check complete")); + mc.getConnection().getConnection().disconnect(net.minecraft.network.chat.Component.literal("Disposable check complete")); finished=true; } } catch(Throwable failure){failure.printStackTrace();try{Files.write(RespawnPacketChecks.file(role+".failure").toPath(),failure.toString().getBytes(StandardCharsets.UTF_8));}catch(IOException ignored){}finished=true;mc.stop();} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java index 540cac1..a9e6621 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java @@ -18,7 +18,7 @@ import net.minecraft.network.Connection; import net.minecraft.network.protocol.Packet; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.protocol.game.ClientboundAddMobPacket; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; import net.minecraft.network.protocol.game.ClientboundRespawnPacket; @@ -35,7 +35,9 @@ interface After {void received(String packet);} private RespawnPacketTrace(File log,After after){this.log=log;this.after=after;} static void install(Connection manager,File log,After after) { Channel channel=manager.channel(); - channel.eventLoop().submit(()->channel.pipeline().addBefore("packet_handler","corpse_packet_trace",new RespawnPacketTrace(log,after))).syncUninterruptibly(); + // Native19 assembles decoded spawn children into a bundle before the + // packet listener. Observe that real wire stream before aggregation. + channel.eventLoop().submit(()->channel.pipeline().addBefore("bundler","corpse_packet_trace",new RespawnPacketTrace(log,after))).syncUninterruptibly(); } @Override public void channelRead(ChannelHandlerContext context,Object packet) throws Exception { String description=describe(packet);if(description!=null)record("IN "+description); @@ -55,11 +57,11 @@ static String describe(Object packet) { if(packet instanceof ICustomPacket) { ICustomPacket f=(ICustomPacket)packet;if(!f.getName().toString().equals("fml:play"))return null; FriendlyByteBuf b=new FriendlyByteBuf(f.getInternalData().duplicate());if(b.readableBytes()<6 || b.readVarInt()!=0)return null; - int type=b.readVarInt(),id=b.readInt();net.minecraft.world.entity.EntityType entityType=net.minecraft.core.Registry.ENTITY_TYPE.byId(type); - String name=entityType==null?"unknown":entityType.getRegistryName().getNamespace(); + int type=b.readVarInt(),id=b.readInt();net.minecraft.world.entity.EntityType entityType=net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.byId(type); + String name=entityType==null?"unknown":net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(entityType).getNamespace(); return "MOD_SPAWN id="+id+" mod="+name+" type="+type+" uuid="+new UUID(b.readLong(),b.readLong()); } - if(!(packet instanceof ClientboundAddMobPacket||packet instanceof ClientboundRespawnPacket||packet instanceof ClientboundRemoveEntitiesPacket||packet instanceof ClientboundForgetLevelChunkPacket + if(!(packet instanceof ClientboundAddEntityPacket||packet instanceof ClientboundRespawnPacket||packet instanceof ClientboundRemoveEntitiesPacket||packet instanceof ClientboundForgetLevelChunkPacket ||packet instanceof ClientboundLevelChunkWithLightPacket||packet instanceof ClientboundPlayerCombatEnterPacket||packet instanceof ClientboundPlayerCombatEndPacket ||packet instanceof ClientboundPlayerCombatKillPacket||packet instanceof ClientboundSetEntityDataPacket||packet instanceof ServerboundClientCommandPacket))return null; if(packet instanceof ClientboundRespawnPacket) { @@ -70,10 +72,10 @@ static String describe(Object packet) { FriendlyByteBuf b=new FriendlyByteBuf(Unpooled.buffer()); try { ((Packet)packet).write(b); - if(packet instanceof ClientboundAddMobPacket) { + if(packet instanceof ClientboundAddEntityPacket) { int id=b.readVarInt();UUID uuid=b.readUUID();int type=b.readVarInt(); - net.minecraft.world.entity.EntityType entityType=Registry.ENTITY_TYPE.byId(type); - String name=entityType==null?"unknown":entityType.getRegistryName().getNamespace(); + net.minecraft.world.entity.EntityType entityType=net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.byId(type); + String name=entityType==null?"unknown":net.minecraft.core.registries.BuiltInRegistries.ENTITY_TYPE.getKey(entityType).getNamespace(); return "MOD_SPAWN id="+id+" mod="+name+" type="+type+" uuid="+uuid; } if(packet instanceof ClientboundRemoveEntitiesPacket){int[] ids=new int[b.readVarInt()];for(int i=0;i>4,net.minecraft.util.Mth.floor(body.getZ())>>4),"Manual corpse fixture spawned before chunk admission: "+body.blockPosition()+" name="+body.getCustomNameTag()); + require(event.getLevel().getChunkSource().hasChunk(net.minecraft.util.Mth.floor(body.getX())>>4,net.minecraft.util.Mth.floor(body.getZ())>>4),"Manual corpse fixture spawned before chunk admission: "+body.blockPosition()+" name="+body.getCustomNameTag()); } } @SubscribeEvent public void started(ServerStartedEvent event) { @@ -98,9 +98,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.9.118021"), "identity/version"); + require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.10.119041"), "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.9.118021") : "2.4.9.118021"), "loaded release version"); + System.getProperty("lootablebodies.expectedIssueVersion","2.4.10.119041") : "2.4.10.119041"), "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 && @@ -217,7 +217,7 @@ static ItemStack stack(int index) { ListTag contents = new ListTag(); CompoundTag item = new CompoundTag(); new ItemStack(Items.DIAMOND,3).save(item); contents.add(item); nested.put("contents", contents); custom.put("nested", nested); - stack.setTag(custom);if(index<=5)stack.setDamageValue(index<4?index+2:index==4?17:9); stack.setHoverName(new net.minecraft.network.chat.TextComponent("Fixture " + index)); + stack.setTag(custom);if(index<=5)stack.setDamageValue(index<4?index+2:index==4?17:9); stack.setHoverName(net.minecraft.network.chat.Component.literal("Fixture " + index)); if (index == 4) stack.enchant(net.minecraft.world.item.enchantment.Enchantments.UNBREAKING, 2); return stack; } @@ -266,7 +266,7 @@ static void death(MinecraftServer server, ServerLevel world, File folder) throws // Assign the fixture ID before the native entity-section manager indexes // the real death-created body, never by mutating an admitted UUID. pendingDeathId=DEATH; - try {player.die(DamageSource.OUT_OF_WORLD);}finally{pendingDeathId=null;} + try {player.die(player.damageSources().outOfWorld());}finally{pendingDeathId=null;} EntityLootableBody corpse = null; for (Entity entity : new ArrayList(FixtureApi.entities(world))) if (entity instanceof EntityLootableBody && "BuildDeath".equals(((EntityLootableBody)entity).getCustomNameTag())) corpse=(EntityLootableBody)entity; diff --git a/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java b/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java index 4531369..1d536e2 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java @@ -2,11 +2,15 @@ import net.minecraft.world.item.Item; import net.minecraft.world.effect.MobEffect; -import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.registries.RegisterEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; /** Forge 1.12 registers controlled dependencies during their registry events. */ public final class TestRegistries { - @SubscribeEvent public void items(RegistryEvent.Register event) {NbtWorldChecks.registerItem();} - @SubscribeEvent public void potions(RegistryEvent.Register event) {EffectHurtChecks.registerPotion();} + @SubscribeEvent public void items(RegisterEvent event) { + if(event.getRegistryKey().equals(net.minecraft.core.registries.Registries.ITEM))NbtWorldChecks.registerItem(); + } + @SubscribeEvent public void potions(RegisterEvent event) { + if(event.getRegistryKey().equals(net.minecraft.core.registries.Registries.MOB_EFFECT))EffectHurtChecks.registerPotion(); + } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java index cd55725..6836b5c 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java @@ -115,14 +115,20 @@ private static void ambiguousBed() throws Exception { CompoundTag twice=new CompoundTag();again.addAdditionalSaveData(twice); RuntimeChecks.require(NameLifetimeChecks.same(once.get("LootableBodiesUnresolved"),twice.get("LootableBodiesUnresolved")),"Ambiguous bed duplicated or changed during reload"); } + private static Object mappingField(net.minecraftforge.registries.MissingMappingsEvent.Mapping mapping,String name) { + try { + java.lang.reflect.Field field=net.minecraftforge.registries.MissingMappingsEvent.Mapping.class.getDeclaredField(name); + field.setAccessible(true);return field.get(mapping); + } catch(ReflectiveOperationException failure){throw new AssertionError("Native mapping observation",failure);} + } private static void soundMapping() throws Exception { net.minecraftforge.registries.IForgeRegistry registry=net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS; - net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping old=new net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("minecraft:entity.experience_orb.touch"),196); - net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping other=new net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("absentfixture:other_sound"),197); - net.minecraftforge.event.RegistryEvent.MissingMappings event=new net.minecraftforge.event.RegistryEvent.MissingMappings<>(new net.minecraft.resources.ResourceLocation("minecraft:soundevents"),registry,Arrays.asList(old,other)); + 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 event=new net.minecraftforge.registries.MissingMappingsEvent(registry.getRegistryKey(),registry,Arrays.asList(old,other)); net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(event); - RuntimeChecks.require(old.getAction()==net.minecraftforge.event.RegistryEvent.MissingMappings.Action.REMAP && old.getTarget()==registry.getValue(new net.minecraft.resources.ResourceLocation("minecraft:entity.experience_orb.pickup")),"Known vanilla sound rename not remapped"); - RuntimeChecks.require(other.getAction()==net.minecraftforge.event.RegistryEvent.MissingMappings.Action.DEFAULT,"Unrelated missing mappings must retain Forge's normal handling"); + 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(other,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.DEFAULT,"Unrelated missing mappings must retain Forge's normal handling"); } private static void nativeRenames() { rename(net.minecraftforge.registries.ForgeRegistries.ITEMS,"sign","oak_sign"); @@ -132,13 +138,13 @@ private static void nativeRenames() { rename(net.minecraftforge.registries.ForgeRegistries.BLOCKS,"sign","oak_sign"); 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.event.RegistryEvent.MissingMappings.Mapping old=new net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("minecraft",oldName),30000); - net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping other=new net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping<>(registry,registry,new net.minecraft.resources.ResourceLocation("absentfixture",oldName),30001); - net.minecraftforge.event.RegistryEvent.MissingMappings event=new net.minecraftforge.event.RegistryEvent.MissingMappings<>(registry.getRegistryName(),registry,Arrays.asList(old,other)); + 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 event=new net.minecraftforge.registries.MissingMappingsEvent(registry.getRegistryKey(),registry,Arrays.asList(old,other)); net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(event); - RuntimeChecks.require(old.getAction()==net.minecraftforge.event.RegistryEvent.MissingMappings.Action.REMAP && old.getTarget()==registry.getValue(new net.minecraft.resources.ResourceLocation("minecraft",current)),"Native registry rename event not remapped: "+oldName); - RuntimeChecks.require(other.getAction()==net.minecraftforge.event.RegistryEvent.MissingMappings.Action.DEFAULT,"Foreign registry rename was consumed"); + 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(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"); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java index f16158a..e48a59c 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java @@ -77,7 +77,7 @@ public VisibilityChecks(MinecraftServer server) throws Exception { ServerLevel w=world();w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null);w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); // Forge25's forced player spawn can fall back to world spawn. // Keep this disposable respawn within the intended viewing area. - w.setDefaultSpawnPos(new BlockPos(96,PLAYER_Y,93),0F);server.getCommands().performCommand(server.createCommandSourceStack(),"gamerule spawnRadius 0"); + w.setDefaultSpawnPos(new BlockPos(96,PLAYER_Y,93),0F);server.getCommands().performPrefixedCommand(server.createCommandSourceStack(),"gamerule spawnRadius 0"); w.setDayTime(6000); for(int x=88;x<=170;x++)for(int z=89;z<=103;z++)w.setBlockAndUpdate(new BlockPos(x,GROUND_Y,z),Blocks.OBSIDIAN.defaultBlockState()); for(ServerPlayer p:new ServerPlayer[]{a,b}) {p.setGameMode(GameType.SURVIVAL);p.getInventory().clearContent();FixtureApi.spawn(p,new BlockPos(96,PLAYER_Y,93),true,false,p.level.dimension());} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java index aabbc7f..c0733fa 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java @@ -2,7 +2,7 @@ import net.minecraft.client.gui.screens.TitleScreen; import net.minecraft.client.gui.screens.DeathScreen; import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; -import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.Component; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -37,8 +37,8 @@ final class VisibilityClientChecks { private net.minecraft.network.Connection traced; private net.minecraft.client.multiplayer.ClientLevel profileWorld; void register(){if(Boolean.getBoolean("lootablebodies.visibility")&&role!=null){net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);MinecraftForge.EVENT_BUS.register(this);}} - @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseClickedEvent.Pre e){e.setCanceled(true);} - @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyboardKeyPressedEvent.Pre e){e.setCanceled(true);} + @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre e){e.setCanceled(true);} + @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre e){e.setCanceled(true);} @SubscribeEvent public void rendered(RenderLivingEvent.Post e){ if(e.getEntity() instanceof EntityLootableBody) { rendered.add(e.getEntity().getUUID()); @@ -66,7 +66,7 @@ final class VisibilityClientChecks { System.out.println("VISIBILITY_PREDEATH_RENDER_READY "+receipt);ack(); }catch(Throwable failure){failure.printStackTrace();finished=true;mc.stop();} } - @SubscribeEvent public void deathScreenDrawn(net.minecraftforge.client.event.ScreenEvent.DrawScreenEvent.Post e) { + @SubscribeEvent public void deathScreenDrawn(net.minecraftforge.client.event.ScreenEvent.Render.Post e) { if(!role.equals("A") || !(e.getScreen() instanceof DeathScreen) || (phase!=2&&phase!=14) || done.contains(phase))return; Minecraft mc=Minecraft.getInstance(); try { @@ -79,7 +79,7 @@ final class VisibilityClientChecks { System.out.println("VISIBILITY_DEATH_RENDER_PROOF "+role+" "+receipt);ack(); } catch(Throwable error){error.printStackTrace();finished=true;mc.stop();} } - @SubscribeEvent public void removed(net.minecraftforge.event.entity.EntityLeaveWorldEvent e) { + @SubscribeEvent public void removed(net.minecraftforge.event.entity.EntityLeaveLevelEvent e) { Entity entity=e.getEntity(); if(entity instanceof EntityLootableBody && entity.level.isClientSide) { System.out.println("VISIBILITY_CLIENT_REMOVAL "+role+" phase="+phase+" uuid="+entity.getUUID()+" reason="+entity.getRemovalReason()+" rendered="+rendered.contains(entity.getUUID())); @@ -115,7 +115,7 @@ final class VisibilityClientChecks { if(n!=phase){phase=n;frames=0;guiFrames=0;respawnSent=false;rendered.clear();deathRendered.clear();System.out.println("VISIBILITY_CLIENT_PHASE "+role+" "+phase);}frames++; if(FixtureConnections.retry(mc,role))connecting=false; if(!connecting && FixtureConnections.ready(role) && mc.level==null && mc.screen instanceof TitleScreen && (phase==0 || phase==9 || phase==11)) { - connecting=true;mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + connecting=true;mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); FixtureConnections.connect(mc); } if(mc.level==null || mc.player==null)return; @@ -166,7 +166,7 @@ final class VisibilityClientChecks { } else if(phase==18) { new File(mc.gameDirectory,"visibility-client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_VISIBILITY_CLIENT_PASS role="+role); mc.player.closeContainer(); - mc.getConnection().getConnection().disconnect(new net.minecraft.network.chat.TextComponent("Disposable check complete")); + mc.getConnection().getConnection().disconnect(net.minecraft.network.chat.Component.literal("Disposable check complete")); finished=true; } else if(phase==20) { if(body(mc,"first")==null&&body(mc,"second")==null)ack(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java index db4969d..978b55b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java @@ -24,7 +24,7 @@ final class VisibilitySkinClientChecks { private SkinManager original; void tick(Minecraft mc) throws Exception { if(stage==0 && mc.screen instanceof TitleScreen) { - mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); FixtureApi.ClientWorlds.create(mc,"skin-fixture","Skin fixture",net.minecraft.world.level.GameType.SURVIVAL,true);stage=1; } else if(stage==1 && mc.level!=null && mc.player!=null && ++frames>60) { VisibilitySkinChecks.run(mc); @@ -42,14 +42,14 @@ void tick(Minecraft mc) throws Exception { } private static final class Panel extends Screen { private final EntityLootableBody[] bodies=new EntityLootableBody[3]; - Panel(Minecraft mc){super(new net.minecraft.network.chat.TextComponent("Skin visibility fixture"));for(int i=0;i<3;i++){bodies[i]=new EntityLootableBody(mc.level);bodies[i].setGameProfile(new GameProfile(new UUID(0,512+i),"VisibilityModel"));}} + Panel(Minecraft mc){super(net.minecraft.network.chat.Component.literal("Skin visibility fixture"));for(int i=0;i<3;i++){bodies[i]=new EntityLootableBody(mc.level);bodies[i].setGameProfile(new GameProfile(new UUID(0,512+i),"VisibilityModel"));}} @Override public void render(com.mojang.blaze3d.vertex.PoseStack matrices,int mouseX,int mouseY,float partial) { fill(matrices,0,0,width,height,0xff304050); String[] names={"Failed skin: wide arms","Failed skin: slim arms","Local texture"};boolean local=LootableBodies.useLocalSkin; try {for(int i=0;i<3;i++) { LootableBodies.useLocalSkin=i==2;int x=(2*i+1)*width/6; drawCenteredString(matrices,font,names[i],x,35,0xffffff); - InventoryScreen.renderEntityInInventory(x,height*2/3,65,20,-20,bodies[i]); + InventoryScreen.renderEntityInInventoryFollowsMouse(matrices,x,height*2/3,65,20,-20,bodies[i]); }} finally {LootableBodies.useLocalSkin=local;} } @Override public boolean isPauseScreen(){return false;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java index 5b047e3..735df3d 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java @@ -161,7 +161,7 @@ private static void negative() { private static void damage() { BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.above(),false); EntityLootableBody body = body(centre); float health = body.getHealth(); - body.hurt(DamageSource.IN_WALL, 1); clear(body); + body.hurt(body.damageSources().inWall(), 1); clear(body); RuntimeChecks.require(body.isAlive() && body.getHealth() == health, "Default suffocation health rules changed"); } @@ -214,7 +214,7 @@ static void reload(ServerLevel target, File folder) throws Exception { CompoundTag expected; try (InputStream in = new FileInputStream(new File(folder,"wall-escape-"+trapped+".nbt"))) { expected = NbtIo.readCompressed(in); } ListTag pos = expected.getList("Pos",6); - BlockPos savedPosition=new BlockPos(pos.getDouble(0),pos.getDouble(1),pos.getDouble(2)); + BlockPos savedPosition=BlockPos.containing(pos.getDouble(0),pos.getDouble(1),pos.getDouble(2)); // Terrain admission completes before the native entity-storage inbox. RuntimeChecks.awaitChunk(world,savedPosition.getX()>>4,savedPosition.getZ()>>4); EntityLootableBody body = RuntimeChecks.find(world, expected.getUUID("UUID")); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java index 38415e6..2687973 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java @@ -81,7 +81,7 @@ static void run(MinecraftServer server,String phase) throws Exception { if(phase.equals("upgrade-final") && ledger.getString("Removed").isEmpty()) { for(int i=0;i0) {destination=(Container)world.getBlockEntity(position(stored.getCompound(stored.size()-1)));for(int i=0;inew ConfigGuiHandler.ConfigGuiFactory(new CorpseConfigGuiFactory())); + ModLoadingContext.get().registerExtensionPoint(ConfigScreenHandler.ConfigScreenFactory.class, + ()->new ConfigScreenHandler.ConfigScreenFactory(new CorpseConfigGuiFactory())); FMLJavaModLoadingContext.get().getModEventBus().addListener(ClientProxy::setup); FMLJavaModLoadingContext.get().getModEventBus().addListener(ClientProxy::renderers); } diff --git a/src/main/java/cyano/lootable/LootableBodies.java b/src/main/java/cyano/lootable/LootableBodies.java index d121b43..080827c 100644 --- a/src/main/java/cyano/lootable/LootableBodies.java +++ b/src/main/java/cyano/lootable/LootableBodies.java @@ -8,12 +8,13 @@ import cyano.lootable.upgrade.ChunkVersionContext; import net.minecraft.world.entity.EntityType; import net.minecraft.world.item.Item; -import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.SpawnEggItem; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.registries.RegisterEvent; +import net.minecraft.core.registries.Registries; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.DistExecutor; import net.minecraftforge.fml.common.Mod; @@ -26,7 +27,7 @@ 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.9.118021"; + public static final String VERSION = "2.4.10.119041"; public static final ResourceLocation CORPSE_ID = new ResourceLocation("lootablebodies:corpse"); public static EntityType CORPSE; public static Item CORPSE_EGG; @@ -59,10 +60,13 @@ public LootableBodies() { event.enqueueWork(cyano.lootable.upgrade.EarlyWorldPreservation::install)); bus.addListener(cyano.lootable.upgrade.LegacyProfessionRegistry::create); bus.addListener((net.minecraftforge.event.entity.EntityAttributeCreationEvent event)->event.put(corpseType(),EntityLootableBody.corpseAttributeDefaults().build())); - bus.addGenericListener(EntityType.class,this::registerEntities); - bus.addGenericListener(Item.class,this::registerItems); - bus.addGenericListener(net.minecraft.world.entity.ai.memory.MemoryModuleType.class,cyano.lootable.upgrade.LegacyRegistryMappings::retiredMemories); - bus.addGenericListener(net.minecraft.world.inventory.MenuType.class,this::registerContainers); + bus.addListener(this::registerEntities); + bus.addListener(this::registerItems); + bus.addListener(cyano.lootable.upgrade.LegacyRegistryMappings::retiredMemories); + bus.addListener(this::registerContainers); + bus.addListener((net.minecraftforge.event.CreativeModeTabEvent.BuildContents event) -> { + if(event.getTab()==CreativeModeTabs.SPAWN_EGGS)event.accept(CORPSE_EGG); + }); MinecraftForge.EVENT_BUS.register(new PlayerDeathEventHandler()); MinecraftForge.EVENT_BUS.register(this); DistExecutor.runWhenOn(Dist.CLIENT,()->ClientProxy::register); @@ -77,21 +81,24 @@ private static synchronized EntityType corpseType() { } return CORPSE; } - private void registerEntities(RegistryEvent.Register> event) { - event.getRegistry().register(corpseType().setRegistryName(CORPSE_ID)); + private void registerEntities(RegisterEvent event) { + event.register(Registries.ENTITY_TYPE,CORPSE_ID,LootableBodies::corpseType); } - private void registerItems(RegistryEvent.Register event) { - CORPSE_EGG=new SpawnEggItem(corpseType(),1,1,new Item.Properties().tab(CreativeModeTab.TAB_MISC)) - .setRegistryName(MODID,"corpse_spawn_egg"); - event.getRegistry().register(CORPSE_EGG); + private void registerItems(RegisterEvent event) { + event.register(Registries.ITEM,new ResourceLocation(MODID,"corpse_spawn_egg"),()-> { + CORPSE_EGG=new SpawnEggItem(corpseType(),1,1,new Item.Properties()); + return CORPSE_EGG; + }); } - private void registerContainers(RegistryEvent.Register> event) { - CORPSE_CONTAINER=net.minecraftforge.common.extensions.IForgeMenuType.create((window,inventory,buffer)-> { - net.minecraft.world.entity.Entity body=inventory.player.level.getEntity(buffer.readVarInt()); - if(!(body instanceof EntityLootableBody))throw new IllegalStateException("Corpse container has no matching entity"); - return new cyano.lootable.graphics.CorpseContainer(window,inventory,(EntityLootableBody)body); + private void registerContainers(RegisterEvent event) { + event.register(Registries.MENU,CORPSE_ID,()-> { + CORPSE_CONTAINER=net.minecraftforge.common.extensions.IForgeMenuType.create((window,inventory,buffer)-> { + net.minecraft.world.entity.Entity body=inventory.player.level.getEntity(buffer.readVarInt()); + if(!(body instanceof EntityLootableBody))throw new IllegalStateException("Corpse container has no matching entity"); + return new cyano.lootable.graphics.CorpseContainer(window,inventory,(EntityLootableBody)body); + }); + return CORPSE_CONTAINER; }); - event.getRegistry().register(CORPSE_CONTAINER.setRegistryName(CORPSE_ID)); } public static LootableBodies getInstance(){return INSTANCE;} @SubscribeEvent public void beforeWorldLoading(ServerAboutToStartEvent event) { @@ -106,12 +113,12 @@ private void registerContainers(RegistryEvent.Register(attribute.getModifiers())); } @@ -139,9 +139,9 @@ public void tick() { // A Forge ticket can tick our centre chunk while neighbouring support // terrain is absent. Never treat those missing collision boxes as air. AABB bounds = getBoundingBox(); - BlockPos from = new BlockPos(bounds.minX + Math.min(0D,getDeltaMovement().x), 0, + BlockPos from = BlockPos.containing(bounds.minX + Math.min(0D,getDeltaMovement().x), 0, bounds.minZ + Math.min(0D,getDeltaMovement().z)); - BlockPos to = new BlockPos(bounds.maxX + Math.max(0D,getDeltaMovement().x), 0, + BlockPos to = BlockPos.containing(bounds.maxX + Math.max(0D,getDeltaMovement().x), 0, bounds.maxZ + Math.max(0D,getDeltaMovement().z)); if(!getCommandSenderWorld().hasChunksAt(from,to)) return; } @@ -377,44 +377,17 @@ public ItemStack mergeItem(int slot, ItemStack stack) { @Override protected void actuallyHurt(DamageSource src, float amount){ - String type = String.valueOf(src == null ? null : src.getMsgId()); // null protection - if(src instanceof EntityDamageSource && src.getEntity() instanceof Player){ - Player player = (Player)src.getEntity(); - ItemStack item = player.getItemInHand(InteractionHand.MAIN_HAND); - if(!emptyStack(item)) { - if (item.getItem() instanceof ShovelItem || item.canPerformAction(net.minecraftforge.common.ToolActions.SHOVEL_DIG)) { - this.kill(); - } - } - } - if(type.equals(DamageSource.OUT_OF_WORLD.getMsgId())) { - super.actuallyHurt(src,amount); // kill command and falling out of the world - return; - } - - if(type.equals(DamageSource.IN_WALL.getMsgId())){ - this.jumpOutOfWall(); - } - - // damage handling - if(LootableBodies.completelyInvulnerable) return; - - if(src instanceof EntityDamageSource ){ - if(LootableBodies.hurtByAttacks) super.actuallyHurt(src,amount); - return; - } - - if(matchesAny(type, DamageSource.ANVIL, DamageSource.CACTUS, DamageSource.FALL, DamageSource.FALLING_BLOCK, DamageSource.FLY_INTO_WALL, DamageSource.IN_FIRE, DamageSource.LAVA, DamageSource.MAGIC, DamageSource.LIGHTNING_BOLT, DamageSource.ON_FIRE)){ - if(LootableBodies.hurtByEnvironment)super.actuallyHurt(src,amount); - return; - } - - if(LootableBodies.hurtByMisc){ - super.actuallyHurt(src,amount); - return; - } - } - + if(CorpseDamageRules.kind(src)==CorpseDamageRules.Kind.ATTACK && src.getEntity() instanceof Player){ + Player player=(Player)src.getEntity(); + ItemStack item=player.getItemInHand(InteractionHand.MAIN_HAND); + if(!emptyStack(item) && (item.getItem() instanceof ShovelItem || item.canPerformAction(net.minecraftforge.common.ToolActions.SHOVEL_DIG))) + this.kill(); + } + if(CorpseDamageRules.isWall(src))this.jumpOutOfWall(); + if(CorpseDamageRules.allows(src,LootableBodies.completelyInvulnerable, + LootableBodies.hurtByAttacks,LootableBodies.hurtByEnvironment,LootableBodies.hurtByMisc)) + super.actuallyHurt(src,amount); + } public GameProfile getGameProfile(){ @@ -426,7 +399,7 @@ public void setGameProfile(GameProfile gp){ } /** Refreshes an existing client body after enabling player skins in the GUI. */ public String getCustomNameTag(){return getCustomName()==null?"":getCustomName().getString();} - public void setCustomNameTag(String name){setCustomName(name==null || name.isEmpty()?null:new net.minecraft.network.chat.TextComponent(name));} + public void setCustomNameTag(String name){setCustomName(name==null || name.isEmpty()?null:net.minecraft.network.chat.Component.literal(name));} public void refreshClientSkin() { GameProfile profile = gpSwap.get(); @@ -466,7 +439,7 @@ public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder } private java.util.List corpseAttributes() { java.util.List result=new java.util.ArrayList<>(); - for(net.minecraft.world.entity.ai.attributes.Attribute key:net.minecraft.core.Registry.ATTRIBUTE) + for(net.minecraft.world.entity.ai.attributes.Attribute key:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES) if(getAttributes().hasAttribute(key))result.add(getAttribute(key)); return result; } @@ -489,7 +462,7 @@ private AttributeInstance attributeNamed(String name) { default:break; } net.minecraft.resources.ResourceLocation id=net.minecraft.resources.ResourceLocation.tryParse(name); - net.minecraft.world.entity.ai.attributes.Attribute key=id==null?null:net.minecraft.core.Registry.ATTRIBUTE.getOptional(id).orElse(null); + net.minecraft.world.entity.ai.attributes.Attribute key=id==null || !net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.containsKey(id)?null:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getValue(id); return key!=null && getAttributes().hasAttribute(key)?getAttribute(key):null; } @@ -531,8 +504,8 @@ public void jumpOutOfWall(){ } private boolean escapeTerrainLoaded(AABB bounds) { - return level.hasChunksAt(new BlockPos(bounds.minX-1,0,bounds.minZ-1), - new BlockPos(bounds.maxX+1,0,bounds.maxZ+1)); + return level.hasChunksAt(BlockPos.containing(bounds.minX-1,0,bounds.minZ-1), + BlockPos.containing(bounds.maxX+1,0,bounds.maxZ+1)); } private boolean tryWallExit(double x,double y,double z) { @@ -548,7 +521,7 @@ private boolean tryWallExit(double x,double y,double z) { @Override public void kill() { if(terminate < 0) terminate = DEATH_COUNTDOWN; - this.hurt(DamageSource.OUT_OF_WORLD, this.getMaxHealth()); + this.hurt(damageSources().outOfWorld(), this.getMaxHealth()); this.setChanged(); } @@ -565,7 +538,7 @@ public void playAmbientSound() { @Override - protected int getExperienceReward(final Player p_getExperiencePoints_1_) { + public int getExperienceReward() { return 0; } @@ -592,8 +565,8 @@ protected void dropCustomDeathLoot(DamageSource source, int lootingModifier, boo } @Override - public boolean canBeControlledByRider() { - return false; + public net.minecraft.world.entity.LivingEntity getControllingPassenger() { + return null; } @@ -805,12 +778,6 @@ private boolean dropAllItems(){ return isEmpty(); } - private static boolean matchesAny(String damageType, DamageSource... list){ - for(int i = 0; i < list.length; i++){ - if(damageType.equalsIgnoreCase(list[i].getMsgId())) return true; - } - return false; - } @Override public MobType getMobType() @@ -1035,7 +1002,7 @@ private void retainInvalid(String source, Tag data) { } private void restoreAttribute(AttributeInstance attribute) { - String name = attribute.getAttribute().getRegistryName().toString(); + String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute()).toString(); for(AttributeModifier modifier : new ArrayList<>(attribute.getModifiers())) attribute.removeModifier(modifier); attribute.setBaseValue(defaultAttributeBases.getOrDefault(name,attribute.getAttribute().getDefaultValue())); List defaults = defaultAttributeModifiers.get(name); @@ -1123,7 +1090,7 @@ private ListTag readAttributes(Tag raw) { ? attributeNamed(record.getString("Name")) : null; Double base = finiteNumber(record.get("Base")); if(attribute == null || base == null) { retainInvalid("Attribute",record); continue; } - String name = attribute.getAttribute().getRegistryName().toString(); + String name = net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKey(attribute.getAttribute()).toString(); // corpse_HP has always allowed 1..Short.MAX_VALUE. Preserve a base // saved under an older config, including vanilla's existing cap. boolean configuredHealth = attribute.getAttribute() == Attributes.MAX_HEALTH @@ -1232,7 +1199,7 @@ private String readName(CompoundTag root,String key) { if(text==null){retainInvalid(key,raw);return null;}name=text.getString(); }catch(RuntimeException malformed){retainInvalid(key,raw);return null;} } - String packet=net.minecraft.network.chat.Component.Serializer.toJson(new net.minecraft.network.chat.TextComponent(name)); + String packet=net.minecraft.network.chat.Component.Serializer.toJson(net.minecraft.network.chat.Component.literal(name)); if(name.length()<=32767 && name.getBytes(StandardCharsets.UTF_8).length<=32767 && packet.getBytes(StandardCharsets.UTF_8).length<=32767)return name.trim().isEmpty()?"":name; } diff --git a/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java b/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java index 246a5eb..61b3843 100644 --- a/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java +++ b/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java @@ -24,8 +24,8 @@ static boolean needsLegacyRecovery(CompoundTag record,Long legacyId) { (record.contains("forge:id") && record.getString("forge:id").isEmpty()); } static void write(CompoundTag accepted,CompoundTag record,MobEffect effect) { - accepted.putByte("Id",(byte)MobEffect.getId(effect)); + accepted.putInt("Id",MobEffect.getId(effect)); if(record.contains("forge:id",8) && !record.getString("forge:id").isEmpty()) - accepted.putString("forge:id",effect.getRegistryName().toString()); + accepted.putString("forge:id",ForgeRegistries.MOB_EFFECTS.getKey(effect).toString()); } } diff --git a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java index bed51b6..22f3d37 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java @@ -23,13 +23,13 @@ public final class CorpseConfigGui extends Screen { private int page,perPage; private boolean invalid; public CorpseConfigGui(Screen parent){this(parent,null,new LinkedHashMap<>(),0);for(String key:keys())pending.put(key,property(key).getString());} - private CorpseConfigGui(Screen parent,String group,Map pending,int page){super(new net.minecraft.network.chat.TextComponent(LootableBodies.NAME));this.parent=parent;this.group=group;this.pending=new LinkedHashMap<>(pending);this.page=page;} + private CorpseConfigGui(Screen parent,String group,Map pending,int page){super(net.minecraft.network.chat.Component.literal(LootableBodies.NAME));this.parent=parent;this.group=group;this.pending=new LinkedHashMap<>(pending);this.page=page;} private static List keys(){List result=new ArrayList<>(Arrays.asList(APPEARANCE));result.addAll(Arrays.asList(RULES));return result;} private static Property property(String key){for(String category:new String[]{"options","corpse damage","corpse decay","corpse recovery"}){Property value=LootableBodies.getConfiguration().getCategory(category).get(key);if(value!=null)return value;}throw new IllegalArgumentException(key);} private Button button(int id,int x,int y,int width,String label,Runnable click){return addRenderableWidget(new SettingButton(id,x,y,width,label,click));} public static final class SettingButton extends Button { public final int id; - SettingButton(int id,int x,int y,int width,String label,Runnable action){super(x,y,width,20,new net.minecraft.network.chat.TextComponent(label),button->action.run());this.id=id;} + SettingButton(int id,int x,int y,int width,String label,Runnable action){super(x,y,width,20,net.minecraft.network.chat.Component.literal(label),button->action.run(),DEFAULT_NARRATION);this.id=id;} } @Override protected void init(){ fields.clear();rows.clear();int left=width/2-155; @@ -42,7 +42,7 @@ public static final class SettingButton extends Button { for(int i=start;i{capture();pending.put(key,Boolean.toString(!Boolean.parseBoolean(pending.get(key))));init(minecraft,width,height);}); - else {EditBox field=new EditBox(font,width/2+60,y+1,95,18,new net.minecraft.network.chat.TranslatableComponent(property(key).getLanguageKey()));field.setMaxLength(64);field.setValue(pending.get(key));fields.put(key,field);addWidget(field);} + else {EditBox field=new EditBox(font,width/2+60,y+1,95,18,net.minecraft.network.chat.Component.translatable(property(key).getLanguageKey()));field.setMaxLength(64);field.setValue(pending.get(key));fields.put(key,field);addWidget(field);} } if(page>0)button(3,left,height-56,80,"<",()->changePage(-1)); if(end",()->changePage(1)); @@ -73,9 +73,9 @@ private boolean validPending(){ } super.render(matrices,mouseX,mouseY,partialTicks);for(EditBox field:fields.values())field.render(matrices,mouseX,mouseY,partialTicks); for(Map.Entry row:rows.entrySet())if(mouseX>=width/2-155 && mouseX=row.getValue() && mouseY=width/2-155 && mouseX=95 && mouseY<115) - renderComponentTooltip(matrices,java.util.Collections.singletonList(new net.minecraft.network.chat.TranslatableComponent("lootablebodies.config.rules.tooltip")),mouseX,mouseY); + renderComponentTooltip(matrices,java.util.Collections.singletonList(net.minecraft.network.chat.Component.translatable("lootablebodies.config.rules.tooltip")),mouseX,mouseY); if(invalid)drawCenteredString(matrices,font,I18n.get("lootablebodies.config.invalid"),width/2,height-72,0xff5555); } @Override public void onClose(){capture();minecraft.setScreen(parent);} diff --git a/src/main/java/cyano/lootable/graphics/CorpseContainer.java b/src/main/java/cyano/lootable/graphics/CorpseContainer.java index aee83e5..ac6e14c 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseContainer.java @@ -84,7 +84,11 @@ public String getSlotTexture() @Override public boolean stillValid(Player entityplayer) { - return targetInventory.stillValid(entityplayer); + boolean valid = targetInventory.stillValid(entityplayer); + // Native19 checks validity before calling clicked. Clear an abandoned + // drag here too, so a rejected distant packet cannot poison the next drag. + if(!valid) resetQuickCraft(); + return valid; } @Override @@ -233,10 +237,10 @@ private boolean tryRelease(Player player, ItemStack stack, boolean includeName) ItemTossEvent event = new ItemTossEvent(dropped,player); if(MinecraftForge.EVENT_BUS.post(event)) return false; if(capturing != null) { - capturing.add(event.getEntityItem()); + capturing.add(event.getEntity()); return true; } - return player.level.addFreshEntity(event.getEntityItem()); + return player.level.addFreshEntity(event.getEntity()); } @Override diff --git a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java index dfabd24..9bbeb94 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java @@ -9,7 +9,6 @@ import com.mojang.blaze3d.systems.RenderSystem; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.TextComponent; /** * Created by Chris on 4/12/2016. @@ -20,7 +19,7 @@ public class CorpseGUIContainer extends net.minecraft.client.gui.screens.invento private final ResourceLocation image = new ResourceLocation(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():new TextComponent(LootableBodies.NAME)); + this(new CorpseContainer(playerItems,entity),playerItems,entity instanceof net.minecraft.world.entity.Entity?((net.minecraft.world.entity.Entity)entity).getDisplayName():Component.literal(LootableBodies.NAME)); } public CorpseGUIContainer(CorpseContainer container, Inventory playerItems, Component title){ super(container,playerItems,title); diff --git a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java index 81df845..5c95eeb 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java @@ -8,7 +8,7 @@ import java.util.Map; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.MultiBufferSource; -import com.mojang.math.Vector3f; +import com.mojang.math.Axis; import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.model.geom.ModelLayers; import net.minecraft.client.model.geom.ModelPart; @@ -38,10 +38,10 @@ public CorpseRenderer(EntityRendererProvider.Context context) { thickArmsModel = new FrozenPlayerModel(context.bakeLayer(ModelLayers.PLAYER),false); addLayer(new HumanoidArmorLayer<>(this, new FrozenArmorModel(context.bakeLayer(ModelLayers.PLAYER_INNER_ARMOR)), - new FrozenArmorModel(context.bakeLayer(ModelLayers.PLAYER_OUTER_ARMOR)))); - addLayer(new ItemInHandLayer<>(this)); + new FrozenArmorModel(context.bakeLayer(ModelLayers.PLAYER_OUTER_ARMOR)),context.getModelManager())); + addLayer(new ItemInHandLayer<>(this,context.getItemInHandRenderer())); addLayer(new ArrowLayer<>(context,this)); - addLayer(new CustomHeadLayer<>(this,context.getModelSet())); + addLayer(new CustomHeadLayer<>(this,context.getModelSet(),context.getItemInHandRenderer())); } public PlayerModel getMainModel() { return getModel(); } @@ -80,8 +80,8 @@ public static ResourceLocation getSkin(GameProfile profile) { @Override protected void setupRotations(EntityLootableBody entity, PoseStack matrices, float age, float yaw, float partialTicks) { - matrices.mulPose(Vector3f.XP.rotationDegrees(90F)); - matrices.mulPose(Vector3f.ZP.rotationDegrees(entity.getRotation())); + matrices.mulPose(Axis.XP.rotationDegrees(90F)); + matrices.mulPose(Axis.ZP.rotationDegrees(entity.getRotation())); matrices.translate(0D, -0.85D, -0.125D); super.setupRotations(entity, matrices, 0F, 0F, partialTicks); } diff --git a/src/main/java/cyano/lootable/graphics/GUIHandler.java b/src/main/java/cyano/lootable/graphics/GUIHandler.java index 4690963..72dd03a 100644 --- a/src/main/java/cyano/lootable/graphics/GUIHandler.java +++ b/src/main/java/cyano/lootable/graphics/GUIHandler.java @@ -11,7 +11,7 @@ public final class GUIHandler { private static final GUIHandler INSTANCE=new GUIHandler(); private GUIHandler() { } public static GUIHandler getInstance(){return INSTANCE;} - public static void open(Player player,EntityLootableBody corpse){if(player instanceof ServerPlayer)NetworkHooks.openGui((ServerPlayer)player,corpse,buf->buf.writeVarInt(corpse.getId()));} + public static void open(Player player,EntityLootableBody corpse){if(player instanceof ServerPlayer)NetworkHooks.openScreen((ServerPlayer)player,corpse,buf->buf.writeVarInt(corpse.getId()));} public Object getServerGuiElement(int id,Player player,Level world,int x,int y,int z){Entity entity=world.getEntity(x);return id==0 && entity instanceof EntityLootableBody?new CorpseContainer(player.getInventory(),(EntityLootableBody)entity):null;} @OnlyIn(Dist.CLIENT) public Object getClientGuiElement(int id,Player player,Level world,int x,int y,int z){Entity entity=world.getEntity(x);return id==0 && entity instanceof EntityLootableBody?new CorpseGUIContainer(player.getInventory(),(EntityLootableBody)entity):null;} } diff --git a/src/main/java/cyano/lootable/upgrade/LegacyProfessionRegistry.java b/src/main/java/cyano/lootable/upgrade/LegacyProfessionRegistry.java index b18c090..7e51099 100644 --- a/src/main/java/cyano/lootable/upgrade/LegacyProfessionRegistry.java +++ b/src/main/java/cyano/lootable/upgrade/LegacyProfessionRegistry.java @@ -2,7 +2,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraftforge.registries.NewRegistryEvent; -import net.minecraftforge.registries.ForgeRegistryEntry; import net.minecraftforge.registries.IForgeRegistry; import net.minecraftforge.registries.RegistryBuilder; @@ -13,15 +12,15 @@ private LegacyProfessionRegistry() { } public static void create(NewRegistryEvent event) { RegistryBuilder builder = new RegistryBuilder() .setName(new ResourceLocation("minecraft:villagerprofessions")) - .setType(Entry.class).setIDRange(0, 32767) + .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 Entry().setRegistryName(new ResourceLocation("minecraft", name))); + registry.register(new ResourceLocation("minecraft", name),new Entry()); }); } - public static final class Entry extends ForgeRegistryEntry { } + public static final class Entry { } } diff --git a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java index cdffb02..e6cd0fe 100644 --- a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java +++ b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java @@ -3,7 +3,9 @@ import cyano.lootable.LootableBodies; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvent; -import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.registries.RegisterEvent; +import net.minecraftforge.registries.MissingMappingsEvent; +import net.minecraft.core.registries.Registries; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -70,50 +72,63 @@ public static ResourceLocation soundReplacement(ResourceLocation old) { return null; } /** This old Long memory had no serializer or saved values in vanilla1.15. */ - public static void retiredMemories(RegistryEvent.Register> event) { - event.getRegistry().register(new net.minecraft.world.entity.ai.memory.MemoryModuleType(java.util.Optional.empty()) - .setRegistryName("minecraft:golem_last_seen_time")); + public static void retiredMemories(RegisterEvent event) { + event.register(Registries.MEMORY_MODULE_TYPE,new ResourceLocation("minecraft:golem_last_seen_time"), + ()->new net.minecraft.world.entity.ai.memory.MemoryModuleType(java.util.Optional.empty())); } - @SubscribeEvent public static void items(RegistryEvent.MissingMappings event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.world.item.Item.class))return; - for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { - ResourceLocation replacement = itemReplacement(mapping.key); + @SubscribeEvent public static void items(MissingMappingsEvent event) { + for (MissingMappingsEvent.Mapping mapping : event.getAllMappings(Registries.ITEM)) { + ResourceLocation replacement = itemReplacement(mapping.getKey()); if (replacement != null) mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(replacement))); } } - @SubscribeEvent public static void blocks(RegistryEvent.MissingMappings event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.world.level.block.Block.class))return; - for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { - ResourceLocation replacement = blockReplacement(mapping.key); + @SubscribeEvent public static void blocks(MissingMappingsEvent event) { + for (MissingMappingsEvent.Mapping mapping : event.getAllMappings(Registries.BLOCK)) { + ResourceLocation replacement = blockReplacement(mapping.getKey()); if (replacement != null) mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.BLOCKS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.BLOCKS.getValue(replacement))); } } - @SubscribeEvent public static void biomes(RegistryEvent.MissingMappings event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.world.level.biome.Biome.class))return; - for(RegistryEvent.MissingMappings.Mapping mapping:event.getAllMappings()) { - ResourceLocation replacement=biomeReplacement(mapping.key); - if(replacement!=null)mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.BIOMES.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.BIOMES.getValue(replacement))); + @SubscribeEvent public static void biomes(MissingMappingsEvent event) { + for(MissingMappingsEvent.Mapping mapping:event.getAllMappings(Registries.BIOME)) { + if(!mapping.getKey().getNamespace().equals("minecraft"))continue; + ResourceLocation replacement=biomeReplacement(mapping.getKey()); + ResourceLocation current=replacement==null?mapping.getKey():replacement; + // 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(); + else if(replacement!=null)throw new UnsafeRegistryUpgrade(null); } } - @SubscribeEvent public static void entities(RegistryEvent.MissingMappings> event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.world.entity.EntityType.class))return; - for(RegistryEvent.MissingMappings.Mapping> mapping:event.getAllMappings()) { - ResourceLocation replacement=entityReplacement(mapping.key); - if(replacement!=null)mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.ENTITIES.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.ENTITIES.getValue(replacement))); + 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(); + } + } + @SubscribeEvent public static void entities(MissingMappingsEvent event) { + for(MissingMappingsEvent.Mapping> mapping:event.>getAllMappings(Registries.ENTITY_TYPE)) { + ResourceLocation replacement=entityReplacement(mapping.getKey()); + if(replacement!=null)mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getValue(replacement))); } } - @SubscribeEvent public static void memories(RegistryEvent.MissingMappings> event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.world.entity.ai.memory.MemoryModuleType.class))return; - for(RegistryEvent.MissingMappings.Mapping> mapping:event.getAllMappings()) - if(mapping.key.equals(new ResourceLocation("minecraft:opened_doors"))) + @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")))); } - @SubscribeEvent public static void sounds(RegistryEvent.MissingMappings event) { - if(!event.getRegistry().getRegistrySuperType().equals(SoundEvent.class))return; - for(RegistryEvent.MissingMappings.Mapping mapping:event.getAllMappings()) { - ResourceLocation replacement=soundReplacement(mapping.key); + @SubscribeEvent public static void sounds(MissingMappingsEvent event) { + for(MissingMappingsEvent.Mapping mapping:event.getAllMappings(Registries.SOUND_EVENT)) { + ResourceLocation replacement=soundReplacement(mapping.getKey()); if(replacement!=null)mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS.getValue(replacement))); } } diff --git a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java index bb6cd2b..6062996 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java +++ b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java @@ -11,7 +11,6 @@ import net.minecraft.nbt.Tag; import net.minecraft.util.datafix.fixes.References; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.TextComponent; /** Shields owned payloads while vanilla converts its own chunk and item schemas. */ final class NativeSafeFixer implements DataFixer { @@ -253,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: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: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"); @@ -283,7 +282,7 @@ static CompoundTag stageCorpse(CompoundTag raw,CompoundTag context) { // Anvil may save this compound before an entity reader runs. safe.remove("LootableBodiesUpgradePayload");safe.remove("LootableBodiesInventorySource"); if(safe.contains("CustomName",8) && context.getInt("DataVersion")<1514) - safe.putString("CustomName",Component.Serializer.toJson(new TextComponent(safe.getString("CustomName")))); + safe.putString("CustomName",Component.Serializer.toJson(Component.literal(safe.getString("CustomName")))); return safe; } private static boolean list(Tag nbt){return nbt instanceof ListTag && (((ListTag)nbt).isEmpty() || ((ListTag)nbt).getElementType()==10);} diff --git a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java index 24db4d9..e59dc17 100644 --- a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java +++ b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java @@ -340,6 +340,7 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co else if(marker==6)version=2586; else if(marker==7)version=2730; else if(marker==8)version=2975; + else if(marker==9)version=3337; if(legacy) {if(version!=512)context.putBoolean("Inferred",true);version=512;} if(legacy || (marker>=1 && marker<=UpgradePreservation.DATA_VERSION)) context.putInt("CorpseWriterDataVersion",version); @@ -348,7 +349,8 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co for(int i=0;i event){ + keepsake=new Item(new Item.Properties()) { + @Override public ICapabilityProvider initCapabilities(ItemStack stack,CompoundTag nbt){return new Stored();} + }.setRegistryName("lootablebodiesbuildtest:keepsake"); + event.getRegistry().register(keepsake); + } + } + private static final class Stored implements ICapabilitySerializable { + private CompoundTag data=new CompoundTag(); + Stored(){data.putString("owner","Disposable fixture");data.putLong("number",9876543210123L);data.putByteArray("opaque",new byte[]{0,1,-1,17});} + 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();} + } +} diff --git a/src/nativeSeed118/java/cyano/lootable/fixture/NativeWorldSeeder118.java b/src/nativeSeed118/java/cyano/lootable/fixture/NativeWorldSeeder118.java new file mode 100644 index 0000000..e0e398f --- /dev/null +++ b/src/nativeSeed118/java/cyano/lootable/fixture/NativeWorldSeeder118.java @@ -0,0 +1,130 @@ +package cyano.lootable.fixture; + +import com.mojang.authlib.GameProfile; +import cyano.lootable.LootableBodies; +import cyano.lootable.entities.EntityLootableBody; +import cyano.lootable.buildtest.FixtureItems; +import java.io.*; +import java.util.*; +import net.minecraft.core.*; +import net.minecraft.nbt.*; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.resources.*; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.*; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.animal.Pig; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.entity.npc.*; +import net.minecraft.world.entity.vehicle.MinecartChest; +import net.minecraft.world.item.*; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.*; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.storage.*; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +/** Compiled against released18 source; runtime contains public34EF, never target19. */ +@Mod("lootablebodiesbuildtest") +public final class NativeWorldSeeder118 { + private static final UUID PLAYER=UUID.fromString("00000000-2222-4222-8222-000000000001"); + public NativeWorldSeeder118(){FixtureItems.register(net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus());net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);} + @SubscribeEvent public void started(ServerStartedEvent event){ + MinecraftServer server=event.getServer(); + try{seed(server);}catch(Throwable failure){failure.printStackTrace();}finally{server.halt(false);} + } + private static void seed(MinecraftServer server)throws Exception { + String minecraft=server.getServerVersion(),version=net.minecraftforge.fml.ModList.get().getModContainerById("lootablebodies").orElseThrow().getModInfo().getVersion().toString(); + require(minecraft.equals("1.18.2") && version.equals("2.4.9.118021") && version.equals(System.getProperty("lootablebodies.fixture.version")),"Wrong public18 native writer identity"); + LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1; + CompoundTag manifest=new CompoundTag();manifest.putString("Minecraft",minecraft);manifest.putString("ModVersion",version); + ListTag bodies=new ListTag(),containers=new ListTag(),vanilla=new ListTag(),biomes=new ListTag(); + for(int dimension:new int[]{0,-1,1}) { + ServerLevel world=server.getLevel(dimensionKey(dimension)); + for(int wave=0;wave<3;wave++) { + int coordinate=wave==0?128:wave==1?4096:8192; + awaitChunk(world,coordinate>>4,coordinate>>4); + LevelChunk chunk=world.getChunk(coordinate>>4,coordinate>>4); + Registry registry=world.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY); + List> choices=Arrays.asList(registry.getHolderOrThrow(Biomes.PLAINS),registry.getHolderOrThrow(Biomes.DESERT),registry.getHolderOrThrow(Biomes.FOREST),registry.getHolderOrThrow(Biomes.TAIGA)); + ListTag sections=new ListTag(); + for(int sectionIndex=0;sectionIndex choice=choices.get(Math.floorMod(sectionY+yy+zz+xx,choices.size())); + section.getBiomes().set(xx,yy,zz,choice);names.add(StringTag.valueOf(choice.unwrapKey().orElseThrow().location().toString())); + } + CompoundTag savedSection=new CompoundTag();savedSection.putInt("Y",sectionY);savedSection.put("Names",names);sections.add(savedSection); + } + chunk.setUnsaved(true); + CompoundTag biomeRecord=new CompoundTag();biomeRecord.putInt("Dimension",dimension);biomeRecord.putInt("Wave",wave);biomeRecord.putInt("ChunkX",coordinate>>4);biomeRecord.putInt("ChunkZ",coordinate>>4);biomeRecord.put("Sections",sections);biomes.add(biomeRecord); + EntityLootableBody body=new EntityLootableBody(world); + body.setUUID(UUID.nameUUIDFromBytes((minecraft+"/"+version+"/"+dimension+"/"+wave).getBytes("UTF-8"))); + body.setPos(coordinate+.5,80,coordinate+.5);body.setNoGravity(true);body.setUserName("SavedFixtureOwner");body.setRotation(37);body.setHealth(7.5F); + for(int slot=0;slot<48;slot++)body.setItem(slot,item(slot)); + for(int slot=70;slot<73;slot++)body.addItem(item(slot)); + if(Boolean.getBoolean("lootablebodies.fixture.recovery")) { + CompoundTag damaged=saved(body);ListTag equipment=damaged.getList("Equipment",10); + equipment.getCompound(20).putString("id","lootablebodiesbuildtest:returning_item"); + CompoundTag duplicate=equipment.getCompound(21).copy();duplicate.putByte("Count",(byte)9);duplicate.getCompound("tag").putString("extension","duplicate slot");equipment.add(duplicate); + CompoundTag invalid=equipment.getCompound(22).copy();invalid.putInt("Slot",999);invalid.putByte("Count",(byte)7);invalid.getCompound("tag").putString("extension","invalid slot");equipment.add(invalid); + damaged.putInt("Name",17);damaged.putDouble("DeathTime",Double.NaN);damaged.putString("Health","opaque health");damaged.putString("ActiveEffects","opaque effects");damaged.putString("HurtTime","opaque hurt timer");damaged.putDouble("FallDistance",Double.POSITIVE_INFINITY); + body.load(damaged);body.setPos(coordinate+.5,80,coordinate+.5); + } + java.lang.reflect.Field vacuum=EntityLootableBody.class.getDeclaredField("vacuumTime");vacuum.setAccessible(true);vacuum.setByte(body,(byte)20); + require(world.addFreshEntity(body),"Historical18 corpse admission"); + CompoundTag record=new CompoundTag();record.putInt("Dimension",dimension);record.putInt("Wave",wave);record.put("Body",saved(body));bodies.add(record); + BlockPos chestPos=new BlockPos(coordinate+2,80,coordinate+2);world.setBlockAndUpdate(chestPos,Blocks.CHEST.defaultBlockState()); + ChestBlockEntity chest=(ChestBlockEntity)world.getBlockEntity(chestPos);for(int slot=0;slot<16;slot++)chest.setItem(slot,item(slot+6)); + CompoundTag chestRecord=new CompoundTag();chestRecord.putInt("Dimension",dimension);chestRecord.putInt("Wave",wave);chestRecord.put("Tile",chest.saveWithFullMetadata());containers.add(chestRecord); + if(wave==0)seedVanilla(world,dimension,coordinate,vanilla); + } + } + ServerLevel world=server.getLevel(Level.OVERWORLD);ServerPlayer player=new ServerPlayer(server,world,new GameProfile(PLAYER,"UpgradeFixture"));player.setPos(136.5,82,136.5); + for(int slot=0;slot<36;slot++)player.getInventory().setItem(slot,item(slot+6)); + for(int slot=0;slot<4;slot++)player.setItemSlot(new EquipmentSlot[]{EquipmentSlot.HEAD,EquipmentSlot.CHEST,EquipmentSlot.LEGS,EquipmentSlot.FEET}[slot],item(slot)); + player.setItemSlot(EquipmentSlot.OFFHAND,item(5));for(int slot=0;slot<27;slot++)player.getEnderChestInventory().setItem(slot,item(slot+6)); + player.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(server,new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND),player){@Override public void send(net.minecraft.network.protocol.Packet packet){}}; + net.minecraft.world.item.crafting.Recipe sugar=server.getRecipeManager().byKey(new ResourceLocation("minecraft:sugar_from_sugar_cane")).orElseThrow();player.awardRecipes(Collections.singleton(sugar)); + net.minecraft.advancements.Advancement advancement=server.getAdvancements().getAdvancement(new ResourceLocation("minecraft:recipes/misc/sugar_from_sugar_cane"));require(advancement!=null,"Missing native18 sugar advancement"); + for(String criterion:advancement.getCriteria().keySet())player.getAdvancements().award(advancement,criterion);player.getAdvancements().save();playerData(server).save(player); + manifest.put("BiomeSamples",biomes);manifest.putString("Recipe","minecraft:sugar_from_sugar_cane");manifest.putString("Advancement","minecraft:recipes/misc/sugar_from_sugar_cane");manifest.putUUID("Player",PLAYER);manifest.put("Bodies",bodies);manifest.put("Containers",containers);manifest.put("Vanilla",vanilla); + server.saveAllChunks(true,true,false);File folder=server.getWorldPath(LevelResource.ROOT).toFile(); + try(FileOutputStream out=new FileOutputStream(new File(folder,"upgrade-fixtures.nbt"))){NbtIo.writeCompressed(manifest,out);} + try(FileWriter out=new FileWriter(new File(folder,"legacy-seed.pass"))){out.write(minecraft+" "+version+"\n");} + System.out.println("LOOTABLE_BODIES_LEGACY_SEED_PASS minecraft="+minecraft+" mod="+version+" corpses=9 dimensions=3"); + } + private static void seedVanilla(ServerLevel world,int dimension,int c,ListTag manifest){ + VillagerProfession[] professions={VillagerProfession.FARMER,VillagerProfession.LIBRARIAN,VillagerProfession.CLERIC,VillagerProfession.ARMORER,VillagerProfession.BUTCHER,VillagerProfession.NITWIT}; + for(int i=0;i dimensionKey(int id){if(id==0)return Level.OVERWORLD;if(id==-1)return Level.NETHER;if(id==1)return Level.END;throw new IllegalArgumentException("dimension "+id);} + private static void awaitChunk(ServerLevel world,int x,int z){world.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.ChunkStatus.FULL,true);require(world.getChunkSource().hasChunk(x,z),"Native18 chunk admission");} + private static PlayerDataStorage playerData(MinecraftServer server)throws Exception {java.lang.reflect.Field found=null;for(Class owner=server.getClass();owner!=null;owner=owner.getSuperclass())for(java.lang.reflect.Field f:owner.getDeclaredFields())if(f.getType()==PlayerDataStorage.class){require(found==null,"Ambiguous native18 player storage");found=f;}require(found!=null,"Missing native18 player storage");found.setAccessible(true);return (PlayerDataStorage)found.get(server);} + private static ItemStack item(int slot){ + ItemStack stack; + if(slot<4)stack=new ItemStack(new Item[]{Items.DIAMOND_HELMET,Items.DIAMOND_CHESTPLATE,Items.DIAMOND_LEGGINGS,Items.DIAMOND_BOOTS}[slot]); + else if(slot==4)stack=new ItemStack(Items.DIAMOND_SWORD);else if(slot==5)stack=new ItemStack(Items.SHIELD);else if(slot==6)stack=new ItemStack(Items.PIG_SPAWN_EGG,3);else if(slot==7)stack=new ItemStack(Blocks.CHEST,2);else if(slot==8)stack=new ItemStack(Items.LIME_BANNER,2);else if(slot==9)stack=new ItemStack(FixtureItems.keepsake,11);else if(slot==10)stack=new ItemStack(Items.WHITE_BED);else if(slot==11)stack=new ItemStack(Items.LIME_BED);else if(slot==12)stack=new ItemStack(Items.OAK_SIGN,11);else stack=new ItemStack(Items.DIAMOND,slot%32+1); + CompoundTag tag=new CompoundTag();tag.putString("fixture_slot","original-"+slot);tag.putLong("typed_long",1234567890123L);tag.putByteArray("typed_bytes",new byte[]{0,-1,17});tag.putLongArray("typed_longs",new long[]{Long.MIN_VALUE,slot,Long.MAX_VALUE}); + if(slot==6){CompoundTag pig=new CompoundTag();pig.putString("id","minecraft:pig");pig.putString("CustomName",net.minecraft.network.chat.Component.Serializer.toJson(new TextComponent("Saved pig egg")));tag.put("EntityTag",pig);} + if(slot==7){CompoundTag tile=new CompoundTag();ListTag items=new ListTag();CompoundTag nested=new ItemStack(Items.EMERALD,17).save(new CompoundTag());nested.putByte("Slot",(byte)3);items.add(nested);tile.put("Items",items);tag.put("BlockEntityTag",tile);} + stack.setTag(tag);if(slot<6)stack.setDamageValue(slot<4?slot+2:slot==4?17:9);stack.setHoverName(new TextComponent("Original keepsake "+slot));if(slot==4)stack.enchant(Enchantments.UNBREAKING,2);return stack; + } + private static void require(boolean condition,String reason){if(!condition)throw new AssertionError(reason);} +} diff --git a/src/nativeSeed118/resources/META-INF/mods.toml b/src/nativeSeed118/resources/META-INF/mods.toml new file mode 100644 index 0000000..695a20c --- /dev/null +++ b/src/nativeSeed118/resources/META-INF/mods.toml @@ -0,0 +1,13 @@ +modLoader="javafml" +loaderVersion="[40,41)" +license="LGPL-2.1-only" +[[mods]] +modId="lootablebodiesbuildtest" +version="1" +displayName="Disposable historical world writer" +[[dependencies.lootablebodiesbuildtest]] +modId="lootablebodies" +mandatory=true +versionRange="[2.4.9.118021]" +ordering="AFTER" +side="BOTH" diff --git a/src/nativeSeed118/resources/pack.mcmeta b/src/nativeSeed118/resources/pack.mcmeta new file mode 100644 index 0000000..1a92065 --- /dev/null +++ b/src/nativeSeed118/resources/pack.mcmeta @@ -0,0 +1 @@ +{"pack":{"pack_format":9,"description":"Disposable historical world writer"}} diff --git a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java index e1c63a6..e188abd 100644 --- a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java @@ -17,8 +17,9 @@ class Native115PacketTest { static { net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap(); } private static final long SEED=0x13572468ABCDEF01L; private ClientboundRespawnPacket packet(ResourceKey dimension,long seed) { - net.minecraft.core.Holder type=RegistryAccess.builtinCopy().registryOrThrow(Registry.DIMENSION_TYPE_REGISTRY).getHolderOrThrow(DimensionType.OVERWORLD_LOCATION); - return new ClientboundRespawnPacket(type,dimension,seed,GameType.SURVIVAL,GameType.CREATIVE,false,true,true); + return new ClientboundRespawnPacket(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD, + dimension,seed,GameType.SURVIVAL,GameType.CREATIVE,false,true, + ClientboundRespawnPacket.KEEP_ALL_DATA,java.util.Optional.empty()); } @Test void native115HashedSeedSurvivesDecodedPacketObservation() throws Exception { FriendlyByteBuf bytes=new FriendlyByteBuf(Unpooled.buffer()); @@ -30,7 +31,7 @@ private ClientboundRespawnPacket packet(ResourceKey dimension,long seed) String expected="RESPAWN dimension=minecraft:the_nether hashedSeed="+SEED; assertEquals(expected,NativeRespawnDescription.describe(outgoing)); assertEquals(expected,NativeRespawnDescription.describe(incoming)); - assertNotNull(incoming.getDimensionType().value(),"Native codec dimension type was lost"); + assertEquals(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD,incoming.getDimensionType(),"Native codec dimension type key was lost"); assertEquals(before.length,bytes.readerIndex()); byte[] after=new byte[before.length];bytes.getBytes(0,after);assertArrayEquals(before,after); } finally {bytes.release();} @@ -49,7 +50,7 @@ private ClientboundRespawnPacket packet(ResourceKey dimension,long seed) NativeRespawnDescription.describe(packet(Level.NETHER,SEED))); } @Test void observationKeepsUnknownDimensionKeysAndEveryDecodedField() throws Exception { - ResourceKey key=ResourceKey.create(Registry.DIMENSION_REGISTRY,new net.minecraft.resources.ResourceLocation("fixture:unhandled")); + ResourceKey key=ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,new net.minecraft.resources.ResourceLocation("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/Native117EntityInboxTest.java b/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java index 909784b..8d3ba2b 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java @@ -49,6 +49,7 @@ public void onTickingStart(Saved e) { } public void onTickingEnd(Saved e) { } public void onTrackingStart(Saved e) { tracked.incrementAndGet(); } public void onTrackingEnd(Saved e) { } + public void onSectionChange(Saved e) { } }, storage); Fixture() { manager.updateChunkStatus(POS, Visibility.TRACKED); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java index 387a98f..d5fc72a 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -/** Execute the pinned native40 spec; config corrections precede mod world hooks. */ +/** Execute the pinned native45 spec; config corrections precede mod world hooks. */ class Native117ForgeConfigTest { private ForgeConfigSpec spec() throws Exception { java.lang.reflect.Field field=ForgeConfig.class.getDeclaredField("serverSpec"); @@ -26,10 +26,11 @@ private Set keys(CommentedConfig config) { spec().correct(config); Set removed=new HashSet<>(previous);removed.removeAll(keys(config)); assertEquals(new HashSet<>(Arrays.asList("removeErroringTileEntities", - "logCascadingWorldGeneration","fixVanillaCascading","dimensionUnloadQueueDelay")),removed); + "logCascadingWorldGeneration","fixVanillaCascading","dimensionUnloadQueueDelay", + "fixAdvancementLoading","treatEmptyTagsAsAir")),removed); assertEquals(new HashSet<>(Arrays.asList("removeErroringBlockEntities","fullBoundingBoxLadders", - "zombieBaseSummonChance","zombieBabyChance","treatEmptyTagsAsAir","fixAdvancementLoading", - "removeErroringEntities","permissionHandler","useItemWithDurationZero","skipEmptyShapelessCheck")),keys(config)); + "zombieBaseSummonChance","zombieBabyChance","advertiseDedicatedServerToLan", + "removeErroringEntities","permissionHandler","useItemWithDurationZero")),keys(config)); assertEquals(false,config.get("server.removeErroringBlockEntities")); assertEquals(0,spec().correct(config),"Native correction must be stable"); } @@ -38,16 +39,18 @@ private Set keys(CommentedConfig config) { config.set("server.zombieBaseSummonChance",0.37D);config.set("server.zombieBabyChance",0.29D); config.set("server.treatEmptyTagsAsAir",true);config.set("server.fixAdvancementLoading",false); config.set("server.removeErroringBlockEntities",true); - config.set("server.removeErroringEntities",true);config.set("server.useItemWithDurationZero",true);config.set("server.skipEmptyShapelessCheck",false); + config.set("server.removeErroringEntities",true);config.set("server.useItemWithDurationZero",true); + config.set("server.advertiseDedicatedServerToLan",false);config.set("server.skipEmptyShapelessCheck",false); spec().correct(config); assertEquals(true,config.get("server.fullBoundingBoxLadders"));assertEquals(0.37D,(Double)config.get("server.zombieBaseSummonChance")); - assertEquals(0.29D,(Double)config.get("server.zombieBabyChance"));assertEquals(true,config.get("server.treatEmptyTagsAsAir")); - assertEquals(false,config.get("server.fixAdvancementLoading"));assertEquals(true,config.get("server.removeErroringBlockEntities")); - assertEquals(true,config.get("server.removeErroringEntities"));assertEquals(true,config.get("server.useItemWithDurationZero"));assertEquals(false,config.get("server.skipEmptyShapelessCheck")); + assertEquals(0.29D,(Double)config.get("server.zombieBabyChance"));assertNull(config.get("server.treatEmptyTagsAsAir")); + assertNull(config.get("server.fixAdvancementLoading"));assertEquals(true,config.get("server.removeErroringBlockEntities")); + assertEquals(true,config.get("server.removeErroringEntities"));assertEquals(true,config.get("server.useItemWithDurationZero")); + assertEquals(false,config.get("server.advertiseDedicatedServerToLan"));assertNull(config.get("server.skipEmptyShapelessCheck")); } @Test void actualNativeDefaultMatchesTheTargetSpecBaseline() throws Exception { CommentedConfig config=CommentedConfig.inMemory();spec().correct(config); - CommentedConfig expected=new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge40-server-default.toml"))); + CommentedConfig expected=new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge45-server-default.toml"))); assertEquals(expected,config); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java index dcb0d5a..f7e3e50 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -/** Native40 loads mods as distinct Java modules: split packages are fatal. */ +/** Native45 loads mods as distinct Java modules: split packages are fatal. */ class Native117ProbeIsolationTest { private Set packages(String archive) throws Exception { Set result=new TreeSet<>(); @@ -22,9 +22,9 @@ private Set packages(String archive) throws Exception { assertFalse(result.isEmpty(),archive);return result; } @Test void integrationModuleHasNoProductionPackageDefinitions() throws Exception { - Set main=packages("build/libs-dev/LootableBodies-2.4.9.118021-deobf.jar"); + Set main=packages("build/libs-dev/LootableBodies-2.4.10.119041-deobf.jar"); Set probe=packages("build/integration-test-mod/lootablebodies-build-tests.jar"); - probe.retainAll(main);assertTrue(probe.isEmpty(),"Native40 module split packages: "+probe); + probe.retainAll(main);assertTrue(probe.isEmpty(),"Native45 module split packages: "+probe); } @Test void independentControlHasNoModOrIntegrationPackages() throws Exception { Set control=packages("build/control-test-mod/forge-platform-control.jar"); diff --git a/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java b/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java new file mode 100644 index 0000000..e5f9bcd --- /dev/null +++ b/src/test/java/cyano/lootable/buildtest/Native119BundleTraceTest.java @@ -0,0 +1,91 @@ +package cyano.lootable.buildtest; + +import io.netty.channel.*; +import io.netty.channel.local.LocalChannel; +import io.netty.buffer.Unpooled; +import java.io.*; +import java.lang.reflect.*; +import java.net.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.TimeUnit; +import net.minecraft.network.*; +import net.minecraft.network.protocol.*; +import net.minecraft.network.protocol.game.*; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.phys.Vec3; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.*; + +/** Real native19 bundling pipeline; the private observer must see decoded wire children. */ +class Native119BundleTraceTest { + @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path temporary; + private ClientboundAddEntityPacket spawn(int id) { + return new ClientboundAddEntityPacket(id,new UUID(1194,id),1,2,3,4,5,EntityType.PIG,6,new Vec3(.1,.2,.3),7); + } + private byte[] bytes(Packet packet) { + FriendlyByteBuf buffer=new FriendlyByteBuf(Unpooled.buffer()); + try {packet.write(buffer);byte[] result=new byte[buffer.readableBytes()];buffer.getBytes(0,result);return result;} + finally {buffer.release();} + } + private List> wire(ClientboundBundlePacket bundle) { + List> wire=new ArrayList<>(); + ConnectionProtocol.PLAY.getBundlerInfo(PacketFlow.CLIENTBOUND).unbundlePacket(bundle,wire::add); + return wire; + } + private final class Fixture implements AutoCloseable { + final EventLoopGroup loop=new DefaultEventLoopGroup(1); + final LocalChannel channel=new LocalChannel(); + final List received=new ArrayList<>(); + final URLClassLoader loader; + final Path log=temporary.resolve("received-packets.txt"); + Fixture() throws Exception { + loader=new URLClassLoader(new URL[]{Path.of("build/integration-test-mod/classes").toUri().toURL()},getClass().getClassLoader()); + channel.attr(BundlerInfo.BUNDLER_PROVIDER).set(ConnectionProtocol.PLAY); + channel.pipeline().addLast("bundler",new PacketBundlePacker(PacketFlow.CLIENTBOUND)); + channel.pipeline().addLast("packet_handler",new ChannelInboundHandlerAdapter(){ + @Override public void channelRead(ChannelHandlerContext context,Object packet){received.add(packet);} + }); + loop.register(channel).sync(); + Connection connection=new Connection(PacketFlow.CLIENTBOUND); + Field field=Connection.class.getDeclaredField("channel");field.setAccessible(true);field.set(connection,channel); + Class observer=Class.forName("cyano.lootable.buildtest.RespawnPacketTrace",true,loader); + Class after=Class.forName("cyano.lootable.buildtest.RespawnPacketTrace$After",true,loader); + Method install=observer.getDeclaredMethod("install",Connection.class,File.class,after);install.setAccessible(true); + install.invoke(null,connection,log.toFile(),null); + } + void receive(List> packets) { + channel.eventLoop().submit(()->packets.forEach(channel.pipeline()::fireChannelRead)).syncUninterruptibly(); + } + @Override public void close() throws Exception { + channel.close().sync();loop.shutdownGracefully(0,2,TimeUnit.SECONDS).sync();loader.close(); + } + } + @Test void actualBundledSpawnIsObservedWithoutChangingNativeDeliveryOrBytes() throws Exception { + ClientboundAddEntityPacket spawn=spawn(12);Packet remove=new ClientboundRemoveEntitiesPacket(12); + ClientboundBundlePacket bundle=new ClientboundBundlePacket(List.of(spawn,remove));byte[] original=bytes(spawn); + try(Fixture fixture=new Fixture()) { + fixture.receive(wire(bundle));assertEquals(1,fixture.received.size()); + ClientboundBundlePacket delivered=assertInstanceOf(ClientboundBundlePacket.class,fixture.received.get(0)); + List> children=new ArrayList<>();delivered.subPackets().forEach(children::add); + assertEquals(List.of(spawn,remove),children);assertArrayEquals(original,bytes(spawn)); + String text=Files.readString(fixture.log); + assertTrue(text.contains(" IN MOD_SPAWN id=12 mod=minecraft ")&&text.contains("uuid="+spawn.getUUID()),"Native bundled spawn disappeared from receiving wire proof"); + assertTrue(text.indexOf(" IN MOD_SPAWN ")> wire=new ArrayList<>(wire(a));wire.add(ordinary);wire.addAll(wire(b)); + try(Fixture fixture=new Fixture()) { + fixture.receive(wire);assertEquals(3,fixture.received.size());assertSame(ordinary,fixture.received.get(1)); + List lines=Files.readAllLines(fixture.log);assertEquals(3,lines.size()); + assertTrue(lines.get(0).contains("uuid="+first.getUUID()));assertTrue(lines.get(1).contains(" IN DESTROY ")); + assertTrue(lines.get(2).contains("uuid="+second.getUUID())); + } + } +} diff --git a/src/test/java/cyano/lootable/buildtest/Native118FixtureGenerator.java b/src/test/java/cyano/lootable/buildtest/Native119FixtureGenerator.java similarity index 59% rename from src/test/java/cyano/lootable/buildtest/Native118FixtureGenerator.java rename to src/test/java/cyano/lootable/buildtest/Native119FixtureGenerator.java index 47aaf0c..5a954ec 100644 --- a/src/test/java/cyano/lootable/buildtest/Native118FixtureGenerator.java +++ b/src/test/java/cyano/lootable/buildtest/Native119FixtureGenerator.java @@ -12,32 +12,34 @@ import net.minecraft.world.level.levelgen.*; import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; import net.minecraft.world.level.storage.PrimaryLevelData; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.levelgen.presets.WorldPresets; +import net.minecraft.world.level.WorldDataConfiguration; import cyano.lootable.upgrade.Native117RegionFixtures; -/** Native18 codec fixture preparation, never authentic historical-writer proof. */ -public final class Native118FixtureGenerator { +/** Native19 codec fixture preparation, never authentic historical-writer proof. */ +public final class Native119FixtureGenerator { public static void main(String[] args)throws Exception { if(args.length!=1)throw new IllegalArgumentException("One new owned output root required"); Path root=Path.of(args[0]);Files.createDirectory(root); new Native117TestEnvironment().launcherSessionOpened(null); - RegistryAccess.Frozen registries=RegistryAccess.BUILTIN.get(); - Registry biomes=registries.registryOrThrow(Registry.BIOME_REGISTRY); - Registry structures=registries.registryOrThrow(Registry.STRUCTURE_SET_REGISTRY); - WorldGenSettings generation=new WorldGenSettings(4267321L,false,false,WorldGenSettings.withOverworld( - registries.registryOrThrow(Registry.DIMENSION_TYPE_REGISTRY),DimensionType.defaultDimensions(registries,4267321L), - new FlatLevelSource(structures,FlatLevelGeneratorSettings.getDefault(biomes,structures)))); - LevelSettings settings=new LevelSettings("Native18 disposable fixture",GameType.CREATIVE,false,Difficulty.NORMAL,true,new GameRules(),DataPackConfig.DEFAULT); - PrimaryLevelData nativeData=new PrimaryLevelData(settings,generation,Lifecycle.stable()); + WorldDimensions dimensions=Native119Registries.lookup().lookupOrThrow(Registries.WORLD_PRESET) + .getOrThrow(WorldPresets.FLAT).value().createWorldDimensions(); + RegistryAccess registries=Native119Registries.forDimensions(dimensions); + WorldOptions options=new WorldOptions(4267321L,false,false); + LevelSettings settings=new LevelSettings("Native19 disposable fixture",GameType.CREATIVE,false,Difficulty.NORMAL,true,new GameRules(),WorldDataConfiguration.DEFAULT); + PrimaryLevelData nativeData=new PrimaryLevelData(settings,options,PrimaryLevelData.SpecialWorldProperty.FLAT,Lifecycle.stable()); nativeData.setSpawn(new BlockPos(0,80,0),0);nativeData.setGameTime(0);nativeData.setDayTime(6000); CompoundTag data=nativeData.createTag(registries,null);data.putLong("LastPlayed",0); - if(data.getInt("DataVersion")!=2975 || !data.contains("WorldGenSettings",10))throw new AssertionError("Native18 header was not encoded"); + int target=net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + if(data.getInt("DataVersion")!=target || !data.contains("WorldGenSettings",10))throw new AssertionError("Native19 header was not encoded"); CompoundTag header=new CompoundTag();header.put("Data",data); - NbtIo.writeCompressed(header,root.resolve("native118-flat-level.dat").toFile()); + NbtIo.writeCompressed(header,root.resolve("native119-flat-level.dat").toFile()); Path world=root.resolve("controlled-entity-source");Files.createDirectory(world); - CompoundTag originalOwned=new CompoundTag();originalOwned.putString("unknownField","retained-native18-header");originalOwned.putLongArray("unknownData",new long[]{Long.MIN_VALUE,1234567890123L,Long.MAX_VALUE}); + CompoundTag originalOwned=new CompoundTag();originalOwned.putString("unknownField","retained-native19-header");originalOwned.putLongArray("unknownData",new long[]{Long.MIN_VALUE,1234567890123L,Long.MAX_VALUE}); header.put("lootablebodies",originalOwned);NbtIo.writeCompressed(header,world.resolve("level.dat").toFile()); CompoundTag corpse=Native117RegionFixtures.body(UUID.fromString("00000000-1171-4171-8171-000000000099"),7); Native117RegionFixtures.write(world,Native117RegionFixtures.chunk(2730,corpse)); - System.out.println("NATIVE118_CODEC_FIXTURE_PREPARED version=2975 markerSource=7/2730 synthetic=true root="+root); + System.out.println("NATIVE119_CODEC_FIXTURE_PREPARED version="+target+" markerSource=7/2730 synthetic=true root="+root); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native119Registries.java b/src/test/java/cyano/lootable/buildtest/Native119Registries.java new file mode 100644 index 0000000..d605c36 --- /dev/null +++ b/src/test/java/cyano/lootable/buildtest/Native119Registries.java @@ -0,0 +1,26 @@ +package cyano.lootable.buildtest; + +import java.util.List; +import java.util.Optional; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.levelgen.WorldDimensions; + +/** One native vanilla bootstrap per test JVM; never historical world evidence. */ +public final class Native119Registries { + private Native119Registries(){ } + private static final class Prepared { + static final HolderLookup.Provider LOOKUP=net.minecraft.data.registries.VanillaRegistries.createLookup(); + } + public static HolderLookup.Provider lookup(){return Prepared.LOOKUP;} + public static RegistryAccess forDimensions(WorldDimensions dimensions) { + return new RegistryAccess.ImmutableRegistryAccess(List.of(dimensions.dimensions())) { + @Override public Optional> lookup(ResourceKey> key) { + Optional> saved=super.lookup(key); + return saved.isPresent()?saved:Native119Registries.lookup().lookup(key); + } + }; + } +} diff --git a/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java b/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java index 44c6853..03083e7 100644 --- a/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java +++ b/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java @@ -13,11 +13,11 @@ class NativeVanillaDataFixerTest { @Test void capturedVanillaOracleStaysIndependentWhenASyntheticCounterWrapsTheGlobalReader(){ DataFixer nativeOracle=NativeVanillaDataFixer.get();DataFixer previous=net.minecraft.util.datafix.DataFixers.getDataFixer(); try { - net.minecraft.util.datafix.DataFixers.DATA_FIXER=new DataFixer(){ + net.minecraft.util.datafix.DataFixers.dataFixer=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.DATA_FIXER=previous;} + }finally{net.minecraft.util.datafix.DataFixers.dataFixer=previous;} } } diff --git a/src/test/java/cyano/lootable/contracts/BuildContractTest.java b/src/test/java/cyano/lootable/contracts/BuildContractTest.java index 4d1b2f3..2c2e64c 100644 --- a/src/test/java/cyano/lootable/contracts/BuildContractTest.java +++ b/src/test/java/cyano/lootable/contracts/BuildContractTest.java @@ -50,9 +50,10 @@ private String text(String path) throws Exception { assertTrue(info.contains("modId=\"lootablebodies\"")); Properties release=new Properties();try(java.io.InputStream input=Files.newInputStream(Paths.get("gradle.properties"))){release.load(input);} assertTrue(info.contains("version=\""+release.getProperty("mod_version")+"\"")); - assertTrue(info.contains("versionRange=\"[1.18.2]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); + assertTrue(info.contains("versionRange=\"[1.19.4]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); assertFalse(Files.exists(Paths.get("build/resources/main/mcmod.info"))); - assertEquals(9,new com.google.gson.JsonParser().parse(text("build/resources/main/pack.mcmeta")).getAsJsonObject().getAsJsonObject("pack").get("pack_format").getAsInt()); + com.google.gson.JsonObject pack=new com.google.gson.JsonParser().parse(text("build/resources/main/pack.mcmeta")).getAsJsonObject().getAsJsonObject("pack"); + assertEquals(13,pack.get("pack_format").getAsInt());assertEquals(12,pack.get("forge:server_data_pack_format").getAsInt()); } @Test void everyOriginalTranslationSurvivesTheNativeJsonFormat() throws Exception { try(DirectoryStream locales=Files.newDirectoryStream(Paths.get("src/test/resources/legacy-translations"),"*.lang")){ diff --git a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java index 36dfd7a..c5fcb40 100644 --- a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java +++ b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java @@ -14,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.*; class ConfigScreenTest { - @TempDir Path directory; + @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path directory; private LegacyConfiguration load(String contents) throws Exception { Path file = directory.resolve("lootablebodies.cfg"); Files.write(file, contents.getBytes(StandardCharsets.UTF_8)); @@ -126,7 +126,7 @@ private void save() throws Exception { } @Test void corpseLayoutKeepsItsOriginalUnlabelledForeground()throws Exception { cyano.lootable.graphics.CorpseGUIContainer screen=new cyano.lootable.graphics.CorpseGUIContainer( - (cyano.lootable.graphics.CorpseContainer)null,new net.minecraft.world.entity.player.Inventory(null),new net.minecraft.network.chat.TextComponent("Saved owner")); + (cyano.lootable.graphics.CorpseContainer)null,new net.minecraft.world.entity.player.Inventory(null),net.minecraft.network.chat.Component.literal("Saved owner")); java.lang.reflect.Method foreground=cyano.lootable.graphics.CorpseGUIContainer.class.getDeclaredMethod( "renderLabels",com.mojang.blaze3d.vertex.PoseStack.class,int.class,int.class);foreground.setAccessible(true); // The original layout uses all seven rows. Native labels would draw diff --git a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java index b204a65..f60c9e3 100644 --- a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java +++ b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java @@ -19,7 +19,7 @@ /** Synthetic boundary contracts; historical world qualification uses real old writers. */ class NativeUpgradeBoundaryTest { - @TempDir Path world; + @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path world; private static final UUID ID=UUID.fromString("00000000-3333-4333-8333-000000000001"); private static final DataFixer VANILLA=DataFixers.getDataFixer(); @AfterEach void close(){UpgradePreservation.close();} @@ -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(8,saved.getInt("LootableBodiesDataVersion"));assertEquals(ID,saved.getUUID("UUID")); + assertEquals("lootablebodies:corpse",saved.getString("id"));assertEquals(9,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 cf5e74b..edd4f8b 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.9.118021", version); + assertEquals("2.4.10.119041", version); assertEquals("forge", values.getProperty("loader_name")); assertEquals("1", values.getProperty("loader_code")); assertTrue(text("src/main/java/cyano/lootable/LootableBodies.java") @@ -44,7 +44,7 @@ private String text(String path) throws Exception { String source = text("src/main/java/cyano/lootable/LootableBodies.java"); assertTrue(source.startsWith("package cyano.lootable;")); assertTrue(source.contains("new ResourceLocation(\"lootablebodies:corpse\")")); - assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.18.2]\"")); + assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.19.4]\"")); } @Test void maintainedReleaseHasLgplAndRetainsTheOriginalNotice() throws Exception { diff --git a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java index 716b222..84cd7a8 100644 --- a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java +++ b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java @@ -15,7 +15,7 @@ /** Synthetic region files test the preservation boundary, not old-world qualification. */ class UpgradeJournalTest { - @TempDir Path world; + @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path world; private static final UUID ID=UUID.fromString("00000000-1111-4111-8111-000000000001"); private Object scan() throws Exception { try { return Class.forName("cyano.lootable.upgrade.UpgradeJournal").getMethod("scan",Path.class).invoke(null,world); } diff --git a/src/test/java/cyano/lootable/entities/Native119DamageTest.java b/src/test/java/cyano/lootable/entities/Native119DamageTest.java new file mode 100644 index 0000000..b6bf4ab --- /dev/null +++ b/src/test/java/cyano/lootable/entities/Native119DamageTest.java @@ -0,0 +1,118 @@ +package cyano.lootable.entities; + +import com.mojang.serialization.Lifecycle; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.BootstapContext; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.Packet; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.damagesource.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** Native45 damage identities must retain the historical config categories. */ +class Native119DamageTest { + private static DamageSources sources; + private static Entity entity; + private static Holder custom; + @BeforeAll static void nativeSources() throws ReflectiveOperationException { + net.minecraft.SharedConstants.tryDetectVersion(); + net.minecraft.server.Bootstrap.bootStrap(); + MappedRegistry registry=new MappedRegistry<>(Registries.DAMAGE_TYPE,Lifecycle.stable()); + DamageTypes.bootstrap(new BootstapContext() { + public Holder.Reference register(ResourceKey key,DamageType value,Lifecycle life) { + return registry.register(key,value,life); + } + public HolderGetter lookup(ResourceKey> key) { + throw new AssertionError("Damage type bootstrap unexpectedly needs another registry: "+key); + } + }); + // Deliberately shares the vanilla anvil message, but has its own mod identity. + custom=registry.register(ResourceKey.create(Registries.DAMAGE_TYPE,new ResourceLocation("example:custom")), + new DamageType("anvil",0),Lifecycle.stable()); + registry.freeze(); + sources=new DamageSources(new RegistryAccess.ImmutableRegistryAccess(java.util.List.of(registry))); + // No world/entity behavior is used here. Plain JUnit has no loaded + // Forge fluid-type registry, so a real entity constructor requires the + // packaged runtime. This token is only an identity passed by native + // DamageSources factories, not proof of entity construction/gameplay. + Class unsafeType=Class.forName("sun.misc.Unsafe"); + java.lang.reflect.Field field=unsafeType.getDeclaredField("theUnsafe");field.setAccessible(true); + entity=(Entity)unsafeType.getMethod("allocateInstance",Class.class).invoke(field.get(null),SourceEntity.class); + } + @Test void fallingNativeHazardsStayEnvironmentalDespiteTheirEntity() { + for(DamageSource source:new DamageSource[]{sources.anvil(entity),sources.fallingBlock(entity)}) { + assertSame(entity,source.getEntity()); + assertEquals(CorpseDamageRules.Kind.ENVIRONMENT,CorpseDamageRules.kind(source)); + assertTrue(CorpseDamageRules.allows(source,false,false,true,false)); + assertFalse(CorpseDamageRules.allows(source,false,true,false,true)); + } + } + @Test void originalEnvironmentalSourcesRetainTheirCategory() { + for(DamageSource source:new DamageSource[]{sources.inFire(),sources.lightningBolt(),sources.onFire(), + sources.lava(),sources.cactus(),sources.fall(),sources.flyIntoWall(),sources.magic()}) + assertEquals(CorpseDamageRules.Kind.ENVIRONMENT,CorpseDamageRules.kind(source),source.toString()); + } + @Test void nativeIndirectDamageWithNoCausingEntityIsStillAnAttack() { + DamageSource source=sources.thrown(entity,null); + assertNull(source.getEntity());assertSame(entity,source.getDirectEntity()); + assertEquals(CorpseDamageRules.Kind.ATTACK,CorpseDamageRules.kind(source)); + } + @Test void nativeDirectAttacksAndCustomEntitySourcesStayAttacks() { + assertEquals(CorpseDamageRules.Kind.ATTACK,CorpseDamageRules.kind(sources.thorns(entity))); + assertEquals(CorpseDamageRules.Kind.ATTACK,CorpseDamageRules.kind(new DamageSource(custom,entity))); + } + @Test void customNonEntityMessageRetainsHistoricalCategory() { + assertEquals("anvil",custom.value().msgId()); + assertEquals(CorpseDamageRules.Kind.ENVIRONMENT,CorpseDamageRules.kind(new DamageSource(custom))); + } + @Test void historicalMessageFallbackKeepsItsOriginalCaseAndEntityPrecedence() { + for(String name:new String[]{"anvil","CACTUS","fall","fallingBlock","flyIntoWall","inFire","lava","magic","lightningBolt","onFire"}) { + Holder type=Holder.direct(new DamageType(name,0)); + assertEquals(CorpseDamageRules.Kind.ENVIRONMENT,CorpseDamageRules.kind(new DamageSource(type)),name); + assertEquals(CorpseDamageRules.Kind.ATTACK,CorpseDamageRules.kind(new DamageSource(type,entity)),name); + } + } + @Test void customVoidAndWallMessagesRetainTheirSpecialHistoricalHandling() { + DamageSource voidSource=new DamageSource(Holder.direct(new DamageType("outOfWorld",0)),entity); + assertTrue(CorpseDamageRules.allows(voidSource,true,false,false,false)); + assertTrue(CorpseDamageRules.isWall(new DamageSource(Holder.direct(new DamageType("inWall",0))))); + assertTrue(CorpseDamageRules.isWall(sources.inWall())); + assertFalse(CorpseDamageRules.isWall(new DamageSource(Holder.direct(new DamageType("INWALL",0))))); + assertFalse(CorpseDamageRules.isWall(null)); + } + @Test void voidBypassesEveryDamageToggleAndCompleteInvulnerability() { + assertEquals(CorpseDamageRules.Kind.VOID,CorpseDamageRules.kind(sources.outOfWorld())); + assertTrue(CorpseDamageRules.allows(sources.outOfWorld(),true,false,false,false)); + } + @Test void allConfigurationCombinationsSelectOnlyTheirCategory() { + for(int bits=0;bits<8;bits++) { + boolean attack=(bits&1)!=0,environment=(bits&2)!=0,misc=(bits&4)!=0; + assertEquals(attack,CorpseDamageRules.allows(sources.thorns(entity),false,attack,environment,misc)); + assertEquals(environment,CorpseDamageRules.allows(sources.anvil(entity),false,attack,environment,misc)); + assertEquals(misc,CorpseDamageRules.allows(sources.generic(),false,attack,environment,misc)); + assertFalse(CorpseDamageRules.allows(sources.anvil(entity),true,attack,environment,misc)); + } + } + @Test void otherNativeHazardsKeepTheHistoricalMiscSetting() { + for(DamageSource source:new DamageSource[]{sources.hotFloor(),sources.drown(),sources.wither(),sources.freeze()}) + assertEquals(CorpseDamageRules.Kind.MISC,CorpseDamageRules.kind(source)); + assertEquals(CorpseDamageRules.Kind.MISC,CorpseDamageRules.kind(null)); + } + private static final class SourceEntity extends Entity { + SourceEntity(){super(EntityType.FALLING_BLOCK,null);} + protected void defineSynchedData(){ } + protected void readAdditionalSaveData(CompoundTag tag){ } + protected void addAdditionalSaveData(CompoundTag tag){ } + public Packet getAddEntityPacket(){throw new UnsupportedOperationException("No network in this unit fixture");} + } +} diff --git a/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java index 24b977e..82c065b 100644 --- a/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java +++ b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java @@ -15,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.*; class Native117ProfileLookupTest { - @TempDir Path directory; + @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path directory; final GameProfile initial=new GameProfile(null,"Owner"); GameProfile cached=new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000201"),"Owner"); GameProfile added; diff --git a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java index 093c6ac..be729a0 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(2976))); + CompoundTag raw=item("minecraft:stone",1);raw.putString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(3338))); } @Test void unrecognisedMetadataAndEnchantmentsRemainRecoverable() { assertNull(ItemMigration.convertNbt(item("minecraft:stone",99),context(1343))); diff --git a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java index a2dcafa..bfd545f 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java @@ -28,14 +28,14 @@ private CompoundTag body(int marker) { body.put("Equipment", equipment); ListTag aux = new ListTag(); aux.add(item(47)); body.put("Aux", aux); return body; } - @Test void currentFormatNamesThe118VanillaSchema() { - assertEquals(2975, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(8, UpgradePreservation.DATA_VERSION); + @Test void currentFormatNamesThe119VanillaSchema() { + assertEquals(3337, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(9, UpgradePreservation.DATA_VERSION); } @Test void released113EquipmentAndOverflowAreRetainedBeforeNativeConversion() { CompoundTag original = body(3), before = original.copy(); CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(8, staged.getInt(UpgradePreservation.VERSION)); + assertEquals(9, 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,12 +53,12 @@ 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(8, staged.getInt(UpgradePreservation.VERSION)); + assertEquals(9, staged.getInt(UpgradePreservation.VERSION)); assertEquals(record, staged.getList("LootableBodiesUnresolved", 10).getCompound(49)); assertEquals(record, original.getList("LootableBodiesUnresolved", 10).getCompound(0)); } @Test void currentRecordsAreNotWrappedOrDuplicatedAgain() { - CompoundTag original = body(8); + CompoundTag original = body(9); assertEquals(original, NativeSafeFixer.stageCorpse(original, context(1976))); } @Test void vanilla114ItemRenamesRunForReleased113InventoryCopies() { diff --git a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java index e8d2236..9af9ed3 100644 --- a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java @@ -24,17 +24,17 @@ private CompoundTag chunk(int[] biomes) { root.put("Level", level); return root; } private CompoundTag convert(CompoundTag root, int source) { - return NbtUtils.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), - DataFixTypes.CHUNK, root, source, 2586); + return DataFixTypes.CHUNK.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), + root, source, 2586); } private int[] currentBiomes() { int[] values = new int[1024]; for (int i = 0; i < values.length; i++) values[i] = i; return values; } - @Test void currentFormatUsesThe118SchemaAndItsOwnMarker() { - assertEquals(2975, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(8, UpgradePreservation.DATA_VERSION); + @Test void currentFormatUsesThe119SchemaAndItsOwnMarker() { + assertEquals(3337, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(9, 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(8, staged.getInt(UpgradePreservation.VERSION)); + assertEquals(9, 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/Native116UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java index 337135f..2ffcb6f 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java @@ -34,8 +34,8 @@ private CompoundTag legacy() { } body.put("Equipment",inventory);return body; } - @Test void currentFormatUsesThe118SchemaAndMarker() { - assertEquals(2975,UpgradePreservation.MINECRAFT_VERSION);assertEquals(8,UpgradePreservation.DATA_VERSION); + @Test void currentFormatUsesThe119SchemaAndMarker() { + assertEquals(3337,UpgradePreservation.MINECRAFT_VERSION);assertEquals(9,UpgradePreservation.DATA_VERSION); } @Test void legacyCorpseUuidSurvivesShieldingWithoutTruncatingCustomInventory() { CompoundTag body=legacy(),before=body.copy(),context=new CompoundTag();context.putInt("DataVersion",2230); @@ -75,7 +75,7 @@ private CompoundTag legacy() { CompoundTag root=new CompoundTag();root.putString("LevelName","Disposable fixture"); root.put("WorldGenSettings",settings());CompoundTag before=root.copy(); CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), - net.minecraft.util.datafix.DataFixTypes.LEVEL.getType(),root,-1,2586); + References.LEVEL,root,-1,2586); assertEquals(before.get("WorldGenSettings"),fixed.get("WorldGenSettings"));assertEquals(before,root); } @Test void registrationProtectsTheNativeLevelReaderBeforeServerCreation() throws Exception { diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java index 564b322..fa3c9c0 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,2975); + return NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,root,source,3337); } @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(8,fixed.getInt(UpgradePreservation.VERSION));assertEquals(2586,fixed.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(9,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")); @@ -36,8 +36,8 @@ private CompoundTag convert(CompoundTag root,int source) { assertEquals(before,raw);assertArrayEquals(bytes,Files.readAllBytes(region)); } @Test void currentMarkerProtectsNativeRootAgainstDamagedStamps() { - CompoundTag corpse=body(ID,8); - for(int version:new int[]{-1,0,512,2230,2586,2730,2975}) { + CompoundTag corpse=body(ID,9); + for(int version:new int[]{-1,0,512,2230,2586,2730,2975,3337}) { CompoundTag raw=chunk(version,corpse),before=raw.copy(); CompoundTag fixed=convert(raw,version); assertEquals(corpse,fixed.getList("Entities",10).getCompound(0));assertEquals(before,raw); @@ -69,39 +69,39 @@ public Dynamic update(DSL.TypeReference type,Dynamic input,int from,in Path candidate=world.resolve("candidate"),region=write(candidate,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); AtomicInteger calls=new AtomicInteger(); try { - DataFixers.DATA_FIXER=observe(nativeFixer,calls,previous,true); + DataFixers.dataFixer=observe(nativeFixer,calls,previous,true); assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(candidate)); assertTrue(calls.get()>0);assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); - } finally {DataFixers.DATA_FIXER=original;} + } finally {DataFixers.dataFixer=original;} } @Test void successfulPreflightConvertsOneImmutableCopyPerChunkNotPerBody() throws Exception { DataFixer original=DataFixers.getDataFixer(),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.DATA_FIXER=observe(nativeFixer,calls,null,false); + DataFixers.dataFixer=observe(nativeFixer,calls,null,false); UpgradeJournal journal=UpgradePreservation.prepareEarly(world);assertEquals(2,journal.size());assertEquals(1,calls.get()); assertSame(journal,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); - } finally {DataFixers.DATA_FIXER=original;} + } finally {DataFixers.dataFixer=original;} } @Test void unchangedIndexCannotReuseAnOldPreflightAfterTheFixerChanges() throws Exception { DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); Path region=write(world,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); UpgradeJournal previous=UpgradePreservation.prepareEarly(world);AtomicInteger calls=new AtomicInteger(); try { - DataFixers.DATA_FIXER=observe(nativeFixer,calls,previous,true); + DataFixers.dataFixer=observe(nativeFixer,calls,previous,true); assertThrows(java.io.IOException.class,()->UpgradePreservation.prepareEarly(world));assertTrue(calls.get()>0); assertSame(previous,UpgradePreservation.journal());assertArrayEquals(bytes,Files.readAllBytes(region)); - } finally {DataFixers.DATA_FIXER=original;} + } finally {DataFixers.dataFixer=original;} } @Test void actualServerFixerIsValidatedAfterTheEarlyLevelHook() throws Exception { DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); Path region=write(world,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); UpgradeJournal previous=UpgradePreservation.prepareEarly(world);AtomicInteger calls=new AtomicInteger(); try { - DataFixers.DATA_FIXER=observe(nativeFixer,calls,previous,true); + 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.DATA_FIXER=original;} + } finally {DataFixers.dataFixer=original;} } } diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java index 52476cf..5d57ab3 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java @@ -14,7 +14,7 @@ /** Native RegionFile codec and entity-chunk shapes; not historical-world qualification. */ class Native117EntityRegionTest { - @TempDir Path world; + @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path world; private static final UUID ID=UUID.fromString("00000000-1171-4171-8171-000000000001"); private CompoundTag body(UUID id) { CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse");body.putUUID("UUID",id); diff --git a/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java index 169b849..ba9f8b2 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java @@ -14,7 +14,7 @@ /** Actual native oversized-chunk writer, never historical-world provenance. */ class Native117ExternalSidecarTest { - @TempDir Path temporary; + @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path temporary; @Test void actualNativeExternalChunkRefusesConservativelyWithAllOriginalBytesIntact() throws Exception { String exported=System.getProperty("lootablebodies.exportNativeExternalWorld"); Path world=exported==null?temporary.resolve("world"):Paths.get(exported); diff --git a/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java index d7bfbb2..48a4f7e 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java @@ -10,19 +10,19 @@ class Native117RegistryRenameTest { final ResourceLocation dirtPath=new ResourceLocation("minecraft:dirt_path"); @Test void grassPathBlockUsesTheActualNativeDirtPath() { assertEquals(dirtPath,LegacyRegistryMappings.blockReplacement(oldPath)); - assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH,Registry.BLOCK.get(dirtPath)); + 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"))); } @Test void grassPathItemUsesTheActualNativeDirtPathItem() { assertEquals(dirtPath,LegacyRegistryMappings.itemReplacement(oldPath)); - assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH.asItem(),Registry.ITEM.get(dirtPath)); + 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"))); } @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"); assertEquals(current,LegacyRegistryMappings.soundReplacement(old)); - assertSame(net.minecraft.sounds.SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES,Registry.SOUND_EVENT.get(current)); + 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"))); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java index 8b56bce..a42f794 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java @@ -1,7 +1,7 @@ package cyano.lootable.upgrade; import com.mojang.serialization.Dynamic; -import net.minecraft.data.BuiltinRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.StringTag; import net.minecraft.resources.ResourceLocation; @@ -21,7 +21,8 @@ private String nativeName(String name,int source){ CavesAndCliffsRenames.RENAMES.forEach((old,current)->{ assertEquals(current,nativeName(old,2730),old); ResourceLocation target=new ResourceLocation(current); - assertTrue(BuiltinRegistries.BIOME.containsKey(target),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); }); } @@ -30,7 +31,8 @@ private String nativeName(String name,int source){ String current=CavesAndCliffsRenames.RENAMES.getOrDefault(renamed,renamed); if(current.equals("minecraft:deep_warm_ocean"))current="minecraft:warm_ocean"; ResourceLocation target=new ResourceLocation(current); - assertTrue(BuiltinRegistries.BIOME.containsKey(target),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); }); } diff --git a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java index 64e58af..e7230bb 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java @@ -31,7 +31,7 @@ private Object transformer()throws Exception { assertTrue(config.has("lootablebodies_preservation"),"Production early-read attachment is missing"); path=config.get("lootablebodies_preservation").getAsString(); } - engine=Native118CoremodEngineTest.actualEngine();assertNotNull(engine,"Use the actual target Forge JavaScript engine/filter"); + engine=Native119CoremodEngineTest.actualEngine();assertNotNull(engine,"Use the actual target Forge JavaScript engine/filter"); try(InputStream input=getClass().getClassLoader().getResourceAsStream(path)) { assertNotNull(input);engine.eval(new InputStreamReader(input,StandardCharsets.UTF_8)); } diff --git a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java index b096557..e8aa6cc 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); assertEquals(1,journal.size()); - CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,2975); + CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3337); CompoundTag actual=converted.getList("Entities",10).getCompound(0); - assertEquals(8,actual.getInt(UpgradePreservation.VERSION)); + assertEquals(9,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(2976,body(new UUID(118,172),7)),before=raw.copy(); + CompoundTag raw=chunk(3338,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)); 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); - CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,2975).getList("Entities",10).getCompound(0); - assertEquals(8,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,3337).getList("Entities",10).getCompound(0); + assertEquals(9,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 ddb6e42..f7f4672 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,2975,2975); + separate?References.ENTITY_CHUNK:References.CHUNK,raw,3337,3337); } private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { - CompoundTag root=chunk(2975,body); + CompoundTag root=chunk(3337,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")); @@ -31,7 +31,7 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { private CompoundTag first(CompoundTag root,boolean separate){return root.getList(separate?"Entities":"entities",10).getCompound(0);} @Test void malformedAxisUsesContainingRegionBeforeEitherNativeLoaderConstructsAnEntity() { for(boolean separate:new boolean[]{false,true}) { - CompoundTag body=body(ID,8);body.put("Pos",pos(Double.NaN,80,160.5)); + CompoundTag body=body(ID,9);body.put("Pos",pos(Double.NaN,80,160.5)); CompoundTag raw=root(body,separate,-11,10),before=raw.copy();CompoundTag converted=fixed(raw,separate),actual=first(converted,separate); assertEquals(pos(-167.5,80,160.5),actual.get("Pos"));assertEquals(before,raw); CompoundTag record=actual.getList(RECOVERY,10).getCompound(0);assertEquals("Pos",record.getString("Source"));assertEquals(body.get("Pos"),record.get("Data")); @@ -40,18 +40,18 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { } } @Test void finiteMismatchedRootAndMissingPositionCannotBeIndexedInTheWrongSection() { - CompoundTag body=body(ID,8);body.put("Pos",pos(160.5,80,160.5)); + CompoundTag body=body(ID,9);body.put("Pos",pos(160.5,80,160.5)); assertEquals(pos(-167.5,80,160.5),first(fixed(root(body,true,-11,10),true),true).get("Pos")); body.remove("Pos");CompoundTag actual=first(fixed(root(body,true,-11,10),true),true); assertEquals(pos(-167.5,64,168.5),actual.get("Pos"));assertFalse(actual.contains(RECOVERY),"Missing data invented a raw record"); } @Test void passengersKeepValidCrossChunkPositionsAndUnknownRecoveryRecords() { - CompoundTag rider=body(new UUID(117,172),8);rider.put("Pos",pos(17,80,8)); - CompoundTag body=body(ID,8);body.put("Pos",pos(8,80,8));ListTag riders=new ListTag();riders.add(rider);body.put("Passengers",riders); + CompoundTag rider=body(new UUID(117,172),9);rider.put("Pos",pos(17,80,8)); + CompoundTag body=body(ID,9);body.put("Pos",pos(8,80,8));ListTag riders=new ListTag();riders.add(rider);body.put("Passengers",riders); assertEquals(body,first(fixed(root(body,true,0,0),true),true)); } @Test void rawRecoveryTagSurvivesPositionRepairExactlyOnce() { - CompoundTag body=body(ID,8);body.putString("Pos","opaque position");body.putString(RECOVERY,"future raw recovery"); + CompoundTag body=body(ID,9);body.putString("Pos","opaque position");body.putString(RECOVERY,"future raw recovery"); CompoundTag result=fixed(root(body,true,0,0),true),actual=first(result,true);ListTag records=actual.getList(RECOVERY,10); assertEquals(2,records.size());assertEquals("Recovery",records.getCompound(0).getString("Source")); assertEquals(StringTag.valueOf("future raw recovery"),records.getCompound(0).get("Data")); @@ -59,12 +59,12 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { assertEquals(result,fixed(result,true)); } @Test void impossibleContainingCoordinatesRefuseBeforeNativeConstruction() { - CompoundTag raw=root(body(ID,8),true,Integer.MAX_VALUE,0),before=raw.copy(); + CompoundTag raw=root(body(ID,9),true,Integer.MAX_VALUE,0),before=raw.copy(); assertThrows(IllegalStateException.class,()->fixed(raw,true));assertEquals(before,raw); } @Test void verticalValuesOutsideTheNativeLoadLimitRetainRawDataBeforeClamping() { for(boolean separate:new boolean[]{false,true})for(double y:new double[]{-20000000.01,20000000.01,-3e7,3e7}) { - CompoundTag body=body(ID,8);body.put("Pos",pos(8.5,y,8.5));CompoundTag raw=root(body,separate,0,0),before=raw.copy(); + CompoundTag body=body(ID,9);body.put("Pos",pos(8.5,y,8.5));CompoundTag raw=root(body,separate,0,0),before=raw.copy(); CompoundTag actual=first(fixed(raw,separate),separate); assertEquals(pos(8.5,64,8.5),actual.get("Pos"));assertEquals(before,raw); assertEquals(body.get("Pos"),actual.getList(RECOVERY,10).getCompound(0).get("Data")); @@ -73,7 +73,7 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { } @Test void nativeHorizontalAndVerticalBoundaryValuesRemainExact() { for(boolean separate:new boolean[]{false,true})for(double y:new double[]{-2e7,2e7}) { - CompoundTag body=body(ID,8);body.put("Pos",pos(3e7,y,3e7)); + CompoundTag body=body(ID,9);body.put("Pos",pos(3e7,y,3e7)); assertEquals(body,first(fixed(root(body,separate,1875000,1875000),separate),separate)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java index 6622cbd..60dd64c 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java @@ -21,7 +21,7 @@ class Native118TerrainBoundaryTest { @TempDir(cleanup=CleanupMode.ON_SUCCESS) Path world; @AfterEach void close(){UpgradePreservation.close();System.out.println("NATIVE118_TERRAIN_FIXTURE="+world);} private CompoundTag root(CompoundTag... bodies) { - CompoundTag root=new CompoundTag();root.putInt("DataVersion",2975);root.putInt("xPos",0);root.putInt("zPos",0);root.putInt("yPos",-4); + CompoundTag root=new CompoundTag();root.putInt("DataVersion",3337);root.putInt("xPos",0);root.putInt("zPos",0);root.putInt("yPos",-4); root.putString("Status","full");root.put("sections",new ListTag()); ListTag list=new ListTag();for(CompoundTag body:bodies)list.add(body.copy());root.put("entities",list);return root; } @@ -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(8,staged.getInt(UpgradePreservation.VERSION)); + CompoundTag staged=fixed.getList("entities",10).getCompound(0);assertEquals(9,staged.getInt(UpgradePreservation.VERSION)); assertEquals(50,staged.getList("LootableBodiesUnresolved",10).size());assertEquals(2730,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); old.remove("__context");assertEquals(before,old);assertArrayEquals(original,Files.readAllBytes(file)); } @@ -91,19 +91,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,2975); + CompoundTag nativeResult=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeCopy,2730,3337); assertFalse(nativeResult.contains("Level"));assertEquals(Integer.MAX_VALUE,nativeResult.getInt("DataVersion")); - CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,2975); - assertFalse(fixed.contains("Level"));assertEquals(2975,fixed.getInt("DataVersion")); + CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,3337); + assertFalse(fixed.contains("Level"));assertEquals(3337,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",2976);CompoundTag before=future.copy(); + CompoundTag future=root();future.putInt("DataVersion",3338);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,2976,2975)); + assertThrows(Error.class,()->NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,future,3338,3337)); assertEquals(before,future);assertArrayEquals(original,Files.readAllBytes(file)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118CoremodEngineTest.java b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java similarity index 97% rename from src/test/java/cyano/lootable/upgrade/Native118CoremodEngineTest.java rename to src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java index e15cf67..6bd15e1 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118CoremodEngineTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java @@ -18,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; /** Use the actual target CoreMods engine and its Java class-access filter. */ -class Native118CoremodEngineTest { +class Native119CoremodEngineTest { static void blackboardFixture()throws Exception { Class launcher=Class.forName("cpw.mods.modlauncher.Launcher"); if(launcher.getField("INSTANCE").get(null)==null) { @@ -36,7 +36,7 @@ static ScriptEngine actualEngine()throws Exception { blackboardFixture(); Class factory=Class.forName("net.minecraftforge.coremod.NashornFactory"); String source=factory.getProtectionDomain().getCodeSource().getLocation().toString(); - assertTrue(source.endsWith("coremods-5.2.5.jar"),source);System.out.println("COREMOD_ENGINE_SOURCE="+source); + assertTrue(source.endsWith("coremods-5.2.4.jar"),source);System.out.println("COREMOD_ENGINE_SOURCE="+source); Method create=factory.getDeclaredMethod("createEngine");create.setAccessible(true); return (ScriptEngine)create.invoke(null); } @@ -70,7 +70,7 @@ private ClassNode transform(Object transformer,ClassNode node)throws Exception { } private Class define(ClassNode node) { ClassWriter writer=new ClassWriter(ClassWriter.COMPUTE_MAXS);node.accept(writer);byte[] bytes=writer.toByteArray(); - class ReaderLoader extends ClassLoader {ReaderLoader(){super(Native118CoremodEngineTest.class.getClassLoader());}Class define(){return defineClass(node.name.replace('/','.'),bytes,0,bytes.length);}} + class ReaderLoader extends ClassLoader {ReaderLoader(){super(Native119CoremodEngineTest.class.getClassLoader());}Class define(){return defineClass(node.name.replace('/','.'),bytes,0,bytes.length);}} return new ReaderLoader().define(); } private void verify(Class reader)throws Exception { diff --git a/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java b/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java new file mode 100644 index 0000000..f3783e5 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java @@ -0,0 +1,56 @@ +package cyano.lootable.upgrade; + +import com.mojang.serialization.Dynamic; +import java.util.*; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.*; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.fixes.References; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.registries.MissingMappingsEvent; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** 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 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); + java.lang.reflect.Field field=mapping.getClass().getDeclaredField("action");field.setAccessible(true); + return (MissingMappingsEvent.Action)field.get(mapping); + } + @Test void knownVanillaBiomeNumericSnapshotsDoNotStandInForDataPackBiomes()throws Exception { + List> keys=cyano.lootable.buildtest.Native119Registries.lookup() + .lookupOrThrow(Registries.BIOME).listElements().map(net.minecraft.core.Holder.Reference::key).toList(); + assertTrue(keys.size()>50); + for(ResourceKey key:keys)assertEquals(MissingMappingsEvent.Action.IGNORE,action(Registries.BIOME,key.location().toString()),key.location().toString()); + for(String old:net.minecraft.util.datafix.fixes.CavesAndCliffsRenames.RENAMES.keySet()) + assertEquals(MissingMappingsEvent.Action.IGNORE,action(Registries.BIOME,old),old); + } + @Test void onlyTheTwoNativeRetiredPoiBindingsAreResolved()throws Exception { + for(String name:List.of("minecraft:unemployed","minecraft:nitwit")) + assertEquals(MissingMappingsEvent.Action.IGNORE,action(Registries.POINT_OF_INTEREST_TYPE,name),name); + } + @Test void unknownOtherModAndUnrelatedMappingsRemainUnresolved()throws Exception { + for(String name:List.of("anothermod:plains","minecraft:unknown_saved_biome")) + assertEquals(MissingMappingsEvent.Action.DEFAULT,action(Registries.BIOME,name),name); + for(String name:List.of("minecraft:farmer","anothermod:nitwit","minecraft:unknown_saved_poi")) + assertEquals(MissingMappingsEvent.Action.DEFAULT,action(Registries.POINT_OF_INTEREST_TYPE,name),name); + assertEquals(MissingMappingsEvent.Action.DEFAULT,action(Registries.ITEM,"minecraft:nitwit")); + } + @Test void actualVanillaPoiFixRemovesOnlyTheRetiredRecordsWithoutMutatingInput() { + CompoundTag root=new CompoundTag(),sections=new CompoundTag(),section=new CompoundTag();ListTag records=new ListTag(); + for(String type:List.of("minecraft:unemployed","minecraft:nitwit","minecraft:farmer")) { + CompoundTag record=new CompoundTag();record.putString("type",type);record.putIntArray("pos",new int[]{0,80,0});record.putInt("free_tickets",1);records.add(record); + } + section.put("Records",records);sections.put("0",section);root.put("Sections",sections);CompoundTag before=root.copy(); + Tag fixed=net.minecraft.util.datafix.DataFixers.getDataFixer().update(References.POI_CHUNK,new Dynamic<>(NbtOps.INSTANCE,root),2975,3337).getValue(); + assertEquals(before,root,"Native conversion must operate on copies"); + ListTag result=((CompoundTag)fixed).getCompound("Sections").getCompound("0").getList("Records",10); + assertEquals(1,result.size());assertEquals("minecraft:farmer",result.getCompound(0).getString("type")); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native118TargetTest.java b/src/test/java/cyano/lootable/upgrade/Native119TargetTest.java similarity index 60% rename from src/test/java/cyano/lootable/upgrade/Native118TargetTest.java rename to src/test/java/cyano/lootable/upgrade/Native119TargetTest.java index e1c759e..5750561 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118TargetTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119TargetTest.java @@ -6,12 +6,19 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -class Native118TargetTest { +class Native119TargetTest { + private static int targetDataVersion() { + net.minecraft.SharedConstants.tryDetectVersion(); + return net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + } @Test void targetMatchesTheActualNativeWorldVersion() { net.minecraft.SharedConstants.tryDetectVersion(); - assertEquals(2975,net.minecraft.SharedConstants.getCurrentVersion().getWorldVersion()); - assertEquals(2975,UpgradePreservation.MINECRAFT_VERSION); - assertEquals(8,UpgradePreservation.DATA_VERSION); + assertEquals("1.19.4",net.minecraft.SharedConstants.getCurrentVersion().getName()); + assertEquals(13,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); + assertEquals(12,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); + assertTrue(targetDataVersion()>2975); + assertEquals(targetDataVersion(),UpgradePreservation.MINECRAFT_VERSION); + assertEquals(9,UpgradePreservation.DATA_VERSION); } @Test void released16MarkerIsMigratedWithAllSlotsAndOriginalsRetained() { assertEquals(2586,cyano.lootable.buildtest.HistoricalVersions.dataVersion("1.16.5")); @@ -25,7 +32,7 @@ class Native118TargetTest { body.put("Equipment",items);CompoundTag before=body.copy(); CompoundTag source=new CompoundTag();source.putInt("DataVersion",2586); CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); - assertEquals(8,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(9,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); @@ -48,7 +55,7 @@ class Native118TargetTest { public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic value,int from,int to){return value;} }; CompoundTag staged=NativeNbt.update(UpgradePreservation.wrapper(identity), - net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,2975); + net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); assertEquals(2586,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); assertEquals(6,body.getInt(UpgradePreservation.VERSION)); } @@ -56,7 +63,7 @@ class Native118TargetTest { 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(8,staged.getInt(UpgradePreservation.VERSION)); + assertEquals(9,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++) { @@ -65,14 +72,38 @@ class Native118TargetTest { } assertEquals(before,body); } - @Test void marker7OverridesDamagedOuterStampWhenTheNative18TargetRuns() { + @Test void marker7OverridesDamagedOuterStampWhenTheNative19TargetRuns() { CompoundTag body=Native117RegionFixtures.body(java.util.UUID.randomUUID(),7),before=body.copy(); com.mojang.datafixers.DataFixer identity=new com.mojang.datafixers.DataFixer() { public com.mojang.datafixers.schemas.Schema getSchema(int version){throw new AssertionError("No schema needed");} public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic value,int from,int to){return value;} }; - CompoundTag staged=NativeNbt.update(UpgradePreservation.wrapper(identity),net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,2975); + CompoundTag staged=NativeNbt.update(UpgradePreservation.wrapper(identity),net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); assertEquals(2730,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); assertEquals(before,body); } + @Test void released18MarkerRetainsItsSourceAndEveryIndexedOriginal() { + assertEquals(2975,cyano.lootable.buildtest.HistoricalVersions.dataVersion("1.18.2")); + 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(9,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++) { + assertEquals("Equipment",records.getCompound(slot).getString("Source")); + assertEquals(before.getList("Equipment",10).getCompound(slot),records.getCompound(slot).getCompound("Data")); + } + assertEquals(before,body); + } + @Test void marker8OverridesDamagedOuterStampWhenTheNative19TargetRuns() { + CompoundTag body=Native117RegionFixtures.body(java.util.UUID.randomUUID(),8),before=body.copy(); + com.mojang.datafixers.DataFixer identity=new com.mojang.datafixers.DataFixer() { + public com.mojang.datafixers.schemas.Schema getSchema(int version){throw new AssertionError("No schema needed");} + public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic value,int from,int to){return value;} + }; + CompoundTag staged=NativeNbt.update(UpgradePreservation.wrapper(identity),net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); + assertEquals(2975,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(before,body); + } } diff --git a/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java b/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java index ae4b58a..201a624 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java +++ b/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java @@ -12,6 +12,7 @@ public static int dataVersion(String minecraft) { if ("1.15.2".equals(minecraft)) return 2230; if ("1.16.5".equals(minecraft)) return 2586; if ("1.17.1".equals(minecraft)) return 2730; + if ("1.18.2".equals(minecraft)) return 2975; throw new IllegalArgumentException("Unsupported historical Minecraft " + minecraft); } } diff --git a/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java b/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java index 44f1f3d..a3bee3c 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java @@ -13,8 +13,10 @@ static String describe(ClientboundRespawnPacket packet) { long seed=0; for(Field field:ClientboundRespawnPacket.class.getDeclaredFields()) { if(field.getType()==ResourceKey.class) { + field.setAccessible(true);ResourceKey key=(ResourceKey)field.get(packet); + if(key==null || !key.isFor(net.minecraft.core.registries.Registries.DIMENSION))continue; if(dimension!=null)throw new AssertionError("Ambiguous respawn world key"); - field.setAccessible(true);dimension=(ResourceKey)field.get(packet); + dimension=key; } else if(field.getType()==long.class) { field.setAccessible(true);seed=field.getLong(packet); } diff --git a/tools/fixtures/forge45-server-default.toml b/tools/fixtures/forge45-server-default.toml new file mode 100644 index 0000000..1858bec --- /dev/null +++ b/tools/fixtures/forge45-server-default.toml @@ -0,0 +1,10 @@ +# Native Forge45.4.5 server spec baseline; packaged output is checked separately. +[server] +zombieBaseSummonChance = 0.1 +zombieBabyChance = 0.05 +fullBoundingBoxLadders = false +removeErroringBlockEntities = false +removeErroringEntities = false +permissionHandler = "forge:default_handler" +useItemWithDurationZero = false +advertiseDedicatedServerToLan = true diff --git a/tools/fixtures/native119-flat-level.dat b/tools/fixtures/native119-flat-level.dat new file mode 100644 index 0000000..f5ba470 Binary files /dev/null and b/tools/fixtures/native119-flat-level.dat differ diff --git a/tools/fixtures/native119-flat-level.provenance.json b/tools/fixtures/native119-flat-level.provenance.json new file mode 100644 index 0000000..2204fb1 --- /dev/null +++ b/tools/fixtures/native119-flat-level.provenance.json @@ -0,0 +1,12 @@ +{ + "kind": "Synthetic flat header produced by actual native19 PrimaryLevelData and WorldDimensions registry codec; not a historical source world", + "minecraft": "1.19.4", + "forge": "45.4.5", + "native_data_version": 3337, + "java": "Temurin17.0.20.1+1", + "sha256": "9792ED9B0BF8A00ACB6BFFAEB50DC8B9C462CD9B6C96A9E6ED5CA65CFD534A55", + "generator_source": "src/test/java/cyano/lootable/buildtest/Native119FixtureGenerator.java", + "generator_source_sha256": "B99F1EB21351313AF1D79D04DEFAFDE492CD0C6838964F9A5143A187472DCD88", + "last_played_normalized": 0, + "world_seed": 4267321 +} diff --git a/tools/fixtures/native119-native-library-excerpt.json b/tools/fixtures/native119-native-library-excerpt.json new file mode 100644 index 0000000..82b27c5 --- /dev/null +++ b/tools/fixtures/native119-native-library-excerpt.json @@ -0,0 +1,63 @@ +{ + "source_url": "https://piston-meta.mojang.com/v1/packages/4fc30c189872faebae2617aba033cc17fec5899f/1.19.4.json", + "source_sha1": "4FC30C189872FAEBAE2617ABA033CC17FEC5899F", + "libraries": [ + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl-glfw/3.3.1/lwjgl-glfw-3.3.1-natives-windows.jar", + "sha1": "ed892f945cf7e79c8756796f32d00fa4ceaf573b", + "size": 145512, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.3.1/lwjgl-glfw-3.3.1-natives-windows.jar" + } + }, + "name": "org.lwjgl:lwjgl-glfw:3.3.1:natives-windows", + "rules": [ + { + "action": "allow", + "os": { + "name": "windows" + } + } + ] + }, + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl-glfw/3.3.1/lwjgl-glfw-3.3.1-natives-windows-arm64.jar", + "sha1": "beda65ee503443e60aa196d58ed31f8d001dc22a", + "size": 123808, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.3.1/lwjgl-glfw-3.3.1-natives-windows-arm64.jar" + } + }, + "name": "org.lwjgl:lwjgl-glfw:3.3.1:natives-windows-arm64", + "rules": [ + { + "action": "allow", + "os": { + "name": "windows" + } + } + ] + }, + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl-glfw/3.3.1/lwjgl-glfw-3.3.1-natives-windows-x86.jar", + "sha1": "b997e3391d6ce8f05487e7335d95c606043884a1", + "size": 139251, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl-glfw/3.3.1/lwjgl-glfw-3.3.1-natives-windows-x86.jar" + } + }, + "name": "org.lwjgl:lwjgl-glfw:3.3.1:natives-windows-x86", + "rules": [ + { + "action": "allow", + "os": { + "name": "windows" + } + } + ] + } + ] +} diff --git a/tools/hosted_policy.py b/tools/hosted_policy.py index b346909..a501130 100644 --- a/tools/hosted_policy.py +++ b/tools/hosted_policy.py @@ -42,13 +42,13 @@ def validate_workflow(text): words=shlex.split(line) if '|' in words: split=words.index('|');tail=words[split+1:] - if tail not in (['tee','$RUNNER_TEMP/native118-build.log'],['tee','-a','$RUNNER_TEMP/native118-build.log']):raise ValueError('Unknown hosted pipe') + if tail not in (['tee','$RUNNER_TEMP/native119-build.log'],['tee','-a','$RUNNER_TEMP/native119-build.log']):raise ValueError('Unknown hosted pipe') words=words[:split] if words and words[-1]=='2>&1':words=words[:-1] allowed=[['set','-euo','pipefail'],['chmod','+x','gradlew'], ['python3','-B','tools/hosted_policy.py'], ['python3','-B','tools/hosted_policy.py','--require-qualified-manifest'], - ['python3','-B','tools/native118_build.py']] + ['python3','-B','tools/native119_build.py']] if words and words[0]=='./gradlew':gradle_roots(shlex.join(words)) elif words not in allowed:raise ValueError('Unknown hosted command: '+line) launches=[line.strip() for line in text.splitlines() if line.strip().startswith('./gradlew ')] @@ -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+\.118021',version) or record.get('minecraft')!='1.18.2' or record.get('forge')!='40.3.12':raise ValueError('Wrong target qualification') + if not re.fullmatch(r'\d+\.\d+\.\d+\.119041',version) or record.get('minecraft')!='1.19.4' or record.get('forge')!='45.4.5':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') @@ -72,7 +72,7 @@ def require_qualified_manifest(record): # Publication still needs independently approved/rehashed local evidence. def check_source(root): - validate_workflow((root/'.github/workflows/native118.yml').read_text(encoding='utf-8')) + validate_workflow((root/'.github/workflows/native119.yml').read_text(encoding='utf-8')) for workflow in ('codeql-analysis.yml','deploy-release.yml'): if any('java-version:' in line and '17.0.20.1+1' in line for line in (root/'.github/workflows'/workflow).read_text(encoding='utf-8').splitlines()): raise ValueError('Non-SemVer JDK input in '+workflow) @@ -89,5 +89,5 @@ def check_source(root): if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--require-qualified-manifest',action='store_true');args=parser.parse_args() check_source(ROOT) - if args.require_qualified_manifest:require_qualified_manifest(json.loads((ROOT/'.github/release118.json').read_text())) + if args.require_qualified_manifest:require_qualified_manifest(json.loads((ROOT/'.github/release119.json').read_text())) print('HOSTED_COMPILE_ARCHIVE_POLICY_OK') diff --git a/tools/local_case_reuse.py b/tools/local_case_reuse.py index a9e4ab1..746c130 100644 --- a/tools/local_case_reuse.py +++ b/tools/local_case_reuse.py @@ -12,9 +12,9 @@ DEPENDENCIES=('gradle.properties','src/integrationTest/resources/log4j2.xml', '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/native118_persistence.py','tools/audit_runtime_logs.py', - 'tools/native_evidence.py','tools/native118_server_inputs.json', - 'tools/native118_generated_sdk.json','tools/native118_worker_budget.json') + 'tools/nbt_regions.py','tools/native119_persistence.py','tools/audit_runtime_logs.py', + 'tools/native_evidence.py','tools/native119_server_inputs.json', + 'tools/native119_worker_budget.json','tools/native119_logs.py') def normal_program(text): tree=ast.parse(text) @@ -35,7 +35,7 @@ def compare_inputs(old,new): def audit_normal(home,details,identity): from verify_world_upgrades import PHASES - from audit_runtime_logs import audit + from native119_logs import audit if details['phases']!=PHASES or details['stamp_mutations'] or not details['source_unchanged']: raise ValueError('Only one complete unchanged normal case is reusable') mods=audit_case_mods(home,identity);receipts=[];logs=[] @@ -46,7 +46,7 @@ def audit_normal(home,details,identity): for path in home.rglob('*'): if path.is_file() and 'crash-reports' in path.parts:raise ValueError('Normal crash report') if path.is_file() and path.name.endswith(('.log','.log.gz')): - audit(path,native=True,fixture_logins=True);logs.append({'path':str(path),'sha256':digest(path)}) + audit(path);logs.append({'path':str(path),'sha256':digest(path)}) if not logs:raise ValueError('Missing normal complete logs') return {'receipts':receipts,'positive_complete_logs':logs} diff --git a/tools/native118_build.py b/tools/native119_build.py similarity index 93% rename from tools/native118_build.py rename to tools/native119_build.py index 72cd31a..d4b5975 100644 --- a/tools/native118_build.py +++ b/tools/native119_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.18.2','40.3.12'): + if (record.get('version'),record.get('minecraft'),record.get('forge'))!=(MOD_VERSION,'1.19.4','45.4.5'): 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') @@ -20,5 +20,5 @@ def check_bundle(manifest,bundle): return actual if __name__=='__main__': - parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--manifest',type=Path,default=Path('.github/release118.json'));parser.add_argument('--bundle',type=Path,default=Path('build/release/bundle'));args=parser.parse_args() + parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--manifest',type=Path,default=Path('.github/release119.json'));parser.add_argument('--bundle',type=Path,default=Path('build/release/bundle'));args=parser.parse_args() check_bundle(args.manifest,args.bundle);print('LOCAL_QUALIFIED_BUNDLE_MATCH') diff --git a/tools/native118_client.py b/tools/native119_client.py similarity index 72% rename from tools/native118_client.py rename to tools/native119_client.py index 71554d0..1c92b55 100644 --- a/tools/native118_client.py +++ b/tools/native119_client.py @@ -1,16 +1,41 @@ -"""Pinned native40 client preparation and descriptor-driven Java17 launch.""" +"""Pinned native45 client preparation and descriptor-driven Java17 launch.""" import hashlib import json import os from pathlib import Path import platform import re +import struct import zipfile -BASE_VERSION = '1.18.2' -VERSION = '1.18.2-forge-40.3.12' -BASE_SHA1 = '334b33fcba3c9be4b7514624c965256535bd7eba' -FORGE_SHA256 = '270C29DEEE4B28627444BAE949873C030DD14DADBCFA83A210901C8B7986281F' +BASE_VERSION = '1.19.4' +VERSION = '1.19.4-forge-45.4.5' +BASE_SHA1 = '4fc30c189872faebae2617aba033cc17fec5899f' +FORGE_SHA256 = '15F2EF110009308A16D7C58E009BEBBAC748F53B021440CB8FC27B90BB255288' + + +def native_for_platform(library,system,architecture): + parts=library['name'].split(':') + if len(parts)<4 or not parts[3].startswith('natives-'): + return True + variants={('windows','amd64'):'natives-windows',('windows','x86'):'natives-windows-x86', + ('windows','arm64'):'natives-windows-arm64',('linux','amd64'):'natives-linux', + ('osx','amd64'):'natives-macos',('osx','arm64'):'natives-macos-arm64'} + if (system,architecture) not in variants: + raise RuntimeError('Unsupported native client platform: '+system+'/'+architecture) + return parts[3]==variants[system,architecture] + + +def validate_native_image(content,system,architecture): + if system!='windows':return + machines={'amd64':0x8664,'x86':0x014c,'arm64':0xAA64} + if architecture not in machines or len(content)<64 or content[:2]!=b'MZ': + raise RuntimeError('Missing or malformed Windows native image') + offset=struct.unpack_from('len(content) or content[offset:offset+4]!=b'PE\0\0': + raise RuntimeError('Malformed Windows native PE header') + if struct.unpack_from('3 and parts[3].startswith('natives-'): + extract(path) classifier = library.get('natives', {}).get(system) if classifier: classifier = classifier.replace('${arch}', '32' if architecture == 'x86' else '64') archive = artifact(downloads['classifiers'][classifier]) - with zipfile.ZipFile(archive) as jar: - for entry in jar.infolist(): - if entry.is_dir() or entry.filename.startswith('META-INF/'): - continue - if Path(entry.filename).suffix.lower() not in ('.dll','.so','.dylib','.jnilib'): - continue - target = natives/Path(entry.filename).name - content = jar.read(entry) - if target.exists() and target.read_bytes() != content: - raise RuntimeError('Conflicting native client input: '+target.name) - target.write_bytes(content) + extract(archive) client = runtime/'versions'/BASE_VERSION/(BASE_VERSION+'.jar') expected = base['downloads']['client']['sha1'] if not client.is_file() or hashlib.sha1(client.read_bytes()).hexdigest() != expected: @@ -145,13 +177,13 @@ 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 native1.18 asset index') + raise RuntimeError('Incorrect official native1.19.4 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', 'auth_player_name':username,'game_directory':str(home),'assets_root':str(assets), 'assets_index_name':base['assetIndex']['id'],'auth_uuid':'00000000-0000-4000-8000-000000000001', - # Native40 explicitly selects OfflineSocialInteractions for0. + # Native45 explicitly selects OfflineSocialInteractions for0. 'auth_access_token':'0','user_type':'legacy','version_type':'release', # Match native Main's explicit empty defaults for offline users. 'clientid':'','auth_xuid':'', @@ -163,4 +195,4 @@ def command(java, home, runtime, assets, prepared, *, username, extra=()): jvm_values = dict(values,version_name=BASE_VERSION) jvm = expand(base['arguments']['jvm']+forge['arguments']['jvm'],jvm_values,**options) game = expand(base['arguments']['game']+forge['arguments']['game'],values,**options) - return [str(java),'-Xmx2G',*jvm,*extra,forge['mainClass'],*game] + return [str(java),'-Dfile.encoding=UTF-8','-Xmx2G',*jvm,*extra,forge['mainClass'],*game] diff --git a/tools/native119_client_diagnostics.json b/tools/native119_client_diagnostics.json new file mode 100644 index 0000000..247f727 --- /dev/null +++ b/tools/native119_client_diagnostics.json @@ -0,0 +1,17 @@ +{ + "minecraft": "1.19.4", + "forge": "45.4.5", + "platform": "Windows amd64", + "qualified": true, + "proof_sha256": "24A5407626528E1AB0E0DC0F9D15CEB96C71E40854B77903E6315BE1D19F79EB", + "events": [ + "minecraft:item.brush.brushing", + "minecraft:item.brush.brush_sand_completed", + "minecraft:item.goat_horn.play", + "minecraft:entity.goat.screaming.horn_break", + "minecraft:music.overworld.cherry_grove" + ], + "sound_definition_sha256": "094B16E7F9D733ACDE8F87AD0206529926F3866E5CE483C78A3C0DA94BF68A31", + "control_sha256": "BA2137006F78F901F2112FACFB075F1037B5BEB8F91D14709AC46FE5830C98A2", + "scope": "Explicit client-only diagnostic classification; generic/server audits remain unchanged. Missing/stale proof or actual errors/resources remain fatal." +} diff --git a/tools/native119_client_logs.py b/tools/native119_client_logs.py new file mode 100644 index 0000000..e93308a --- /dev/null +++ b/tools/native119_client_logs.py @@ -0,0 +1,58 @@ +"""Explicit native45 client diagnostics, separate from strict server/error audits.""" +from pathlib import Path +import gzip,hashlib,json,re,tempfile +from native119_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('native119_client_diagnostics.json') + proof_path=Path(proof_path) if proof_path else project/'.gradle/native119-client-sound-qualification-64/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.19.4','45.4.5','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'): + if not proof.get(group):raise RuntimeError('Missing native client diagnostic evidence') + for name,expected in proof[group].items(): + path=Path(name) + if not path.is_absolute() or not path.is_file() or path.is_symlink() or hashlib.sha256(path.read_bytes()).hexdigest().upper()!=expected:raise RuntimeError('Changed native client diagnostic input: '+name) + definition=Path(proof['sound_definition']);data=definition.read_bytes() + if hashlib.sha256(data).hexdigest().upper()!=policy.get('sound_definition_sha256'):raise RuntimeError('Changed official sound definitions') + definitions=json.loads(data) + if len(set(policy['events']))!=5 or any(not name.startswith('minecraft:') or name.removeprefix('minecraft:') in definitions for name in policy['events']):raise RuntimeError('Native diagnostic is no longer an unbound vanilla event') + return policy + +def audit_client(path,**options): + path=Path(path);raw=path.read_bytes() + text=(gzip.decompress(raw) if path.name.endswith('.gz') else raw).decode('utf-8') + lines=text.splitlines(keepends=True) + missing=[line for line in text.splitlines() if re.search(r'(?i)\bMissing (?:texture|model|sound file)\s*:|\bFile .+ does not exist, cannot add it to event',line)] + if missing:raise RuntimeError(str(path)+'\n'+'\n'.join(missing)) + selected=[i for i,line in enumerate(lines) if 'Missing sound for event:' in line] + if not selected: + result=strict_audit(path,**options) + result.update(classified_native_sound_warnings=0,raw_strict_audit_passed=True,residual_strict_audit_passed=True) + return result + policy=validated_baseline();events=policy['events'];observed=[] + pattern=r'^\[\d{2}:\d{2}:\d{2}(?:\.\d{3})?\] \[Render thread/WARN\] \[net\.minecraft\.client\.sounds\.SoundEngine\]: Missing sound for event: (minecraft:[a-z0-9_./]+)$' + for index in selected: + match=re.fullmatch(pattern,lines[index].rstrip('\r\n')) + if match is None:raise RuntimeError('Unclassified native client sound diagnostic: '+lines[index].rstrip()) + observed.append(match[1]) + if len(observed)%len(events) or observed!=events*(len(observed)//len(events)): + raise RuntimeError('Incomplete, changed or unknown native client sound diagnostic batch') + chosen=set(selected);residual=''.join(line for i,line in enumerate(lines) if i not in chosen) + # 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='native119-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 + result.update(path=str(path),lines=len(lines),classified_native_sound_warnings=len(observed),classified_native_sound_events=observed, + raw_strict_audit_passed=False,residual_strict_audit_passed=True,native_sound_control_proof_sha256=policy['proof_sha256'],raw_sha256=hashlib.sha256(raw).hexdigest().upper()) + return result diff --git a/tools/native119_logs.py b/tools/native119_logs.py new file mode 100644 index 0000000..0bb6dbe --- /dev/null +++ b/tools/native119_logs.py @@ -0,0 +1,20 @@ +"""Current-target log contract, separate from retained legacy controls.""" +import gzip,re +from audit_runtime_logs import audit as _legacy_audit + +def audit(path,development=False,upgrade=False,fixture_logins=False,native=True): + if fixture_logins: + raise RuntimeError('Legacy login-retry classification is not native45 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') + lines=re.sub(r'\x1b\[[0-9;]*m','',text).splitlines() + failures=[line for line in lines if re.search( + r'/ERROR\]|/FATAL\]|\bERROR (?:Error processing|Unable to locate)|^AL lib: \(EE\)|Caused by:',line)] + if failures:raise RuntimeError(str(path)+'\n'+'\n'.join(failures)) + # 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.19.4' + result['legacy_native_controls_used']=False + return result diff --git a/tools/native118_persistence.py b/tools/native119_persistence.py similarity index 82% rename from tools/native118_persistence.py rename to tools/native119_persistence.py index 6ffcaa9..fabfe41 100644 --- a/tools/native118_persistence.py +++ b/tools/native119_persistence.py @@ -1,4 +1,4 @@ -"""Read-only native18 saved entity inspection, distinct from old-writer proof.""" +"""Read-only native19 saved entity inspection, distinct from old-writer proof.""" import re,struct from nbt_regions import Tag,read_region @@ -7,7 +7,7 @@ def count_persisted(world,wave): def selected(region):return wave<0 or region.name==['r.0.0.mca','r.8.8.mca','r.16.16.mca'][wave] names=('lootablebodies.Corpse','lootablebodies:corpse') # A region can still contain unopened old terrain chunks. Inspect both - # actual layouts without relabelling untouched terrain as converted18. + # actual layouts without relabelling untouched terrain as converted19. for region in world.rglob('region/r.*.mca'): if not selected(region):continue for chunk in read_region(region).values(): @@ -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,2975):raise ValueError('Wrong native18 entity-chunk data version') + if root.get('DataVersion')!=Tag(3,3337):raise ValueError('Wrong native19 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 native18 entity-chunk Position/index') - if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native18 root Entities list') + raise ValueError('Wrong native19 entity-chunk Position/index') + if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native19 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,8):raise ValueError('Wrong native18 corpse identity/marker') + if entity.value['id']!=Tag(8,'lootablebodies:corpse') or entity.value.get('LootableBodiesDataVersion')!=Tag(3,9):raise ValueError('Wrong native19 corpse identity/marker') identity=entity.value.get('UUID') - if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native18 corpse UUID type') + if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native19 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/native119_server_inputs.json b/tools/native119_server_inputs.json new file mode 100644 index 0000000..48a4983 --- /dev/null +++ b/tools/native119_server_inputs.json @@ -0,0 +1,107 @@ +{ + "libraries/com/electronwill/night-config/core/3.6.4/core-3.6.4.jar": "1BC71C67B54BA9C8E5030B3DDAEF7DEE16AE43FEE6850368F76B0CFFEF819700", + "libraries/com/electronwill/night-config/toml/3.6.4/toml-3.6.4.jar": "3CB3D0EF3352E204CDA68215FBE41AFDC7F56425EF4FD8E84006964A656117D4", + "libraries/com/github/jponge/lzma-java/1.3/lzma-java-1.3.jar": "75CCC0E24D37ED5B8916E1BCFFAD1E067C2B1CFA21B635287F968DB5C3C1963A", + "libraries/com/github/oshi/oshi-core/6.2.2/oshi-core-6.2.2.jar": "EFBCA93DF18F3EB16964A92D8342F0674DD5D90DA575EF8E04EF2E4EABEC6910", + "libraries/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar": "766AD2A0783F2687962C8AD74CEECC38A28B9F72A2D085EE438B7813E928D0C7", + "libraries/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar": "4241C14A7727C34FEEA6507EC801318A3D4A90F070E4525681079FB94EE4C593", + "libraries/com/google/code/gson/gson/2.10/gson-2.10.jar": "0CDD163CE3598A20FC04EEE71B140B24F6F2A3B35F0A499DBBDD9852E83FBFAF", + "libraries/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar": "03D0329547C13DA9E17C634D1049EA2EAD093925E290567E1A364FD6B1FC7FF8", + "libraries/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar": "A171EE4C734DD2DA837E4B16BE9DF4661AFAB72A41ADAF31EB84DFDAF936CA26", + "libraries/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar": "6DB0C3A244C397429C2E362EA2837C3622D5B68BB95105D37C21C36E5BC70ABF", + "libraries/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar": "A42EDC9CAB792E39FE39BB94F3FCA655ED157FF87A8AF78E1D6BA5B07C4A00AB", + "libraries/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar": "2994A7EB78F2710BD3D3BFB639B2C94E219CEDAC0D4D084D516E78C16DDDECF6", + "libraries/com/mojang/authlib/3.18.38/authlib-3.18.38.jar": "7EB3C3E09336C5A22D73EA33982693D3939C106913F2E465D764BC915572DEC0", + "libraries/com/mojang/brigadier/1.0.18/brigadier-1.0.18.jar": "EDC4926AA4B49010F6E7AC46EFD623FB38F9517344D26F6251D79A26A9738C0B", + "libraries/com/mojang/datafixerupper/6.0.6/datafixerupper-6.0.6.jar": "C5722083471A0F3245194B1EFCBE7BE8B2D3E42720006CAC31CF3E8C8B24F9A8", + "libraries/com/mojang/logging/1.1.1/logging-1.1.1.jar": "C1756EAF0685DA94142BACC84309BB7CCDDF65E004638C3299AB645710B5938C", + "libraries/com/nothome/javaxdelta/2.0.1/javaxdelta-2.0.1.jar": "A661DFCB749053FC26EADA2A0A8E619A8EAD1CA9EDA29E1698687AA2CDD80152", + "libraries/commons-io/commons-io/2.11.0/commons-io-2.11.0.jar": "961B2F6D87DBACC5D54ABF45AB7A6E2495F89B75598962D8C723CEA9BC210908", + "libraries/commons-io/commons-io/2.4/commons-io-2.4.jar": "CC6A41DC3EAACC9E440A6BD0D2890B20D36B4EE408FE2D67122F328BB6E01581", + "libraries/cpw/mods/bootstraplauncher/1.1.2/bootstraplauncher-1.1.2.jar": "853B61FD165CAF4103B5B969AFBC5B2B037ABC310ED644518CD967B50D5189A4", + "libraries/cpw/mods/modlauncher/10.0.8/modlauncher-10.0.8.jar": "A8529D8EF3C754DFFE8873ECA20778C8814B04AB6B9AB1A892D840BF7A9832A1", + "libraries/cpw/mods/securejarhandler/2.1.6/securejarhandler-2.1.6.jar": "8C76415D17ECA923D88F62354F2D41EE523759B24AD9BBAD55032188DFE0158C", + "libraries/de/oceanlabs/mcp/mcp_config/1.19.4-20230314.122934/mcp_config-1.19.4-20230314.122934-mappings-merged.txt": "B1278B29F28C113AAFEC5C6D1CEA02653F2A97FEAF28DF8B48DBE70EB0D0125D", + "libraries/de/oceanlabs/mcp/mcp_config/1.19.4-20230314.122934/mcp_config-1.19.4-20230314.122934-mappings.txt": "5B1A699624AE356B7D8B9A2F1272A1D38AA4941F9EB57F2825C5FF3510A3FFEF", + "libraries/de/oceanlabs/mcp/mcp_config/1.19.4-20230314.122934/mcp_config-1.19.4-20230314.122934.zip": "C50B4070F92FBDAF00206B673B3669400ABFB78B61BAA13467B566468477AE8F", + "libraries/de/siegmar/fastcsv/2.2.2/fastcsv-2.2.2.jar": "BFDA71ABCD7C2B62DB498DEBA24C14AEC0E9059A9C183477840AC1BAE9C98AD2", + "libraries/io/netty/netty-buffer/4.1.82.Final/netty-buffer-4.1.82.Final.jar": "A7CE7F6D8C1E82B044CD56765DAB4338251596416DE03B1F9A05D7402429C29D", + "libraries/io/netty/netty-codec/4.1.82.Final/netty-codec-4.1.82.Final.jar": "170C0CE091C2E1CC1D952D87A6A30DEB773922507BBC609B962B539F03F53407", + "libraries/io/netty/netty-common/4.1.82.Final/netty-common-4.1.82.Final.jar": "D5923DB1CC68B26BAF5368F1F76EC2C6100D12DB8406C6DE00EB6DBC08052550", + "libraries/io/netty/netty-handler/4.1.82.Final/netty-handler-4.1.82.Final.jar": "31015505983DC57EEC491AD4873411BA51F0F3BEB24D22D509336C72511019CB", + "libraries/io/netty/netty-resolver/4.1.82.Final/netty-resolver-4.1.82.Final.jar": "4CFEBA7EC535964A35722178AEECC87EC186DBA0203D04389A850E8E62575383", + "libraries/io/netty/netty-transport-classes-epoll/4.1.82.Final/netty-transport-classes-epoll-4.1.82.Final.jar": "73F827FCA18870959B9581C6875130576E5DE684CFB4E6F346F8C1C266840BA3", + "libraries/io/netty/netty-transport-native-epoll/4.1.82.Final/netty-transport-native-epoll-4.1.82.Final-linux-aarch_64.jar": "C9551F4622B4C44C9E7DC8C6DC17874B4DDB953A943F402740E64126180EE85E", + "libraries/io/netty/netty-transport-native-epoll/4.1.82.Final/netty-transport-native-epoll-4.1.82.Final-linux-x86_64.jar": "D4651F6082FB60F4218F100322DE33E350184F5287900D163698298CF72DDBE7", + "libraries/io/netty/netty-transport-native-unix-common/4.1.82.Final/netty-transport-native-unix-common-4.1.82.Final.jar": "1096F16527A51DD5F44CED90636110A429CC7359A9A8A91DB25000B747793429", + "libraries/io/netty/netty-transport/4.1.82.Final/netty-transport-4.1.82.Final.jar": "C210069BE4AB4120F5AC7CB5E4A126D3A007E78A64FD44C9971220E013DBAEF1", + "libraries/it/unimi/dsi/fastutil/8.5.9/fastutil-8.5.9.jar": "9578BF2A1700CF20D21746A2EE89E57BA1ABBD37FA9FEDA68FF5E9A28473A7F9", + "libraries/net/java/dev/jna/jna-platform/5.12.1/jna-platform-5.12.1.jar": "8CE969116CAC95BD61B07A8D5E07174B352E63301473CAAC72C395E3C08488D2", + "libraries/net/java/dev/jna/jna/5.12.1/jna-5.12.1.jar": "91A814AC4F40D60DEE91D842E1A8AD874C62197984403D0E3C30D39E55CF53B3", + "libraries/net/jodah/typetools/0.8.3/typetools-0.8.3.jar": "F853E21F7DE6BD192F0CE09E64A6C8B0E154C9CF9F34E62F08D5E26F73CE3B19", + "libraries/net/minecraft/server/1.19.4-20230314.122934/server-1.19.4-20230314.122934-extra.jar": "D698CB9EA269B9496D3F44D98C090735D2996F449FF3FD8A7EEA292C8A01B166", + "libraries/net/minecraft/server/1.19.4-20230314.122934/server-1.19.4-20230314.122934-extra.jar.cache": "D17155F4672285E06D2085CD5ACE5771FDF236DB2DDE6100A6A4D05A3AC07FDF", + "libraries/net/minecraft/server/1.19.4-20230314.122934/server-1.19.4-20230314.122934-mappings.txt": "097DD353BD96A1A8C0C1C16505E202957730CA5F1095F089487E7CED4A20FAD8", + "libraries/net/minecraft/server/1.19.4-20230314.122934/server-1.19.4-20230314.122934-slim.jar": "1A1B5226B24EE3C62B5E0EFC41949EBD1A692A89F65337F07601A40A172EDD45", + "libraries/net/minecraft/server/1.19.4-20230314.122934/server-1.19.4-20230314.122934-slim.jar.cache": "63CB09C1A6E1CE0BF7FFA1FAA7292E046DD757A1054EEBA3F9F4DFA5D984D9E0", + "libraries/net/minecraft/server/1.19.4-20230314.122934/server-1.19.4-20230314.122934-srg.jar": "7A280BB20E1F1358FDBFCAF734DEE60FA6E8218EE838FC5EFD0FCD292ECFA2F8", + "libraries/net/minecraft/server/1.19.4-20230314.122934/server-1.19.4-20230314.122934-unpacked.jar": "75A895446E216D24F282F0C3C63503E7537DB9323104449343FA87CDFC97D372", + "libraries/net/minecraft/server/1.19.4/server-1.19.4.jar": "A524A10DA550741785C8C3A0FB39047F106B0C7C2CFA255E8278CB6F1ABE3F53", + "libraries/net/minecraftforge/ForgeAutoRenamingTool/0.1.22/ForgeAutoRenamingTool-0.1.22-all.jar": "C459A06A0FF995C31335100C84FB018F12B392BB0574B5CF9E448B10F7CB3679", + "libraries/net/minecraftforge/JarJarFileSystems/0.3.19/JarJarFileSystems-0.3.19.jar": "BEAA1875DD433F2DD152395C3E3C52EF2BB99F20B74860A07D70F7C4626E56D4", + "libraries/net/minecraftforge/JarJarMetadata/0.3.19/JarJarMetadata-0.3.19.jar": "6141C710B5B7B673BD544740A31339F0754D7A794CC46B952B7EF7897B7CE265", + "libraries/net/minecraftforge/JarJarSelector/0.3.19/JarJarSelector-0.3.19.jar": "F7ACFBAFC9A715EE7D7CE2D539815B3EBCCEBC5064E0A7252C4A6A1C71D1E740", + "libraries/net/minecraftforge/accesstransformers/8.0.4/accesstransformers-8.0.4.jar": "3225E2A513F658DF686FE8B2AE5B4808525851A2C238CE65E38847CC4FFC6441", + "libraries/net/minecraftforge/binarypatcher/1.1.1/binarypatcher-1.1.1.jar": "E6E85E0B78179D0683751F671E7DC8AEE68668883C1B50A2036813A377868177", + "libraries/net/minecraftforge/coremods/5.2.4/coremods-5.2.4.jar": "DB32DC040AF2EEA1216A145A3F5FC6BC45D006740FE696DA5A9A445006167C9F", + "libraries/net/minecraftforge/eventbus/6.0.3/eventbus-6.0.3.jar": "4BC0D5EAA086A4AECC613771FD02AD04373A06E83359185EE87F5CAAE4F29897", + "libraries/net/minecraftforge/fmlcore/1.19.4-45.4.5/fmlcore-1.19.4-45.4.5.jar": "180FEC0B47315C4A8DAA6FD17F6F57E787BB63D872D1BB2B1FCA94FF41EE92BA", + "libraries/net/minecraftforge/fmlloader/1.19.4-45.4.5/fmlloader-1.19.4-45.4.5.jar": "72F72920D376763B341AC3159D9E79946850AD0C1D3CE9CABA60C3C58F3F224B", + "libraries/net/minecraftforge/forge/1.19.4-45.4.5/forge-1.19.4-45.4.5-server.jar": "FFA7F65BBCB88AEA51AC5B2D37DCCE89DFCDEED0E54EDCCD6CB915E084E288D3", + "libraries/net/minecraftforge/forge/1.19.4-45.4.5/forge-1.19.4-45.4.5-universal.jar": "A16683ABD744EC0A99F062230118C0C16368EB1C1925F337B42E6C86A5217F8E", + "libraries/net/minecraftforge/forge/1.19.4-45.4.5/unix_args.txt": "AA5FF958BECF39940DD734F53EEF554390A0ADBC2F27BF4212A7C1224742A1A0", + "libraries/net/minecraftforge/forge/1.19.4-45.4.5/win_args.txt": "5CFAE247E85ECBB7A0E98250635C6DC94C711B8DF74BEF2ACBF18A39792563B6", + "libraries/net/minecraftforge/forgespi/6.0.0/forgespi-6.0.0.jar": "F7BBE556D381B9C138AAE2A348DDF42ECB72DE4F1F181C11DF0CCC5234E494F0", + "libraries/net/minecraftforge/installertools/1.4.1/installertools-1.4.1.jar": "C475B48E225524F3358F569CA196FC1FE459575DBA45A9317618661E30C4ED1C", + "libraries/net/minecraftforge/jarsplitter/1.1.4/jarsplitter-1.1.4.jar": "7B7B2882921900B74CAD59008761CB18F7DA1A5BCA17CD7648251E0A65070067", + "libraries/net/minecraftforge/javafmllanguage/1.19.4-45.4.5/javafmllanguage-1.19.4-45.4.5.jar": "ED5ECE16E4211E7ACB0EFEEA5C21DC5114F63A785C43ECD37816FD547BFD5FD0", + "libraries/net/minecraftforge/lowcodelanguage/1.19.4-45.4.5/lowcodelanguage-1.19.4-45.4.5.jar": "1069DD4D40115382D8F7AC85EF7BA5CD025B1E373C8FEFF8BAD9E59FD35B0B1F", + "libraries/net/minecraftforge/mclanguage/1.19.4-45.4.5/mclanguage-1.19.4-45.4.5.jar": "D06179A4684CD5EB125332D01C507D0C47A964220FAB448EB741EA5EAF4746E5", + "libraries/net/minecraftforge/srgutils/0.4.3/srgutils-0.4.3.jar": "94027D592A75D9437845C5C5A9B21708697E48EC2143B991BD264B4C7D2C0C51", + "libraries/net/minecraftforge/srgutils/0.4.9/srgutils-0.4.9.jar": "362DAEAD41B74BAA84B51A98E6BE9896C77CE15B37413CA1B367B01052F39ADE", + "libraries/net/minecraftforge/srgutils/0.5.6/srgutils-0.5.6.jar": "1AAF0178677961242C52E101C1BA8E1EE96DAD054295E20EB15B98F4E6FE2E3D", + "libraries/net/minecraftforge/unsafe/0.2.0/unsafe-0.2.0.jar": "946937E182AB3B518B5B88264F367E1F35AC12BC83ABED220490C60C717BE239", + "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/antlr/antlr4-runtime/4.9.1/antlr4-runtime-4.9.1.jar": "A80502C3140AE7ACFBB1F57847C8EB1C101461A969215EC38C3F1EBDFF61AC29", + "libraries/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar": "D919D904486C037F8D193412DA0C92E22A9FA24230B9D67A57855C5C31C7E94E", + "libraries/org/apache/logging/log4j/log4j-api/2.19.0/log4j-api-2.19.0.jar": "5CCB24AD9F92E768D0BC456D3061A737951262DF803E004D2CAD096B75A88D60", + "libraries/org/apache/logging/log4j/log4j-core/2.19.0/log4j-core-2.19.0.jar": "B4A1796FAB7BFC36DF015C1B4052459147997E8D215A7199D71D05F9E747E4F4", + "libraries/org/apache/logging/log4j/log4j-slf4j2-impl/2.19.0/log4j-slf4j2-impl-2.19.0.jar": "825605EACB2D5605B105C53D4108C18125E0F82F62960D0BE583278B9C524F3C", + "libraries/org/apache/maven/maven-artifact/3.8.5/maven-artifact-3.8.5.jar": "91172BC294D6EAB02FC9F45F4EA01FD0E418962D128CF489ABEA7B6957D988EE", + "libraries/org/checkerframework/checker-qual/2.0.0/checker-qual-2.0.0.jar": "FC8441632F5FA5537492C9F026D1C8B1ADB6A7796F46031B04B4CC0622427995", + "libraries/org/codehaus/mojo/animal-sniffer-annotations/1.14/animal-sniffer-annotations-1.14.jar": "2068320BD6BAD744C3673AB048F67E30BEF8F518996FA380033556600669905D", + "libraries/org/jline/jline-reader/3.12.1/jline-reader-3.12.1.jar": "4D1037C7110027ED64816C510FAD2AFA0A5AA9BBC38FB9F0E772870823FCAD2A", + "libraries/org/jline/jline-terminal/3.12.1/jline-terminal-3.12.1.jar": "EB6115680F732562F0E8E6777F127DECE81E23B25015925F3C4A2A900D48CC9E", + "libraries/org/joml/joml/1.10.5/joml-1.10.5.jar": "CAC9F22F83A7AA33EEBDA73C16FF5261E3CB4911B6BAFCF4C79EA486099D0C9A", + "libraries/org/openjdk/nashorn/nashorn-core/15.4/nashorn-core-15.4.jar": "6F816E84DFD63A81D4EAA7829C08337BBAFF3EC683FF3BF6BBD90D017A00DC6F", + "libraries/org/ow2/asm/asm-analysis/9.2/asm-analysis-9.2.jar": "878FBE521731C072D14D2D65B983B1BEAE6AD06FDA0007B6A8BAE81F73F433C4", + "libraries/org/ow2/asm/asm-analysis/9.7.1/asm-analysis-9.7.1.jar": "85B29371884BA31BB76EDF22323C2C24E172C3267A67152EBA3D1CCC2E041EF2", + "libraries/org/ow2/asm/asm-commons/9.2/asm-commons-9.2.jar": "BE4CE53138A238BB522CD781CF91F3BA5CE2F6CA93EC62D46A162A127225E0A6", + "libraries/org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar": "7AEFD0D5C0901701C69F7513FEDA765FB6BE33AF2CE7AA17C5781FC87657C511", + "libraries/org/ow2/asm/asm-commons/9.7.1/asm-commons-9.7.1.jar": "9A579B54D292AD9BE171D4313FD4739C635592C2B5AC3A459BBD1049CDDEC6A0", + "libraries/org/ow2/asm/asm-tree/9.2/asm-tree-9.2.jar": "AABF9BD23091A4EBFC109C1F3EE7CF3E4B89F6BA2D3F51C5243F16B3CFFAE011", + "libraries/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar": "C43ECF17B539C777E15DA7B5B86553B377E2D39A683DE6285567D5283888E7EF", + "libraries/org/ow2/asm/asm-tree/9.7.1/asm-tree-9.7.1.jar": "9929881F59EB6B840E86D54570C77B59CE721D104E6DFD7A40978991C2D3B41F", + "libraries/org/ow2/asm/asm-util/9.7.1/asm-util-9.7.1.jar": "F885BE71B5C90556F5F1AD1C4F9276B29B96057C497D46666FE4DDBEC3CB43C6", + "libraries/org/ow2/asm/asm/9.2/asm-9.2.jar": "B9D4FE4D71938DF38839F0ECA42AAAA64CF8B313D678DA036F0CB3CA199B47F5", + "libraries/org/ow2/asm/asm/9.6/asm-9.6.jar": "3C6FAC2424DB3D4A853B669F4E3D1D9C3C552235E19A319673F887083C2303A1", + "libraries/org/ow2/asm/asm/9.7.1/asm-9.7.1.jar": "8CADD43AC5EB6D09DE05FAECCA38B917A040BB9139C7EDEB4CC81C740B713281", + "libraries/org/slf4j/slf4j-api/2.0.1/slf4j-api-2.0.1.jar": "B36B99B8D99EA7857554D9B25DFDBD1FB25378C926F7B9E0249983C99335B2C4", + "libraries/org/spongepowered/mixin/0.8.5/mixin-0.8.5.jar": "CA15A907E4D1F6B38CEFED51D7DF96A83347DCFB0BC8CED53111519E991D887D", + "libraries/trove/trove/1.0.2/trove-1.0.2.jar": "AD68A86CFD0255A2BC0525CE11EC49640CAEA7E34FF91EB89C3193DCCCD77F05", + "run.bat": "C6708D24693BC14F0E4A731F0CD4ADE5F5A66F16A17BFEED49FFF0D824F2CB63", + "run.sh": "84389856D003023253BBEB7409F27A7F8AE8DDD22B7FD59C6A14045665A9E277", + "user_jvm_args.txt": "60908CCC470BC913341006FB076BAF75B1CE2D9C132B837AEC133E8D98474278" +} diff --git a/tools/native119_worker_budget.json b/tools/native119_worker_budget.json new file mode 100644 index 0000000..6039fae --- /dev/null +++ b/tools/native119_worker_budget.json @@ -0,0 +1,24 @@ +{ + "minecraft": "1.19.4", + "forge": "45.4.5", + "java": "Temurin17.0.20.1+1", + "platform": "Windows amd64", + "qualified": true, + "reloads": 200, + "jvms": 4, + "jvm_args": [], + "scope": "Actual native45 default preparation workers plus Main owner-thread apply executor,200reloads/four independent JVMs before any world/candidate. No native40 override or baseline reused.", + "max_concurrent_workers": 2, + "no_candidate": true, + "no_worlds": true, + "source_and_sdk_seals_unchanged": true, + "controller_sha256": "A6A78C3762981344E8BF70E22EFFF19C72769B4FEEB06E18ECA8CC4940037C58", + "server_input_manifest_sha256": "78D29D355E881D9111135B5CB39C738B3F2F3F088467668C410A576D143E90C1", + "qualification_evidence_sha256": "923E74DE68419C8D1418F219476D1870064B20C9BFFC7A6F5AE2CE86E6C8EDF2", + "preparation_workers_observed": [ + 23 + ], + "visible_cpus_observed": [ + 24 + ] +} diff --git a/tools/native_evidence.py b/tools/native_evidence.py index ff750b5..91659c7 100644 --- a/tools/native_evidence.py +++ b/tools/native_evidence.py @@ -1,7 +1,7 @@ -"""Independently audit native18 local receipts, complete logs and diagnostic bytes. +"""Independently audit native19 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 native18 +Relocated auditing is a separately tested download contract; hosted native19 execution and promotion remain disabled and have not been exercised. """ from pathlib import Path,PurePosixPath @@ -15,21 +15,13 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): receipt=json.loads((directory/'result.json').read_text()) 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 hosted deadline') - if (receipt['returncode']==0)==refusal:raise ValueError('Unexpected hosted JVM exit') + if receipt['timeout']!=(180 if refusal else 240):raise ValueError('Changed native deadline') + if (receipt['returncode']==0)==refusal:raise ValueError('Unexpected native JVM exit') command=receipt['command'] - # Default-worker receipts remain their original sealed proof. The one - # independently qualified native40 override is limited to history/refusal - # phases; it cannot excuse another cap, duplicate flag, CPU or GC selection. - if any(s.startswith(('-XX:ActiveProcessorCount=','-XX:ParallelGCThreads=','-XX:ConcGCThreads=')) + # Native45 was qualified with defaults; no old CPU/worker/GC cap is valid. + if any(s.startswith(('-XX:ActiveProcessorCount','-XX:ParallelGCThreads','-XX:ConcGCThreads','-Dmax.bg.threads')) or (s.startswith(('-XX:+Use','-XX:-Use')) and s.endswith('GC')) for s in command): - raise ValueError('Unqualified native40 worker or GC override') - worker_args=[s for s in command if s.startswith('-Dmax.bg.threads')] - if worker_args: - from verify_world_upgrades import PHASES - if (tuple(worker_args)!=HISTORICAL_JVM_ARGS or - not ((refusal and phase=='abort') or (not refusal and phase in PHASES))): - raise ValueError('Unqualified or out-of-scope native40 preparation-worker override') + raise ValueError('Unqualified native45 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. @@ -42,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 native40 modules') + if len(arguments)!=1 or arguments[0] not in expected or '--nogui' not in command:raise ValueError('Receipt did not launch native45 modules') if not any(item.startswith('-Xlog:gc*,safepoint:file=') for item in command):raise ValueError('Missing native17 unified GC evidence') if 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'): @@ -56,7 +48,7 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): raise ValueError('Incomplete hosted diagnostic attempt') for name in (phase+'.log',phase+'.latest.log',phase+'.debug.log'): if not (home/name).is_file() or not (home/name).stat().st_size:raise ValueError('Missing complete native log: '+name) - text=(home/(phase+'.log')).read_text(errors='replace') + text=(home/(phase+'.log')).read_text(encoding='utf-8') if refusal: if "Lootable Bodies could not preserve this world's corpses" not in text:raise ValueError('Missing preservation refusal') if any(s in text for s in ('Preparing start region','Preparing spawn area','LOOTABLE_BODIES_RUNTIME_PASS','Done (')): @@ -65,7 +57,7 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): if not comparison['sealed_source_unchanged'] or any(comparison[k] for k in ('world_added','world_removed','world_changed')): raise ValueError('Unsafe refusal changed source/world data') elif 'LOOTABLE_BODIES_WORLD_UPGRADE_PASS phase='+phase not in text: - raise ValueError('Missing actual hosted phase assertions') + raise ValueError('Missing actual native phase assertions') logs={str(home/name):digest(home/name) for name in (phase+'.log',phase+'.latest.log',phase+'.debug.log')} if refusal:logs[str(home/'comparison.json')]=digest(home/'comparison.json') return {'receipt':str(directory/'result.json'),'sha256':digest(directory/'result.json'), @@ -103,7 +95,7 @@ def audit_case_mods(home,report): def audit_shard(folder,report,*,relocated=False): positive=folder/'migration-results';aborts=folder/'migration-abort' records=[];logs=[] - from audit_runtime_logs import audit + from native119_logs import audit for case,details in report['cases'].items(): home=positive/case mods=audit_case_mods(home,report) @@ -118,7 +110,7 @@ def audit_shard(folder,report,*,relocated=False): if len(declared)!=len(records) or actual!=declared:raise ValueError('Missing, duplicate or extra native receipt') for path in positive.rglob('*'): if path.is_file() and path.name.endswith(('.log','.log.gz')): - audit(path,native=True,fixture_logins=True);logs.append({'path':str(path),'sha256':digest(path)}) + audit(path);logs.append({'path':str(path),'sha256':digest(path)}) if path.is_file() and 'crash-reports' in path.parts:raise ValueError('Unexpected hosted crash report') if not logs:raise ValueError('Missing hosted positive complete logs') return {'receipts':records,'positive_complete_logs':logs} diff --git a/tools/native_runtime.py b/tools/native_runtime.py index 37e7973..6b0db45 100644 --- a/tools/native_runtime.py +++ b/tools/native_runtime.py @@ -1,4 +1,4 @@ -"""Disposable official Forge 40 launches, separate from the development bootstrap.""" +"""Disposable official Forge45 launches, separate from the development bootstrap.""" import hashlib import json import os @@ -12,20 +12,32 @@ from command_logs import run_logged from runtime_diagnostics import run_monitored,validate_logging_inputs -FORGE = 'forge-1.18.2-40.3.12' -INSTALLER_SHA256 = 'DCFF493F6C2212BDF6AE087F98029CD317A79759AB1009DCE23E63E7DBA0E216' -SERVER_INPUTS = json.loads(Path(__file__).with_name('native118_server_inputs.json').read_text(encoding='utf-8')) -# Native40's copied-world history harness uses its separately qualified single -# preparation worker. Ordinary/client games retain native CPU/worker/GC defaults. -WORKER_BUDGET_PROOF = json.loads(Path(__file__).with_name('native118_worker_budget.json').read_text(encoding='utf-8')) -if (WORKER_BUDGET_PROOF['minecraft'], WORKER_BUDGET_PROOF['forge'], - WORKER_BUDGET_PROOF['reloads'], WORKER_BUDGET_PROOF['jvms'], - WORKER_BUDGET_PROOF['qualified']) != ('1.18.2', '40.3.12', 200, 4, True): - raise RuntimeError('Missing native40 resource-policy qualification') -HISTORICAL_JVM_ARGS = tuple(WORKER_BUDGET_PROOF['jvm_args']) -if (HISTORICAL_JVM_ARGS != ('-Dmax.bg.threads=1',) or - WORKER_BUDGET_PROOF['native_preparation_workers'] != 1): - raise RuntimeError('Missing exact native40 historical preparation-worker qualification') +FORGE = 'forge-1.19.4-45.4.5' +INSTALLER_SHA256 = '1B4475D6ECE01E7242855504FBE0FE37AC79EFD7B3281BF42B8DCF390D421F35' +SERVER_INPUTS = json.loads(Path(__file__).with_name('native119_server_inputs.json').read_text(encoding='utf-8')) +WORKER_BUDGET_PATH = Path(__file__).with_name('native119_worker_budget.json') +WORKER_BUDGET_PROOF = json.loads(WORKER_BUDGET_PATH.read_text(encoding='utf-8')) +# Native45 uses its actual default CPU/worker/GC configuration. A new target's +# history cannot borrow an older engine's resource qualification. +HISTORICAL_JVM_ARGS = () + + +def require_historical_budget(): + 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.19.4','45.4.5',200,4,True): + raise RuntimeError('Native45 historical runtime policy has not been qualified') + if proof.get('jvm_args')!=[]: + raise RuntimeError('Unqualified native45 worker or GC override') + source=Path(__file__).resolve().parents[1]/'src/controlTest/java/buildcontrol/ResourceControl.java' + manifest=Path(__file__).with_name('native119_server_inputs.json') + if proof.get('controller_sha256')!=digest(source) or proof.get('server_input_manifest_sha256')!=digest(manifest): + raise RuntimeError('Native45 resource qualification source or SDK binding changed') + if not __import__('re').fullmatch('[A-F0-9]{64}',proof.get('qualification_evidence_sha256','')): + raise RuntimeError('Missing native45 resource qualification identity') + evidence=Path(__file__).resolve().parents[1]/'.gradle/native119-resource-qualification/proof.json' + if not evidence.is_file() or digest(evidence)!=proof['qualification_evidence_sha256']: + raise RuntimeError('Missing or changed actual local native45 resource qualification evidence') + return HISTORICAL_JVM_ARGS def project_assets(project): @@ -43,24 +55,17 @@ def fingerprint(directory): def verify_server_inputs(distribution): - """Pin raw inputs and seal both installer-generated archive contents. + """Pin the exact completed SDK after both official installer sides. - Only installer directory timestamps and ZIP entry order may vary. The - returned hashes bind the actual SDK bytes prepared for every process. + Native45 generated archives match the installer's declared expected hashes. + No changed/truncated class, intermediate, resource or extra byte is accepted. """ - from native_sdk_contents import verify_generated_archive - generated=json.loads(Path(__file__).with_name('native118_generated_sdk.json').read_text()) actual={} for name,expected in SERVER_INPUTS.items(): artifact=Path(distribution)/name - if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing native40 SDK input: '+name) + if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing native45 SDK input: '+name) value=digest(artifact) - if value!=expected: - seal=generated.get(name) - if not seal or seal['canonical_archive_sha256']!=expected: - raise RuntimeError('Incorrect official Forge40 server input: '+name) - verify_generated_archive(artifact,seal) - if digest(artifact)!=value:raise RuntimeError('Native SDK changed during verification: '+name) + if value!=expected:raise RuntimeError('Incorrect official Forge45 server input: '+name) actual[name]=value return actual @@ -82,7 +87,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 40 server installation') + raise FileNotFoundError('Incomplete official Forge45 server installation') verify_server_inputs(distribution) home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() @@ -119,6 +124,7 @@ def prepare_server(home, distribution, candidate=None, probe=None): def run_java(java, home, phase, *, timeout=240, jvm_args=()): """Run one owned Forge JVM, retaining failure evidence before returning.""" + if phase=='abort' or phase.startswith('upgrade-'):require_historical_budget() java = Path(java).resolve() home = home.resolve() release = java.parent.parent / 'release' @@ -141,8 +147,8 @@ def run_java(java, home, phase, *, timeout=240, jvm_args=()): # Relative output avoids the drive-letter delimiter in unified logging. gc = diagnostics.relative_to(home) / 'gc.txt' arguments = 'win_args.txt' if os.name == 'nt' else 'unix_args.txt' - command = [str(java), '-Xms256m', '-Xmx2G', '-Xlog:gc*,safepoint:file=' + gc.as_posix() + ':time,uptime,level,tags', - *jvm_args, '@libraries/net/minecraftforge/forge/1.18.2-40.3.12/' + arguments, '--nogui'] + 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.19.4-45.4.5/' + arguments, '--nogui'] validate_logging_inputs(command) def initial_diagnostics(process): # Short refusals and successful phases must retain real thread/heap @@ -184,14 +190,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 != 2975 or generator.kind != 8 or generator.value != 'minecraft:flat': + if version.kind != 3 or version.value != 3337 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 native2975 flat codec header') from error + raise RuntimeError('Expected an explicit native3337 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':2975, 'generator':'minecraft:flat', + 'sha256':expected_sha256, 'native_data_version':3337, 'generator':'minecraft:flat', 'only_header_copied':True}, indent=2)+'\n', encoding='utf-8') @@ -203,18 +209,18 @@ def run_server(java, home, phase, *, timeout=240, extra=(), marker=None, message raise RuntimeError('Refusing stale runtime evidence for ' + phase) completed = run_java(java, home, phase, timeout=timeout, jvm_args=['-Xmx2G', '-Dlootablebodies.phase='+phase, *extra]) - text = log.read_text(encoding='utf-8', errors='replace') + text = log.read_text(encoding='utf-8') if completed.returncode or not marker.is_file() or (message and message not in text): raise RuntimeError('Native packaged ' + phase + ' failed: ' + str(log)) if list((home / 'crash-reports').glob('*')): raise RuntimeError('Native runtime crash reports: ' + str(home)) - from audit_runtime_logs import audit - audit(log,native=True) + from native119_logs import audit + audit(log) for name in ('latest.log', 'debug.log'): runtime_log = home / (phase+'.'+name) if not runtime_log.is_file() or not runtime_log.stat().st_size: raise FileNotFoundError('Missing complete native log: '+str(runtime_log)) - audit(runtime_log,native=True) + audit(runtime_log) return log @@ -264,13 +270,13 @@ def validate_client_paths(paths): def client_inputs(runtime, prepared): - from native118_client import load + from native119_client import load base, forge, record = load(runtime, prepared) return base, forge, [Path(p) for p in record['classpath']], Path(record['natives']) def prepare_assets(runtime, cache=None): - raise RuntimeError('Use pinned native1.18 assets; inherited cache-copy hydration is disabled') + raise RuntimeError('Use pinned native1.19.4 assets; inherited cache-copy hydration is disabled') def prepare_client(home, candidate=None, probe=None): @@ -287,29 +293,29 @@ def prepare_client(home, candidate=None, probe=None): def client_command(java, home, runtime, assets, *, username='LootableTest', extra=(), prepared=None): - from native118_client import command + from native119_client import command if prepared is None: - prepared=Path(__file__).resolve().parents[1]/'.gradle/native118-client-inputs' + prepared=Path(__file__).resolve().parents[1]/'.gradle/native119-client-inputs' return command(java,home,runtime,assets,prepared,username=username,extra=extra) def install(java, home, client=False): - """Install native40 locally; its installer owns the server downloads.""" + """Install native45 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.18.2-40.3.12/'+installer.name,installer,INSTALLER_SHA256) + fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.19.4-45.4.5/'+installer.name,installer,INSTALLER_SHA256) if client: - from native118_client import BASE_VERSION,BASE_SHA1 + from native119_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.18.2.json'},base) + _download({'sha1':BASE_SHA1,'url':'https://piston-meta.mojang.com/v1/packages/'+BASE_SHA1+'/1.19.4.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, '--installClient' if client else '--installServer',str(home)], cwd=home,path=home/'install.log',timeout=600) if client: - from native118_client import prepare + from native119_client import prepare inputs=prepare(home,home/'prepared-inputs')['files'] else: inputs=verify_server_inputs(home) diff --git a/tools/owned_process.py b/tools/owned_process.py index 498f212..e410ebf 100644 --- a/tools/owned_process.py +++ b/tools/owned_process.py @@ -77,6 +77,10 @@ def reap(self,timeout=None): def native_process(command,*,cwd,log,timeout,console_input=False): + command=list(command) + encodings=[s for s in command if s.startswith('-Dfile.encoding=')] + if encodings and encodings!=['-Dfile.encoding=UTF-8']:raise ValueError('Owned runtime requires one explicit UTF-8 encoding') + if not encodings:command.insert(1,'-Dfile.encoding=UTF-8') validate_logging_inputs(command) java=Path(command[0]).resolve();metadata=java.parent.parent/'release' values=dict(line.split('=',1) for line in metadata.read_text().splitlines() if '=' in line) diff --git a/tools/prepare_native118_tools.py b/tools/prepare_native118_tools.py new file mode 100644 index 0000000..7e7b370 --- /dev/null +++ b/tools/prepare_native118_tools.py @@ -0,0 +1,39 @@ +"""Compile an isolated writer against exact public18, never the target19 API.""" +import argparse,json,os,shutil,subprocess +from pathlib import Path +from prepare_legacy_tools import prepare,digest +from command_logs import run_build_input_logged + +SOURCE='89f0a9f64ffff41b5fd751528c630ffee47998cb' +PUBLIC_MAIN='34EF41EFCE4666A684EA202DAF8712162A1C403A90120BA11EF0260533FB690F' +OUTPUT='native118-seed/lootablebodies-native118-world-fixtures-reobf.jar' + +def main(): + p=argparse.ArgumentParser(description=__doc__) + p.add_argument('--output',type=Path,required=True) + p.add_argument('--java17-home',type=Path,required=True) + p.add_argument('--java25-home',type=Path,required=True) + p.add_argument('--public-main',type=Path,required=True) + a=p.parse_args();project=Path(__file__).resolve().parents[1];output=a.output.resolve() + if digest(a.public_main)!=PUBLIC_MAIN:raise ValueError('Native18 writer requires genuine public34EF main bytes') + source=prepare(project,output,SOURCE) + env=dict(os.environ,JAVA_HOME=str(a.java17_home.resolve()),GRADLE_USER_HOME='D:/GradleCaches/minecraft-mods-shared',LOOTABLEBODIES_JAVA17_HOME=str(a.java17_home.resolve()),LOOTABLEBODIES_MAVENIZER_JAVA_HOME=str(a.java25_home.resolve())) + env.pop('GITHUB_ACTIONS',None) + writer_files={f.relative_to(project).as_posix():digest(f) for f in (project/'src/nativeSeed118').rglob('*') if f.is_file()} + init=project/'gradle/native118-writer.init.gradle';init_hash=digest(init) + (output/'writer-inputs.json').write_text(json.dumps({'source':SOURCE,'public_main_sha256':PUBLIC_MAIN,'files':writer_files,'init_sha256':init_hash},indent=2)+'\n',encoding='utf-8') + command=[str(a.java17_home.resolve()/'bin/java.exe'),'-Xmx64m','-Xms64m','-Dfile.encoding=UTF-8','-Dorg.gradle.appname=gradlew','-classpath',str(source/'gradle/wrapper/gradle-wrapper.jar'),'org.gradle.wrapper.GradleWrapperMain','prepareNative118WorldFixtures','--init-script='+str(init),'-PhistoricalWriterSource='+str(project),'--offline','--no-daemon','--no-build-cache','--console=plain','--stacktrace'] + run_build_input_logged(command,cwd=source,env=env,path=output/'build.log',timeout=1800) + artifact=source/'build'/OUTPUT + if not artifact.is_file() or not artifact.stat().st_size:raise FileNotFoundError(artifact) + if digest(init)!=init_hash or any(digest(project/name)!=value for name,value in writer_files.items()):raise ValueError('Historical writer sources changed during compilation') + target=project/'build'/OUTPUT;target.parent.mkdir(parents=True,exist_ok=True) + if target.exists():raise FileExistsError(target) + shutil.copy2(artifact,target) + main=output/'public-main'/a.public_main.name;main.parent.mkdir();shutil.copy2(a.public_main,main) + if digest(main)!=PUBLIC_MAIN:raise ValueError('Copied public18 main bytes changed') + record={'source':SOURCE,'public_main':str(main),'public_main_sha256':PUBLIC_MAIN,'artifact':OUTPUT,'sha256':digest(artifact),'writer_sources':writer_files,'production_java':'17.0.20.1+1','compiled_against':'Exact released18 native40; target19 classes not used','runtime_qualified':False} + (output/'tools.json').write_text(json.dumps(record,indent=2)+'\n',encoding='utf-8') + print('PUBLIC18_NATIVE_WRITER_COMPILE_PASS_RUNTIME_PENDING',flush=True) + +if __name__=='__main__':main() diff --git a/tools/prepared_worlds.py b/tools/prepared_worlds.py index ecb1adb..5317c1d 100644 --- a/tools/prepared_worlds.py +++ b/tools/prepared_worlds.py @@ -13,8 +13,8 @@ def unique(pairs): result[name]=value return result record=json.loads(path.read_text(encoding='utf-8'),object_pairs_hook=unique) - if record.get('schema')!=1 or record.get('target_data_version')!=2975: - raise ValueError('Wrong native18 prepared manifest schema/target') + if record.get('schema')!=1 or record.get('target_data_version')!=3337: + raise ValueError('Wrong native19 prepared manifest schema/target') origins=record.get('origins',{}) if set(origins)!=set(expected):raise ValueError('Prepared manifest must contain every exact origin') paths=[Path(details['world']).resolve() for details in origins.values()] diff --git a/tools/test_historical_runtime_budget.py b/tools/test_historical_runtime_budget.py index 3e4acab..f62959e 100644 --- a/tools/test_historical_runtime_budget.py +++ b/tools/test_historical_runtime_budget.py @@ -10,7 +10,7 @@ class HistoricalRuntimeBudgetTests(unittest.TestCase): - def test_every_historical_phase_bounds_native_reload_workers_without_changing_its_gate(self): + def test_every_historical_phase_keeps_native45_defaults_without_changing_its_gate(self): launches = [] def launch(java, home, phase, **options): @@ -28,7 +28,7 @@ def launch(java, home, phase, **options): self.assertEqual(PHASES, [p for p, options in launches]) for phase, options in launches: self.assertEqual(240, options['timeout']) - self.assertEqual(['-Dmax.bg.threads=1'],[p for p in options['jvm_args'] if p.startswith('-Dmax.bg.threads=')]) + self.assertEqual([],[p for p in options['jvm_args'] if p.startswith('-Dmax.bg.threads=')]) self.assertFalse(any(p.startswith(('-XX:ActiveProcessorCount','-XX:+Use')) for p in options['jvm_args'])) self.assertIn('-Dlootablebodies.phase='+phase, options['jvm_args']) @@ -37,7 +37,7 @@ def test_unsafe_refusals_use_the_same_budget_and_keep_their_shorter_deadline(sel run_abort(Path('java'), Path('owned')) options=launch.call_args.kwargs self.assertEqual(180, options['timeout']) - self.assertEqual(['-Dmax.bg.threads=1'],[p for p in options['jvm_args'] if p.startswith('-Dmax.bg.threads=')]) + self.assertEqual([],[p for p in options['jvm_args'] if p.startswith('-Dmax.bg.threads=')]) self.assertFalse(any(p.startswith(('-XX:ActiveProcessorCount','-XX:+Use')) for p in options['jvm_args'])) self.assertNotIn('-Dlootablebodies.phase=abort', options['jvm_args']) @@ -56,6 +56,28 @@ def launch(java, home, phase, **options): native_runtime.run_server(Path('java'), Path(directory), 'ordinary') self.assertFalse(any(p.startswith(('-XX:ActiveProcessorCount','-Dmax.bg.threads','-XX:+Use')) for p in arguments)) + def test_missing_target_qualification_refuses_before_any_historical_process_starts(self): + import json + with tempfile.TemporaryDirectory() as directory: + policy=Path(directory)/'policy.json' + for change in ({'qualified':False},{'minecraft':'1.18.2','qualified':True}): + policy.write_text(json.dumps(change)) + with patch.object(native_runtime,'WORKER_BUDGET_PATH',policy),patch.object(native_runtime,'run_monitored') as launch: + with self.assertRaisesRegex(RuntimeError,'has not been qualified'): + native_runtime.run_java(Path('not-a-jdk'),Path('owned'),'upgrade-near') + launch.assert_not_called() + + 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.19.4','forge':'45.4.5','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: + with self.assertRaises(RuntimeError):native_runtime.run_java(Path('not-a-jdk'),Path('owned'),'abort') + launch.assert_not_called() + if __name__ == '__main__': unittest.main() diff --git a/tools/test_hosted_policy.py b/tools/test_hosted_policy.py index d88b56b..563642a 100644 --- a/tools/test_hosted_policy.py +++ b/tools/test_hosted_policy.py @@ -5,7 +5,7 @@ class HostedPolicyTests(unittest.TestCase): def test_raw_jdk_version_is_rejected_before_java_preparation(self): - text=(Path(__file__).resolve().parents[1]/'.github/workflows/native118.yml').read_text() + text=(Path(__file__).resolve().parents[1]/'.github/workflows/native119.yml').read_text() with self.assertRaises(ValueError):validate_workflow(text.replace('17.0.20+101','17.0.20.1+1')) def test_hosted_roots_allow_archive_builds_and_reject_every_test_or_game_entry(self): @@ -18,28 +18,28 @@ def test_current_workflow_is_compile_archive_only(self): root=Path(__file__).resolve().parents[1];check_source(root) def test_injected_python_games_full_suites_and_sdk_installers_are_rejected(self): - text=(Path(__file__).resolve().parents[1]/'.github/workflows/native118.yml').read_text() + text=(Path(__file__).resolve().parents[1]/'.github/workflows/native119.yml').read_text() for command in ('python3 -m unittest discover -s tools','python3 tools/verify_world_upgrades.py','python3 tools/native_runtime.py','python3 tools/prepare_native117_tools.py','xvfb-run ./gradlew runClient','python3 tools/new_game_runner.py','curl https://example.invalid/server.jar','java -jar server.jar'): changed=text.replace(' chmod +x gradlew',' '+command+'\n chmod +x gradlew') with self.subTest(command=command),self.assertRaises(ValueError):validate_workflow(changed) def test_missing_fork_guard_or_pipe_failure_guard_is_rejected(self): - text=(Path(__file__).resolve().parents[1]/'.github/workflows/native118.yml').read_text() + text=(Path(__file__).resolve().parents[1]/'.github/workflows/native119.yml').read_text() for removed in ("if: github.repository == 'MinecraftModDevelopmentMods/LootableBodies'",'set -euo pipefail','shell: bash'): 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.9.118021';base='LootableBodies-'+version + version='2.4.10.119041';base='LootableBodies-'+version names=['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom'] - good={'version':version,'minecraft':'1.18.2','forge':'40.3.12','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in names}} + good={'version':version,'minecraft':'1.19.4','forge':'45.4.5','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.9.118021';base='LootableBodies-'+version - good={'version':version,'minecraft':'1.18.2','forge':'40.3.12','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.10.119041';base='LootableBodies-'+version + good={'version':version,'minecraft':'1.19.4','forge':'45.4.5','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_migration_ci.py b/tools/test_migration_ci.py index bd62bbb..9d01eb0 100644 --- a/tools/test_migration_ci.py +++ b/tools/test_migration_ci.py @@ -36,10 +36,12 @@ def test_six_shards_cover_every_origin_exactly_once(self): for value in [-1,6]: with self.assertRaises(ValueError):ci.partition(list(ORIGINS),value) - def test_all_940_phases_and_188_refusals_retain_every_earlier_case(self): + def test_all_1140_phases_and_228_refusals_retain_every_earlier_case(self): result = ci.aggregate(self.reports(), list(ORIGINS), 'a'*40) - self.assertEqual((94,940,188),(len(result['cases']),result['jvm_phases'],result['abort_cases'])) - native17=[name for name in result['cases'] if not name.startswith(('released-117-','recovery-117-','via117-'))] + self.assertEqual((114,1140,228),(len(result['cases']),result['jvm_phases'],result['abort_cases'])) + native18=[name for name in result['cases'] if not name.startswith(('released-118-','recovery-118-','via118-'))] + self.assertEqual(94,len(native18));self.assertEqual(940,sum(len(result['cases'][name]['phases']) for name in native18));self.assertEqual(188,sum(len(result['aborts'][name]['conditions']) for name in native18)) + native17=[name for name in native18 if not name.startswith(('released-117-','recovery-117-','via117-'))] self.assertEqual(76,len(native17));self.assertEqual(760,sum(len(result['cases'][name]['phases']) for name in native17)) self.assertEqual(152,sum(len(result['aborts'][name]['conditions']) for name in native17)) native16=[name for name in native17 if not name.startswith(('released-116-','recovery-116-','via116-'))] diff --git a/tools/test_native118_build.py b/tools/test_native119_build.py similarity index 87% rename from tools/test_native118_build.py rename to tools/test_native119_build.py index 15bef29..c82786a 100644 --- a/tools/test_native118_build.py +++ b/tools/test_native119_build.py @@ -1,16 +1,16 @@ import json,tempfile,unittest from pathlib import Path -from native118_build import check_bundle +from native119_build import check_bundle from native_runtime import digest from release_identity import MOD_VERSION,MAIN_JAR -class Native118BuildContractTests(unittest.TestCase): +class Native119BuildContractTests(unittest.TestCase): def test_missing_changed_extra_and_unqualified_inputs_are_fatal(self): with tempfile.TemporaryDirectory() as directory: 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.18.2','forge':'40.3.12','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{p.name:digest(p) for p in bundle.iterdir()}} + record={'version':MOD_VERSION,'minecraft':'1.19.4','forge':'45.4.5','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): @@ -29,15 +29,15 @@ def test_missing_changed_extra_and_unqualified_inputs_are_fatal(self): def test_hosted_target_contains_no_game_or_sdk_command(self): from pathlib import Path import re - text=(Path(__file__).resolve().parents[1]/'.github/workflows/native118.yml').read_text() + text=(Path(__file__).resolve().parents[1]/'.github/workflows/native119.yml').read_text() commands='\n'.join(re.findall(r'(?m)^ run: \|\n((?: .*\n)+)',text)) self.assertIn('./gradlew assemble verifyProductionRuntimeClasspath prepareReleaseBundle ',commands) for forbidden in ('./gradlew test','./gradlew build','./gradlew check','unittest discover','verify_world_upgrades.py','native_runtime.py','verify_packaged','native117_ci.py','integrationTest','--seed-only'): self.assertNotIn(forbidden,commands) uploads=re.findall(r'(?m)^ uses: actions/upload-artifact@[^\n]+\n(.*?)(?=^ -|\Z)',text,re.S) self.assertEqual(2,len(uploads));self.assertTrue(all('if-no-files-found: error' in s for s in uploads)) - independent=(Path(__file__).resolve().parents[1]/'.github/workflows/artifact-audit-1.18.yml').read_text() - self.assertIn('python3 -B tools/native118_build.py --bundle audited',independent) + independent=(Path(__file__).resolve().parents[1]/'.github/workflows/artifact-audit-1.19.yml').read_text() + self.assertIn('python3 -B tools/native119_build.py --bundle audited',independent) self.assertIn('run-id: ${{ github.event.workflow_run.id }}',independent) self.assertIn('if-no-files-found: error',independent) for forbidden in ('./gradlew','actions/setup-java@','native_runtime.py','verify_packaged'): diff --git a/tools/test_native119_client.py b/tools/test_native119_client.py new file mode 100644 index 0000000..9bd41c3 --- /dev/null +++ b/tools/test_native119_client.py @@ -0,0 +1,56 @@ +import hashlib,json,shutil,struct,tempfile,unittest,zipfile +from pathlib import Path +from unittest.mock import patch +import native119_client as client + + +class Native119ArchitectureTests(unittest.TestCase): + def test_os_only_rules_cannot_select_other_windows_architectures(self): + libraries=json.loads((Path(__file__).parent/'fixtures/native119-native-library-excerpt.json').read_text())['libraries'] + for architecture,expected in [('amd64','natives-windows'),('x86','natives-windows-x86'),('arm64','natives-windows-arm64')]: + self.assertEqual([expected],[item['name'].split(':')[3] for item in libraries if client.native_for_platform(item,'windows',architecture)]) + with self.assertRaises(RuntimeError):client.native_for_platform(libraries[0],'windows','unknown') + + def test_native_dll_header_and_actual_cpu_type_are_required(self): + image=bytearray(128);image[:2]=b'MZ';struct.pack_into('iiii',0,0,0,number))}) - def chunk(self,bodies,version=2975,position=(8,8)): +class Native119PersistenceTest(unittest.TestCase): + def body(self,number=1,marker=9):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=3337,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 - def test_native18_saved_entities_count_all_dimensions_without_mutation(self): + def test_native19_saved_entities_count_all_dimensions_without_mutation(self): with tempfile.TemporaryDirectory() as directory: 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 [(7,2975,(8,8)),(8,2730,(8,8)),(8,2975,(9,8))]: + for marker,version,position in [(8,3337,(8,8)),(9,2975,(8,8)),(9,3337,(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) @@ -27,7 +27,7 @@ def test_duplicate_active_id_and_malformed_list_cannot_pass(self): if not duplicate:chunk['root'].value['Entities']=Tag(8,'malformed') self.write(world,chunk) with self.assertRaises(ValueError):count_persisted(world,0) - def test_corpses_in_old_or_flattened_terrain_cannot_count_as_saved_native18(self): + def test_corpses_in_old_or_flattened_terrain_cannot_count_as_saved_native19(self): for flattened in [False,True]: with self.subTest(flattened=flattened),tempfile.TemporaryDirectory() as directory: world=Path(directory);root={'entities':Tag(9,(10,[self.body()]))} if flattened else {'Level':Tag(10,{'Entities':Tag(9,(10,[self.body(1,7)]))})} @@ -50,17 +50,17 @@ def test_stamp_damage_exercises_all_nine_native17_entity_chunks(self): for chunk in read_region(path).values():self.assertEqual(7,chunk['root'].value['Entities'].value[1][0].value['LootableBodiesDataVersion'].value) def test_valid_future_entity_fixture_preserves_original_and_rejects_wrong_source(self): from verify_upgrade_abort import inject_future_entity_version - for version in [2730,2975]: + for version in [2975,3337]: with self.subTest(version=version),tempfile.TemporaryDirectory() as directory: - home=Path(directory);world=home/'world';path=self.write(world,self.chunk([self.body(marker=7)],version)) + home=Path(directory);world=home/'world';path=self.write(world,self.chunk([self.body(marker=8)],version)) original=path.read_bytes() - if version!=2730: + if version!=2975: with self.assertRaises(ValueError):inject_future_entity_version(world) self.assertEqual(original,path.read_bytes());self.assertFalse((home/'future-entity-original.mca').exists()) else: mutation=inject_future_entity_version(world) self.assertEqual(original,(home/'future-entity-original.mca').read_bytes()) - self.assertEqual(Tag(3,2976),read_region(path)[264]['root'].value['DataVersion']) - self.assertEqual(7,mutation['corpse_marker']) + self.assertEqual(Tag(3,3338),read_region(path)[264]['root'].value['DataVersion']) + self.assertEqual(8,mutation['corpse_marker']) if __name__=='__main__':unittest.main() diff --git a/tools/test_native45_platform_config.py b/tools/test_native45_platform_config.py new file mode 100644 index 0000000..d189fe7 --- /dev/null +++ b/tools/test_native45_platform_config.py @@ -0,0 +1,31 @@ +import tempfile,unittest +from pathlib import Path +from native_runtime import fingerprint +from verify_upgrade_abort import allowed_native45_platform_additions + +class Native45ConfigTest(unittest.TestCase): + def target(self,root,text=None): + path=root/'serverconfig/forge-server.toml';path.parent.mkdir() + path.write_bytes(text or (Path(__file__).parent/'fixtures/forge45-server-default.toml').read_bytes());return path + def old(self):return (Path(__file__).parent/'fixtures/forge36-server-default.toml').read_bytes() + def test_exact_actual_native_default_addition(self): + with tempfile.TemporaryDirectory() as directory: + world=Path(directory);self.target(world);after=fingerprint(world) + self.assertEqual(['serverconfig/forge-server.toml'],allowed_native45_platform_additions({},after,world)) + def test_exact_actual_native_spec_retirement_preserves_supported_choices(self): + with tempfile.TemporaryDirectory() as directory: + world=Path(directory);path=self.target(world,self.old().replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true'));original=path.read_bytes();before=fingerprint(world) + path.write_bytes((Path(__file__).parent/'fixtures/forge45-server-default.toml').read_bytes().replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true')) + self.assertEqual(['serverconfig/forge-server.toml'],allowed_native45_platform_additions(before,fingerprint(world),world,original)) + def test_supported_mutation_unknown_retirement_or_wrong_type_cannot_pass(self): + for change in ['value','unknown','type']: + with self.subTest(change=change),tempfile.TemporaryDirectory() as directory: + world=Path(directory);original=self.old() + if change=='unknown':original+=b'unknownFlag = true\n' + if change=='type':original=original.replace(b'removeErroringEntities = false',b'removeErroringEntities = 0') + path=self.target(world,original);before=fingerprint(world);target=(Path(__file__).parent/'fixtures/forge45-server-default.toml').read_bytes() + if change=='value':target=target.replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true') + path.write_bytes(target) + self.assertEqual([],allowed_native45_platform_additions(before,fingerprint(world),world,original)) + +if __name__=='__main__':unittest.main() diff --git a/tools/test_native_evidence.py b/tools/test_native_evidence.py index b11fc34..def937c 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.18.2-40.3.12/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], + '@libraries/net/minecraftforge/forge/1.19.4-45.4.5/'+('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.18.2-40.3.12/win_args.txt') + record['command'].append('@libraries/net/minecraftforge/forge/1.19.4-45.4.5/win_args.txt') (folder/'result.json').write_text(json.dumps(record)) with self.assertRaises(ValueError):audit_phase(home,'upgrade-near',relocated=True) @@ -87,12 +87,12 @@ def test_old_processor_caps_worker_overrides_and_gc_selection_are_fatal(self): (folder/'result.json').write_text(json.dumps(record)) with self.assertRaises(ValueError):audit_phase(home,'upgrade-near') - def test_exact_native40_serial_budget_is_scoped_and_duplicate_overrides_fail(self): + def test_old_single_worker_and_duplicate_overrides_both_fail(self): with tempfile.TemporaryDirectory() as directory: home=Path(directory);folder,record=self.fixture(home) record['command'].insert(1,'-Dmax.bg.threads=1') (folder/'result.json').write_text(json.dumps(record)) - audit_phase(home,'upgrade-near') + with self.assertRaises(ValueError):audit_phase(home,'upgrade-near') record['command'].insert(1,'-Dmax.bg.threads=1') (folder/'result.json').write_text(json.dumps(record)) with self.assertRaises(ValueError):audit_phase(home,'upgrade-near') diff --git a/tools/test_native_server_preparation.py b/tools/test_native_server_preparation.py index 580fb8f..1d0a40a 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.18.2/server-1.18.2.jar' + relative='libraries/net/minecraft/server/1.19.4/server-1.19.4.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 Forge40 server input'):runtime.install(Path('fixture-java'),home) + with self.assertRaisesRegex(RuntimeError,'Incorrect official Forge45 server input'):runtime.install(Path('fixture-java'),home) prefetch.assert_not_called();self.assertEqual(1,installer.call_count) self.assertFalse((home/'runtime-inputs.json').exists()) def test_missing_installed_input_is_fatal(self): diff --git a/tools/test_prepared_worlds.py b/tools/test_prepared_worlds.py index 5600c2a..c57cbfd 100644 --- a/tools/test_prepared_worlds.py +++ b/tools/test_prepared_worlds.py @@ -11,7 +11,7 @@ def fixture(self,root): records={name:{'world':str(world.resolve()),'files':fingerprint(world),'minecraft':mc,'mod_version':version,'jar_sha256':sha} for name,(mc,version,sha) in expected.items()} second=root/'second';second.mkdir();(second/'level.dat').write_bytes(b'separate synthetic source') records['two'].update(world=str(second.resolve()),files=fingerprint(second)) - path=root/'inputs.json';record={'schema':1,'target_data_version':2975,'origins':records};path.write_text(json.dumps(record)) + path=root/'inputs.json';record={'schema':1,'target_data_version':3337,'origins':records};path.write_text(json.dumps(record)) return path,record,expected,world def test_exact_subset_reuses_original_paths_and_seals(self): with tempfile.TemporaryDirectory() as directory: diff --git a/tools/verify_multiplayer.py b/tools/verify_multiplayer.py index 659266c..3a94184 100644 --- a/tools/verify_multiplayer.py +++ b/tools/verify_multiplayer.py @@ -8,7 +8,8 @@ import socket import subprocess import time -from audit_runtime_logs import audit +from native119_logs import audit +from native119_client_logs import audit_client from fixture_login import LoginMonitor, start_clients, write_receipts, archive_live_logs from native_runtime import project_assets,FORGE,digest,prepare_server as prepare,run_server as run,client_command as native_client_command,prepare_client,seed_flat_fixture from owned_process import native_process @@ -93,7 +94,8 @@ def start(name): raise RuntimeError('Missing multiplayer check result') write_receipts(root) for name in ('server','clientA','clientB'): - audit(root/(name+'.log'),native=True);audit(root/name/'logs/latest.log',native=True);audit(root/name/'logs/debug.log',native=True) + selected_audit=audit if name=='server' else audit_client + selected_audit(root/(name+'.log'),native=True);selected_audit(root/name/'logs/latest.log',native=True);selected_audit(root/name/'logs/debug.log',native=True) if list((root/name).rglob('crash-reports/*')): raise RuntimeError('Crash report '+name) if digest(root/name/'mods'/MAIN)!=before: raise RuntimeError('Candidate mismatch '+name) for role in ('A','B'): diff --git a/tools/verify_native_respawn_control.py b/tools/verify_native_respawn_control.py index 28493e0..bc50dc6 100644 --- a/tools/verify_native_respawn_control.py +++ b/tools/verify_native_respawn_control.py @@ -1,13 +1,15 @@ -"""Exercise native40 delayed respawns/FIFO packets without Lootable Bodies.""" +"""Exercise native45 delayed respawns/FIFO packets without Lootable Bodies.""" import argparse,json,socket,subprocess,time,os,re from pathlib import Path -from audit_runtime_logs import audit, forge36_dead_player_movement_blocks +from native119_logs import audit +from native119_client_logs import audit_client +from audit_runtime_logs import audit as legacy_audit, forge36_dead_player_movement_blocks from native_runtime import prepare_server,prepare_client,client_command,project_assets,FORGE,digest,seed_flat_fixture from owned_process import native_process def control_error_counts(path): """Retained native36 log-fixture contract; never used to qualify native37.""" - report=audit(path,native=True) + report=legacy_audit(path,native=True) _,count=forge36_dead_player_movement_blocks(path.read_text(errors='replace').splitlines(),position_only=True) if count!=8:raise RuntimeError('Native control did not reproduce eight delayed position packets') return count,report['known_forge36_dead_player_movement_failures']-count @@ -73,13 +75,13 @@ def start(name): if list((root/name).rglob('crash-reports/*.txt')):raise RuntimeError('Unexpected native control crash') for path in (root/(name+'.log'),root/name/'logs/latest.log',root/name/'logs/debug.log'): if not path.is_file():raise FileNotFoundError(path) - reports.append(audit(path,native=True)) + reports.append((audit if name=='server' else audit_client)(path,native=True)) 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('Native40 control did not exercise all eight real delayed respawn/FIFO actions') - result={'lootable_bodies_installed':False,'respawns':8,'native40_delayed_fifo_batches':batches, - 'legacy36_error_matcher_used':False,'native40_complete_logs_clean':True, + raise RuntimeError('Native45 control did not exercise all eight real delayed respawn/FIFO actions') + result={'lootable_bodies_installed':False,'respawns':8,'native45_delayed_fifo_batches':batches, + 'legacy36_error_matcher_used':False,'native45_complete_logs_audited':True,'no_unclassified_diagnostics':True, '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_normal_server.py b/tools/verify_normal_server.py index 021c742..db2357a 100644 --- a/tools/verify_normal_server.py +++ b/tools/verify_normal_server.py @@ -7,7 +7,7 @@ import time import re import shutil -from audit_runtime_logs import audit +from native119_logs import audit from verify_packaged import prepare, digest, FORGE, MAIN from native_runtime import seed_loader_defaults,seed_flat_fixture from owned_process import native_process diff --git a/tools/verify_packaged.py b/tools/verify_packaged.py index 4bd59cf..9b1b318 100644 --- a/tools/verify_packaged.py +++ b/tools/verify_packaged.py @@ -1,4 +1,4 @@ -"""Verify the exact production jar in disposable native Forge 40 servers.""" +"""Verify the exact production jar in disposable native Forge 45 servers.""" import argparse import json from pathlib import Path diff --git a/tools/verify_packaged_client.py b/tools/verify_packaged_client.py index e9cf3c4..44d77e7 100644 --- a/tools/verify_packaged_client.py +++ b/tools/verify_packaged_client.py @@ -1,11 +1,11 @@ -"""Run the exact production jar and isolated verifier in official Forge 40.""" +"""Run the exact production jar and isolated verifier in official Forge 45.""" import argparse import hashlib import json from pathlib import Path import shutil import subprocess -from audit_runtime_logs import audit +from native119_client_logs import audit_client as audit from native_runtime import project_assets,prepare_client,client_command,fingerprint from release_identity import MAIN_JAR from runtime_diagnostics import run_monitored,validate_logging_inputs diff --git a/tools/verify_upgrade_abort.py b/tools/verify_upgrade_abort.py index c8a4a15..1593da2 100644 --- a/tools/verify_upgrade_abort.py +++ b/tools/verify_upgrade_abort.py @@ -100,6 +100,33 @@ def allowed_native40_platform_additions(before,after,world,original_config=None) except (ValueError,KeyError,UnicodeError):return [] + +def allowed_native45_platform_additions(before,after,world,original_config=None): + """Exact Forge45 corrections; no unknown keys or supported choices dropped.""" + name='serverconfig/forge-server.toml' + if name not in after:return [] + actual=(world/name).read_bytes() + if digest(world/name)!=after[name]:return [] + try: + defaults=tomllib.loads((Path(__file__).parent/'fixtures/forge45-server-default.toml').read_text()) + upgraded=tomllib.loads(actual.decode('utf-8')) + if name not in before:return [name] if same_typed_values(defaults,upgraded) else [] + if original_config is None or before[name]!=__import__('hashlib').sha256(original_config).hexdigest().upper():return [] + previous=tomllib.loads(original_config.decode('utf-8'));server=previous['server'] + retired={'removeErroringTileEntities':bool,'logCascadingWorldGeneration':bool, + 'fixVanillaCascading':bool,'dimensionUnloadQueueDelay':int,'clumpingThreshold':int, + 'fixAdvancementLoading':bool,'treatEmptyTagsAsAir':bool,'skipEmptyShapelessCheck':bool} + for key,kind in retired.items(): + if key not in server:continue + value=server.pop(key) + if type(value) is not kind:return [] + if key=='dimensionUnloadQueueDelay' and value<0:return [] + if key=='clumpingThreshold' and not 64<=value<=1024:return [] + for key,value in defaults['server'].items():server.setdefault(key,value) + return [name] if same_typed_values(previous,upgraded) else [] + except (ValueError,KeyError,UnicodeError):return [] + + def inject_unsafe_journal(world, case): if case not in ('damaged-index', 'blocked-journal'): raise ValueError('Unknown abort fixture: ' + case) @@ -123,7 +150,7 @@ def inject_unsafe_journal(world, case): def inject_future_entity_version(world): - """One copied genuine public17 chunk, valid future Int; never touch source.""" + """One copied genuine public18 chunk, valid future Int; never touch source.""" from nbt_regions import Tag,read_region,write_disposable_region import struct path=world/'entities/r.0.0.mca' @@ -135,17 +162,17 @@ def inject_future_entity_version(world): if entries is None or entries.kind!=9 or entries.value[0] not in (0,10):raise ValueError('Malformed genuine entity list') bodies=[e for e in entries.value[1] if e.value.get('id',Tag(8,'')).value=='lootablebodies:corpse'] if bodies:candidates.append((index,root,bodies)) - if len(candidates)!=1:raise ValueError('Expected one genuine near public17 corpse chunk') + if len(candidates)!=1:raise ValueError('Expected one genuine near public18 corpse chunk') index,root,bodies=candidates[0] - if root.get('DataVersion')!=Tag(3,2730) or any(e.value.get('LootableBodiesDataVersion')!=Tag(3,7) for e in bodies): - raise ValueError('Future-version control requires genuine native17 writer data') + if root.get('DataVersion')!=Tag(3,2975) or any(e.value.get('LootableBodiesDataVersion')!=Tag(3,8) for e in bodies): + raise ValueError('Future-version control requires genuine native18 writer data') 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,2976);write_disposable_region(path,chunks) - return {'region':path.relative_to(world).as_posix(),'chunk_index':index,'source_version':2730, - 'injected_version':2976,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), - 'original_copy':str(original),'corpse_marker':7} + root['DataVersion']=Tag(3,3338);write_disposable_region(path,chunks) + return {'region':path.relative_to(world).as_posix(),'chunk_index':index,'source_version':2975, + 'injected_version':3338,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), + 'original_copy':str(original),'corpse_marker':8} def run_abort(java, home): @@ -187,7 +214,7 @@ def main(): 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) - text = log.read_text(encoding='utf-8', errors='replace') + 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)) if mutation and 'Saved entity chunk comes from a newer Minecraft version' not in text: @@ -197,7 +224,7 @@ def main(): raise RuntimeError('World loading continued after unsafe preservation') after = source_files(world) raw_after = dict(after) - platform_additions = allowed_native40_platform_additions(before, after, world, original_config) + platform_additions = allowed_native45_platform_additions(before, after, world, original_config) lock_updates=allowed_native_session_lock(before,after,world,original_lock) for name in platform_additions+lock_updates: if name in before: after[name] = before[name] diff --git a/tools/verify_visibility.py b/tools/verify_visibility.py index fcccffa..2c17837 100644 --- a/tools/verify_visibility.py +++ b/tools/verify_visibility.py @@ -9,7 +9,8 @@ import os from native_runtime import seed_flat_fixture from owned_process import native_process -from audit_runtime_logs import audit +from native119_logs import audit +from native119_client_logs import audit_client from fixture_login import LoginMonitor, start_clients, write_receipts, archive_live_logs from verify_multiplayer import client_command from verify_packaged import prepare, FORGE, MAIN, digest, run @@ -111,7 +112,8 @@ def start(name): write_receipts(home) for name in ['server','clientA','clientB']: if digest(home/name/'mods'/MAIN)!=before: raise RuntimeError('Candidate copy changed') - audit(home/(name+'.log'),native=True);audit(home/name/'logs/latest.log',native=True);audit(home/name/'logs/debug.log',native=True) + selected_audit=audit if name=='server' else audit_client + selected_audit(home/(name+'.log'),native=True);selected_audit(home/name/'logs/latest.log',native=True);selected_audit(home/name/'logs/debug.log',native=True) if list((home/name).rglob('crash-reports/*.txt')): raise RuntimeError('Crash report') for role in ['A','B']: if not (home/('client'+role)/(args.mode+'-client.pass')).is_file(): raise RuntimeError('Client did not finish '+role) diff --git a/tools/verify_world_upgrades.py b/tools/verify_world_upgrades.py index 1ed11df..1e1e818 100644 --- a/tools/verify_world_upgrades.py +++ b/tools/verify_world_upgrades.py @@ -66,6 +66,18 @@ 'via117-115': ORIGINS['released-115'], 'via117-116': ORIGINS['released-116'], }) +ORIGINS.update({ + 'released-118': ('1.18.2','2.4.9.118021','34EF41EFCE4666A684EA202DAF8712162A1C403A90120BA11EF0260533FB690F'), + 'recovery-118': ('1.18.2','2.4.9.118021','34EF41EFCE4666A684EA202DAF8712162A1C403A90120BA11EF0260533FB690F'), + 'via118-110': ORIGINS['original-110'], + 'via118-111': ORIGINS['released-111'], + 'via118-112': ORIGINS['released-112'], + 'via118-113': ORIGINS['released-113'], + 'via118-114': ORIGINS['released-114'], + 'via118-115': ORIGINS['released-115'], + 'via118-116': ORIGINS['released-116'], + 'via118-117': ORIGINS['released-117'], +}) FORGE = {'1.12.2': 'forge-1.12.2-14.23.5.2859','1.10.2': 'forge-1.10.2-12.18.3.2511', '1.11.2': 'forge-1.11.2-13.20.1.2588'} FORGE_SHA = {'1.12.2': 'BD8314729D787E35164476D75AFB337AFB9783917E4F6FA1F77F049ADBA22F2D','1.10.2': '809C2EA5B4D4F3E5A294D3A326744F20FEC412A1263E708297D00F512E377542', '1.11.2': 'B6DFF35B5CCB0E6353FB89B5330FE930DB8FBE8B63787C9FC2F70196BD108998'} @@ -562,7 +574,7 @@ def verify(args, project): raise ValueError('--prepared-sources or --seeds is required for the upgrade matrix') if prepared and args.seeds:raise ValueError('Choose one unambiguous source route') if not args.forge_home: - raise ValueError('--forge-home is required for the native18 upgrade matrix') + raise ValueError('--forge-home is required for the native19 upgrade matrix') root = args.output.resolve() root.mkdir(parents=True, exist_ok=False) candidate = project / 'build/libs' / MAIN_JAR @@ -614,7 +626,7 @@ def run_origin(origin): for phase in PHASES: run_upgrade(args.java.resolve(), home, phase) wave = 0 if phase.startswith('upgrade-near') else 1 if phase.startswith('upgrade-far') else 2 if phase == 'upgrade-late' else -1 - from native118_persistence import count_persisted + from native119_persistence import count_persisted persisted = count_persisted(world,wave) if persisted < (3 if wave >= 0 else 8): raise RuntimeError('Missing persisted corpses after world save')