diff --git a/.gitattributes b/.gitattributes index 998e11e..053b3ce 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,6 @@ gradlew text eol=lf *.bat text eol=crlf *.jar binary *.png binary + +# Preserve the exact native Forge default configuration fixture, including EOF. +tools/fixtures/forge37-server-default.toml whitespace=-blank-at-eof diff --git a/.github/release117.json b/.github/release117.json new file mode 100644 index 0000000..d333c17 --- /dev/null +++ b/.github/release117.json @@ -0,0 +1,13 @@ +{ + "version": "2.4.8.117011", + "minecraft": "1.17.1", + "forge": "37.1.1", + "files": { + "CHANGELOG.txt": "A90E21C1F4647ABF284521BE7F51E1DC0E4D6D9E50A092C8E42C9E60D9B3D1AA", + "LootableBodies-2.4.8.117011-javadoc.jar": "20CC1CCB2680B1753AC1BE7D8F956E50F52C22A44A78E3C984F3A4E126EFA946", + "LootableBodies-2.4.8.117011-sources.jar": "DA3C60A228AFF8F484E6BA77A1054F108E0A71543F0AF5478FC0A0C05BF004FF", + "LootableBodies-2.4.8.117011.jar": "854A46D80B8599966E43B3EAED407CD1A99FE4FB019AEE9138FE2F98E8DDA9FA", + "LootableBodies-2.4.8.117011.pom": "A7EBC042C6AD954AA2E52A8BFA11D2883BFB6E77B43A7420FB035DBA1FB8A6B9", + "SHA256SUMS": "7B53FC4734A5854B2696B6536A9D2463C0CD7FCFDC213F3B67C5EA6ECAF1ABEB" + } +} diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 524880c..4873c2f 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -1,5 +1,6 @@ """Test the dispatcher's actual selection and publication preflight scripts offline.""" import json +import hashlib import os from pathlib import Path import shutil @@ -24,6 +25,8 @@ ['released-113', 'recovery-113', 'via113-110', 'via113-111', 'via113-112'] for stamps in ['normal', 'stamps']] MATRIX115 = ['World upgrade matrix', 'Prepare verified migration inputs', 'Validate release workflow'] + ['Historical world shard ' + str(i) for i in range(6)] +CORE117 = ['Build, test, and audit', 'Analyze Java', 'Validation'] +MATRIX117 = ['Prepare verified migration inputs', 'Validate release workflow'] + ['Historical world shard ' + str(i) for i in range(12)] + ['Native117 ' + x for x in ['servers','default','config','skin','issues','names','rejected','multiplayer','visibility','respawn','control']] MATRIX116 = ['World upgrade matrix', 'Prepare verified migration inputs', 'Validate release workflow'] + ['Historical world shard ' + str(i) for i in range(12)] MATRIX = [origin + ' / ' + stamps for origin in ['original-110', 'released-110', 'original-111', 'recovery-110'] @@ -53,7 +56,7 @@ def select(self, version, repository=REPOSITORY, ref='refs/heads/master-1.12'): values = dict(line.split('=', 1) for line in output.read_text().splitlines()) if output.exists() else {} return result.returncode, values - def validate(self, minecraft, names=None, mutation=None, release_exists=False, wrong_tag=False, audit_success=True): + def validate(self, minecraft, names=None, mutation=None, release_exists=False, wrong_tag=False, audit_success=True, qualification=None): suffix, branch, forge = { '1.10.2': ('110021', '1.10.2', '12.18.3.2511'), '1.11.2': ('111021', '1.11.2-dev', '13.20.1.2588'), @@ -61,10 +64,11 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.13.2': ('113021', 'master-1.13.2', '25.0.223'), '1.14.4': ('114041', 'master-1.14.4', '28.2.26'), '1.15.2': ('115021', 'master-1.15.2', '31.2.57'), - '1.16.5': ('116051', 'master-1.16.5', '36.2.42')}[minecraft] + '1.16.5': ('116051', 'master-1.16.5', '36.2.42'), + '1.17.1': ('117011', 'master-1.17.1', '37.1.1')}[minecraft] version = '2.4.2.' + suffix values = dict(minecraft_version=minecraft, forge_version=forge, loader_name='forge', loader_code='1', - java_version='8', java_toolchain_version='8.0.502+7', gradle_java_version='17', mod_id='lootablebodies', + java_version='16' if minecraft=='1.17.1' else '8', java_toolchain_version='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: @@ -73,9 +77,15 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.13.2': MATRIX113 + ['Validate release workflow'], '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 + def command(args, **options): if args[0] == 'git': return SHA + '\n' + if '/actions/workflows/native117.yml/' in args[-1]: + row=dict(id=117,head_sha=SHA,head_branch='master-1.17.1',event='push',path='.github/workflows/native117.yml',conclusion='success') + if qualification:row.update(qualification) + return json.dumps({'workflow_runs':[row]}) if '/actions/workflows/' in args[-1]: return json.dumps({'workflow_runs': [{'id': 1, 'display_title': 'Audit ' + minecraft + ' artifacts from ' + SHA, 'conclusion': 'success' if audit_success else 'failure'}]}) @@ -95,8 +105,8 @@ def existing(args, **options): patch('subprocess.check_output', side_effect=command), patch('subprocess.run', side_effect=existing): exec(compile(code, 'release-preflight', 'exec'), {}) - def test_future_117_candidate_cannot_dispatch_publication(self): - code, values = self.select('2.4.8.117011') + def test_future_118_candidate_cannot_dispatch_publication(self): + code, values = self.select('2.4.9.118021') self.assertNotEqual(0, code) self.assertEqual({}, values) @@ -217,6 +227,44 @@ def test_116_audit_handles_both_registered_workflow_names(self): self.assertIn('LootableBodies 1.12 CI', [name.strip() for name in names]) self.assertIn('LootableBodies 1.16 CI', [name.strip() for name in names]) + def test_117_routes_to_the_accepted_target(self): + code,values=self.select('2.4.8.117011') + self.assertEqual(0,code);self.assertEqual('master-1.17.1',values['source_branch']) + + def test_117_requires_all_native_scenarios_and_twelve_historical_shards(self): + self.validate('1.17.1') + for missing in CORE117+MATRIX117: + with self.subTest(missing=missing),self.assertRaises(SystemExit): + self.validate('1.17.1',names=[x for x in CORE117+MATRIX117 if x!=missing]) + + def test_117_requires_an_exact_successful_merged_native_run(self): + for mutation in [{'head_sha':'b'*40},{'head_branch':'master-1.16.5'},{'event':'pull_request'},{'path':'.github/workflows/build.yml'},{'conclusion':'failure'}]: + with self.subTest(mutation=mutation),self.assertRaises(SystemExit): + self.validate('1.17.1',qualification=mutation) + + def test_117_requires_the_independent_audit_and_java16(self): + with self.assertRaises(SystemExit):self.validate('1.17.1',audit_success=False) + with self.assertRaises(SystemExit):self.validate('1.17.1',mutation={'java_version':'8'}) + + def test_117_promotes_exact_six_files_and_rejects_changed_or_extra_bytes(self): + code=script('Verify all six native117 bytes before publication').split("python3 - <<'PY'\n",1)[1].split('\nPY',1)[0] + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);bundle=root/'build/release/bundle';bundle.mkdir(parents=True);meta=root/'.github';meta.mkdir() + names=['main.jar','sources.jar','javadoc.jar','release.pom','CHANGELOG.txt','SHA256SUMS'];expected={} + for name in names: + data=('approved '+name).encode();(bundle/name).write_bytes(data);expected[name]=hashlib.sha256(data).hexdigest().upper() + (meta/'release117.json').write_text(json.dumps({'files':expected})) + previous=Path.cwd() + try: + os.chdir(root);exec(compile(code,'immutable-native117-promotion','exec'),{}) + for name in names: + f=bundle/name;data=f.read_bytes();f.write_bytes(data+b'changed') + with self.assertRaises(SystemExit):exec(compile(code,'changed-native117-promotion','exec'),{}) + f.write_bytes(data) + (bundle/'extra.txt').write_text('unapproved') + with self.assertRaises(SystemExit):exec(compile(code,'extra-native117-promotion','exec'),{}) + finally:os.chdir(previous) + def test_identity_changes_are_rejected(self): for mutation in [{'mod_id': 'other'}, {'minecraft_version': '1.10.2'}, {'mod_license': 'CC0-1.0'}]: with self.subTest(mutation=mutation), self.assertRaises(SystemExit): diff --git a/.github/workflows/artifact-audit-1.17.yml b/.github/workflows/artifact-audit-1.17.yml new file mode 100644 index 0000000..debec8e --- /dev/null +++ b/.github/workflows/artifact-audit-1.17.yml @@ -0,0 +1,68 @@ +name: Audit LootableBodies 1.17 artifacts +run-name: Audit 1.17.1 artifacts from ${{ github.event.workflow_run.head_sha }} +on: + workflow_run: + workflows: [LootableBodies 1.17 CI] + types: [completed] +permissions: + contents: read + actions: read +jobs: + audit: + if: >- + github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && + github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'master-1.17.1' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.event.workflow_run.head_sha }} + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '16.0.2+7' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.17.1-${{ github.event.workflow_run.head_sha }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + path: audited + - name: Independently verify every accepted byte + env: + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + python3 - <<'PY' + import hashlib,json,os + from pathlib import Path + expected=json.loads(Path('.github/release117.json').read_text())['files'] + actual={p.name:hashlib.sha256(p.read_bytes()).hexdigest().upper() for p in Path('audited').iterdir() if p.is_file()} + if actual != expected:raise SystemExit('Native117 audited bytes differ from the accepted six files') + Path('independent-audit.json').write_text(json.dumps({'source':os.environ['SOURCE_SHA'],'files':actual,'all_six_exact':True},indent=2)+'\n') + PY + - name: Audit archive identity, class60, licences and mappings without rebuilding + env: + LOOTABLEBODIES_JAVA16_HOME: ${{ env.JAVA_HOME_16_X64 }} + LOOTABLEBODIES_MAVENIZER_JAVA_HOME: ${{ env.JAVA_HOME_25_X64 }} + run: | + chmod +x ./gradlew + ./gradlew verifyPreparedReleaseArtifacts -PpreparedReleaseDir=audited --no-daemon --console=plain --stacktrace | tee archive-audit.log + if grep -E '^> Task :(compileJava|jar|sourcesJar|javadocJar)( |$)' archive-audit.log; then exit 1; fi + - name: Retain independent audit evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-independent-audit-${{ github.event.workflow_run.head_sha }} + if-no-files-found: error + path: | + independent-audit.json + archive-audit.log diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fb932af..e1a3001 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-1.16.5 + - master-1.17.1 - 'feature/**' pull_request: branches: - - master-1.16.5 + - master-1.17.1 schedule: - cron: '43 7 * * 4' @@ -16,6 +16,10 @@ permissions: contents: read security-events: write +concurrency: + group: lootablebodies-1.17-codeql-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze Java @@ -32,11 +36,11 @@ jobs: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install pinned Java 8 compilation toolchain + - name: Install pinned Java 16 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: '16.0.2+7' - name: Install pinned Java 17 Gradle runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 @@ -46,7 +50,7 @@ jobs: - name: Pin production JDK run: | - echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle @@ -64,7 +68,7 @@ jobs: gradle_args=( clean classes --no-build-cache --rerun-tasks --no-daemon --stacktrace --max-workers=2 - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_16_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false -Dorg.gradle.java.installations.auto-download=false ) diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 56db9a5..746e8de 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 or 116051. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051 or 117011. required: true type: string curseforge_release_level: @@ -37,6 +37,7 @@ jobs: outputs: release_sha: ${{ steps.validate.outputs.release_sha }} release_version: ${{ steps.validate.outputs.release_version }} + qualification_run: ${{ steps.validate.outputs.qualification_run }} minecraft_version: ${{ steps.target.outputs.minecraft_version }} source_branch: ${{ steps.target.outputs.source_branch }} steps: @@ -48,7 +49,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)$ ]] + [[ "$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)$ ]] 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 ;; @@ -57,6 +58,7 @@ jobs: *.114041) minecraft_version=1.14.4; source_branch=master-1.14.4 ;; *.115021) minecraft_version=1.15.2; source_branch=master-1.15.2 ;; *.116051) minecraft_version=1.16.5; source_branch=master-1.16.5 ;; + *.117011) minecraft_version=1.17.1; source_branch=master-1.17.1 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -87,13 +89,15 @@ jobs: '1.13.2': ('master-1.13.2', '25.0.223', '113021'), '1.14.4': ('master-1.14.4', '28.2.26', '114041'), '1.15.2': ('master-1.15.2', '31.2.57', '115021'), - '1.16.5': ('master-1.16.5', '36.2.42', '116051')} + '1.16.5': ('master-1.16.5', '36.2.42', '116051'), + '1.17.1': ('master-1.17.1', '37.1.1', '117011')} 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' expected = {'minecraft_version': minecraft, 'forge_version': forge, - 'loader_name': 'forge', 'loader_code': '1', 'java_version': '8', - 'java_toolchain_version': '8.0.502+7', 'gradle_java_version': '17', + 'loader_name': 'forge', 'loader_code': '1', 'java_version': '16' if native17 else '8', + 'java_toolchain_version': '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'} @@ -120,11 +124,26 @@ jobs: if minecraft in ('1.15.2', '1.16.5'): required.update(['Validate release workflow', 'Prepare verified migration inputs', 'World upgrade matrix'] + ['Historical world shard ' + str(i) for i in range(6 if minecraft == '1.15.2' else 12)]) + qualification_run = '' + if native17: + required = {'Prepare verified migration inputs', 'Build, test, and audit', + 'Analyze Java', 'Validation', 'Validate release workflow'} + required.update('Historical world shard ' + str(i) for i in range(12)) + required.update('Native117 ' + group for group in + ['servers', 'default', 'config', 'skin', 'issues', 'names', + 'rejected', 'multiplayer', 'visibility', 'respawn', 'control']) + runs = json.loads(subprocess.check_output(['gh', 'api', + f'repos/{repo}/actions/workflows/native117.yml/runs?event=push&branch=master-1.17.1&head_sha={sha}&per_page=100'], text=True))['workflow_runs'] + exact = [r for r in runs if r['head_sha'] == sha and r['head_branch'] == 'master-1.17.1' + and r['event'] == 'push' and r['path'] == '.github/workflows/native117.yml'] + if not exact or max(exact, key=lambda r:r['id'])['conclusion'] != 'success': + raise SystemExit('No successful exact-merge native117 qualification') + qualification_run = str(max(exact, key=lambda r:r['id'])['id']) for name in required: 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'): + if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1'): target = minecraft.rsplit('.', 1)[0] audits = json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/actions/workflows/artifact-audit-{target}.yml/runs?event=workflow_run&per_page=100'], text=True))['workflow_runs'] @@ -142,7 +161,7 @@ jobs: if tag.returncode != 0 and '404' not in tag.stderr: raise SystemExit('Could not verify the release tag') with open(os.environ['GITHUB_OUTPUT'], 'a') as out: - out.write(f'release_sha={sha}\nrelease_version={version}\n') + out.write(f'release_sha={sha}\nrelease_version={version}\nqualification_run={qualification_run}\n') PY build: @@ -166,7 +185,7 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: ${{ 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: @@ -174,16 +193,41 @@ jobs: java-version: '17.0.1+12' - name: Pin the production toolchain run: | - echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then + echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" + else + echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + fi echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + - name: Download the qualified native117 six-file bundle + if: needs.preflight.outputs.minecraft_version == '1.17.1' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.17.1-${{ needs.preflight.outputs.release_sha }} + run-id: ${{ needs.preflight.outputs.qualification_run }} + github-token: ${{ github.token }} + path: build/release/bundle + - name: Verify all six native117 bytes before publication + if: needs.preflight.outputs.minecraft_version == '1.17.1' + run: | + python3 - <<'PY' + import hashlib,json + from pathlib import Path + expected=json.loads(Path('.github/release117.json').read_text())['files'] + actual={p.name:hashlib.sha256(p.read_bytes()).hexdigest().upper() for p in Path('build/release/bundle').iterdir() if p.is_file()} + if actual != expected:raise SystemExit('Qualified native117 bundle differs from the accepted six files') + PY + chmod +x ./gradlew + ./gradlew verifyPreparedReleaseArtifacts -PpreparedReleaseDir=build/release/bundle --no-daemon --stacktrace - name: Build, test and audit the release + if: needs.preflight.outputs.minecraft_version != '1.17.1' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ --no-daemon --stacktrace \ - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" \ + -Dorg.gradle.java.installations.paths="$JAVA_HOME,${LOOTABLEBODIES_JAVA16_HOME:-${LOOTABLEBODIES_JAVA8_HOME:-}},$JAVA_HOME_25_X64" \ -Dorg.gradle.java.installations.auto-detect=false \ -Dorg.gradle.java.installations.auto-download=false - name: Stage the exact audited bundle @@ -283,7 +327,7 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: '8.0.502+7' + java-version: ${{ 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: @@ -291,7 +335,11 @@ jobs: java-version: '17.0.1+12' - name: Pin the production toolchain run: | - echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + if [ '${{ needs.preflight.outputs.minecraft_version }}' = '1.17.1' ]; then + echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" + else + echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + fi echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -313,7 +361,7 @@ jobs: chmod +x ./gradlew ./gradlew publishMavenJavaPublicationToReleaseRepository \ -PpreparedReleaseDir="$RUNNER_TEMP/release-input" --no-daemon --stacktrace \ - -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" \ + -Dorg.gradle.java.installations.paths="$JAVA_HOME,${LOOTABLEBODIES_JAVA16_HOME:-${LOOTABLEBODIES_JAVA8_HOME:-}},$JAVA_HOME_25_X64" \ -Dorg.gradle.java.installations.auto-detect=false \ -Dorg.gradle.java.installations.auto-download=false @@ -343,7 +391,7 @@ jobs: game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - java: Java 8 + java: ${{ 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/native117.yml b/.github/workflows/native117.yml new file mode 100644 index 0000000..b496c6a --- /dev/null +++ b/.github/workflows/native117.yml @@ -0,0 +1,316 @@ +name: LootableBodies 1.17 CI + +on: + push: + branches: [master-1.17.1] + pull_request: + branches: [master-1.17.1] + +permissions: + contents: read + actions: read + +concurrency: + group: native117-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHONDONTWRITEBYTECODE: '1' + +jobs: + quick: + name: Fast build and contracts + # Full Linux qualification runs once on the merged MMD source, before + # publication. PRs retain deterministic build, test and archive checks. + if: github.repository != 'MinecraftModDevelopmentMods/LootableBodies' || github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '16.0.2+7' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + - name: Test tools and audit the native117 archives + env: + LOOTABLEBODIES_JAVA16_HOME: ${{ env.JAVA_HOME_16_X64 }} + LOOTABLEBODIES_MAVENIZER_JAVA_HOME: ${{ env.JAVA_HOME_25_X64 }} + run: | + chmod +x gradlew + python3 -B -m unittest discover -s tools -p 'test_*.py' -v 2>&1 | tee "$RUNNER_TEMP/native117-python-tools.log" + ./gradlew test assemble javadoc verifyProductionRuntimeClasspath prepareReleaseBundle --no-daemon --no-build-cache --stacktrace + - name: Retain fast-check evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-fast-${{ github.sha }} + if-no-files-found: error + path: | + build/test-results/** + build/reports/** + build/release/bundle/* + ${{ runner.temp }}/native117-python-tools.log + + prepare: + name: Prepare verified migration inputs + if: github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + fetch-depth: 0 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '16.0.2+7' + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 + with: + cache-disabled: true + - name: Pin separate runtimes and test tools + run: | + chmod +x gradlew + echo "GRADLE_USER_HOME=$RUNNER_TEMP/lootablebodies117-gradle" >> "$GITHUB_ENV" + echo "LOOTABLEBODIES_JAVA16_HOME=$JAVA_HOME_16_X64" >> "$GITHUB_ENV" + echo "LOOTABLEBODIES_JAVA8_HOME=$JAVA_HOME_8_X64" >> "$GITHUB_ENV" + echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" + python3 -B -m unittest discover -s tools -p 'test_*.py' -v 2>&1 | tee "$RUNNER_TEMP/native117-python-tools.log" + - name: Two clean uncached builds with offline replay + run: python3 -B tools/native117_ci.py build + - name: Install graphical runtime support + run: | + sudo apt-get update -o Acquire::Retries=2 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 + sudo apt-get install --yes --no-install-recommends xvfb x11-xserver-utils libgl1-mesa-dri libglu1-mesa libopenal1 pulseaudio pulseaudio-utils + pulseaudio --start --exit-idle-time=-1 + pactl load-module module-null-sink sink_name=minecraft + echo "PULSE_SINK=minecraft" >> "$GITHUB_ENV" + echo "ALSOFT_DRIVERS=pulse" >> "$GITHUB_ENV" + echo "LIBGL_ALWAYS_SOFTWARE=true" >> "$GITHUB_ENV" + - name: Download the original sealed thirty-world bundle + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-1.16.5-migration-inputs-9edcc246bc156d93dabd51970b5a832d7d53a511 + run-id: 40909860288 + github-token: ${{ github.token }} + path: build/prior116-download + - name: Qualify mapped launches and prepare immutable native inputs once + run: xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 -B tools/native117_ci.py prepare + - name: Upload the exact six qualified files + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: LootableBodies-1.17.1-${{ github.sha }} + if-no-files-found: error + path: build/release/bundle/* + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-history-inputs-${{ github.sha }} + if-no-files-found: error + path: build/migration-ci-inputs/* + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-platform-inputs-${{ github.sha }} + if-no-files-found: error + path: build/ci117-platform-inputs/* + - name: Retain preparation and mapped-runtime evidence on every outcome + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-preparation-evidence-${{ github.sha }}-${{ github.run_attempt }} + if-no-files-found: error + include-hidden-files: true + path: | + build/ci117-commands/** + build/ci117-build-proof/** + build/test-results/** + build/reports/** + build/integration-run/** + build/upgrade-contract-run/** + build/client-integration-run/** + build/ordinary-development/** + build/native37-resource-budget/**/*.log + build/native37-resource-budget/**/*.json + build/native37-resource-budget/**/*.diagnostics/** + build/native37-resource-budget/**/mods/*.jar + ${{ runner.temp }}/lootablebodies117-first-build/** + ${{ runner.temp }}/native117-python-tools.log + build/migration-seeds/**/*.log + build/migration-seeds/**/*.diagnostics/** + build/migration-seeds/**/crash-reports/** + build/migration-seeds/**/world/** + build/migration-seeds/**/mods/*.jar + build/migration-seeds/**/config/** + build/migration-seeds/**/*.properties + build/migration-seeds/*report.json + build/migration-seeds/provenance.json + build/native-server-distribution/**/*.log + build/native-server-distribution/runtime-inputs.json + build/native-client-distribution/**/*.log + build/native-client-distribution/runtime-inputs.json + .gradle/legacy-tools/released-116/build*.log + .gradle/legacy-tools/released-116/source.json + .gradle/legacy-tools/released-116/tools.json + - name: Preserve incomplete SDK inputs after preparation failure + if: failure() && hashFiles('build/native-server-distribution/**', 'build/native-client-distribution/**', 'build/ci117-platform-inputs/**', 'build/migration-ci-inputs/**') != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-preparation-failure-inputs-${{ github.sha }}-${{ github.run_attempt }} + if-no-files-found: error + include-hidden-files: true + path: | + build/native-server-distribution/** + build/native-client-distribution/** + build/ci117-platform-inputs/** + build/migration-ci-inputs/** + + upgrade: + name: Historical world shard ${{ matrix.shard }} + needs: prepare + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '16.0.2+7' + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: native117-history-inputs-${{ github.sha }} + path: build/migration-ci-inputs + - name: Restore and qualify assigned original worlds + run: | + python3 -B tools/migration_ci.py restore --commit "$GITHUB_SHA" + python3 -B tools/migration_ci.py shard --commit "$GITHUB_SHA" --shards 12 --shard ${{ matrix.shard }} --java "$JAVA_HOME/bin/java" + - name: Retain full successful and failed world evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-upgrade-shard-${{ matrix.shard }}-${{ github.sha }} + if-no-files-found: error + include-hidden-files: true + path: | + build/migration-ci/report.json + build/migration-ci-inputs/inputs.json + build/migration-results/**/*.log + build/migration-results/**/*.log.gz + build/migration-results/**/*.json + build/migration-results/**/*.properties + build/migration-results/**/*.diagnostics/** + build/migration-results/**/world/** + build/migration-results/**/mods/*.jar + build/migration-results/**/config/** + build/migration-results/**/crash-reports/** + build/migration-abort/**/*.log + build/migration-abort/**/*.log.gz + build/migration-abort/**/*.json + build/migration-abort/**/*.properties + build/migration-abort/**/*.diagnostics/** + build/migration-abort/**/world/** + build/migration-abort/**/mods/*.jar + build/migration-abort/**/config/** + build/migration-abort/**/original-journal/** + build/migration-abort/**/crash-reports/** + + scenarios: + name: Native117 ${{ matrix.group }} + needs: prepare + runs-on: ubuntu-latest + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + group: [servers, default, config, skin, issues, names, rejected, multiplayer, visibility, respawn, control] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '16.0.2+7' + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: native117-platform-inputs-${{ github.sha }} + path: build/ci117-platform-inputs + - name: Restore exact qualified runtime inputs + run: python3 -B tools/migration_ci.py restore --commit "$GITHUB_SHA" --folder build/ci117-platform-inputs + - name: Install graphical runtime support + run: | + sudo apt-get update -o Acquire::Retries=2 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 + sudo apt-get install --yes --no-install-recommends xvfb x11-xserver-utils libgl1-mesa-dri libglu1-mesa libopenal1 pulseaudio pulseaudio-utils + pulseaudio --start --exit-idle-time=-1 + pactl load-module module-null-sink sink_name=minecraft + echo "PULSE_SINK=minecraft" >> "$GITHUB_ENV" + echo "ALSOFT_DRIVERS=pulse" >> "$GITHUB_ENV" + echo "LIBGL_ALWAYS_SOFTWARE=true" >> "$GITHUB_ENV" + - name: Run the assigned native scenario once + run: xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 -B tools/native117_ci.py scenario --group ${{ matrix.group }} + - name: Retain complete successful and failed scenario evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-scenario-${{ matrix.group }}-${{ github.sha }} + if-no-files-found: error + include-hidden-files: true + path: | + build/ci117-commands/** + build/ci117-scenarios/**/*.log + build/ci117-scenarios/**/*.log.gz + build/ci117-scenarios/**/*.json + build/ci117-scenarios/**/*.txt + build/ci117-scenarios/**/*.diagnostics/** + build/ci117-scenarios/**/world/** + build/ci117-scenarios/**/saves/** + build/ci117-scenarios/**/mods/*.jar + build/ci117-scenarios/**/config/** + build/ci117-scenarios/**/crash-reports/** + build/ci117-scenarios/**/screenshots/*.png + build/ci117-platform-inputs/inputs.json + + aggregate: + name: Build, test, and audit + needs: [prepare, upgrade, scenarios] + if: always() && github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Every required execution must succeed + env: + PREPARE: ${{ needs.prepare.result }} + UPGRADE: ${{ needs.upgrade.result }} + SCENARIOS: ${{ needs.scenarios.result }} + run: test "$PREPARE" = success && test "$UPGRADE" = success && test "$SCENARIOS" = success + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: native117-upgrade-shard-*-${{ github.sha }} + path: build/migration-shards + - name: Independently audit all 76 cases and 912 native receipts + run: python3 -B tools/migration_ci.py aggregate --commit "$GITHUB_SHA" --shards 12 --folder build/migration-shards + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native117-world-upgrades-${{ github.sha }} + if-no-files-found: error + path: build/migration-summary.json diff --git a/.github/workflows/release-workflow-checks.yml b/.github/workflows/release-workflow-checks.yml index 6b3d3a3..00778d8 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.16.5'] + branches: ['feature/**', 'master-1.17.1'] pull_request: - branches: ['master-1.16.5'] + branches: ['master-1.17.1'] permissions: contents: read jobs: @@ -24,7 +24,7 @@ jobs: - name: Check release workflow syntax 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/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 - 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 10be37e..5dba50a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,9 +1,6 @@ -2.4.7.116051 - Minecraft 1.16.5, Forge +2.4.8.117011 - Minecraft 1.17.1, Forge -Updated for Minecraft 1.16.5. +Updated for Minecraft 1.17.1. -Includes the inventory, corpse and config improvements from 1.15.2. -Preserves bodies and their contents when upgrading older worlds, -including bodies in chunks visited later. - -Back up the whole world, including its data folder, before upgrading. +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 4b4a7f4..7240e20 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lootable Bodies for Minecraft 1.16.5 +# Lootable Bodies for Minecraft 1.17.1 Your items stay on a corpse when you die. Right-click the body to recover them. -Use Forge 36.2.42 and Java 8. Install `LootableBodies-2.4.7.116051.jar` +Use Forge 37.1.1 and Java 16. Install `LootableBodies-2.4.8.117011.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 and 1.15.2 corpses are preserved when upgrading, +Existing 1.10.2, 1.11.2, 1.12.2, 1.13.2, 1.14.4, 1.15.2 and 1.16.5 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 7624cd1..1448b54 100644 --- a/build.gradle +++ b/build.gradle @@ -14,18 +14,18 @@ group = project.mod_group version = project.mod_version base.archivesName = project.mod_artifact -// Gradle uses Java 17; every compiler, test and game process uses this JDK 8. +// Gradle uses Java 17; every compiler, test and game process uses this JDK 16. // Eclipse does not inherit the command-line helper's environment. Keep its // machine-specific toolchain selection in an ignored local properties file. Properties localJavaSetup = new Properties() File localJavaSetupFile = file('gradle-local.properties') if (localJavaSetupFile.isFile()) localJavaSetupFile.withInputStream { localJavaSetup.load(it) } -def pinnedJava8 = providers.environmentVariable('LOOTABLEBODIES_JAVA8_HOME') - .orElse(providers.gradleProperty('lootablebodies.java8.home')) - .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.java8.home') }) -def exactJava8Spec = pinnedJava8.isPresent() ? +def pinnedJava16 = providers.environmentVariable('LOOTABLEBODIES_JAVA16_HOME') + .orElse(providers.gradleProperty('lootablebodies.java16.home')) + .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.java16.home') }) +def exactJava16Spec = pinnedJava16.isPresent() ? org.gradle.jvm.toolchain.internal.SpecificInstallationToolchainSpec.fromJavaHome( - project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava8.get())) : null + project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava16.get())) : null def pinnedJava25 = providers.environmentVariable('LOOTABLEBODIES_MAVENIZER_JAVA_HOME') .orElse(providers.gradleProperty('lootablebodies.mavenizer.java.home')) .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.mavenizer.java.home') }) @@ -37,18 +37,18 @@ ext.mavenizerLauncher = exactJava25Spec != null ? javaToolchains.launcherFor(exa fgtools.configure('mavenizer') { javaLauncher.set(mavenizerLauncher) } java { toolchain { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(16) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() withJavadocJar() } -ext.productionLauncher = exactJava8Spec != null ? javaToolchains.launcherFor(exactJava8Spec) : +ext.productionLauncher = exactJava16Spec != null ? javaToolchains.launcherFor(exactJava16Spec) : javaToolchains.launcherFor(java.toolchain) fgtools.configure('slimelauncher') { javaLauncher.set(productionLauncher) } -def productionCompiler = exactJava8Spec != null ? javaToolchains.compilerFor(exactJava8Spec) : +def productionCompiler = exactJava16Spec != null ? javaToolchains.compilerFor(exactJava16Spec) : javaToolchains.compilerFor(java.toolchain) -def productionJavadoc = exactJava8Spec != null ? javaToolchains.javadocToolFor(exactJava8Spec) : +def productionJavadoc = exactJava16Spec != null ? javaToolchains.javadocToolFor(exactJava16Spec) : javaToolchains.javadocToolFor(java.toolchain) tasks.register('verifyJavaSetup') { @@ -58,9 +58,9 @@ tasks.register('verifyJavaSetup') { def metadata = productionLauncher.get().metadata Properties release = new Properties() metadata.installationPath.file('release').asFile.withInputStream { release.load(it) } - if (release.getProperty('SEMANTIC_VERSION') != '"8.0.502+7"' || - release.getProperty('IMPLEMENTOR') != '"Eclipse Adoptium"') { - throw new GradleException("Production requires Temurin 8.0.502+7, found ${metadata.javaRuntimeVersion} ${metadata.vendor}") + if (release.getProperty('SEMANTIC_VERSION') != '"16.0.2+7"' || + release.getProperty('IMPLEMENTOR') != '"Eclipse Foundation"') { + throw new GradleException("Production requires Temurin 16.0.2+7, found ${metadata.javaRuntimeVersion} ${metadata.vendor}") } logger.lifecycle("Gradle Java: ${System.getProperty('java.runtime.version')}; production: ${metadata}") def worker = mavenizerLauncher.get().metadata @@ -69,14 +69,14 @@ tasks.register('verifyJavaSetup') { if (workerRelease.getProperty('JAVA_RUNTIME_VERSION') != '"25.0.3+9-LTS"' || workerRelease.getProperty('IMPLEMENTOR') != '"Eclipse Adoptium"') throw new GradleException('The separate Mavenizer worker requires Temurin 25.0.3+9-LTS') - logger.lifecycle("Mavenizer worker: ${worker}; game/compilation remain on Java 8") + logger.lifecycle("Mavenizer worker: ${worker}; game/compilation remain on Java 16") } } tasks.withType(JavaCompile).configureEach { dependsOn 'verifyJavaSetup' javaCompiler = productionCompiler - sourceCompatibility = '1.8' - targetCompatibility = '1.8' + sourceCompatibility = '16' + targetCompatibility = '16' options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs','1000']) } @@ -86,6 +86,16 @@ tasks.withType(Test).configureEach { javaLauncher = productionLauncher workingDir = projectDir } + +// Focused native-boundary regressions need no graphical/control probe build. +// The full test task retains its archive/isolation prerequisites and all tests. +tasks.register('nativeBoundaryTest', Test) { + group = 'verification' + description = 'Runs native 1.17 conversion and storage regressions without launching Minecraft.' + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + include '**/Native117*Test.class' +} tasks.withType(JavaExec).configureEach { javaLauncher = productionLauncher } tasks.withType(Javadoc).configureEach { javadocTool = productionJavadoc @@ -100,7 +110,7 @@ tasks.withType(AbstractArchiveTask).configureEach { filteringCharset = 'UTF-8' } ext.archiveTextPatterns = ['**/*.info', '**/*.java', '**/*.json', '**/*.toml', '**/*.mcmeta', '**/*.html', - '**/*.css', '**/*.js', '**/*.md', '**/*.txt', '**/package-list', '**/LICENSE', '**/LICENSE-CC0', '**/NOTICE'] + '**/*.css', '**/*.js', '**/*.md', '**/*.txt', '**/*.xml', '**/*.properties', '**/package-list', '**/LICENSE', '**/LICENSE-CC0', '**/NOTICE'] ext.normalizeArchiveText = { details -> details.filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('lf'), eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) @@ -113,49 +123,19 @@ minecraft { accessTransformer = 'META-INF/accesstransformer.cfg' runs { configureEach { - mainClass = 'net.minecraftforge.userdev.LaunchTesting' workingDir.convention layout.projectDirectory.dir( providers.gradleProperty('gameRunDirectory').getOrElse('run')) + jvmArgs '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED' systemProperty 'log4j.configurationFile', file('gradle/log4j2.xml').toURI().toString() + mods { lootablebodies { source sourceSets.main } } } - register('client') { client=true; args '--assetsDir', minecraftAssets.asFile.absolutePath, '--assetIndex', '1.16' } - register('server') { client=false; args '--nogui' } - } -} -minecraft.runs.configureEach { options -> - systemProperties.put('user.home',minecraftGameHome.asFile.absolutePath) - environment.put('APPDATA',minecraftGameHome.asFile.absolutePath) - environment.put('target',options.name=='client'?'fmluserdevclient':'fmluserdevserver') - environment.put('MCP_MAPPINGS',"${mapping_channel}_${mapping_version}") - environment.put('MCP_VERSION',mcp_version.toString()) - environment.put('FORGE_VERSION',forge_version.toString()) - environment.put('FORGE_GROUP','net.minecraftforge') - environment.put('MC_VERSION',minecraft_version.toString()) - String root=layout.buildDirectory.dir('forge16-development/main').get().asFile.absolutePath - environment.put('MOD_CLASSES',"${mod_id}%%${root}${File.pathSeparator}${mod_id}%%${root}") - if(options.name=='client') { - environment.put('assetDirectory',minecraftAssets.asFile.absolutePath) - environment.put('assetIndex','1.16') - environment.put('nativesDirectory',new File(gradle.gradleUserHomeDir,"caches/minecraftforge/forgegradle/slime-launcher/cache/net/minecraftforge/forge/${minecraft_version}-${forge_version}/natives").absolutePath) + register('client') + register('server') { args '--nogui' } } } - tasks.withType(JavaExec).configureEach { run -> if (run.name in ['runClient','runServer']) { - environment 'target', run.name=='runClient'?'fmluserdevclient':'fmluserdevserver' - environment 'MCP_MAPPINGS', "${mapping_channel}_${mapping_version}" - environment 'MCP_VERSION', mcp_version - environment 'FORGE_VERSION', forge_version - environment 'FORGE_GROUP', 'net.minecraftforge' - environment 'MC_VERSION', minecraft_version - String root=layout.buildDirectory.dir('forge16-development/main').get().asFile.absolutePath - environment 'MOD_CLASSES', "${mod_id}%%${root}${File.pathSeparator}${mod_id}%%${root}" - if(run.name=='runClient') { - File cache=new File(gradle.gradleUserHomeDir,'caches/forge_gradle') - environment 'assetDirectory',minecraftAssets.asFile.absolutePath - environment 'assetIndex','1.16' - environment 'nativesDirectory',new File(cache,"minecraftforge/forgegradle/slime-launcher/cache/net/minecraftforge/forge/${minecraft_version}-${forge_version}/natives").absolutePath - } + environment 'MOD_CLASSES', "${mod_id}%%${layout.buildDirectory.dir('forge17-development/main').get().asFile.absolutePath}" } } @@ -172,7 +152,7 @@ tasks.register('prepareProductionRunDirectory') { } // Legacy FML discovers classes and processed resources from a single root. -ext.developmentOutput = layout.buildDirectory.dir('forge16-development/main') +ext.developmentOutput = layout.buildDirectory.dir('forge17-development/main') tasks.register('syncDevelopmentOutput', Sync) { dependsOn 'classes' from tasks.named('compileJava', JavaCompile).flatMap { it.destinationDirectory } @@ -208,6 +188,7 @@ repositories { dependencies { implementation minecraft.dependency("net.minecraftforge:forge:${minecraft_version}-${forge_version}") testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.platform:junit-platform-launcher:1.12.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2' } @@ -230,7 +211,7 @@ tasks.named('jar', Jar) { 'Implementation-Version': project.version, 'Implementation-Vendor': project.mod_vendor, 'Maven-Artifact': "${project.group}:${base.archivesName.get()}:${project.version}", 'Implementation-License': project.mod_license, - 'Built-On-Java': '8', 'Built-On': "${minecraft_version}-${forge_version}", + 'Built-On-Java': '16', 'Built-On': "${minecraft_version}-${forge_version}", 'FMLAT': 'accesstransformer.cfg') filesMatching(archiveTextPatterns, normalizeArchiveText) } @@ -251,6 +232,14 @@ ext.releaseJar = renamer.classes(tasks.named('jar', Jar)) { filesMatching(archiveTextPatterns, normalizeArchiveText) } } +// Java 16 writes this extensionless index with the host line separator. Keep +// the accepted 1.17 documentation bytes identical on Windows and Linux. +tasks.named('javadocJar', Jar) { + filesMatching('element-list') { + filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('crlf'), + eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) + } +} ['apiElements', 'runtimeElements'].each { name -> configurations.named(name) { artifacts.clear() } artifacts { add(name, releaseJar) } diff --git a/gradle.properties b/gradle.properties index dda0b50..159593e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,16 +5,16 @@ org.gradle.caching=true org.gradle.parallel=false org.gradle.workers.max=2 net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.16.5 -forge_version=36.2.42 -mapping_channel=snapshot -mapping_version=20210309-1.16.5 +minecraft_version=1.17.1 +forge_version=37.1.1 +mapping_channel=official +mapping_version=1.17.1 # Release identity and dispatcher target metadata. loader_name=forge loader_code=1 -java_version=8 -java_toolchain_version=8.0.502+7 +java_version=16 +java_toolchain_version=16.0.2+7 gradle_java_version=17 curseforge_project_id=240968 @@ -25,7 +25,7 @@ mod_vendor=Cyanobacterium mod_license=LGPL-2.1-only mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=2.4.7.116051 +mod_version=2.4.8.117011 mod_group=zone.moddev.mc.lootablebodies mod_authors=Cyanobacterium,SkyBlade1978 mod_description=Saves your items on a lootable corpse when you die. @@ -33,4 +33,4 @@ 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=20210115.111550 +mcp_version=20210706.113038 diff --git a/gradle/assets.gradle b/gradle/assets.gradle index b773e78..d297410 100644 --- a/gradle/assets.gradle +++ b/gradle/assets.gradle @@ -1,7 +1,8 @@ // Keep development launches independent of any installed Minecraft instance. ext.minecraftGameHome = layout.projectDirectory.dir('.gradle/minecraft-home') String assetFolder=org.gradle.internal.os.OperatingSystem.current().isMacOsX()?'Library/Application Support/minecraft/assets':'.minecraft/assets' -ext.minecraftAssets = minecraftGameHome.dir(assetFolder) +ext.minecraftAssets = providers.gradleProperty('minecraftAssetsDirectory').isPresent() ? + layout.projectDirectory.dir(providers.gradleProperty('minecraftAssetsDirectory').get()) : minecraftGameHome.dir(assetFolder) tasks.register('prepareMinecraftAssets') { group = 'ide' doLast { @@ -16,10 +17,10 @@ tasks.register('prepareMinecraftAssets') { java.nio.file.Files.move(pending.toPath(),output.toPath(),java.nio.file.StandardCopyOption.REPLACE_EXISTING) } } - File descriptor=new File(root,'1.16.5.json') - download('https://piston-meta.mojang.com/v1/packages/fba9f7833e858a1257d810d21a3a9e3c967f9077/1.16.5.json','fba9f7833e858a1257d810d21a3a9e3c967f9077',descriptor) + File descriptor=new File(root,'1.17.1.json') + download('https://piston-meta.mojang.com/v1/packages/e0e7ab5ed6f55bbd874ef95be3c9356d67e64b57/1.17.1.json','e0e7ab5ed6f55bbd874ef95be3c9356d67e64b57',descriptor) def version=new groovy.json.JsonSlurper().parse(descriptor) - File index=new File(root,'indexes/1.16.json') + File index=new File(root,'indexes/1.17.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 66facc5..25ae12e 100644 --- a/gradle/audit.gradle +++ b/gradle/audit.gradle @@ -33,7 +33,7 @@ ext.auditReleaseArchives = { File directory -> if (!path.startsWith('cyano/lootable/')) throw new GradleException("Foreign production class ${path}") byte[] bytes = zip.getInputStream(entry).bytes int major = ((bytes[6] & 255) << 8) | (bytes[7] & 255) - if (major != 52) throw new GradleException("${path} is class major ${major}") + if (major != 60) throw new GradleException("${path} is class major ${major}") } if (path.endsWith('.lang')) { java.nio.charset.StandardCharsets.UTF_8.newDecoder().decode( @@ -60,15 +60,17 @@ 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.16.5]"'].each { + ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.17.1]"'].each { if(!info.contains(it))throw new GradleException("Incorrect production metadata: ${it}") } - if(new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('pack.mcmeta')).getText('UTF-8')).pack.pack_format!=6) - throw new GradleException('Native resource pack format must be 6') + if(new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('pack.mcmeta')).getText('UTF-8')).pack.pack_format!=7) + throw new GradleException('Native resource pack format must be 7') 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.DataFixesManager field_210902_d'] || + if(atRules!=['public-f net.minecraft.util.datafix.DataFixers f_14510_', + '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') throw new GradleException('Missing or changed native world-reader access boundary') def languages=names.findAll {it.startsWith('assets/lootablebodies/lang/') && !it.endsWith('/')} @@ -78,7 +80,7 @@ ext.auditReleaseArchives = { File directory -> throw new GradleException('Bundled sources or dependencies in main jar') String entity = new String(zip.getInputStream(zip.getEntry( 'cyano/lootable/entities/EntityLootableBody.class')).bytes, 'ISO-8859-1') - if (!entity.contains('func_70037_a') || !entity.contains('func_213281_b')) + if (!entity.contains('m_7378_') || !entity.contains('m_7380_')) throw new GradleException('Production jar does not have runtime SRG mappings') } else if (index == 1 && !names.contains('cyano/lootable/LootableBodies.java')) { throw new GradleException('Sources jar does not contain production sources') diff --git a/gradle/ide.gradle b/gradle/ide.gradle index ef6ffc0..269c9ba 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -1,5 +1,5 @@ eclipse { - jdt.javaRuntimeName = 'JavaSE-1.8' + jdt.javaRuntimeName = 'JavaSE-16' classpath { downloadSources = true downloadJavadoc = true @@ -36,7 +36,7 @@ tasks.register('configureEclipseBuildship') { File localJavaSetup = file('gradle-local.properties') Properties toolchains = new Properties() if (localJavaSetup.isFile()) localJavaSetup.withInputStream { toolchains.load(it) } - toolchains.setProperty('lootablebodies.java8.home', + toolchains.setProperty('lootablebodies.java16.home', productionLauncher.get().metadata.installationPath.asFile.canonicalPath) toolchains.setProperty('lootablebodies.mavenizer.java.home', mavenizerLauncher.get().metadata.installationPath.asFile.canonicalPath) @@ -64,15 +64,16 @@ tasks.register('isolateEclipseProductionRuns') { File launch = file(name) if (!launch.isFile()) throw new GradleException("Generate ${name} with genEclipseRuns first") String contents = launch.getText('UTF-8') - File cache=new File(gradle.gradleUserHomeDir,'caches/forge_gradle') - contents=contents.replace('value="{asset_index}"','value="1.16"') - .replace('value="{assets_root}"',"value=\"${minecraftAssets.asFile.absolutePath}\"") - .replace('value="{natives}"',"value=\"${new File(cache,"minecraftforge/forgegradle/slime-launcher/cache/net/minecraftforge/forge/${minecraft_version}-${forge_version}/natives").absolutePath}\"") + File nativeRoot=file(providers.gradleProperty('minecraftNativesDirectory') + .getOrElse('.gradle/native117-client-inputs/natives')) + if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native1.17 client inputs before generating Eclipse launches') + contents=contents.replace('{asset_index}','1.17') + .replace('{assets_root}',minecraftAssets.asFile.absolutePath) + .replace('{natives}',nativeRoot.absolutePath) String eclipseRoot=file('bin/main').absolutePath String eclipseMods="${mod_id}%%${eclipseRoot}${File.pathSeparator}${mod_id}%%${eclipseRoot}" contents=contents.replaceAll(/()/) {whole,start,end -> start+eclipseMods+end} - contents = contents.replace('/JavaSE-8', '/JavaSE-1.8') - contents = contents.replace('key="MC_VERSION" value="${MC_VERSION}"', 'key="MC_VERSION" value="1.16.5"') + contents = contents.replace('/JavaSE-8', '/JavaSE-16') contents = contents.replaceAll(//, '') contents = contents.replace('', ' \n') @@ -126,7 +127,8 @@ tasks.register('verifyEclipseProductionClasspath') { xml =~ /(?i)(src\/test|bin\/test|junit-|integration-test|mockito|byte-buddy)/ || !xml.contains('PROJECT_ATTR" value="LootableBodies"')) throw new GradleException("${name} has the wrong project or exposes tests") - if (!xml.contains('key="MC_VERSION" value="1.16.5"')) + if (!xml.contains('--fml.mcVersion 1.17.1') || xml.contains('{asset_index}') || + xml.contains('{assets_root}') || xml.contains('{natives}')) throw new GradleException('Eclipse Minecraft version is unresolved') def launchModel = new groovy.xml.XmlSlurper().parseText(xml) String classes = launchModel.mapAttribute.mapEntry.find { it.@key.text() == 'MOD_CLASSES' }.@value.text() diff --git a/gradle/native116-writer.init.gradle b/gradle/native116-writer.init.gradle new file mode 100644 index 0000000..60a64b6 --- /dev/null +++ b/gradle/native116-writer.init.gradle @@ -0,0 +1,26 @@ +// Compile historical fixtures with the released 1.16 API, never the candidate API. +gradle.projectsEvaluated { + def p = gradle.rootProject + File writerRoot = p.file(p.property('historicalWriterSource')) + def classes = p.layout.buildDirectory.dir('native116-seed/classes') + def compile = p.tasks.register('compileNative116Seed', JavaCompile) { + dependsOn 'classes' + source p.fileTree(new File(writerRoot, 'src/nativeSeed116/java')) + classpath = p.files(p.sourceSets.main.output, p.sourceSets.main.compileClasspath) + destinationDirectory = classes + options.compilerArgs.add('-proc:none') + } + def jar = p.tasks.register('native116SeedJar', Jar) { + dependsOn compile + archiveFileName = 'lootablebodies-native116-world-fixtures.jar' + destinationDirectory = p.layout.buildDirectory.dir('native116-seed') + from classes + from new File(writerRoot, 'src/nativeSeed116/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('native116-seed/lootablebodies-native116-world-fixtures-reobf.jar') + } + p.tasks.register('prepareNative116WorldFixtures') { dependsOn packaged } +} diff --git a/gradle/release.gradle b/gradle/release.gradle index 6f9f26d..425c509 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,10 +5,10 @@ def expectedIdentity = [ mod_id: 'lootablebodies', mod_artifact: 'LootableBodies', mod_group: 'zone.moddev.mc.lootablebodies', mod_vendor: 'Cyanobacterium', mod_name: "DrCyano's Lootable Bodies", mod_authors: 'Cyanobacterium,SkyBlade1978', - minecraft_version: '1.16.5', forge_version: '36.2.42', - mapping_channel: 'snapshot', mapping_version: '20210309-1.16.5', + minecraft_version: '1.17.1', forge_version: '37.1.1', + mapping_channel: 'official', mapping_version: '1.17.1', loader_name: 'forge', loader_code: '1', curseforge_project_id: '240968', - java_version: '8', java_toolchain_version: '8.0.502+7', gradle_java_version: '17', + java_version: '16', java_toolchain_version: '16.0.2+7', gradle_java_version: '17', mod_license: 'LGPL-2.1-only', mod_license_name: 'GNU Lesser General Public License, Version 2.1', mod_license_url: 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt', mod_url: 'https://github.com/MinecraftModDevelopmentMods/LootableBodies', @@ -182,3 +182,9 @@ tasks.register('validateMavenRelease') { tasks.withType(AbstractPublishToMaven).configureEach { dependsOn 'validateMavenRelease', 'verifyPreparedReleaseArtifacts' } + +tasks.withType(org.gradle.api.publish.maven.tasks.GenerateMavenPom).configureEach { + doLast { + destination.setText(destination.getText('UTF-8').replace('\r\n','\n'), 'UTF-8') + } +} diff --git a/gradle/runtime.gradle b/gradle/runtime.gradle index d71bbae..1f02836 100644 --- a/gradle/runtime.gradle +++ b/gradle/runtime.gradle @@ -5,7 +5,11 @@ tasks.register('compileIntegrationTestMod', JavaCompile) { group = 'verification' dependsOn 'classes' source fileTree('src/integrationTest/java') - source fileTree('src/testSupport/java') + source fileTree('src/testSupport/java') { + // Native37 mod modules cannot define a production package twice. + // This native-codec fixture belongs to unit/private-writer classpaths. + exclude 'cyano/lootable/upgrade/**' + } classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath) destinationDirectory = probeClasses options.compilerArgs.add('-proc:none') @@ -71,6 +75,18 @@ def prepareOwnedLoaderConfig = { File root -> } } +// Native37 cannot decode a bare FLAT generator through the ordinary properties +// path. Seed the exact native codec header in a newly owned fixture world. +def prepareOwnedFlatWorld = { File root -> + File header = file('tools/fixtures/native117-flat-level.dat') + if (!header.isFile() || releaseDigest(header) != '358E4B91F290AFB219D85B09435B4A641035412686E31A09CF5C69F5A28515CB') + 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') + world.mkdirs() + new File(world,'level.dat').bytes = header.bytes +} + def runDirectory = layout.dir(providers.provider { file(providers.gradleProperty('integrationDirectory').getOrElse('build/integration-run')) }) tasks.register('prepareIntegrationTest') { dependsOn probeJar @@ -83,12 +99,13 @@ tasks.register('prepareIntegrationTest') { } new File(root,'mods').mkdirs() prepareOwnedLoaderConfig(root) + prepareOwnedFlatWorld(root) copy { from probeJar; into new File(root,'mods') } new File(root,'eula.txt').text = 'eula=true\n' ['ops','whitelist','banned-players','banned-ips'].each { new File(root,"${it}.json").text='[]\n' } new File(root,'server.properties').text = '''level-name=world level-seed=4267321 -level-type=FLAT +level-type=DEFAULT online-mode=false server-ip=127.0.0.1 server-port=0 @@ -105,6 +122,7 @@ max-tick-time=120000 def phaseTask = { String phase, Object prerequisite -> tasks.register("integration${phase.capitalize()}Process", JavaExec) { group = 'verification' + timeout = java.time.Duration.ofSeconds(240) dependsOn prerequisite dependsOn { def run = tasks.getByName('runServer'); run.taskDependencies.getDependencies(run) } actions.clear() @@ -115,12 +133,14 @@ def phaseTask = { String phase, Object prerequisite -> def previousProperties = new LinkedHashMap(run.systemProperties) def previousClasspath = files(run.classpath) def previousArgs = new ArrayList(run.args) + def previousJvmArgs = new ArrayList(run.jvmArgs) def previousMain = run.mainClass.orNull try { options.workingDir.set(runDirectory) run.workingDir(runDirectory) run.systemProperty('lootablebodies.phase', phase) run.systemProperty('log4j.configurationFile', file('src/integrationTest/resources/log4j2.xml').toURI()) + run.jvmArgs('-Xlog:gc*,safepoint:file=integration-'+phase+'.gc.txt:time,uptime,pid,level,tags') run.executable = productionLauncher.get().executablePath.asFile.path run.setClasspath(files(developmentOutput, run.classpath.filter { !sourceSets.main.output.files.contains(it) })) run.exec() @@ -130,6 +150,7 @@ def phaseTask = { String phase, Object prerequisite -> run.setSystemProperties(previousProperties) run.setClasspath(previousClasspath) run.setArgs(previousArgs) + run.setJvmArgs(previousJvmArgs) run.mainClass.set(previousMain) } File marker = new File(runDirectory.get().asFile,"world/${phase}.pass") @@ -138,6 +159,9 @@ def phaseTask = { String phase, Object prerequisite -> if (!logs.isFile() || logs.text =~ /(?i)(AssertionError|Encountered an unexpected exception|Exception stopping server)/) throw new GradleException("Runtime failure in ${logs}") copy { from logs; into runDirectory; rename { "${phase}.log" } } + File debug = new File(runDirectory.get().asFile,'logs/debug.log') + if (!debug.isFile() || !debug.length()) throw new GradleException('Missing complete native debug log') + copy { from debug; into runDirectory; rename { "${phase}.debug.log" } } if (!fileTree(new File(runDirectory.get().asFile,'crash-reports')).isEmpty()) throw new GradleException('Runtime crash reports found') } @@ -261,9 +285,22 @@ tasks.register('preparePackagedForgeControl') { dependsOn packagedControl } // Eclipse generation has finalized the production models' environment. minecraft.runs.register('platformControl') { client=false - args '--nogui' + // Native37 supplies BootstrapLauncher only inside its named run configs. + // This separate model needs the exact pinned server metadata parameters. + mainClass.set('cpw.mods.bootstraplauncher.BootstrapLauncher') + args '--launchTarget','forgeserveruserdev','--gameDir','.', + '--fml.forgeVersion',project.forge_version,'--fml.mcVersion',project.minecraft_version, + '--fml.forgeGroup','net.minecraftforge','--fml.mcpVersion','20210706.113038','--nogui' + jvmArgs '-p','{modules}','--add-modules','ALL-MODULE-PATH', + '--add-opens','java.base/java.util.jar=cpw.mods.securejarhandler', + '--add-exports','java.base/sun.security.util=cpw.mods.securejarhandler', + '--add-exports','jdk.naming.dns/com.sun.jndi.dns=java.naming' + systemProperty 'legacyClassPath.file','{minecraft_classpath_file}' + systemProperty 'ignoreList','bootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,client-extra,fmlcore,javafmllanguage,mclanguage,forge-' + systemProperty 'mergeModules','jna-5.8.0.jar,jna-platform-58.0.jar,java-objc-bridge-1.0.0.jar' + environment 'MCP_MAPPINGS','{mcp_mappings}' workingDir=layout.buildDirectory.dir('forge-control-run') - String root=layout.buildDirectory.dir('forge16-development/control').get().asFile.absolutePath + String root=layout.buildDirectory.dir('forge17-development/control').get().asFile.absolutePath environment.put('MOD_CLASSES',"forgeplatformcontrol%%${root}${File.pathSeparator}forgeplatformcontrol%%${root}") } def prepareForgeControl = tasks.register('prepareForgeControl') { @@ -276,10 +313,11 @@ def prepareForgeControl = tasks.register('prepareForgeControl') { } new File(root,'mods').mkdirs() prepareOwnedLoaderConfig(root) - File controlOutput=layout.buildDirectory.dir('forge16-development/control').get().asFile + File controlOutput=layout.buildDirectory.dir('forge17-development/control').get().asFile + prepareOwnedFlatWorld(root) copy { from controlClasses;from 'src/controlTest/resources';into controlOutput } new File(root,'eula.txt').text='eula=true\n' - new File(root,'server.properties').text='level-name=world\nlevel-type=FLAT\nserver-ip=127.0.0.1\nserver-port=0\nonline-mode=false\n' + new File(root,'server.properties').text='level-name=world\nlevel-type=DEFAULT\nserver-ip=127.0.0.1\nserver-port=0\nonline-mode=false\n' ['ops','whitelist','banned-players','banned-ips'].each { new File(root,"${it}.json").text='[]\n' } } } @@ -297,7 +335,7 @@ tasks.register('forgeControlServer') { mustRunAfter tasks.named('test') doLast { File root=layout.buildDirectory.dir('forge-control-run').get().asFile - File controlOutput=layout.buildDirectory.dir('forge16-development/control').get().asFile + File controlOutput=layout.buildDirectory.dir('forge17-development/control').get().asFile JavaExec run=tasks.getByName('runPlatformControl') def options = minecraft.runs.getByName('platformControl') File previousDir = options.workingDir.get().asFile diff --git a/settings.gradle b/settings.gradle index d6dc9f9..252f72d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,6 @@ plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' } rootProject.name = 'LootableBodies' + +// Publication remains restricted to the confirmed MMD release controller in +// gradle/release.gradle; local Maven and unconfirmed uploads are rejected there. diff --git a/src/baselineTest/java/cyano/lootable/buildtest/Original112Checks.java b/src/baselineTest/java/cyano/lootable/buildtest/Original112Checks.java index 7f9874a..b6519f8 100644 --- a/src/baselineTest/java/cyano/lootable/buildtest/Original112Checks.java +++ b/src/baselineTest/java/cyano/lootable/buildtest/Original112Checks.java @@ -5,7 +5,7 @@ import java.io.*; import java.util.*; import net.minecraft.init.Items; -import net.minecraft.item.ItemStack; +import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.world.WorldServer; import net.minecraftforge.fml.common.*; diff --git a/src/baselineTest/java/cyano/lootable/buildtest/RuntimeChecks.java b/src/baselineTest/java/cyano/lootable/buildtest/RuntimeChecks.java index afab521..3995f64 100644 --- a/src/baselineTest/java/cyano/lootable/buildtest/RuntimeChecks.java +++ b/src/baselineTest/java/cyano/lootable/buildtest/RuntimeChecks.java @@ -1,10 +1,10 @@ package cyano.lootable.buildtest; -import net.minecraft.item.ItemStack; +import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.init.Items; final class RuntimeChecks { static void require(boolean value,String message){if(!value)throw new AssertionError(message);} static ItemStack stack(int index) { - ItemStack stack = index < 4 ? new ItemStack(new net.minecraft.item.Item[]{Items.DIAMOND_HELMET, Items.DIAMOND_CHESTPLATE, Items.DIAMOND_LEGGINGS, Items.DIAMOND_BOOTS}[index],1,index+2) : + ItemStack stack = index < 4 ? new ItemStack(new net.minecraft.world.item.Item[]{Items.DIAMOND_HELMET, Items.DIAMOND_CHESTPLATE, Items.DIAMOND_LEGGINGS, Items.DIAMOND_BOOTS}[index],1,index+2) : index == 4 ? new ItemStack(Items.DIAMOND_SWORD,1,17) : index == 5 ? new ItemStack(Items.SHIELD,1,9) : new ItemStack(Items.DYE, 1 + index % 32, index % 16); NBTTagCompound custom = new NBTTagCompound(); diff --git a/src/controlTest/java/buildcontrol/ForgeControl.java b/src/controlTest/java/buildcontrol/ForgeControl.java index c7fcf43..cc87061 100644 --- a/src/controlTest/java/buildcontrol/ForgeControl.java +++ b/src/controlTest/java/buildcontrol/ForgeControl.java @@ -1,43 +1,48 @@ package buildcontrol; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.event.server.FMLServerStartedEvent; +import net.minecraftforge.fmlserverevents.FMLServerStartedEvent; import net.minecraftforge.event.TickEvent; /** Independent native Forge control, without a Lootable Bodies dependency. */ @Mod("forgeplatformcontrol") public final class ForgeControl { public ForgeControl() { net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); + net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus().addListener(this::loadComplete); net.minecraftforge.fml.DistExecutor.runWhenOn(net.minecraftforge.api.distmarker.Dist.CLIENT,()->()->net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(new ClientControl())); } + private void loadComplete(net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent event) { + int rounds=Integer.getInteger("lootablebodies.control.resourceReloads",0); + if(rounds>0)event.enqueueWork(()->ResourceControl.run(rounds)); + } @SubscribeEvent public void started(FMLServerStartedEvent event) { if(net.minecraftforge.fml.ModList.get().getModContainerById("lootablebodies").isPresent())throw new AssertionError("Independent Forge control contains Lootable Bodies"); if(net.minecraftforge.fml.loading.FMLEnvironment.dist==net.minecraftforge.api.distmarker.Dist.DEDICATED_SERVER) { if(Boolean.getBoolean("lootablebodies.control.tcpServer"))System.out.println("FORGE_PLATFORM_CONTROL_READY"); - else {System.out.println("FORGE_PLATFORM_CONTROL_PASS");event.getServer().initiateShutdown(false);} + else {System.out.println("FORGE_PLATFORM_CONTROL_PASS");event.getServer().halt(false);} } } private int checkTicks,checkDeaths; @SubscribeEvent public void deathTick(TickEvent.ServerTickEvent event) { if(event.phase!=TickEvent.Phase.END || !Boolean.getBoolean("lootablebodies.control.deaths"))return; - net.minecraft.server.MinecraftServer server=net.minecraftforge.fml.server.ServerLifecycleHooks.getCurrentServer(); - net.minecraft.entity.player.ServerPlayerEntity p=server.getPlayerList().getPlayerByUsername("ControlOnly"); + net.minecraft.server.MinecraftServer server=net.minecraftforge.fmllegacy.server.ServerLifecycleHooks.getCurrentServer(); + net.minecraft.server.level.ServerPlayer p=server.getPlayerList().getPlayerByName("ControlOnly"); if(p==null || !p.isAlive())return; if(++checkTicks==1) { - p.setGameType(net.minecraft.world.GameType.SURVIVAL); + 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.world.setBlockState(new net.minecraft.util.math.BlockPos(x+a,80,z+b),net.minecraft.block.Blocks.OBSIDIAN.getDefaultState()); - p.func_242111_a(p.world.getDimensionKey(),new net.minecraft.util.math.BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),0F,true,false); - p.connection.setPlayerLocation(x,81,z,0,0); + 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()); + 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); } if(checkTicks==50){ - int px=(int)Math.floor(p.getPosX()),pz=(int)Math.floor(p.getPosZ()); + int px=(int)Math.floor(p.getX()),pz=(int)Math.floor(p.getZ()); for(int ax=-6;ax<=6;ax++)for(int az=-6;az<=6;az++){ - p.world.setBlockState(new net.minecraft.util.math.BlockPos(px+ax,77,pz+az),net.minecraft.block.Blocks.OBSIDIAN.getDefaultState()); - p.world.removeBlock(new net.minecraft.util.math.BlockPos(px+ax,79,pz+az),false); - p.world.removeBlock(new net.minecraft.util.math.BlockPos(px+ax,80,pz+az),false); + p.level.setBlockAndUpdate(new net.minecraft.core.BlockPos(px+ax,77,pz+az),net.minecraft.world.level.block.Blocks.OBSIDIAN.defaultBlockState()); + p.level.removeBlock(new net.minecraft.core.BlockPos(px+ax,79,pz+az),false); + p.level.removeBlock(new net.minecraft.core.BlockPos(px+ax,80,pz+az),false); } - p.onKillCommand();checkTicks=0;System.out.println("NATIVE_DEATH "+(++checkDeaths));} + p.kill();checkTicks=0;System.out.println("NATIVE_DEATH "+(++checkDeaths));} } public static final class ClientControl { private int stage,ticks; @@ -45,19 +50,19 @@ public static final class ClientControl { if(event.phase!=TickEvent.Phase.END)return; net.minecraft.client.Minecraft mc=net.minecraft.client.Minecraft.getInstance(); if(Integer.getInteger("lootablebodies.control.port",0)>0) {tcp(mc);return;} - if(stage==0 && mc.currentScreen instanceof net.minecraft.client.gui.screen.MainMenuScreen) { - mc.gameSettings.pauseOnLostFocus=false; + if(stage==0 && mc.screen instanceof net.minecraft.client.gui.screens.TitleScreen) { + mc.options.pauseOnLostFocus=false; if(Boolean.getBoolean("lootablebodies.control.world")) { createWorld(mc);stage=1; }else finish(mc); - }else if(stage==1 && mc.world!=null && mc.player!=null) { + }else if(stage==1 && mc.level!=null && mc.player!=null) { try { java.nio.file.Files.write(java.nio.file.Paths.get("config/forge-client.toml"),"\n# disposable watcher control\n".getBytes(java.nio.charset.StandardCharsets.UTF_8),java.nio.file.StandardOpenOption.APPEND); }catch(java.io.IOException failure){throw new IllegalStateException(failure);} stage=4; }else if(stage==4 && ++ticks>=80) { - mc.world.sendQuittingDisconnectingPacket();mc.unloadWorld();stage=2; - }else if(stage==2 && mc.world==null && !mc.isIntegratedServerRunning())finish(mc); + mc.level.disconnect();mc.clearLevel();stage=2; + }else if(stage==2 && mc.level==null && !mc.hasSingleplayerServer())finish(mc); } private int connections,wait,deathTicks,respawns; private volatile boolean hold; @@ -67,7 +72,7 @@ public static final class ClientControl { private void delayNativePackets(net.minecraft.client.Minecraft mc) { if(!installed) { installed=true; - mc.getConnection().getNetworkManager().channel().pipeline().addBefore("packet_handler","native_latency_control",new io.netty.channel.ChannelOutboundHandlerAdapter(){ + mc.getConnection().getConnection().channel().pipeline().addBefore("packet_handler","native_latency_control",new io.netty.channel.ChannelOutboundHandlerAdapter(){ @Override public void handlerAdded(io.netty.channel.ChannelHandlerContext ctx){delayContext=ctx;} @Override public void write(io.netty.channel.ChannelHandlerContext ctx,Object message,io.netty.channel.ChannelPromise promise) throws Exception { if(hold || !delayed.isEmpty()){delayed.add(new Object[]{message,promise});return;} @@ -75,7 +80,7 @@ private void delayNativePackets(net.minecraft.client.Minecraft mc) { } }); } - boolean next=mc.currentScreen instanceof net.minecraft.client.gui.screen.DeathScreen && deathTicks<40; + boolean next=mc.screen instanceof net.minecraft.client.gui.screens.DeathScreen && deathTicks<40; boolean release=hold && !next;hold=next; if(release)delayContext.executor().execute(()->{ if(hold)return; @@ -84,61 +89,61 @@ private void delayNativePackets(net.minecraft.client.Minecraft mc) { delayed.clear();delayContext.flush(); }); } - private net.minecraft.client.network.ServerPinger pinger; + private net.minecraft.client.multiplayer.ServerStatusPinger pinger; private net.minecraft.client.multiplayer.ServerData destination; private void tcp(net.minecraft.client.Minecraft mc) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30; + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30; if(++wait>1600)throw new AssertionError("Native Forge control login timed out"); - if(stage==1 && wait%40==0 && mc.currentScreen instanceof net.minecraft.client.gui.screen.ConnectingScreen) { + if(stage==1 && wait%40==0 && mc.screen instanceof net.minecraft.client.gui.screens.ConnectScreen) { try { - for(java.lang.reflect.Field field:net.minecraft.client.gui.screen.ConnectingScreen.class.getDeclaredFields()) - if(field.getType()==net.minecraft.network.NetworkManager.class) { - field.setAccessible(true);net.minecraft.network.NetworkManager manager=(net.minecraft.network.NetworkManager)field.get(mc.currentScreen); + for(java.lang.reflect.Field field:net.minecraft.client.gui.screens.ConnectScreen.class.getDeclaredFields()) + if(field.getType()==net.minecraft.network.Connection.class) { + field.setAccessible(true);net.minecraft.network.Connection manager=(net.minecraft.network.Connection)field.get(mc.screen); if(manager!=null && manager.channel()!=null) { - System.out.println("NATIVE_FORGE_TCP_STATE autoRead="+manager.channel().config().isAutoRead()+" active="+manager.channel().isActive()+" protocol="+manager.channel().attr(net.minecraft.network.NetworkManager.PROTOCOL_ATTRIBUTE_KEY).get()); + System.out.println("NATIVE_FORGE_TCP_STATE autoRead="+manager.channel().config().isAutoRead()+" active="+manager.channel().isActive()+" protocol="+manager.channel().attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).get()); } } }catch(ReflectiveOperationException failure){throw new IllegalStateException(failure);} } - if(stage==0 && mc.currentScreen instanceof net.minecraft.client.gui.screen.MainMenuScreen) { + if(stage==0 && mc.screen instanceof net.minecraft.client.gui.screens.TitleScreen) { if(connections>=10){finish(mc);return;} destination=new net.minecraft.client.multiplayer.ServerData("Owned native control","127.0.0.1:"+Integer.getInteger("lootablebodies.control.port"),false); if(Boolean.getBoolean("lootablebodies.control.ping")) { - pinger=new net.minecraft.client.network.ServerPinger(); - try{pinger.ping(destination,()->{});}catch(java.net.UnknownHostException failure){throw new IllegalStateException(failure);} + pinger=new net.minecraft.client.multiplayer.ServerStatusPinger(); + try{pinger.pingServer(destination,()->{});}catch(java.net.UnknownHostException failure){throw new IllegalStateException(failure);} stage=5; - }else {mc.displayGuiScreen(new net.minecraft.client.gui.screen.ConnectingScreen(new net.minecraft.client.gui.screen.MainMenuScreen(),mc,destination));stage=1;} + }else {net.minecraft.client.gui.screens.ConnectScreen.startConnecting(new net.minecraft.client.gui.screens.TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(destination.ip),destination);stage=1;} wait=0; }else if(stage==5) { - pinger.pingPendingNetworks(); - if(destination.pingToServer>=0){pinger.clearPendingNetworks();mc.displayGuiScreen(new net.minecraft.client.gui.screen.ConnectingScreen(new net.minecraft.client.gui.screen.MainMenuScreen(),mc,destination));stage=1;wait=0;} - }else if(stage==1 && mc.world!=null && mc.player!=null) { + pinger.tick(); + if(destination.ping>=0){pinger.removeAll();net.minecraft.client.gui.screens.ConnectScreen.startConnecting(new net.minecraft.client.gui.screens.TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(destination.ip),destination);stage=1;wait=0;} + }else if(stage==1 && mc.level!=null && mc.player!=null) { if(Boolean.getBoolean("lootablebodies.control.deaths")) { delayNativePackets(mc); - if(mc.currentScreen instanceof net.minecraft.client.gui.screen.DeathScreen) { - if(++deathTicks==60){mc.player.respawnPlayer();System.out.println("NATIVE_RESPAWN "+(++respawns));} + if(mc.screen instanceof net.minecraft.client.gui.screens.DeathScreen) { + if(++deathTicks==60){mc.player.respawn();System.out.println("NATIVE_RESPAWN "+(++respawns));} }else if(mc.player.isAlive())deathTicks=0; if(respawns>=8){finish(mc);return;} wait=0;return; } System.out.println("NATIVE_FORGE_TCP_JOIN "+(++connections)); - mc.world.sendQuittingDisconnectingPacket();mc.unloadWorld();mc.displayGuiScreen(new net.minecraft.client.gui.screen.MainMenuScreen());stage=0;wait=0; - }else if(stage==1 && mc.currentScreen instanceof net.minecraft.client.gui.screen.DisconnectedScreen) { + mc.level.disconnect();mc.clearLevel();mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen());stage=0;wait=0; + }else if(stage==1 && mc.screen instanceof net.minecraft.client.gui.screens.DisconnectedScreen) { throw new AssertionError("NATIVE_FORGE_TCP_LOGIN_FAILURE joins="+connections); } } private void finish(net.minecraft.client.Minecraft mc) { - System.out.println("FORGE_PLATFORM_CONTROL_PASS");mc.shutdown();stage=3; + System.out.println("FORGE_PLATFORM_CONTROL_PASS");mc.stop();stage=3; } private void createWorld(net.minecraft.client.Minecraft client) { - net.minecraft.util.registry.DynamicRegistries.Impl registries=net.minecraft.util.registry.DynamicRegistries.func_239770_b_(); - net.minecraft.util.registry.Registry types=registries.getRegistry(net.minecraft.util.registry.Registry.DIMENSION_TYPE_KEY); - net.minecraft.util.registry.Registry biomes=registries.getRegistry(net.minecraft.util.registry.Registry.BIOME_KEY); - net.minecraft.world.gen.settings.DimensionGeneratorSettings settings=new net.minecraft.world.gen.settings.DimensionGeneratorSettings(4267321L,false,false, - net.minecraft.world.gen.settings.DimensionGeneratorSettings.func_242749_a(types, - net.minecraft.world.DimensionType.getDefaultSimpleRegistry(types,biomes,registries.getRegistry(net.minecraft.util.registry.Registry.NOISE_SETTINGS_KEY),4267321L), - new net.minecraft.world.gen.FlatChunkGenerator(net.minecraft.world.gen.FlatGenerationSettings.func_242869_a(biomes)))); - client.createWorld("platform-fixture",new net.minecraft.world.WorldSettings("Platform fixture",net.minecraft.world.GameType.CREATIVE,false,net.minecraft.world.Difficulty.NORMAL,false,new net.minecraft.world.GameRules(),net.minecraft.util.datafix.codec.DatapackCodec.VANILLA_CODEC),registries,settings); + net.minecraft.core.RegistryAccess.RegistryHolder registries=net.minecraft.core.RegistryAccess.builtin(); + 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(types,biomes,registries.registryOrThrow(net.minecraft.core.Registry.NOISE_GENERATOR_SETTINGS_REGISTRY),4267321L), + new net.minecraft.world.level.levelgen.FlatLevelSource(net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings.getDefault(biomes)))); + 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); } } } diff --git a/src/controlTest/java/buildcontrol/ResourceControl.java b/src/controlTest/java/buildcontrol/ResourceControl.java new file mode 100644 index 0000000..c5e0553 --- /dev/null +++ b/src/controlTest/java/buildcontrol/ResourceControl.java @@ -0,0 +1,52 @@ +package buildcontrol; + +import java.nio.file.*; +import java.util.concurrent.*; +import net.minecraft.Util; +import net.minecraft.commands.Commands; +import net.minecraft.core.RegistryAccess; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerResources; +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; + +/** Actual packaged native resource loading, without Lootable Bodies or a world. */ +final class ResourceControl { + private ResourceControl() { } + static void run(int rounds) { + try { + if(rounds!=50)throw new AssertionError("Each bounded resource qualification shard must contain50reloads"); + if(net.minecraftforge.fml.ModList.get().getModContainerById("lootablebodies").isPresent()) + throw new AssertionError("Independent resource control contains Lootable Bodies"); + if(net.minecraftforge.fmllegacy.server.ServerLifecycleHooks.getCurrentServer()!=null) + throw new AssertionError("Resource control has already constructed a server"); + Executor prepare=Util.backgroundExecutor(); + if(!(prepare instanceof ForkJoinPool) || ((ForkJoinPool)prepare).getParallelism()!=1) + throw new AssertionError("Historical native resource control requires one actual preparation worker"); + int cpus=Runtime.getRuntime().availableProcessors(); + System.out.println("NATIVE37_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers=1 rounds="+rounds); + RegistryAccess.RegistryHolder registries=RegistryAccess.builtin(); + try(PackRepository packs=new PackRepository(PackType.SERVER_DATA,new ServerPacksSource())) { + MinecraftServer.configurePackRepository(packs,DataPackConfig.DEFAULT,false); + 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(ServerResources resources=ServerResources.loadResources(packs.openAllSelected(),registries, + Commands.CommandSelection.DEDICATED,2,prepare,Runnable::run).get(12,TimeUnit.SECONDS)) { + if(resources.getRecipeManager().getRecipes().isEmpty())throw new AssertionError("No native recipes loaded"); + } + if(round%10==0)System.out.println("NATIVE37_RESOURCE_RELOAD_COMPLETE="+round); + } + } + if(net.minecraftforge.fmllegacy.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=1 noWorld=true noLootableBodies=true\n",StandardOpenOption.CREATE_NEW); + System.out.println("NATIVE37_RESOURCE_CONTROL_PASS rounds="+rounds); + System.out.flush();System.exit(0); + } catch(Throwable failure) { + failure.printStackTrace();System.out.flush();System.exit(2); + } + } +} diff --git a/src/controlTest/resources/META-INF/mods.toml b/src/controlTest/resources/META-INF/mods.toml index fb4e1dd..6a1bfa0 100644 --- a/src/controlTest/resources/META-INF/mods.toml +++ b/src/controlTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[36,)" +loaderVersion="[37,)" license="LGPL-2.1-only" [[mods]] modId="forgeplatformcontrol" diff --git a/src/controlTest/resources/pack.mcmeta b/src/controlTest/resources/pack.mcmeta index 442e262..bd8aafc 100644 --- a/src/controlTest/resources/pack.mcmeta +++ b/src/controlTest/resources/pack.mcmeta @@ -1 +1 @@ -{"pack":{"pack_format":6,"description":"Disposable Forge control"}} +{"pack":{"pack_format":7,"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 fe9a8fd..57deb6b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java @@ -1,19 +1,19 @@ package cyano.lootable.buildtest; -import net.minecraft.block.Blocks; -import net.minecraft.entity.boss.dragon.phase.Phase; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerLevel; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; import java.io.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.MainMenuScreen; -import net.minecraft.entity.Entity; -import net.minecraft.util.Hand; -import net.minecraft.util.ScreenShotHelper; -import net.minecraft.world.*; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.InteractionHand; +import net.minecraft.client.Screenshot; +import net.minecraft.world.level.GameType; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -37,6 +37,9 @@ public final class ClientChecks { @SubscribeEvent public void keyboard(net.minecraftforge.client.event.GuiScreenEvent.KeyboardKeyPressedEvent.Pre event) { event.setCanceled(true); } + @SubscribeEvent public void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Color event) { + if(Boolean.getBoolean("lootablebodies.configUiCheck"))try {configScreen.tooltip(event);}catch(Throwable error){fail(error);} + } public void register() { if (Boolean.getBoolean("lootablebodies.clientCheck")) { net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); @@ -47,93 +50,93 @@ public void register() { if(event.phase != TickEvent.Phase.END) return; Minecraft mc = Minecraft.getInstance(); // The verifier never needs to capture the user's mouse or keyboard. - if(mc.isGameFocused()) mc.mouseHelper.ungrabMouse(); - net.minecraft.client.settings.KeyBinding.unPressAllKeys(); + if(mc.isWindowActive()) mc.mouseHandler.releaseMouse(); + net.minecraft.client.KeyMapping.releaseAll(); if(stage!=reportedStage || ticks%60==0) { - System.out.println("GRAPHICAL_STAGE "+stage+" screen="+(mc.currentScreen==null?"world":mc.currentScreen.getClass().getSimpleName())+ - " position="+(mc.player==null?"none":mc.player.getPosition())+" focus="+mc.isGameFocused()); + System.out.println("GRAPHICAL_STAGE "+stage+" screen="+(mc.screen==null?"world":mc.screen.getClass().getSimpleName())+ + " position="+(mc.player==null?"none":mc.player.blockPosition())+" focus="+mc.isWindowActive()); reportedStage=stage; } - if(ticks>0 && ticks%150==0) ScreenShotHelper.saveScreenshot(mc.gameDir,"diagnostic-stage-"+stage+"-tick-"+ticks+".png",mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); + if(ticks>0 && ticks%150==0) Screenshot.grab(mc.gameDirectory,"diagnostic-stage-"+stage+"-tick-"+ticks+".png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); if(++ticks > 12000) fail(new AssertionError("client timeout stage="+stage)); try { - RuntimeChecks.require(!new File(mc.gameDir,"abort").exists(),"Disposable graphical check aborted"); + RuntimeChecks.require(!new File(mc.gameDirectory,"abort").exists(),"Disposable graphical check aborted"); if(Boolean.getBoolean("lootablebodies.configUiCheck")) {configScreen.tick(mc);return;} if(Boolean.getBoolean("lootablebodies.namelessClientCheck")) {namelessNames.tick(mc);return;} if(Boolean.getBoolean("lootablebodies.visibilitySkinCheck")) {visibilitySkins.tick(mc);return;} if(Boolean.getBoolean("lootablebodies.issueClientCheck")) {reportedIssues.tick(mc);return;} - if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { - mc.gameSettings.pauseOnLostFocus=false; - mc.gameSettings.framerateLimit=30; - mc.gameSettings.renderDistanceChunks=4; + if(stage==0 && mc.screen instanceof TitleScreen) { + mc.options.pauseOnLostFocus=false; + mc.options.framerateLimit=30; + mc.options.renderDistance=4; stage=1; - FixtureApi.ClientWorlds.create(mc,"build-fixture","Build fixture",net.minecraft.world.GameType.CREATIVE,false); - } else if(stage==1 && mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null) { + FixtureApi.ClientWorlds.create(mc,"build-fixture","Build fixture",net.minecraft.world.level.GameType.CREATIVE,false); + } else if(stage==1 && mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null) { stage=2; - pending=mc.getIntegratedServer().runAsync(() -> { - try { RuntimeChecks.run(mc.getIntegratedServer(),"fresh"); } + pending=mc.getSingleplayerServer().submitAsync(() -> { + try { RuntimeChecks.run(mc.getSingleplayerServer(),"fresh"); } catch(Exception error) { throw new RuntimeException(error); } - positionViewer(mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID())); + positionViewer(mc.getSingleplayerServer().getPlayerList().getPlayer(mc.player.getUUID())); }); } else if(stage==2 && pending.isDone()) { pending.get(); NamelessCorpseClientChecks.englishContract(mc); - for(Entity entity : FixtureApi.entities(mc.world)) if(entity instanceof EntityLootableBody && entity.getUniqueID().equals(RuntimeChecks.NEAR)) { - mc.playerController.interactWithEntity(mc.player,entity,Hand.MAIN_HAND); + for(Entity entity : FixtureApi.entities(mc.level)) if(entity instanceof EntityLootableBody && entity.getUUID().equals(RuntimeChecks.NEAR)) { + mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND); stage=3; break; } - } else if(stage==3 && mc.currentScreen instanceof CorpseGUIContainer && ++frames>20) { - RuntimeChecks.require(((CorpseGUIContainer)mc.currentScreen).getContainer().inventorySlots.size()==84,"rendered GUI slots"); - ScreenShotHelper.saveScreenshot(mc.gameDir,"corpse-gui-fresh.png",mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); - mc.player.closeScreen(); stage=4; + } else if(stage==3 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { + RuntimeChecks.require(((CorpseGUIContainer)mc.screen).getMenu().slots.size()==84,"rendered GUI slots"); + Screenshot.grab(mc.gameDirectory,"corpse-gui-fresh.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + mc.player.closeContainer(); stage=4; FixtureConnections.disconnect(mc); } else if(stage==4 && FixtureConnections.stopped(mc)) { - stage=5; mc.loadWorld("build-fixture"); - } else if(stage==5 && mc.player!=null && mc.world!=null && mc.getIntegratedServer()!=null) { + stage=5; mc.loadLevel("build-fixture"); + } else if(stage==5 && mc.player!=null && mc.level!=null && mc.getSingleplayerServer()!=null) { stage=6; - pending=mc.getIntegratedServer().runAsync(() -> { - try { RuntimeChecks.run(mc.getIntegratedServer(),"deferred"); } + pending=mc.getSingleplayerServer().submitAsync(() -> { + try { RuntimeChecks.run(mc.getSingleplayerServer(),"deferred"); } catch(Exception error) { throw new RuntimeException(error); } - positionViewer(mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID())); + positionViewer(mc.getSingleplayerServer().getPlayerList().getPlayer(mc.player.getUUID())); }); } else if(stage==6 && pending.isDone()) { pending.get(); - for(Entity entity:FixtureApi.entities(mc.world)) if(entity instanceof EntityLootableBody && entity.getUniqueID().equals(RuntimeChecks.NEAR)) { - mc.playerController.interactWithEntity(mc.player,entity,Hand.MAIN_HAND); + for(Entity entity:FixtureApi.entities(mc.level)) if(entity instanceof EntityLootableBody && entity.getUUID().equals(RuntimeChecks.NEAR)) { + mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND); stage=7; frames=0; break; } - } else if(stage==7 && mc.currentScreen instanceof CorpseGUIContainer && ++frames>20) { - ScreenShotHelper.saveScreenshot(mc.gameDir,"corpse-gui-reload.png",mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); - mc.player.closeScreen(); stage=20; connected.start(mc); + } else if(stage==7 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { + Screenshot.grab(mc.gameDirectory,"corpse-gui-reload.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + mc.player.closeContainer(); stage=20; connected.start(mc); } else if(stage==20 && connected.tick(mc)) { - mc.player.closeScreen();drops.start(mc);stage=21; + mc.player.closeContainer();drops.start(mc);stage=21; } else if(stage==21 && drops.tick(mc)) { - mc.player.closeScreen();nbt.start(mc);stage=22; + mc.player.closeContainer();nbt.start(mc);stage=22; } else if(stage==22 && nbt.tick(mc)) { - try(FileWriter out=new FileWriter(new File(mc.gameDir,"client.pass"))) { + try(FileWriter out=new FileWriter(new File(mc.gameDirectory,"client.pass"))) { out.write("fresh/reload GUI; connected death/respawn; network shift/normal recovery; two save/reloads verified\n"); } System.out.println("LOOTABLE_BODIES_CLIENT_PASS"); - stage=8; mc.player.closeScreen(); FixtureConnections.disconnect(mc); + stage=8; mc.player.closeContainer(); FixtureConnections.disconnect(mc); } else if(stage==8 && FixtureConnections.stopped(mc)) { - stage=9; mc.shutdown(); + stage=9; mc.stop(); } } catch(Throwable error) { fail(error); } } - private static void positionViewer(net.minecraft.entity.player.ServerPlayerEntity player) { - ServerWorld world=player.getServerWorld(); + private static void positionViewer(net.minecraft.server.level.ServerPlayer player) { + ServerLevel world=player.getLevel(); RuntimeChecks.awaitChunk(world,0,0); for(int x=7;x<=9;x++)for(int z=6;z<=8;z++) - world.setBlockState(new net.minecraft.util.math.BlockPos(x,79,z),net.minecraft.block.Blocks.STONE.getDefaultState()); - RuntimeChecks.require(world.getBlockState(new net.minecraft.util.math.BlockPos(8,79,7)).getBlock()==net.minecraft.block.Blocks.STONE, + world.setBlockAndUpdate(new net.minecraft.core.BlockPos(x,79,z),net.minecraft.world.level.block.Blocks.STONE.defaultBlockState()); + RuntimeChecks.require(world.getBlockState(new net.minecraft.core.BlockPos(8,79,7)).getBlock()==net.minecraft.world.level.block.Blocks.STONE, "Graphical recovery needs a stable floor below the viewing player"); - player.setGameType(GameType.CREATIVE); - player.abilities.allowFlying=true;player.abilities.isFlying=true;player.sendPlayerAbilities(); - player.connection.setPlayerLocation(8.5,80,7.5,0,0); + player.setGameMode(GameType.CREATIVE); + player.getAbilities().mayfly=true;player.getAbilities().flying=true;player.onUpdateAbilities(); + player.connection.teleport(8.5,80,7.5,0,0); } private void fail(Throwable error) { error.printStackTrace(); - Minecraft.getInstance().shutdown(); + Minecraft.getInstance().stop(); stage=99; } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java index 3da9e26..dec961c 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java @@ -1,5 +1,5 @@ package cyano.lootable.buildtest; -import net.minecraft.client.gui.screen.MainMenuScreen; +import net.minecraft.client.gui.screens.TitleScreen; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseConfigGui; @@ -9,75 +9,100 @@ import java.util.concurrent.Future; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.button.Button; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.util.ScreenShotHelper; -import net.minecraft.world.*; -import net.minecraftforge.fml.client.gui.screen.ModListScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.Screenshot; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraftforge.fmlclient.gui.screen.ModListScreen; /** Real native Mods button and widgets, without operating the user's desktop. */ final class ConfigClientChecks { - private int stage,frames,step;private File report;private ModListScreen mods;private CorpseConfigGui root;private Future pending; + private int stage,frames,step,tooltipsChecked;private File report;private ModListScreen mods;private CorpseConfigGui root;private Future pending; + void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Color event) { + Screen screen=Minecraft.getInstance().screen; + if(!(screen instanceof CorpseConfigGui))return; + int width=0,height=event.getComponents().size()==1?-2:0; + for(net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent line:event.getComponents()) { + width=Math.max(width,line.getWidth(event.getFontRenderer()));height+=line.getHeight(); + } + RuntimeChecks.require(event.getX()>=3 && event.getX()+width+3<=screen.width && event.getY()>=4 && event.getY()+height+4<=screen.height, + "Native configuration tooltip clipped: "+event.getX()+","+event.getY()+" "+width+"x"+height+" screen="+screen.width+"x"+screen.height); + tooltipsChecked++; + } void tick(Minecraft mc)throws Exception { if(++frames<20)return;frames=0; - if(report==null)report=new File(mc.gameDir,"config-checks.txt"); - if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; + 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; 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.gameDir,"config-restart.pass").createNewFile();finish(mc);stage=5;return; + check(!LootableBodies.useLocalSkin && LootableBodies.displayNameTag,"appearance_survives_restart");new File(mc.gameDirectory,"config-restart.pass").createNewFile();finish(mc);stage=5;return; } check(LootableBodies.corpseHP==20 && LootableBodies.ticksPerItemDecay==6000,"startup_defaults");openMods(mc);stage=1; } else if(stage==1) { screenshot(mc,"01-mods-config-button.png");openConfig(mc);stage=2; } else if(stage==2) { if(step==0){screenshot(mc,"02-config-root.png");click(root,1);step=1;return;} - if(step==1){screenshot(mc,"03-appearance.png");CorpseConfigGui appearance=(CorpseConfigGui)mc.currentScreen; + if(step==1){screenshot(mc,"03-appearance.png");CorpseConfigGui appearance=(CorpseConfigGui)mc.screen; click(appearance,20);click(appearance,21);click(appearance,201);click(root,200); check(!LootableBodies.useLocalSkin && LootableBodies.displayNameTag,"child_cancel_discards_appearance"); - openConfig(mc);click(root,1);appearance=(CorpseConfigGui)mc.currentScreen;click(appearance,20);click(appearance,21);click(appearance,200);click(root,201); + openConfig(mc);click(root,1);appearance=(CorpseConfigGui)mc.screen;click(appearance,20);click(appearance,21);click(appearance,200);click(root,201); check(!LootableBodies.useLocalSkin && LootableBodies.displayNameTag,"root_cancel_discards_staged_child_changes"); - openConfig(mc);click(root,1);appearance=(CorpseConfigGui)mc.currentScreen;click(appearance,20);click(appearance,21);click(appearance,200);click(root,200); + openConfig(mc);click(root,1);appearance=(CorpseConfigGui)mc.screen;click(appearance,20);click(appearance,21);click(appearance,200);click(root,200); check(LootableBodies.useLocalSkin && !LootableBodies.displayNameTag,"appearance_done_applies_immediately"); openConfig(mc);click(root,2);step=2;return;} - CorpseConfigGui rules=(CorpseConfigGui)mc.currentScreen; + CorpseConfigGui rules=(CorpseConfigGui)mc.screen; if(step==2){screenshot(mc,"04-world-rules-page-one.png"); text(rules,"corpse_HP","31");click(rules,21); - check(field(rules,"corpse_HP").getText().equals("31"),"boolean_toggle_retains_other_pending_text"); - click(rules,5);check(field(rules,"corpse_HP").getText().equals("20.0"),"defaults_restore_health"); - text(rules,"corpse_HP","NaN");click(rules,200);check(mc.currentScreen==rules,"nonfinite_health_is_not_saved");text(rules,"corpse_HP","31");text(rules,"item_decay_rate","0"); + check(field(rules,"corpse_HP").getValue().equals("31"),"boolean_toggle_retains_other_pending_text"); + click(rules,5);check(field(rules,"corpse_HP").getValue().equals("20.0"),"defaults_restore_health"); + text(rules,"corpse_HP","NaN");click(rules,200);check(mc.screen==rules,"nonfinite_health_is_not_saved");text(rules,"corpse_HP","31");text(rules,"item_decay_rate","0"); for(String invalid:Arrays.asList("bad","1:-2","29826:9:43","1:2:3:4")) { - text(rules,"corpse_decay_time",invalid);click(rules,200);check(mc.currentScreen==rules,"invalid_time_not_saved_"+invalid); + text(rules,"corpse_decay_time",invalid);click(rules,200);check(mc.screen==rules,"invalid_time_not_saved_"+invalid); } text(rules,"corpse_decay_time","0:00:10");step=3;return;} screenshot(mc,"04-world-rules-page-two.png");click(rules,200);click(root,200); check(LootableBodies.corpseHP==20 && LootableBodies.corpseDecayTime==72000 && LootableBodies.ticksPerItemDecay==6000,"gameplay_unchanged_until_restart"); - mc.displayGuiScreen(null);FixtureApi.ClientWorlds.create(mc,"config-fixture","Config fixture",net.minecraft.world.GameType.CREATIVE,false);stage=3; - } else if(stage==3 && mc.world!=null && mc.player!=null) { - pending=mc.getIntegratedServer().runAsync(()->{EntityLootableBody body=RuntimeChecks.fixture(mc.getIntegratedServer().getWorld(net.minecraft.world.World.OVERWORLD),UUID.fromString("00000000-0000-4000-8000-000000000941"),8,true);body.setCustomNameVisible(false);try{RuntimeChecks.saveExpected(mc.getIntegratedServer().getWorld(net.minecraft.world.World.OVERWORLD).getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile(),"config-body",body);}catch(Exception failure){throw new RuntimeException(failure);}});stage=4; + mc.setScreen(null);FixtureApi.ClientWorlds.create(mc,"config-fixture","Config fixture",net.minecraft.world.level.GameType.CREATIVE,false);stage=3; + } else if(stage==3 && mc.level!=null && mc.player!=null) { + pending=mc.getSingleplayerServer().submitAsync(()->{EntityLootableBody body=RuntimeChecks.fixture(mc.getSingleplayerServer().getLevel(net.minecraft.world.level.Level.OVERWORLD),UUID.fromString("00000000-0000-4000-8000-000000000941"),8,true);body.setCustomNameVisible(false);try{RuntimeChecks.saveExpected(mc.getSingleplayerServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(),"config-body",body);}catch(Exception failure){throw new RuntimeException(failure);}});stage=4; } else if(stage==4 && pending.isDone()) { pending.get();openMods(mc);openConfig(mc);check(!button(root,2).active,"world_rules_disabled_while_world_open");stage=6; } else if(stage==6) { - screenshot(mc,"05-world-rules-disabled.png");click(root,1);CorpseConfigGui appearance=(CorpseConfigGui)mc.currentScreen;click(appearance,20);click(appearance,21);click(appearance,200);click(root,200); + screenshot(mc,"05-world-rules-disabled.png");click(root,1);CorpseConfigGui appearance=(CorpseConfigGui)mc.screen;click(appearance,20);click(appearance,21);click(appearance,200);click(root,200); check(!LootableBodies.useLocalSkin && LootableBodies.displayNameTag,"appearance_changes_while_world_open"); FixtureConnections.disconnect(mc);stage=5; - } else if(stage==5 && FixtureConnections.stopped(mc))finish(mc); + } else if(stage==5 && FixtureConnections.stopped(mc)) { + check(tooltipsChecked>0,"native_tooltip_bounds_checked_at_left_centre_right");finish(mc); + } } private void openMods(Minecraft mc)throws Exception { - mods=new ModListScreen(new MainMenuScreen());mc.displayGuiScreen(mods);List list=(List)get(mods,"mods"); - for(int i=0;i)entries.getEventListeners()).get(i);mods.getClass().getMethod("setSelected",entry.getClass()).invoke(mods,entry);net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.GuiScreenEvent.DrawScreenEvent.Pre(mods,new com.mojang.blaze3d.matrix.MatrixStack(),0,0,0F));check(button(mods,20).active,"mods_config_button_enabled");return;} + mods=new ModListScreen(new TitleScreen());mc.setScreen(mods);List list=(List)get(mods,"mods"); + for(int i=0;i 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.getEventListeners())if(value instanceof CorpseConfigGui.SettingButton && ((CorpseConfigGui.SettingButton)value).id==id)return (CorpseConfigGui.SettingButton)value;throw new AssertionError("Missing GUI button "+id);} + 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);} - @SuppressWarnings("unchecked") private static TextFieldWidget field(CorpseConfigGui screen,String key)throws Exception { - for(int attempt=0;attempt<10;attempt++){TextFieldWidget 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;} + @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); } - private static void text(CorpseConfigGui screen,String key,String value)throws Exception {field(screen,key).setText(value);} + private static void text(CorpseConfigGui screen,String key,String value)throws Exception {field(screen,key).setValue(value);} private void check(boolean condition,String label)throws Exception {RuntimeChecks.require(condition,label);try(FileWriter out=new FileWriter(report,true)){out.write("PASS "+label+"\n");}System.out.println("CONFIG_CHECK_PASS "+label);} - private static void screenshot(Minecraft mc,String name){ScreenShotHelper.saveScreenshot(mc.gameDir,name,mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} - private static void finish(Minecraft mc)throws Exception {new File(mc.gameDir,"client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_CONFIG_CLIENT_PASS");mc.shutdown();} + @SuppressWarnings("unchecked") private static void screenshot(Minecraft mc,String name)throws Exception { + if(mc.screen instanceof CorpseConfigGui) { + Screen screen=mc.screen;Map rows=(Map)get(screen,"rows"); + Collection positions=rows.isEmpty()?java.util.Collections.singletonList(100):rows.values(); + for(int y:positions)for(int x:new int[]{screen.width/2-150,screen.width/2,screen.width/2+150}) + screen.render(new com.mojang.blaze3d.vertex.PoseStack(),x,y+5,0); + } + Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString())); + } + private static void finish(Minecraft mc)throws Exception {new File(mc.gameDirectory,"client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_CONFIG_CLIENT_PASS");mc.stop();} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java index be1ea62..2e764f5 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java @@ -1,9 +1,9 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.Slot; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.inventory.Inventory; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.SimpleContainer; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -11,19 +11,21 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.DeathScreen; -import net.minecraft.client.gui.screen.MainMenuScreen; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.inventory.container.ClickType; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.client.gui.screens.DeathScreen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.Hand; -import net.minecraft.util.ScreenShotHelper; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.GameType; +import net.minecraft.world.InteractionHand; +import net.minecraft.client.Screenshot; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameType; /** Real connected-player death, container packets and persisted recovery. */ final class ConnectedClientChecks { @@ -31,26 +33,26 @@ private interface Action { void run() throws Exception; } private int stage, frames; private Future pending; private UUID playerId, corpseId; - private CompoundNBT visibleExpected; + private CompoundTag visibleExpected; private File report; void start(Minecraft mc) { - playerId=mc.player.getUniqueID(); - report=new File(mc.gameDir,"connected-checks.txt"); + playerId=mc.player.getUUID(); + report=new File(mc.gameDirectory,"connected-checks.txt"); RuntimeChecks.require(!report.exists(),"Refusing stale connected-client report"); } boolean tick(Minecraft mc) throws Exception { frames++; if(stage==0) { schedule(mc,() -> prepareDeath(mc)); next(1); - } else if(stage==1 && done() && mc.currentScreen instanceof DeathScreen && frames>20) { + } else if(stage==1 && done() && mc.screen instanceof DeathScreen && frames>20) { screenshot(mc,"connected-player-death.png"); - mc.player.respawnPlayer(); next(2); - } else if(stage==2 && mc.player!=null && mc.player.isAlive() && !(mc.currentScreen instanceof DeathScreen)) { + mc.player.respawn(); next(2); + } else if(stage==2 && mc.player!=null && mc.player.isAlive() && !(mc.screen instanceof DeathScreen)) { schedule(mc,() -> { - ServerPlayerEntity player=player(mc); EntityLootableBody corpse=corpse(mc); + ServerPlayer player=player(mc); EntityLootableBody corpse=corpse(mc); RuntimeChecks.require(player.isAlive(),"Connected respawn failed"); - player.connection.setPlayerLocation(24.5,80,7.5,0,0); + player.connection.teleport(24.5,80,7.5,0,0); RuntimeChecks.saveExpected(folder(mc),"connected-death-before",corpse); visibleExpected=read(folder(mc),"connected-death-before"); mark("connected_player_death_and_respawn"); @@ -59,8 +61,8 @@ boolean tick(Minecraft mc) throws Exception { if(open(mc)) next(4); } else if(stage==4 && gui(mc) && frames>20) { verifyGui(mc); screenshot(mc,"connected-corpse-before-recovery.png"); - RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.openContainer.getSlot(6).getStack(),RuntimeChecks.stack(19)),"Client recovery slot NBT differs"); - mc.playerController.windowClick(mc.player.openContainer.windowId,6,0,ClickType.QUICK_MOVE,mc.player); + RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(6).getItem(),RuntimeChecks.stack(19)),"Client recovery slot NBT differs"); + mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,6,0,ClickType.QUICK_MOVE,mc.player); next(5); } else if(stage==5 && frames>30) { schedule(mc,() -> { @@ -69,52 +71,52 @@ boolean tick(Minecraft mc) throws Exception { mark("network_shift_click_exact_nbt"); }); next(6); } else if(stage==6 && done()) { - verifyGui(mc); checkPlayer(mc.player.inventory,19); + verifyGui(mc); checkPlayer(mc.player.getInventory(),19); screenshot(mc,"connected-corpse-after-shift-recovery.png"); quit(mc); next(7); } else if(stage==7 && FixtureConnections.stopped(mc)) { - mc.loadWorld("build-fixture"); next(8); + mc.loadLevel("build-fixture"); next(8); } else if(stage==8 && ready(mc) && frames>30) { schedule(mc,() -> { verifyReload(mc,"connected-after-recovery","connected-player-after-recovery"); - RuntimeChecks.require(player(mc).inventory.mainInventory.get(9).isEmpty(),"Normal-click destination not empty"); + RuntimeChecks.require(player(mc).getInventory().items.get(9).isEmpty(),"Normal-click destination not empty"); mark("first_save_reload_corpse_and_player"); }); next(9); } else if(stage==9 && done()) { if(open(mc)) next(10); } else if(stage==10 && gui(mc) && frames>20) { - verifyGui(mc); checkPlayer(mc.player.inventory,19); + verifyGui(mc); checkPlayer(mc.player.getInventory(),19); screenshot(mc,"connected-corpse-first-reload.png"); - RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.openContainer.getSlot(7).getStack(),RuntimeChecks.stack(20)),"Client normal-click slot NBT differs"); - int window=mc.player.openContainer.windowId; - mc.playerController.windowClick(window,7,0,ClickType.PICKUP,mc.player); - mc.playerController.windowClick(window,48,0,ClickType.PICKUP,mc.player); + RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(7).getItem(),RuntimeChecks.stack(20)),"Client normal-click slot NBT differs"); + int window=mc.player.containerMenu.containerId; + mc.gameMode.handleInventoryMouseClick(window,7,0,ClickType.PICKUP,mc.player); + mc.gameMode.handleInventoryMouseClick(window,48,0,ClickType.PICKUP,mc.player); next(11); } else if(stage==11 && frames>30) { schedule(mc,() -> { verifyRecovery(mc,"connected-after-recovery",7,20,"connected-final"); - checkPlayer(player(mc).inventory,19); - RuntimeChecks.require((player(mc).inventory.getItemStack()).isEmpty(),"Server cursor retained a duplicate"); + checkPlayer(player(mc).getInventory(),19); + RuntimeChecks.require(player(mc).containerMenu.getCarried().isEmpty(),"Server cursor retained a duplicate"); savePlayer(mc,"connected-player-final"); mark("network_normal_click_exact_nbt"); }); next(12); } else if(stage==12 && done()) { - verifyGui(mc); checkPlayer(mc.player.inventory,19);checkPlayer(mc.player.inventory,20); - RuntimeChecks.require((mc.player.inventory.getItemStack()).isEmpty(),"Client cursor retained a duplicate"); + verifyGui(mc); checkPlayer(mc.player.getInventory(),19);checkPlayer(mc.player.getInventory(),20); + RuntimeChecks.require((mc.player.containerMenu.getCarried()).isEmpty(),"Client cursor retained a duplicate"); screenshot(mc,"connected-corpse-after-normal-recovery.png"); quit(mc); next(13); } else if(stage==13 && FixtureConnections.stopped(mc)) { - mc.loadWorld("build-fixture"); next(14); + mc.loadLevel("build-fixture"); next(14); } else if(stage==14 && ready(mc) && frames>30) { schedule(mc,() -> { verifyReload(mc,"connected-final","connected-player-final"); - checkPlayer(player(mc).inventory,19);checkPlayer(player(mc).inventory,20); + checkPlayer(player(mc).getInventory(),19);checkPlayer(player(mc).getInventory(),20); mark("second_save_reload_corpse_and_player"); }); next(15); } else if(stage==15 && done()) { if(open(mc)) next(16); } else if(stage==16 && gui(mc) && frames>20) { - verifyGui(mc); checkPlayer(mc.player.inventory,19);checkPlayer(mc.player.inventory,20); + verifyGui(mc); checkPlayer(mc.player.getInventory(),19);checkPlayer(mc.player.getInventory(),20); screenshot(mc,"connected-corpse-final-reload.png"); System.out.println("LOOTABLE_BODIES_CONNECTED_CLIENT_PASS"); return true; @@ -122,87 +124,87 @@ boolean tick(Minecraft mc) throws Exception { return false; } private void prepareDeath(Minecraft mc) throws Exception { - MinecraftServer server=mc.getIntegratedServer(); ServerPlayerEntity player=player(mc); + MinecraftServer server=mc.getSingleplayerServer(); ServerPlayer player=player(mc); for(int x=21;x<=27;x++) for(int z=5;z<=11;z++) - player.world.setBlockState(new BlockPos(x,79,z),Blocks.STONE.getDefaultState()); - player.world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(false,null); - player.world.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_SPAWNING).set(false,null); - player.setGameType(GameType.SURVIVAL); player.inventory.clear();player.inventory.currentItem=0; - for(int slot=0;slot<6;slot++)player.setItemStackToSlot(EntityLootableBody.EQUIPMENT_SLOTS[slot],RuntimeChecks.stack(slot)); - for(int slot=9;slot<=11;slot++)player.inventory.mainInventory.set(slot,RuntimeChecks.stack(slot+10)); - player.connection.setPlayerLocation(24.5,80,8.5,0,0); - player.container.detectAndSendChanges(); player.onKillCommand(); + player.level.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); + player.level.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); + player.level.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); + player.setGameMode(GameType.SURVIVAL); player.getInventory().clearContent();player.getInventory().selected=0; + for(int slot=0;slot<6;slot++)player.setItemSlot(EntityLootableBody.EQUIPMENT_SLOTS[slot],RuntimeChecks.stack(slot)); + for(int slot=9;slot<=11;slot++)player.getInventory().items.set(slot,RuntimeChecks.stack(slot+10)); + player.connection.teleport(24.5,80,8.5,0,0); + player.containerMenu.broadcastChanges(); player.kill(); RuntimeChecks.require(!player.isAlive(),"Connected player did not die"); EntityLootableBody corpse=null; - for(Entity entity:FixtureApi.entities(player.world)) + for(Entity entity:FixtureApi.entities(player.level)) if(entity instanceof EntityLootableBody && player.getGameProfile().getName().equals(((EntityLootableBody)entity).getCustomNameTag())) corpse=(EntityLootableBody)entity; RuntimeChecks.require(corpse!=null,"Connected death did not create a corpse"); - corpseId=corpse.getUniqueID();corpse.setNoGravity(true);corpse.setNoAI(true);corpse.setMotion(0,corpse.getMotion().y,corpse.getMotion().z);corpse.setMotion(corpse.getMotion().x,0,corpse.getMotion().z);corpse.setMotion(corpse.getMotion().x,corpse.getMotion().y,0); + corpseId=corpse.getUUID();corpse.setNoGravity(true);corpse.setNoAi(true);corpse.setDeltaMovement(0,corpse.getDeltaMovement().y,corpse.getDeltaMovement().z);corpse.setDeltaMovement(corpse.getDeltaMovement().x,0,corpse.getDeltaMovement().z);corpse.setDeltaMovement(corpse.getDeltaMovement().x,corpse.getDeltaMovement().y,0); RuntimeChecks.disableVacuum(corpse); - for(int slot=0;slot<6;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getStackInSlot(slot),RuntimeChecks.stack(slot)),"Connected death equipment slot "+slot); - for(int slot=6;slot<=8;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getStackInSlot(slot),RuntimeChecks.stack(slot+13)),"Connected death main slot "+slot); - RuntimeChecks.require(player.inventory.write(new ListNBT()).size()==0,"Death retained player inventory alongside corpse"); + for(int slot=0;slot<6;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getItem(slot),RuntimeChecks.stack(slot)),"Connected death equipment slot "+slot); + for(int slot=6;slot<=8;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getItem(slot),RuntimeChecks.stack(slot+13)),"Connected death main slot "+slot); + RuntimeChecks.require(player.getInventory().save(new ListTag()).size()==0,"Death retained player inventory alongside corpse"); System.out.println("CONNECTED_CORPSE_UUID "+corpseId); } private void verifyRecovery(Minecraft mc,String before,int slot,int item,String after) throws Exception { EntityLootableBody corpse=corpse(mc); - RuntimeChecks.require((corpse.getStackInSlot(slot)).isEmpty(),"Network recovery left source stack"); - checkPlayer(player(mc).inventory,item); - CompoundNBT expected=read(folder(mc),before),actual=new CompoundNBT(); - ListNBT equipment=expected.getList("Equipment",10); + RuntimeChecks.require((corpse.getItem(slot)).isEmpty(),"Network recovery left source stack"); + checkPlayer(player(mc).getInventory(),item); + CompoundTag expected=read(folder(mc),before),actual=new CompoundTag(); + ListTag equipment=expected.getList("Equipment",10); for(int i=equipment.size()-1;i>=0;i--)if(equipment.getCompound(i).getByte("Slot")==slot)equipment.remove(i); - corpse.writeUnlessPassenger(actual);RuntimeChecks.compare(expected,actual); + corpse.saveAsPassenger(actual);RuntimeChecks.compare(expected,actual); RuntimeChecks.saveExpected(folder(mc),after,corpse);visibleExpected=actual.copy(); } private void verifyReload(Minecraft mc,String corpseName,String playerName) throws Exception { - mc.getIntegratedServer().getWorld(FixtureApi.dimensionKey(0)).getChunk(1,0); + RuntimeChecks.awaitChunk(mc.getSingleplayerServer().getLevel(FixtureApi.dimensionKey(0)),1,0); EntityLootableBody corpse=corpse(mc);RuntimeChecks.require(corpse.isAlive(),"Saved corpse is no longer accessible"); corpse.baseTick();RuntimeChecks.verifyExpected(folder(mc),corpseName,corpse); - CompoundNBT expected=read(folder(mc),playerName); - RuntimeChecks.require(expected.get("Inventory").equals(player(mc).inventory.write(new ListNBT())),"Recovered player inventory changed on disk reload"); - player(mc).connection.setPlayerLocation(24.5,80,7.5,0,0); + CompoundTag expected=read(folder(mc),playerName); + RuntimeChecks.require(expected.get("Inventory").equals(player(mc).getInventory().save(new ListTag())),"Recovered player inventory changed on disk reload"); + player(mc).connection.teleport(24.5,80,7.5,0,0); visibleExpected=read(folder(mc),corpseName); } private void savePlayer(Minecraft mc,String name) throws Exception { - CompoundNBT expected=new CompoundNBT();expected.put("Inventory",player(mc).inventory.write(new ListNBT())); - try(FileOutputStream out=new FileOutputStream(new File(folder(mc),name+".nbt"))){CompressedStreamTools.writeCompressed(expected,out);} - mc.getIntegratedServer().getPlayerList().saveAllPlayerData();mc.getIntegratedServer().save(false,true,false); + CompoundTag expected=new CompoundTag();expected.put("Inventory",player(mc).getInventory().save(new ListTag())); + try(FileOutputStream out=new FileOutputStream(new File(folder(mc),name+".nbt"))){NbtIo.writeCompressed(expected,out);} + mc.getSingleplayerServer().getPlayerList().saveAll();mc.getSingleplayerServer().saveAllChunks(false,true,false); } private void verifyGui(Minecraft mc) { - RuntimeChecks.require(gui(mc) && mc.player.openContainer.inventorySlots.size()==84,"Connected GUI missing slots"); - ItemStack[] expected=new ItemStack[48];ListNBT list=visibleExpected.getList("Equipment",10); - for(int i=0;i {try{action.run();}catch(Exception error){throw new RuntimeException(error);}});} + private void schedule(Minecraft mc,Action action){pending=mc.getSingleplayerServer().submitAsync(() -> {try{action.run();}catch(Exception error){throw new RuntimeException(error);}});} private boolean done() throws Exception {if(!pending.isDone())return false;pending.get();return true;} private void next(int value){stage=value;frames=0;System.out.println("CONNECTED_CLIENT_STAGE "+stage);} - private static boolean ready(Minecraft mc){return mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null;} - private static boolean gui(Minecraft mc){return mc.currentScreen instanceof CorpseGUIContainer;} + private static boolean ready(Minecraft mc){return mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null;} + private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} private boolean open(Minecraft mc){ - for(Entity entity:FixtureApi.entities(mc.world))if(entity instanceof EntityLootableBody && entity.getUniqueID().equals(corpseId)){ - mc.playerController.interactWithEntity(mc.player,entity,Hand.MAIN_HAND);return true; + for(Entity entity:FixtureApi.entities(mc.level))if(entity instanceof EntityLootableBody && entity.getUUID().equals(corpseId)){ + mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND);return true; } return false; } - private static void screenshot(Minecraft mc,String name){ScreenShotHelper.saveScreenshot(mc.gameDir,name,mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} - private static void quit(Minecraft mc){mc.player.closeScreen();FixtureConnections.disconnect(mc);} + private static void screenshot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} private void mark(String name) throws Exception { try(FileWriter out=new FileWriter(report,true)){out.write("PASS "+name+"\n");} System.out.println("CLIENT_CHECK_PASS "+name); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java index 6fc62a8..03ead60 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java @@ -1,16 +1,15 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.ClickType; -import net.minecraft.inventory.container.Container; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.network.play.client.CCreativeInventoryActionPacket; -import net.minecraft.network.play.client.CCloseWindowPacket; -import net.minecraft.network.play.client.CClickWindowPacket; -import net.minecraft.network.play.server.SSetSlotPacket; -import net.minecraft.network.play.server.SWindowItemsPacket; -import net.minecraft.network.play.server.SConfirmTransactionPacket; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.network.protocol.game.ServerboundSetCreativeModeSlotPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket; +import net.minecraft.network.protocol.game.ServerboundContainerClickPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import com.mojang.authlib.GameProfile; import cyano.lootable.entities.EntityLootableBody; @@ -18,18 +17,25 @@ import io.netty.buffer.Unpooled; import java.io.*; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.inventory.*; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; -import net.minecraft.network.*; -import net.minecraft.network.play.client.*; -import net.minecraft.network.play.server.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.game.ServerboundContainerClickPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket; +import net.minecraft.network.protocol.game.ServerboundSetCreativeModeSlotPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.*; +import net.minecraft.world.level.GameType; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.EntityJoinWorldEvent; import net.minecraftforge.event.entity.item.ItemTossEvent; @@ -39,12 +45,11 @@ final class ContainerInteractionChecks { private interface Check { void run() throws Exception; } private static MinecraftServer server; - private static ServerWorld world; + private static ServerLevel world; private static int sequence, failures; - private static short transaction; private static final List results=new ArrayList<>(); - static void run(MinecraftServer instance,ServerWorld target,File folder) throws Exception { + static void run(MinecraftServer instance,ServerLevel target,File folder) throws Exception { server=instance;world=target;failures=0;results.clear(); check("hotbar_all_nine_empty_and_occupied",ContainerInteractionChecks::hotbar); check("hotbar_armor_validation_and_limits",ContainerInteractionChecks::armorSwap); @@ -80,7 +85,7 @@ static void run(MinecraftServer instance,ServerWorld target,File folder) throws System.out.println("LOOTABLE_BODIES_CONTAINER_PASS checks="+results.size()); } - static void runDrops(MinecraftServer instance,ServerWorld target,File folder) throws Exception { + static void runDrops(MinecraftServer instance,ServerLevel target,File folder) throws Exception { server=instance;world=target;failures=0;results.clear(); for(boolean creative:new boolean[]{false,true}) for(int source=0;source<3;source++) for(int button=0;button<2;button++) for(int rejection=0;rejection<3;rejection++) { @@ -117,22 +122,22 @@ static void runDrops(MinecraftServer instance,ServerWorld target,File folder) th System.out.println("LOOTABLE_BODIES_DROP_CREATIVE_PASS checks="+results.size()); } private static void toolThrow(int rejection) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);b.setInventorySlotContents(4,item(4,1));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);b.setItem(4,item(4,1));Map before=total(b,p); RejectedDrops reject=new RejectedDrops();reject.toss=rejection==1;reject.join=rejection==2;MinecraftForge.EVENT_BUS.register(reject); try{packet(p,4,0,ClickType.THROW,null);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} - conserved(before,b,p);equal(b.getStackInSlot(4),rejection==0?null:item(4,1),"Equipment throw changed damage/enchantment/custom data"); + conserved(before,b,p);equal(b.getItem(4),rejection==0?null:item(4,1),"Equipment throw changed damage/enchantment/custom data"); } private static void interruptDrag(boolean outside) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);b.setInventorySlotContents(6,item(120,17));p.inventory.setItemStack(item(121,10));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);b.setItem(6,item(120,17));p.containerMenu.setCarried(item(121,10));Map before=total(b,p); packet(p,-999,0,ClickType.QUICK_CRAFT,null);packet(p,7,1,ClickType.QUICK_CRAFT,null); packet(p,outside?-999:6,0,outside?ClickType.PICKUP:ClickType.THROW,null); - equal(p.inventory.getItemStack(),item(121,10),"Interrupted drag dropped cursor");equal(b.getStackInSlot(6),item(120,17),"Interrupted drag threw source");conserved(before,b,p); + equal(p.containerMenu.getCarried(),item(121,10),"Interrupted drag dropped cursor");equal(b.getItem(6),item(120,17),"Interrupted drag threw source");conserved(before,b,p); } private static void throwing(boolean creative,int source,int button,int rejection,boolean capture) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL); + EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL); fill(b);fill(p);open(p,b);ItemStack original=item(120,17); - if(source==0)b.setInventorySlotContents(6,original.copy()); - else if(source==1)p.inventory.mainInventory.set(9,original.copy());else p.inventory.setItemStack(original.copy()); + if(source==0)b.setItem(6,original.copy()); + else if(source==1)p.getInventory().items.set(9,original.copy());else p.containerMenu.setCarried(original.copy()); if(capture){p.captureDrops(new java.util.ArrayList<>());p.captureDrops().add(new ItemEntity(world,96.5,80,96.5,item(121,11)));} Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=rejection==1;reject.join=rejection==2; RecordingConnection connection=(RecordingConnection)p.connection;int initialSyncs=connection.resyncs; @@ -142,63 +147,67 @@ private static void throwing(boolean creative,int source,int button,int rejectio conserved(before,b,p);RuntimeChecks.require((p.captureDrops()!=null)==capture,"Toss changed capture state"); int amount=source==2?(button==0?17:1):(button==0?1:17); ItemStack remaining=rejection==0 && amount==17?null:item(120,rejection==0?17-amount:17); - equal(source==2?p.inventory.getItemStack():p.openContainer.getSlot(source==0?6:48).getStack(),remaining,"Drop quantity/source restoration differs"); + equal(source==2?p.containerMenu.getCarried():p.containerMenu.getSlot(source==0?6:48).getItem(),remaining,"Drop quantity/source restoration differs"); if(rejection!=0)RuntimeChecks.require(connection.resyncs>initialSyncs,"Rejected predicted drop did not resync initiating client"); if(capture)RuntimeChecks.require(p.captureDrops().size()==2,"Toss discarded pre-existing captured item"); if(rejection==0 && !capture){ - int dropped=0;for(Entity e:FixtureApi.entities(world))if(e instanceof ItemEntity && !e.removed){equal(((ItemEntity)e).getItem(),item(120,amount),"Accepted drop NBT differs");dropped++;} + int dropped=0;for(Entity e:FixtureApi.entities(world))if(e instanceof ItemEntity && !e.isRemoved()){equal(((ItemEntity)e).getItem(),item(120,amount),"Accepted drop NBT differs");dropped++;} RuntimeChecks.require(dropped==1,"Accepted toss did not create exactly one entity"); } } private static void creativeFallback(boolean toss,boolean swap) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();fill(b);fill(p);p.setGameType(GameType.CREATIVE);open(p,b); - if(swap){b.setInventorySlotContents(0,item(0,1));p.inventory.mainInventory.set(0,pumpkins(64));} - else p.inventory.setItemStack(item(120,17)); + EntityLootableBody b=body();ServerPlayer p=player();fill(b);fill(p);p.setGameMode(GameType.CREATIVE);open(p,b); + if(swap){b.setItem(0,item(0,1));p.getInventory().items.set(0,pumpkins(64));} + else p.containerMenu.setCarried(item(120,17)); Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=toss;reject.join=!toss;MinecraftForge.EVENT_BUS.register(reject); try{if(swap)packet(p,0,0,ClickType.SWAP,null);else close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} conserved(before,b,p);RuntimeChecks.require(b.isAlive(),"Creative fallback owner no longer alive"); } private static void collect(int mode) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player(),q=player();open(p,b);open(q,b); - b.setInventorySlotContents(6,item(120,20));b.setInventorySlotContents(7,item(120,30));b.setInventorySlotContents(8,item(121,14)); - ItemStack differentDamage=item(120,12);differentDamage.setDamage((differentDamage.getDamage()+1)%16);b.setInventorySlotContents(9,differentDamage); - p.inventory.mainInventory.set(9,item(120,10));p.inventory.setItemStack(mode==3?ItemStack.EMPTY:item(120,mode==2?64:5)); - if(mode==4)p.setGameType(GameType.CREATIVE); - if(mode==5){q.inventory.setItemStack(item(120,4));packet(q,10,0,ClickType.PICKUP_ALL,null);} + EntityLootableBody b=body();ServerPlayer p=player(),q=player();open(p,b);open(q,b); + b.setItem(6,item(120,20));b.setItem(7,item(120,30));b.setItem(8,item(121,14)); + ItemStack differentDamage=item(120,12);differentDamage.setDamageValue((differentDamage.getDamageValue()+1)%16);b.setItem(9,differentDamage); + p.getInventory().items.set(9,item(120,10));p.containerMenu.setCarried(mode==3?ItemStack.EMPTY:item(120,mode==2?64:5)); + if(mode==4)p.setGameMode(GameType.CREATIVE); + if(mode==5){q.containerMenu.setCarried(item(120,4));packet(q,10,0,ClickType.PICKUP_ALL,null);} Map before=total(b,p,q);int wanted=mode==3?0:mode==2?64:mode==5?15:64; packet(p,10,mode==1?1:0,ClickType.PICKUP_ALL,null); - RuntimeChecks.require(((p.inventory.getItemStack()).isEmpty()?0:p.inventory.getItemStack().getCount())==wanted,"Double click count/capacity differs"); - equal(b.getStackInSlot(8),item(121,14),"Double click collected different custom NBT");equal(b.getStackInSlot(9),differentDamage,"Double click collected different damage"); - if(mode==0)equal(p.inventory.mainInventory.get(9),item(120,1),"Forward collection order differs"); - if(mode==1)equal(b.getStackInSlot(6),item(120,1),"Reverse collection order differs"); + RuntimeChecks.require(((p.containerMenu.getCarried()).isEmpty()?0:p.containerMenu.getCarried().getCount())==wanted,"Double click count/capacity differs"); + equal(b.getItem(8),item(121,14),"Double click collected different custom NBT");equal(b.getItem(9),differentDamage,"Double click collected different damage"); + if(mode==0)equal(p.getInventory().items.get(9),item(120,1),"Forward collection order differs"); + if(mode==1)equal(b.getItem(6),item(120,1),"Reverse collection order differs"); conserved(before,b,p,q); } private static void fallbackOrder(boolean offhand) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b); - if(offhand){fill(p);p.inventory.offHandInventory.set(0,item(120,60));} - p.inventory.setItemStack(item(120,10));Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=true;MinecraftForge.EVENT_BUS.register(reject); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b); + if(offhand){fill(p);p.getInventory().offhand.set(0,item(120,60));} + p.containerMenu.setCarried(item(120,10));Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=true;MinecraftForge.EVENT_BUS.register(reject); try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} - equal(p.inventory.getStackInSlot(offhand?40:0),item(120,offhand?64:10),"Rejected-drop fallback changed vanilla player insertion priority");conserved(before,b,p); + equal(p.getInventory().getItem(offhand?40:0),item(120,offhand?64:10),"Rejected-drop fallback changed vanilla player insertion priority");conserved(before,b,p); } private static void cloneStack(boolean creative,boolean occupied) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setInventorySlotContents(6,item(120,17)); - if(occupied)p.inventory.setItemStack(item(121,4));Map expected=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setItem(6,item(120,17)); + if(occupied)p.containerMenu.setCarried(item(121,4));Map expected=total(b,p); if(creative && !occupied)add(expected,item(120,64));packet(p,6,2,ClickType.CLONE,null); - equal(b.getStackInSlot(6),item(120,17),"Creative clone removed source");equal(p.inventory.getItemStack(),occupied?item(121,4):creative?item(120,64):null,"Clone cursor/NBT differs");conserved(expected,b,p); + equal(b.getItem(6),item(120,17),"Creative clone removed source");equal(p.containerMenu.getCarried(),occupied?item(121,4):creative?item(120,64):null,"Clone cursor/NBT differs");conserved(expected,b,p); } private static void creativeDrag(boolean creative) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);p.inventory.setItemStack(pumpkins(64));Map expected=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);p.containerMenu.setCarried(pumpkins(64));Map expected=total(b,p); if(creative){add(expected,pumpkins(65));} // 1 armor + two main stacks, with original cursor consumed by vanilla 1.10. - drag(p,2,0,6,7);equal(b.getStackInSlot(0),creative?pumpkins(1):null,"Creative fill armor capacity differs"); - equal(b.getStackInSlot(6),creative?pumpkins(64):null,"Creative fill main capacity differs");equal(b.getStackInSlot(7),creative?pumpkins(64):null,"Creative fill second main differs");conserved(expected,b,p); + drag(p,2,0,6,7);equal(b.getItem(0),creative?pumpkins(1):null,"Creative fill armor capacity differs"); + equal(b.getItem(6),creative?pumpkins(64):null,"Creative fill main capacity differs");equal(b.getItem(7),creative?pumpkins(64):null,"Creative fill second main differs");conserved(expected,b,p); } private static void creativePacket(boolean creative,boolean cancelled) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setInventorySlotContents(6,item(120,17));CompoundNBT unchanged=data(b); - RejectedDrops reject=new RejectedDrops();reject.toss=cancelled;MinecraftForge.EVENT_BUS.register(reject);PacketBuffer wire=new PacketBuffer(Unpooled.buffer()); - try{wire.writeShort(cancelled?-1:9);wire.writeItemStack(item(121,23));CCreativeInventoryActionPacket packet=new CCreativeInventoryActionPacket();packet.readPacketData(wire);p.connection.processCreativeInventoryAction(packet);} + EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setItem(6,item(120,17));CompoundTag unchanged=data(b); + RejectedDrops reject=new RejectedDrops();reject.toss=cancelled;MinecraftForge.EVENT_BUS.register(reject);FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + try { + new ServerboundSetCreativeModeSlotPacket(cancelled?-1:9,item(121,23)).write(wire); + p.connection.handleSetCreativeModeSlot(new ServerboundSetCreativeModeSlotPacket(wire)); + RuntimeChecks.require(!wire.isReadable(),"Native creative packet left undecoded fields"); + } finally{wire.release();MinecraftForge.EVENT_BUS.unregister(reject);} - RuntimeChecks.require(unchanged.equals(data(b)),"Creative inventory packet changed corpse instead of player");equal(p.inventory.mainInventory.get(9),creative&&!cancelled?item(121,23):null,"Creative player packet permission/target differs"); - if(cancelled)RuntimeChecks.require(FixtureApi.entities(world).stream().noneMatch(e->e instanceof ItemEntity && !e.removed),"Cancelled creative minted drop spawned anyway"); + RuntimeChecks.require(unchanged.equals(data(b)),"Creative inventory packet changed corpse instead of player");equal(p.getInventory().items.get(9),creative&&!cancelled?item(121,23):null,"Creative player packet permission/target differs"); + if(cancelled)RuntimeChecks.require(FixtureApi.entities(world).stream().noneMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cancelled creative minted drop spawned anyway"); } private static void check(String name,Check test) { @@ -207,125 +216,136 @@ private static void check(String name,Check test) { 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);} } - private static ServerPlayerEntity player() { - ServerPlayerEntity p=new ServerPlayerEntity(server,world, - new GameProfile(UUID.nameUUIDFromBytes(("container-player-"+sequence++).getBytes(java.nio.charset.StandardCharsets.UTF_8)),"ContainerFixture"), - new net.minecraft.server.management.PlayerInteractionManager(world)); - p.connection=new RecordingConnection(p);p.setGameType(GameType.SURVIVAL); - p.setPosition(96.5,80,96.5);world.getChunk(6,6);return p; + private static ServerPlayer player() { + ServerPlayer p=new ServerPlayer(server,world, + new GameProfile(UUID.nameUUIDFromBytes(("container-player-"+sequence++).getBytes(java.nio.charset.StandardCharsets.UTF_8)),"ContainerFixture")); + p.connection=new RecordingConnection(p);p.setGameMode(GameType.SURVIVAL); + p.setPos(96.5,80,96.5);world.getChunk(6,6);return p; } private static EntityLootableBody body(){ - EntityLootableBody b=new EntityLootableBody(world);b.setPosition(96.5,80,96.5);b.setNoGravity(true);RuntimeChecks.disableVacuum(b); - world.getChunk(6,6);RuntimeChecks.require(world.addEntity(b),"Container corpse spawn");return b; + EntityLootableBody b=new EntityLootableBody(world);b.setPos(96.5,80,96.5);b.setNoGravity(true);RuntimeChecks.disableVacuum(b); + world.getChunk(6,6);RuntimeChecks.require(world.addFreshEntity(b),"Container corpse spawn");return b; } - private static CorpseContainer open(ServerPlayerEntity p,EntityLootableBody b){ - CorpseContainer c=new CorpseContainer(7,p.inventory,b);p.openContainer=c;c.addListener(p);return c; + private static CorpseContainer open(ServerPlayer p,EntityLootableBody b){ + CorpseContainer c=new CorpseContainer(7,p.getInventory(),b);p.containerMenu=c;p.initMenu(c);return c; } private static ItemStack item(int id,int count){ItemStack s=RuntimeChecks.stack(id);s.setCount(count);return s;} private static ItemStack pumpkins(int count){ItemStack s=new ItemStack(Blocks.CARVED_PUMPKIN,count);s.setTag(item(120,1).getTag().copy());return s;} - private static void fill(ServerPlayerEntity p){for(int i=0;i<36;i++)p.inventory.mainInventory.set(i,item(100+i,64));} - private static void fill(EntityLootableBody b){for(int i=0;i<48;i++)b.setInventorySlotContents(i,item(i,i<6?1:64));b.addItem(item(70,31));} - private static void packet(ServerPlayerEntity p,int slot,int button,ClickType type,ItemStack predicted) throws Exception { - PacketBuffer wire=new PacketBuffer(Unpooled.buffer()); - try{ - wire.writeByte(p.openContainer.windowId);wire.writeShort(slot);wire.writeByte(button);wire.writeShort(++transaction); - wire.writeEnumValue(type);wire.writeItemStack(predicted == null ? ItemStack.EMPTY : predicted); - CClickWindowPacket packet=new CClickWindowPacket();packet.readPacketData(wire);p.connection.processClickWindow(packet); - } finally{wire.release();} - } - private static void click(ServerPlayerEntity p,int slot,int button,ClickType type) throws Exception { + private static void fill(ServerPlayer p){for(int i=0;i<36;i++)p.getInventory().items.set(i,item(100+i,64));} + private static void fill(EntityLootableBody b){for(int i=0;i<48;i++)b.setItem(i,item(i,i<6?1:64));b.addItem(item(70,31));} + private static void packet(ServerPlayer p,int slot,int button,ClickType type,ItemStack predicted) throws Exception { + packetWithState(p,slot,button,type,predicted,p.containerMenu.getStateId()); + } + private static void packetWithState(ServerPlayer p,int slot,int button,ClickType type,ItemStack predicted,int stateId) throws Exception { + it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap changed=new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); + if(predicted!=null && slot>=0)changed.put(slot,predicted.copy()); + // These server-side fixtures deliberately report pre-action predictions; + // native broadcasts must repair them. Real clients cover normal prediction. + ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket( + p.containerMenu.containerId,stateId,slot,button,type,p.containerMenu.getCarried().copy(),changed); + FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + try { + outgoing.write(wire);ServerboundContainerClickPacket decoded=new ServerboundContainerClickPacket(wire); + RuntimeChecks.require(!wire.isReadable(),"Native menu packet left undecoded fields"); + p.connection.handleContainerClick(decoded); + } finally {wire.release();} + } + private static void click(ServerPlayer p,int slot,int button,ClickType type) throws Exception { ItemStack predicted=null; if((type==ClickType.PICKUP || type==ClickType.QUICK_MOVE) && slot>=0){ - ItemStack s=p.openContainer.getSlot(slot).getStack();predicted=s.copy(); + ItemStack s=p.containerMenu.getSlot(slot).getItem();predicted=s.copy(); } packet(p,slot,button,type,predicted); } - private static void close(ServerPlayerEntity p) throws Exception { - PacketBuffer wire=new PacketBuffer(Unpooled.buffer()); - try{wire.writeByte(p.openContainer.windowId);CCloseWindowPacket packet=new CCloseWindowPacket();packet.readPacketData(wire);p.connection.processCloseWindow(packet);} - finally{wire.release();} + private static void close(ServerPlayer p) throws Exception { + FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + try { + new ServerboundContainerClosePacket(p.containerMenu.containerId).write(wire); + p.connection.handleContainerClose(new ServerboundContainerClosePacket(wire)); + RuntimeChecks.require(!wire.isReadable(),"Native close packet left undecoded fields"); + } finally {wire.release();} } - private static void drag(ServerPlayerEntity p,int mode,int... slots) throws Exception { + private static void drag(ServerPlayer p,int mode,int... slots) throws Exception { packet(p,-999,mode<<2,ClickType.QUICK_CRAFT,null); for(int slot:slots)packet(p,slot,1|(mode<<2),ClickType.QUICK_CRAFT,null); packet(p,-999,2|(mode<<2),ClickType.QUICK_CRAFT,null); } private static void equal(ItemStack actual,ItemStack expected,String reason){RuntimeChecks.require(RuntimeChecks.sameStack(actual == null ? ItemStack.EMPTY : actual,expected == null ? ItemStack.EMPTY : expected),reason);} - private static CompoundNBT data(EntityLootableBody b){CompoundNBT n=new CompoundNBT();b.writeAdditional(n);return n;} + private static CompoundTag data(EntityLootableBody b){CompoundTag n=new CompoundTag();b.addAdditionalSaveData(n);return n;} private static void add(Map all,ItemStack s){ - if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundNBT n=s.write(new CompoundNBT());n.remove("Count"); + if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=s.save(new CompoundTag());n.remove("Count"); String key=n.toString();all.put(key,all.getOrDefault(key,0)+s.getCount()); } - private static Map total(EntityLootableBody b,ServerPlayerEntity... players){ + private static Map total(EntityLootableBody b,ServerPlayer... players){ Map all=new TreeMap<>(); - for(int i=0;i<48;i++)add(all,b.getStackInSlot(i));ListNBT aux=data(b).getList("Aux",10); - for(int i=0;iemptyList():p.captureDrops())add(all,e.getItem());} - for(Entity e:FixtureApi.entities(world))if(e instanceof ItemEntity && !e.removed)add(all,((ItemEntity)e).getItem()); + for(int i=0;i<48;i++)add(all,b.getItem(i));ListTag aux=data(b).getList("Aux",10); + for(int i=0;iemptyList():p.captureDrops())add(all,e.getItem());} + for(Entity e:FixtureApi.entities(world))if(e instanceof ItemEntity && !e.isRemoved())add(all,((ItemEntity)e).getItem()); return all; } - private static void conserved(Map expected,EntityLootableBody b,ServerPlayerEntity... players){ + private static void conserved(Map expected,EntityLootableBody b,ServerPlayer... players){ RuntimeChecks.require(expected.equals(total(b,players)),"Item count/type/damage/custom NBT lost or duplicated"); - CompoundNBT before=data(b),immutable=before.copy();EntityLootableBody copy=new EntityLootableBody(world);copy.readAdditional(before); + CompoundTag before=data(b),immutable=before.copy();EntityLootableBody copy=new EntityLootableBody(world);copy.readAdditionalSaveData(before); RuntimeChecks.require(Objects.equals(before.get("Equipment"),data(copy).get("Equipment")) && Objects.equals(before.get("Aux"),data(copy).get("Aux")),"Post-interaction NBT round trip changed inventory"); RuntimeChecks.require(before.equals(immutable),"Post-interaction reader mutated saved input"); } private static void hotbar() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b); for(int key=0;key<9;key++){ - p.inventory.mainInventory.set(key,item(30+key,13));Map before=total(b,p); - click(p,6,key,ClickType.SWAP);equal(b.getStackInSlot(6),item(30+key,13),"Empty hotbar swap failed");equal(p.inventory.mainInventory.get(key),null,"Hotbar source remained"); - click(p,6,key,ClickType.SWAP);equal(b.getStackInSlot(6),null,"Hotbar return left source"); - b.setInventorySlotContents(6,item(19,23));before=total(b,p); - click(p,6,key,ClickType.SWAP);equal(b.getStackInSlot(6),item(30+key,13),"Occupied hotbar incoming differs");equal(p.inventory.mainInventory.get(key),item(19,23),"Occupied hotbar outgoing differs"); - click(p,6,key,ClickType.SWAP);conserved(before,b,p);b.setInventorySlotContents(6,null);p.inventory.mainInventory.set(key,ItemStack.EMPTY); + p.getInventory().items.set(key,item(30+key,13));Map before=total(b,p); + click(p,6,key,ClickType.SWAP);equal(b.getItem(6),item(30+key,13),"Empty hotbar swap failed");equal(p.getInventory().items.get(key),null,"Hotbar source remained"); + click(p,6,key,ClickType.SWAP);equal(b.getItem(6),null,"Hotbar return left source"); + b.setItem(6,item(19,23));before=total(b,p); + click(p,6,key,ClickType.SWAP);equal(b.getItem(6),item(30+key,13),"Occupied hotbar incoming differs");equal(p.getInventory().items.get(key),item(19,23),"Occupied hotbar outgoing differs"); + click(p,6,key,ClickType.SWAP);conserved(before,b,p);b.setItem(6,null);p.getInventory().items.set(key,ItemStack.EMPTY); } } private static void armorSwap() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.mainInventory.set(0,item(4,1)); - click(p,0,0,ClickType.SWAP);equal(b.getStackInSlot(0),null,"Sword inserted into armor slot"); - p.inventory.mainInventory.set(0,pumpkins(64));Map before=total(b,p);click(p,0,0,ClickType.SWAP); - equal(b.getStackInSlot(0),pumpkins(1),"Hotbar armor limit ignored");equal(p.inventory.mainInventory.get(0),pumpkins(63),"Hotbar armor remainder lost");conserved(before,b,p); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.getInventory().items.set(0,item(4,1)); + click(p,0,0,ClickType.SWAP);equal(b.getItem(0),null,"Sword inserted into armor slot"); + p.getInventory().items.set(0,pumpkins(64));Map before=total(b,p);click(p,0,0,ClickType.SWAP); + equal(b.getItem(0),pumpkins(1),"Hotbar armor limit ignored");equal(p.getInventory().items.get(0),pumpkins(63),"Hotbar armor remainder lost");conserved(before,b,p); } private static void wearableShift() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();p.inventory.mainInventory.set(9,pumpkins(64));open(p,b); + EntityLootableBody b=body();ServerPlayer p=player();p.getInventory().items.set(9,pumpkins(64));open(p,b); Map before=total(b,p);click(p,48,0,ClickType.QUICK_MOVE); - RuntimeChecks.require(!(b.getStackInSlot(0)).isEmpty() && b.getStackInSlot(0).getCount()==1,"Shift-click put a stack into the one-item head slot"); + RuntimeChecks.require(!(b.getItem(0)).isEmpty() && b.getItem(0).getCount()==1,"Shift-click put a stack into the one-item head slot"); conserved(before,b,p); } private static void dragDistribution(int mode) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(item(19,10));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(19,10));Map before=total(b,p); drag(p,mode,6,7,8);int each=mode==0?3:1; - for(int i=6;i<=8;i++)equal(b.getStackInSlot(i),item(19,each),"Drag distribution differs"); - equal(p.inventory.getItemStack(),item(19,10-3*each),"Drag cursor remainder differs");conserved(before,b,p); + for(int i=6;i<=8;i++)equal(b.getItem(i),item(19,each),"Drag distribution differs"); + equal(p.containerMenu.getCarried(),item(19,10-3*each),"Drag cursor remainder differs");conserved(before,b,p); } private static void dragMixed() throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,64));b.setInventorySlotContents(7,item(19,63));b.setInventorySlotContents(8,item(20,11)); - ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(item(19,12));Map before=total(b,p); - drag(p,0,0,6,7,8,9,48);equal(b.getStackInSlot(0),null,"Drag bypassed armor validity");equal(b.getStackInSlot(6),item(19,64),"Drag changed saturated slot");equal(b.getStackInSlot(8),item(20,11),"Drag merged different NBT"); - RuntimeChecks.require(b.getStackInSlot(7).getCount()<=64,"Drag overfilled partial slot");conserved(before,b,p); + EntityLootableBody b=body();b.setItem(6,item(19,64));b.setItem(7,item(19,63));b.setItem(8,item(20,11)); + ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(19,12));Map before=total(b,p); + drag(p,0,0,6,7,8,9,48);equal(b.getItem(0),null,"Drag bypassed armor validity");equal(b.getItem(6),item(19,64),"Drag changed saturated slot");equal(b.getItem(8),item(20,11),"Drag merged different NBT"); + RuntimeChecks.require(b.getItem(7).getCount()<=64,"Drag overfilled partial slot");conserved(before,b,p); } private static void dragArmor() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(pumpkins(64));Map before=total(b,p); - drag(p,0,0,6);equal(b.getStackInSlot(0),pumpkins(1),"Drag head limit ignored");equal(b.getStackInSlot(6),pumpkins(32),"Drag main allocation differs");equal(p.inventory.getItemStack(),pumpkins(31),"Drag head remainder lost");conserved(before,b,p); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(pumpkins(64));Map before=total(b,p); + drag(p,0,0,6);equal(b.getItem(0),pumpkins(1),"Drag head limit ignored");equal(b.getItem(6),pumpkins(32),"Drag main allocation differs");equal(p.containerMenu.getCarried(),pumpkins(31),"Drag head remainder lost");conserved(before,b,p); } private static void dragCancel() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(item(19,10));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(19,10));Map before=total(b,p); packet(p,-999,2,ClickType.QUICK_CRAFT,null);packet(p,-999,0,ClickType.QUICK_CRAFT,null);packet(p,6,1,ClickType.QUICK_CRAFT,null); packet(p,48,0,ClickType.PICKUP,null);packet(p,-999,2,ClickType.QUICK_CRAFT,null); - equal(b.getStackInSlot(6),null,"Cancelled drag wrote selected slot");equal(p.inventory.getItemStack(),item(19,10),"Cancelled drag consumed cursor");conserved(before,b,p); + equal(b.getItem(6),null,"Cancelled drag wrote selected slot");equal(p.containerMenu.getCarried(),item(19,10),"Cancelled drag consumed cursor");conserved(before,b,p); } private static void fullPlayer() throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));ServerPlayerEntity p=player();fill(p);open(p,b); - Map before=total(b,p);packet(p,6,0,ClickType.QUICK_MOVE,null);equal(b.getStackInSlot(6),item(19,23),"Full player inventory consumed corpse stack");conserved(before,b,p); + EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer p=player();fill(p);open(p,b); + Map before=total(b,p);packet(p,6,0,ClickType.QUICK_MOVE,null);equal(b.getItem(6),item(19,23),"Full player inventory consumed corpse stack");conserved(before,b,p); } private static void partialPlayer() throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));ServerPlayerEntity p=player();fill(p);p.inventory.mainInventory.set(35,item(19,63));open(p,b); - Map before=total(b,p);click(p,6,0,ClickType.QUICK_MOVE);equal(p.inventory.mainInventory.get(35),item(19,64),"Partial merge did not fill one space");equal(b.getStackInSlot(6),item(19,22),"Partial merge discarded remainder");conserved(before,b,p); + EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer p=player();fill(p);p.getInventory().items.set(35,item(19,63));open(p,b); + Map before=total(b,p);click(p,6,0,ClickType.QUICK_MOVE);equal(p.getInventory().items.get(35),item(19,64),"Partial merge did not fill one space");equal(b.getItem(6),item(19,22),"Partial merge discarded remainder");conserved(before,b,p); } private static void fullCorpse() throws Exception { - EntityLootableBody b=body();fill(b);ServerPlayerEntity p=player();p.inventory.mainInventory.set(9,item(110,23));open(p,b); - CompoundNBT unchanged=data(b);Map before=total(b,p);packet(p,48,0,ClickType.QUICK_MOVE,null);b.baseTick(); + EntityLootableBody b=body();fill(b);ServerPlayer p=player();p.getInventory().items.set(9,item(110,23));open(p,b); + CompoundTag unchanged=data(b);Map before=total(b,p);packet(p,48,0,ClickType.QUICK_MOVE,null);b.baseTick(); RuntimeChecks.require(unchanged.get("Equipment").equals(data(b).get("Equipment")) && unchanged.get("Aux").equals(data(b).get("Aux")),"Full corpse overwrote indexed/Aux contents");conserved(before,b,p); } public static final class RejectedDrops { @@ -334,27 +354,27 @@ public static final class RejectedDrops { @SubscribeEvent public void toss(ItemTossEvent e){if(toss)e.setCanceled(true);} } private static void displacement(boolean rejectJoin,boolean rejectToss) throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(0,item(0,1));ServerPlayerEntity p=player();fill(p);p.inventory.mainInventory.set(0,pumpkins(64));open(p,b);Map before=total(b,p); + EntityLootableBody b=body();b.setItem(0,item(0,1));ServerPlayer p=player();fill(p);p.getInventory().items.set(0,pumpkins(64));open(p,b);Map before=total(b,p); RejectedDrops reject=new RejectedDrops();reject.join=rejectJoin;reject.toss=rejectToss;MinecraftForge.EVENT_BUS.register(reject); try{click(p,0,0,ClickType.SWAP);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} - equal(b.getStackInSlot(0),pumpkins(1),"Displacement changed normal incoming swap");equal(p.inventory.mainInventory.get(0),pumpkins(63),"Displacement changed hotbar remainder");conserved(before,b,p); - if(!rejectJoin && !rejectToss)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.removed),"Successful displacement no longer drops normally"); + equal(b.getItem(0),pumpkins(1),"Displacement changed normal incoming swap");equal(p.getInventory().items.get(0),pumpkins(63),"Displacement changed hotbar remainder");conserved(before,b,p); + if(!rejectJoin && !rejectToss)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Successful displacement no longer drops normally"); } private static void closeCursor(boolean rejected,boolean full) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();if(full){fill(b);fill(p);}open(p,b);p.inventory.setItemStack(item(120,17));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();if(full){fill(b);fill(p);}open(p,b);p.containerMenu.setCarried(item(120,17));Map before=total(b,p); RejectedDrops reject=new RejectedDrops();reject.join=rejected;MinecraftForge.EVENT_BUS.register(reject); try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} - equal(p.inventory.getItemStack(),null,"Closing left a nonsaved cursor stack");conserved(before,b,p); - if(!rejected)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.removed),"Cursor close no longer performs normal accepted drop"); + equal(p.containerMenu.getCarried(),null,"Closing left a nonsaved cursor stack");conserved(before,b,p); + if(!rejected)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cursor close no longer performs normal accepted drop"); } private static void capturedCursor() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(item(120,17)); + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(120,17)); p.captureDrops(new java.util.ArrayList<>());p.captureDrops().add(new ItemEntity(world,96.5,80,96.5,item(121,11)));Map before=total(b,p); close(p);RuntimeChecks.require(p.captureDrops()!=null,"Container close changed caller's drop capture state");conserved(before,b,p); } private static void dyingCursor() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();fill(p);open(p,b);p.inventory.setItemStack(item(120,17)); - b.onKillCommand();b.baseTick();Map before=total(b,p); + EntityLootableBody b=body();ServerPlayer p=player();fill(p);open(p,b);p.containerMenu.setCarried(item(120,17)); + b.kill();b.baseTick();Map before=total(b,p); RejectedDrops reject=new RejectedDrops();reject.join=true;MinecraftForge.EVENT_BUS.register(reject); try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} for(int i=0;i<15;i++)b.baseTick(); @@ -363,53 +383,56 @@ private static void dyingCursor() throws Exception { } private static void twoPlayerShift() throws Exception { for(int order=0;order<2;order++){ - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));ServerPlayerEntity a=player(),c=player();open(a,b);open(c,b); - ServerPlayerEntity first=order==0?a:c,second=order==0?c:a;Map before=total(b,a,c); - click(first,6,0,ClickType.QUICK_MOVE);packet(second,6,0,ClickType.QUICK_MOVE,item(19,23)); - a.openContainer.detectAndSendChanges();c.openContainer.detectAndSendChanges(); - RuntimeChecks.require(!second.openContainer.getCanCraft(second),"Stale transaction was not rejected"); - RuntimeChecks.require(((RecordingConnection)second.connection).resyncs>0 && ((RecordingConnection)second.connection).rejections>0,"Stale viewer did not receive inventory resync"); + EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer a=player(),c=player();open(a,b);open(c,b); + ServerPlayer first=order==0?a:c,second=order==0?c:a;Map before=total(b,a,c); + int staleState=second.containerMenu.getStateId(); + click(first,6,0,ClickType.QUICK_MOVE);second.containerMenu.broadcastChanges(); + RecordingConnection staleConnection=(RecordingConnection)second.connection;int beforeResync=staleConnection.resyncs; + packetWithState(second,6,0,ClickType.QUICK_MOVE,item(19,23),staleState); + a.containerMenu.broadcastChanges();c.containerMenu.broadcastChanges(); + RuntimeChecks.require(staleConnection.resyncs>beforeResync && staleConnection.lastContentStateId==second.containerMenu.getStateId(), + "Stale menu state did not receive an authoritative native state correction"); // Accepted clicks rely on the initiating client's prediction; only the // rejected stale viewer requires an authoritative window resync. - equal(first.openContainer.getSlot(6).getStack(),null,"First server container remained stale"); + equal(first.containerMenu.getSlot(6).getItem(),null,"First server container remained stale"); equal(((RecordingConnection)second.connection).slots.get(6),null,"Rejected viewer source remained stale");conserved(before,b,a,c); } } private static void interleavedDrag(boolean swap) throws Exception { - EntityLootableBody b=body();ServerPlayerEntity a=player(),c=player();open(a,b);open(c,b);a.inventory.setItemStack(item(19,10)); - if(swap)c.inventory.mainInventory.set(0,item(20,7));else c.inventory.setItemStack(item(20,7));Map before=total(b,a,c); + EntityLootableBody b=body();ServerPlayer a=player(),c=player();open(a,b);open(c,b);a.containerMenu.setCarried(item(19,10)); + if(swap)c.getInventory().items.set(0,item(20,7));else c.containerMenu.setCarried(item(20,7));Map before=total(b,a,c); packet(a,-999,0,ClickType.QUICK_CRAFT,null);packet(a,6,1,ClickType.QUICK_CRAFT,null);packet(a,7,1,ClickType.QUICK_CRAFT,null); click(c,6,0,swap?ClickType.SWAP:ClickType.PICKUP);packet(a,-999,2,ClickType.QUICK_CRAFT,null); - equal(b.getStackInSlot(6),item(20,7),"Other player's stack was overwritten by drag");equal(b.getStackInSlot(7),item(19,5),"Surviving drag slot differs");equal(a.inventory.getItemStack(),item(19,5),"Interrupted drag lost remainder");conserved(before,b,a,c); + equal(b.getItem(6),item(20,7),"Other player's stack was overwritten by drag");equal(b.getItem(7),item(19,5),"Surviving drag slot differs");equal(a.containerMenu.getCarried(),item(19,5),"Interrupted drag lost remainder");conserved(before,b,a,c); } private static void twoPlayerClose() throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));ServerPlayerEntity a=player(),c=player();fill(a);open(a,b);open(c,b);Map before=total(b,a,c); + EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer a=player(),c=player();fill(a);open(a,b);open(c,b);Map before=total(b,a,c); click(a,6,0,ClickType.PICKUP);packet(c,6,0,ClickType.QUICK_MOVE,item(19,23));RejectedDrops reject=new RejectedDrops();reject.join=true;MinecraftForge.EVENT_BUS.register(reject); try{close(a);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} - c.openContainer.detectAndSendChanges();conserved(before,b,a,c); + c.containerMenu.broadcastChanges();conserved(before,b,a,c); } private static void stale(boolean removed) throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));ServerPlayerEntity p=player();open(p,b);Map before=total(b,p); - if(removed)b.remove();else p.setPosition(200,80,200); - packet(p,6,0,ClickType.QUICK_MOVE,null);equal(b.getStackInSlot(6),item(19,23),"Stale packet extracted before tick closed GUI");conserved(before,b,p); + EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer p=player();open(p,b);Map before=total(b,p); + if(removed)b.discard();else p.setPos(200,80,200); + packet(p,6,0,ClickType.QUICK_MOVE,null);equal(b.getItem(6),item(19,23),"Stale packet extracted before tick closed GUI");conserved(before,b,p); } private static void staleDrag() throws Exception { - EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(item(19,10));Map before=total(b,p); - packet(p,-999,0,ClickType.QUICK_CRAFT,null);packet(p,6,1,ClickType.QUICK_CRAFT,null);p.setPosition(200,80,200); - packet(p,-999,2,ClickType.QUICK_CRAFT,null);equal(b.getStackInSlot(6),null,"Distant drag still distributed items"); - p.setPosition(96.5,80,96.5);drag(p,0,7);equal(b.getStackInSlot(7),item(19,10),"New drag reused invalid selection");conserved(before,b,p); - } - private static final class RecordingConnection extends ServerPlayNetHandler { - final Map slots=new HashMap<>();int resyncs,rejections; - RecordingConnection(ServerPlayerEntity p){super(server,new NetworkManager(PacketDirection.SERVERBOUND),p);} - @Override public void sendPacket(IPacket packet){ - if(!(packet instanceof SWindowItemsPacket || packet instanceof SSetSlotPacket || packet instanceof SConfirmTransactionPacket))return; - PacketBuffer wire=new PacketBuffer(Unpooled.buffer()); - try{packet.writePacketData(wire);int window=wire.readByte(); - if(packet instanceof SWindowItemsPacket){int n=wire.readShort();for(int i=0;i=0)slots.put(slot,s);} - else{wire.readShort();if(!wire.readBoolean())rejections++;} - }catch(IOException error){throw new RuntimeException(error);}finally{wire.release();} + EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(19,10));Map before=total(b,p); + packet(p,-999,0,ClickType.QUICK_CRAFT,null);packet(p,6,1,ClickType.QUICK_CRAFT,null);p.setPos(200,80,200); + packet(p,-999,2,ClickType.QUICK_CRAFT,null);equal(b.getItem(6),null,"Distant drag still distributed items"); + p.setPos(96.5,80,96.5);drag(p,0,7);equal(b.getItem(7),item(19,10),"New drag reused invalid selection");conserved(before,b,p); + } + private static final class RecordingConnection extends ServerGamePacketListenerImpl { + final Map slots=new HashMap<>();int resyncs,lastContentStateId=-1; + ItemStack carried=ItemStack.EMPTY; + RecordingConnection(ServerPlayer p){super(server,new Connection(PacketFlow.SERVERBOUND),p);} + @Override public void send(Packet packet){ + NativeMenuObservation observed=NativeMenuObservation.decode(packet);if(observed==null)return; + if(observed.content) { + slots.clear();for(int index=0;index=0)slots.put(observed.slot,observed.item); + else if(observed.containerId==-1)carried=observed.item; } } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java index ede2651..af09b9d 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java @@ -1,8 +1,8 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.Slot; -import net.minecraft.inventory.Inventory; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.SimpleContainer; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -10,18 +10,20 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.MainMenuScreen; -import net.minecraft.entity.Entity; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.inventory.container.ClickType; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; -import net.minecraft.util.Hand; -import net.minecraft.util.ScreenShotHelper; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.GameType; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.InteractionHand; +import net.minecraft.client.Screenshot; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameType; import net.minecraftforge.common.MinecraftForge; /** Actual connected client predictions, authoritative corrections and disk reload. */ @@ -32,9 +34,10 @@ private interface Action {void run() throws Exception;} private int stage,frames; private Future pending; private UUID playerId; - private CompoundNBT savedBody; - private ListNBT savedPlayer; - void start(Minecraft mc){playerId=mc.player.getUniqueID();} + private CompoundTag savedBody; + private ListTag savedPlayer; + private final Set preexistingDrops=new HashSet<>(); + void start(Minecraft mc){playerId=mc.player.getUUID();} boolean tick(Minecraft mc) throws Exception { frames++; if(stage==0){schedule(mc,()->prepare(mc));next(1);} @@ -43,80 +46,83 @@ else if(stage==2 && gui(mc) && frames>20){ equal(slot(mc,6),item(120,17),"Initial client source");click(mc,6,0,ClickType.THROW);next(3); } else if(stage==3 && frames>20){ equal(slot(mc,6),item(120,16),"Accepted client single throw"); - schedule(mc,()->{equal(body(mc).getStackInSlot(6),item(120,16),"Accepted server single throw");drop(mc,item(120,1));mark(mc,"network_accepted_throw");reject.toss=true;MinecraftForge.EVENT_BUS.register(reject);});next(4); + schedule(mc,()->{equal(body(mc).getItem(6),item(120,16),"Accepted server single throw");drop(mc,item(120,1));mark(mc,"network_accepted_throw");reject.toss=true;MinecraftForge.EVENT_BUS.register(reject);});next(4); } else if(stage==4 && done()) {click(mc,6,1,ClickType.THROW);next(5);} else if(stage==5 && frames>20){ equal(slot(mc,6),item(120,16),"Cancelled throw did not correct client source");screenshot(mc,"drop-cancelled-source-resync.png"); - schedule(mc,()->{equal(body(mc).getStackInSlot(6),item(120,16),"Cancelled server throw");mark(mc,"network_cancelled_throw_source_resync");reject.toss=false;reject.join=true;});next(6); + schedule(mc,()->{equal(body(mc).getItem(6),item(120,16),"Cancelled server throw");mark(mc,"network_cancelled_throw_source_resync");reject.toss=false;reject.join=true;});next(6); } else if(stage==6 && done()){click(mc,6,0,ClickType.PICKUP);next(7);} - else if(stage==7 && frames>20){equal(mc.player.inventory.getItemStack(),item(120,16),"Pickup cursor");click(mc,-999,1,ClickType.PICKUP);next(8);} + else if(stage==7 && frames>20){equal(mc.player.containerMenu.getCarried(),item(120,16),"Pickup cursor");click(mc,-999,1,ClickType.PICKUP);next(8);} else if(stage==8 && frames>20){ - equal(mc.player.inventory.getItemStack(),item(120,16),"Rejected outside drop did not correct client cursor");equal(slot(mc,6),null,"Outside source reappeared");screenshot(mc,"drop-rejected-cursor-resync.png"); - schedule(mc,()->{equal(player(mc).inventory.getItemStack(),item(120,16),"Rejected server outside cursor");mark(mc,"network_rejected_outside_cursor_resync");reject.join=false;});next(9); + equal(mc.player.containerMenu.getCarried(),item(120,16),"Rejected outside drop did not correct client cursor");equal(slot(mc,6),null,"Outside source reappeared");screenshot(mc,"drop-rejected-cursor-resync.png"); + schedule(mc,()->{equal(player(mc).containerMenu.getCarried(),item(120,16),"Rejected server outside cursor");mark(mc,"network_rejected_outside_cursor_resync");reject.join=false;});next(9); } else if(stage==9 && done()){click(mc,-999,0,ClickType.PICKUP);next(10);} else if(stage==10 && frames>20){ - equal(mc.player.inventory.getItemStack(),null,"Accepted outside client cursor"); - schedule(mc,()->{drop(mc,item(120,16));equal(player(mc).inventory.getItemStack(),null,"Accepted outside server cursor");mark(mc,"network_accepted_outside_drop"); - body(mc).setInventorySlotContents(6,item(120,17));player(mc).inventory.mainInventory.set(9,item(120,5));refresh(mc);});next(11); + equal(mc.player.containerMenu.getCarried(),null,"Accepted outside client cursor"); + schedule(mc,()->{drop(mc,item(120,16));equal(player(mc).containerMenu.getCarried(),null,"Accepted outside server cursor");mark(mc,"network_accepted_outside_drop"); + body(mc).setItem(6,item(120,17));player(mc).getInventory().items.set(9,item(120,5));refresh(mc);});next(11); } else if(stage==11 && done() && frames>20){click(mc,48,0,ClickType.PICKUP);click(mc,10,0,ClickType.PICKUP_ALL);next(12);} else if(stage==12 && frames>20){ - equal(mc.player.inventory.getItemStack(),item(120,42),"Double-click client cursor");equal(slot(mc,8),item(121,10),"Double-click changed unlike NBT"); - schedule(mc,()->{equal(player(mc).inventory.getItemStack(),item(120,42),"Double-click server cursor");equal(body(mc).getStackInSlot(6),null,"Double-click source 6");equal(body(mc).getStackInSlot(7),null,"Double-click source 7");mark(mc,"network_double_click_exact_nbt");});next(13); + equal(mc.player.containerMenu.getCarried(),item(120,42),"Double-click client cursor");equal(slot(mc,8),item(121,10),"Double-click changed unlike NBT"); + schedule(mc,()->{equal(player(mc).containerMenu.getCarried(),item(120,42),"Double-click server cursor");equal(body(mc).getItem(6),null,"Double-click source 6");equal(body(mc).getItem(7),null,"Double-click source 7");mark(mc,"network_double_click_exact_nbt");});next(13); } else if(stage==13 && done()){click(mc,48,0,ClickType.PICKUP);next(14);} - else if(stage==14 && frames>20){schedule(mc,()->{player(mc).setGameType(GameType.CREATIVE);body(mc).setInventorySlotContents(6,item(120,17));refresh(mc);});next(15);} - else if(stage==15 && done() && frames>20){RuntimeChecks.require(mc.player.abilities.isCreativeMode,"Client not creative");click(mc,6,2,ClickType.CLONE);next(16);} + else if(stage==14 && frames>20){schedule(mc,()->{player(mc).setGameMode(GameType.CREATIVE);body(mc).setItem(6,item(120,17));refresh(mc);});next(15);} + else if(stage==15 && done() && frames>20){RuntimeChecks.require(mc.player.getAbilities().instabuild,"Client not creative");click(mc,6,2,ClickType.CLONE);next(16);} else if(stage==16 && frames>20){ - equal(mc.player.inventory.getItemStack(),item(120,64),"Creative clone client NBT");equal(slot(mc,6),item(120,17),"Creative clone changed source"); + equal(mc.player.containerMenu.getCarried(),item(120,64),"Creative clone client NBT");equal(slot(mc,6),item(120,17),"Creative clone changed source"); click(mc,-999,8,ClickType.QUICK_CRAFT);click(mc,7,9,ClickType.QUICK_CRAFT);click(mc,9,9,ClickType.QUICK_CRAFT);click(mc,-999,10,ClickType.QUICK_CRAFT);next(17); } else if(stage==17 && frames>20){ - equal(slot(mc,7),item(120,64),"Creative client fill first");equal(slot(mc,9),item(120,64),"Creative client fill second");equal(mc.player.inventory.getItemStack(),null,"Creative fill vanilla cursor");screenshot(mc,"drop-creative-filled.png"); - schedule(mc,()->{equal(body(mc).getStackInSlot(7),item(120,64),"Creative server fill first");equal(body(mc).getStackInSlot(9),item(120,64),"Creative server fill second");equal(player(mc).inventory.mainInventory.get(9),item(120,42),"Creative operation changed recovered inventory");mark(mc,"network_creative_clone_and_fill"); - for(int i=0;i<48;i++)body(mc).setInventorySlotContents(i,item(i,i<6?1:64));body(mc).addItem(item(70,31)); - for(int i=0;i<36;i++)player(mc).inventory.mainInventory.set(i,item(100+i,64));player(mc).inventory.setItemStack(item(122,17));reject.toss=true;refresh(mc);});next(18); - } else if(stage==18 && done() && frames>20){equal(mc.player.inventory.getItemStack(),item(122,17),"Creative close cursor fixture");mc.player.closeScreen();next(19);} + equal(slot(mc,7),item(120,64),"Creative client fill first");equal(slot(mc,9),item(120,64),"Creative client fill second");equal(mc.player.containerMenu.getCarried(),null,"Creative fill vanilla cursor");screenshot(mc,"drop-creative-filled.png"); + schedule(mc,()->{equal(body(mc).getItem(7),item(120,64),"Creative server fill first");equal(body(mc).getItem(9),item(120,64),"Creative server fill second");equal(player(mc).getInventory().items.get(9),item(120,42),"Creative operation changed recovered inventory");mark(mc,"network_creative_clone_and_fill"); + for(int i=0;i<48;i++)body(mc).setItem(i,item(i,i<6?1:64));body(mc).addItem(item(70,31)); + for(int i=0;i<36;i++)player(mc).getInventory().items.set(i,item(100+i,64));player(mc).containerMenu.setCarried(item(122,17));reject.toss=true;refresh(mc);});next(18); + } else if(stage==18 && done() && frames>20){equal(mc.player.containerMenu.getCarried(),item(122,17),"Creative close cursor fixture");mc.player.closeContainer();next(19);} else if(stage==19 && frames>20){ - equal(mc.player.inventory.getItemStack(),null,"Closed creative client cursor"); - schedule(mc,()->{equal(player(mc).inventory.getItemStack(),null,"Closed creative server cursor");CompoundNBT data=new CompoundNBT();body(mc).writeAdditional(data);ListNBT aux=data.getList("Aux",10);int count=0; - for(int i=0;i{equal(player(mc).containerMenu.getCarried(),null,"Closed creative server cursor");CompoundTag data=new CompoundTag();body(mc).addAdditionalSaveData(data);ListTag aux=data.getList("Aux",10);int count=0; + for(int i=0;i20){schedule(mc,()->{ - player(mc).world.getChunk(2,0);body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"drop-creative-final",body(mc)); - RuntimeChecks.require(savedPlayer.equals(player(mc).inventory.write(new ListNBT())),"Creative player inventory changed on disk reload");player(mc).connection.setPlayerLocation(40.5,80,7.5,0,0); + 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); mark(mc,"creative_cancelled_close_full_inventory_disk_reload");});next(23); } else if(stage==23 && done()){if(open(mc))next(24);} else if(stage==24 && gui(mc) && frames>20){ - ItemStack[] expected=new ItemStack[48];ListNBT list=savedBody.getList("Equipment",10);for(int i=0;i(FixtureApi.entities(player(mc).world)))if(e instanceof ItemEntity && !e.removed){equal(((ItemEntity)e).getItem(),expected,"Accepted world drop NBT/count");count++;e.remove();}RuntimeChecks.require(count==1,"Expected exactly one accepted drop");} - private static ItemStack slot(Minecraft mc,int index){return mc.player.openContainer.getSlot(index).getStack();} - private static void click(Minecraft mc,int slot,int button,ClickType type){mc.playerController.windowClick(mc.player.openContainer.windowId,slot,button,type,mc.player);} - private static boolean gui(Minecraft mc){return mc.currentScreen instanceof CorpseGUIContainer;} - private static boolean ready(Minecraft mc){return mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null;} - private boolean open(Minecraft mc){for(Entity e:FixtureApi.entities(mc.world))if(e.getUniqueID().equals(bodyId)){mc.playerController.interactWithEntity(mc.player,e,Hand.MAIN_HAND);return true;}return false;} - private ServerPlayerEntity player(Minecraft mc){return mc.getIntegratedServer().getPlayerList().getPlayerByUUID(playerId);} - private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(mc.getIntegratedServer().getWorld(FixtureApi.dimensionKey(0)),bodyId);} - private File folder(Minecraft mc){return player(mc).getServerWorld().getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile();} - private void refresh(Minecraft mc){player(mc).openContainer.detectAndSendChanges();player(mc).sendContainerToPlayer(player(mc).openContainer);} - private void schedule(Minecraft mc,Action action){pending=mc.getIntegratedServer().runAsync(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} + private static void equal(ItemStack a,ItemStack b,String message){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),message+" expected="+b+" actual="+a);} + private void drop(Minecraft mc,ItemStack expected){int count=0;for(Entity e:new ArrayList(FixtureApi.entities(player(mc).getLevel())))if(e instanceof ItemEntity && !e.isRemoved() && !preexistingDrops.contains(e.getUUID())){equal(((ItemEntity)e).getItem(),expected,"Accepted new world drop NBT/count");count++;e.discard();}RuntimeChecks.require(count==1,"Expected exactly one new accepted drop, actual="+count);} + private static ItemStack slot(Minecraft mc,int index){return mc.player.containerMenu.getSlot(index).getItem();} + private static void click(Minecraft mc,int slot,int button,ClickType type){mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,button,type,mc.player);} + private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} + private static boolean ready(Minecraft mc){return mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null;} + private boolean open(Minecraft mc){for(Entity e:FixtureApi.entities(mc.level))if(e.getUUID().equals(bodyId)){mc.gameMode.interact(mc.player,e,InteractionHand.MAIN_HAND);return true;}return false;} + private ServerPlayer player(Minecraft mc){return mc.getSingleplayerServer().getPlayerList().getPlayer(playerId);} + private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(mc.getSingleplayerServer().getLevel(FixtureApi.dimensionKey(0)),bodyId);} + private File folder(Minecraft mc){return player(mc).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile();} + private void refresh(Minecraft mc){player(mc).containerMenu.broadcastChanges();player(mc).containerMenu.broadcastFullState();} + private void schedule(Minecraft mc,Action action){pending=mc.getSingleplayerServer().submitAsync(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} private boolean done() throws Exception {if(!pending.isDone())return false;pending.get();return true;} private void next(int value){stage=value;frames=0;System.out.println("DROP_CLIENT_STAGE "+stage);} - private static void screenshot(Minecraft mc,String name){ScreenShotHelper.saveScreenshot(mc.gameDir,name,mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} - private void mark(Minecraft mc,String name) throws Exception {try(FileWriter out=new FileWriter(new File(mc.gameDir,"drop-client-checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("DROP_CLIENT_CHECK_PASS "+name);} - private static void quit(Minecraft mc){mc.player.closeScreen();FixtureConnections.disconnect(mc);} + private static void screenshot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private void mark(Minecraft mc,String name) throws Exception {try(FileWriter out=new FileWriter(new File(mc.gameDirectory,"drop-client-checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("DROP_CLIENT_CHECK_PASS "+name);} + private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java index 9cef779..0108d7f 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java @@ -1,72 +1,87 @@ package cyano.lootable.buildtest; -import net.minecraft.potion.EffectInstance; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.StringNBT; -import net.minecraft.nbt.ShortNBT; -import net.minecraft.nbt.LongNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.IntArrayNBT; -import net.minecraft.nbt.FloatNBT; -import net.minecraft.nbt.DoubleNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ByteNBT; -import net.minecraft.nbt.ByteArrayNBT; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.ByteArrayTag; import cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; -import net.minecraft.entity.ai.attributes.Attributes; -import net.minecraft.potion.Effects; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; -import net.minecraft.potion.*; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectCategory; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerLevel; /** Real legacy readers, ticking and round trips; no production test dependency. */ final class EffectHurtChecks { private interface Check {void run() throws Exception;} - private static ServerWorld world; + private static ServerLevel world; private static int failures; private static final List results=new ArrayList<>(); - private static Effect extended; + private static MobEffect extended; static void registerPotion(){ - RuntimeChecks.require(Effect.get(200)==null,"Fixture potion ID already occupied"); - extended=new Effect(net.minecraft.potion.EffectType.BENEFICIAL,0x123456){}; + RuntimeChecks.require(MobEffect.byId(200)==null,"Fixture potion ID already occupied"); + extended=new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0x123456){}; // Forge 1.12 assigns IDs through registry events. Fill the isolated test // registry through its public API so unsigned-byte coverage still uses 200. - for(int i=0;Effect.get(199)==null && i<200;i++) - net.minecraftforge.registries.ForgeRegistries.POTIONS.register(new Effect(net.minecraft.potion.EffectType.BENEFICIAL,0){}.setRegistryName("lootablebodiesbuildtest:padding_"+i)); - net.minecraftforge.registries.ForgeRegistries.POTIONS.register(extended.setRegistryName("lootablebodiesbuildtest:extended_effect")); - RuntimeChecks.require(Effect.getId(extended)==200,"Fixture potion must exercise unsigned byte ID 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")); + 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. if(System.getProperty("lootablebodies.phase","").startsWith("upgrade-")) for(int i=0;i<200;i++) { - net.minecraft.util.ResourceLocation name=new net.minecraft.util.ResourceLocation("lootablebodiesbuildtest:padding_"+i); - if(!net.minecraftforge.registries.ForgeRegistries.POTIONS.containsKey(name)) - net.minecraftforge.registries.ForgeRegistries.POTIONS.register(new Effect(net.minecraft.potion.EffectType.BENEFICIAL,0){}.setRegistryName(name)); + 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)); } } - static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { + static void run(MinecraftServer server,ServerLevel target,File folder) throws Exception { world=target;failures=0;results.clear(); - INBT[] wrong={StringNBT.valueOf("opaque"),new CompoundNBT(),new ByteArrayNBT(new byte[]{1}),new IntArrayNBT(new int[]{1}),new ListNBT()}; - for(int i=0;ibadHurt(raw));} - for(INBT raw:new INBT[]{ShortNBT.valueOf((short)-1),IntNBT.valueOf(32768),LongNBT.valueOf(65537),LongNBT.valueOf(Long.MAX_VALUE),FloatNBT.valueOf(2),DoubleNBT.valueOf(2.5),DoubleNBT.valueOf(Double.NaN),DoubleNBT.valueOf(Double.POSITIVE_INFINITY)})check("HurtTime_invalid_"+raw,()->badHurt(raw)); - for(INBT raw:new INBT[]{ByteNBT.valueOf((byte)7),ShortNBT.valueOf((short)10),IntNBT.valueOf(11),LongNBT.valueOf(32767)})check("HurtTime_lossless_"+raw,()->validHurt(raw)); - check("missing_hurt_time_defaults_zero",()->{CompoundNBT n=base();n.remove("HurtTime");EntityLootableBody b=read(n);require(b.hurtTime==0,"Missing hurt time not zero");stable(b);}); - for(int i=0;ibadList(raw));} - check("ActiveEffects_wrong_element_type",()->{ListNBT l=new ListNBT();l.add(StringNBT.valueOf("opaque"));badList(l);}); - check("empty_and_missing_effects_keep_defaults",()->{for(boolean empty:new boolean[]{false,true}){CompoundNBT n=base();if(empty)n.put("ActiveEffects",new ListNBT());EntityLootableBody b=read(n);require(b.getActivePotionEffects().isEmpty(),"Unexpected default effect");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy empty effects quarantined");stable(b);}}); - for(String key:new String[]{"Id","Duration","Amplifier"})for(int i=0;ibadEffect(k,raw));} - for(INBT raw:new INBT[]{IntNBT.valueOf(-1),IntNBT.valueOf(256),LongNBT.valueOf(4294967297L),DoubleNBT.valueOf(1),ByteNBT.valueOf((byte)201)})check("Id_invalid_or_absent_registry_"+raw,()->badEffect("Id",raw)); + Tag[] wrong={StringTag.valueOf("opaque"),new CompoundTag(),new ByteArrayTag(new byte[]{1}),new IntArrayTag(new int[]{1}),new ListTag()}; + for(int i=0;ibadHurt(raw));} + for(Tag raw:new Tag[]{ShortTag.valueOf((short)-1),IntTag.valueOf(32768),LongTag.valueOf(65537),LongTag.valueOf(Long.MAX_VALUE),FloatTag.valueOf(2),DoubleTag.valueOf(2.5),DoubleTag.valueOf(Double.NaN),DoubleTag.valueOf(Double.POSITIVE_INFINITY)})check("HurtTime_invalid_"+raw,()->badHurt(raw)); + for(Tag raw:new Tag[]{ByteTag.valueOf((byte)7),ShortTag.valueOf((short)10),IntTag.valueOf(11),LongTag.valueOf(32767)})check("HurtTime_lossless_"+raw,()->validHurt(raw)); + check("missing_hurt_time_defaults_zero",()->{CompoundTag n=base();n.remove("HurtTime");EntityLootableBody b=read(n);require(b.hurtTime==0,"Missing hurt time not zero");stable(b);}); + for(int i=0;ibadList(raw));} + check("ActiveEffects_wrong_element_type",()->{ListTag l=new ListTag();l.add(StringTag.valueOf("opaque"));badList(l);}); + check("empty_and_missing_effects_keep_defaults",()->{for(boolean empty:new boolean[]{false,true}){CompoundTag n=base();if(empty)n.put("ActiveEffects",new ListTag());EntityLootableBody b=read(n);require(b.getActiveEffects().isEmpty(),"Unexpected default effect");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy empty effects quarantined");stable(b);}}); + for(String key:new String[]{"Id","Duration","Amplifier"})for(int i=0;ibadEffect(k,raw));} + for(Tag raw:new Tag[]{IntTag.valueOf(-1),IntTag.valueOf(256),LongTag.valueOf(4294967297L),DoubleTag.valueOf(1),ByteTag.valueOf((byte)201)})check("Id_invalid_or_absent_registry_"+raw,()->badEffect("Id",raw)); check("Id_missing_retains_record",()->badEffect("Id",null)); - for(INBT raw:new INBT[]{IntNBT.valueOf(-1),LongNBT.valueOf(2147483648L),LongNBT.valueOf(4294967496L),FloatNBT.valueOf(100),DoubleNBT.valueOf(Double.NaN),DoubleNBT.valueOf(Double.NEGATIVE_INFINITY)})check("Duration_invalid_"+raw,()->badEffect("Duration",raw)); - for(INBT raw:new INBT[]{ShortNBT.valueOf((short)-1),IntNBT.valueOf(128),IntNBT.valueOf(256),LongNBT.valueOf(4294967296L),FloatNBT.valueOf(1),DoubleNBT.valueOf(Double.POSITIVE_INFINITY)})check("Amplifier_invalid_"+raw,()->badEffect("Amplifier",raw)); + for(Tag raw:new Tag[]{IntTag.valueOf(-1),LongTag.valueOf(2147483648L),LongTag.valueOf(4294967496L),FloatTag.valueOf(100),DoubleTag.valueOf(Double.NaN),DoubleTag.valueOf(Double.NEGATIVE_INFINITY)})check("Duration_invalid_"+raw,()->badEffect("Duration",raw)); + for(Tag raw:new Tag[]{ShortTag.valueOf((short)-1),IntTag.valueOf(128),IntTag.valueOf(256),LongTag.valueOf(4294967296L),FloatTag.valueOf(1),DoubleTag.valueOf(Double.POSITIVE_INFINITY)})check("Amplifier_invalid_"+raw,()->badEffect("Amplifier",raw)); for(String key:new String[]{"Ambient","ShowParticles","ShowIcon"}){ - for(int i=0;ibadFlag(k,raw));} - for(INBT raw:new INBT[]{ByteNBT.valueOf((byte)-1),IntNBT.valueOf(2),LongNBT.valueOf(256),FloatNBT.valueOf(1),DoubleNBT.valueOf(Double.NaN)})check(key+"_invalid_"+raw,()->badFlag(key,raw)); + for(int i=0;ibadFlag(k,raw));} + for(Tag raw:new Tag[]{ByteTag.valueOf((byte)-1),IntTag.valueOf(2),LongTag.valueOf(256),FloatTag.valueOf(1),DoubleTag.valueOf(Double.NaN)})check(key+"_invalid_"+raw,()->badFlag(key,raw)); } check("lossless_integral_effect_fields",EffectHurtChecks::numeric); check("missing_optional_effect_fields_keep_legacy_defaults",EffectHurtChecks::missingOptional); @@ -85,43 +100,43 @@ static void run(MinecraftServer server,ServerWorld target,File folder) throws Ex check("repeated_read_removes_obsolete_potion_modifiers",EffectHurtChecks::repeatedAttributes); check("reused_output_does_not_keep_previous_effect_list",EffectHurtChecks::reusedOutput); check("invalid_effect_and_hurt_fields_keep_complete_unresolved_inventory",EffectHurtChecks::inventory); - check("native_icon_flag_survives_without_quarantine",()->{CompoundNBT e=effect();e.putBoolean("ShowIcon",false);EntityLootableBody b=read(with(e));require(!b.getActivePotionEffect(Effects.NIGHT_VISION).isShowIcon(),"Native icon visibility changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Native icon visibility quarantined");stable(b);}); - check("missing_icon_flag_follows_particle_default",()->{CompoundNBT e=effect();e.remove("ShowIcon");e.putBoolean("ShowParticles",false);EntityLootableBody b=read(with(e));require(!b.getActivePotionEffect(Effects.NIGHT_VISION).isShowIcon(),"Missing native icon default changed");stable(b);}); + check("native_icon_flag_survives_without_quarantine",()->{CompoundTag e=effect();e.putBoolean("ShowIcon",false);EntityLootableBody b=read(with(e));require(!b.getEffect(MobEffects.NIGHT_VISION).showIcon(),"Native icon visibility changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Native icon visibility quarantined");stable(b);}); + check("missing_icon_flag_follows_particle_default",()->{CompoundTag e=effect();e.remove("ShowIcon");e.putBoolean("ShowParticles",false);EntityLootableBody b=read(with(e));require(!b.getEffect(MobEffects.NIGHT_VISION).showIcon(),"Missing native icon default changed");stable(b);}); check("metadata_only_does_not_prevent_empty_decay",EffectHurtChecks::empty); try(PrintWriter out=new PrintWriter(new File(folder,"effect-hurt-checks.txt"),"UTF-8")){for(String s:results)out.println(s);out.println("checks="+results.size()+" failures="+failures);} require(failures==0,"Effect/hurt regressions: "+failures+" / "+results.size());System.out.println("LOOTABLE_BODIES_EFFECT_HURT_PASS checks="+results.size()); } private static void check(String name,Check test){try{test.run();results.add("PASS "+name);System.out.println("EFFECT_HURT_PASS "+name);}catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("EFFECT_HURT_FAIL "+name+": "+e);e.printStackTrace();}} private static void require(boolean c,String why){RuntimeChecks.require(c,why);} - private static CompoundNBT saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} - private static CompoundNBT base(){EntityLootableBody b=new EntityLootableBody(world);b.setPosition(160.5,80,160.5);b.setNoGravity(true);b.setInventorySlotContents(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(b);return saved(b);} - private static EntityLootableBody read(CompoundNBT n){CompoundNBT before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.read(n);require(NameLifetimeChecks.same(before,n),"Input mutated");return b;} - static CompoundNBT effect(Effect p,int duration,int amplifier){return new EffectInstance(p,duration,amplifier,false,true).write(new CompoundNBT());} - private static CompoundNBT effect(){return effect(Effects.NIGHT_VISION,200,1);} - private static CompoundNBT with(CompoundNBT e){CompoundNBT n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(e));return n;} - private static void stable(EntityLootableBody b){CompoundNBT a=saved(b),z=saved(read(a));for(String k:new String[]{"Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY,"ActiveEffects","HurtTime","Health","AbsorptionAmount","Attributes"})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);require(RuntimeChecks.sameStack(b.getStackInSlot(6),NbtRecoveryChecks.item(120,17)),"Complete known inventory changed");} - private static void badHurt(INBT raw){CompoundNBT n=base();n.put("HurtTime",raw.copy());EntityLootableBody b=read(n);require(b.hurtTime==0 && b.isAlive(),"Bad hurt time activated");NameLifetimeChecks.retained(b,"HurtTime",raw);stable(b);} - private static void validHurt(INBT raw){CompoundNBT n=base();n.put("HurtTime",raw);EntityLootableBody b=read(n);int v=((NumberNBT)raw).getInt();require(b.hurtTime==v,"Valid countdown narrowed");stable(b);b.baseTick();require(b.hurtTime==v-1,"Countdown no longer ticks");} - private static void badList(INBT raw){CompoundNBT n=base();n.put("ActiveEffects",raw.copy());EntityLootableBody b=read(n);require(b.getActivePotionEffects().isEmpty(),"Bad effect list activated");NameLifetimeChecks.retained(b,"ActiveEffects",raw);stable(b);} - private static void badEffect(String key,INBT raw){CompoundNBT e=effect();if(raw==null)e.remove(key);else e.put(key,raw.copy());EntityLootableBody b=read(with(e));require(b.getActivePotionEffects().isEmpty(),"Malformed effect activated");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);} - private static void badFlag(String key,INBT raw){CompoundNBT e=effect();e.put(key,raw.copy());EntityLootableBody b=read(with(e));EffectInstance p=b.getActivePotionEffect(Effects.NIGHT_VISION);require(p!=null && p.getDuration()==200 && p.getAmplifier()==1,"Bad optional flag lost valid effect");require(!p.isAmbient() && p.doesShowParticles(),"Bad optional flag did not use defaults");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);} - private static void numeric(){for(int type=0;type<4;type++){CompoundNBT e=effect();INBT[] ids={ByteNBT.valueOf((byte)16),ShortNBT.valueOf((short)16),IntNBT.valueOf(16),LongNBT.valueOf(16)};INBT[] one={ByteNBT.valueOf((byte)1),ShortNBT.valueOf((short)1),IntNBT.valueOf(1),LongNBT.valueOf(1)};e.put("Id",ids[type]);e.put("Duration",one[type]);e.put("Amplifier",one[type].copy());e.put("Ambient",one[type].copy());e.put("ShowParticles",one[type].copy());EntityLootableBody b=read(with(e));EffectInstance p=b.getActivePotionEffect(Effects.NIGHT_VISION);require(p!=null && p.getDuration()==1 && p.getAmplifier()==1 && p.isAmbient() && p.doesShowParticles(),"Lossless integral effect changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid numbers quarantined");stable(b);}} - private static void missingOptional(){CompoundNBT e=effect();for(String k:new String[]{"Duration","Amplifier","Ambient","ShowParticles"})e.remove(k);EntityLootableBody b=read(with(e));EffectInstance p=b.getActivePotionEffect(Effects.NIGHT_VISION);require(p!=null && p.getDuration()==0 && p.getAmplifier()==0 && !p.isAmbient() && p.doesShowParticles(),"Legacy defaults changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Missing optional fields quarantined");stable(b);} - private static void validDuration(int d){EntityLootableBody b=read(with(effect(Effects.NIGHT_VISION,d,0)));b.baseTick();EffectInstance p=b.getActivePotionEffect(Effects.NIGHT_VISION);require(d==0?p==null:p!=null && p.getDuration()==d-1,"Valid duration tick/expiry changed");stable(b);} - private static void signedAmplifiers(){for(int a:new int[]{-128,-1,0,127}){EntityLootableBody b=read(with(effect(Effects.NIGHT_VISION,200,a)));require(b.getActivePotionEffect(Effects.NIGHT_VISION).getAmplifier()==Math.max(0,a),"Native 1.11 byte amplifier clamping changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Canonical signed byte quarantined");stable(b);}} - private static void curativeItems(){CompoundNBT e=effect();CompoundNBT item=NbtRecoveryChecks.item(120,7).write(new CompoundNBT());ListNBT list=new ListNBT();list.add(item);e.put("CurativeItems",list);EntityLootableBody b=read(with(e));require(b.getActivePotionEffect(Effects.NIGHT_VISION).getCurativeItems().size()==1 && RuntimeChecks.sameStack(b.getActivePotionEffect(Effects.NIGHT_VISION).getCurativeItems().get(0),ItemStack.read(item)),"Forge curative item NBT changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy Forge curative items quarantined");stable(b);} - private static void badCurativeItems(){CompoundNBT e=effect();e.putString("CurativeItems","opaque");EntityLootableBody b=read(with(e));NameLifetimeChecks.retained(b,"ActiveEffect",e);require(!b.getActivePotionEffect(Effects.NIGHT_VISION).getCurativeItems().isEmpty(),"Malformed curative list did not use native defaults");stable(b);} - private static void extendedId(){for(boolean wide:new boolean[]{false,true}){CompoundNBT e=effect(extended,200,1);if(wide)e.putInt("Id",200);EntityLootableBody b=read(with(e));require(b.getActivePotionEffect(extended)!=null && b.getActivePotionEffect(extended).getDuration()==200,"Registered unsigned ID changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid extended ID quarantined");stable(b);}} - private static void alias(){CompoundNBT first=effect(extended,200,1),second=effect(extended,900,3),n=base();second.putLong("Id",200);n.put("ActiveEffects",NbtRecoveryChecks.list(first,second));EntityLootableBody b=read(n);require(b.getActivePotionEffect(extended).getDuration()==200,"Numeric alias bypassed first valid effect");NameLifetimeChecks.retained(b,"ActiveEffect",second);stable(b);} - private static void validTail(){CompoundNBT bad=effect();bad.putString("Duration","opaque");CompoundNBT n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(bad,effect()));EntityLootableBody b=read(n);require(b.getActivePotionEffect(Effects.NIGHT_VISION).getDuration()==200,"Bad first effect hid valid tail");NameLifetimeChecks.retained(b,"ActiveEffect",bad);stable(b);} - private static void duplicate(){CompoundNBT first=effect(),second=effect(Effects.NIGHT_VISION,900,3),n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(first,second));EntityLootableBody b=read(n);require(b.getActivePotionEffect(Effects.NIGHT_VISION).getDuration()==200,"Duplicate overwrote first effect");NameLifetimeChecks.retained(b,"ActiveEffect",second);stable(b);} - private static void unknown(){CompoundNBT e=effect();e.putString("Opaque","retain effect extension");EntityLootableBody b=read(with(e));require(b.getActivePotionEffects().size()==1,"Unknown sibling lost understood effect");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);} - private static void repeated(){EntityLootableBody b=read(with(effect(Effects.INVISIBILITY,200,0)));b.baseTick();require(b.isInvisible(),"Fixture not invisible");b.read(base());require(b.getActivePotionEffects().isEmpty() && !b.isInvisible(),"Previous effects or render state survived read");stable(b);} - private static void attributes(){EntityLootableBody original=new EntityLootableBody(world);original.setInventorySlotContents(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(original);original.addPotionEffect(new EffectInstance(Effects.HEALTH_BOOST,200,1));original.addPotionEffect(new EffectInstance(Effects.SPEED,200,0));original.addPotionEffect(new EffectInstance(Effects.ABSORPTION,200,0));original.setHealth(original.getMaxHealth());CompoundNBT n=saved(original);EntityLootableBody b=read(n);require(b.getMaxHealth()==28 && b.getHealth()==28 && b.getAbsorptionAmount()==4 && b.getActivePotionEffects().size()==3,"Valid potion health/attributes/absorption changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy potion fields quarantined");stable(b);for(int i=0;i<200;i++)b.baseTick();require(b.getActivePotionEffects().isEmpty() && b.getMaxHealth()==20 && b.getAbsorptionAmount()==0 && b.getHealth()<=20,"Expiry did not clean up normal modifiers");} - private static void repeatedAttributes(){EntityLootableBody original=new EntityLootableBody(world);original.addPotionEffect(new EffectInstance(Effects.HEALTH_BOOST,200,1));EntityLootableBody b=read(saved(original));b.read(base());require(b.getActivePotionEffects().isEmpty() && b.getMaxHealth()==20,"Obsolete effects/modifiers survived read");stable(b);} - private static void reusedOutput(){EntityLootableBody b=read(base());CompoundNBT n=with(effect());b.writeAdditional(n);require(!n.contains("ActiveEffects"),"Writer retained obsolete ActiveEffects");stable(b);} - private static void inventory(){CompoundNBT n=base(),missing=NbtRecoveryChecks.entry(121,11,7);missing.putString("id","absentfixture:missing");n.getList("Equipment",10).add(missing);CompoundNBT raw=malformedFields();for(String k:raw.keySet())n.put(k,raw.get(k).copy());EntityLootableBody b=read(n);require(b.isAlive(),"Bad fields made corpse inaccessible");NbtRecoveryChecks.retained(b,missing);verifyWorldFields(b);stable(b);stable(read(saved(b)));} - private static void empty(){CompoundNBT n=base();n.put("Equipment",new ListNBT());n.putString("HurtTime","opaque");EntityLootableBody b=read(n);require(b.isEmpty(),"Metadata counts as inventory");NameLifetimeChecks.retained(b,"HurtTime",n.get("HurtTime"));} - static CompoundNBT malformedFields(){CompoundNBT n=new CompoundNBT();n.putLong("HurtTime",65537);CompoundNBT bad=effect();bad.putString("Duration","opaque saved duration");CompoundNBT unknown=effect();unknown.putInt("Id",250);unknown.putString("Opaque","missing effect data");n.put("ActiveEffects",NbtRecoveryChecks.list(bad,unknown));return n;} - static void verifyWorldFields(EntityLootableBody b){require(b.hurtTime==0 && b.getActivePotionEffects().isEmpty(),"Malformed disk effect/hurt state activated");CompoundNBT raw=malformedFields();NameLifetimeChecks.retained(b,"HurtTime",raw.get("HurtTime"));ListNBT list=raw.getList("ActiveEffects",10);for(int i=0;i dimension) { - player.func_242111_a(dimension,position,0F,forced,notify); + static void spawn(net.minecraft.server.level.ServerPlayer player,net.minecraft.core.BlockPos position,boolean forced,boolean notify,net.minecraft.resources.ResourceKey dimension) { + player.setRespawnPosition(dimension,position,0F,forced,notify); } - static net.minecraft.util.RegistryKey dimensionKey(int id) { - if(id==0)return World.OVERWORLD;if(id==-1)return World.THE_NETHER;if(id==1)return World.THE_END; + static net.minecraft.resources.ResourceKey dimensionKey(int id) { + if(id==0)return Level.OVERWORLD;if(id==-1)return Level.NETHER;if(id==1)return Level.END; throw new IllegalArgumentException("Unknown fixture dimension "+id); } - static int dimensionId(World world) { - if(world.getDimensionKey()==World.OVERWORLD)return 0; - if(world.getDimensionKey()==World.THE_NETHER)return -1; - if(world.getDimensionKey()==World.THE_END)return 1; - throw new IllegalArgumentException("Unknown fixture dimension "+world.getDimensionKey()); + static int dimensionId(Level world) { + if(world.dimension()==Level.OVERWORLD)return 0; + if(world.dimension()==Level.NETHER)return -1; + if(world.dimension()==Level.END)return 1; + throw new IllegalArgumentException("Unknown fixture dimension "+world.dimension()); } - static net.minecraft.world.storage.PlayerData playerData(MinecraftServer server) { + static net.minecraft.world.level.storage.PlayerDataStorage playerData(MinecraftServer server) { java.util.List fields=new java.util.ArrayList<>(); for(Class type=server.getClass();type!=null;type=type.getSuperclass())for(java.lang.reflect.Field field:type.getDeclaredFields()) - if(field.getType()==net.minecraft.world.storage.PlayerData.class)fields.add(field); + if(field.getType()==net.minecraft.world.level.storage.PlayerDataStorage.class)fields.add(field); if(fields.size()!=1)throw new IllegalStateException("Ambiguous fixture player reader"); - try {java.lang.reflect.Field field=fields.get(0);field.setAccessible(true);return (net.minecraft.world.storage.PlayerData)field.get(server);} + try {java.lang.reflect.Field field=fields.get(0);field.setAccessible(true);return (net.minecraft.world.level.storage.PlayerDataStorage)field.get(server);} catch(ReflectiveOperationException failure){throw new IllegalStateException(failure);} } @OnlyIn(Dist.CLIENT) static final class ClientWorlds { - static void create(net.minecraft.client.Minecraft client,String folder,String title,net.minecraft.world.GameType mode,boolean commands) { - net.minecraft.util.registry.DynamicRegistries.Impl registries=net.minecraft.util.registry.DynamicRegistries.func_239770_b_(); - net.minecraft.util.registry.Registry types=registries.getRegistry(net.minecraft.util.registry.Registry.DIMENSION_TYPE_KEY); - net.minecraft.util.registry.Registry biomes=registries.getRegistry(net.minecraft.util.registry.Registry.BIOME_KEY); - net.minecraft.world.gen.settings.DimensionGeneratorSettings settings=new net.minecraft.world.gen.settings.DimensionGeneratorSettings(4267321L,false,false, - net.minecraft.world.gen.settings.DimensionGeneratorSettings.func_242749_a(types, - net.minecraft.world.DimensionType.getDefaultSimpleRegistry(types,biomes,registries.getRegistry(net.minecraft.util.registry.Registry.NOISE_SETTINGS_KEY),4267321L), - new net.minecraft.world.gen.FlatChunkGenerator(net.minecraft.world.gen.FlatGenerationSettings.func_242869_a(biomes)))); - client.createWorld(folder,new net.minecraft.world.WorldSettings(title,mode,false,net.minecraft.world.Difficulty.NORMAL,commands,new net.minecraft.world.GameRules(),net.minecraft.util.datafix.codec.DatapackCodec.VANILLA_CODEC),registries,settings); + static void create(net.minecraft.client.Minecraft client,String folder,String title,net.minecraft.world.level.GameType mode,boolean commands) { + net.minecraft.core.RegistryAccess.RegistryHolder registries=net.minecraft.core.RegistryAccess.builtin(); + 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(types,biomes,registries.registryOrThrow(net.minecraft.core.Registry.NOISE_GENERATOR_SETTINGS_REGISTRY),4267321L), + new net.minecraft.world.level.levelgen.FlatLevelSource(net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings.getDefault(biomes)))); + 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); } } - static List entities(World world) { - if (world instanceof ServerWorld) return ((ServerWorld) world).getEntities().collect(Collectors.toList()); + static List entities(Level world) { + if (world instanceof ServerLevel) { + List result=new ArrayList<>();((ServerLevel)world).getAllEntities().forEach(result::add);return result; + } return ClientEntities.read(world); } - static Entity loadEntity(CompoundNBT data, World world) { - return net.minecraft.entity.EntityType.loadEntityAndExecute(data, world, entity -> entity); + static Entity loadEntity(CompoundTag data, Level world) { + return net.minecraft.world.entity.EntityType.loadEntityRecursive(data, world, entity -> entity); } - static Entity readChunkEntity(CompoundNBT data, ServerWorld world, net.minecraft.world.chunk.Chunk chunk) { + 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. - CompoundNBT level=new CompoundNBT();net.minecraft.nbt.ListNBT list=new net.minecraft.nbt.ListNBT();list.add(data.copy());level.put("Entities",list); + CompoundTag root=new CompoundTag(),level=new CompoundTag();net.minecraft.nbt.ListTag list=new net.minecraft.nbt.ListTag();list.add(data.copy());level.put("Entities",list); + level.putInt("xPos",chunk.getPos().x);level.putInt("zPos",chunk.getPos().z);root.put("Level",level);root.putInt("DataVersion",2730); + // 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,2730,2730); try { java.lang.reflect.Method nativeRead=null; - for(java.lang.reflect.Method method:net.minecraft.world.chunk.storage.ChunkSerializer.class.getDeclaredMethods()) - if(java.lang.reflect.Modifier.isStatic(method.getModifiers()) && !method.isSynthetic() && method.getReturnType()==void.class && java.util.Arrays.equals(method.getParameterTypes(),new Class[]{CompoundNBT.class,net.minecraft.world.chunk.Chunk.class})) { + for(java.lang.reflect.Method method:net.minecraft.world.level.chunk.storage.ChunkSerializer.class.getDeclaredMethods()) + if(java.lang.reflect.Modifier.isStatic(method.getModifiers()) && !method.isSynthetic() && method.getReturnType()==void.class && java.util.Arrays.equals(method.getParameterTypes(),new Class[]{ServerLevel.class,CompoundTag.class,net.minecraft.world.level.chunk.LevelChunk.class})) { if(nativeRead!=null)throw new IllegalStateException("Ambiguous native entity admission");nativeRead=method; } if(nativeRead==null)throw new IllegalStateException("Missing native entity admission"); - nativeRead.setAccessible(true);nativeRead.invoke(null,level,chunk); - for(net.minecraft.util.ClassInheritanceMultiMap section:chunk.getEntityLists())for(Entity entity:section)return entity; - return null; + nativeRead.setAccessible(true);nativeRead.invoke(null,world,fixed.getCompound("Level"),chunk); + Object manager=singleField(ServerLevel.class,net.minecraft.world.level.entity.PersistentEntitySectionManager.class).get(world); + net.minecraft.world.level.entity.EntitySectionStorage sections=(net.minecraft.world.level.entity.EntitySectionStorage) + singleField(manager.getClass(),net.minecraft.world.level.entity.EntitySectionStorage.class).get(manager); + java.util.UUID id=fixed.getCompound("Level").getList("Entities",10).getCompound(0).getUUID("UUID"); + return sections.getExistingSectionsInChunk(chunk.getPos().toLong()).flatMap(section->section.getEntities()) + .filter(entity->entity instanceof Entity && ((Entity)entity).getUUID().equals(id)).map(entity->(Entity)entity).findFirst().orElse(null); }catch(ReflectiveOperationException failure){throw new IllegalStateException("Native chunk admission failed",failure);} } + private static java.lang.reflect.Field singleField(Class owner,Class type) { + java.lang.reflect.Field found=null; + for(java.lang.reflect.Field field:owner.getDeclaredFields())if(field.getType()==type) { + if(found!=null)throw new IllegalStateException("Ambiguous native field of type "+type.getName());found=field; + } + if(found==null)throw new IllegalStateException("Missing native field of type "+type.getName());found.setAccessible(true);return found; + } + static boolean sectionContains(Entity entity,int chunkX,int chunkZ) { + try { + Object callback=singleField(Entity.class,net.minecraft.world.level.entity.EntityInLevelCallback.class).get(entity); + java.lang.reflect.Field key=null,storage=null; + for(java.lang.reflect.Field field:callback.getClass().getDeclaredFields()) { + if(field.getType()==long.class){if(key!=null)throw new IllegalStateException("Ambiguous native entity section key");key=field;} + if(field.getType()==net.minecraft.world.level.entity.EntitySection.class){if(storage!=null)throw new IllegalStateException("Ambiguous native entity section");storage=field;} + } + if(key==null || storage==null)return false;key.setAccessible(true);storage.setAccessible(true); + long current=key.getLong(callback);net.minecraft.core.SectionPos position=net.minecraft.core.SectionPos.of(current); + // Legacy native admission indexes entities without going through + // ServerLevel.addEntity's Forge onAddedToWorld callback. + return position.x()==chunkX && position.z()==chunkZ + && current==net.minecraft.core.SectionPos.asLong(entity.blockPosition()) + && ((net.minecraft.world.level.entity.EntitySection)storage.get(callback)).getEntities().anyMatch(member->member==entity); + }catch(ReflectiveOperationException failure){throw new IllegalStateException("Cannot inspect native entity section",failure);} + } + static void awaitEntityInbox(ServerLevel world,int x,int z) { + RuntimeChecks.require(world.getServer().isSameThread(),"Entity readiness must run on the native server thread"); + try { + net.minecraft.world.level.entity.PersistentEntitySectionManager manager= + (net.minecraft.world.level.entity.PersistentEntitySectionManager)singleField( + ServerLevel.class,net.minecraft.world.level.entity.PersistentEntitySectionManager.class).get(world); + long chunk=net.minecraft.world.level.ChunkPos.asLong(x,z); + try { + NativeEntityInbox.await(manager,chunk,()->{ + // The real server loop services both executors. Fixture + // callbacks execute before that loop, so pump both queues. + boolean serverTask=world.getServer().pollTask(); + boolean chunkTask=world.getChunkSource().pollTask(); + return serverTask || chunkTask; + },System::nanoTime,System.nanoTime()+java.util.concurrent.TimeUnit.SECONDS.toNanos(30),world.getServer()::managedBlock); + }catch(IllegalStateException failure){ + net.minecraft.world.level.chunk.LevelChunk cached=world.getChunkSource().getChunkNow(x,z); + String state="full="+(cached==null?"absent":cached.getFullStatus())+" manager="+manager.gatherStats(); + for(java.lang.reflect.Field field:manager.getClass().getDeclaredFields()) + if(it.unimi.dsi.fastutil.longs.Long2ObjectMap.class.isAssignableFrom(field.getType())) { + field.setAccessible(true);Object value=((it.unimi.dsi.fastutil.longs.Long2ObjectMap)field.get(manager)).get(chunk); + if(value instanceof Enum)state+=" "+field.getName()+"="+value; + } + System.out.println("NATIVE_ENTITY_INBOX_STATE chunk="+x+","+z+" "+state);throw failure; + } + }catch(IllegalAccessException failure){throw new IllegalStateException("Cannot inspect native entity inbox",failure);} + } static void awaitTicking(Entity entity) { - ServerWorld world=(ServerWorld)entity.world;int x=net.minecraft.util.math.MathHelper.floor(entity.getPosX())>>4,z=net.minecraft.util.math.MathHelper.floor(entity.getPosZ())>>4; - world.forceChunk(x,z,true); - net.minecraft.world.server.ServerChunkProvider provider=world.getChunkProvider(); + ServerLevel world=(ServerLevel)entity.level;int x=net.minecraft.util.Mth.floor(entity.getX())>>4,z=net.minecraft.util.Mth.floor(entity.getZ())>>4; + world.setChunkForced(x,z,true); + net.minecraft.server.level.ServerChunkCache provider=world.getChunkSource(); long deadline=System.nanoTime()+java.util.concurrent.TimeUnit.SECONDS.toNanos(30); - while(!provider.isChunkLoaded(entity)) { + while(!world.isPositionEntityTicking(entity.blockPosition())) { RuntimeChecks.require(System.nanoTime()true); - for(int task=0;task<1000 && provider.driveOneTask();task++) { } + for(int task=0;task<1000 && provider.pollTask();task++) { } if((tick&31)==0)Thread.yield(); } provider.save(true); - RuntimeChecks.require(!provider.chunkExists(x,z),"Fixture chunk remained loaded after native ticket expiry: "+x+","+z); + RuntimeChecks.require(!provider.hasChunk(x,z),"Fixture chunk remained loaded after native ticket expiry: "+x+","+z); } - static void flush(MinecraftServer server) { server.save(true, true, false); } + static void flush(MinecraftServer server) { server.saveAllChunks(true, true, false); } @SuppressWarnings("unchecked") - static java.util.Set tracked(Entity entity) { + static java.util.Set tracked(Entity entity) { try { - Object manager=((ServerWorld)entity.world).getChunkProvider().chunkManager; + Object manager=((ServerLevel)entity.level).getChunkSource().chunkMap; java.lang.reflect.Field map=null; for(java.lang.reflect.Field field:manager.getClass().getDeclaredFields()) if(it.unimi.dsi.fastutil.ints.Int2ObjectMap.class.isAssignableFrom(field.getType())) { @@ -104,7 +167,7 @@ static java.util.Set tracked(Ent } if(map==null)throw new IllegalStateException("Missing native tracking map"); map.setAccessible(true); - Object tracker=((it.unimi.dsi.fastutil.ints.Int2ObjectMap)map.get(manager)).get(entity.getEntityId()); + Object tracker=((it.unimi.dsi.fastutil.ints.Int2ObjectMap)map.get(manager)).get(entity.getId()); if(tracker==null)return java.util.Collections.emptySet(); java.lang.reflect.Field players=null; for(java.lang.reflect.Field field:tracker.getClass().getDeclaredFields()) @@ -113,14 +176,19 @@ static java.util.Set tracked(Ent } if(players==null)throw new IllegalStateException("Missing native tracking set"); players.setAccessible(true); - return new java.util.HashSet<>((java.util.Set)players.get(tracker)); + java.util.Set result=new java.util.HashSet<>(); + for(Object connection:(java.util.Set)players.get(tracker)) { + if(!(connection instanceof net.minecraft.server.network.ServerPlayerConnection))throw new IllegalStateException("Unexpected native tracking member"); + result.add(((net.minecraft.server.network.ServerPlayerConnection)connection).getPlayer()); + } + return result; }catch(ReflectiveOperationException failure){throw new IllegalStateException("Cannot inspect native tracking",failure);} } @OnlyIn(Dist.CLIENT) private static final class ClientEntities { - static List read(World world) { + static List read(Level world) { List result = new ArrayList<>(); - ((net.minecraft.client.world.ClientWorld) world).getAllEntities().forEach(result::add); + ((net.minecraft.client.multiplayer.ClientLevel) world).entitiesForRendering().forEach(result::add); return result; } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java index 064ec74..10bfa28 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java @@ -4,33 +4,34 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.DisconnectedScreen; -import net.minecraft.client.gui.screen.MainMenuScreen; +import net.minecraft.client.gui.screens.DisconnectedScreen; +import net.minecraft.client.gui.screens.TitleScreen; import net.minecraft.client.multiplayer.ServerData; -import net.minecraft.client.gui.screen.ConnectingScreen; +import net.minecraft.client.gui.screens.ConnectScreen; /** Uses ordinary Forge connection setup; retries only a runner-confirmed pre-login encoding failure. */ final class FixtureConnections { private static Thread retiringServer; private FixtureConnections() {} static void disconnect(Minecraft mc) { - if(mc.getIntegratedServer()!=null)retiringServer=mc.getIntegratedServer().getExecutionThread(); - if(mc.player!=null)mc.player.closeScreen(); - if(mc.world!=null)mc.world.sendQuittingDisconnectingPacket(); - mc.unloadWorld();mc.displayGuiScreen(new MainMenuScreen()); + if(mc.getSingleplayerServer()!=null)retiringServer=mc.getSingleplayerServer().getRunningThread(); + if(mc.player!=null)mc.player.closeContainer(); + if(mc.level!=null)mc.level.disconnect(); + mc.clearLevel();mc.setScreen(new TitleScreen()); } static boolean stopped(Minecraft mc) { - return ServerStopGate.ready(mc.world==null,mc.isIntegratedServerRunning(),retiringServer); + return ServerStopGate.ready(mc.level==null,mc.hasSingleplayerServer(),retiringServer); } static boolean ready(String role) { return "A".equals(role)||new File(System.getProperty("lootablebodies.multiplayer.sync"),"client-A-joined").isFile(); } static void connect(Minecraft mc) { String address="127.0.0.1:"+Integer.parseInt(System.getProperty("lootablebodies.multiplayer.port")); - mc.displayGuiScreen(new ConnectingScreen(new MainMenuScreen(),mc,new ServerData("Disposable check",address,false))); + ServerData destination=new ServerData("Disposable check",address,false); + ConnectScreen.startConnecting(new TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(address),destination); } static boolean retry(Minecraft mc,String role) throws Exception { - if(!(mc.currentScreen instanceof DisconnectedScreen)||mc.world!=null)return false; + if(!(mc.screen instanceof DisconnectedScreen)||mc.level!=null)return false; File sync=new File(System.getProperty("lootablebodies.multiplayer.sync")); File permit=new File(sync,"login-retry-"+role),used=new File(sync,"login-retried-"+role); if(!permit.isFile())return false; @@ -39,7 +40,7 @@ static boolean retry(Minecraft mc,String role) throws Exception { RuntimeChecks.require(requested==previous+1&&requested<=3,"Invalid fixture login retry permit"); Files.write(used.toPath(),Integer.toString(requested).getBytes(StandardCharsets.UTF_8)); System.out.println("FIXTURE_FORGE_LOGIN_RETRY role="+role+" attempt="+requested); - mc.displayGuiScreen(new MainMenuScreen());return true; + mc.setScreen(new TitleScreen());return true; } private static int read(File file) throws Exception { return Integer.parseInt(new String(Files.readAllBytes(file.toPath()),StandardCharsets.UTF_8).trim()); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java index 21705dc..7de8758 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java @@ -1,21 +1,21 @@ package cyano.lootable.buildtest; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.Direction; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; import net.minecraftforge.common.capabilities.ICapabilitySerializable; -import net.minecraftforge.fml.common.registry.GameRegistry; +import net.minecraftforge.fmllegacy.common.registry.GameRegistry; /** Controlled dependency used only by sealed disposable upgrade worlds. */ public final class FixtureItems { public static Item keepsake; public static void register(net.minecraftforge.eventbus.api.IEventBus bus) { keepsake=new Item(new Item.Properties()) { - @Override public ICapabilityProvider initCapabilities(ItemStack stack,CompoundNBT nbt){return new Stored();} + @Override public ICapabilityProvider initCapabilities(ItemStack stack,CompoundTag nbt){return new Stored();} }.setRegistryName("lootablebodiesbuildtest:keepsake"); bus.register(new RegistryItems()); } @@ -23,12 +23,12 @@ public static final class RegistryItems { @net.minecraftforge.eventbus.api.SubscribeEvent public void register(net.minecraftforge.event.RegistryEvent.Register event) {event.getRegistry().register(keepsake);} } - private static final class Stored implements ICapabilitySerializable { - private CompoundNBT data=new CompoundNBT(); + 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 boolean hasCapability(Capability capability,Direction side){return false;} public net.minecraftforge.common.util.LazyOptional getCapability(Capability capability,Direction side){return net.minecraftforge.common.util.LazyOptional.empty();} - public CompoundNBT serializeNBT(){return data.copy();} - public void deserializeNBT(CompoundNBT nbt){data=nbt.copy();} + public CompoundTag serializeNBT(){return data.copy();} + public void deserializeNBT(CompoundTag nbt){data=nbt.copy();} } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java index 9d650d9..4b1bfb9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java @@ -1,73 +1,86 @@ package cyano.lootable.buildtest; -import net.minecraft.entity.ai.attributes.Attribute; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.StringNBT; -import net.minecraft.nbt.ShortNBT; -import net.minecraft.nbt.LongNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.IntArrayNBT; -import net.minecraft.nbt.FloatNBT; -import net.minecraft.nbt.DoubleNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ByteNBT; -import net.minecraft.nbt.ByteArrayNBT; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.ByteArrayTag; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.entity.ai.attributes.Attributes; -import net.minecraft.entity.ai.attributes.*; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerLevel; /** Load real entities, compare complete item data and exercise the legacy attribute engine. */ final class HealthAttributeChecks { private interface Check {void run() throws Exception;} - private static ServerWorld world; + private static ServerLevel world; private static final List results=new ArrayList<>(); private static int failures; private static final UUID MOD=UUID.fromString("00000000-0000-4000-8000-000000000141"); - static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { + static void run(MinecraftServer server,ServerLevel target,File folder) throws Exception { world=target;results.clear();failures=0; - INBT[] wrong={StringNBT.valueOf("opaque"),new CompoundNBT(),new ListNBT(),new ByteArrayNBT(new byte[]{1}),new IntArrayNBT(new int[]{1})}; + Tag[] wrong={StringTag.valueOf("opaque"),new CompoundTag(),new ListTag(),new ByteArrayTag(new byte[]{1}),new IntArrayTag(new int[]{1})}; for(String key:new String[]{"Health","AbsorptionAmount"}) { - for(int i=0;ibadScalar(key,raw));} - for(double value:new double[]{Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY,-1,Double.MAX_VALUE,1e-300})check(key+"_invalid_value_"+value,()->badScalar(key,DoubleNBT.valueOf(value))); + for(int i=0;ibadScalar(key,raw));} + for(double value:new double[]{Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY,-1,Double.MAX_VALUE,1e-300})check(key+"_invalid_value_"+value,()->badScalar(key,DoubleTag.valueOf(value))); } - check("health_above_effective_max_retained",()->badScalar("Health",FloatNBT.valueOf(21))); + check("health_above_effective_max_retained",()->badScalar("Health",FloatTag.valueOf(21))); for(int type=0;type<6;type++){final int t=type;check("valid_numeric_scalar_type_"+type,()->numeric(t));} check("valid_zero_health_keeps_death_semantics",HealthAttributeChecks::zero); check("missing_health_defaults_to_loaded_max",HealthAttributeChecks::missingHealth); check("configured_health_and_follow_range_defaults",HealthAttributeChecks::configured); check("configured_health_above_engine_cap_keeps_existing_clamp",HealthAttributeChecks::configuredCap); check("previous_configured_health_survives_later_config_change",HealthAttributeChecks::configuredHistory); - for(int i=0;ibadList(raw));} - check("empty_attribute_list_keeps_constructor_defaults",()->{CompoundNBT n=base();n.put("Attributes",new ListNBT());EntityLootableBody b=read(n);value(b.getMaxHealth(),20,"Empty list changed max");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid empty list quarantined");stable(b);}); - check("Attributes_wrong_element_type",()->{ListNBT raw=new ListNBT();raw.add(StringNBT.valueOf("opaque"));badList(raw);}); + for(int i=0;ibadList(raw));} + check("empty_attribute_list_keeps_constructor_defaults",()->{CompoundTag n=base();n.put("Attributes",new ListTag());EntityLootableBody b=read(n);value(b.getMaxHealth(),20,"Empty list changed max");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid empty list quarantined");stable(b);}); + check("Attributes_wrong_element_type",()->{ListTag raw=new ListTag();raw.add(StringTag.valueOf("opaque"));badList(raw);}); check("attribute_missing_name",()->badAttribute("Name",null)); - check("attribute_wrong_name_type",()->badAttribute("Name",IntNBT.valueOf(1))); - check("attribute_unknown_name_retained",()->badAttribute("Name",StringNBT.valueOf("absentfixture:attribute"))); + check("attribute_wrong_name_type",()->badAttribute("Name",IntTag.valueOf(1))); + check("attribute_unknown_name_retained",()->badAttribute("Name",StringTag.valueOf("absentfixture:attribute"))); check("attribute_missing_base",()->badAttribute("Base",null)); - for(int i=0;ibadAttribute("Base",raw));} - for(double value:new double[]{Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,-1,0,Double.MAX_VALUE})check("Base_invalid_value_"+value,()->badAttribute("Base",DoubleNBT.valueOf(value))); + for(int i=0;ibadAttribute("Base",raw));} + for(double value:new double[]{Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY,-1,0,Double.MAX_VALUE})check("Base_invalid_value_"+value,()->badAttribute("Base",DoubleTag.valueOf(value))); check("invalid_base_does_not_hide_later_valid_attribute",HealthAttributeChecks::validTail); check("duplicate_attribute_keeps_first_valid_record",HealthAttributeChecks::duplicateAttribute); check("unknown_attribute_payload_preserved_with_valid_fields",HealthAttributeChecks::unknownAttributeData); - check("Modifiers_wrong_list_type_keeps_base",()->badModifierList(StringNBT.valueOf("opaque"))); - check("Modifiers_wrong_element_type_keeps_base",()->{ListNBT l=new ListNBT();l.add(IntNBT.valueOf(3));badModifierList(l);}); + check("Modifiers_wrong_list_type_keeps_base",()->badModifierList(StringTag.valueOf("opaque"))); + check("Modifiers_wrong_element_type_keeps_base",()->{ListTag l=new ListTag();l.add(IntTag.valueOf(3));badModifierList(l);}); for(String field:new String[]{"Name","Amount","Operation","UUIDMost","UUIDLeast"}) { check("modifier_missing_"+field,()->badModifier(field,null)); - check("modifier_wrong_type_"+field,()->badModifier(field,field.equals("Name")?IntNBT.valueOf(1):StringNBT.valueOf("opaque"))); + check("modifier_wrong_type_"+field,()->badModifier(field,field.equals("Name")?IntTag.valueOf(1):StringTag.valueOf("opaque"))); } - check("modifier_blank_name",()->badModifier("Name",StringNBT.valueOf(""))); - for(double value:new double[]{Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY})check("modifier_nonfinite_amount_"+value,()->badModifier("Amount",DoubleNBT.valueOf(value))); - for(INBT value:new INBT[]{IntNBT.valueOf(-1),IntNBT.valueOf(3),LongNBT.valueOf(4294967296L),FloatNBT.valueOf(1F)})check("modifier_invalid_operation_"+value,()->badModifier("Operation",value)); - check("modifier_fractional_UUID_not_coerced",()->badModifier("UUIDMost",DoubleNBT.valueOf(1.5))); + check("modifier_blank_name",()->badModifier("Name",StringTag.valueOf(""))); + for(double value:new double[]{Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY})check("modifier_nonfinite_amount_"+value,()->badModifier("Amount",DoubleTag.valueOf(value))); + for(Tag value:new Tag[]{IntTag.valueOf(-1),IntTag.valueOf(3),LongTag.valueOf(4294967296L),FloatTag.valueOf(1F)})check("modifier_invalid_operation_"+value,()->badModifier("Operation",value)); + check("modifier_fractional_UUID_not_coerced",()->badModifier("UUIDMost",DoubleTag.valueOf(1.5))); check("duplicate_modifier_UUID_keeps_first",HealthAttributeChecks::duplicateModifier); check("bad_modifier_keeps_valid_base_and_tail",HealthAttributeChecks::modifierTail); check("unknown_modifier_payload_retained",HealthAttributeChecks::unknownModifierData); @@ -94,50 +107,50 @@ private static void check(String name,Check test){ catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("HEALTH_ATTRIBUTE_FAIL "+name+": "+e);e.printStackTrace();} finally{LootableBodies.corpseHP=hp;for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))world.removeEntity(e);} } - static CompoundNBT base(){EntityLootableBody b=new EntityLootableBody(world);b.setPosition(160.5,80,160.5);b.setNoGravity(true);b.setUserName("HealthFixture");b.setInventorySlotContents(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(b);return NameLifetimeChecks.saved(b);} - static EntityLootableBody read(CompoundNBT n){CompoundNBT before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.read(n);RuntimeChecks.require(NameLifetimeChecks.same(before,n),"Input NBT mutated");return b;} - static CompoundNBT attribute(String name,double base){CompoundNBT n=new CompoundNBT();n.putString("Name",name);n.putDouble("Base",base);return n;} - private static void legacyUuid(CompoundNBT n,UUID id){n.remove("UUID");n.putLong("UUIDMost",id.getMostSignificantBits());n.putLong("UUIDLeast",id.getLeastSignificantBits());} - static CompoundNBT modifier(double amount,int operation){CompoundNBT n=new CompoundNBT();n.putString("Name","Fixture modifier");n.putDouble("Amount",amount);n.putInt("Operation",operation);legacyUuid(n,MOD);return n;} - static CompoundNBT with(CompoundNBT a){CompoundNBT n=base();n.put("Attributes",NbtRecoveryChecks.list(a));return n;} + 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;} + static CompoundTag attribute(String name,double base){CompoundTag n=new CompoundTag();n.putString("Name",name);n.putDouble("Base",base);return n;} + private static void legacyUuid(CompoundTag n,UUID id){n.remove("UUID");n.putLong("UUIDMost",id.getMostSignificantBits());n.putLong("UUIDLeast",id.getLeastSignificantBits());} + static CompoundTag modifier(double amount,int operation){CompoundTag n=new CompoundTag();n.putString("Name","Fixture modifier");n.putDouble("Amount",amount);n.putInt("Operation",operation);legacyUuid(n,MOD);return n;} + static CompoundTag with(CompoundTag a){CompoundTag n=base();n.put("Attributes",NbtRecoveryChecks.list(a));return n;} private static void value(double a,double b,String why){RuntimeChecks.require(Double.isFinite(a) && Math.abs(a-b)<1e-6,why+" expected="+b+" actual="+a);} - private static void inventoryKept(EntityLootableBody b){RuntimeChecks.require(RuntimeChecks.sameStack(b.getStackInSlot(6),NbtRecoveryChecks.item(120,17)),"Complete recoverable inventory changed");} - private static void stable(EntityLootableBody b){CompoundNBT a=NameLifetimeChecks.saved(b),z=NameLifetimeChecks.saved(read(a));for(String key:new String[]{"Health","AbsorptionAmount","Attributes","Equipment","Aux",NameLifetimeChecks.RECOVERY,NbtRecoveryChecks.RECOVERY})RuntimeChecks.require(NameLifetimeChecks.same(a.get(key),z.get(key)),"Round trip changed "+key);inventoryKept(b);} - private static void retained(EntityLootableBody b,String key,INBT raw){NameLifetimeChecks.retained(b,key,raw);} - private static void badScalar(String key,INBT raw){CompoundNBT n=base();n.put(key,raw.copy());EntityLootableBody b=read(n);value(key.equals("Health")?b.getHealth():b.getAbsorptionAmount(),key.equals("Health")?20:0,"Bad scalar did not use safe default");RuntimeChecks.require(b.isAlive(),"Malformed scalar made corpse inaccessible");retained(b,key,raw);stable(b);} - private static void numeric(int type){INBT[] values={ByteNBT.valueOf((byte)1),ShortNBT.valueOf((short)1),IntNBT.valueOf(1),LongNBT.valueOf(1),FloatNBT.valueOf(1),DoubleNBT.valueOf(1)};CompoundNBT n=base();n.put("Health",values[type]);n.put("AbsorptionAmount",values[type].copy());EntityLootableBody b=read(n);value(b.getHealth(),1,"Valid Health changed");value(b.getAbsorptionAmount(),1,"Valid absorption changed");stable(b);} - private static void zero(){CompoundNBT n=base();n.putFloat("Health",0);EntityLootableBody b=read(n);value(b.getHealth(),0,"Legitimate zero revived");RuntimeChecks.require(!b.isAlive(),"Zero health no longer starts death handling");inventoryKept(b);} - private static void missingHealth(){CompoundNBT n=with(attribute("generic.maxHealth",30));n.remove("Health");EntityLootableBody b=read(n);value(b.getHealth(),30,"Missing Health did not use loaded max");stable(b);} - private static void configured(){LootableBodies.corpseHP=37;CompoundNBT n=base();n.remove("Attributes");n.remove("Health");EntityLootableBody b=read(n);value(b.getMaxHealth(),37,"Config health changed");value(b.getAttribute(Attributes.FOLLOW_RANGE).getBaseValue(),16,"EntityLiving follow-range default changed");stable(b);} - private static void configuredCap(){LootableBodies.corpseHP=Short.MAX_VALUE;EntityLootableBody original=new EntityLootableBody(world);CompoundNBT n=NameLifetimeChecks.saved(original);EntityLootableBody b=read(n);value(b.getMaxHealth(),original.getMaxHealth(),"Existing configured health clamp changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy configured base quarantined");} - private static void configuredHistory(){LootableBodies.corpseHP=Short.MAX_VALUE;EntityLootableBody old=new EntityLootableBody(world);old.setInventorySlotContents(6,NbtRecoveryChecks.item(120,17));CompoundNBT n=NameLifetimeChecks.saved(old);LootableBodies.corpseHP=20;EntityLootableBody b=read(n);value(b.getMaxHealth(),old.getMaxHealth(),"Valid old configured base was replaced by today's default");value(b.getHealth(),old.getHealth(),"Valid old configured health changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy historical config quarantined");stable(b);} - private static void badList(INBT raw){CompoundNBT n=base();n.put("Attributes",raw.copy());EntityLootableBody b=read(n);value(b.getMaxHealth(),20,"Bad list changed max health");retained(b,"Attributes",raw);stable(b);} - private static void badAttribute(String field,INBT raw){CompoundNBT a=attribute("generic.maxHealth",30);if(raw==null)a.remove(field);else a.put(field,raw.copy());EntityLootableBody b=read(with(a));value(b.getMaxHealth(),20,"Bad attribute changed default");retained(b,"Attribute",a);stable(b);} - private static void validTail(){CompoundNBT a=attribute("generic.maxHealth",Double.NaN),n=base();n.put("Attributes",NbtRecoveryChecks.list(a,attribute("generic.maxHealth",30),attribute("generic.followRange",24)));EntityLootableBody b=read(n);value(b.getMaxHealth(),30,"Invalid first entry hid valid max health");value(b.getAttribute(Attributes.FOLLOW_RANGE).getBaseValue(),24,"Valid tail lost");retained(b,"Attribute",a);stable(b);} - private static void duplicateAttribute(){CompoundNBT duplicate=attribute("generic.maxHealth",40),n=base();n.put("Attributes",NbtRecoveryChecks.list(attribute("generic.maxHealth",30),duplicate));EntityLootableBody b=read(n);value(b.getMaxHealth(),30,"Duplicate overwrote first max health");retained(b,"Attribute",duplicate);stable(b);} - private static void unknownAttributeData(){CompoundNBT a=attribute("generic.maxHealth",30);a.putString("Opaque","retain");EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Known fields lost with unknown sibling");retained(b,"Attribute",a);stable(b);} - private static void badModifierList(INBT raw){CompoundNBT a=attribute("generic.maxHealth",30);a.put("Modifiers",raw.copy());EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Invalid modifiers lost valid base");retained(b,"Attribute",a);stable(b);} - private static void badModifier(String field,INBT raw){CompoundNBT m=modifier(5,0);if(raw==null)m.remove(field);else m.put(field,raw.copy());CompoundNBT a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(m));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Malformed modifier activated");retained(b,"Attribute",a);stable(b);} - private static void duplicateModifier(){CompoundNBT a=attribute("generic.maxHealth",20);a.put("Modifiers",NbtRecoveryChecks.list(modifier(2,0),modifier(7,0)));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),22,"Duplicate modifier replaced first");retained(b,"Attribute",a);stable(b);} - private static void modifierTail(){CompoundNBT bad=modifier(Double.NaN,0),good=modifier(5,0),a=attribute("generic.maxHealth",30);legacyUuid(good,new UUID(0,142));a.put("Modifiers",NbtRecoveryChecks.list(bad,good));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),35,"Invalid modifier hid valid tail or base");retained(b,"Attribute",a);stable(b);} - private static void unknownModifierData(){CompoundNBT m=modifier(5,0),a=attribute("generic.maxHealth",30);m.putString("Opaque","retain");a.put("Modifiers",NbtRecoveryChecks.list(m));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),35,"Known modifier fields lost");retained(b,"Attribute",a);stable(b);} - private static void overflow(){CompoundNBT a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(modifier(Double.MAX_VALUE,2)));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Overflowing modifier activated");retained(b,"Attribute",a);stable(b);} - private static void validModifiers(){CompoundNBT a=attribute("generic.maxHealth",30);CompoundNBT m0=modifier(-2,0),m1=modifier(.5,1),m2=modifier(.25,2);legacyUuid(m1,new UUID(0,142));legacyUuid(m2,new UUID(0,143));a.put("Modifiers",NbtRecoveryChecks.list(m0,m1,m2));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),52.5,"Valid signed/three-operation modifiers changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid modifiers quarantined");stable(b);} - private static void zeroUuid(){CompoundNBT a=attribute("generic.maxHealth",30),m=modifier(5,0);m.putLong("UUIDMost",0);m.putLong("UUIDLeast",0);m.putByte("Operation",(byte)0);m.putInt("Amount",5);a.put("Modifiers",NbtRecoveryChecks.list(m));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),35,"Zero UUID or lossless numbers rejected");stable(b);} - private static void validBases(){for(INBT raw:new INBT[]{ByteNBT.valueOf((byte)30),ShortNBT.valueOf((short)30),IntNBT.valueOf(30),LongNBT.valueOf(30),FloatNBT.valueOf(30.5F),DoubleNBT.valueOf(30.5)}){CompoundNBT a=attribute("generic.maxHealth",30);a.put("Base",raw);CompoundNBT n=with(a);n.putDouble("Health",12.5);EntityLootableBody b=read(n);value(b.getMaxHealth(),((NumberNBT)raw).getDouble(),"Valid base changed");value(b.getHealth(),12.5,"Fractional Health changed");stable(b);}} - private static void repeated(){EntityLootableBody b=read(with(attribute("generic.maxHealth",30)));CompoundNBT n=base();n.remove("Attributes");n.remove("Health");n.remove("AbsorptionAmount");b.setAbsorptionAmount(5);b.read(n);value(b.getMaxHealth(),20,"Missing Attributes retained previous max");value(b.getHealth(),20,"Missing Health retained previous value");value(b.getAbsorptionAmount(),0,"Missing absorption retained previous value");stable(b);} - private static void repeatedModifiers(){CompoundNBT a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(modifier(5,0)));EntityLootableBody b=read(with(a));b.read(with(attribute("generic.maxHealth",20)));value(b.getMaxHealth(),20,"Repeated read kept obsolete modifier");stable(b);} - private static void pendingRemoval(){CompoundNBT n=base();n.putFloat("Health",0);EntityLootableBody b=read(n);b.baseTick();n=base();n.putDouble("Health",Double.NaN);b.read(n);b.baseTick();RuntimeChecks.require(b.isAlive(),"Repaired owner still dead");inventoryKept(b);stable(b);n=base();n.putFloat("Health",3.5F);b.removed=true;b.read(n);RuntimeChecks.require(b.isAlive(),"Reused entity kept stale dead flag");value(b.getHealth(),3.5,"Reset healed valid wounded corpse");} - private static void inventory(){CompoundNBT n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);n.putDouble("Health",Double.NaN);CompoundNBT a=attribute("generic.maxHealth",Double.NaN);n.put("Attributes",NbtRecoveryChecks.list(a,attribute("generic.followRange",24)));EntityLootableBody b=read(n);RuntimeChecks.require(b.isAlive(),"Invalid health made recoverable inventory inaccessible");NbtRecoveryChecks.retained(b,raw);retained(b,"Attribute",a);retained(b,"Health",n.get("Health"));stable(b);stable(read(NameLifetimeChecks.saved(b)));} - private static void empty(){CompoundNBT n=base();n.put("Equipment",new ListNBT());n.putDouble("Health",Double.NaN);EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Health evidence counted as inventory");retained(b,"Health",n.get("Health"));} - private static void healthy(){CompoundNBT n=base();EntityLootableBody b=read(n);RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy save gained recovery records");stable(b);} + private static void inventoryKept(EntityLootableBody b){RuntimeChecks.require(RuntimeChecks.sameStack(b.getItem(6),NbtRecoveryChecks.item(120,17)),"Complete recoverable inventory changed");} + private static void stable(EntityLootableBody b){CompoundTag a=NameLifetimeChecks.saved(b),z=NameLifetimeChecks.saved(read(a));for(String key:new String[]{"Health","AbsorptionAmount","Attributes","Equipment","Aux",NameLifetimeChecks.RECOVERY,NbtRecoveryChecks.RECOVERY})RuntimeChecks.require(NameLifetimeChecks.same(a.get(key),z.get(key)),"Round trip changed "+key);inventoryKept(b);} + private static void retained(EntityLootableBody b,String key,Tag raw){NameLifetimeChecks.retained(b,key,raw);} + private static void badScalar(String key,Tag raw){CompoundTag n=base();n.put(key,raw.copy());EntityLootableBody b=read(n);value(key.equals("Health")?b.getHealth():b.getAbsorptionAmount(),key.equals("Health")?20:0,"Bad scalar did not use safe default");RuntimeChecks.require(b.isAlive(),"Malformed scalar made corpse inaccessible");retained(b,key,raw);stable(b);} + private static void numeric(int type){Tag[] values={ByteTag.valueOf((byte)1),ShortTag.valueOf((short)1),IntTag.valueOf(1),LongTag.valueOf(1),FloatTag.valueOf(1),DoubleTag.valueOf(1)};CompoundTag n=base();n.put("Health",values[type]);n.put("AbsorptionAmount",values[type].copy());EntityLootableBody b=read(n);value(b.getHealth(),1,"Valid Health changed");value(b.getAbsorptionAmount(),1,"Valid absorption changed");stable(b);} + private static void zero(){CompoundTag n=base();n.putFloat("Health",0);EntityLootableBody b=read(n);value(b.getHealth(),0,"Legitimate zero revived");RuntimeChecks.require(!b.isAlive(),"Zero health no longer starts death handling");inventoryKept(b);} + private static void missingHealth(){CompoundTag n=with(attribute("generic.maxHealth",30));n.remove("Health");EntityLootableBody b=read(n);value(b.getHealth(),30,"Missing Health did not use loaded max");stable(b);} + private static void configured(){LootableBodies.corpseHP=37;CompoundTag n=base();n.remove("Attributes");n.remove("Health");EntityLootableBody b=read(n);value(b.getMaxHealth(),37,"Config health changed");value(b.getAttribute(Attributes.FOLLOW_RANGE).getBaseValue(),16,"EntityLiving follow-range default changed");stable(b);} + private static void configuredCap(){LootableBodies.corpseHP=Short.MAX_VALUE;EntityLootableBody original=new EntityLootableBody(world);CompoundTag n=NameLifetimeChecks.saved(original);EntityLootableBody b=read(n);value(b.getMaxHealth(),original.getMaxHealth(),"Existing configured health clamp changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy configured base quarantined");} + private static void configuredHistory(){LootableBodies.corpseHP=Short.MAX_VALUE;EntityLootableBody old=new EntityLootableBody(world);old.setItem(6,NbtRecoveryChecks.item(120,17));CompoundTag n=NameLifetimeChecks.saved(old);LootableBodies.corpseHP=20;EntityLootableBody b=read(n);value(b.getMaxHealth(),old.getMaxHealth(),"Valid old configured base was replaced by today's default");value(b.getHealth(),old.getHealth(),"Valid old configured health changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy historical config quarantined");stable(b);} + private static void badList(Tag raw){CompoundTag n=base();n.put("Attributes",raw.copy());EntityLootableBody b=read(n);value(b.getMaxHealth(),20,"Bad list changed max health");retained(b,"Attributes",raw);stable(b);} + private static void badAttribute(String field,Tag raw){CompoundTag a=attribute("generic.maxHealth",30);if(raw==null)a.remove(field);else a.put(field,raw.copy());EntityLootableBody b=read(with(a));value(b.getMaxHealth(),20,"Bad attribute changed default");retained(b,"Attribute",a);stable(b);} + private static void validTail(){CompoundTag a=attribute("generic.maxHealth",Double.NaN),n=base();n.put("Attributes",NbtRecoveryChecks.list(a,attribute("generic.maxHealth",30),attribute("generic.followRange",24)));EntityLootableBody b=read(n);value(b.getMaxHealth(),30,"Invalid first entry hid valid max health");value(b.getAttribute(Attributes.FOLLOW_RANGE).getBaseValue(),24,"Valid tail lost");retained(b,"Attribute",a);stable(b);} + private static void duplicateAttribute(){CompoundTag duplicate=attribute("generic.maxHealth",40),n=base();n.put("Attributes",NbtRecoveryChecks.list(attribute("generic.maxHealth",30),duplicate));EntityLootableBody b=read(n);value(b.getMaxHealth(),30,"Duplicate overwrote first max health");retained(b,"Attribute",duplicate);stable(b);} + private static void unknownAttributeData(){CompoundTag a=attribute("generic.maxHealth",30);a.putString("Opaque","retain");EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Known fields lost with unknown sibling");retained(b,"Attribute",a);stable(b);} + private static void badModifierList(Tag raw){CompoundTag a=attribute("generic.maxHealth",30);a.put("Modifiers",raw.copy());EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Invalid modifiers lost valid base");retained(b,"Attribute",a);stable(b);} + private static void badModifier(String field,Tag raw){CompoundTag m=modifier(5,0);if(raw==null)m.remove(field);else m.put(field,raw.copy());CompoundTag a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(m));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Malformed modifier activated");retained(b,"Attribute",a);stable(b);} + private static void duplicateModifier(){CompoundTag a=attribute("generic.maxHealth",20);a.put("Modifiers",NbtRecoveryChecks.list(modifier(2,0),modifier(7,0)));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),22,"Duplicate modifier replaced first");retained(b,"Attribute",a);stable(b);} + private static void modifierTail(){CompoundTag bad=modifier(Double.NaN,0),good=modifier(5,0),a=attribute("generic.maxHealth",30);legacyUuid(good,new UUID(0,142));a.put("Modifiers",NbtRecoveryChecks.list(bad,good));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),35,"Invalid modifier hid valid tail or base");retained(b,"Attribute",a);stable(b);} + private static void unknownModifierData(){CompoundTag m=modifier(5,0),a=attribute("generic.maxHealth",30);m.putString("Opaque","retain");a.put("Modifiers",NbtRecoveryChecks.list(m));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),35,"Known modifier fields lost");retained(b,"Attribute",a);stable(b);} + private static void overflow(){CompoundTag a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(modifier(Double.MAX_VALUE,2)));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Overflowing modifier activated");retained(b,"Attribute",a);stable(b);} + private static void validModifiers(){CompoundTag a=attribute("generic.maxHealth",30);CompoundTag m0=modifier(-2,0),m1=modifier(.5,1),m2=modifier(.25,2);legacyUuid(m1,new UUID(0,142));legacyUuid(m2,new UUID(0,143));a.put("Modifiers",NbtRecoveryChecks.list(m0,m1,m2));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),52.5,"Valid signed/three-operation modifiers changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid modifiers quarantined");stable(b);} + private static void zeroUuid(){CompoundTag a=attribute("generic.maxHealth",30),m=modifier(5,0);m.putLong("UUIDMost",0);m.putLong("UUIDLeast",0);m.putByte("Operation",(byte)0);m.putInt("Amount",5);a.put("Modifiers",NbtRecoveryChecks.list(m));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),35,"Zero UUID or lossless numbers rejected");stable(b);} + private static void validBases(){for(Tag raw:new Tag[]{ByteTag.valueOf((byte)30),ShortTag.valueOf((short)30),IntTag.valueOf(30),LongTag.valueOf(30),FloatTag.valueOf(30.5F),DoubleTag.valueOf(30.5)}){CompoundTag a=attribute("generic.maxHealth",30);a.put("Base",raw);CompoundTag n=with(a);n.putDouble("Health",12.5);EntityLootableBody b=read(n);value(b.getMaxHealth(),((NumericTag)raw).getAsDouble(),"Valid base changed");value(b.getHealth(),12.5,"Fractional Health changed");stable(b);}} + private static void repeated(){EntityLootableBody b=read(with(attribute("generic.maxHealth",30)));CompoundTag n=base();n.remove("Attributes");n.remove("Health");n.remove("AbsorptionAmount");b.setAbsorptionAmount(5);b.load(n);value(b.getMaxHealth(),20,"Missing Attributes retained previous max");value(b.getHealth(),20,"Missing Health retained previous value");value(b.getAbsorptionAmount(),0,"Missing absorption retained previous value");stable(b);} + private static void repeatedModifiers(){CompoundTag a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(modifier(5,0)));EntityLootableBody b=read(with(a));b.load(with(attribute("generic.maxHealth",20)));value(b.getMaxHealth(),20,"Repeated read kept obsolete modifier");stable(b);} + private static void pendingRemoval(){CompoundTag n=base();n.putFloat("Health",0);EntityLootableBody b=read(n);b.baseTick();n=base();n.putDouble("Health",Double.NaN);b.load(n);b.baseTick();RuntimeChecks.require(b.isAlive(),"Repaired owner still dead");inventoryKept(b);stable(b);n=base();n.putFloat("Health",3.5F);b.discard();RuntimeChecks.require(b.isRemoved(),"Native removal fixture was not applied");b.load(n);RuntimeChecks.require(b.isAlive(),"Reused entity kept stale dead flag");value(b.getHealth(),3.5,"Reset healed valid wounded corpse");} + private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);n.putDouble("Health",Double.NaN);CompoundTag a=attribute("generic.maxHealth",Double.NaN);n.put("Attributes",NbtRecoveryChecks.list(a,attribute("generic.followRange",24)));EntityLootableBody b=read(n);RuntimeChecks.require(b.isAlive(),"Invalid health made recoverable inventory inaccessible");NbtRecoveryChecks.retained(b,raw);retained(b,"Attribute",a);retained(b,"Health",n.get("Health"));stable(b);stable(read(NameLifetimeChecks.saved(b)));} + private static void empty(){CompoundTag n=base();n.put("Equipment",new ListTag());n.putDouble("Health",Double.NaN);EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Health evidence counted as inventory");retained(b,"Health",n.get("Health"));} + private static void healthy(){CompoundTag n=base();EntityLootableBody b=read(n);RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy save gained recovery records");stable(b);} private static void nativeUuid(boolean conflict,boolean malformed){ - CompoundNBT modifier=modifier(5,0);modifier.remove("UUIDMost");modifier.remove("UUIDLeast"); - modifier.putUniqueId("UUID",MOD); + CompoundTag modifier=modifier(5,0);modifier.remove("UUIDMost");modifier.remove("UUIDLeast"); + modifier.putUUID("UUID",MOD); if(conflict){modifier.putLong("UUIDMost",3);modifier.putLong("UUIDLeast",4);} if(malformed)modifier.putIntArray("UUID",new int[]{1,2,3}); - CompoundNBT attribute=attribute("minecraft:generic.max_health",30);attribute.put("Modifiers",NbtRecoveryChecks.list(modifier)); + CompoundTag attribute=attribute("minecraft:generic.max_health",30);attribute.put("Modifiers",NbtRecoveryChecks.list(modifier)); EntityLootableBody body=read(with(attribute));value(body.getMaxHealth(),conflict||malformed?30:35,"Native UUID modifier handling changed"); if(conflict||malformed)retained(body,"Attribute",attribute); else RuntimeChecks.require(!NameLifetimeChecks.saved(body).contains(NameLifetimeChecks.RECOVERY),"Valid native modifier quarantined"); @@ -145,25 +158,25 @@ private static void nativeUuid(boolean conflict,boolean malformed){ } private static void legacyForgeAttributes(){ String[][] names={{"forge.swimSpeed","forge:swim_speed"},{"forge.nameTagDistance","forge:nametag_distance"},{"forge.entity_gravity","forge:entity_gravity"}}; - double[] bases={1.75D,70D,.12D};ListNBT list=new ListNBT();for(int i=0;i results = new ArrayList<>(); private static int failures; private static int sequence; - private static ServerWorld world; + private static ServerLevel world; private static MinecraftServer server; - public static void run(MinecraftServer instance, ServerWorld target, File folder) throws Exception { + public static void run(MinecraftServer instance, ServerLevel target, File folder) throws Exception { server=instance; world=target; results.clear(); failures=0; check("failed_corpse_spawn_keeps_original_drops",InventorySafetyChecks::failedSpawn); check("cancelled_corpse_spawn_leaves_vanilla_drops",InventorySafetyChecks::cancelledSpawn); @@ -70,11 +73,11 @@ public static void run(MinecraftServer instance, ServerWorld target, File folder } private static void check(String name,Check check) { List before=new ArrayList<>(FixtureApi.entities(world)); - String keep=world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).toString(); - String mobLoot=world.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_LOOT).toString(); + String keep=world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).toString(); + String mobLoot=world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).toString(); try { - world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(false,null); - world.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_LOOT).set(false,null); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).set(false,null); check.run(); results.add("PASS "+name); System.out.println("INVENTORY_CHECK_PASS "+name); } catch(Throwable error) { @@ -82,57 +85,56 @@ private static void check(String name,Check check) { System.out.println("INVENTORY_CHECK_FAIL "+name+": "+error); error.printStackTrace(); } finally { - world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(Boolean.parseBoolean(keep),null); - world.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_LOOT).set(Boolean.parseBoolean(mobLoot),null); + 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); } } - private static ServerPlayerEntity player() { - ServerPlayerEntity player=new ServerPlayerEntity(server,world, - new GameProfile(UUID.nameUUIDFromBytes(("inventory-fixture-"+(sequence++)).getBytes(java.nio.charset.StandardCharsets.UTF_8)),"InventoryFixture"), - new net.minecraft.server.management.PlayerInteractionManager(world)); - player.connection=new net.minecraft.network.play.ServerPlayNetHandler(server, - new net.minecraft.network.NetworkManager(net.minecraft.network.PacketDirection.SERVERBOUND),player); - player.setPosition(64.5,80,64.5); RuntimeChecks.awaitChunk(world,4,4); + private static ServerPlayer player() { + ServerPlayer player=new ServerPlayer(server,world, + new GameProfile(UUID.nameUUIDFromBytes(("inventory-fixture-"+(sequence++)).getBytes(java.nio.charset.StandardCharsets.UTF_8)),"InventoryFixture")); + player.connection=new net.minecraft.server.network.ServerGamePacketListenerImpl(server, + new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND),player); + player.setPos(64.5,80,64.5); RuntimeChecks.awaitChunk(world,4,4); return player; } private static EntityLootableBody body() { EntityLootableBody body=new EntityLootableBody(world); - body.setPosition(64.5,80,64.5); body.setNoGravity(true); + body.setPos(64.5,80,64.5); body.setNoGravity(true); RuntimeChecks.awaitChunk(world,4,4); RuntimeChecks.disableVacuum(body); return body; } private static ItemStack item(int id,int count) { ItemStack stack=RuntimeChecks.stack(id); stack.setCount(count); return stack; } - private static List drops(ServerPlayerEntity player,ItemStack... stacks) { + private static List drops(ServerPlayer player,ItemStack... stacks) { List result=new ArrayList<>(); - for(ItemStack stack:stacks) result.add(new ItemEntity(player.getEntityWorld(),player.getPosX(),player.getPosY(),player.getPosZ(),stack)); + for(ItemStack stack:stacks) result.add(new ItemEntity(player.getCommandSenderWorld(),player.getX(),player.getY(),player.getZ(),stack)); return result; } - private static void handle(PlayerDeathEventHandler handler,ServerPlayerEntity player,List drops) { + private static void handle(PlayerDeathEventHandler handler,ServerPlayer player,List drops) { handler.entityDropEvent(new LivingDropsEvent(player,DamageSource.OUT_OF_WORLD,drops,0,false)); } - private static CompoundNBT data(EntityLootableBody body) { - CompoundNBT result=new CompoundNBT(); body.writeAdditional(result); return result; + private static CompoundTag data(EntityLootableBody body) { + CompoundTag result=new CompoundTag(); body.addAdditionalSaveData(result); return result; } - private static CompoundNBT encoded(ItemStack stack) { return stack.write(new CompoundNBT()); } + private static CompoundTag encoded(ItemStack stack) { return stack.save(new CompoundTag()); } private static Map inventory(EntityLootableBody body) { Map result=new TreeMap<>(); - for(int i=0;i result,ItemStack stack) { if((stack == null || stack.isEmpty()) || stack.getCount()<=0) return; - CompoundNBT tag=encoded(stack); tag.remove("Count"); + CompoundTag tag=encoded(stack); tag.remove("Count"); String key=tag.toString(); result.put(key,result.getOrDefault(key,0)+stack.getCount()); } private static Map dropped() { Map result=new TreeMap<>(); - for(Entity entity:FixtureApi.entities(world)) if(entity instanceof ItemEntity && !entity.removed) add(result,((ItemEntity)entity).getItem()); + for(Entity entity:FixtureApi.entities(world)) if(entity instanceof ItemEntity && !entity.isRemoved()) add(result,((ItemEntity)entity).getItem()); return result; } private static EntityLootableBody newest(Set before) { @@ -140,11 +142,11 @@ private static EntityLootableBody newest(Set before) { throw new AssertionError("No corpse spawned"); } private static void failedSpawn() { - ServerPlayerEntity player=player(); RejectingWorld rejected=new RejectingWorld(world); - player.world=rejected; - ItemStack bones=new ItemStack(Items.BONE,9); bones.setDisplayName(new net.minecraft.util.text.StringTextComponent("Saved bones")); + 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")); List drops=drops(player,bones,item(19,23)); - List before=new ArrayList<>(); for(ItemEntity e:drops) before.add(encoded(e.getItem())); + 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)); handle(handler,player,drops); @@ -159,21 +161,21 @@ public static final class RejectCorpse { } } private static void cancelledSpawn() { - ServerPlayerEntity player=player(); player.inventory.mainInventory.set(0,item(4,1)); player.inventory.mainInventory.set(9,item(19,23)); - Map expected=new TreeMap<>(); add(expected,player.inventory.mainInventory.get(0));add(expected,player.inventory.mainInventory.get(9)); + ServerPlayer player=player(); player.getInventory().items.set(0,item(4,1)); player.getInventory().items.set(9,item(19,23)); + 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.onDeath(DamageSource.OUT_OF_WORLD); } finally { MinecraftForge.EVENT_BUS.unregister(reject); } + try { player.die(DamageSource.OUT_OF_WORLD); } 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"); } private static void staleEquipment() { - ServerPlayerEntity player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); - ItemStack previous=item(0,1); player.setItemStackToSlot(EquipmentSlotType.HEAD,previous); + 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)); handle(handler,player,drops(player,previous)); - ItemStack current=item(1,1); player.setItemStackToSlot(EquipmentSlotType.HEAD,current); + ItemStack current=item(1,1); player.setItemSlot(EquipmentSlot.HEAD,current); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); Map expected=new TreeMap<>(); add(expected,current);add(expected,previous); Set before=new HashSet<>(FixtureApi.entities(world)); @@ -189,15 +191,15 @@ private static void staleEquipment() { return (Map)field.get(handler); } private static void cacheLifecycle() throws Exception { - ServerPlayerEntity player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); - player.setItemStackToSlot(EquipmentSlotType.HEAD,item(0,1)); + ServerPlayer player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); + player.setItemSlot(EquipmentSlot.HEAD,item(0,1)); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); - handle(handler,player,drops(player,player.getItemStackFromSlot(EquipmentSlotType.HEAD))); + handle(handler,player,drops(player,player.getItemBySlot(EquipmentSlot.HEAD))); RuntimeChecks.require(!cache(handler).containsKey(player),"Equipment cache retains completed death"); - world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(true,null); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(true,null); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); RuntimeChecks.require(!cache(handler).containsKey(player),"keepInventory death cached equipment"); - world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(false,null); + 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); handler.entityDeathEvent(canceled); RuntimeChecks.require(!cache(handler).containsKey(player),"Cancelled death cached equipment"); @@ -219,45 +221,45 @@ public static final class RejectItems { } } private static void failedRelease() throws Exception { - EntityLootableBody corpse=body(); corpse.setInventorySlotContents(6,item(19,23)); - RuntimeChecks.require(world.addEntity(corpse),"Rejected-release owner spawn"); + EntityLootableBody corpse=body(); corpse.setItem(6,item(19,23)); + RuntimeChecks.require(world.addFreshEntity(corpse),"Rejected-release owner spawn"); Map expected=inventory(corpse); RejectItems reject=new RejectItems(); reject.rejectAll=true; MinecraftForge.EVENT_BUS.register(reject); - try { corpse.onKillCommand(); for(int i=0;i<25;i++) corpse.baseTick(); } + try { corpse.kill(); for(int i=0;i<25;i++) corpse.baseTick(); } finally { MinecraftForge.EVENT_BUS.unregister(reject); } RuntimeChecks.require(expected.equals(inventory(corpse)),"Rejected item spawn erased corpse inventory"); RuntimeChecks.require(corpse.isAlive() && FixtureApi.entities(world).contains(corpse),"Rejected release removed the only inventory owner"); - CompoundNBT saved=data(corpse); EntityLootableBody loaded=body();loaded.readAdditional(saved); + CompoundTag saved=data(corpse); EntityLootableBody loaded=body();loaded.readAdditionalSaveData(saved); RuntimeChecks.require(expected.equals(inventory(loaded)),"Rejected items do not survive NBT reload"); - corpse.onKillCommand();for(int i=0;i<12;i++)corpse.baseTick(); + corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); RuntimeChecks.require(expected.equals(dropped()),"Retry did not drop exact saved stacks once"); } private static void partialRelease() { - EntityLootableBody corpse=body();corpse.setInventorySlotContents(6,item(19,23));corpse.setInventorySlotContents(7,item(20,11)); - RuntimeChecks.require(world.addEntity(corpse),"Partial-release owner spawn"); + EntityLootableBody corpse=body();corpse.setItem(6,item(19,23));corpse.setItem(7,item(20,11)); + RuntimeChecks.require(world.addFreshEntity(corpse),"Partial-release owner spawn"); Map expected=inventory(corpse); RejectItems reject=new RejectItems();reject.rejectFixture="slot-20";MinecraftForge.EVENT_BUS.register(reject); - try { corpse.onKillCommand();for(int i=0;i<12;i++)corpse.baseTick(); } + try { corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); } finally { MinecraftForge.EVENT_BUS.unregister(reject); } Map combined=new TreeMap<>(dropped());inventory(corpse).forEach((k,v)->combined.put(k,combined.getOrDefault(k,0)+v)); RuntimeChecks.require(expected.equals(combined),"Partial release lost/duplicated a stack"); - RuntimeChecks.require((corpse.getStackInSlot(6)).isEmpty() && RuntimeChecks.sameStack(corpse.getStackInSlot(7),item(20,11)),"Partial release did not retain only rejected slot"); - corpse.onKillCommand();for(int i=0;i<12;i++)corpse.baseTick(); + RuntimeChecks.require((corpse.getItem(6)).isEmpty() && RuntimeChecks.sameStack(corpse.getItem(7),item(20,11)),"Partial release did not retain only rejected slot"); + corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); RuntimeChecks.require(expected.equals(dropped()),"Partial retry duplicated previously accepted drops"); } private static void vanillaEquipment() { - world.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_LOOT).set(true,null); - EntityLootableBody corpse=body();corpse.setInventorySlotContents(0,item(0,1));corpse.setDropChance(EquipmentSlotType.HEAD,2F); - RuntimeChecks.require(world.addEntity(corpse),"Equipment-release owner spawn"); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).set(true,null); + EntityLootableBody corpse=body();corpse.setItem(0,item(0,1));corpse.setDropChance(EquipmentSlot.HEAD,2F); + RuntimeChecks.require(world.addFreshEntity(corpse),"Equipment-release owner spawn"); Map expected=inventory(corpse); - corpse.onKillCommand();for(int i=0;i<12;i++)corpse.baseTick(); + corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); RuntimeChecks.require(expected.equals(dropped()),"Vanilla mob equipment path duplicates corpse inventory"); } private static void vacuumOnce() { EntityLootableBody first=body(),second=body(); try { Field f=EntityLootableBody.class.getDeclaredField("vacuumTime");f.setAccessible(true);f.setByte(first,(byte)0);f.setByte(second,(byte)0); } catch(Exception error){throw new AssertionError(error);} - ItemEntity entity=new ItemEntity(world,64.5,80,64.5,item(19,23));RuntimeChecks.require(world.addEntity(entity),"Vacuum input spawn"); + ItemEntity entity=new ItemEntity(world,64.5,80,64.5,item(19,23));RuntimeChecks.require(world.addFreshEntity(entity),"Vacuum input spawn"); Map expected=new TreeMap<>();add(expected,entity.getItem().copy()); first.baseTick();second.baseTick(); Map all=inventory(first);inventory(second).forEach((k,v)->all.put(k,all.getOrDefault(k,0)+v)); @@ -265,131 +267,134 @@ private static void vacuumOnce() { } private static void emptyRemoval() { EntityLootableBody corpse=body(); - RuntimeChecks.require((corpse.decrStackSize(6,1)).isEmpty(),"Empty removal must return EMPTY"); - corpse.setInventorySlotContents(6,item(19,23)); CompoundNBT expected=encoded(corpse.getStackInSlot(6)); - RuntimeChecks.require((corpse.decrStackSize(6,-1)).isEmpty(),"Negative removal created a stack"); - RuntimeChecks.require(expected.equals(encoded(corpse.getStackInSlot(6))),"Negative removal changed stack count"); - RuntimeChecks.require((corpse.decrStackSize(6,0)).isEmpty(),"Zero removal created a stack"); - ItemStack removed=corpse.decrStackSize(6,40);RuntimeChecks.require(RuntimeChecks.sameStack(removed,item(19,23)) && (corpse.getStackInSlot(6)).isEmpty(),"Full extraction did not clear slot"); + RuntimeChecks.require((corpse.removeItem(6,1)).isEmpty(),"Empty removal must return EMPTY"); + corpse.setItem(6,item(19,23)); CompoundTag expected=encoded(corpse.getItem(6)); + RuntimeChecks.require((corpse.removeItem(6,-1)).isEmpty(),"Negative removal created a stack"); + RuntimeChecks.require(expected.equals(encoded(corpse.getItem(6))),"Negative removal changed stack count"); + RuntimeChecks.require((corpse.removeItem(6,0)).isEmpty(),"Zero removal created a stack"); + ItemStack removed=corpse.removeItem(6,40);RuntimeChecks.require(RuntimeChecks.sameStack(removed,item(19,23)) && (corpse.getItem(6)).isEmpty(),"Full extraction did not clear slot"); } private static void repeatedRead() { - EntityLootableBody source=body();for(int i=6;i<48;i++)source.setInventorySlotContents(i,item(i,1));source.addItem(item(70,7)); - CompoundNBT input=data(source),immutable=input.copy(); - EntityLootableBody target=body(); target.readAdditional(input); target.readAdditional(input); + EntityLootableBody source=body();for(int i=6;i<48;i++)source.setItem(i,item(i,1));source.addItem(item(70,7)); + CompoundTag input=data(source),immutable=input.copy(); + EntityLootableBody target=body(); target.readAdditionalSaveData(input); target.readAdditionalSaveData(input); RuntimeChecks.require(data(target).get("Equipment").equals(input.get("Equipment")) && data(target).get("Aux").equals(input.get("Aux")),"Repeated read appended overflow or changed slots"); - CompoundNBT sparse=new CompoundNBT();ListNBT list=new ListNBT();CompoundNBT slot=encoded(item(19,23));slot.putByte("Slot",(byte)47);list.add(slot);sparse.put("Equipment",list); - target.readAdditional(sparse); - RuntimeChecks.require((target.getStackInSlot(6)).isEmpty() && RuntimeChecks.sameStack(target.getStackInSlot(47),item(19,23)) && !data(target).contains("Aux"),"Sparse reread kept stale inventory/overflow"); + CompoundTag sparse=new CompoundTag();ListTag list=new ListTag();CompoundTag slot=encoded(item(19,23));slot.putByte("Slot",(byte)47);list.add(slot);sparse.put("Equipment",list); + target.readAdditionalSaveData(sparse); + RuntimeChecks.require((target.getItem(6)).isEmpty() && RuntimeChecks.sameStack(target.getItem(47),item(19,23)) && !data(target).contains("Aux"),"Sparse reread kept stale inventory/overflow"); RuntimeChecks.require(immutable.equals(input),"Reader mutated source NBT"); } private static void staleAccess() { - ServerPlayerEntity player=player();EntityLootableBody corpse=body();CorpseContainer container=new CorpseContainer(player.inventory,corpse); - RuntimeChecks.require(container.canInteractWith(player),"Near living access failed"); - player.setPosition(100,80,100);RuntimeChecks.require(!container.canInteractWith(player),"Distant container remained accessible"); - player.setPosition(64.5,80,64.5);corpse.remove();RuntimeChecks.require(!container.canInteractWith(player),"Removed corpse remained accessible"); - corpse=body();container=new CorpseContainer(player.inventory,corpse);player.world=new RejectingWorld(world); - RuntimeChecks.require(!container.canInteractWith(player),"Cross-world container remained accessible"); - player.world=world;player.setHealth(0); - RuntimeChecks.require(!container.canInteractWith(player),"Dead player retained container access"); - player.setHealth(player.getMaxHealth());corpse.onKillCommand();corpse.baseTick(); - RuntimeChecks.require(!container.canInteractWith(player),"Dying corpse remained accessible"); + ServerPlayer player=player();EntityLootableBody corpse=body();CorpseContainer container=new CorpseContainer(player.getInventory(),corpse); + RuntimeChecks.require(container.stillValid(player),"Near living access failed"); + player.setPos(100,80,100);RuntimeChecks.require(!container.stillValid(player),"Distant container remained accessible"); + player.setPos(64.5,80,64.5);corpse.discard();RuntimeChecks.require(!container.stillValid(player),"Removed corpse remained accessible"); + corpse=body();container=new CorpseContainer(player.getInventory(),corpse);player.level=new RejectingWorld(world); + RuntimeChecks.require(!container.stillValid(player),"Cross-world container remained accessible"); + player.level=world;player.setHealth(0); + RuntimeChecks.require(!container.stillValid(player),"Dead player retained container access"); + player.setHealth(player.getMaxHealth());corpse.kill();corpse.baseTick(); + RuntimeChecks.require(!container.stillValid(player),"Dying corpse remained accessible"); } private static void overflow() { - EntityLootableBody corpse=body();for(int i=6;i<48;i++)corpse.setInventorySlotContents(i,item(i,1));corpse.addItem(item(70,7)); - CompoundNBT before=data(corpse);corpse.baseTick(); + EntityLootableBody corpse=body();for(int i=6;i<48;i++)corpse.setItem(i,item(i,1));corpse.addItem(item(70,7)); + CompoundTag before=data(corpse);corpse.baseTick(); RuntimeChecks.require(before.get("Equipment").equals(data(corpse).get("Equipment")),"Full inventory overwritten by overflow"); - corpse.removeStackFromSlot(47);corpse.baseTick();RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getStackInSlot(47),item(70,7)),"Overflow did not fill free slot"); + corpse.removeItemNoUpdate(47);corpse.baseTick();RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getItem(47),item(70,7)),"Overflow did not fill free slot"); EntityLootableBody large=body();large.addItem(item(19,100)); Map expected=new TreeMap<>();add(expected,item(19,100)); RuntimeChecks.require(expected.equals(inventory(large)),"Oversized incoming stack lost items"); - for(int i=6;i<48;i++) RuntimeChecks.require((large.getStackInSlot(i)).isEmpty() || large.getStackInSlot(i).getCount()<=64,"Oversized input bypassed slot capacity"); - EntityLootableBody queued=body();for(int i=6;i<48;i++)queued.setInventorySlotContents(i,item(i,1)); - queued.addItem(item(70,100));queued.removeStackFromSlot(47);expected=inventory(queued); + for(int i=6;i<48;i++) RuntimeChecks.require((large.getItem(i)).isEmpty() || large.getItem(i).getCount()<=64,"Oversized input bypassed slot capacity"); + EntityLootableBody queued=body();for(int i=6;i<48;i++)queued.setItem(i,item(i,1)); + queued.addItem(item(70,100));queued.removeItemNoUpdate(47);expected=inventory(queued); queued.baseTick(); - RuntimeChecks.require(expected.equals(inventory(queued)) && queued.getStackInSlot(47).getCount()==64,"Overflow transfer lost items or exceeded slot capacity"); - queued.removeStackFromSlot(46);queued.baseTick(); - RuntimeChecks.require(RuntimeChecks.sameStack(queued.getStackInSlot(46),item(70,36)) && !data(queued).contains("Aux"),"Overflow remainder changed count or custom NBT"); + RuntimeChecks.require(expected.equals(inventory(queued)) && queued.getItem(47).getCount()==64,"Overflow transfer lost items or exceeded slot capacity"); + queued.removeItemNoUpdate(46);queued.baseTick(); + RuntimeChecks.require(RuntimeChecks.sameStack(queued.getItem(46),item(70,36)) && !data(queued).contains("Aux"),"Overflow remainder changed count or custom NBT"); } private static void twoViewers() { - EntityLootableBody corpse=body();corpse.setInventorySlotContents(6,item(19,23));ServerPlayerEntity a=player(),b=player(); - CorpseContainer ca=new CorpseContainer(a.inventory,corpse),cb=new CorpseContainer(b.inventory,corpse); - RuntimeChecks.require(RuntimeChecks.sameStack(ca.transferStackInSlot(a,6),item(19,23)),"First recovery failed"); - RuntimeChecks.require((cb.transferStackInSlot(b,6)).isEmpty(),"Second viewer recovered the same stack"); - Map all=inventory(corpse);for(ItemStack s:a.inventory.mainInventory)add(all,s);for(ItemStack s:b.inventory.mainInventory)add(all,s); + EntityLootableBody corpse=body();corpse.setItem(6,item(19,23));ServerPlayer a=player(),b=player(); + CorpseContainer ca=new CorpseContainer(a.getInventory(),corpse),cb=new CorpseContainer(b.getInventory(),corpse); + RuntimeChecks.require(RuntimeChecks.sameStack(ca.quickMoveStack(a,6),item(19,23)),"First recovery failed"); + RuntimeChecks.require((cb.quickMoveStack(b,6)).isEmpty(),"Second viewer recovered the same stack"); + Map all=inventory(corpse);for(ItemStack s:a.getInventory().items)add(all,s);for(ItemStack s:b.getInventory().items)add(all,s); Map expected=new TreeMap<>();add(expected,item(19,23));RuntimeChecks.require(expected.equals(all),"Two viewer recovery changed inventory/NBT"); } private static void emptyValidation() { - EntityLootableBody corpse=body();ServerPlayerEntity player=player();CorpseContainer container=new CorpseContainer(player.inventory,corpse); - RuntimeChecks.require(!corpse.isItemValidForSlot(0,null),"Empty armor input accepted"); - RuntimeChecks.require(!container.getSlot(0).isItemValid(null),"Container empty armor input accepted"); - } - private static net.minecraft.world.chunk.Chunk cleanChunk(EntityLootableBody corpse) { - net.minecraft.world.chunk.Chunk chunk=world.getChunkAt(corpse.getPosition()); - chunk.setLastSaveTime(world.getGameTime());chunk.setModified(false); - RuntimeChecks.require(!chunk.isModified(),"Dirty-chunk test precondition"); + EntityLootableBody corpse=body();ServerPlayer player=player();CorpseContainer container=new CorpseContainer(player.getInventory(),corpse); + RuntimeChecks.require(!corpse.canPlaceItem(0,null),"Empty armor input accepted"); + RuntimeChecks.require(!container.getSlot(0).mayPlace(null),"Container empty armor input accepted"); + } + private static net.minecraft.world.level.chunk.LevelChunk cleanChunk(EntityLootableBody corpse) { + net.minecraft.world.level.chunk.LevelChunk chunk=world.getChunkAt(corpse.blockPosition()); + chunk.setUnsaved(false); + RuntimeChecks.require(!chunk.isUnsaved(),"Dirty-chunk test precondition"); return chunk; } private static void dirtyInventory() { - EntityLootableBody corpse=body();corpse.setInventorySlotContents(6,item(19,23)); - RuntimeChecks.require(world.addEntity(corpse),"Dirty-chunk corpse spawn"); - net.minecraft.world.chunk.Chunk chunk=cleanChunk(corpse);corpse.markDirty(); - RuntimeChecks.require(chunk.isModified(),"Inventory markDirty does not schedule a chunk save"); + EntityLootableBody corpse=body();corpse.setItem(6,item(19,23)); + RuntimeChecks.require(world.addFreshEntity(corpse),"Dirty-chunk corpse spawn"); + net.minecraft.world.level.chunk.LevelChunk chunk=cleanChunk(corpse);corpse.setChanged(); + RuntimeChecks.require(chunk.isUnsaved(),"Inventory markDirty does not schedule a chunk save"); } private static void dirtyMutations() { - EntityLootableBody corpse=body();RuntimeChecks.require(world.addEntity(corpse),"Mutation corpse spawn"); - net.minecraft.world.chunk.Chunk chunk=cleanChunk(corpse);corpse.setInventorySlotContents(6,item(19,23)); - RuntimeChecks.require(chunk.isModified(),"Slot insertion not scheduled for save"); - cleanChunk(corpse);corpse.decrStackSize(6,1);RuntimeChecks.require(chunk.isModified(),"Extraction not scheduled for save"); - cleanChunk(corpse);corpse.removeStackFromSlot(6);RuntimeChecks.require(chunk.isModified(),"Removal not scheduled for save"); - cleanChunk(corpse);corpse.initializeItems(new ItemStack[]{item(20,11)});RuntimeChecks.require(chunk.isModified(),"Initial inventory not scheduled for save"); - for(int i=6;i<48;i++)corpse.setInventorySlotContents(i,item(i,1)); - cleanChunk(corpse);corpse.addItem(item(70,7));RuntimeChecks.require(chunk.isModified(),"Overflow insertion not scheduled for save"); - } - static void seedBlockedRelease(ServerWorld target,File folder) throws Exception { + EntityLootableBody corpse=body();RuntimeChecks.require(world.addFreshEntity(corpse),"Mutation corpse spawn"); + net.minecraft.world.level.chunk.LevelChunk chunk=cleanChunk(corpse);corpse.setItem(6,item(19,23)); + RuntimeChecks.require(chunk.isUnsaved(),"Slot insertion not scheduled for save"); + cleanChunk(corpse);corpse.removeItem(6,1);RuntimeChecks.require(chunk.isUnsaved(),"Extraction not scheduled for save"); + cleanChunk(corpse);corpse.removeItemNoUpdate(6);RuntimeChecks.require(chunk.isUnsaved(),"Removal not scheduled for save"); + cleanChunk(corpse);corpse.initializeItems(new ItemStack[]{item(20,11)});RuntimeChecks.require(chunk.isUnsaved(),"Initial inventory not scheduled for save"); + for(int i=6;i<48;i++)corpse.setItem(i,item(i,1)); + cleanChunk(corpse);corpse.addItem(item(70,7));RuntimeChecks.require(chunk.isUnsaved(),"Overflow insertion not scheduled for save"); + } + static void seedBlockedRelease(ServerLevel target,File folder) throws Exception { EntityLootableBody corpse=new EntityLootableBody(target); - corpse.setUniqueId(UUID.fromString("00000000-0000-4000-8000-000000000105")); - corpse.setPosition(48.5,80,8.5);corpse.setNoGravity(true);corpse.setUserName("BlockedRelease"); + corpse.setUUID(UUID.fromString("00000000-0000-4000-8000-000000000105")); + corpse.setPos(48.5,80,8.5);corpse.setNoGravity(true);corpse.setUserName("BlockedRelease"); RuntimeChecks.awaitChunk(target,3,0); - for(int i=0;i<48;i++)corpse.setInventorySlotContents(i,RuntimeChecks.stack(i)); + for(int i=0;i<48;i++)corpse.setItem(i,RuntimeChecks.stack(i)); corpse.addItem(RuntimeChecks.stack(70));corpse.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(corpse); - RuntimeChecks.require(target.addEntity(corpse),"Blocked-release fixture spawn"); + RuntimeChecks.require(target.addFreshEntity(corpse),"Blocked-release fixture spawn"); Map expected=inventory(corpse); RejectItems reject=new RejectItems();reject.rejectAll=true;MinecraftForge.EVENT_BUS.register(reject); - try {corpse.onKillCommand();for(int i=0;i<25;i++)corpse.baseTick();} + try {corpse.kill();for(int i=0;i<25;i++)corpse.baseTick();} finally {MinecraftForge.EVENT_BUS.unregister(reject);} RuntimeChecks.require(corpse.isAlive() && FixtureApi.entities(target).contains(corpse) && expected.equals(inventory(corpse)),"Blocked-release fixture lost inventory/owner"); RuntimeChecks.saveExpected(folder,"rejected-release",corpse); } /** Controlled false-return seam; no dimension registration, disk writes or chunks of its own. */ - private static final class RejectingWorld extends World { - private final ServerWorld delegate; int attempts; - RejectingWorld(ServerWorld target) { - super((net.minecraft.world.storage.ISpawnWorldInfo)target.getWorldInfo(),target.getDimensionKey(),target.getDimensionType(),()->net.minecraft.profiler.EmptyProfiler.INSTANCE,false,false,target.getSeed()); + 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.dimensionType(),()->net.minecraft.util.profiling.InactiveProfiler.INSTANCE,false,false,target.getSeed()); delegate=target; } - @Override public net.minecraft.util.registry.DynamicRegistries func_241828_r(){return delegate.func_241828_r();} - @Override public float func_230487_a_(net.minecraft.util.Direction direction,boolean shade){return delegate.func_230487_a_(direction,shade);} - @Override public AbstractChunkProvider getChunkProvider(){return delegate.getChunkProvider();} - @Override public net.minecraft.world.biome.Biome getNoiseBiomeRaw(int x,int y,int z){return delegate.getNoiseBiomeRaw(x,y,z);} + @Override public net.minecraft.core.RegistryAccess registryAccess(){return delegate.registryAccess();} + @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.world.level.biome.Biome getUncachedNoiseBiome(int x,int y,int z){return delegate.getUncachedNoiseBiome(x,y,z);} - @Override public net.minecraft.world.ITickList getPendingBlockTicks(){return delegate.getPendingBlockTicks();} - @Override public net.minecraft.world.ITickList getPendingFluidTicks(){return delegate.getPendingFluidTicks();} - @Override public net.minecraft.item.crafting.RecipeManager getRecipeManager(){return delegate.getRecipeManager();} - @Override public net.minecraft.scoreboard.Scoreboard getScoreboard(){return delegate.getScoreboard();} - @Override public net.minecraft.tags.ITagCollectionSupplier getTags(){return delegate.getTags();} + @Override public net.minecraft.world.level.TickList getBlockTicks(){return delegate.getBlockTicks();} + @Override public net.minecraft.world.level.TickList getLiquidTicks(){return delegate.getLiquidTicks();} + @Override public net.minecraft.world.item.crafting.RecipeManager getRecipeManager(){return delegate.getRecipeManager();} + @Override public net.minecraft.world.scores.Scoreboard getScoreboard(){return delegate.getScoreboard();} + @Override public net.minecraft.tags.TagContainer getTagManager(){return delegate.getTagManager();} @Override public net.minecraft.server.MinecraftServer getServer(){return delegate.getServer();} @Override public long getGameTime(){return delegate==null?0:delegate.getGameTime();} - @Override public void notifyBlockUpdate(net.minecraft.util.math.BlockPos pos,net.minecraft.block.BlockState oldState,net.minecraft.block.BlockState state,int flags){delegate.notifyBlockUpdate(pos,oldState,state,flags);} - @Override public void playSound(net.minecraft.entity.player.PlayerEntity player,double x,double y,double z,net.minecraft.util.SoundEvent sound,net.minecraft.util.SoundCategory category,float volume,float pitch){delegate.playSound(player,x,y,z,sound,category,volume,pitch);} - @Override public void playMovingSound(net.minecraft.entity.player.PlayerEntity player,Entity entity,net.minecraft.util.SoundEvent sound,net.minecraft.util.SoundCategory category,float volume,float pitch){delegate.playMovingSound(player,entity,sound,category,volume,pitch);} - @Override public Entity getEntityByID(int id){return delegate.getEntityByID(id);} - @Override public net.minecraft.world.storage.MapData getMapData(String id){return delegate.getMapData(id);} - @Override public void registerMapData(net.minecraft.world.storage.MapData data){delegate.registerMapData(data);} - @Override public int getNextMapId(){return delegate.getNextMapId();} - @Override public void sendBlockBreakProgress(int id,net.minecraft.util.math.BlockPos pos,int progress){delegate.sendBlockBreakProgress(id,pos,progress);} - @Override public void playEvent(net.minecraft.entity.player.PlayerEntity player,int type,net.minecraft.util.math.BlockPos pos,int data){delegate.playEvent(player,type,pos,data);} - @Override public java.util.List getPlayers(){return delegate.getPlayers();} - @Override public boolean addEntity(Entity entity){attempts++;return false;} + @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 Entity getEntity(int id){return delegate.getEntity(id);} + @Override public net.minecraft.world.level.saveddata.maps.MapItemSavedData getMapData(String id){return delegate.getMapData(id);} + @Override public void setMapData(String id,net.minecraft.world.level.saveddata.maps.MapItemSavedData data){delegate.setMapData(id,data);} + @Override public int getFreeMapId(){return delegate.getFreeMapId();} + @Override public 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 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();} + @Override public boolean addFreshEntity(Entity entity){attempts++;return false;} } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java index f37b79f..44a454d 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java @@ -1,10 +1,10 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.Container; -import net.minecraft.entity.boss.dragon.phase.Phase; -import net.minecraft.inventory.Inventory; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; +import net.minecraft.world.SimpleContainer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; @@ -13,30 +13,33 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.UUID; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.*; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.GameType; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; -import net.minecraftforge.fml.network.NetworkRegistry; -import net.minecraftforge.fml.network.simple.*; +import net.minecraftforge.fmllegacy.network.NetworkRegistry; +import net.minecraftforge.fmllegacy.network.simple.*; import net.minecraftforge.api.distmarker.Dist; /** Disposable two-client coordinator. Step messages acknowledge prior TCP clicks on the server thread. */ public final class MultiplayerChecks { public static final UUID BODY=UUID.fromString("00000000-0000-4000-8000-000000000108"); - public static final SimpleChannel CHANNEL=NetworkRegistry.newSimpleChannel(new net.minecraft.util.ResourceLocation("lootablebodiesbuildtest:steps"),()->"1","1"::equals,"1"::equals); + public static final SimpleChannel CHANNEL=NetworkRegistry.newSimpleChannel(new net.minecraft.resources.ResourceLocation("lootablebodiesbuildtest:steps"),()->"1","1"::equals,"1"::equals); private final MinecraftServer server; private final File sync; private EntityLootableBody body; private int phase,ticks; private boolean finishing; - public static void registerChannel(){CHANNEL.registerMessage(0,Step.class,(message,buffer)->buffer.writeInt(message.number),buffer->new Step(buffer.readInt()),(message,supplier)->{net.minecraftforge.fml.network.NetworkEvent.Context context=supplier.get();context.enqueueWork(()->acknowledge(message,context.getSender()));context.setPacketHandled(true);});CHANNEL.registerMessage(1,FixtureDeath.class,(message,buffer)->{},buffer->new FixtureDeath(),(message,supplier)->{net.minecraftforge.fml.network.NetworkEvent.Context context=supplier.get();context.enqueueWork(()->{RuntimeChecks.require(Boolean.getBoolean("lootablebodies.issueClientCheck") && context.getSender()!=null,"Unexpected fixture death signal");context.getSender().onKillCommand();});context.setPacketHandled(true);});} + public static void registerChannel(){CHANNEL.registerMessage(0,Step.class,(message,buffer)->buffer.writeInt(message.number),buffer->new Step(buffer.readInt()),(message,supplier)->{net.minecraftforge.fmllegacy.network.NetworkEvent.Context context=supplier.get();context.enqueueWork(()->acknowledge(message,context.getSender()));context.setPacketHandled(true);});CHANNEL.registerMessage(1,FixtureDeath.class,(message,buffer)->{},buffer->new FixtureDeath(),(message,supplier)->{net.minecraftforge.fmllegacy.network.NetworkEvent.Context context=supplier.get();context.enqueueWork(()->{RuntimeChecks.require(Boolean.getBoolean("lootablebodies.issueClientCheck") && context.getSender()!=null,"Unexpected fixture death signal");context.getSender().kill();});context.setPacketHandled(true);});} public static final class FixtureDeath { } public MultiplayerChecks(MinecraftServer server) throws Exception { this.server=server;sync=sync(); @@ -45,48 +48,48 @@ public MultiplayerChecks(MinecraftServer server) throws Exception { net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); } public static File sync(){return new File(System.getProperty("lootablebodies.multiplayer.sync"));} - private ServerPlayerEntity player(String role){return server.getPlayerList().getPlayerByUsername("Container"+role);} + private ServerPlayer player(String role){return server.getPlayerList().getPlayerByName("Container"+role);} private boolean ack(String role,int step){return new File(sync,"Container"+role+".step-"+step).isFile();} @SubscribeEvent public void tick(TickEvent.ServerTickEvent e){ if(e.phase!=TickEvent.Phase.END || finishing)return; try{ RuntimeChecks.require(++ticks<2400,"Two-client timeout phase="+phase); if(new File(sync,"abort").exists())throw new AssertionError("Runner aborted multiplayer fixture"); - ServerPlayerEntity a=player("A"),b=player("B"); + ServerPlayer a=player("A"),b=player("B"); if(phase==0 && a!=null && b!=null){ - ServerWorld w=server.getWorld(FixtureApi.dimensionKey(0)); - for(int x=93;x<=100;x++)for(int z=93;z<=100;z++)w.setBlockState(new BlockPos(x,79,z),Blocks.STONE.getDefaultState()); - w.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_SPAWNING).set(false,null); - body=new EntityLootableBody(w);body.setUniqueId(BODY);body.setPosition(96.5,80.5,96.5);body.setNoGravity(true);body.setUserName("SharedFixture");RuntimeChecks.disableVacuum(body); - body.setInventorySlotContents(6,stack(19,23));RuntimeChecks.require(w.addEntity(body),"Shared corpse spawn"); - a.setGameType(GameType.SURVIVAL);b.setGameType(GameType.SURVIVAL);a.inventory.clear();b.inventory.clear(); - a.connection.setPlayerLocation(96.5,80,94.5,0,0);b.connection.setPlayerLocation(98.5,80,96.5,0,0); + ServerLevel w=server.getLevel(FixtureApi.dimensionKey(0)); + for(int x=93;x<=100;x++)for(int z=93;z<=100;z++)w.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); + w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); + body=new EntityLootableBody(w);body.setUUID(BODY);body.setPos(96.5,80.5,96.5);body.setNoGravity(true);body.setUserName("SharedFixture");RuntimeChecks.disableVacuum(body); + body.setItem(6,stack(19,23));RuntimeChecks.require(w.addFreshEntity(body),"Shared corpse spawn"); + a.setGameMode(GameType.SURVIVAL);b.setGameMode(GameType.SURVIVAL);a.getInventory().clearContent();b.getInventory().clearContent(); + a.connection.teleport(96.5,80,94.5,0,0);b.connection.teleport(98.5,80,96.5,0,0); setPhase(1); }else if(phase==1 && ack("A",1) && ack("B",1))setPhase(2); else if(phase==2 && ack("A",2) && ack("B",2)){ - RuntimeChecks.require((body.getStackInSlot(6)).isEmpty() && count(a,19)+count(b,19)==23,"Two TCP clients duplicated/lost contested stack"); + RuntimeChecks.require((body.getItem(6)).isEmpty() && count(a,19)+count(b,19)==23,"Two TCP clients duplicated/lost contested stack"); RuntimeChecks.require(count(a,19)==0 || count(b,19)==0,"Both clients recovered the same stack"); mark("two_tcp_clients_contested_shift_click"); - a.inventory.clear();b.inventory.clear();body.clear(); - a.inventory.setItemStack(stack(19,10));b.inventory.mainInventory.set(0,stack(20,7));refresh(a);refresh(b);setPhase(3); + a.getInventory().clearContent();b.getInventory().clearContent();body.clearContent(); + a.containerMenu.setCarried(stack(19,10));b.getInventory().items.set(0,stack(20,7));refresh(a);refresh(b);setPhase(3); }else if(phase==3 && ack("A",3))setPhase(4); else if(phase==4 && ack("B",4)){ - equal(body.getStackInSlot(6),stack(20,7),"TCP hotbar swap failed");setPhase(5); + equal(body.getItem(6),stack(20,7),"TCP hotbar swap failed");setPhase(5); }else if(phase==5 && ack("A",5)){ - equal(body.getStackInSlot(6),stack(20,7),"TCP drag overwrote other player's NBT");equal(body.getStackInSlot(7),stack(19,5),"TCP drag allocation");equal(a.inventory.getItemStack(),stack(19,5),"TCP cursor remainder"); + equal(body.getItem(6),stack(20,7),"TCP drag overwrote other player's NBT");equal(body.getItem(7),stack(19,5),"TCP drag allocation");equal(a.containerMenu.getCarried(),stack(19,5),"TCP cursor remainder"); mark("two_tcp_clients_hotbar_interleaved_with_drag");setPhase(6); }else if(phase==6 && ack("A",6)){ - equal(a.inventory.getItemStack(),null,"TCP deposit cursor not empty");RuntimeChecks.require(count(a,19)==5,"TCP drag remainder deposit");setPhase(7); + equal(a.containerMenu.getCarried(),null,"TCP deposit cursor not empty");RuntimeChecks.require(count(a,19)==5,"TCP drag remainder deposit");setPhase(7); }else if(phase==7 && ack("A",7) && ack("B",7)){ - File folder=server.getWorld(FixtureApi.dimensionKey(0)).getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile(); + File folder=server.getLevel(FixtureApi.dimensionKey(0)).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); RuntimeChecks.saveExpected(folder,"multiplayer-body",body);savePlayer(folder,a,"A");savePlayer(folder,b,"B"); - server.getPlayerList().saveAllPlayerData();server.save(false,true,false);mark("two_tcp_clients_final_gui_and_saved_nbt"); + server.getPlayerList().saveAll();server.saveAllChunks(false,true,false);mark("two_tcp_clients_final_gui_and_saved_nbt"); setPhase(8); - }else if(phase==8 && server.getPlayerList().getCurrentPlayerCount()==0){ + }else if(phase==8 && server.getPlayerList().getPlayerCount()==0){ Files.write(new File(sync,"multiplayer.pass").toPath(),"two TCP clients passed\n".getBytes(StandardCharsets.UTF_8)); - System.out.println("LOOTABLE_BODIES_MULTIPLAYER_PASS");finishing=true;server.initiateShutdown(false); + System.out.println("LOOTABLE_BODIES_MULTIPLAYER_PASS");finishing=true;server.halt(false); } - }catch(Throwable error){error.printStackTrace();finishing=true;server.initiateShutdown(false);} + }catch(Throwable error){error.printStackTrace();finishing=true;server.halt(false);} } private void setPhase(int value) throws Exception { phase=value;File temporary=new File(sync,"phase.tmp");Files.write(temporary.toPath(),Integer.toString(value).getBytes(StandardCharsets.UTF_8)); @@ -96,23 +99,23 @@ private void setPhase(int value) throws Exception { private void mark(String name) throws Exception{try(FileWriter out=new FileWriter(new File(sync,"checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("MULTIPLAYER_CHECK_PASS "+name);} public static ItemStack stack(int i,int n){ItemStack s=RuntimeChecks.stack(i);s.setCount(n);return s;} public static void equal(ItemStack a,ItemStack b,String reason){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),reason);} - private static int count(ServerPlayerEntity p,int index){int n=0;ItemStack expected=RuntimeChecks.stack(index);for(int i=0;iSystem.out.println(message.getString()));} + private void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java index 8f6481e..b7e3dc4 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java @@ -1,16 +1,16 @@ package cyano.lootable.buildtest; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.StringNBT; -import net.minecraft.nbt.ShortNBT; -import net.minecraft.nbt.LongNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.IntArrayNBT; -import net.minecraft.nbt.FloatNBT; -import net.minecraft.nbt.DoubleNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ByteNBT; -import net.minecraft.nbt.ByteArrayNBT; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.ByteArrayTag; import com.mojang.authlib.GameProfile; import cyano.lootable.LootableBodies; @@ -18,35 +18,46 @@ import java.io.*; import java.lang.reflect.Field; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.item.Items; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerLevel; /** Real legacy-engine contracts for owned scalar fields; no production dependency. */ final class NameLifetimeChecks { static final String RECOVERY="LootableBodiesInvalidFields"; private interface Check {void run() throws Exception;} - private static ServerWorld world; + private static ServerLevel world; private static final List results=new ArrayList<>(); private static int failures; - static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { + static void run(MinecraftServer server,ServerLevel target,File folder) throws Exception { world=target;results.clear();failures=0; - INBT[] wrong={StringNBT.valueOf("not a timer"),FloatNBT.valueOf(1.5F),DoubleNBT.valueOf(Double.NaN),DoubleNBT.valueOf(Double.POSITIVE_INFINITY),new CompoundNBT(),new ListNBT(),new ByteArrayNBT(new byte[]{1}),new IntArrayNBT(new int[]{1})}; + Tag[] wrong={StringTag.valueOf("not a timer"),FloatTag.valueOf(1.5F),DoubleTag.valueOf(Double.NaN),DoubleTag.valueOf(Double.POSITIVE_INFINITY),new CompoundTag(),new ListTag(),new ByteArrayTag(new byte[]{1}),new IntArrayTag(new int[]{1})}; for(String key:new String[]{"Vac","DeathTime","DecayTime"}) { - for(int i=0;iinvalid(key,value));} - for(long value:new long[]{Long.MIN_VALUE,-1,Long.MAX_VALUE,10001})check(key+"_invalid_value_"+value,()->invalid(key,LongNBT.valueOf(value))); + for(int i=0;iinvalid(key,value));} + for(long value:new long[]{Long.MIN_VALUE,-1,Long.MAX_VALUE,10001})check(key+"_invalid_value_"+value,()->invalid(key,LongTag.valueOf(value))); } - for(int i=0;ibadName("Name",value));} - check("Name_over_packet_string_limit",()->badName("Name",StringNBT.valueOf(repeat('n',32768)))); - check("Name_utf8_bytes_over_packet_limit",()->badName("Name",StringNBT.valueOf(repeat('\u20ac',10923)))); - check("CustomName_utf8_bytes_over_packet_limit",()->badName("CustomName",StringNBT.valueOf(repeat('\u20ac',10923)))); + for(int i=0;ibadName("Name",value));} + check("Name_over_packet_string_limit",()->badName("Name",StringTag.valueOf(repeat('n',32768)))); + check("Name_utf8_bytes_over_packet_limit",()->badName("Name",StringTag.valueOf(repeat('\u20ac',10923)))); + check("CustomName_utf8_bytes_over_packet_limit",()->badName("CustomName",StringTag.valueOf(repeat('\u20ac',10923)))); check("name_exact_packet_byte_limit_preserved",NameLifetimeChecks::packetBoundary); - check("old_maximum_name_remains_recoverable_when_native_json_cannot_fit",()->{CompoundNBT n=base();String original=repeat('a',32767);n.putString("Name",original);EntityLootableBody b=read(n);RuntimeChecks.require(b.getCustomNameTag().equals("Saved Owner"),"Unencodable native name entered metadata");retained(b,"Name",StringNBT.valueOf(original));stable(b);}); - check("CustomName_wrong_type_retained",()->badName("CustomName",IntNBT.valueOf(27))); - check("CustomName_over_packet_string_limit",()->badName("CustomName",StringNBT.valueOf(repeat('c',32768)))); + check("old_maximum_name_remains_recoverable_when_native_json_cannot_fit",()->{CompoundTag n=base();String original=repeat('a',32767);n.putString("Name",original);EntityLootableBody b=read(n);RuntimeChecks.require(b.getCustomNameTag().equals("Saved Owner"),"Unencodable native name entered metadata");retained(b,"Name",StringTag.valueOf(original));stable(b);}); + check("CustomName_wrong_type_retained",()->badName("CustomName",IntTag.valueOf(27))); + check("CustomName_over_packet_string_limit",()->badName("CustomName",StringTag.valueOf(repeat('c',32768)))); check("custom_name_fallback_and_source_immutable",NameLifetimeChecks::fallback); check("valid_name_authoritative_without_username_regex",NameLifetimeChecks::namePriority); check("save_before_first_tick_keeps_name",NameLifetimeChecks::earlySave); @@ -81,49 +92,49 @@ private static void check(String name,Check test) { long clock=world.getGameTime(),rate=LootableBodies.ticksPerItemDecay,life=LootableBodies.corpseDecayTime; boolean decay=LootableBodies.allowCorpseDecay,empty=LootableBodies.decayOnlyWhenEmpty; Set before=new HashSet<>(FixtureApi.entities(world)); - try{((net.minecraft.world.storage.IServerWorldInfo)world.getWorldInfo()).setGameTime(10000);LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1;test.run();results.add("PASS "+name);System.out.println("NAME_LIFETIME_PASS "+name);} + 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.storage.IServerWorldInfo)world.getWorldInfo()).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))world.removeEntity(e);} } - static CompoundNBT base(){EntityLootableBody b=new EntityLootableBody(world);b.setPosition(160.5,80,160.5);b.setNoGravity(true);b.setCustomNameTag("Saved Owner");b.setInventorySlotContents(6,NbtRecoveryChecks.item(120,17));CompoundNBT n=new CompoundNBT();b.writeUnlessPassenger(n);n.putString("Name","Saved Owner");n.putByte("Vac",(byte)20);return n;} - static EntityLootableBody read(CompoundNBT n){CompoundNBT copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.read(n);RuntimeChecks.require(same(n,copy),"Input NBT mutated");return b;} - static CompoundNBT saved(EntityLootableBody b){CompoundNBT n=new CompoundNBT();b.writeUnlessPassenger(n);return n;} - static void retained(EntityLootableBody b,String key,INBT raw){ListNBT l=saved(b).getList(RECOVERY,10);boolean found=false;for(int i=0;i=0 && saved(b).getLong("DecayTime")>=0 && termination(b)<0,"Negative world clock caused invalid lifetime");} - private static EntityLootableBody tool(long now,long interval){((net.minecraft.world.storage.IServerWorldInfo)world.getWorldInfo()).setGameTime(now);EntityLootableBody b=timed(0);b.clear();ItemStack tool=new ItemStack(Items.DIAMOND_PICKAXE);tool.setDamage(17);b.setInventorySlotContents(6,tool);LootableBodies.ticksPerItemDecay=interval;return b;} - private static void catchup(long now,long interval){EntityLootableBody b=tool(now,interval);b.baseTick();ItemStack s=b.getStackInSlot(6);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getCount()==1 && s.getDamage()==s.getMaxDamage()-1,"Catchup overflow repaired, erased or under-decayed tool");long stamp=now-now%interval;RuntimeChecks.require(saved(b).getLong("DecayTime")==stamp,"Catchup timestamp wrapped or lost remainder");b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==stamp && s.getDamage()==s.getMaxDamage()-1,"Catchup repeated");} + private static void missingName(){EntityLootableBody b=read(base());b.baseTick();CompoundTag n=base();n.remove("Name");n.remove("CustomName");b.load(n);RuntimeChecks.require(b.getCustomNameTag().isEmpty() && b.getGameProfile()==null,"Repeated load kept old attribution");b.baseTick();RuntimeChecks.require(b.getGameProfile()==null && !saved(b).contains("Name"),"Old profile resurrected");} + private static void reusedWriter(){EntityLootableBody b=read(base());b.baseTick();CompoundTag n=saved(b);n.putByte("Vac",(byte)3);b.setUserName("");b.addAdditionalSaveData(n);RuntimeChecks.require(!n.contains("Name") && !n.contains("Vac"),"Writer resurrected old scalar fields");} + private static void clearedCustomName(){EntityLootableBody b=read(base());b.baseTick();CompoundTag n=saved(b);b.setUserName("");b.saveAsPassenger(n);RuntimeChecks.require(!n.contains("CustomName") && read(n).getCustomNameTag().isEmpty(),"Reused vanilla CustomName restored cleared attribution");} + private static void blankName(){CompoundTag n=base();n.putString("Name"," ");EntityLootableBody b=read(n);b.baseTick();RuntimeChecks.require(b.getCustomNameTag().isEmpty() && b.getGameProfile()==null && !saved(b).contains("Name"),"Blank name semantics changed");} + private static void missingTimers(){EntityLootableBody b=read(base());CompoundTag n=base();n.remove("DeathTime");n.remove("DecayTime");n.remove("Vac");((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(11000);b.load(n);CompoundTag s=saved(b);RuntimeChecks.require(s.getLong("DeathTime")==11000 && s.getLong("DecayTime")==11000 && !s.contains("Vac"),"Repeated load kept stale timers");} + private static void animation(){CompoundTag n=base();n.putLong("DeathTime",9999);EntityLootableBody b=read(n);RuntimeChecks.require(b.deathTime==0,"Custom lifetime loaded into vanilla short death animation");} + private static void integral(int type){Tag[] values={ByteTag.valueOf((byte)10),ShortTag.valueOf((short)10),IntTag.valueOf(10),LongTag.valueOf(10)};CompoundTag n=base();for(String k:new String[]{"Vac","DeathTime","DecayTime"})n.put(k,values[type].copy());EntityLootableBody b=read(n);CompoundTag s=saved(b);RuntimeChecks.require(s.getByte("Vac")==10 && s.getLong("DeathTime")==10 && s.getLong("DecayTime")==10 && !s.contains(RECOVERY),"Lossless integral scalar changed");} + private static void zero(){CompoundTag n=base();for(String k:new String[]{"Vac","DeathTime","DecayTime"})n.putLong(k,0);EntityLootableBody b=read(n);CompoundTag s=saved(b);RuntimeChecks.require(s.getLong("DeathTime")==0 && s.getLong("DecayTime")==0 && s.contains("Vac") && s.getByte("Vac")==0 && !s.contains(RECOVERY),"Valid zero discarded");} + private static void vacuumLimit(){CompoundTag n=base();n.putLong("Vac",20);RuntimeChecks.require(!saved(read(n)).contains(RECOVERY),"Valid limit rejected");invalid("Vac",IntTag.valueOf(21));} + private static EntityLootableBody timed(long timestamp){CompoundTag n=base();n.putLong("DeathTime",timestamp);n.putLong("DecayTime",timestamp);return read(n);} + private static void future(){CompoundTag n=base();n.putLong("DeathTime",Long.MAX_VALUE);n.putLong("DecayTime",Long.MAX_VALUE);EntityLootableBody b=read(n);LootableBodies.ticksPerItemDecay=10;((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(10011);b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==10010,"Future timestamp stalled normal progression");} + private static void rollback(){EntityLootableBody b=timed(9999);((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(100);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=false;LootableBodies.ticksPerItemDecay=10;b.baseTick();RuntimeChecks.require(saved(b).getLong("DeathTime")==100 && saved(b).getLong("DecayTime")==100 && termination(b)<0,"Clock rollback did not rebase safely");} + private static void negativeClock(){EntityLootableBody b=timed(0);((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(Long.MIN_VALUE);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=false;LootableBodies.ticksPerItemDecay=10;b.baseTick();RuntimeChecks.require(saved(b).getLong("DeathTime")>=0 && saved(b).getLong("DecayTime")>=0 && termination(b)<0,"Negative world clock caused invalid lifetime");} + private static EntityLootableBody tool(long now,long interval){((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(now);EntityLootableBody b=timed(0);b.clearContent();ItemStack tool=new ItemStack(Items.DIAMOND_PICKAXE);tool.setDamageValue(17);b.setItem(6,tool);LootableBodies.ticksPerItemDecay=interval;return b;} + private static void catchup(long now,long interval){EntityLootableBody b=tool(now,interval);b.baseTick();ItemStack s=b.getItem(6);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getCount()==1 && s.getDamageValue()==s.getMaxDamage()-1,"Catchup overflow repaired, erased or under-decayed tool");long stamp=now-now%interval;RuntimeChecks.require(saved(b).getLong("DecayTime")==stamp,"Catchup timestamp wrapped or lost remainder");b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==stamp && s.getDamageValue()==s.getMaxDamage()-1,"Catchup repeated");} private static void largeCatchup(){catchup((1L<<31)+1,1);} private static void maxCatchup(){catchup(Long.MAX_VALUE,6000);} - private static void longInterval(){EntityLootableBody b=tool(Long.MAX_VALUE,Long.MAX_VALUE/2);b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==Long.MAX_VALUE-1 && b.getStackInSlot(6).getDamage()==19,"Wide interval arithmetic changed");} - private static void boundary(){EntityLootableBody b=tool(10,10);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=false;LootableBodies.corpseDecayTime=10;b.baseTick();RuntimeChecks.require(b.getStackInSlot(6).getDamage()==17 && termination(b)<0,"Strict > boundary changed");((net.minecraft.world.storage.IServerWorldInfo)world.getWorldInfo()).setGameTime(11);b.baseTick();RuntimeChecks.require(b.getStackInSlot(6).getDamage()==18 && termination(b)>=0,"Elapsed threshold ignored");} + private static void longInterval(){EntityLootableBody b=tool(Long.MAX_VALUE,Long.MAX_VALUE/2);b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==Long.MAX_VALUE-1 && b.getItem(6).getDamageValue()==19,"Wide interval arithmetic changed");} + private static void boundary(){EntityLootableBody b=tool(10,10);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=false;LootableBodies.corpseDecayTime=10;b.baseTick();RuntimeChecks.require(b.getItem(6).getDamageValue()==17 && termination(b)<0,"Strict > boundary changed");((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(11);b.baseTick();RuntimeChecks.require(b.getItem(6).getDamageValue()==18 && termination(b)>=0,"Elapsed threshold ignored");} private static void occupied(){EntityLootableBody b=timed(0);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=true;LootableBodies.corpseDecayTime=40;b.baseTick();RuntimeChecks.require(saved(b).getLong("DeathTime")==10000 && termination(b)<0,"Occupied default no longer pauses expiration");} - private static void disabled(){EntityLootableBody b=tool(Long.MAX_VALUE,-1);b.baseTick();RuntimeChecks.require(b.getStackInSlot(6).getDamage()==17 && saved(b).getLong("DecayTime")==0 && saved(b).getLong("DeathTime")==0,"Disabled timer changed state");} + private static void disabled(){EntityLootableBody b=tool(Long.MAX_VALUE,-1);b.baseTick();RuntimeChecks.require(b.getItem(6).getDamageValue()==17 && saved(b).getLong("DecayTime")==0 && saved(b).getLong("DeathTime")==0,"Disabled timer changed state");} private static int termination(EntityLootableBody b){try{Field f=EntityLootableBody.class.getDeclaredField("terminate");f.setAccessible(true);return f.getInt(b);}catch(Exception e){throw new AssertionError(e);}} - private static void metadataOnly(){CompoundNBT n=base();n.put("Equipment",new ListNBT());n.putString("DeathTime","opaque");EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Scalar evidence counted as inventory");LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=true;LootableBodies.corpseDecayTime=40;((net.minecraft.world.storage.IServerWorldInfo)world.getWorldInfo()).setGameTime(10041);b.baseTick();RuntimeChecks.require(termination(b)>=0,"Scalar evidence made empty corpse immortal");} - private static void inventory(){CompoundNBT n=base();CompoundNBT missing=NbtRecoveryChecks.entry(121,11,7);missing.putString("id","absentfixture:missing");n.getList("Equipment",10).add(missing);n.putString("DeathTime","bad");n.put("Name",new CompoundNBT());EntityLootableBody b=read(n);NbtRecoveryChecks.retained(b,missing);stable(b);b.read(saved(b));stable(b);RuntimeChecks.require(saved(b).getList(RECOVERY,10).size()==2 && saved(b).getList(NbtRecoveryChecks.RECOVERY,10).size()==1,"Raw scalar/item evidence accumulated or disappeared");} - private static void unknownRecovery(){CompoundNBT n=base(),record=new CompoundNBT();record.putString("Future","opaque");n.put(RECOVERY,NbtRecoveryChecks.list(record));EntityLootableBody b=read(n);RuntimeChecks.require(saved(b).getList(RECOVERY,10).getCompound(0).equals(record),"Unknown metadata evidence changed");stable(b);} - private static void wrongRecovery(){CompoundNBT n=base();StringNBT raw=StringNBT.valueOf("opaque");n.put(RECOVERY,raw);EntityLootableBody b=read(n);retained(b,"Recovery",raw);stable(b);} + private static void metadataOnly(){CompoundTag n=base();n.put("Equipment",new ListTag());n.putString("DeathTime","opaque");EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Scalar evidence counted as inventory");LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=true;LootableBodies.corpseDecayTime=40;((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(10041);b.baseTick();RuntimeChecks.require(termination(b)>=0,"Scalar evidence made empty corpse immortal");} + private static void inventory(){CompoundTag n=base();CompoundTag missing=NbtRecoveryChecks.entry(121,11,7);missing.putString("id","absentfixture:missing");n.getList("Equipment",10).add(missing);n.putString("DeathTime","bad");n.put("Name",new CompoundTag());EntityLootableBody b=read(n);NbtRecoveryChecks.retained(b,missing);stable(b);b.load(saved(b));stable(b);RuntimeChecks.require(saved(b).getList(RECOVERY,10).size()==2 && saved(b).getList(NbtRecoveryChecks.RECOVERY,10).size()==1,"Raw scalar/item evidence accumulated or disappeared");} + private static void unknownRecovery(){CompoundTag n=base(),record=new CompoundTag();record.putString("Future","opaque");n.put(RECOVERY,NbtRecoveryChecks.list(record));EntityLootableBody b=read(n);RuntimeChecks.require(saved(b).getList(RECOVERY,10).getCompound(0).equals(record),"Unknown metadata evidence changed");stable(b);} + private static void wrongRecovery(){CompoundTag n=base();StringTag raw=StringTag.valueOf("opaque");n.put(RECOVERY,raw);EntityLootableBody b=read(n);retained(b,"Recovery",raw);stable(b);} private static void healthy(){EntityLootableBody b=read(base());b.baseTick();stable(b);RuntimeChecks.require(!saved(b).contains(RECOVERY),"Healthy save gained invalid-field data");} // DoubleNBT.equals uses ==, so NaN copies are unequal without any mutation. - static boolean same(INBT a,INBT b){return TypedNbt.same(a,b);} + static boolean same(Tag a,Tag b){return TypedNbt.same(a,b);} private static String repeat(char c,int size){char[] a=new char[size];Arrays.fill(a,c);return new String(a);} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java index 0469b4e..485ff9a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java @@ -1,7 +1,7 @@ package cyano.lootable.buildtest; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.util.registry.Registry; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.core.Registry; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; @@ -11,20 +11,21 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.MainMenuScreen; -import net.minecraft.client.resources.Language; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityType; -import net.minecraft.block.Blocks; -import net.minecraft.item.Items; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.client.resources.language.LanguageInfo; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.item.Items; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.Hand; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.ScreenShotHelper; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.InteractionHand; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.client.Screenshot; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.GameType; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -36,12 +37,12 @@ final class NamelessCorpseClientChecks { private static final UUID BODY = UUID.fromString("00000000-0000-4000-8000-000000000119"); private int stage, frames, locale; private Future pending; - private Language originalLanguage; + private LanguageInfo originalLanguage; private boolean originalUnicode, originalBidi; private boolean renderedBody; @SubscribeEvent public void rendered(RenderLivingEvent.Post event) { - if(event.getEntity().getUniqueID().equals(BODY)) renderedBody=true; + if(event.getEntity().getUUID().equals(BODY)) renderedBody=true; } static void englishContract(Minecraft mc) throws Exception { @@ -60,96 +61,96 @@ private static String translation(Minecraft mc, String code) throws Exception { private static void assertLocale(Minecraft mc, String code) throws Exception { String expected = translation(mc, code); - EntityLootableBody body = new EntityLootableBody(mc.world); - body.setPosition(18.5,80,18.5); + EntityLootableBody body = new EntityLootableBody(mc.level); + body.setPos(18.5,80,18.5); RuntimeChecks.disableVacuum(body); - for (int slot=0;slot<48;slot++) body.setInventorySlotContents(slot,RuntimeChecks.stack(slot)); + for (int slot=0;slot<48;slot++) body.setItem(slot,RuntimeChecks.stack(slot)); body.addItem(RuntimeChecks.stack(70)); - CompoundNBT data = NameLifetimeChecks.saved(body); - CompoundNBT retained = new CompoundNBT(); + CompoundTag data = NameLifetimeChecks.saved(body); + CompoundTag retained = new CompoundTag(); retained.putString("Source","FutureFixture"); retained.putString("Data","retain verbatim"); data.put("LootableBodiesUnresolved",NbtRecoveryChecks.list(retained)); - body = (EntityLootableBody)FixtureApi.loadEntity(data,mc.world); + body = (EntityLootableBody)FixtureApi.loadEntity(data,mc.level); RuntimeChecks.require(body != null && new ResourceLocation("lootablebodies:corpse").equals(body.getType().getRegistryName()), "Registry/display identity changed"); - CompoundNBT before = NameLifetimeChecks.saved(body); + 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); RuntimeChecks.require(!body.hasCustomName() && !before.contains("Name") && !before.contains("CustomName"), "Localized fallback became an owner name"); RuntimeChecks.require(NameLifetimeChecks.same(before,NameLifetimeChecks.saved(body)), "Reading fallback mutated complete NBT"); - EntityLootableBody loaded = (EntityLootableBody)FixtureApi.loadEntity(before.copy(),mc.world); + EntityLootableBody loaded = (EntityLootableBody)FixtureApi.loadEntity(before.copy(),mc.level); RuntimeChecks.require(expected.equals(loaded.getName().getString()) && NameLifetimeChecks.same(before,NameLifetimeChecks.saved(loaded)), "Fallback reload changed UUID, inventory, recovery or other NBT"); for (String owner : new String[]{"Owner Fixture", "entity.Corpse.name", "Owner \u00e9\u540d"}) { - body.setUserName(owner); CompoundNBT named = NameLifetimeChecks.saved(body); + body.setUserName(owner); CompoundTag named = NameLifetimeChecks.saved(body); RuntimeChecks.require(owner.equals(body.getName().getString()) && owner.equals(body.getDisplayName().getString()), "Owner name was translated/renamed"); RuntimeChecks.require(NameLifetimeChecks.same(named,NameLifetimeChecks.saved(body)), "Owner lookup mutated NBT"); } - body.setUserName(""); CompoundNBT cleared = NameLifetimeChecks.saved(body); + body.setUserName(""); CompoundTag cleared = NameLifetimeChecks.saved(body); RuntimeChecks.require(expected.equals(body.getName().getString()) && !body.hasCustomName() && NameLifetimeChecks.same(cleared,NameLifetimeChecks.saved(body)), "Cleared owner did not regain transient fallback"); ItemStack egg = new ItemStack(LootableBodies.CORPSE_EGG); - CompoundNBT eggBefore = egg.write(new CompoundNBT()); - RuntimeChecks.require(egg.getDisplayName().getString().equals(net.minecraft.client.resources.I18n.format("item.lootablebodies.corpse_spawn_egg")) && !egg.getDisplayName().getString().contains("entity.Corpse.name"), "Untranslated spawn egg in "+code); - RuntimeChecks.require(NameLifetimeChecks.same(eggBefore,egg.write(new CompoundNBT())), "Egg name lookup mutated its native ID/NBT"); + CompoundTag eggBefore = egg.save(new CompoundTag()); + RuntimeChecks.require(egg.getHoverName().getString().equals(net.minecraft.client.resources.language.I18n.get("item.lootablebodies.corpse_spawn_egg")) && !egg.getHoverName().getString().contains("entity.Corpse.name"), "Untranslated spawn egg in "+code); + RuntimeChecks.require(NameLifetimeChecks.same(eggBefore,egg.save(new CompoundTag())), "Egg name lookup mutated its native ID/NBT"); System.out.println("NAME_TRANSLATION_PASS "+code); } void tick(Minecraft mc) throws Exception { - if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; - originalLanguage=mc.getLanguageManager().getCurrentLanguage(); - originalUnicode=mc.gameSettings.forceUnicodeFont; originalBidi=mc.fontRenderer.getBidiFlag(); + if(stage==0 && mc.screen instanceof TitleScreen) { + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; + originalLanguage=mc.getLanguageManager().getSelected(); + originalUnicode=mc.options.forceUnicodeFont; originalBidi=mc.font.isBidirectional(); MinecraftForge.EVENT_BUS.register(this); - FixtureApi.ClientWorlds.create(mc,"name-fixture","Name fixture",net.minecraft.world.GameType.CREATIVE,false);stage=1; - } else if(stage==1 && mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null) { - pending=mc.getIntegratedServer().runAsync(() -> { - ServerWorld world=mc.getIntegratedServer().getWorld(FixtureApi.dimensionKey(0)); - world.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_SPAWNING).set(false,null); - for(int x=12;x<=24;x++)for(int z=12;z<=24;z++)world.setBlockState(new BlockPos(x,79,z),Blocks.STONE.getDefaultState()); - EntityLootableBody body=new EntityLootableBody(world);body.setUniqueId(BODY);body.setPosition(18.5,80,18.5);body.setNoGravity(true);RuntimeChecks.disableVacuum(body); - for(int i=0;i<48;i++)body.setInventorySlotContents(i,RuntimeChecks.stack(i)); - RuntimeChecks.require(world.addEntity(body),"Spawn nameless graphical fixture"); - EntityLootableBody named=new EntityLootableBody(world);named.setPosition(20.5,80,18.5);named.setNoGravity(true);named.setUserName("Owner Fixture");RuntimeChecks.disableVacuum(named);world.addEntity(named); - net.minecraft.entity.player.ServerPlayerEntity player=mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID()); - player.abilities.isFlying=true;player.sendPlayerAbilities();player.connection.setPlayerLocation(18.5,80,14.5,0,12); + 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) { + pending=mc.getSingleplayerServer().submitAsync(() -> { + ServerLevel world=mc.getSingleplayerServer().getLevel(FixtureApi.dimensionKey(0)); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); + for(int x=12;x<=24;x++)for(int z=12;z<=24;z++)world.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); + EntityLootableBody body=new EntityLootableBody(world);body.setUUID(BODY);body.setPos(18.5,80,18.5);body.setNoGravity(true);RuntimeChecks.disableVacuum(body); + for(int i=0;i<48;i++)body.setItem(i,RuntimeChecks.stack(i)); + RuntimeChecks.require(world.addFreshEntity(body),"Spawn nameless graphical fixture"); + EntityLootableBody named=new EntityLootableBody(world);named.setPos(20.5,80,18.5);named.setNoGravity(true);named.setUserName("Owner Fixture");RuntimeChecks.disableVacuum(named);world.addFreshEntity(named); + net.minecraft.server.level.ServerPlayer player=mc.getSingleplayerServer().getPlayerList().getPlayer(mc.player.getUUID()); + player.getAbilities().flying=true;player.onUpdateAbilities();player.connection.teleport(18.5,80,14.5,0,12); });stage=2;frames=0; } else if(stage==2 && pending.isDone() && ++frames>30) { pending.get();stage=3;frames=0; } else if(stage==3) { // Keep the disposable view attached to the current network player. // The integrated-server join may replace its initial camera entity. - mc.setRenderViewEntity(mc.player);mc.gameSettings.setPointOfView(net.minecraft.client.settings.PointOfView.FIRST_PERSON); - mc.player.rotationYaw=mc.player.prevRotationYaw=0; - mc.player.rotationPitch=mc.player.prevRotationPitch=20; + mc.setCameraEntity(mc.player);mc.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON); + mc.player.setYRot(0);mc.player.yRotO=0; + mc.player.setXRot(20);mc.player.xRotO=20; String code=LOCALES[locale]; if(frames++==0) { renderedBody=false; - mc.getLanguageManager().setCurrentLanguage(new Language(code,code,code,false)); + mc.getLanguageManager().setSelected(new LanguageInfo(code,code,code,false)); mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager()); // Native16 LanguageMap controls bidirectional text after resource reload. assertLocale(mc,code); - try(PrintWriter out=new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(mc.gameDir,"name-checks.txt"),true),StandardCharsets.UTF_8))) {out.println("PASS locale_"+code+"_fallback_owner_egg_complete_nbt");} + try(PrintWriter out=new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File(mc.gameDirectory,"name-checks.txt"),true),StandardCharsets.UTF_8))) {out.println("PASS locale_"+code+"_fallback_owner_egg_complete_nbt");} } if(frames==15 && Arrays.asList("en_us","de_de","en_pt","es_es","zh_cn").contains(code)) { - System.out.println("NAME_CAMERA player="+mc.player.getPosition()+" view="+mc.getRenderViewEntity().getPosition()+" yaw="+mc.player.rotationYaw+" pitch="+mc.player.rotationPitch+" rendered="+renderedBody); + System.out.println("NAME_CAMERA player="+mc.player.blockPosition()+" view="+mc.getCameraEntity().blockPosition()+" yaw="+mc.player.getYRot()+" pitch="+mc.player.getXRot()+" rendered="+renderedBody); RuntimeChecks.require(renderedBody,"No corpse rendered for label screenshot "+code); - ScreenShotHelper.saveScreenshot(mc.gameDir,"name-"+code+".png",mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); + Screenshot.grab(mc.gameDirectory,"name-"+code+".png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); } if(frames>20) {frames=0;locale++;if(locale==LOCALES.length) { - mc.getLanguageManager().setCurrentLanguage(originalLanguage);mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager()); - RuntimeChecks.require(mc.fontRenderer.getBidiFlag()==originalBidi,"Restored language direction changed");stage=4; + mc.getLanguageManager().setSelected(originalLanguage);mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager()); + RuntimeChecks.require(mc.font.isBidirectional()==originalBidi,"Restored language direction changed");stage=4; }} } else if(stage==4) { - for(Entity entity:FixtureApi.entities(mc.world))if(entity.getUniqueID().equals(BODY)) { + for(Entity entity:FixtureApi.entities(mc.level))if(entity.getUUID().equals(BODY)) { RuntimeChecks.require("Corpse".equals(entity.getName().getString()) && !entity.hasCustomName(),"Network corpse has wrong English fallback/custom name"); - mc.playerController.interactWithEntity(mc.player,entity,Hand.MAIN_HAND);stage=5;frames=0;break; + mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND);stage=5;frames=0;break; } - } else if(stage==5 && mc.currentScreen instanceof CorpseGUIContainer && ++frames>20) { - for(int slot=0;slot<48;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.openContainer.getSlot(slot).getStack(),RuntimeChecks.stack(slot)),"Name fix changed network inventory slot "+slot); - ScreenShotHelper.saveScreenshot(mc.gameDir,"name-english-inventory.png",mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); - new File(mc.gameDir,"client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_NAME_TRANSLATION_PASS locales="+LOCALES.length); - mc.player.closeScreen();FixtureConnections.disconnect(mc);stage=6; - } else if(stage==6 && FixtureConnections.stopped(mc)) {mc.shutdown();stage=7;} + } else if(stage==5 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { + for(int slot=0;slot<48;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(slot)),"Name fix changed network inventory slot "+slot); + Screenshot.grab(mc.gameDirectory,"name-english-inventory.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + new File(mc.gameDirectory,"client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_NAME_TRANSLATION_PASS locales="+LOCALES.length); + mc.player.closeContainer();FixtureConnections.disconnect(mc);stage=6; + } else if(stage==6 && FixtureConnections.stopped(mc)) {mc.stop();stage=7;} } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java new file mode 100644 index 0000000..35360cd --- /dev/null +++ b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java @@ -0,0 +1,78 @@ +package cyano.lootable.buildtest; + +import com.mojang.datafixers.*; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.serialization.Dynamic; +import java.nio.file.*; +import java.util.concurrent.atomic.AtomicInteger; +import net.minecraft.server.MinecraftServer; +import net.minecraft.nbt.*; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +/** Controlled packaged pre-world conversion/refusal; separate from old writers. */ +public final class NativePreflightChecks { + static final String INJECTED="NATIVE117_INJECTED_ENTITY_CHUNK_REFUSAL"; + private static final AtomicInteger early=new AtomicInteger(),worlds=new AtomicInteger(),starts=new AtomicInteger(); + private static String mode; + static void install(net.minecraftforge.eventbus.api.IEventBus bus) { + mode=System.getProperty("lootablebodies.nativePreflight",""); + if(mode.isEmpty())return; + if(!mode.equals("refuse") && !mode.equals("convert"))throw new IllegalArgumentException("Unknown preflight control"); + net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(new NativePreflightChecks()); + Runtime.getRuntime().addShutdownHook(new Thread(()->{ + try {Files.writeString(Path.of("preflight-control.receipt"),"mode="+mode+" earlyConversions="+early.get()+" worldLoads="+worlds.get()+" serverStarts="+starts.get()+"\n",StandardOpenOption.CREATE_NEW);} + catch(Exception failure){failure.printStackTrace();} + },"PreflightControlReceipt")); + bus.addListener((net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent event)->event.enqueueWork(()->{ + DataFixer nativeFixer=NativeVanillaDataFixer.get(); + DataFixer observer=new DataFixer() { + public Schema getSchema(int version){return nativeFixer.getSchema(version);} + public Dynamic update(DSL.TypeReference type,Dynamic input,int from,int to) { + if(type.typeName().equals("entity_chunk") && net.minecraftforge.fmllegacy.server.ServerLifecycleHooks.getCurrentServer()==null) { + boolean preflight=java.util.Arrays.stream(Thread.currentThread().getStackTrace()).anyMatch( + frame->frame.getClassName().equals("cyano.lootable.upgrade.UpgradeJournal") && frame.getMethodName().equals("validateEntityChunks")); + if(!preflight || worlds.get()!=0 || starts.get()!=0)throw new AssertionError("Entity conversion missed the pre-world boundary"); + early.incrementAndGet();System.out.println("NATIVE117_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); + })); + } + @SubscribeEvent public void loaded(net.minecraftforge.event.world.WorldEvent.Load event){worlds.incrementAndGet();} + @SubscribeEvent public void starting(net.minecraftforge.fmlserverevents.FMLServerAboutToStartEvent 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"); + net.minecraft.server.level.ServerLevel world=server.overworld();RuntimeChecks.awaitChunk(world,0,0); + cyano.lootable.entities.EntityLootableBody body=RuntimeChecks.find(world, + java.util.UUID.fromString("00000000-1171-4171-8171-000000000099")); + RuntimeChecks.require(body!=null && body.getContainerSize()==48,"Native entity storage did not admit the preserved corpse"); + for(int slot=0;slot<48;slot++) { + net.minecraft.world.item.ItemStack item=body.getItem(slot); + // The synthetic codec source uses diamonds even in armour slots. + // Existing placement rules preserve those invalid records in Aux. + if(slot<4){RuntimeChecks.require(item.isEmpty(),"Invalid diamond entered an armour slot");continue;} + RuntimeChecks.require(item.getItem()==net.minecraft.world.item.Items.DIAMOND && item.getCount()==slot+1 + && item.hasTag() && item.getTag().getString("keep").equals("slot-"+slot),"Preflight inventory changed at slot"+slot); + } + CompoundTag saved=new CompoundTag();body.saveAsPassenger(saved); + RuntimeChecks.require(saved.getInt("LootableBodiesDataVersion")==7,"Preflight corpse marker was not advanced"); + ListTag aux=saved.getList("Aux",10);RuntimeChecks.require(aux.size()==5,"Preflight Aux record count changed"); + for(int index=0;index<5;index++) { + int sourceSlot=index<4?index:47;CompoundTag item=aux.getCompound(index); + RuntimeChecks.require(item.getString("id").equals("minecraft:diamond") && item.getByte("Count")==sourceSlot+1 + && item.getCompound("tag").getString("keep").equals("slot-"+sourceSlot),"Preflight Aux changed at record"+index); + } + RuntimeChecks.require(saved.getList("LootableBodiesUnresolved",10).stream().anyMatch(value->value instanceof CompoundTag + && ((CompoundTag)value).getString("Source").equals("FixtureUnknown") + && ((CompoundTag)value).getCompound("Data").getString("id").equals("minecraft:diamond") + && ((CompoundTag)value).getCompound("Data").getByte("Count")==47 + && ((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"); + } +} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java index bda4408..8e07372 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java @@ -1,9 +1,9 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.Slot; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.inventory.Inventory; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.SimpleContainer; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -11,17 +11,19 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.MainMenuScreen; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.inventory.container.ClickType; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; -import net.minecraft.util.Hand; -import net.minecraft.util.ScreenShotHelper; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.GameType; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.InteractionHand; +import net.minecraft.client.Screenshot; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameType; /** Real GUI recovery after loading malformed NBT, including unresolved data across another save. */ final class NbtClientChecks { @@ -30,70 +32,70 @@ private interface Action {void run() throws Exception;} private UUID playerId; private int stage,frames; private Future pending; - private CompoundNBT visible; + private CompoundTag visible; private Map before; - private ListNBT expectedPlayer; - void start(Minecraft mc){playerId=mc.player.getUniqueID();} + private ListTag expectedPlayer; + void start(Minecraft mc){playerId=mc.player.getUUID();} boolean tick(Minecraft mc) throws Exception { frames++;RuntimeChecks.require(frames<300,"NBT graphical stage timeout "+stage); if(stage==0){schedule(mc,()->{ - ServerPlayerEntity p=player(mc);p.inventory.clear();p.inventory.setItemStack(ItemStack.EMPTY);p.setGameType(GameType.SURVIVAL); - for(int x=157;x<=171;x++)for(int z=157;z<=163;z++)p.world.setBlockState(new BlockPos(x,79,z),Blocks.STONE.getDefaultState()); - NbtWorldChecks.seed(p.getServerWorld(),bodyId,160,folder(mc),"graphical"); + ServerPlayer p=player(mc);p.getInventory().clearContent();p.containerMenu.setCarried(ItemStack.EMPTY);p.setGameMode(GameType.SURVIVAL); + for(int x=157;x<=171;x++)for(int z=157;z<=163;z++)p.level.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); + NbtWorldChecks.seed(p.getLevel(),bodyId,160,folder(mc),"graphical"); });next(1);} else if(stage==1 && done() && frames>20){quit(mc);next(2);} - else if(stage==2 && FixtureConnections.stopped(mc)){mc.loadWorld("build-fixture");next(3);} - else if(stage==3 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.setPlayerLocation(168.5,80,159.5,0,0));next(30);} + else if(stage==2 && FixtureConnections.stopped(mc)){mc.loadLevel("build-fixture");next(3);} + else if(stage==3 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(168.5,80,159.5,0,0));next(30);} else if(stage==30 && done() && frames>20){schedule(mc,()->{ EntityLootableBody b=body(mc);for(int i=0;i<8;i++)b.baseTick();NbtWorldChecks.verify(b,false); before=NbtRecoveryChecks.items(b);visible=full(b);RuntimeChecks.saveExpected(folder(mc),"nbt-client-before",b); });next(4);} else if(stage==4 && done() && frames>20){if(open(mc))next(5);} - else if(stage==5 && gui(mc) && frames>20){verifyGui(mc);equal(mc.player.openContainer.getSlot(6).getStack(),NbtRecoveryChecks.item(120,17),"Malformed GUI recovery source differs");screenshot(mc,"nbt-recovered-corpse.png");mark(mc,"malformed_disk_load_gui_exact_inventory");mc.playerController.windowClick(mc.player.openContainer.windowId,6,0,ClickType.QUICK_MOVE,mc.player);next(6);} + else if(stage==5 && gui(mc) && frames>20){verifyGui(mc);equal(mc.player.containerMenu.getSlot(6).getItem(),NbtRecoveryChecks.item(120,17),"Malformed GUI recovery source differs");screenshot(mc,"nbt-recovered-corpse.png");mark(mc,"malformed_disk_load_gui_exact_inventory");mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,6,0,ClickType.QUICK_MOVE,mc.player);next(6);} else if(stage==6 && frames>25){schedule(mc,()->{ - Map after=NbtRecoveryChecks.items(body(mc));for(int i=0;i after=NbtRecoveryChecks.items(body(mc));for(int i=0;i20){schedule(mc,()->player(mc).connection.setPlayerLocation(168.5,80,159.5,0,0));next(90);} + 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==9 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(168.5,80,159.5,0,0));next(90);} else if(stage==90 && done() && frames>20){schedule(mc,()->{ body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"nbt-client-after",body(mc)); RuntimeChecks.require(Objects.equals(visible.get(NbtRecoveryChecks.RECOVERY),NbtRecoveryChecks.saved(body(mc)).get(NbtRecoveryChecks.RECOVERY)),"Unresolved raw data changed on graphical disk reload"); NbtWorldChecks.verifyMetadata(body(mc));RuntimeChecks.require(NameLifetimeChecks.same(visible.get(NameLifetimeChecks.RECOVERY),full(body(mc)).get(NameLifetimeChecks.RECOVERY)),"Raw scalar evidence changed on graphical reload"); - RuntimeChecks.require(expectedPlayer.equals(player(mc).inventory.write(new ListNBT())),"Recovered player NBT changed on graphical disk reload");checkPlayer(player(mc).inventory);mark(mc,"malformed_corpse_and_player_disk_reload"); + RuntimeChecks.require(expectedPlayer.equals(player(mc).getInventory().save(new ListTag())),"Recovered player NBT changed on graphical disk reload");checkPlayer(player(mc).getInventory());mark(mc,"malformed_corpse_and_player_disk_reload"); });next(10);} else if(stage==10 && done() && frames>20){if(open(mc))next(11);} - else if(stage==11 && gui(mc) && frames>20){verifyGui(mc);checkPlayer(mc.player.inventory);screenshot(mc,"nbt-after-recovery-reload.png");System.out.println("LOOTABLE_BODIES_NBT_CLIENT_PASS");return true;} + else if(stage==11 && gui(mc) && frames>20){verifyGui(mc);checkPlayer(mc.player.getInventory());screenshot(mc,"nbt-after-recovery-reload.png");System.out.println("LOOTABLE_BODIES_NBT_CLIENT_PASS");return true;} if((stage==4 || stage==5 || stage==10 || stage==11) && !gui(mc) && frames%10==0) { open(mc); if(frames==60) { - System.out.println("NBT_GUI_CLIENT_STATE position="+mc.player.getPosition()+" container="+mc.player.openContainer.getClass().getSimpleName()); - for(Entity entity:FixtureApi.entities(mc.world))if(entity instanceof EntityLootableBody)System.out.println("NBT_GUI_CLIENT_BODY uuid="+entity.getUniqueID()+" position="+entity.getPosition()+" id="+entity.getEntityId()); - schedule(mc,()->System.out.println("NBT_GUI_SERVER_STATE player="+player(mc).getPosition()+" body="+body(mc).getPosition()+" usable="+body(mc).isUsableByPlayer(player(mc))+" alive="+body(mc).isAlive()+" health="+body(mc).getHealth()+" container="+player(mc).openContainer.getClass().getSimpleName())); + System.out.println("NBT_GUI_CLIENT_STATE position="+mc.player.blockPosition()+" container="+mc.player.containerMenu.getClass().getSimpleName()); + for(Entity entity:FixtureApi.entities(mc.level))if(entity instanceof EntityLootableBody)System.out.println("NBT_GUI_CLIENT_BODY uuid="+entity.getUUID()+" position="+entity.blockPosition()+" id="+entity.getId()); + schedule(mc,()->System.out.println("NBT_GUI_SERVER_STATE player="+player(mc).blockPosition()+" body="+body(mc).blockPosition()+" usable="+body(mc).stillValid(player(mc))+" alive="+body(mc).isAlive()+" health="+body(mc).getHealth()+" container="+player(mc).containerMenu.getClass().getSimpleName())); } } return false; } - private void verifyGui(Minecraft mc){ItemStack[] expected=new ItemStack[48];ListNBT list=visible.getList("Equipment",10);for(int i=0;i actual=new TreeMap<>();for(int i=0;i expected=new TreeMap<>();add(expected,NbtRecoveryChecks.item(120,17));RuntimeChecks.require(actual.equals(expected),"Malformed GUI recovered player stack changed");} - private static void add(Map map,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundNBT n=s.write(new CompoundNBT());n.remove("Count");String key=n.toString();map.put(key,map.getOrDefault(key,0)+s.getCount());} + private void verifyGui(Minecraft mc){ItemStack[] expected=new ItemStack[48];ListTag list=visible.getList("Equipment",10);for(int i=0;i actual=new TreeMap<>();for(int i=0;i expected=new TreeMap<>();add(expected,NbtRecoveryChecks.item(120,17));RuntimeChecks.require(actual.equals(expected),"Malformed GUI recovered player stack changed");} + private static void add(Map map,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=s.save(new CompoundTag());n.remove("Count");String key=n.toString();map.put(key,map.getOrDefault(key,0)+s.getCount());} private static void equal(ItemStack a,ItemStack b,String why){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),why);} - private static CompoundNBT full(EntityLootableBody b){CompoundNBT n=new CompoundNBT();b.writeUnlessPassenger(n);return n;} - private ServerPlayerEntity player(Minecraft mc){return mc.getIntegratedServer().getPlayerList().getPlayerByUUID(playerId);} - private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(mc.getIntegratedServer().getWorld(FixtureApi.dimensionKey(0)),bodyId);} - private File folder(Minecraft mc){return player(mc).getServerWorld().getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile();} - private static boolean ready(Minecraft mc){return mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null;} - private static boolean gui(Minecraft mc){return mc.currentScreen instanceof CorpseGUIContainer;} - private boolean open(Minecraft mc){for(Entity e:FixtureApi.entities(mc.world))if(e.getUniqueID().equals(bodyId)){RuntimeChecks.require(((EntityLootableBody)e).getCustomNameTag().equals("NbtFixture"),"Malformed name reached tracked client entity");RuntimeChecks.require(e instanceof EntityLootableBody && Float.isFinite(((EntityLootableBody)e).getHealth()) && ((EntityLootableBody)e).getHealth()>0,"Malformed health reached tracked client entity");RuntimeChecks.require(((EntityLootableBody)e).getActivePotionEffects().isEmpty() && ((EntityLootableBody)e).hurtTime==0,"Malformed effect/hurt state reached tracked client entity");PositionMotionChecks.finite((EntityLootableBody)e);RotationFallChecks.finite((EntityLootableBody)e);mc.playerController.interactWithEntity(mc.player,e,Hand.MAIN_HAND);return true;}return false;} - private void schedule(Minecraft mc,Action action){pending=mc.getIntegratedServer().runAsync(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} + private static CompoundTag full(EntityLootableBody b){CompoundTag n=new CompoundTag();b.saveAsPassenger(n);return n;} + private ServerPlayer player(Minecraft mc){return mc.getSingleplayerServer().getPlayerList().getPlayer(playerId);} + private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(mc.getSingleplayerServer().getLevel(FixtureApi.dimensionKey(0)),bodyId);} + private File folder(Minecraft mc){return player(mc).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile();} + private static boolean ready(Minecraft mc){return mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null;} + private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} + private boolean open(Minecraft mc){for(Entity e:FixtureApi.entities(mc.level))if(e.getUUID().equals(bodyId)){RuntimeChecks.require(((EntityLootableBody)e).getCustomNameTag().equals("NbtFixture"),"Malformed name reached tracked client entity");RuntimeChecks.require(e instanceof EntityLootableBody && Float.isFinite(((EntityLootableBody)e).getHealth()) && ((EntityLootableBody)e).getHealth()>0,"Malformed health reached tracked client entity");RuntimeChecks.require(((EntityLootableBody)e).getActiveEffects().isEmpty() && ((EntityLootableBody)e).hurtTime==0,"Malformed effect/hurt state reached tracked client entity");PositionMotionChecks.finite((EntityLootableBody)e);RotationFallChecks.finite((EntityLootableBody)e);mc.gameMode.interact(mc.player,e,InteractionHand.MAIN_HAND);return true;}return false;} + private void schedule(Minecraft mc,Action action){pending=mc.getSingleplayerServer().submitAsync(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} private boolean done() throws Exception{if(!pending.isDone())return false;pending.get();return true;} private void next(int value){stage=value;frames=0;System.out.println("NBT_CLIENT_STAGE "+stage);} - private static void screenshot(Minecraft mc,String name){ScreenShotHelper.saveScreenshot(mc.gameDir,name,mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} - private static void quit(Minecraft mc){mc.player.closeScreen();FixtureConnections.disconnect(mc);} - private static void mark(Minecraft mc,String name) throws Exception{try(FileWriter out=new FileWriter(new File(mc.gameDir,"nbt-client-checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("NBT_CLIENT_CHECK_PASS "+name);} + private static void screenshot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} + private static void mark(Minecraft mc,String name) throws Exception{try(FileWriter out=new FileWriter(new File(mc.gameDirectory,"nbt-client-checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("NBT_CLIENT_CHECK_PASS "+name);} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java index 8cbdb13..4079846 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java @@ -1,64 +1,72 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.Slot; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.StringNBT; -import net.minecraft.nbt.ShortNBT; -import net.minecraft.nbt.LongNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.FloatNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ByteNBT; +import net.minecraft.world.inventory.Slot; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ByteTag; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerLevel; /** Malformed data is never permission to erase the surrounding inventory. */ final class NbtRecoveryChecks { static final String RECOVERY="LootableBodiesUnresolved"; private interface Check {void run() throws Exception;} - private static ServerWorld world; + private static ServerLevel world; private static int failures; private static final List results=new ArrayList<>(); - static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { + static void run(MinecraftServer server,ServerLevel target,File folder) throws Exception { world=target;failures=0;results.clear(); check("duplicate_distinct_stacks_keep_first_and_retain_second",()->duplicate(false)); check("duplicate_identical_stacks_are_not_deduplicated",()->duplicate(true)); check("missing_duplicate_does_not_erase_known_stack",NbtRecoveryChecks::missingDuplicate); - for(int slot:new int[]{-1,48,127,256}) {final int s=slot;check("invalid_slot_"+s,()->invalidSlot(IntNBT.valueOf(s)));} + for(int slot:new int[]{-1,48,127,256}) {final int s=slot;check("invalid_slot_"+s,()->invalidSlot(IntTag.valueOf(s)));} check("missing_slot_does_not_default_to_helmet",()->invalidSlot(null)); - check("string_slot_is_not_coerced_to_zero",()->invalidSlot(StringNBT.valueOf("7"))); - check("fractional_slot_is_not_truncated",()->invalidSlot(FloatNBT.valueOf(6.5F))); - check("long_slot_is_not_narrowed",()->invalidSlot(LongNBT.valueOf(4294967302L))); + check("string_slot_is_not_coerced_to_zero",()->invalidSlot(StringTag.valueOf("7"))); + check("fractional_slot_is_not_truncated",()->invalidSlot(FloatTag.valueOf(6.5F))); + check("long_slot_is_not_narrowed",()->invalidSlot(LongTag.valueOf(4294967302L))); check("valid_wide_slot_is_read_without_byte_wrap",NbtRecoveryChecks::validWideSlot); - check("missing_equipment_item_raw_retained",()->unreadable("Equipment","id",StringNBT.valueOf("absentfixture:missing"))); - check("missing_aux_item_raw_retained",()->unreadable("Aux","id",StringNBT.valueOf("absentfixture:missing"))); - check("air_item_is_not_a_replacement",()->unreadable("Equipment","id",StringNBT.valueOf("minecraft:air"))); - check("numeric_item_id_not_guessed",()->unreadable("Equipment","id",ShortNBT.valueOf((short)264))); + check("missing_equipment_item_raw_retained",()->unreadable("Equipment","id",StringTag.valueOf("absentfixture:missing"))); + check("missing_aux_item_raw_retained",()->unreadable("Aux","id",StringTag.valueOf("absentfixture:missing"))); + check("air_item_is_not_a_replacement",()->unreadable("Equipment","id",StringTag.valueOf("minecraft:air"))); + check("numeric_item_id_not_guessed",()->unreadable("Equipment","id",ShortTag.valueOf((short)264))); check("missing_count_retained_raw",()->unreadable("Equipment","Count",null)); - check("zero_count_retained_raw",()->unreadable("Equipment","Count",ByteNBT.valueOf((byte)0))); - check("negative_count_retained_raw",()->unreadable("Equipment","Count",ByteNBT.valueOf((byte)-1))); - check("wide_count_does_not_wrap_to_zero",()->unreadable("Equipment","Count",IntNBT.valueOf(256))); - check("string_count_retained_raw",()->unreadable("Equipment","Count",StringNBT.valueOf("17"))); - check("fractional_count_retained_raw",()->unreadable("Equipment","Count",FloatNBT.valueOf(17.5F))); + check("zero_count_retained_raw",()->unreadable("Equipment","Count",ByteTag.valueOf((byte)0))); + check("negative_count_retained_raw",()->unreadable("Equipment","Count",ByteTag.valueOf((byte)-1))); + check("wide_count_does_not_wrap_to_zero",()->unreadable("Equipment","Count",IntTag.valueOf(256))); + check("string_count_retained_raw",()->unreadable("Equipment","Count",StringTag.valueOf("17"))); + check("fractional_count_retained_raw",()->unreadable("Equipment","Count",FloatTag.valueOf(17.5F))); check("valid_integer_count_preserved",NbtRecoveryChecks::integerCount); - check("wrong_damage_type_retained_raw",()->unreadable("Equipment","Damage",StringNBT.valueOf("17"))); - check("negative_damage_not_reset",()->unreadable("Equipment","Damage",ShortNBT.valueOf((short)-1))); - check("wide_damage_not_narrowed",()->unreadable("Equipment","Damage",IntNBT.valueOf(32768))); - check("wrong_custom_tag_type_retained_raw",()->unreadable("Equipment","tag",StringNBT.valueOf("opaque"))); + check("wrong_damage_type_retained_raw",()->unreadable("Equipment","Damage",StringTag.valueOf("17"))); + check("negative_damage_not_reset",()->unreadable("Equipment","Damage",ShortTag.valueOf((short)-1))); + check("wide_damage_not_narrowed",()->unreadable("Equipment","Damage",IntTag.valueOf(32768))); + check("wrong_custom_tag_type_retained_raw",()->unreadable("Equipment","tag",StringTag.valueOf("opaque"))); check("unknown_capability_payload_retained_raw",NbtRecoveryChecks::unknownCaps); check("native116_unknown_capabilities_survive_copy_split_and_reload",NbtRecoveryChecks::nativeCaps); check("native116_capability_initialization_exposes_unhandled_data",NbtRecoveryChecks::initializedCaps); check("registered_capability_payload_remains_recoverable",NbtRecoveryChecks::knownCaps); - check("unknown_stack_root_data_retained_raw",()->unreadable("Equipment","UnknownPayload",StringNBT.valueOf("retain verbatim"))); + check("unknown_stack_root_data_retained_raw",()->unreadable("Equipment","UnknownPayload",StringTag.valueOf("retain verbatim"))); check("wrong_equipment_list_type_retained",()->wrongList("Equipment")); check("wrong_aux_list_type_retained",()->wrongList("Aux")); check("empty_aux_record_retained_without_null_queue",NbtRecoveryChecks::emptyAux); @@ -91,70 +99,70 @@ private static void check(String name,Check test){ finally{for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))world.removeEntity(e);} } static ItemStack item(int index,int count){ItemStack s=RuntimeChecks.stack(index);s.setCount(count);return s;} - static CompoundNBT entry(int index,int count,int slot){CompoundNBT n=item(index,count).write(new CompoundNBT());n.putByte("Slot",(byte)slot);return n;} - static ListNBT list(CompoundNBT... entries){ListNBT l=new ListNBT();for(CompoundNBT n:entries)l.add(n.copy());return l;} - static CompoundNBT root(CompoundNBT... entries){CompoundNBT n=new CompoundNBT();n.put("Equipment",list(entries));n.putByte("Vac",(byte)20);return n;} - static CompoundNBT saved(EntityLootableBody b){CompoundNBT n=new CompoundNBT();b.writeAdditional(n);return n;} - static EntityLootableBody read(CompoundNBT n){CompoundNBT copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.readAdditional(n);RuntimeChecks.require(n.equals(copy),"Reader modified source NBT");return b;} + static CompoundTag entry(int index,int count,int slot){CompoundTag n=item(index,count).save(new CompoundTag());n.putByte("Slot",(byte)slot);return n;} + static ListTag list(CompoundTag... entries){ListTag l=new ListTag();for(CompoundTag n:entries)l.add(n.copy());return l;} + static CompoundTag root(CompoundTag... entries){CompoundTag n=new CompoundTag();n.put("Equipment",list(entries));n.putByte("Vac",(byte)20);return n;} + static CompoundTag saved(EntityLootableBody b){CompoundTag n=new CompoundTag();b.addAdditionalSaveData(n);return n;} + static EntityLootableBody read(CompoundTag n){CompoundTag copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.readAdditionalSaveData(n);RuntimeChecks.require(n.equals(copy),"Reader modified source NBT");return b;} private static void equal(ItemStack a,ItemStack b,String why){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),why);} - private static void add(Map sums,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundNBT n=s.write(new CompoundNBT());n.remove("Count");String k=n.toString();sums.put(k,sums.getOrDefault(k,0)+s.getCount());} - static Map items(EntityLootableBody b){Map sums=new TreeMap<>();for(int i=0;i<48;i++)add(sums,b.getStackInSlot(i));ListNBT aux=saved(b).getList("Aux",10);for(int i=0;i sums,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=s.save(new CompoundTag());n.remove("Count");String k=n.toString();sums.put(k,sums.getOrDefault(k,0)+s.getCount());} + static Map items(EntityLootableBody b){Map sums=new TreeMap<>();for(int i=0;i<48;i++)add(sums,b.getItem(i));ListTag aux=saved(b).getList("Aux",10);for(int i=0;i sums=new TreeMap<>();for(ItemStack s:expected)add(sums,s);RuntimeChecks.require(items(b).equals(sums),"Recoverable item/count/damage/full NBT changed: expected="+sums+" actual="+items(b)+" retained="+saved(b).get(RECOVERY));} - static void retained(EntityLootableBody b,INBT raw){ListNBT records=saved(b).getList(RECOVERY,10);boolean found=false;for(int i=0;i>4,coordinate>>4);RawBody b=new RawBody(w);b.setUniqueId(id);b.setPosition(coordinate+.5,80,coordinate+.5);b.setNoGravity(true);b.setUserName("NbtFixture");b.setRotation(37);RuntimeChecks.disableVacuum(b); - for(int i=0;i<48;i++)b.setInventorySlotContents(i,known(i));RuntimeChecks.require(w.addEntity(b),"Raw fixture spawn"); - CompoundNBT n=new CompoundNBT();RuntimeChecks.require(b.writeUnlessPassenger(n),"Raw fixture serialization"); - try(FileOutputStream out=new FileOutputStream(new File(folder,"raw-"+name+".nbt"))){CompressedStreamTools.writeCompressed(n,out);} + static void seed(ServerLevel w,UUID id,int coordinate,File folder,String name) throws Exception { + RuntimeChecks.awaitChunk(w,coordinate>>4,coordinate>>4);RawBody b=new RawBody(w);b.setUUID(id);b.setPos(coordinate+.5,80,coordinate+.5);b.setNoGravity(true);b.setUserName("NbtFixture");b.setRotation(37);RuntimeChecks.disableVacuum(b); + for(int i=0;i<48;i++)b.setItem(i,known(i));RuntimeChecks.require(w.addFreshEntity(b),"Raw fixture spawn"); + CompoundTag n=new CompoundTag();RuntimeChecks.require(b.saveAsPassenger(n),"Raw fixture serialization"); + try(FileOutputStream out=new FileOutputStream(new File(folder,"raw-"+name+".nbt"))){NbtIo.writeCompressed(n,out);} } private static ItemStack known(int i){return i==6?NbtRecoveryChecks.item(120,17):NbtRecoveryChecks.item(i,i<6?1:64);} - private static CompoundNBT rawEquipment(){ - CompoundNBT n=new CompoundNBT();ListNBT equipment=new ListNBT(); + private static CompoundTag rawEquipment(){ + CompoundTag n=new CompoundTag();ListTag equipment=new ListTag(); for(int i=0;i<48;i++) { - CompoundNBT entry=known(i).write(new CompoundNBT());entry.putByte("Slot",(byte)i); + CompoundTag entry=known(i).save(new CompoundTag());entry.putByte("Slot",(byte)i); if(i==7){entry=NbtRecoveryChecks.entry(123,9,7);entry.putString("id",RETURNING);} if(i==8)entry=missing(124,5,8); if(i==9){entry=NbtRecoveryChecks.entry(125,1,9);entry.putByte("Count",(byte)0);} - if(i==10){entry=NbtRecoveryChecks.entry(129,6,10);CompoundNBT caps=new CompoundNBT();caps.putString("absentfixture:inventory","opaque capabilities");entry.put("ForgeCaps",caps);} + if(i==10){entry=NbtRecoveryChecks.entry(129,6,10);CompoundTag caps=new CompoundTag();caps.putString("absentfixture:inventory","opaque capabilities");entry.put("ForgeCaps",caps);} equipment.add(entry); } - equipment.add(NbtRecoveryChecks.entry(121,11,6));CompoundNBT bad=NbtRecoveryChecks.entry(122,13,6);bad.putInt("Slot",999);equipment.add(bad);n.put("Equipment",equipment); + equipment.add(NbtRecoveryChecks.entry(121,11,6));CompoundTag bad=NbtRecoveryChecks.entry(122,13,6);bad.putInt("Slot",999);equipment.add(bad);n.put("Equipment",equipment); n.put("Aux",NbtRecoveryChecks.list(NbtRecoveryChecks.entry(126,7,0),missing(127,3,0),NbtRecoveryChecks.entry(128,2,0),NbtRecoveryChecks.entry(130,19,0),NbtRecoveryChecks.entry(131,23,0))); n.putInt("Vac",256); n.putLong("DeathTime",Long.MIN_VALUE); n.putDouble("DecayTime",Double.NaN); - CompoundNBT name=new CompoundNBT();name.putString("Opaque","saved malformed attribution");n.put("Name",name); - CompoundNBT health=HealthAttributeChecks.malformedFields();for(String key:health.keySet())n.put(key,health.get(key).copy()); - CompoundNBT effects=EffectHurtChecks.malformedFields();for(String key:effects.keySet())n.put(key,effects.get(key).copy()); - CompoundNBT rotation=RotationFallChecks.malformedFields();for(String key:rotation.keySet())n.put(key,rotation.get(key).copy()); + CompoundTag name=new CompoundTag();name.putString("Opaque","saved malformed attribution");n.put("Name",name); + CompoundTag health=HealthAttributeChecks.malformedFields();for(String key:health.getAllKeys())n.put(key,health.get(key).copy()); + CompoundTag effects=EffectHurtChecks.malformedFields();for(String key:effects.getAllKeys())n.put(key,effects.get(key).copy()); + CompoundTag rotation=RotationFallChecks.malformedFields();for(String key:rotation.getAllKeys())n.put(key,rotation.get(key).copy()); return n; } - private static CompoundNBT missing(int index,int count,int slot){CompoundNBT n=NbtRecoveryChecks.entry(index,count,slot);n.putString("id","absentfixture:missing");return n;} + private static CompoundTag missing(int index,int count,int slot){CompoundTag n=NbtRecoveryChecks.entry(index,count,slot);n.putString("id","absentfixture:missing");return n;} private static final class RawBody extends EntityLootableBody { - RawBody(World w){super(w);} - @Override public boolean writeUnlessPassenger(CompoundNBT n){if(removed)return false;n.putString("id","lootablebodies:corpse");writeWithoutTypeId(n);return true;} - @Override public void writeAdditional(CompoundNBT n){super.writeAdditional(n);CompoundNBT raw=rawEquipment();for(String key:raw.keySet())n.put(key,raw.get(key).copy());n.put("Pos",PositionMotionChecks.vector(Double.NaN,80,getPosZ()));n.put("Motion",PositionMotionChecks.vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY));} + RawBody(Level w){super(w);} + @Override public boolean saveAsPassenger(CompoundTag n){if(isRemoved())return false;n.putString("id","lootablebodies:corpse");saveWithoutId(n);return true;} + @Override public void addAdditionalSaveData(CompoundTag n){super.addAdditionalSaveData(n);CompoundTag raw=rawEquipment();for(String key:raw.getAllKeys())n.put(key,raw.get(key).copy());n.put("Pos",PositionMotionChecks.vector(Double.NaN,80,getZ()));n.put("Motion",PositionMotionChecks.vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY));} } private static void settle(EntityLootableBody b){for(int i=0;i<8;i++)b.baseTick();} - private static void identity(EntityLootableBody b,UUID id,int coordinate){RuntimeChecks.require(b.isAlive() && b.getUniqueID().equals(id) && b.getPosX()==coordinate+8.5 && b.getPosY()==80 && b.getPosZ()==coordinate+.5 && b.rotationYaw==37 && b.getCustomNameTag().equals("NbtFixture"),"Recovered corpse identity/position/rotation/name/health changed");} - private static void add(Map totals,ItemStack s){CompoundNBT n=s.write(new CompoundNBT());n.remove("Count");String key=n.toString();totals.put(key,totals.getOrDefault(key,0)+s.getCount());} + private static void identity(EntityLootableBody b,UUID id,int coordinate){RuntimeChecks.require(b.isAlive() && b.getUUID().equals(id) && b.getX()==coordinate+8.5 && b.getY()==80 && b.getZ()==coordinate+.5 && b.getYRot()==37 && b.getCustomNameTag().equals("NbtFixture"),"Recovered corpse identity/position/rotation/name/health changed");} + private static void add(Map totals,ItemStack s){CompoundTag n=s.save(new CompoundTag());n.remove("Count");String key=n.toString();totals.put(key,totals.getOrDefault(key,0)+s.getCount());} static void verify(EntityLootableBody b,boolean restored){ verifyMetadata(b); Map expected=new TreeMap<>();for(int i=0;i<48;i++)if(i<7 || i>10)add(expected,known(i)); for(int[] spec:new int[][]{{121,11},{122,13},{126,7},{128,2},{130,19},{131,23}})add(expected,NbtRecoveryChecks.item(spec[0],spec[1])); - if(restored){CompoundNBT raw=NbtRecoveryChecks.entry(123,9,7);raw.putString("id",RETURNING);ItemStack s=ItemStack.read(raw);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getItem()==returning,"Returning fixture item not registered");add(expected,s);} + if(restored){CompoundTag raw=NbtRecoveryChecks.entry(123,9,7);raw.putString("id",RETURNING);ItemStack s=ItemStack.of(raw);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getItem()==returning,"Returning fixture item not registered");add(expected,s);} RuntimeChecks.require(expected.equals(NbtRecoveryChecks.items(b)),"Disk recovery item/count/damage/custom NBT lost or duplicated"); - CompoundNBT source=rawEquipment();int rawCount=0; - for(String list:new String[]{"Equipment","Aux"}){ListNBT entries=source.getList(list,10);for(int i=0;i=0 && saved.getLong("DeathTime")<=b.world.getGameTime() && saved.getLong("DecayTime")>=0 && saved.getLong("DecayTime")<=b.world.getGameTime(),"Malformed disk timers entered active state"); + RuntimeChecks.require(!saved.contains("Vac") && saved.getLong("DeathTime")>=0 && saved.getLong("DeathTime")<=b.level.getGameTime() && saved.getLong("DecayTime")>=0 && saved.getLong("DecayTime")<=b.level.getGameTime(),"Malformed disk timers entered active state"); for(String key:new String[]{"Name","Vac","DeathTime","DecayTime"})NameLifetimeChecks.retained(b,key,raw.get(key)); RuntimeChecks.require(saved.getList(NameLifetimeChecks.RECOVERY,10).size()==16,"Disk metadata evidence grew or disappeared"); } private static void snapshot(File folder,String name,EntityLootableBody b) throws Exception {RuntimeChecks.saveExpected(folder,"nbt-"+name,b);} private static void verifySnapshot(File folder,String name,EntityLootableBody b) throws Exception { - b.baseTick();RuntimeChecks.verifyExpected(folder,"nbt-"+name,b);CompoundNBT expected;try(FileInputStream in=new FileInputStream(new File(folder,"nbt-"+name+".nbt"))){expected=CompressedStreamTools.readCompressed(in);} + b.baseTick();RuntimeChecks.verifyExpected(folder,"nbt-"+name,b);CompoundTag expected;try(FileInputStream in=new FileInputStream(new File(folder,"nbt-"+name+".nbt"))){expected=NbtIo.readCompressed(in);} RuntimeChecks.require(Objects.equals(expected.get(NbtRecoveryChecks.RECOVERY),NbtRecoveryChecks.saved(b).get(NbtRecoveryChecks.RECOVERY)),"Raw recovery NBT changed on actual disk reload"); RuntimeChecks.require(NameLifetimeChecks.same(expected.get(NameLifetimeChecks.RECOVERY),NbtRecoveryChecks.saved(b).get(NameLifetimeChecks.RECOVERY)),"Raw scalar data changed on actual disk reload"); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java index 74edbcd..d4c5e14 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java @@ -1,22 +1,24 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.Slot; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.inventory.Slot; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.CompoundTag; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.events.PlayerDeathEventHandler; import java.util.*; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.item.Items; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.DamageSource; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.GameType; +import net.minecraft.world.level.GameType; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayerFactory; import com.mojang.authlib.GameProfile; @@ -35,26 +37,26 @@ private static void check(String name, Check action) { private static void require(boolean ok, String message) { if (!ok) throw new AssertionError(message); } public static void run(MinecraftServer server) throws Exception { failures=0; - ServerWorld world=server.getWorld(FixtureApi.dimensionKey(0)); + ServerLevel world=server.getLevel(FixtureApi.dimensionKey(0)); check("native_received_packet_observer_is_non_destructive", () -> { - net.minecraft.network.PacketBuffer buffer=new net.minecraft.network.PacketBuffer(io.netty.buffer.Unpooled.buffer()); + net.minecraft.network.FriendlyByteBuf buffer=new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.buffer()); try { EntityLootableBody corpse=new EntityLootableBody(world); - net.minecraft.network.play.server.SSpawnMobPacket sent=new net.minecraft.network.play.server.SSpawnMobPacket(corpse); - sent.writePacketData(buffer); - net.minecraft.network.play.server.SSpawnMobPacket received=new net.minecraft.network.play.server.SSpawnMobPacket();received.readPacketData(buffer); + net.minecraft.network.protocol.game.ClientboundAddMobPacket sent=new net.minecraft.network.protocol.game.ClientboundAddMobPacket(corpse); + sent.write(buffer); + net.minecraft.network.protocol.game.ClientboundAddMobPacket received=new net.minecraft.network.protocol.game.ClientboundAddMobPacket(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.play.server.SRespawnPacket outgoing=new net.minecraft.network.play.server.SRespawnPacket(world.getDimensionType(),world.getDimensionKey(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,true); - outgoing.writePacketData(buffer); - net.minecraft.network.play.server.SRespawnPacket incoming=new net.minecraft.network.play.server.SRespawnPacket();incoming.readPacketData(buffer); + net.minecraft.network.protocol.game.ClientboundRespawnPacket outgoing=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(world.dimensionType(),world.dimension(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,true); + 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"); require(RespawnPacketTrace.describe(incoming).equals("RESPAWN dimension=minecraft:overworld hashedSeed="+0x13572468ABCDEF01L),"Native 1.16 hashed seed missing from the packet observation"); } finally {buffer.release();} }); check("rejected_spawn_keeps_death_inventory", () -> { - ServerPlayerEntity player=FakePlayerFactory.get(world,new GameProfile(UUID.randomUUID(),"PortFixture")); + 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) { @@ -67,36 +69,36 @@ public static void run(MinecraftServer server) throws Exception { }); check("duplicate_slots_preserve_both_records", () -> { EntityLootableBody body=new EntityLootableBody(world); - CompoundNBT nbt=new CompoundNBT(); body.writeAdditional(nbt); - ListNBT equipment=new ListNBT(); - for(int i=0;i<2;i++) { CompoundNBT item=new ItemStack(Items.DIAMOND,i+2).write(new CompoundNBT()); item.putByte("Slot",(byte)10); equipment.add(item); } - nbt.put("Equipment",equipment); body.readAdditional(nbt); - CompoundNBT saved=new CompoundNBT();body.writeAdditional(saved); + CompoundTag nbt=new CompoundTag(); body.addAdditionalSaveData(nbt); + ListTag equipment=new ListTag(); + for(int i=0;i<2;i++) { CompoundTag item=new ItemStack(Items.DIAMOND,i+2).save(new CompoundTag()); item.putByte("Slot",(byte)10); equipment.add(item); } + nbt.put("Equipment",equipment); body.readAdditionalSaveData(nbt); + CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); int count=0; for(String key:Arrays.asList("Equipment","Aux")) { - ListNBT list=saved.getList(key,10); + ListTag list=saved.getList(key,10); for(int i=0;i { - EntityLootableBody body=new EntityLootableBody(world); CompoundNBT nbt=new CompoundNBT();body.writeAdditional(nbt); - CompoundNBT item=new ItemStack(Items.DIAMOND,7).write(new CompoundNBT());item.putString("id","missingfixture:keepsake");item.putByte("Slot",(byte)10); - ListNBT list=new ListNBT();list.add(item);nbt.put("Equipment",list);body.readAdditional(nbt); - CompoundNBT saved=new CompoundNBT();body.writeAdditional(saved); + EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); + CompoundTag item=new ItemStack(Items.DIAMOND,7).save(new CompoundTag());item.putString("id","missingfixture:keepsake");item.putByte("Slot",(byte)10); + ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);body.readAdditionalSaveData(nbt); + CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); require(saved.getList("LootableBodiesUnresolved",10).size()==1,"missing item was erased"); require(!body.isEmpty(),"recoverable item allowed empty-body decay"); }); check("invalid_slot_retains_inventory", () -> { - EntityLootableBody body=new EntityLootableBody(world); CompoundNBT nbt=new CompoundNBT();body.writeAdditional(nbt); - CompoundNBT item=new ItemStack(Items.EMERALD,9).write(new CompoundNBT());item.putInt("Slot",12345); - ListNBT list=new ListNBT();list.add(item);nbt.put("Equipment",list);body.readAdditional(nbt); - CompoundNBT saved=new CompoundNBT();body.writeAdditional(saved); + EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); + CompoundTag item=new ItemStack(Items.EMERALD,9).save(new CompoundTag());item.putInt("Slot",12345); + ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);body.readAdditionalSaveData(nbt); + CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); require(!body.isEmpty(),"invalid slot discarded inventory"); }); check("malformed_name_and_timer_are_retained", () -> { - EntityLootableBody body=new EntityLootableBody(world); CompoundNBT nbt=new CompoundNBT();body.writeAdditional(nbt); - nbt.put("Name",IntNBT.valueOf(17));nbt.putDouble("DeathTime",Double.NaN);body.readAdditional(nbt); - CompoundNBT saved=new CompoundNBT();body.writeAdditional(saved); + EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); + nbt.put("Name",IntTag.valueOf(17));nbt.putDouble("DeathTime",Double.NaN);body.readAdditionalSaveData(nbt); + CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); require(saved.getList("LootableBodiesInvalidFields",10).size()==2,"malformed fields lost without recovery"); }); System.out.println("PORT_BASELINE_RESULT failures="+failures); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java index 95e38df..60cf2aa 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java @@ -1,42 +1,53 @@ package cyano.lootable.buildtest; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.StringNBT; -import net.minecraft.nbt.ShortNBT; -import net.minecraft.nbt.LongNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.IntArrayNBT; -import net.minecraft.nbt.FloatNBT; -import net.minecraft.nbt.DoubleNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ByteNBT; -import net.minecraft.nbt.ByteArrayNBT; -import net.minecraft.dispenser.Position; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.core.PositionImpl; import cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; -import net.minecraft.entity.*; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.ShortTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.storage.ChunkLoader; +import net.minecraft.world.phys.AABB; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.storage.ChunkStorage; /** Validate before vanilla builds geometry, then exercise actual chunk admission. */ final class PositionMotionChecks { private interface Check {void run() throws Exception;} - private static ServerWorld world; + private static ServerLevel world; private static int failures; private static final List results=new ArrayList<>(); - static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { + static void run(MinecraftServer server,ServerLevel target,File folder) throws Exception { world=target;failures=0;results.clear(); - INBT[] wrong={StringNBT.valueOf("opaque"),new CompoundNBT(),new ByteArrayNBT(new byte[]{1,2,3}),new IntArrayNBT(new int[]{1,2,3}),DoubleNBT.valueOf(Double.NaN)}; + Tag[] wrong={StringTag.valueOf("opaque"),new CompoundTag(),new ByteArrayTag(new byte[]{1,2,3}),new IntArrayTag(new int[]{1,2,3}),DoubleTag.valueOf(Double.NaN)}; for(String key:new String[]{"Pos","Motion"}) { - for(int i=0;ibadVector(key,raw));} - for(INBT raw:new INBT[]{StringNBT.valueOf("opaque"),new CompoundNBT(),new ByteArrayNBT(new byte[]{1})})check(key+"_wrong_element_type_"+raw.getId(),()->{ListNBT list=new ListNBT();for(int i=0;i<3;i++)list.add(raw.copy());badVector(key,list);}); + for(int i=0;ibadVector(key,raw));} + for(Tag raw:new Tag[]{StringTag.valueOf("opaque"),new CompoundTag(),new ByteArrayTag(new byte[]{1})})check(key+"_wrong_element_type_"+raw.getId(),()->{ListTag list=new ListTag();for(int i=0;i<3;i++)list.add(raw.copy());badVector(key,list);}); for(int count:new int[]{0,1,2,4})check(key+"_wrong_length_"+count,()->length(key,count)); for(int axis=0;axis<3;axis++)for(double v:new double[]{Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY}){final int a=axis;check(key+"_nonfinite_axis_"+axis+"_"+v,()->component(key,a,v));} for(int type=0;type<6;type++){final int t=type;check(key+"_valid_numeric_type_"+type,()->numeric(key,t));} @@ -44,6 +55,7 @@ static void run(MinecraftServer server,ServerWorld target,File folder) throws Ex } for(int axis=0;axis<3;axis++)for(double v:new double[]{-10.01,10.01,Double.MAX_VALUE,-Double.MAX_VALUE}){final int a=axis;check("Motion_outside_engine_limit_axis_"+axis+"_"+v,()->component("Motion",a,v));} for(int axis=0;axis<3;axis++)for(double v:new double[]{-30000000.01,30000000.01,Double.MAX_VALUE,-Double.MAX_VALUE}){final int a=axis;check("Pos_outside_engine_limit_axis_"+axis+"_"+v,()->component("Pos",a,v));} + for(double y:new double[]{-20000000.01,20000000.01})check("Pos_outside_native_vertical_limit_"+y,()->component("Pos",1,y)); check("valid_negative_coordinates_and_high_altitude",PositionMotionChecks::signed); check("valid_boundary_values_preserved",PositionMotionChecks::boundaries); check("valid_void_position_keeps_saved_value",PositionMotionChecks::voidPosition); @@ -63,33 +75,33 @@ static void run(MinecraftServer server,ServerWorld target,File folder) throws Ex } private static void check(String name,Check test){try{test.run();results.add("PASS "+name);System.out.println("POSITION_MOTION_PASS "+name);}catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("POSITION_MOTION_FAIL "+name+": "+e);e.printStackTrace();}} private static void require(boolean c,String why){RuntimeChecks.require(c,why);} - static ListNBT vector(double... values){ListNBT l=new ListNBT();for(double v:values)l.add(DoubleNBT.valueOf(v));return l;} - private static CompoundNBT saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} - private static CompoundNBT base(){EntityLootableBody b=new EntityLootableBody(world);b.setPosition(160.5,80,160.5);b.setNoGravity(true);b.setUserName("PositionFixture");for(int i=0;i<48;i++)b.setInventorySlotContents(i,RuntimeChecks.stack(i));b.addItem(RuntimeChecks.stack(70));b.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(b);b.setMotion(.25,b.getMotion().y,b.getMotion().z);b.setMotion(b.getMotion().x,-.5,b.getMotion().z);b.setMotion(b.getMotion().x,b.getMotion().y,.75);return saved(b);} - private static EntityLootableBody read(CompoundNBT n){CompoundNBT before=n.copy();Entity e=FixtureApi.loadEntity(n,world);require(e instanceof EntityLootableBody,"Factory discarded corpse");require(NameLifetimeChecks.same(before,n),"Input NBT mutated");return (EntityLootableBody)e;} - static void finite(EntityLootableBody b){for(double v:new double[]{b.getPosX(),b.getPosY(),b.getPosZ(),b.getMotion().x,b.getMotion().y,b.getMotion().z,b.prevPosX,b.prevPosY,b.prevPosZ,b.lastTickPosX,b.lastTickPosY,b.lastTickPosZ})require(Double.isFinite(v),"Nonfinite geometry/derived position");AxisAlignedBB a=b.getBoundingBox();for(double v:new double[]{a.minX,a.minY,a.minZ,a.maxX,a.maxY,a.maxZ})require(Double.isFinite(v),"Nonfinite bounding box");require(Math.abs(a.minX-(b.getPosX()-b.getWidth()/2))<1e-6 && a.minY==b.getPosY() && Math.abs(a.maxZ-(b.getPosZ()+b.getWidth()/2))<1e-6,"Bounding box differs from sanitized position");} - private static double[] values(EntityLootableBody b,String key){return key.equals("Pos")?new double[]{b.getPosX(),b.getPosY(),b.getPosZ()}:new double[]{b.getMotion().x,b.getMotion().y,b.getMotion().z};} + static ListTag vector(double... values){ListTag l=new ListTag();for(double v:values)l.add(DoubleTag.valueOf(v));return l;} + private static CompoundTag saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} + private static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setUserName("PositionFixture");for(int i=0;i<48;i++)b.setItem(i,RuntimeChecks.stack(i));b.addItem(RuntimeChecks.stack(70));b.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(b);b.setDeltaMovement(.25,b.getDeltaMovement().y,b.getDeltaMovement().z);b.setDeltaMovement(b.getDeltaMovement().x,-.5,b.getDeltaMovement().z);b.setDeltaMovement(b.getDeltaMovement().x,b.getDeltaMovement().y,.75);return saved(b);} + private static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();Entity e=FixtureApi.loadEntity(n,world);require(e instanceof EntityLootableBody,"Factory discarded corpse");require(NameLifetimeChecks.same(before,n),"Input NBT mutated");return (EntityLootableBody)e;} + static void finite(EntityLootableBody b){for(double v:new double[]{b.getX(),b.getY(),b.getZ(),b.getDeltaMovement().x,b.getDeltaMovement().y,b.getDeltaMovement().z,b.xo,b.yo,b.zo,b.xOld,b.yOld,b.zOld})require(Double.isFinite(v),"Nonfinite geometry/derived position");AABB a=b.getBoundingBox();for(double v:new double[]{a.minX,a.minY,a.minZ,a.maxX,a.maxY,a.maxZ})require(Double.isFinite(v),"Nonfinite bounding box");require(Math.abs(a.minX-(b.getX()-b.getBbWidth()/2))<1e-6 && a.minY==b.getY() && Math.abs(a.maxZ-(b.getZ()+b.getBbWidth()/2))<1e-6,"Bounding box differs from sanitized position");} + private static double[] values(EntityLootableBody b,String key){return key.equals("Pos")?new double[]{b.getX(),b.getY(),b.getZ()}:new double[]{b.getDeltaMovement().x,b.getDeltaMovement().y,b.getDeltaMovement().z};} private static double fallback(String key,int axis){return key.equals("Pos") && axis==1?64:0;} private static void sameValue(double a,double z,String why){require(Double.isFinite(a) && a==z,why+" expected="+z+" actual="+a);} - private static void kept(EntityLootableBody b){CompoundNBT n=base(),a=saved(b);for(String k:new String[]{"Equipment","Aux"})require(NameLifetimeChecks.same(n.get(k),a.get(k)),"Complete inventory changed "+k);require(b.getSizeInventory()==48 && b.getCustomNameTag().equals("PositionFixture"),"Identity/default changed");} - private static void stable(EntityLootableBody b){finite(b);kept(b);CompoundNBT a=saved(b),z=saved(read(a));for(String k:new String[]{"Pos","Motion","Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);} - private static void badVector(String key,INBT raw){CompoundNBT n=base();n.put(key,raw.copy());EntityLootableBody b=read(n);for(int i=0;i<3;i++)sameValue(values(b,key)[i],fallback(key,i),"Bad vector component");NameLifetimeChecks.retained(b,key,raw);stable(b);} - private static void component(String key,int axis,double value){double[] v=key.equals("Pos")?new double[]{160.5,80,160.5}:new double[]{.25,-.5,.75};v[axis]=value;CompoundNBT n=base();ListNBT raw=vector(v);n.put(key,raw);EntityLootableBody b=read(n);for(int i=0;i<3;i++)sameValue(values(b,key)[i],i==axis?fallback(key,i):v[i],"Lost valid sibling or activated invalid component");NameLifetimeChecks.retained(b,key,raw);stable(b);} - private static void length(String key,int count){double[] v=new double[count];for(int i=0;i>4,coordinate>>4);BlockPos center=new BlockPos(coordinate,0,coordinate); - for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)world.removeBlock(center.add(x,0,z),false); - world.setBlockState(center,Blocks.STONE.getDefaultState()); + for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)world.removeBlock(center.offset(x,0,z),false); + world.setBlockAndUpdate(center,Blocks.STONE.defaultBlockState()); EntityLootableBody body=body(world,coordinate,-1); LootableBodies.voidPlatform=false;body.baseTick(); - RuntimeChecks.require(body.getPosY()==-1 && world.isAirBlock(center.add(1,0,0)),"Disabled void platform changed behaviour"); + RuntimeChecks.require(body.getY()==-1 && world.isEmptyBlock(center.offset(1,0,0)),"Disabled void platform changed behaviour"); LootableBodies.voidPlatform=true;LootableBodies.dimensionSpecificPlatform=dimensionBlocks;body.baseTick(); Block expected=!dimensionBlocks || dimension==0?Blocks.COBBLESTONE:dimension==-1?Blocks.NETHERRACK:Blocks.END_STONE; - RuntimeChecks.require(body.getPosY()==1.5 && body.isAlive(),"Enabled void platform did not rescue corpse"); + RuntimeChecks.require(body.getY()==1.5 && body.isAlive(),"Enabled void platform did not rescue corpse"); for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++) - RuntimeChecks.require(world.getBlockState(center.add(x,0,z)).getBlock()==(x==0 && z==0?Blocks.STONE:expected),"Void platform changed existing terrain or dimension block"); + RuntimeChecks.require(world.getBlockState(center.offset(x,0,z)).getBlock()==(x==0 && z==0?Blocks.STONE:expected),"Void platform changed existing terrain or dimension block"); conserved(body);checks++; } - ServerWorld world=server.getWorld(FixtureApi.dimensionKey(0));world.getChunk(30,30); - for(int x=487;x<=489;x++)for(int z=487;z<=489;z++)for(int y=79;y<=82;y++)world.setBlockState(new BlockPos(x,y,z),Blocks.WATER.getDefaultState()); + ServerLevel world=server.getLevel(FixtureApi.dimensionKey(0));world.getChunk(30,30); + for(int x=487;x<=489;x++)for(int z=487;z<=489;z++)for(int y=79;y<=82;y++)world.setBlockAndUpdate(new BlockPos(x,y,z),Blocks.WATER.defaultBlockState()); EntityLootableBody body=body(world,488,80); RuntimeChecks.require(body.handleWaterMovement(),"Water fixture was not submerged"); - LootableBodies.corpseBuoyancy=false;body.setMotion(body.getMotion().x,0,body.getMotion().z);body.baseTick(); - RuntimeChecks.require(body.getMotion().y==0,"Disabled buoyancy accelerated the corpse"); - LootableBodies.corpseBuoyancy=true;body.setMotion(body.getMotion().x,0,body.getMotion().z);body.baseTick(); - RuntimeChecks.require(body.getMotion().y>0,"Enabled buoyancy did not lift the corpse");conserved(body);checks++; + LootableBodies.corpseBuoyancy=false;body.setDeltaMovement(body.getDeltaMovement().x,0,body.getDeltaMovement().z);body.baseTick(); + RuntimeChecks.require(body.getDeltaMovement().y==0,"Disabled buoyancy accelerated the corpse"); + LootableBodies.corpseBuoyancy=true;body.setDeltaMovement(body.getDeltaMovement().x,0,body.getDeltaMovement().z);body.baseTick(); + RuntimeChecks.require(body.getDeltaMovement().y>0,"Enabled buoyancy did not lift the corpse");conserved(body);checks++; System.out.println("LOOTABLE_BODIES_112_RECOVERY_PASS checks="+checks); } finally {LootableBodies.corpseBuoyancy=buoyancy;LootableBodies.voidPlatform=platform;LootableBodies.dimensionSpecificPlatform=specific;} } - private static EntityLootableBody body(ServerWorld world,int coordinate,double y) { - EntityLootableBody body=new EntityLootableBody(world);body.setPosition(coordinate+.5,y,coordinate+.5);body.setNoGravity(true); - body.setInventorySlotContents(6,RuntimeChecks.stack(420));RuntimeChecks.disableVacuum(body);return body; + private static EntityLootableBody body(ServerLevel world,int coordinate,double y) { + EntityLootableBody body=new EntityLootableBody(world);body.setPos(coordinate+.5,y,coordinate+.5);body.setNoGravity(true); + body.setItem(6,RuntimeChecks.stack(420));RuntimeChecks.disableVacuum(body);return body; } private static void conserved(EntityLootableBody body) { - RuntimeChecks.require(RuntimeChecks.sameStack(body.getStackInSlot(6),RuntimeChecks.stack(420)),"Recovery option changed complete item NBT"); - CompoundNBT saved=new CompoundNBT();body.writeUnlessPassenger(saved);EntityLootableBody reloaded=new EntityLootableBody(body.world);reloaded.read(saved); - RuntimeChecks.require(RuntimeChecks.sameStack(reloaded.getStackInSlot(6),RuntimeChecks.stack(420)),"Recovery option inventory changed on reload"); + RuntimeChecks.require(RuntimeChecks.sameStack(body.getItem(6),RuntimeChecks.stack(420)),"Recovery option changed complete item NBT"); + CompoundTag saved=new CompoundTag();body.saveAsPassenger(saved);EntityLootableBody reloaded=new EntityLootableBody(body.level);reloaded.load(saved); + RuntimeChecks.require(RuntimeChecks.sameStack(reloaded.getItem(6),RuntimeChecks.stack(420)),"Recovery option inventory changed on reload"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java index e4730aa..8281d12 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java @@ -1,36 +1,46 @@ package cyano.lootable.buildtest; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.world.server.ServerChunkProvider; -import net.minecraft.network.play.client.CHeldItemChangePacket; -import net.minecraft.network.play.client.CCloseWindowPacket; -import net.minecraft.network.play.client.CClickWindowPacket; -import net.minecraft.network.play.client.CClientStatusPacket; -import net.minecraft.network.play.server.SPlayerListItemPacket; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; +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.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import com.mojang.authlib.GameProfile; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseContainer; import java.io.*; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.item.Items; -import net.minecraft.inventory.container.ClickType; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; -import net.minecraft.network.*; -import net.minecraft.network.play.client.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.item.Items; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClickPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket; +import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; import io.netty.buffer.Unpooled; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.management.PlayerInteractionManager; -import net.minecraft.util.math.*; -import net.minecraft.world.*; -import net.minecraft.world.chunk.Chunk; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.chunk.LevelChunk; /** Public issue reproductions in real Forge; excluded from production artifacts. */ @@ -38,11 +48,11 @@ final class ReportedIssueChecks { private interface Check { void run() throws Exception; } private static final List results = new ArrayList<>(); private static MinecraftServer server; - private static ServerWorld world; - private static int failures, sequence, transaction; + private static ServerLevel world; + private static int failures, sequence; private static File folder; - static void run(MinecraftServer instance, ServerWorld target, File directory) throws Exception { + static void run(MinecraftServer instance, ServerLevel target, File directory) throws Exception { server=instance; world=target; folder=directory; results.clear(); failures=0; check("issue5_exact_two_deaths_hotbar_one_then_switch_without_inventory_sync", ReportedIssueChecks::deathSequence); check("issue17_loaded_platform_keeps_gravity_and_inventory", ReportedIssueChecks::loadedPlatform); @@ -63,127 +73,136 @@ static void run(MinecraftServer instance, ServerWorld target, File directory) th private static void check(String name, Check action) { Set before=new HashSet<>(FixtureApi.entities(world)); try {action.run(); results.add("PASS "+name); System.out.println("REPORTED_ISSUE_PASS "+name);} - catch(Throwable error) {failures++;results.add("FAIL "+name+": "+error);System.out.println("REPORTED_ISSUE_FAIL "+name+": "+error);error.printStackTrace();} + catch(Throwable error) {failures++;results.add("FAIL "+name+": "+error);System.out.println("REPORTED_ISSUE_FAIL "+name+": "+error);error.printStackTrace(); + if(Boolean.getBoolean("lootablebodies.diagnosticFailFast"))throw new IllegalStateException("Fail-fast diagnostic issue check: "+name,error);} finally { for(Entity entity:new ArrayList(FixtureApi.entities(world))) if(!before.contains(entity)) { - if(entity instanceof ServerPlayerEntity) server.getPlayerList().playerLoggedOut((ServerPlayerEntity)entity); + if(entity instanceof ServerPlayer) server.getPlayerList().remove((ServerPlayer)entity); else world.removeEntity(entity); } } } private static ItemStack shovel() { ItemStack stack=new ItemStack(Items.DIAMOND_SHOVEL); - stack.setTag(RuntimeChecks.stack(300).getTag().copy());stack.setDamage(17); - stack.setDisplayName(new net.minecraft.util.text.StringTextComponent("Issue 5 original shovel")); return stack; + stack.setTag(RuntimeChecks.stack(300).getTag().copy());stack.setDamageValue(17); + stack.setHoverName(new net.minecraft.network.chat.TextComponent("Issue 5 original shovel")); return stack; } static Map inventory(EntityLootableBody body) { Map total=new TreeMap<>(); - for(int i=0;i<48;i++) add(total,body.getStackInSlot(i)); - CompoundNBT n=new CompoundNBT();body.writeAdditional(n); - ListNBT aux=n.getList("Aux",10); - for(int i=0;i total,ItemStack stack) { if((stack == null || stack.isEmpty()) || stack.getCount()<=0)return; - CompoundNBT n=stack.write(new CompoundNBT());n.remove("Count"); + CompoundTag n=stack.save(new CompoundTag());n.remove("Count"); total.put(n.toString(),total.getOrDefault(n.toString(),0)+stack.getCount()); } private static Map tagged(EntityLootableBody body) { Map n=inventory(body); n.keySet().removeIf(k->!k.contains("fixture"));return n; } - private static Map tagged(ServerPlayerEntity player) { + private static Map tagged(ServerPlayer player) { Map n=new TreeMap<>(); - for(int i=0;i before=new HashSet<>(FixtureApi.entities(world));player.onKillCommand(); + private static EntityLootableBody death(ServerPlayer player) { + Set before=new HashSet<>(FixtureApi.entities(world));player.kill(); for(Entity e:FixtureApi.entities(world))if(e instanceof EntityLootableBody && !before.contains(e))return (EntityLootableBody)e; throw new AssertionError("Real player death did not create corpse"); } - private static ServerPlayerEntity respawn(ServerPlayerEntity player) { - ServerPlayNetHandler connection=player.connection; - connection.processClientStatus(new CClientStatusPacket(CClientStatusPacket.State.PERFORM_RESPAWN)); - ServerPlayerEntity replacement=connection.player; + private static ServerPlayer respawn(ServerPlayer player) { + ServerGamePacketListenerImpl connection=player.connection; + connection.handleClientCommand(new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN)); + ServerPlayer replacement=connection.player; RuntimeChecks.require(replacement!=player && replacement.isAlive(),"Actual player recreation did not occur"); - replacement.setPosition(32.5,80,0.5);return replacement; + replacement.setPos(32.5,80,0.5);return replacement; } - private static void click(ServerPlayerEntity p,int slot,int button,ClickType type) throws IOException { - // 1.12 QUICK_MOVE returns EMPTY after completely draining its source slot. - ItemStack predicted=ItemStack.EMPTY; - PacketBuffer wire=new PacketBuffer(Unpooled.buffer()); + private static void click(ServerPlayer p,int slot,int button,ClickType type) throws IOException { + QuietConnection observed=(QuietConnection)p.connection;int before=observed.menuEvents; + // Server-side conservation fixture: omit predicted slots and require + // native authoritative corrections. Real clients test normal prediction. + ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket(p.containerMenu.containerId, + p.containerMenu.getStateId(),slot,button,type,p.containerMenu.getCarried().copy(), + new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap()); + FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); try { - wire.writeByte(p.openContainer.windowId);wire.writeShort(slot);wire.writeByte(button);wire.writeShort(++transaction); - wire.writeEnumValue(type);wire.writeItemStack(predicted == null ? ItemStack.EMPTY : predicted); - CClickWindowPacket packet=new CClickWindowPacket();packet.readPacketData(wire);p.connection.processClickWindow(packet); - RuntimeChecks.require(p.openContainer.getCanCraft(p),"Packet prediction rejected the click transaction"); + outgoing.write(wire);p.connection.handleContainerClick(new ServerboundContainerClickPacket(wire)); + RuntimeChecks.require(!wire.isReadable(),"Native issue click left undecoded fields"); + RuntimeChecks.require(observed.menuEvents>before && observed.menuState==p.containerMenu.getStateId() + && RuntimeChecks.sameStack(observed.carried,p.containerMenu.getCarried()),"Native menu correction did not match its authoritative state/cursor"); } finally {wire.release();} } private static void deathSequence() throws Exception { - world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(false,null);RuntimeChecks.awaitChunk(world,2,0); - ServerPlayerEntity p=new ServerPlayerEntity(server,world,new GameProfile(UUID.randomUUID(),"IssueFive"),new PlayerInteractionManager(world)); - p.connection=new QuietConnection(p);p.setGameType(GameType.SURVIVAL);p.setPosition(32.5,80,0.5); + 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().sendPacketToAllPlayers(new net.minecraft.network.play.server.SPlayerListItemPacket( - net.minecraft.network.play.server.SPlayerListItemPacket.Action.ADD_PLAYER,p)); - RuntimeChecks.require(world.addEntity(p),"Fixture player spawn");server.getPlayerList().addPlayer(p); - p.inventory.currentItem=4;p.inventory.mainInventory.set(4,shovel());p.inventory.mainInventory.set(9,RuntimeChecks.stack(301)); + 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); + 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"); - p=respawn(p);CorpseContainer gui=new CorpseContainer(17,p.inventory,first);p.openContainer=gui;gui.addListener(p); + p=respawn(p);CorpseContainer gui=new CorpseContainer(17,p.getInventory(),first);p.containerMenu=gui;p.initMenu(gui); click(p,4,0,ClickType.SWAP); - RuntimeChecks.require(RuntimeChecks.sameStack(p.inventory.mainInventory.get(0),shovel()),"Key1 did not recover original main hand"); - for(int slot=0;slot<48;slot++)if(gui.getSlot(slot).getHasStack())click(p,slot,0,ClickType.QUICK_MOVE); - RuntimeChecks.require(first.isEmpty() && expected.equals(tagged(p)),"First recovery lost or duplicated tagged inventory: remaining="+inventory(first)+" expected="+expected+" player="+tagged(p)+" canCraft="+gui.getCanCraft(p)); - PacketBuffer close=new PacketBuffer(Unpooled.buffer()); - try {close.writeByte(gui.windowId);CCloseWindowPacket packet=new CCloseWindowPacket();packet.readPacketData(close);p.connection.processCloseWindow(packet);} + RuntimeChecks.require(RuntimeChecks.sameStack(p.getInventory().items.get(0),shovel()),"Key1 did not recover original main hand"); + for(int slot=0;slot<48;slot++)if(gui.getSlot(slot).hasItem())click(p,slot,0,ClickType.QUICK_MOVE); + RuntimeChecks.require(first.isEmpty() && expected.equals(tagged(p)),"First recovery lost or duplicated tagged inventory: remaining="+inventory(first)+" expected="+expected+" player="+tagged(p)+" state="+gui.getStateId()); + FriendlyByteBuf close=new FriendlyByteBuf(Unpooled.buffer()); + try {new ServerboundContainerClosePacket(gui.containerId).write(close);p.connection.handleContainerClose(new ServerboundContainerClosePacket(close));RuntimeChecks.require(!close.isReadable(),"Native issue close left undecoded fields");} finally {close.release();} - int active=-1;for(int slot=1;slot<9;slot++)if(!(p.inventory.mainInventory.get(slot)).isEmpty() && p.inventory.mainInventory.get(slot).hasTag() && p.inventory.mainInventory.get(slot).getTag().contains("fixture"))active=slot; + int active=-1;for(int slot=1;slot<9;slot++)if(!(p.getInventory().items.get(slot)).isEmpty() && p.getInventory().items.get(slot).hasTag() && p.getInventory().items.get(slot).getTag().contains("fixture"))active=slot; RuntimeChecks.require(active>0,"Shift-click did not leave a different occupied hotbar slot"); - PacketBuffer held=new PacketBuffer(Unpooled.buffer()); - try {held.writeShort(active);CHeldItemChangePacket packet=new CHeldItemChangePacket();packet.readPacketData(held);p.connection.processHeldItemChange(packet);} + FriendlyByteBuf held=new FriendlyByteBuf(Unpooled.buffer()); + try {new ServerboundSetCarriedItemPacket(active).write(held);p.connection.handleSetCarriedItem(new ServerboundSetCarriedItemPacket(held));RuntimeChecks.require(!held.isReadable(),"Native held-item packet left undecoded fields");} finally {held.release();} // Deliberately do not tick/sync container or reopen inventory here. EntityLootableBody second=death(p); RuntimeChecks.require(expected.equals(tagged(second)),"Second death lost original key1 shovel or another exact stack"); - RuntimeChecks.require(RuntimeChecks.sameStack(second.getStackInSlot(4),RuntimeChecks.stack(301)),"Second death active hand differs"); + RuntimeChecks.require(RuntimeChecks.sameStack(second.getItem(4),RuntimeChecks.stack(301)),"Second death active hand differs"); System.out.println("ISSUE5_REAL_RESPAWN_AND_HELD_ITEM_PACKET_PASS"); - p=respawn(p);server.getPlayerList().playerLoggedOut(p); - CompoundNBT n=new CompoundNBT();second.writeUnlessPassenger(n); - EntityLootableBody loaded=new EntityLootableBody(world);loaded.read(n); + p=respawn(p);server.getPlayerList().remove(p); + CompoundTag n=new CompoundTag();second.saveAsPassenger(n); + EntityLootableBody loaded=new EntityLootableBody(world);loaded.load(n); RuntimeChecks.require(expected.equals(tagged(loaded)),"Second corpse NBT reload lost recovery"); } - private static EntityLootableBody fullBody(int cx,int cz,double offset) { + private static EntityLootableBody fullBody(int cx,int cz,double offset) {return fullBody(cx,cz,offset,null);} + private static EntityLootableBody fullBody(int cx,int cz,double offset,UUID savedId) { RuntimeChecks.awaitChunk(world,cx,cz); EntityLootableBody body=new EntityLootableBody(world);body.setUserName("OfflineIssue17"); - body.setPosition(cx*16+offset,80,cz*16+8.5); - for(int i=0;i<48;i++)body.setInventorySlotContents(i,RuntimeChecks.stack(i)); + if(savedId!=null)body.setUUID(savedId); + body.setPos(cx*16+offset,80,cz*16+8.5); + for(int i=0;i<48;i++)body.setItem(i,RuntimeChecks.stack(i)); body.addItem(RuntimeChecks.stack(70));body.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(body); - RuntimeChecks.require(!body.hasNoGravity(),"Issue17 fixture must use real gravity"); - RuntimeChecks.require(world.addEntity(body),"Physics fixture spawn");return body; + RuntimeChecks.require(!body.isNoGravity(),"Issue17 fixture must use real gravity"); + RuntimeChecks.require(world.addFreshEntity(body),"Physics fixture spawn");return body; } private static void loadedPlatform() { int c=96;for(int x=c-2;x<=c+2;x++)for(int z=c-2;z<=c+2;z++)RuntimeChecks.awaitChunk(world,x,z); - world.setBlockState(new BlockPos(c*16+8,79,c*16+8),Blocks.STONE.getDefaultState()); + world.setBlockAndUpdate(new BlockPos(c*16+8,79,c*16+8),Blocks.STONE.defaultBlockState()); EntityLootableBody b=fullBody(c,c,8.5);Map expected=inventory(b); FixtureApi.awaitTicking(b); - for(int i=0;i<40;i++)world.updateEntity(b); - RuntimeChecks.require(b.isOnGround() && b.getPosY()==80 && b.isAlive() && expected.equals(inventory(b)),"Loaded platform/normal gravity lost body or inventory"); + for(int i=0;i<40;i++)world.tickNonPassenger(b); + RuntimeChecks.require(b.isOnGround() && b.getY()==80 && b.isAlive() && expected.equals(inventory(b)),"Loaded platform/normal gravity lost body or inventory"); } private static void normalTickGate() { - EntityLootableBody b=fullBody(112,112,8.5);int ticks=b.ticksExisted; - for(int i=0;i<40;i++)world.updateEntity(b); - RuntimeChecks.require(b.ticksExisted==ticks && b.getPosY()==80 && !world.getChunkProvider().chunkExists(113,112),"Vanilla unloaded-neighbour tick guard did not hold"); + EntityLootableBody b=fullBody(112,112,8.5);int ticks=b.tickCount; + // Native 1.17 gates dispatch through its entity-section tick list. + // tickNonPassenger alone would bypass that real world dispatcher. + for(int i=0;i<40;i++)world.tick(()->true); + RuntimeChecks.require(b.tickCount==ticks && b.getY()==80 && !world.getChunkSource().hasChunk(113,112),"Vanilla unloaded-neighbour tick guard did not hold"); } private static void unloadReload() throws Exception { - EntityLootableBody b=fullBody(128,128,8.5);UUID uuid=b.getUniqueID();Map expected=inventory(b); + EntityLootableBody b=fullBody(128,128,8.5);UUID uuid=b.getUUID();Map expected=inventory(b); unloadChunk(128,128);world.tick(()->true); - RuntimeChecks.require(!world.getChunkProvider().chunkExists(128,128),"Fixture chunk did not actually unload"); + RuntimeChecks.require(!world.getChunkSource().hasChunk(128,128),"Fixture chunk did not actually unload"); RuntimeChecks.require(!FixtureApi.entities(world).contains(b),"Unloaded corpse still receives normal world ticks"); RuntimeChecks.awaitChunk(world,128,128);EntityLootableBody loaded=RuntimeChecks.find(world,uuid); RuntimeChecks.require(loaded.isAlive() && expected.equals(inventory(loaded)),"Normal chunk unload/load lost inventory"); @@ -192,36 +211,37 @@ private static final class Support implements AutoCloseable { EntityLootableBody body; int cx,cz,nx,nz; BlockPos platform; boolean forced; Map expected; - void unload(){world.forceChunk(cx,cz,false);net.minecraft.util.math.ChunkPos pos=new net.minecraft.util.math.ChunkPos(cx,cz);world.getChunkProvider().registerTicket(net.minecraft.world.server.TicketType.FORCED,pos,0,pos);unloadChunk(nx,nz);RuntimeChecks.require(!world.getChunkProvider().chunkExists(nx,nz),"Support chunk did not unload");} - void frozen(){for(int i=0;i<100;i++)body.tick();System.out.println("ISSUE17_UNLOADED_SUPPORT y="+body.getPosY()+" alive="+body.isAlive()+" inventory="+inventory(body).size()+" supportLoaded="+world.getChunkProvider().chunkExists(nx,nz));RuntimeChecks.require(body.isAlive() && body.getPosY()==80 && expected.equals(inventory(body)),"Corpse fell through unloaded neighbour support and lost recoverable inventory");} - void resume(){RuntimeChecks.awaitChunk(world,nx,nz);FixtureApi.awaitTicking(body);for(int i=0;i<10;i++)world.updateEntity(body);RuntimeChecks.require(body.isOnGround() && body.getPosY()==80 && body.addedToChunk && expected.equals(inventory(body)),"Corpse failed to resume safely after support terrain loaded");} - @Override public void close(){if(forced)world.forceChunk(cx,cz,false);net.minecraft.util.math.ChunkPos pos=new net.minecraft.util.math.ChunkPos(cx,cz);world.getChunkProvider().releaseTicket(net.minecraft.world.server.TicketType.FORCED,pos,0,pos);} + void unload(){world.setChunkForced(cx,cz,false);net.minecraft.world.level.ChunkPos pos=new net.minecraft.world.level.ChunkPos(cx,cz);world.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.FORCED,pos,0,pos);unloadChunk(nx,nz);RuntimeChecks.require(!world.getChunkSource().hasChunk(nx,nz),"Support chunk did not unload");} + void frozen(){for(int i=0;i<100;i++)body.tick();System.out.println("ISSUE17_UNLOADED_SUPPORT y="+body.getY()+" alive="+body.isAlive()+" inventory="+inventory(body).size()+" supportLoaded="+world.getChunkSource().hasChunk(nx,nz));RuntimeChecks.require(body.isAlive() && body.getY()==80 && expected.equals(inventory(body)),"Corpse fell through unloaded neighbour support and lost recoverable inventory");} + void resume(){RuntimeChecks.awaitChunk(world,nx,nz);FixtureApi.awaitTicking(body);for(int i=0;i<10;i++)world.tickNonPassenger(body);RuntimeChecks.require(body.isOnGround() && body.getY()==80 && FixtureApi.sectionContains(body,cx,cz) && expected.equals(inventory(body)),"Corpse failed to resume safely after support terrain loaded");} + @Override public void close(){if(forced)world.setChunkForced(cx,cz,false);net.minecraft.world.level.ChunkPos pos=new net.minecraft.world.level.ChunkPos(cx,cz);world.getChunkSource().removeRegionTicket(net.minecraft.server.level.TicketType.FORCED,pos,0,pos);} } - private static Support support(int axis,int direction) { + private static Support support(int axis,int direction) {return support(axis,direction,null);} + private static Support support(int axis,int direction,UUID savedId) { Support f=new Support();f.cx=144+(sequence++)*8;f.cz=f.cx; f.nx=f.cx+(axis==0?direction:0);f.nz=f.cz+(axis==1?direction:0); RuntimeChecks.awaitChunk(world,f.cx,f.cz);RuntimeChecks.awaitChunk(world,f.nx,f.nz); double x=f.cx*16+(axis==0?(direction>0?15.6:.4):8.5); double z=f.cz*16+(axis==1?(direction>0?15.6:.4):8.5); - int floorX=MathHelper.floor(x),floorZ=MathHelper.floor(z); + int floorX=Mth.floor(x),floorZ=Mth.floor(z); f.platform=new BlockPos(floorX+(axis==0?direction:0),79,floorZ+(axis==1?direction:0)); // A body can straddle a chunk boundary while its centre remains in the ticketed chunk. // Remove only the two fixture columns' ground, leaving one thin supporting platform. for(int y=0;y<5;y++){world.removeBlock(new BlockPos(floorX,y,floorZ),false);world.removeBlock(new BlockPos(f.platform.getX(),y,f.platform.getZ()),false);} - world.setBlockState(f.platform,Blocks.STONE.getDefaultState()); - f.body=fullBody(f.cx,f.cz,8.5);f.body.setPosition(x,80,z);f.expected=inventory(f.body); - f.forced=world.forceChunk(f.cx,f.cz,true); - RuntimeChecks.require(f.forced && world.getChunkProvider().chunkExists(f.cx,f.cz),"Native forced chunk unavailable"); + world.setBlockAndUpdate(f.platform,Blocks.STONE.defaultBlockState()); + f.body=fullBody(f.cx,f.cz,8.5,savedId);f.body.setPos(x,80,z);f.expected=inventory(f.body); + f.forced=world.setChunkForced(f.cx,f.cz,true); + RuntimeChecks.require(f.forced && world.getChunkSource().hasChunk(f.cx,f.cz),"Native forced chunk unavailable"); FixtureApi.awaitTicking(f.body); - for(int i=0;i<10;i++)world.updateEntity(f.body); - RuntimeChecks.require(f.body.isOnGround() && f.body.getPosY()==80 && f.body.isAlive(),"Loaded neighbour platform control failed"); + for(int i=0;i<10;i++)world.tickNonPassenger(f.body); + RuntimeChecks.require(f.body.isOnGround() && f.body.getY()==80 && f.body.isAlive(),"Loaded neighbour platform control failed"); return f; } private static void forcedSupport(int axis,int direction) { try(Support f=support(axis,direction)) { - CompoundNBT before=new CompoundNBT();f.body.writeUnlessPassenger(before); + CompoundTag before=new CompoundTag();f.body.saveAsPassenger(before); f.unload();f.frozen(); - CompoundNBT after=new CompoundNBT();f.body.writeUnlessPassenger(after); + CompoundTag after=new CompoundTag();f.body.saveAsPassenger(after); RuntimeChecks.require(before.equals(after),"Waiting for terrain changed healthy saved NBT"); f.resume(); } @@ -231,9 +251,9 @@ private static void realVoid() { world.removeBlock(f.platform,false); // 1.11 applies ordinary 4-HP void hits with hurt-resistance intervals. // Allow that native sequence to finish; retain the same conservation gate. - for(int i=0;i<250 && f.body.isAlive();i++)world.updateEntity(f.body); + for(int i=0;i<250 && f.body.isAlive();i++)world.tickNonPassenger(f.body); for(int i=0;i<15;i++)f.body.baseTick(); - System.out.println("VOID_CONTROL y="+f.body.getPosY()+" alive="+f.body.isAlive()+" items="+inventory(f.body).size()+" motion="+f.body.getMotion().y); + System.out.println("VOID_CONTROL y="+f.body.getY()+" alive="+f.body.isAlive()+" items="+inventory(f.body).size()+" motion="+f.body.getDeltaMovement().y); RuntimeChecks.require(!f.body.isAlive() && inventory(f.body).isEmpty(),"Loaded real void behavior changed into a rescue feature"); } } @@ -243,33 +263,37 @@ private static void unloadChunk(int x,int z) { private static void flushChunks() { FixtureApi.flush(server); } static void seedPaused() throws Exception { - try(Support f=support(0,1)) { - f.body.setUniqueId(UUID.fromString("00000000-0000-4000-8000-000000000117")); + try(Support f=support(0,1,UUID.fromString("00000000-0000-4000-8000-000000000117"))) { f.unload();f.frozen();RuntimeChecks.saveExpected(folder,"issue17-paused",f.body); try(PrintWriter out=new PrintWriter(new File(folder,"issue17-chunks.txt"),"UTF-8")){out.println(f.cx+" "+f.cz+" "+f.nx+" "+f.nz);} } } - static void coldReload(MinecraftServer instance,ServerWorld target,File directory) throws Exception { + static void coldReload(MinecraftServer instance,ServerLevel target,File directory) throws Exception { server=instance;world=target;folder=directory; String[] coords=new String(java.nio.file.Files.readAllBytes(new File(folder,"issue17-chunks.txt").toPath()),java.nio.charset.StandardCharsets.UTF_8).trim().split(" "); int cx=Integer.parseInt(coords[0]),cz=Integer.parseInt(coords[1]),nx=Integer.parseInt(coords[2]),nz=Integer.parseInt(coords[3]); RuntimeChecks.awaitChunk(world,cx,cz); - RuntimeChecks.require(!world.getChunkProvider().chunkExists(nx,nz),"Deferred support chunk opened too early"); + RuntimeChecks.require(!world.getChunkSource().hasChunk(nx,nz),"Deferred support chunk opened too early"); EntityLootableBody b=RuntimeChecks.find(world,UUID.fromString("00000000-0000-4000-8000-000000000117")); RuntimeChecks.verifyExpected(folder,"issue17-paused",b); - for(int i=0;i<100;i++)world.updateEntity(b); - RuntimeChecks.require(b.isAlive() && b.getPosY()==80,"Cold-loaded paused corpse fell before support loaded"); + for(int i=0;i<100;i++)world.tickNonPassenger(b); + RuntimeChecks.require(b.isAlive() && b.getY()==80,"Cold-loaded paused corpse fell before support loaded"); RuntimeChecks.verifyExpected(folder,"issue17-paused",b); for(int x=cx-2;x<=cx+2;x++)for(int z=cz-2;z<=cz+2;z++)RuntimeChecks.awaitChunk(world,x,z); FixtureApi.awaitTicking(b); - for(int i=0;i<20;i++)world.updateEntity(b); - RuntimeChecks.require(b.isOnGround() && b.getPosY()==80 && b.isAlive(),"Cold-loaded corpse failed to resume on restored platform"); + for(int i=0;i<20;i++)world.tickNonPassenger(b); + RuntimeChecks.require(b.isOnGround() && b.getY()==80 && b.isAlive(),"Cold-loaded corpse failed to resume on restored platform"); RuntimeChecks.verifyExpected(folder,"issue17-paused",b); - world.forceChunk(cx,cz,false); + world.setChunkForced(cx,cz,false); System.out.println("ISSUE17_COLD_RELOAD_DEFERRED_SUPPORT_AND_COMPLETE_NBT_PASS"); } - private static final class QuietConnection extends ServerPlayNetHandler { - QuietConnection(ServerPlayerEntity p){super(server,new NetworkManager(PacketDirection.SERVERBOUND),p);} - @Override public void sendPacket(IPacket packet) { } + private static final class QuietConnection extends ServerGamePacketListenerImpl { + int menuEvents,menuState=-1;ItemStack carried=ItemStack.EMPTY; + QuietConnection(ServerPlayer p){super(server,new Connection(PacketFlow.SERVERBOUND),p);} + @Override public void send(Packet packet) { + NativeMenuObservation observed=NativeMenuObservation.decode(packet);if(observed==null)return; + if(observed.containerId==player.containerMenu.containerId){menuEvents++;menuState=observed.stateId;if(observed.content)carried=observed.carried;} + else if(observed.containerId==-1)carried=observed.item; + } } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java index 2e31a98..fa2a277 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java @@ -1,11 +1,11 @@ package cyano.lootable.buildtest; -import net.minecraft.client.gui.screen.MainMenuScreen; -import net.minecraft.client.gui.screen.DeathScreen; -import net.minecraft.inventory.Inventory; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.client.gui.screens.TitleScreen; +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.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -14,20 +14,24 @@ import java.util.concurrent.Future; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.button.Button; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.item.Items; -import net.minecraft.block.Blocks; -import net.minecraft.inventory.container.ClickType; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; -import net.minecraft.network.play.client.CHeldItemChangePacket; -import net.minecraft.util.*; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.*; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; +import net.minecraft.client.Screenshot; +import net.minecraft.world.InteractionHand; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.GameType; /** Exact issue5 sequence over a real connected client and two disk reloads. */ final class ReportedIssueClientChecks { @@ -37,70 +41,70 @@ private interface Action {void run() throws Exception;} private Future pending; private UUID firstId,secondId; private File report; - private CompoundNBT savedBody,savedPlayer; + private CompoundTag savedBody,savedPlayer; static ItemStack original() { ItemStack s=new ItemStack(Items.DIAMOND_SHOVEL); - s.setTag(RuntimeChecks.stack(300).getTag().copy());s.setDamage(17); - s.setDisplayName(new net.minecraft.util.text.StringTextComponent("Issue 5 original shovel"));return s; + s.setTag(RuntimeChecks.stack(300).getTag().copy());s.setDamageValue(17); + s.setHoverName(new net.minecraft.network.chat.TextComponent("Issue 5 original shovel"));return s; } void tick(Minecraft mc) throws Exception { frames++; - if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { - report=new File(mc.gameDir,"issue5-client-checks.txt");RuntimeChecks.require(!report.exists(),"Stale issue5 report"); - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; - FixtureApi.ClientWorlds.create(mc,"issue-fixture","Issue fixture",net.minecraft.world.GameType.SURVIVAL,true);next(1); + 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; + 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,()->{ - ServerWorld w=player(mc).getServerWorld();w.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(false,null); - w.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_SPAWNING).set(false,null); - for(int x=29;x<=36;x++)for(int z=-3;z<=4;z++)w.setBlockState(new BlockPos(x,79,z),Blocks.STONE.getDefaultState()); - ServerPlayerEntity p=player(mc);p.setGameType(GameType.SURVIVAL);p.inventory.clear();p.inventory.currentItem=4; - p.inventory.mainInventory.set(4,original());p.inventory.mainInventory.set(9,RuntimeChecks.stack(301)); - p.connection.setPlayerLocation(32.5,80,.5,0,0);p.container.detectAndSendChanges(); - firstId=die(p,null).getUniqueID(); + ServerLevel w=player(mc).getLevel();w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); + w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); + for(int x=29;x<=36;x++)for(int z=-3;z<=4;z++)w.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); + ServerPlayer p=player(mc);p.setGameMode(GameType.SURVIVAL);p.getInventory().clearContent();p.getInventory().selected=4; + p.getInventory().items.set(4,original());p.getInventory().items.set(9,RuntimeChecks.stack(301)); + p.connection.teleport(32.5,80,.5,0,0);p.containerMenu.broadcastChanges(); + firstId=die(p,null).getUUID(); });next(2); - } else if(stage==2 && done() && mc.currentScreen instanceof DeathScreen && frames>10) { - mc.player.respawnPlayer();next(3); - } else if(stage==3 && ready(mc) && mc.player.isAlive() && !(mc.currentScreen instanceof DeathScreen)) { - schedule(mc,()->player(mc).connection.setPlayerLocation(32.5,80,.5,0,0));next(4); + } else if(stage==2 && done() && mc.screen instanceof DeathScreen && frames>10) { + mc.player.respawn();next(3); + } else if(stage==3 && ready(mc) && mc.player.isAlive() && !(mc.screen instanceof DeathScreen)) { + schedule(mc,()->player(mc).connection.teleport(32.5,80,.5,0,0));next(4); } else if(stage==4 && done() && open(mc,firstId)) {next(5); } else if(stage==5 && gui(mc) && frames>15) { - equal(mc.player.openContainer.getSlot(4).getStack(),original(),"First corpse main hand");shot(mc,"issue5-first-main-hand.png"); - mc.playerController.windowClick(mc.player.openContainer.windowId,4,0,ClickType.SWAP,mc.player);next(6); + equal(mc.player.containerMenu.getSlot(4).getItem(),original(),"First corpse main hand");shot(mc,"issue5-first-main-hand.png"); + mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,4,0,ClickType.SWAP,mc.player);next(6); } else if(stage==6 && frames>15) { - equal(mc.player.inventory.mainInventory.get(0),original(),"Key1 original shovel recovery"); - for(int slot=0;slot<48;slot++)if(mc.player.openContainer.getSlot(slot).getHasStack()) - mc.playerController.windowClick(mc.player.openContainer.windowId,slot,0,ClickType.QUICK_MOVE,mc.player); + equal(mc.player.getInventory().items.get(0),original(),"Key1 original shovel recovery"); + for(int slot=0;slot<48;slot++)if(mc.player.containerMenu.getSlot(slot).hasItem()) + mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,0,ClickType.QUICK_MOVE,mc.player); next(7); } else if(stage==7 && frames>20) { - equal(mc.player.inventory.mainInventory.get(0),original(),"Original shovel changed before switch"); - active=-1;for(int slot=1;slot<9;slot++)if(RuntimeChecks.sameStack(mc.player.inventory.mainInventory.get(slot),RuntimeChecks.stack(301)))active=slot; + equal(mc.player.getInventory().items.get(0),original(),"Original shovel changed before switch"); + active=-1;for(int slot=1;slot<9;slot++)if(RuntimeChecks.sameStack(mc.player.getInventory().items.get(slot),RuntimeChecks.stack(301)))active=slot; RuntimeChecks.require(active>0,"No different recovered hotbar item"); mark("first_death_respawn_key1_and_shift_recovery"); - mc.player.closeScreen();mc.player.inventory.currentItem=active; - mc.getConnection().sendPacket(new CHeldItemChangePacket(active)); + mc.player.closeContainer();mc.player.getInventory().selected=active; + mc.getConnection().send(new ServerboundSetCarriedItemPacket(active)); // Same ordered connection: held-item change, then a build-only death signal. // This uses the vanilla death path without native command permissions. MultiplayerChecks.CHANNEL.sendToServer(new MultiplayerChecks.FixtureDeath());next(8); - } else if(stage==8 && mc.currentScreen instanceof DeathScreen && frames>10) { - mc.player.respawnPlayer();next(9); - } else if(stage==9 && ready(mc) && mc.player.isAlive() && !(mc.currentScreen instanceof DeathScreen)) { + } else if(stage==8 && mc.screen instanceof DeathScreen && frames>10) { + mc.player.respawn();next(9); + } else if(stage==9 && ready(mc) && mc.player.isAlive() && !(mc.screen instanceof DeathScreen)) { schedule(mc,()->{ - ServerPlayerEntity p=player(mc);EntityLootableBody second=null; - for(Entity e:FixtureApi.entities(p.world))if(e instanceof EntityLootableBody && !e.getUniqueID().equals(firstId) && p.getGameProfile().getName().equals(((EntityLootableBody)e).getCustomNameTag()))second=(EntityLootableBody)e; - RuntimeChecks.require(second!=null,"Second actual death corpse absent");secondId=second.getUniqueID(); - verifyOriginals(second);equal(second.getStackInSlot(4),RuntimeChecks.stack(301),"Second selected main hand"); - RuntimeChecks.require(p.inventory.write(new ListNBT()).size()==0,"Second death duplicated player inventory"); + ServerPlayer p=player(mc);EntityLootableBody second=null; + for(Entity e:FixtureApi.entities(p.level))if(e instanceof EntityLootableBody && !e.getUUID().equals(firstId) && p.getGameProfile().getName().equals(((EntityLootableBody)e).getCustomNameTag()))second=(EntityLootableBody)e; + RuntimeChecks.require(second!=null,"Second actual death corpse absent");secondId=second.getUUID(); + verifyOriginals(second);equal(second.getItem(4),RuntimeChecks.stack(301),"Second selected main hand"); + RuntimeChecks.require(p.getInventory().save(new ListTag()).size()==0,"Second death duplicated player inventory"); if(Boolean.getBoolean("lootablebodies.issueClient.offsetViewer")) - p.connection.setPlayerLocation(64.5,4,64.5,0,0); - else p.connection.setPlayerLocation(32.5,80,.5,0,0); + p.connection.teleport(64.5,4,64.5,0,0); + else p.connection.teleport(32.5,80,.5,0,0); mark("second_death_without_inventory_resync_preserves_original_shovel_and_all_tags"); });next(10); } else if(stage==10 && done()) { if(Boolean.getBoolean("lootablebodies.issueClient.offsetViewer") && !distantViewerObserved) { if(frames<40)return; boolean present=false; - for(Entity e:FixtureApi.entities(mc.world))if(e.getUniqueID().equals(secondId))present=true; + for(Entity e:FixtureApi.entities(mc.level))if(e.getUUID().equals(secondId))present=true; if(present) { RuntimeChecks.require(frames<200,"Distant viewer did not leave corpse tracking range"); return; @@ -111,69 +115,69 @@ void tick(Minecraft mc) throws Exception { if(open(mc,secondId))next(11); } else if(stage==11 && gui(mc) && frames>20) { verifyClientOriginals(mc);shot(mc,"issue5-second-corpse.png"); - mc.player.closeScreen();schedule(mc,()->save(mc));next(12); + 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.loadWorld("issue-fixture");next(14); + mc.loadLevel("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); } else if(stage==16 && gui(mc) && frames>20) { verifyClientOriginals(mc);shot(mc,"issue5-reloaded-corpse.png"); - for(int slot=0;slot<48;slot++)if(mc.player.openContainer.getSlot(slot).getHasStack()) - mc.playerController.windowClick(mc.player.openContainer.windowId,slot,0,ClickType.QUICK_MOVE,mc.player); + for(int slot=0;slot<48;slot++)if(mc.player.containerMenu.getSlot(slot).hasItem()) + mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,0,ClickType.QUICK_MOVE,mc.player); next(17); } else if(stage==17 && frames>20) { - mc.player.closeScreen();schedule(mc,()->{ + mc.player.closeContainer();schedule(mc,()->{ RuntimeChecks.require(body(mc).isEmpty(),"Final recovery left duplicate corpse items"); Map expected=originals(),actual=new TreeMap<>(); - ServerPlayerEntity p=player(mc); - for(int i=0;i25) { schedule(mc,()->{verifyDisk(mc);mark("second_disk_reload_complete_player_and_corpse_inventory");});next(21); } else if(stage==21 && done()) { - new File(mc.gameDir,"client.pass").createNewFile(); + new File(mc.gameDirectory,"client.pass").createNewFile(); System.out.println("LOOTABLE_BODIES_ISSUE5_CLIENT_PASS");quit(mc);next(22); - } else if(stage==22 && FixtureConnections.stopped(mc)) {mc.shutdown();next(23);} + } else if(stage==22 && FixtureConnections.stopped(mc)) {mc.stop();next(23);} } - private EntityLootableBody die(ServerPlayerEntity p,UUID skip) { - Set before=new HashSet<>(FixtureApi.entities(p.world));p.onKillCommand(); - for(Entity e:FixtureApi.entities(p.world))if(e instanceof EntityLootableBody && !before.contains(e))return (EntityLootableBody)e; + private EntityLootableBody die(ServerPlayer p,UUID skip) { + Set before=new HashSet<>(FixtureApi.entities(p.level));p.kill(); + for(Entity e:FixtureApi.entities(p.level))if(e instanceof EntityLootableBody && !before.contains(e))return (EntityLootableBody)e; throw new AssertionError("Actual connected death absent"); } private static void addTagged(Map result,ItemStack stack) { if((stack == null || stack.isEmpty()) || !stack.hasTag() || !stack.getTag().contains("fixture"))return; - CompoundNBT n=stack.write(new CompoundNBT());n.remove("Count");result.put(n.toString(),result.getOrDefault(n.toString(),0)+stack.getCount()); + CompoundTag n=stack.save(new CompoundTag());n.remove("Count");result.put(n.toString(),result.getOrDefault(n.toString(),0)+stack.getCount()); } private static Map originals(){Map m=new TreeMap<>();addTagged(m,original());addTagged(m,RuntimeChecks.stack(301));return m;} private static void verifyOriginals(EntityLootableBody b) { Map n=new TreeMap<>(ReportedIssueChecks.inventory(b));n.keySet().removeIf(k->!k.contains("fixture")); RuntimeChecks.require(originals().equals(n),"Second corpse lost/duplicated exact tagged inventory"); } - private static void verifyClientOriginals(Minecraft mc){Map n=new TreeMap<>();for(int i=0;i<48;i++)addTagged(n,mc.player.openContainer.getSlot(i).getStack());RuntimeChecks.require(originals().equals(n),"Connected GUI differs from exact originals");} + private static void verifyClientOriginals(Minecraft mc){Map n=new TreeMap<>();for(int i=0;i<48;i++)addTagged(n,mc.player.containerMenu.getSlot(i).getItem());RuntimeChecks.require(originals().equals(n),"Connected GUI differs from exact originals");} private void save(Minecraft mc) throws Exception { - savedBody=new CompoundNBT();body(mc).writeUnlessPassenger(savedBody);savedPlayer=new CompoundNBT();savedPlayer.put("Inventory",player(mc).inventory.write(new ListNBT())); - File folder=player(mc).getServerWorld().getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile(); - try(FileOutputStream out=new FileOutputStream(new File(folder,"issue5-corpse-expected.nbt"))){CompressedStreamTools.writeCompressed(savedBody,out);} - try(FileOutputStream out=new FileOutputStream(new File(folder,"issue5-player-expected.nbt"))){CompressedStreamTools.writeCompressed(savedPlayer,out);} - mc.getIntegratedServer().getPlayerList().saveAllPlayerData();mc.getIntegratedServer().save(false,true,false); + savedBody=new CompoundTag();body(mc).saveAsPassenger(savedBody);savedPlayer=new CompoundTag();savedPlayer.put("Inventory",player(mc).getInventory().save(new ListTag())); + File folder=player(mc).getLevel().getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); + try(FileOutputStream out=new FileOutputStream(new File(folder,"issue5-corpse-expected.nbt"))){NbtIo.writeCompressed(savedBody,out);} + try(FileOutputStream out=new FileOutputStream(new File(folder,"issue5-player-expected.nbt"))){NbtIo.writeCompressed(savedPlayer,out);} + mc.getSingleplayerServer().getPlayerList().saveAll();mc.getSingleplayerServer().saveAllChunks(false,true,false); } private void verifyDisk(Minecraft mc) { - player(mc).getServerWorld().getChunk(2,0);CompoundNBT actual=new CompoundNBT();body(mc).writeUnlessPassenger(actual); - for(String key:Arrays.asList("id","UUIDMost","UUIDLeast","Equipment","Aux","Name"))RuntimeChecks.require(Objects.equals(savedBody.get(key),actual.get(key)),"Disk corpse differs: "+key); - RuntimeChecks.require(savedPlayer.getList("Inventory",10).equals(player(mc).inventory.write(new ListNBT())),"Complete player inventory differs after disk reload"); + RuntimeChecks.awaitChunk(player(mc).getLevel(),2,0);CompoundTag actual=new CompoundTag();body(mc).saveAsPassenger(actual); + for(String key:Arrays.asList("id","UUID","UUIDMost","UUIDLeast","Equipment","Aux","Name"))RuntimeChecks.require(Objects.equals(savedBody.get(key),actual.get(key)),"Disk corpse differs: "+key); + RuntimeChecks.require(savedPlayer.getList("Inventory",10).equals(player(mc).getInventory().save(new ListTag())),"Complete player inventory differs after disk reload"); } - private ServerPlayerEntity player(Minecraft mc){return mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID());} - private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(player(mc).getServerWorld(),secondId);} + private ServerPlayer player(Minecraft mc){return mc.getSingleplayerServer().getPlayerList().getPlayer(mc.player.getUUID());} + private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(player(mc).getLevel(),secondId);} private static void equal(ItemStack a,ItemStack b,String reason){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),reason);} - private static boolean ready(Minecraft mc){return mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null;} - private static boolean gui(Minecraft mc){return mc.currentScreen instanceof CorpseGUIContainer;} + private static boolean ready(Minecraft mc){return mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null;} + private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} private boolean open(Minecraft mc,UUID id) throws Exception { // Sending an interaction does not mean the server has opened its GUI. if(gui(mc)){approachPending=false;return true;} @@ -183,18 +187,18 @@ private boolean open(Minecraft mc,UUID id) throws Exception { // Resolve the fixture's UUID on the server, move the viewer back // into range, then wait for normal tracking and interaction. schedule(mc,()->{ - ServerPlayerEntity p=player(mc); - EntityLootableBody corpse=RuntimeChecks.find(p.getServerWorld(),id); - if(p.getDistanceSq(corpse)>9) { - double x=corpse.getPosX(),z=corpse.getPosZ()-2; - int y=net.minecraft.util.math.MathHelper.floor(corpse.getPosY()); + ServerPlayer p=player(mc); + EntityLootableBody corpse=RuntimeChecks.find(p.getLevel(),id); + if(p.distanceToSqr(corpse)>9) { + double x=corpse.getX(),z=corpse.getZ()-2; + int y=net.minecraft.util.Mth.floor(corpse.getY()); // A respawn can leave the viewer far from the original // 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.world.setBlockState(new BlockPos(x+dx,y-1,z+dz),Blocks.STONE.getDefaultState()); - p.setMotion(0,0,0); - p.connection.setPlayerLocation(x,y,z,0,0); + p.level.setBlockAndUpdate(new BlockPos(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); } }); @@ -203,17 +207,17 @@ private boolean open(Minecraft mc,UUID id) throws Exception { } if(!done())return false; approachPending=false; - for(Entity e:FixtureApi.entities(mc.world))if(e instanceof EntityLootableBody && e.getUniqueID().equals(id) && mc.player.getDistanceSq(e)<=9){ - System.out.println("ISSUE5_GUI_OPEN_REQUEST corpse="+id+" distance_sq="+mc.player.getDistanceSq(e)); - mc.playerController.interactWithEntity(mc.player,e,Hand.MAIN_HAND); + for(Entity e:FixtureApi.entities(mc.level))if(e instanceof EntityLootableBody && e.getUUID().equals(id) && mc.player.distanceToSqr(e)<=9){ + System.out.println("ISSUE5_GUI_OPEN_REQUEST corpse="+id+" distance_sq="+mc.player.distanceToSqr(e)); + mc.gameMode.interact(mc.player,e,InteractionHand.MAIN_HAND); break; } return false; } - private void schedule(Minecraft mc,Action a){pending=mc.getIntegratedServer().runAsync(()->{try{a.run();}catch(Exception e){throw new RuntimeException(e);}});} + private void schedule(Minecraft mc,Action a){pending=mc.getSingleplayerServer().submitAsync(()->{try{a.run();}catch(Exception e){throw new RuntimeException(e);}});} private boolean done() throws Exception {if(!pending.isDone())return false;pending.get();return true;} private void next(int n){stage=n;frames=0;System.out.println("ISSUE5_CLIENT_STAGE "+n);} private void mark(String name) throws Exception {try(FileWriter out=new FileWriter(report,true)){out.write("PASS "+name+"\n");}System.out.println("ISSUE5_CLIENT_CHECK_PASS "+name);} - private static void shot(Minecraft mc,String name){ScreenShotHelper.saveScreenshot(mc.gameDir,name,mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} - private static void quit(Minecraft mc){mc.player.closeScreen();FixtureConnections.disconnect(mc);} + private static void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java index 3f73dca..ba4e076 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java @@ -1,7 +1,7 @@ package cyano.lootable.buildtest; -import net.minecraft.inventory.container.Container; -import net.minecraft.entity.boss.dragon.phase.Phase; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; +import net.minecraft.server.level.ServerLevel; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.LootableBodies; @@ -9,13 +9,15 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.block.Blocks; -import net.minecraft.item.ItemStack; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.item.ItemStack; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.*; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; @@ -24,8 +26,8 @@ public final class RespawnPacketChecks { private final MinecraftServer server; private int index,phase,ticks,phaseTicks,startupTicks; - private ServerPlayerEntity dead; - private ServerPlayerEntity observerDead; + private ServerPlayer dead; + private ServerPlayer observerDead; private EntityLootableBody body; private EntityLootableBody observerBody; private final boolean simultaneous=Boolean.getBoolean("lootablebodies.respawn.simultaneous"); @@ -40,7 +42,7 @@ public final class RespawnPacketChecks { } static void verifySlowClientStartup(MinecraftServer server) throws Exception { String previous=System.getProperty("lootablebodies.multiplayer.sync"); - File folder=new File(server.getWorld(FixtureApi.dimensionKey(0)).getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile(),"respawn-startup-regression"); + File folder=new File(server.getLevel(FixtureApi.dimensionKey(0)).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(),"respawn-startup-regression"); Files.createDirectories(folder.toPath()); RespawnPacketChecks fixture=null; try { @@ -59,56 +61,56 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { } } static File file(String name){return new File(System.getProperty("lootablebodies.multiplayer.sync"),name);} - private ServerPlayerEntity player(String role){return server.getPlayerList().getPlayerByUsername("Container"+role);} + 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(ServerPlayerEntity p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.netManager,file(p.getGameProfile().getName()+"-server-packets.txt"),null);} + private void trace(ServerPlayer p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.getConnection(),file(p.getGameProfile().getName()+"-server-packets.txt"),null);} @SubscribeEvent public void tick(TickEvent.ServerTickEvent e) { if(e.phase!=TickEvent.Phase.END||finished)return; try { RuntimeChecks.require(++ticks<20000,"Respawn matrix total timeout"); RuntimeChecks.require(!file("abort").exists(),"Respawn matrix aborted"); - ServerPlayerEntity a=player("A"),b=player("B");trace(a);trace(b); + ServerPlayer a=player("A"),b=player("B");trace(a);trace(b); if(index==0&&phase==0&&(a==null||b==null||!ack("A",0)||!ack("B",0))) { RuntimeChecks.require(++startupTicks<2400,"Clients did not finish startup before the respawn matrix"); return; } phaseTicks++; - if(phaseTicks%100==0)System.out.println("RESPAWN_MATRIX_STATE case="+index+" phase="+phase+" A="+(a==null?null:a.getPosition())+" body="+(body==null?null:body.getPosition())+" tracked="+(body==null?null:FixtureApi.tracked(body))); + if(phaseTicks%100==0)System.out.println("RESPAWN_MATRIX_STATE case="+index+" phase="+phase+" A="+(a==null?null:a.blockPosition())+" body="+(body==null?null:body.blockPosition())+" tracked="+(body==null?null:FixtureApi.tracked(body))); RuntimeChecks.require(phaseTicks<500,"Corpse missing after accepted respawn case="+index+" phase="+phase); if(phase==0 && a!=null&&b!=null&&ack("A",0)&&ack("B",0)) { - int kind=index%8;net.minecraft.util.RegistryKey dim=kind==7?net.minecraft.world.World.THE_NETHER:net.minecraft.world.World.OVERWORLD;ServerWorld w=server.getWorld(dim); - if(a.world.getDimensionKey()!=dim)transfer(a,dim); - if(b.world.getDimensionKey()!=dim)transfer(b,dim); + int kind=index%8;net.minecraft.resources.ResourceKey dim=kind==7?net.minecraft.world.level.Level.NETHER:net.minecraft.world.level.Level.OVERWORLD;ServerLevel w=server.getLevel(dim); + 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; for(int dx=-6;dx<=6;dx++)for(int dz=-6;dz<=6;dz++) { - BlockPos floor=new BlockPos(x+dx,79,z+dz);w.setBlockState(floor,Blocks.OBSIDIAN.getDefaultState()); + BlockPos floor=new BlockPos(x+dx,79,z+dz);w.setBlockAndUpdate(floor,Blocks.OBSIDIAN.defaultBlockState()); for(int y=80;y<=84;y++)w.removeBlock(new BlockPos(x+dx,y,z+dz),false); } - w.getGameRules().get(net.minecraft.world.GameRules.DO_MOB_SPAWNING).set(false,null);w.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(false,null); - a.setGameType(GameType.SURVIVAL);b.setGameType(GameType.SURVIVAL);a.inventory.clear();b.inventory.clear(); - FixtureApi.spawn(a,new BlockPos(kind==5?160:(int)Math.floor(x),80,(int)Math.floor(z)-3),true,false,a.world.getDimensionKey()); - FixtureApi.spawn(b,new BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),true,false,b.world.getDimensionKey()); - if(kind==7)FixtureApi.spawn(a,new BlockPos(96,80,93),true,false,a.world.getDimensionKey()); - for(int dx=157;dx<=164;dx++)for(int dz=90;dz<=98;dz++)w.setBlockState(new BlockPos(dx,79,dz),Blocks.OBSIDIAN.getDefaultState()); - a.inventory.mainInventory.set(9,RuntimeChecks.stack(520+index));a.container.detectAndSendChanges(); - if(simultaneous){b.inventory.mainInventory.set(9,RuntimeChecks.stack(620+index));b.container.detectAndSendChanges();} - a.connection.setPlayerLocation(x,80,z,0,0);b.connection.setPlayerLocation(kind==4?160.5:x+2,80,z-2.5,0,25); + 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(); + FixtureApi.spawn(a,new BlockPos(kind==5?160:(int)Math.floor(x),80,(int)Math.floor(z)-3),true,false,a.level.dimension()); + FixtureApi.spawn(b,new BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),true,false,b.level.dimension()); + if(kind==7)FixtureApi.spawn(a,new BlockPos(96,80,93),true,false,a.level.dimension()); + for(int dx=157;dx<=164;dx++)for(int dz=90;dz<=98;dz++)w.setBlockAndUpdate(new BlockPos(dx,79,dz),Blocks.OBSIDIAN.defaultBlockState()); + a.getInventory().items.set(9,RuntimeChecks.stack(520+index));a.containerMenu.broadcastChanges(); + if(simultaneous){b.getInventory().items.set(9,RuntimeChecks.stack(620+index));b.containerMenu.broadcastChanges();} + a.connection.teleport(x,80,z,0,0);b.connection.teleport(kind==4?160.5:x+2,80,z-2.5,0,25); state(1); } else if(phase==1&&ack("A",1)&&ack("B",1)&&phaseTicks>10) { - dead=a;Set before=new HashSet<>(FixtureApi.entities(a.world));a.onKillCommand();body=null; - for(Entity ent:FixtureApi.entities(a.world))if(ent instanceof EntityLootableBody&&!before.contains(ent))body=(EntityLootableBody)ent; + dead=a;Set before=new HashSet<>(FixtureApi.entities(a.level));a.kill();body=null; + for(Entity ent:FixtureApi.entities(a.level))if(ent instanceof EntityLootableBody&&!before.contains(ent))body=(EntityLootableBody)ent; RuntimeChecks.require(body!=null,"No server death corpse");state(2); if(simultaneous) { - observerDead=b;before=new HashSet<>(FixtureApi.entities(b.world));b.onKillCommand();observerBody=null; - for(Entity ent:FixtureApi.entities(b.world))if(ent instanceof EntityLootableBody&&!before.contains(ent))observerBody=(EntityLootableBody)ent; + observerDead=b;before=new HashSet<>(FixtureApi.entities(b.level));b.kill();observerBody=null; + for(Entity ent:FixtureApi.entities(b.level))if(ent instanceof EntityLootableBody&&!before.contains(ent))observerBody=(EntityLootableBody)ent; RuntimeChecks.require(observerBody!=null,"No observer death corpse"); } } else if(phase==2&&a!=null&&a!=dead&&a.isAlive()&&(!simultaneous||(b!=null&&b!=observerDead&&b.isAlive()))) { RuntimeChecks.require(body.isAlive()&&VisibilityChecks.has(body,520+index),"Server corpse/inventory lost during respawn"); - if(a.world.getDimensionKey()!=body.world.getDimensionKey())transfer(a,body.world.getDimensionKey()); - a.connection.setPlayerLocation(body.getPosX(),body.getPosY(),body.getPosZ()-2.5,0,25); - if(b.world.getDimensionKey()!=body.world.getDimensionKey())transfer(b,body.world.getDimensionKey()); - b.connection.setPlayerLocation(body.getPosX()+1.5,body.getPosY(),body.getPosZ()-2.5,0,25); + if(a.level.dimension()!=body.level.dimension())transfer(a,body.level.dimension()); + a.connection.teleport(body.getX(),body.getY(),body.getZ()-2.5,0,25); + if(b.level.dimension()!=body.level.dimension())transfer(b,body.level.dimension()); + b.connection.teleport(body.getX()+1.5,body.getY(),body.getZ()-2.5,0,25); // With no players left, Forge may unload/reload the Nether between // death and return. Saved UUIDs survive; runtime entity IDs do not. body=live(body);if(simultaneous)observerBody=live(observerBody);state(3); @@ -116,43 +118,43 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { // In a cold restart, many ordinary saved bodies can collide and // move the new corpse beyond the player's interaction reach. // Follow it with the viewer; never freeze production physics. - if(a!=null&&a.getDistanceSq(body)>9)a.connection.setPlayerLocation(body.getPosX(),body.getPosY(),body.getPosZ()-2.5,0,25); + if(a!=null&&a.distanceToSqr(body)>9)a.connection.teleport(body.getX(),body.getY(),body.getZ()-2.5,0,25); if(phaseTicks==10){writeTracked(a,"A");writeTracked(b,"B");} if(!ack("A",3)||!ack("B",3))return; - RuntimeChecks.require(a.openContainer instanceof cyano.lootable.graphics.CorpseContainer,"Owner GUI not backed by server container"); + RuntimeChecks.require(a.containerMenu instanceof cyano.lootable.graphics.CorpseContainer,"Owner GUI not backed by server container"); RuntimeChecks.require(body.isAlive()&&VisibilityChecks.has(body,520+index),"Corpse lost exact tagged items"); if(simultaneous)RuntimeChecks.require(observerBody.isAlive()&&VisibilityChecks.has(observerBody,620+index),"Observer corpse lost exact tagged items"); try(FileWriter out=new FileWriter(file("checks.txt"),true)){out.write("PASS case="+index+" kind="+(index%8)+" owner_and_observer_after_respawn\n");} - a.closeScreen();b.closeScreen();index++; + a.closeContainer();b.closeContainer();index++; if(index==count){state(9);}else state(0); - } else if(phase==9&&server.getPlayerList().getCurrentPlayerCount()==0) { - Files.write(file("respawn.pass").toPath(),new byte[]{1});System.out.println("LOOTABLE_BODIES_RESPAWN_MATRIX_PASS");finished=true;server.initiateShutdown(false); + } else if(phase==9&&server.getPlayerList().getPlayerCount()==0) { + Files.write(file("respawn.pass").toPath(),new byte[]{1});System.out.println("LOOTABLE_BODIES_RESPAWN_MATRIX_PASS");finished=true;server.halt(false); } - } catch(Throwable failure){failure.printStackTrace();try{Files.write(file("server.failure").toPath(),failure.toString().getBytes(StandardCharsets.UTF_8));}catch(IOException ignored){}finished=true;server.initiateShutdown(false);} + } catch(Throwable failure){failure.printStackTrace();try{Files.write(file("server.failure").toPath(),failure.toString().getBytes(StandardCharsets.UTF_8));}catch(IOException ignored){}finished=true;server.halt(false);} } private void state(int value) throws Exception { - phase=value;phaseTicks=0;String line=index+" "+phase+" "+(body==null?"none":body.getUniqueID())+" "+(body==null?-1:body.getEntityId()); + phase=value;phaseTicks=0;String line=index+" "+phase+" "+(body==null?"none":body.getUUID())+" "+(body==null?-1:body.getId()); File tmp=file("state.tmp");Files.write(tmp.toPath(),line.getBytes(StandardCharsets.UTF_8)); // Windows readers briefly deny replacement while their handle is open. for(int retry=0;;retry++)try{Files.move(tmp.toPath(),file("state").toPath(),StandardCopyOption.REPLACE_EXISTING,StandardCopyOption.ATOMIC_MOVE);break;} catch(java.nio.file.AccessDeniedException busy){if(retry==50)throw busy;Thread.sleep(1);} System.out.println("RESPAWN_MATRIX_PHASE "+line); } - private void transfer(ServerPlayerEntity player,net.minecraft.util.RegistryKey dimension) { - ServerWorld target=server.getWorld(dimension); + private void transfer(ServerPlayer player,net.minecraft.resources.ResourceKey dimension) { + ServerLevel target=server.getLevel(dimension); // This is an absolute fixture move, not a portal-generation check. // Use the native16 command path, including its real respawn packet. - player.teleport(target,96.5,80,96.5,player.rotationYaw,0); + player.teleportTo(target,96.5,80,96.5,player.getYRot(),0); } - private void writeTracked(ServerPlayerEntity player,String role) throws Exception { - List expected=new ArrayList<>();ServerWorld w=player.getServerWorld(); - for(Entity e:FixtureApi.entities(w))if(e instanceof EntityLootableBody&&e.isAlive()&&FixtureApi.tracked(e).contains(player))expected.add(e.getEntityId()+" "+e.getUniqueID()); + private void writeTracked(ServerPlayer player,String role) throws Exception { + List expected=new ArrayList<>();ServerLevel w=player.getLevel(); + for(Entity e:FixtureApi.entities(w))if(e instanceof EntityLootableBody&&e.isAlive()&&FixtureApi.tracked(e).contains(player))expected.add(e.getId()+" "+e.getUUID()); Collections.sort(expected);Files.write(file("expected-"+index+"-"+role+".txt").toPath(),expected,StandardCharsets.UTF_8); } private EntityLootableBody live(EntityLootableBody previous) { - ServerWorld w=server.getWorld(previous.world.getDimensionKey()); - w.getChunk((int)Math.floor(previous.getPosX()/16),(int)Math.floor(previous.getPosZ()/16)); - return RuntimeChecks.find(w,previous.getUniqueID()); + ServerLevel w=server.getLevel(previous.level.dimension()); + w.getChunk((int)Math.floor(previous.getX()/16),(int)Math.floor(previous.getZ()/16)); + return RuntimeChecks.find(w,previous.getUUID()); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java index 671013c..ae06627 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java @@ -1,9 +1,9 @@ package cyano.lootable.buildtest; -import net.minecraft.client.gui.screen.MainMenuScreen; -import net.minecraft.client.gui.screen.DeathScreen; -import net.minecraft.inventory.container.Container; -import net.minecraft.entity.boss.dragon.phase.Phase; -import net.minecraft.util.text.StringTextComponent; +import net.minecraft.client.gui.screens.TitleScreen; +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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -15,15 +15,16 @@ import java.util.*; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.button.Button; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.entity.Entity; -import net.minecraft.item.ItemStack; -import net.minecraft.util.*; -import net.minecraft.server.management.PlayerProfileCache; -import net.minecraft.tileentity.SkullTileEntity; -import net.minecraft.world.World; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.client.Screenshot; +import net.minecraft.world.InteractionHand; +import net.minecraft.server.players.GameProfileCache; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import net.minecraft.world.level.Level; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -38,13 +39,13 @@ final class RespawnPacketClientChecks { private int entityId; private final Set completed=new HashSet<>(); private File observations; - private World profileWorld; + 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.GuiScreenEvent.MouseClickedEvent.Pre e){e.setCanceled(true);} @SubscribeEvent public void key(net.minecraftforge.client.event.GuiScreenEvent.KeyboardKeyPressedEvent.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.currentScreen instanceof DeathScreen) { + if(mc.screen instanceof DeathScreen) { int kind=index%8,delay=kind==1?1:kind==2?20:kind==3?60:0; if(!requested&&deathTicks>=delay){ if(Boolean.getBoolean("lootablebodies.respawn.counterCollision")&&kind==2) { @@ -61,89 +62,90 @@ private void respawn(Minecraft mc) { } catch(ReflectiveOperationException error){throw new RuntimeException(error);} observe(mc,"SEEDED_CLIENT_COUNTER_COLLISION next_id="+entityId); } - requested=true;mc.player.respawnPlayer();observe(mc,"REQUEST_RESPAWN without_corpse_barrier delay="+delay); + requested=true;mc.player.respawn();observe(mc,"REQUEST_RESPAWN without_corpse_barrier delay="+delay); } } else if(mc.player.isAlive()){requested=false;deathTicks=0;} } @SubscribeEvent public void tick(TickEvent.ClientTickEvent e) { Minecraft mc=Minecraft.getInstance(); if(e.phase==TickEvent.Phase.START) { - if(mc.world!=null&&mc.world!=profileWorld) { - profileWorld=mc.world; + if(mc.level!=null&&mc.level!=profileWorld) { + profileWorld=mc.level; // IPacket tests use cached synthetic profiles, never real accounts or // live profile-service availability. The local_texture default stays false. - SkullTileEntity.setProfileCache(new PlayerProfileCache(null,new File(mc.gameDir,"fixture-usercache.json")) { - @Override public GameProfile getGameProfileForUsername(String name) { + GameProfileCache fixtureCache=new GameProfileCache(null,new File(mc.gameDirectory,"fixture-usercache.json")) { + @Override public Optional get(String name) { RuntimeChecks.require(name.matches("Container[AB]"),"Unexpected packet fixture profile"); UUID id=UUID.nameUUIDFromBytes(("OfflinePlayer:"+name).getBytes(StandardCharsets.UTF_8)); GameProfile p=new GameProfile(id,name); String texture="{\"timestamp\":0,\"profileId\":\""+id.toString().replace("-","")+"\",\"profileName\":\""+name+"\",\"textures\":{}}"; - p.getProperties().put("textures",new Property("textures",Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8))));return p; + p.getProperties().put("textures",new Property("textures",Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8))));return Optional.of(p); } - }); + }; + fixtureCache.setExecutor(mc::execute);SkullBlockEntity.setProfileCache(fixtureCache); } return; } - if(mc.isGameFocused())mc.mouseHelper.ungrabMouse();net.minecraft.client.settings.KeyBinding.unPressAllKeys(); + if(mc.isWindowActive())mc.mouseHandler.releaseMouse();net.minecraft.client.KeyMapping.releaseAll(); try { - if(finished){mc.shutdown();return;} + if(finished){mc.stop();return;} RuntimeChecks.require(++ticks<20000,"Respawn client total timeout");RuntimeChecks.require(!RespawnPacketChecks.file("abort").exists(),"Respawn runner aborted"); String[] state=new String(Files.readAllBytes(RespawnPacketChecks.file("state").toPath()),StandardCharsets.UTF_8).trim().split(" "); int nextIndex=Integer.parseInt(state[0]),nextPhase=Integer.parseInt(state[1]); if(nextIndex!=index||nextPhase!=phase){index=nextIndex;phase=nextPhase;frames=0;guiFrames=0;System.out.println("RESPAWN_CLIENT_PHASE "+role+" "+index+" "+phase);} 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.currentScreen instanceof MainMenuScreen) { - connecting=true;mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; + if(!connecting&&FixtureConnections.ready(role)&&mc.screen instanceof TitleScreen) { + connecting=true;mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=4; FixtureConnections.connect(mc); } - if(mc.world==null||mc.player==null)return; + if(mc.level==null||mc.player==null)return; if(!traced) { - traced=true;observations=new File(mc.gameDir,"main-packet-observations.txt"); - RespawnPacketTrace.install(mc.getConnection().getNetworkManager(),new File(mc.gameDir,"client-packets.txt"),packet->mc.runAsync(()->{observe(mc,"AFTER "+packet);respawn(mc);})); + traced=true;observations=new File(mc.gameDirectory,"main-packet-observations.txt"); + RespawnPacketTrace.install(mc.getConnection().getConnection(),new File(mc.gameDirectory,"client-packets.txt"),packet->mc.submitAsync(()->{observe(mc,"AFTER "+packet);respawn(mc);})); } - if(mc.currentScreen instanceof DeathScreen)deathTicks++; + if(mc.screen instanceof DeathScreen)deathTicks++; respawn(mc); if(frames%100==0)observe(mc,"TICK phase="+phase+" frame="+frames); RuntimeChecks.require(frames<450,"Client corpse missing case="+index+" phase="+phase+" role="+role); String key=index+"-"+phase;if(completed.contains(key))return; - if(phase==0&&frames>10){mc.player.closeScreen();ack(key);} + if(phase==0&&frames>10){mc.player.closeContainer();ack(key);} else if(phase==1&&frames>20){ if(index%4==1)Thread.sleep(250); // Coalesce an ordinary FIFO packet burst; no packet reordering. ack(key); } else if(phase==3&&frames>20) { File expected=RespawnPacketChecks.file("expected-"+index+"-"+role+".txt");if(!expected.isFile())return; for(String line:Files.readAllLines(expected.toPath(),StandardCharsets.UTF_8)) { - String[] pair=line.split(" ");Entity tracked=mc.world.getEntityByID(Integer.parseInt(pair[0])); - if(!(tracked instanceof EntityLootableBody)||!tracked.getUniqueID().equals(UUID.fromString(pair[1]))||tracked.removed||!FixtureApi.entities(mc.world).contains(tracked))return; + String[] pair=line.split(" ");Entity tracked=mc.level.getEntity(Integer.parseInt(pair[0])); + if(!(tracked instanceof EntityLootableBody)||!tracked.getUUID().equals(UUID.fromString(pair[1]))||tracked.isRemoved()||!FixtureApi.entities(mc.level).contains(tracked))return; } - Entity entity=mc.world.getEntityByID(entityId); - if(!(entity instanceof EntityLootableBody)||!entity.getUniqueID().equals(uuid)||!FixtureApi.entities(mc.world).contains(entity)||entity.removed)return; + Entity entity=mc.level.getEntity(entityId); + if(!(entity instanceof EntityLootableBody)||!entity.getUUID().equals(uuid)||!FixtureApi.entities(mc.level).contains(entity)||entity.isRemoved())return; if(role.equals("A")) { - if(!(mc.currentScreen instanceof CorpseGUIContainer)){mc.playerController.interactWithEntity(mc.player,entity,Hand.MAIN_HAND);return;} - boolean found=false;for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.openContainer.getSlot(slot).getStack(),RuntimeChecks.stack(520+index)))found=true; + if(!(mc.screen instanceof CorpseGUIContainer)){mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND);return;} + boolean found=false;for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(520+index)))found=true; if(!found||++guiFrames<10)return; } observe(mc,"VERIFIED owner_or_observer exact_uuid_loaded_after_respawn"); - if(index%8==0||index%8==7)ScreenShotHelper.saveScreenshot(mc.gameDir,"respawn-case-"+index+".png",mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); + if(index%8==0||index%8==7)Screenshot.grab(mc.gameDirectory,"respawn-case-"+index+".png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); ack(key); } else if(phase==9) { - new File(mc.gameDir,"respawn-client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_RESPAWN_CLIENT_PASS "+role); - mc.player.closeScreen(); + new File(mc.gameDirectory,"respawn-client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_RESPAWN_CLIENT_PASS "+role); + mc.player.closeContainer(); // 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().getNetworkManager().closeChannel(new net.minecraft.util.text.StringTextComponent("Disposable check complete")); + mc.getConnection().getConnection().disconnect(new net.minecraft.network.chat.TextComponent("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.shutdown();} + } 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();} } private void ack(String key) throws Exception {completed.add(key);Files.write(RespawnPacketChecks.file(role+".case-"+key).toPath(),new byte[]{1});} private synchronized void observe(Minecraft mc,String event) { try(FileWriter out=new FileWriter(observations,true)) { - StringBuilder bodies=new StringBuilder();if(mc.world!=null)for(Entity ent:FixtureApi.entities(mc.world))if(ent instanceof EntityLootableBody)bodies.append(ent.getEntityId()).append(':').append(ent.getUniqueID()).append(':').append(ent.removed).append(','); - Entity mapped=mc.world==null?null:mc.world.getEntityByID(entityId); - out.write(System.nanoTime()+" case="+index+" phase="+phase+" "+event+" world="+(mc.world==null?"none":FixtureApi.dimensionId(mc.world))+" player="+(mc.player==null?"none":mc.player.getEntityId())+" mapped="+(mapped==null?null:mapped.getUniqueID())+" loaded="+bodies+"\n"); + StringBuilder bodies=new StringBuilder();if(mc.level!=null)for(Entity ent:FixtureApi.entities(mc.level))if(ent instanceof EntityLootableBody)bodies.append(ent.getId()).append(':').append(ent.getUUID()).append(':').append(ent.isRemoved()).append(','); + Entity mapped=mc.level==null?null:mc.level.getEntity(entityId); + out.write(System.nanoTime()+" case="+index+" phase="+phase+" "+event+" world="+(mc.level==null?"none":FixtureApi.dimensionId(mc.level))+" player="+(mc.player==null?"none":mc.player.getId())+" mapped="+(mapped==null?null:mapped.getUUID())+" loaded="+bodies+"\n"); } catch(IOException error){throw new RuntimeException(error);} } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java index 0d9a971..22deeb6 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java @@ -1,11 +1,13 @@ package cyano.lootable.buildtest; -import net.minecraft.network.play.server.SEntityMetadataPacket; -import net.minecraft.network.play.server.SRespawnPacket; -import net.minecraft.network.play.server.SDestroyEntitiesPacket; -import net.minecraft.network.play.server.SCombatPacket; -import net.minecraft.network.play.server.SChunkDataPacket; -import net.minecraft.network.play.server.SUnloadChunkPacket; -import net.minecraft.util.registry.Registry; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerCombatEnterPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerCombatEndPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket; +import net.minecraft.network.protocol.game.ClientboundLevelChunkPacket; +import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; +import net.minecraft.core.Registry; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -13,13 +15,17 @@ import java.io.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import net.minecraft.network.NetworkManager; -import net.minecraft.network.IPacket; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.*; -import net.minecraft.network.play.client.CClientStatusPacket; -import net.minecraftforge.fml.common.network.ByteBufUtils; -import net.minecraftforge.fml.network.ICustomPacket; +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.ClientboundForgetLevelChunkPacket; +import net.minecraft.network.protocol.game.ClientboundLevelChunkPacket; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; +import net.minecraftforge.fmllegacy.network.ICustomPacket; /** Observes decoded packets without dropping, delaying, mutating or reordering them. */ final class RespawnPacketTrace extends ChannelDuplexHandler { @@ -28,7 +34,7 @@ interface After {void received(String packet);} private final After after; private long sequence; private RespawnPacketTrace(File log,After after){this.log=log;this.after=after;} - static void install(NetworkManager manager,File log,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(); } @@ -49,40 +55,39 @@ static void install(NetworkManager manager,File log,After after) { static String describe(Object packet) { if(packet instanceof ICustomPacket) { ICustomPacket f=(ICustomPacket)packet;if(!f.getName().toString().equals("fml:play"))return null; - PacketBuffer b=new PacketBuffer(f.getInternalData().duplicate());if(b.readableBytes()<6 || b.readVarInt()!=0)return null; - int type=b.readVarInt(),id=b.readInt();net.minecraft.entity.EntityType entityType=net.minecraft.util.registry.Registry.ENTITY_TYPE.getByValue(type); + 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(); return "MOD_SPAWN id="+id+" mod="+name+" type="+type+" uuid="+new UUID(b.readLong(),b.readLong()); } - if(!(packet instanceof SSpawnMobPacket||packet instanceof SRespawnPacket||packet instanceof SDestroyEntitiesPacket||packet instanceof SUnloadChunkPacket - ||packet instanceof SChunkDataPacket||packet instanceof SCombatPacket||packet instanceof SEntityMetadataPacket||packet instanceof CClientStatusPacket))return null; - if(packet instanceof SSpawnMobPacket) { - // Native 1.15 sends metadata separately. Observe only the decoded - // spawn scalars and UUID; never alter the live receiving packet. - int id=net.minecraftforge.fml.common.ObfuscationReflectionHelper.getPrivateValue(SSpawnMobPacket.class,(SSpawnMobPacket)packet,"field_149042_a"); - int type=net.minecraftforge.fml.common.ObfuscationReflectionHelper.getPrivateValue(SSpawnMobPacket.class,(SSpawnMobPacket)packet,"field_149040_b"); - UUID uuid=null; - try {for(java.lang.reflect.Field field:SSpawnMobPacket.class.getDeclaredFields())if(field.getType()==UUID.class) {field.setAccessible(true);uuid=(UUID)field.get(packet);}} - catch(ReflectiveOperationException invalid) {throw new AssertionError("Fixture spawn observation",invalid);} - net.minecraft.entity.EntityType entityType=Registry.ENTITY_TYPE.getByValue(type); - String name=entityType==null?"unknown":entityType.getRegistryName().getNamespace(); - return "MOD_SPAWN id="+id+" mod="+name+" type="+type+" uuid="+uuid; - } - if(packet instanceof SRespawnPacket) { - return NativeRespawnDescription.describe((SRespawnPacket)packet); + if(!(packet instanceof ClientboundAddMobPacket||packet instanceof ClientboundRespawnPacket||packet instanceof ClientboundRemoveEntitiesPacket||packet instanceof ClientboundForgetLevelChunkPacket + ||packet instanceof ClientboundLevelChunkPacket||packet instanceof ClientboundPlayerCombatEnterPacket||packet instanceof ClientboundPlayerCombatEndPacket + ||packet instanceof ClientboundPlayerCombatKillPacket||packet instanceof ClientboundSetEntityDataPacket||packet instanceof ServerboundClientCommandPacket))return null; + if(packet instanceof ClientboundRespawnPacket) { + return NativeRespawnDescription.describe((ClientboundRespawnPacket)packet); } // Vanilla strips receiving-side accessors from a dedicated distribution. // Read an independent serialization, never its live reader index/fields. - PacketBuffer b=new PacketBuffer(Unpooled.buffer()); + FriendlyByteBuf b=new FriendlyByteBuf(Unpooled.buffer()); try { - ((IPacket)packet).writePacketData(b); - if(packet instanceof SDestroyEntitiesPacket){int[] ids=new int[b.readVarInt()];for(int i=0;i)packet).write(b); + if(packet instanceof ClientboundAddMobPacket) { + 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(); + 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 results=new ArrayList<>(); - static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { + static void run(MinecraftServer server,ServerLevel target,File folder) throws Exception { world=target;failures=0;results.clear(); - INBT[] wrong={StringNBT.valueOf("opaque"),new CompoundNBT(),new ByteArrayNBT(new byte[]{1,2}),new IntArrayNBT(new int[]{1,2}),new ListNBT()}; - for(int i=0;ibadFall(raw));} - for(int i=0;ibadRotation(raw));} - for(INBT raw:new INBT[]{StringNBT.valueOf("opaque"),new CompoundNBT(),new ByteArrayNBT(new byte[]{1})})check("Rotation_wrong_element_type_"+raw.getId(),()->{ListNBT l=new ListNBT();l.add(raw.copy());l.add(raw.copy());badRotation(l);}); + Tag[] wrong={StringTag.valueOf("opaque"),new CompoundTag(),new ByteArrayTag(new byte[]{1,2}),new IntArrayTag(new int[]{1,2}),new ListTag()}; + for(int i=0;ibadFall(raw));} + for(int i=0;ibadRotation(raw));} + for(Tag raw:new Tag[]{StringTag.valueOf("opaque"),new CompoundTag(),new ByteArrayTag(new byte[]{1})})check("Rotation_wrong_element_type_"+raw.getId(),()->{ListTag l=new ListTag();l.add(raw.copy());l.add(raw.copy());badRotation(l);}); for(int count:new int[]{0,1,3})check("Rotation_wrong_length_"+count,()->length(count)); for(int axis=0;axis<2;axis++)for(double value:new double[]{Double.NaN,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY,Double.MAX_VALUE,-Double.MAX_VALUE,1e-300,-1e-300}){final int a=axis;check("Rotation_bad_component_"+axis+"_"+value,()->component(a,value));} - for(INBT raw:new INBT[]{FloatNBT.valueOf(Float.NaN),FloatNBT.valueOf(Float.NEGATIVE_INFINITY),FloatNBT.valueOf(Float.POSITIVE_INFINITY),DoubleNBT.valueOf(Double.NaN),DoubleNBT.valueOf(Double.NEGATIVE_INFINITY),DoubleNBT.valueOf(Double.POSITIVE_INFINITY),IntNBT.valueOf(-1),FloatNBT.valueOf(-.25F),DoubleNBT.valueOf(-Double.MAX_VALUE),DoubleNBT.valueOf(Double.MAX_VALUE),DoubleNBT.valueOf(1e-300)})check("FallDistance_invalid_"+raw,()->badFall(raw)); + for(Tag raw:new Tag[]{FloatTag.valueOf(Float.NaN),FloatTag.valueOf(Float.NEGATIVE_INFINITY),FloatTag.valueOf(Float.POSITIVE_INFINITY),DoubleTag.valueOf(Double.NaN),DoubleTag.valueOf(Double.NEGATIVE_INFINITY),DoubleTag.valueOf(Double.POSITIVE_INFINITY),IntTag.valueOf(-1),FloatTag.valueOf(-.25F),DoubleTag.valueOf(-Double.MAX_VALUE),DoubleTag.valueOf(Double.MAX_VALUE),DoubleTag.valueOf(1e-300)})check("FallDistance_invalid_"+raw,()->badFall(raw)); for(int type=0;type<6;type++){final int t=type;check("valid_rotation_numeric_type_"+type,()->numericRotation(t));check("valid_fall_numeric_type_"+type,()->numericFall(t));} for(float yaw:new float[]{0F,-0F,37.25F,-37.25F,359.75F,-359.75F,360F,-360F,720.25F,-720.25F,Float.MAX_VALUE,-Float.MAX_VALUE})check("valid_yaw_orientation_and_derived_angles_"+Float.floatToIntBits(yaw),()->validYaw(yaw)); for(float pitch:new float[]{-90,90,720,-720,Float.MAX_VALUE,-Float.MAX_VALUE})check("valid_pitch_keeps_existing_flat_corpse_behavior_"+pitch,()->validPitch(pitch)); - for(float distance:new float[]{0F,-0F,Float.MIN_VALUE,12.5F,Float.MAX_VALUE})check("valid_representable_fall_distance_"+Float.floatToIntBits(distance),()->validFall(FloatNBT.valueOf(distance),distance)); + for(float distance:new float[]{0F,-0F,Float.MIN_VALUE,12.5F,Float.MAX_VALUE})check("valid_representable_fall_distance_"+Float.floatToIntBits(distance),()->validFall(FloatTag.valueOf(distance),distance)); check("missing_rotation_and_fall_default_to_zero",RotationFallChecks::missing); check("repeated_read_resets_previous_head_body_and_fall_state",RotationFallChecks::repeated); check("healthy_saved_layout_and_defaults_unchanged",RotationFallChecks::healthy); @@ -61,33 +73,33 @@ static void run(MinecraftServer server,ServerWorld target,File folder) throws Ex } private static void check(String name,Check test){boolean env=LootableBodies.hurtByEnvironment,inv=LootableBodies.completelyInvulnerable;try{test.run();results.add("PASS "+name);System.out.println("ROTATION_FALL_PASS "+name);}catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("ROTATION_FALL_FAIL "+name+": "+e);e.printStackTrace();}finally{LootableBodies.hurtByEnvironment=env;LootableBodies.completelyInvulnerable=inv;}} private static void require(boolean c,String why){RuntimeChecks.require(c,why);} - private static CompoundNBT saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} - private static CompoundNBT base(){EntityLootableBody b=new EntityLootableBody(world);b.setPosition(160.5,80,160.5);b.setNoGravity(true);b.setUserName("RotationFixture");b.setRotation(37.25F);b.fallDistance=12.5F;for(int i=0;i<48;i++)b.setInventorySlotContents(i,RuntimeChecks.stack(i));b.addItem(RuntimeChecks.stack(70));b.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(b);return saved(b);} - private static EntityLootableBody read(CompoundNBT n){CompoundNBT before=n.copy();Entity e=FixtureApi.loadEntity(n,world);require(e instanceof EntityLootableBody,"Factory discarded corpse");require(NameLifetimeChecks.same(before,n),"Source NBT mutated");return (EntityLootableBody)e;} - static ListNBT rotation(float... angles){ListNBT l=new ListNBT();for(float a:angles)l.add(FloatNBT.valueOf(a));return l;} - static ListNBT wideRotation(double... angles){ListNBT l=new ListNBT();for(double a:angles)l.add(DoubleNBT.valueOf(a));return l;} - static void finite(EntityLootableBody b){for(float v:new float[]{b.rotationYaw,b.rotationPitch,b.prevRotationYaw,b.prevRotationPitch,b.rotationYawHead,b.prevRotationYawHead,b.renderYawOffset,b.prevRenderYawOffset,b.fallDistance})require(Float.isFinite(v),"Nonfinite rotation/derived angle/fall distance");require(b.fallDistance>=0,"Negative fall distance");} + private static CompoundTag saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} + private static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setUserName("RotationFixture");b.setRotation(37.25F);b.fallDistance=12.5F;for(int i=0;i<48;i++)b.setItem(i,RuntimeChecks.stack(i));b.addItem(RuntimeChecks.stack(70));b.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(b);return saved(b);} + private static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();Entity e=FixtureApi.loadEntity(n,world);require(e instanceof EntityLootableBody,"Factory discarded corpse");require(NameLifetimeChecks.same(before,n),"Source NBT mutated");return (EntityLootableBody)e;} + static ListTag rotation(float... angles){ListTag l=new ListTag();for(float a:angles)l.add(FloatTag.valueOf(a));return l;} + static ListTag wideRotation(double... angles){ListTag l=new ListTag();for(double a:angles)l.add(DoubleTag.valueOf(a));return l;} + static void finite(EntityLootableBody b){for(float v:new float[]{b.getYRot(),b.getXRot(),b.yRotO,b.xRotO,b.yHeadRot,b.yHeadRotO,b.yBodyRot,b.yBodyRotO,b.fallDistance})require(Float.isFinite(v),"Nonfinite rotation/derived angle/fall distance");require(b.fallDistance>=0,"Negative fall distance");} private static void value(float a,float z,String why){require(Float.isFinite(a) && a==z,why+" expected="+z+" actual="+a);} - private static void angles(EntityLootableBody b,float yaw){finite(b);for(float v:new float[]{b.rotationYaw,b.prevRotationYaw,b.rotationYawHead,b.prevRotationYawHead,b.renderYawOffset,b.prevRenderYawOffset})value(v,yaw,"Yaw/derived orientation differs");value(b.rotationPitch,0,"Corpse pitch no longer flat");value(b.prevRotationPitch,0,"Previous pitch differs");} - private static void kept(EntityLootableBody b){CompoundNBT n=base(),a=saved(b);for(String k:new String[]{"Equipment","Aux"})require(NameLifetimeChecks.same(n.get(k),a.get(k)),"Complete inventory changed "+k);require(b.getSizeInventory()==48 && b.getCustomNameTag().equals("RotationFixture"),"Corpse identity/slots changed");} - private static void stable(EntityLootableBody b){finite(b);kept(b);CompoundNBT a=saved(b),z=saved(read(a));for(String k:new String[]{"Rotation","FallDistance","Pos","Motion","Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);} - private static void badFall(INBT raw){CompoundNBT n=base();n.put("FallDistance",raw.copy());EntityLootableBody b=read(n);value(b.fallDistance,0,"Bad fall distance activated");NameLifetimeChecks.retained(b,"FallDistance",raw);stable(b);} - private static void badRotation(INBT raw){CompoundNBT n=base();n.put("Rotation",raw.copy());EntityLootableBody b=read(n);angles(b,0);NameLifetimeChecks.retained(b,"Rotation",raw);stable(b);} - private static void component(int axis,double raw){CompoundNBT n=base();ListNBT r=axis==0?wideRotation(raw,17):wideRotation(37.25,raw);n.put("Rotation",r);EntityLootableBody b=read(n);angles(b,axis==0?0:37.25F);NameLifetimeChecks.retained(b,"Rotation",r);stable(b);} - private static void length(int count){float[] a=new float[count];for(int i=0;i0 && b.getHealth()<20:b.getHealth()==20,"Landing changed configured damage behavior");kept(b);} - static CompoundNBT malformedFields(){CompoundNBT n=new CompoundNBT();n.put("Rotation",wideRotation(37,Double.NaN));n.putDouble("FallDistance",Double.NaN);return n;} - static void verifyWorldFields(EntityLootableBody b){finite(b);value(b.rotationYaw,37,"Valid disk yaw lost");value(b.rotationPitch,0,"Disk corpse pitch changed");value(b.fallDistance,0,"Disk fall distance unsafe");CompoundNBT raw=malformedFields();NameLifetimeChecks.retained(b,"Rotation",raw.get("Rotation"));NameLifetimeChecks.retained(b,"FallDistance",raw.get("FallDistance"));} + private static void chunkAdmission(){CompoundTag n=base();n.put("Pos",PositionMotionChecks.vector(Double.NaN,80,160.5));n.put("Rotation",wideRotation(37.25,Double.NaN));n.putDouble("FallDistance",Double.NaN);CompoundTag before=n.copy();Entity e=FixtureApi.readChunkEntity(n,world,RuntimeChecks.emptyChunk(world,10,10));require(e instanceof EntityLootableBody && !e.isRemoved(),"Chunk lost malformed corpse");EntityLootableBody b=(EntityLootableBody)e;require(b.getX()==168.5 && b.getY()==80 && b.getZ()==160.5,"Position recovery changed");angles(b,37.25F);value(b.fallDistance,0,"Bad distance at admission");require(NameLifetimeChecks.same(before,n),"Chunk mutated raw input");stable(b);} + private static void zeroHealth(){CompoundTag n=base();n.put("Rotation",rotation(Float.NaN,Float.NaN));n.putDouble("FallDistance",Double.NaN);n.putFloat("Health",0);EntityLootableBody b=read(n);require(b.getHealth()==0 && !b.isAlive(),"Rotation/fall repair healed zero health");angles(b,0);kept(b);} + private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);CompoundTag fields=malformedFields();for(String k:fields.getAllKeys())n.put(k,fields.get(k).copy());EntityLootableBody b=read(n);require(b.isAlive(),"Bad fields made inventory inaccessible");verifyWorldFields(b);NbtRecoveryChecks.retained(b,raw);stable(b);NbtRecoveryChecks.retained(read(saved(b)),raw);} + private static void empty(){CompoundTag n=base();n.put("Equipment",new ListTag());n.remove("Aux");n.putString("Rotation","opaque");EntityLootableBody b=read(n);require(b.isEmpty(),"Rotation evidence counts as items");NameLifetimeChecks.retained(b,"Rotation",n.get("Rotation"));} + private static void landing(boolean damage,boolean bad){LootableBodies.hurtByEnvironment=damage;LootableBodies.completelyInvulnerable=!damage;world.getChunk(10,10);world.setBlockAndUpdate(new BlockPos(160,79,160),Blocks.STONE.defaultBlockState());CompoundTag n=base();if(bad)n.putDouble("FallDistance",Double.NaN);EntityLootableBody b=read(n);if(bad)value(b.fallDistance,0,"Malformed distance entered landing");b.move(net.minecraft.world.entity.MoverType.SELF,new net.minecraft.world.phys.Vec3(0,-1,0));value(b.fallDistance,0,"Landing failed to reset distance");require(damage && !bad?b.getHealth()>0 && b.getHealth()<20:b.getHealth()==20,"Landing changed configured damage behavior");kept(b);} + static CompoundTag malformedFields(){CompoundTag n=new CompoundTag();n.put("Rotation",wideRotation(37,Double.NaN));n.putDouble("FallDistance",Double.NaN);return n;} + static void verifyWorldFields(EntityLootableBody b){finite(b);value(b.getYRot(),37,"Valid disk yaw lost");value(b.getXRot(),0,"Disk corpse pitch changed");value(b.fallDistance,0,"Disk fall distance unsafe");CompoundTag raw=malformedFields();NameLifetimeChecks.retained(b,"Rotation",raw.get("Rotation"));NameLifetimeChecks.retained(b,"FallDistance",raw.get("FallDistance"));} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java index 79dba35..3265b79 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java @@ -1,11 +1,11 @@ package cyano.lootable.buildtest; -import net.minecraft.world.biome.Biomes; -import net.minecraft.enchantment.Enchantments; -import net.minecraft.network.play.ServerPlayNetHandler; -import net.minecraft.util.text.StringTextComponent; -import net.minecraft.nbt.LongArrayNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.nbt.LongArrayTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; import com.mojang.authlib.GameProfile; import cyano.lootable.LootableBodies; @@ -14,20 +14,23 @@ import cyano.lootable.graphics.GUIHandler; import java.io.*; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.item.Items; -import net.minecraft.inventory.EquipmentSlotType; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.Items; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongArrayTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.DamageSource; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.common.util.FakePlayerFactory; import net.minecraftforge.fml.common.*; -import net.minecraftforge.fml.event.server.*; +import net.minecraftforge.fmlserverevents.FMLServerStartedEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; /** Build-only verifier. Never included in the production source set or jars. */ @@ -38,10 +41,12 @@ public final class RuntimeChecks { static final UUID DEATH = UUID.fromString("00000000-0000-4000-8000-000000000103"); public static volatile Throwable failure; private static boolean defaultsChecked; + private static UUID pendingDeathId; private boolean rejectedFirstCorpseOpen; public RuntimeChecks() { MultiplayerChecks.registerChannel(); net.minecraftforge.eventbus.api.IEventBus bus=net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus(); + NativePreflightChecks.install(bus); bus.register(new TestRegistries());FixtureItems.register(bus); net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); net.minecraftforge.fml.DistExecutor.runWhenOn(net.minecraftforge.api.distmarker.Dist.CLIENT,()->()->{ @@ -50,21 +55,23 @@ public RuntimeChecks() { } @SubscribeEvent public void rejectFirstCorpseOpen(net.minecraftforge.event.entity.player.PlayerInteractEvent.EntityInteract event) { if(Boolean.getBoolean("lootablebodies.rejectFirstCorpseOpen") && !rejectedFirstCorpseOpen - && !event.getWorld().isRemote && event.getTarget() instanceof EntityLootableBody) { + && !event.getWorld().isClientSide && event.getTarget() instanceof EntityLootableBody) { rejectedFirstCorpseOpen=true; event.setCanceled(true); System.out.println("CORPSE_GUI_FIRST_INTERACTION_REJECTED_CONTROL"); } } @SubscribeEvent(priority=net.minecraftforge.eventbus.api.EventPriority.LOWEST) public void fixtureAdmission(net.minecraftforge.event.entity.EntityJoinWorldEvent event) { - if(Boolean.getBoolean("lootablebodies.assertFixtureAdmission") && event.getEntity() instanceof EntityLootableBody && !event.getWorld().isRemote && !event.getEntity().addedToChunk) { + if(pendingDeathId!=null && event.getEntity() instanceof EntityLootableBody && !event.getWorld().isClientSide + && "BuildDeath".equals(((EntityLootableBody)event.getEntity()).getCustomNameTag()))event.getEntity().setUUID(pendingDeathId); + if(Boolean.getBoolean("lootablebodies.assertFixtureAdmission") && event.getEntity() instanceof EntityLootableBody && !event.getWorld().isClientSide && !event.getEntity().isAddedToWorld()) { EntityLootableBody body=(EntityLootableBody)event.getEntity(); - require(event.getWorld().getChunkProvider().chunkExists(net.minecraft.util.math.MathHelper.floor(body.getPosX())>>4,net.minecraft.util.math.MathHelper.floor(body.getPosZ())>>4),"Manual corpse fixture spawned before chunk admission: "+body.getPosition()+" name="+body.getCustomNameTag()); + require(event.getWorld().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(FMLServerStartedEvent event) { if ((net.minecraftforge.fml.loading.FMLEnvironment.dist==net.minecraftforge.api.distmarker.Dist.CLIENT)) return; - MinecraftServer server = net.minecraftforge.fml.server.ServerLifecycleHooks.getCurrentServer(); + MinecraftServer server = net.minecraftforge.fmllegacy.server.ServerLifecycleHooks.getCurrentServer(); if(Boolean.getBoolean("lootablebodies.respawn")) { try{new RespawnPacketChecks(server);}catch(Exception error){throw new RuntimeException(error);}return; } @@ -79,20 +86,20 @@ public RuntimeChecks() { } catch (Throwable error) { failure = error; error.printStackTrace(); - } finally { server.initiateShutdown(false); } + } finally { server.halt(false); } } static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); } static boolean sameStack(ItemStack first,ItemStack second) { - return ItemStack.areItemStacksEqual(first==null?ItemStack.EMPTY:first,second==null?ItemStack.EMPTY:second); + return ItemStack.matches(first==null?ItemStack.EMPTY:first,second==null?ItemStack.EMPTY:second); } public static void run(MinecraftServer server, String phase) throws Exception { - ServerWorld world = server.getWorld(FixtureApi.dimensionKey(0)); - File folder = world.getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile(); - require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.7.116051"), "identity/version"); + 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.8.117011"), "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.7.116051") : "2.4.7.116051"), "loaded release version"); + System.getProperty("lootablebodies.expectedIssueVersion","2.4.8.117011") : "2.4.8.117011"), "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 && @@ -104,7 +111,9 @@ public static void run(MinecraftServer server, String phase) throws Exception { // Freeze timers only in this verifier while comparing exact inventories. LootableBodies.allowCorpseDecay = false; LootableBodies.ticksPerItemDecay = -1; - if(phase.equals("upgrade-contracts")) { + if(phase.equals("preflight-conversion")) { + NativePreflightChecks.verify(server); + } else if(phase.equals("upgrade-contracts")) { UpgradeChecks.run(server); } else if(phase.startsWith("upgrade-")) { WorldUpgradeChecks.run(server,phase); @@ -170,13 +179,13 @@ public static void run(MinecraftServer server, String phase) throws Exception { } else if (phase.equals("reload") || phase.equals("deferred")) { if (phase.equals("reload") && new File(folder,"wall-escape-checks.txt").isFile()) WallEscapeChecks.reload(world,folder); if(new File(folder,"issue17-chunks.txt").isFile()) ReportedIssueChecks.coldReload(server,world,folder); - world.getChunk(0, 0); + awaitChunk(world,0,0); EntityLootableBody near = find(world, NEAR); verifyExpected(folder, "near", near); - world.getChunk(2, 0); + awaitChunk(world,2,0); verifyExpected(folder, "death", find(world, DEATH)); if(new File(folder,"rejected-release.nbt").isFile()) { - world.getChunk(3,0); + awaitChunk(world,3,0); EntityLootableBody retained=find(world,UUID.fromString("00000000-0000-4000-8000-000000000105")); require(retained.isAlive(),"Blocked-release owner did not survive reload"); // The saved profile is recreated lazily on the first entity tick. @@ -184,98 +193,103 @@ public static void run(MinecraftServer server, String phase) throws Exception { verifyExpected(folder,"rejected-release",retained); } if (phase.equals("deferred")) { - world.getChunk(256, 256); + awaitChunk(world,256,256); verifyExpected(folder, "far", find(world, FAR)); } else { - require(!world.getChunkProvider().chunkExists(256, 256), "deferred chunk opened early"); + require(!world.getChunkSource().hasChunk(256, 256), "deferred chunk opened early"); } } else throw new AssertionError("Unknown phase " + phase); - server.save(false,true,false); + server.saveAllChunks(false,true,false); File marker = new File(folder, phase + ".pass"); try (FileWriter output = new FileWriter(marker)) { output.write("corpse-inventory-preserved\n"); } System.out.println("LOOTABLE_BODIES_RUNTIME_PASS phase=" + phase); } - static net.minecraft.world.chunk.Chunk emptyChunk(ServerWorld world,int x,int z) { - net.minecraft.util.registry.Registry registry=world.func_241828_r().getRegistry(net.minecraft.util.registry.Registry.BIOME_KEY); - net.minecraft.world.biome.Biome[] biomes=new net.minecraft.world.biome.Biome[1024];java.util.Arrays.fill(biomes,registry.getOrThrow(net.minecraft.world.biome.Biomes.PLAINS));return new net.minecraft.world.chunk.Chunk(world,new net.minecraft.util.math.ChunkPos(x,z),new net.minecraft.world.biome.BiomeContainer(registry,biomes)); + static net.minecraft.world.level.chunk.LevelChunk emptyChunk(ServerLevel world,int x,int z) { + net.minecraft.core.Registry registry=world.registryAccess().registryOrThrow(net.minecraft.core.Registry.BIOME_REGISTRY); + int[] biomes=new int[16*((world.getHeight()+3)/4)];java.util.Arrays.fill(biomes,registry.getId(registry.getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS))); + return new net.minecraft.world.level.chunk.LevelChunk(world,new net.minecraft.world.level.ChunkPos(x,z),new net.minecraft.world.level.chunk.ChunkBiomeContainer(registry,world,biomes)); } static ItemStack stack(int index) { - ItemStack stack=index<4?new ItemStack(new net.minecraft.item.Item[]{Items.DIAMOND_HELMET,Items.DIAMOND_CHESTPLATE,Items.DIAMOND_LEGGINGS,Items.DIAMOND_BOOTS}[index]):index==4?new ItemStack(Items.DIAMOND_SWORD):index==5?new ItemStack(Items.SHIELD):new ItemStack(Items.LAPIS_LAZULI,1+index%32); - CompoundNBT custom = new CompoundNBT(); + ItemStack stack=index<4?new ItemStack(new net.minecraft.world.item.Item[]{Items.DIAMOND_HELMET,Items.DIAMOND_CHESTPLATE,Items.DIAMOND_LEGGINGS,Items.DIAMOND_BOOTS}[index]):index==4?new ItemStack(Items.DIAMOND_SWORD):index==5?new ItemStack(Items.SHIELD):new ItemStack(Items.LAPIS_LAZULI,1+index%32); + CompoundTag custom = new CompoundTag(); custom.putString("fixture", "slot-" + index); - custom.put("typed_longs",new LongArrayNBT(new long[]{Long.MIN_VALUE,index,Long.MAX_VALUE})); - CompoundNBT nested = new CompoundNBT(); nested.putInt("unchanged", index * 17); - ListNBT contents = new ListNBT(); - CompoundNBT item = new CompoundNBT(); new ItemStack(Items.DIAMOND,3).write(item); contents.add(item); + custom.put("typed_longs",new LongArrayTag(new long[]{Long.MIN_VALUE,index,Long.MAX_VALUE})); + CompoundTag nested = new CompoundTag(); nested.putInt("unchanged", index * 17); + 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.setDamage(index<4?index+2:index==4?17:9); stack.setDisplayName(new net.minecraft.util.text.StringTextComponent("Fixture " + index)); - if (index == 4) stack.addEnchantment(net.minecraft.enchantment.Enchantments.UNBREAKING, 2); + 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)); + if (index == 4) stack.enchant(net.minecraft.world.item.enchantment.Enchantments.UNBREAKING, 2); return stack; } - static net.minecraft.world.chunk.Chunk awaitChunk(ServerWorld world,int x,int z) { - net.minecraft.world.chunk.Chunk admitted=(net.minecraft.world.chunk.Chunk)world.getChunkProvider().getChunk(x,z,net.minecraft.world.chunk.ChunkStatus.FULL,true); - require(world.getChunkProvider().chunkExists(x,z),"Fixture must await native chunk admission before spawning a corpse"); + static net.minecraft.world.level.chunk.LevelChunk awaitChunk(ServerLevel world,int x,int z) { + net.minecraft.world.level.chunk.LevelChunk admitted=(net.minecraft.world.level.chunk.LevelChunk)world.getChunkSource().getChunk(x,z,net.minecraft.world.level.chunk.ChunkStatus.FULL,true); + require(world.getChunkSource().hasChunk(x,z),"Fixture must await native chunk admission before spawning a corpse"); + FixtureApi.awaitEntityInbox(world,x,z); return admitted; } - static EntityLootableBody fixture(ServerWorld world, UUID uuid, int coordinate, boolean full) { + static EntityLootableBody fixture(ServerLevel world, UUID uuid, int coordinate, boolean full) { awaitChunk(world,coordinate >> 4,coordinate >> 4); EntityLootableBody body = new EntityLootableBody(world); - body.setUniqueId(uuid); body.setPosition(coordinate+0.5, 80, coordinate+0.5); + body.setUUID(uuid); body.setPos(coordinate+0.5, 80, coordinate+0.5); body.setNoGravity(true); body.setUserName("BuildFixture"); body.setRotation(37); - require(body.getSizeInventory() == 48, "48 inventory slots"); - for (int i=0; i<48; i++) if (full || i % 7 == 0 || i == 47) body.setInventorySlotContents(i,stack(i)); + require(body.getContainerSize() == 48, "48 inventory slots"); + for (int i=0; i<48; i++) if (full || i % 7 == 0 || i == 47) body.setItem(i,stack(i)); if (full) { body.addItem(stack(70)); body.addItem(stack(71)); } disableVacuum(body); - require(world.addEntity(body), "fixture spawn"); + require(world.addFreshEntity(body), "fixture spawn"); + require(body.isAddedToWorld(),"Fresh fixture missed Forge's admission callback"); return body; } - static void verifyRoundTrip(ServerWorld world, EntityLootableBody body) { - CompoundNBT before = new CompoundNBT(); body.writeUnlessPassenger(before); + static void verifyRoundTrip(ServerLevel world, EntityLootableBody body) { + CompoundTag before = new CompoundTag(); body.saveAsPassenger(before); require(before.getString("id").equals("lootablebodies:corpse"), "persistent entity name: " + before.getString("id")); Entity copy = FixtureApi.loadEntity(before.copy(), world); require(copy instanceof EntityLootableBody, "NBT factory registration"); - CompoundNBT after = new CompoundNBT(); copy.writeUnlessPassenger(after); + CompoundTag after = new CompoundTag(); copy.saveAsPassenger(after); compare(before,after); - if (body.getUniqueID().equals(NEAR)) { + if (body.getUUID().equals(NEAR)) { require(before.getList("Equipment",10).size()==48, "all indexed stacks"); require(before.getList("Aux",10).size()==2, "ordered overflow"); } } - static void death(MinecraftServer server, ServerWorld world, File folder) throws Exception { - ServerPlayerEntity player = new ServerPlayerEntity(server, world, - new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000104"),"BuildDeath"), - new net.minecraft.server.management.PlayerInteractionManager(world)); - player.connection = new net.minecraft.network.play.ServerPlayNetHandler(server, - new net.minecraft.network.NetworkManager(net.minecraft.network.PacketDirection.SERVERBOUND), player); - player.setPosition(32.5,80,0.5); awaitChunk(world,2,0); require(world.getChunkProvider().chunkExists(2,0),"Death fixture must await native chunk admission before spawning its corpse"); - world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(false,null); - player.inventory.mainInventory.set(0,stack(4)); - player.inventory.mainInventory.set(9,stack(19)); - player.setItemStackToSlot(EquipmentSlotType.HEAD,stack(0)); - player.setItemStackToSlot(EquipmentSlotType.OFFHAND,stack(5)); - player.onDeath(DamageSource.OUT_OF_WORLD); + static void death(MinecraftServer server, ServerLevel world, File folder) throws Exception { + ServerPlayer player = new ServerPlayer(server, world, + new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000104"),"BuildDeath")); + player.connection = new net.minecraft.server.network.ServerGamePacketListenerImpl(server, + new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND), player); + player.setPos(32.5,80,0.5); awaitChunk(world,2,0); require(world.getChunkSource().hasChunk(2,0),"Death fixture must await native chunk admission before spawning its corpse"); + world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); + player.getInventory().items.set(0,stack(4)); + player.getInventory().items.set(9,stack(19)); + player.setItemSlot(EquipmentSlot.HEAD,stack(0)); + player.setItemSlot(EquipmentSlot.OFFHAND,stack(5)); + // 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;} EntityLootableBody corpse = null; for (Entity entity : new ArrayList(FixtureApi.entities(world))) if (entity instanceof EntityLootableBody && "BuildDeath".equals(((EntityLootableBody)entity).getCustomNameTag())) corpse=(EntityLootableBody)entity; require(corpse != null, "actual player death created corpse"); - require(ItemStack.areItemStacksEqual(corpse.getStackInSlot(0),stack(0)),"death helmet"); - require(ItemStack.areItemStacksEqual(corpse.getStackInSlot(4),stack(4)),"death main hand"); - require(ItemStack.areItemStacksEqual(corpse.getStackInSlot(5),stack(5)),"death offhand"); - require(ItemStack.areItemStacksEqual(corpse.getStackInSlot(6),stack(19)),"death inventory custom NBT"); - Object gui = GUIHandler.getInstance().getServerGuiElement(0,player,world,corpse.getEntityId(),0,0); + require(ItemStack.matches(corpse.getItem(0),stack(0)),"death helmet"); + require(ItemStack.matches(corpse.getItem(4),stack(4)),"death main hand"); + require(ItemStack.matches(corpse.getItem(5),stack(5)),"death offhand"); + require(ItemStack.matches(corpse.getItem(6),stack(19)),"death inventory custom NBT"); + Object gui = GUIHandler.getInstance().getServerGuiElement(0,player,world,corpse.getId(),0,0); require(gui instanceof CorpseContainer,"server inventory GUI"); CorpseContainer container = (CorpseContainer)gui; - require(container.inventorySlots.size()==84 && container.canInteractWith(player),"container slots/access"); - ItemStack recovered = container.transferStackInSlot(player,6); - require(ItemStack.areItemStacksEqual(recovered,stack(19)),"shift-click recovery"); - require((corpse.getStackInSlot(6)).isEmpty(),"no duplicate in corpse"); - corpse.setUniqueId(DEATH); corpse.setNoGravity(true); - corpse.setMotion(0,corpse.getMotion().y,corpse.getMotion().z); corpse.setMotion(corpse.getMotion().x,0,corpse.getMotion().z); corpse.setMotion(corpse.getMotion().x,corpse.getMotion().y,0); + require(container.slots.size()==84 && container.stillValid(player),"container slots/access"); + ItemStack recovered = container.quickMoveStack(player,6); + require(ItemStack.matches(recovered,stack(19)),"shift-click recovery"); + require((corpse.getItem(6)).isEmpty(),"no duplicate in corpse"); + require(corpse.getUUID().equals(DEATH) && world.getEntity(DEATH)==corpse,"Death fixture UUID was not indexed before admission");corpse.setNoGravity(true); + corpse.setDeltaMovement(0,corpse.getDeltaMovement().y,corpse.getDeltaMovement().z); corpse.setDeltaMovement(corpse.getDeltaMovement().x,0,corpse.getDeltaMovement().z); corpse.setDeltaMovement(corpse.getDeltaMovement().x,corpse.getDeltaMovement().y,0); disableVacuum(corpse); saveExpected(folder,"death",corpse); } - static EntityLootableBody find(ServerWorld world, UUID uuid) { - for(Entity entity : FixtureApi.entities(world)) if(entity.getUniqueID().equals(uuid)) return (EntityLootableBody)entity; + static EntityLootableBody find(ServerLevel world, UUID uuid) { + for(Entity entity : FixtureApi.entities(world)) if(entity.getUUID().equals(uuid)) return (EntityLootableBody)entity; throw new AssertionError("Saved corpse missing " + uuid); } static void disableVacuum(EntityLootableBody body) { @@ -285,15 +299,15 @@ static void disableVacuum(EntityLootableBody body) { } catch (Exception error) { throw new AssertionError(error); } } static void saveExpected(File folder,String name,EntityLootableBody body) throws Exception { - CompoundNBT data = new CompoundNBT(); body.writeUnlessPassenger(data); - CompressedStreamTools.writeCompressed(data,new FileOutputStream(new File(folder,name+".nbt"))); + CompoundTag data = new CompoundTag(); body.saveAsPassenger(data); + NbtIo.writeCompressed(data,new FileOutputStream(new File(folder,name+".nbt"))); } static void verifyExpected(File folder,String name,EntityLootableBody body) throws Exception { - CompoundNBT expected = CompressedStreamTools.readCompressed(new FileInputStream(new File(folder,name+".nbt"))); - CompoundNBT actual = new CompoundNBT(); body.writeUnlessPassenger(actual); compare(expected,actual); + CompoundTag expected = NbtIo.readCompressed(new FileInputStream(new File(folder,name+".nbt"))); + CompoundTag actual = new CompoundTag(); body.saveAsPassenger(actual); compare(expected,actual); } - static void compare(CompoundNBT expected,CompoundNBT actual) { - for(String key : Arrays.asList("id","UUIDMost","UUIDLeast","Pos","Rotation","CustomName","Equipment","Aux","DeathTime","DecayTime")) + static void compare(CompoundTag expected,CompoundTag actual) { + for(String key : Arrays.asList("id","UUID","UUIDMost","UUIDLeast","Pos","Rotation","CustomName","Equipment","Aux","DeathTime","DecayTime")) require(Objects.equals(expected.get(key),actual.get(key)),"Saved field differs " + key + ": " + expected.get(key) + " != " + actual.get(key)); if(expected.contains("Name")) require(expected.getString("Name").equals(actual.getString("Name")),"profile name"); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java b/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java index ffc8022..4531369 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java @@ -1,12 +1,12 @@ package cyano.lootable.buildtest; -import net.minecraft.item.Item; -import net.minecraft.potion.Effect; +import net.minecraft.world.item.Item; +import net.minecraft.world.effect.MobEffect; import net.minecraftforge.event.RegistryEvent; 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 potions(RegistryEvent.Register event) {EffectHurtChecks.registerPotion();} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java index 7b9da02..cd55725 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java @@ -1,25 +1,31 @@ package cyano.lootable.buildtest; -import net.minecraft.item.Items; -import net.minecraft.inventory.container.Slot; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.StringNBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.IntNBT; -import net.minecraft.nbt.DoubleNBT; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.world.item.Items; +import net.minecraft.world.inventory.Slot; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.CompoundTag; import java.io.*; import java.lang.reflect.*; import java.nio.file.*; import java.util.*; import java.util.zip.DeflaterOutputStream; -import net.minecraft.entity.Entity; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.datafix.TypeReferences; +import net.minecraft.util.datafix.fixes.References; import cyano.lootable.upgrade.UpgradePreservation; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerLevel; import cyano.lootable.entities.EntityLootableBody; @@ -27,35 +33,35 @@ final class UpgradeChecks { static void registerOwnFixer() { NativeVanillaDataFixer.get(); // Capture the independent vanilla reference first. - final com.mojang.datafixers.DataFixer nativeFixer=net.minecraft.util.datafix.DataFixesManager.getDataFixer(); + final com.mojang.datafixers.DataFixer nativeFixer=net.minecraft.util.datafix.DataFixers.getDataFixer(); UpgradePreservation.register(new com.mojang.datafixers.DataFixer() { public com.mojang.datafixers.schemas.Schema getSchema(int key){return nativeFixer.getSchema(key);} public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic input,int source,int target) { com.mojang.serialization.Dynamic result=nativeFixer.update(type,input,source,target); - if(type.typeName().equals("item_stack") && sourceconversion("lootablebodies.Corpse",IntNBT.valueOf(512))); - check("native_111_identity_survives",()->conversion("lootablebodies:corpse",IntNBT.valueOf(922))); + failures=0;sequence=0;world=server.getLevel(FixtureApi.dimensionKey(0));registerOwnFixer(); + output=new File(world.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile().getParentFile(),"upgrade-contract-fixtures"); + check("legacy_id_before_entity_construction",()->conversion("lootablebodies.Corpse",IntTag.valueOf(512))); + check("native_111_identity_survives",()->conversion("lootablebodies:corpse",IntTag.valueOf(922))); check("missing_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",null)); - check("wrong_type_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",StringNBT.valueOf("damaged"))); - check("nonfinite_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",DoubleNBT.valueOf(Double.NaN))); - check("negative_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",IntNBT.valueOf(-1))); - check("incorrect_new_stamp_does_not_skip_legacy_items",()->conversion("lootablebodies.Corpse",IntNBT.valueOf(922))); + check("wrong_type_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",StringTag.valueOf("damaged"))); + check("nonfinite_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",DoubleTag.valueOf(Double.NaN))); + check("negative_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",IntTag.valueOf(-1))); + check("incorrect_new_stamp_does_not_skip_legacy_items",()->conversion("lootablebodies.Corpse",IntTag.valueOf(922))); check("unknown_items_keep_source_context",UpgradeChecks::missingItem); check("repeated_fixing_and_loading_never_duplicates",UpgradeChecks::repeated); check("old_recovery_payload_is_not_mutated",UpgradeChecks::recovery); @@ -72,8 +78,8 @@ static void run(MinecraftServer server) throws Exception { check("native114_registry_rename_events_reach_the_bridge",UpgradeChecks::nativeRenames); check("retired_vanilla_professions_are_read_only_compatibility",UpgradeChecks::legacyProfessions); check("native114_tracking_range_preserves32blocks",()-> { - RuntimeChecks.require(cyano.lootable.LootableBodies.CORPSE.func_233602_m_()*16==32,"Native tracking range changed from32blocks"); - RuntimeChecks.require(cyano.lootable.LootableBodies.CORPSE.getUpdateFrequency()==1 && cyano.lootable.LootableBodies.CORPSE.shouldSendVelocityUpdates(),"Tracking update settings changed"); + RuntimeChecks.require(cyano.lootable.LootableBodies.CORPSE.clientTrackingRange()*16==32,"Native tracking range changed from32blocks"); + RuntimeChecks.require(cyano.lootable.LootableBodies.CORPSE.updateInterval()==1 && cyano.lootable.LootableBodies.CORPSE.trackDeltas(),"Tracking update settings changed"); }); check("unstamped_native112_chunk_keeps_white_bed_in_vanilla_chest",()->nativeChest(false)); check("legacy_corpse_does_not_downgrade_native112_chunk_items",()->nativeChest(true)); @@ -86,36 +92,36 @@ private static void check(String name,Check check) { catch(Throwable error){failures++;System.out.println("UPGRADE_CONTRACT_FAIL "+name+": "+error);error.printStackTrace();} } private static void nativeChest(boolean legacy) throws Exception { - CompoundNBT entity=body(legacy?"lootablebodies.Corpse":"lootablebodies:corpse"); + CompoundTag entity=body(legacy?"lootablebodies.Corpse":"lootablebodies:corpse"); if(!legacy)entity.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").putString("id","minecraft:pig"); - CompoundNBT input=chunk(entity,legacy?IntNBT.valueOf(1343):null); - CompoundNBT chest=new CompoundNBT();chest.putString("id","minecraft:chest");chest.putInt("x",2);chest.putInt("y",80);chest.putInt("z",2); - CompoundNBT bed=new CompoundNBT();bed.putString("id","minecraft:bed");bed.putByte("Count",(byte)1);bed.putShort("Damage",(short)0);bed.putByte("Slot",(byte)0); - ListNBT items=new ListNBT();items.add(bed);chest.put("Items",items);ListNBT tiles=new ListNBT();tiles.add(chest);input.getCompound("Level").put("TileEntities",tiles); - CompoundNBT fixed=processChunk(input,"1.12.2"); - CompoundNBT actual=fixed.getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); - CompoundNBT expected=bed.copy();expected.putString("id","minecraft:white_bed");expected.remove("Damage"); + CompoundTag input=chunk(entity,legacy?IntTag.valueOf(1343):null); + CompoundTag chest=new CompoundTag();chest.putString("id","minecraft:chest");chest.putInt("x",2);chest.putInt("y",80);chest.putInt("z",2); + CompoundTag bed=new CompoundTag();bed.putString("id","minecraft:bed");bed.putByte("Count",(byte)1);bed.putShort("Damage",(short)0);bed.putByte("Slot",(byte)0); + ListTag items=new ListTag();items.add(bed);chest.put("Items",items);ListTag tiles=new ListTag();tiles.add(chest);input.getCompound("Level").put("TileEntities",tiles); + CompoundTag fixed=processChunk(input,"1.12.2"); + CompoundTag actual=fixed.getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); + CompoundTag expected=bed.copy();expected.putString("id","minecraft:white_bed");expected.remove("Damage"); RuntimeChecks.require(NameLifetimeChecks.same(expected,actual),"Native white bed changed under chunk conversion: "+actual); - CompoundNBT converted=saved(fixed.getCompound("Level").getList("Entities",10).getCompound(0)); + CompoundTag converted=saved(fixed.getCompound("Level").getList("Entities",10).getCompound(0)); RuntimeChecks.require(inventory(converted).size()==48,"Legacy/native corpse inventory changed while correcting vanilla context"); } private static void ambiguousBed() throws Exception { - CompoundNBT input=body("lootablebodies:corpse"),item=new CompoundNBT();item.putString("id","minecraft:bed");item.putByte("Count",(byte)1);item.putShort("Damage",(short)0);item.putByte("Slot",(byte)6); - ListNBT items=new ListNBT();items.add(item);input.put("Equipment",items); - CompoundNBT once=saved(process(chunk(input,null))),record=once.getList("LootableBodiesUnresolved",10).getCompound(0); + CompoundTag input=body("lootablebodies:corpse"),item=new CompoundTag();item.putString("id","minecraft:bed");item.putByte("Count",(byte)1);item.putShort("Damage",(short)0);item.putByte("Slot",(byte)6); + ListTag items=new ListTag();items.add(item);input.put("Equipment",items); + CompoundTag once=saved(process(chunk(input,null))),record=once.getList("LootableBodiesUnresolved",10).getCompound(0); RuntimeChecks.require(once.getList("LootableBodiesUnresolved",10).size()==1 && NameLifetimeChecks.same(item,record.get("Data")) && record.getCompound("SourceVersions").getBoolean("Ambiguous"),"Ambiguous bed was guessed or lost"); - EntityLootableBody again=new EntityLootableBody(world);again.readAdditional(once); + EntityLootableBody again=new EntityLootableBody(world);again.readAdditionalSaveData(once); RuntimeChecks.require(!again.isEmpty(),"Raw recoverable bed allowed empty-corpse decay"); - CompoundNBT twice=new CompoundNBT();again.writeAdditional(twice); + 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 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.util.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.util.ResourceLocation("absentfixture:other_sound"),197); - net.minecraftforge.event.RegistryEvent.MissingMappings event=new net.minecraftforge.event.RegistryEvent.MissingMappings<>(new net.minecraft.util.ResourceLocation("minecraft:soundevents"),registry,Arrays.asList(old,other)); + 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.common.MinecraftForge.EVENT_BUS.post(event); - RuntimeChecks.require(old.getAction()==net.minecraftforge.event.RegistryEvent.MissingMappings.Action.REMAP && old.getTarget()==registry.getValue(new net.minecraft.util.ResourceLocation("minecraft:entity.experience_orb.pickup")),"Known vanilla sound rename not remapped"); + 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"); } private static void nativeRenames() { @@ -127,33 +133,33 @@ private static void nativeRenames() { rename(net.minecraftforge.registries.ForgeRegistries.BLOCKS,"wall_sign","oak_wall_sign"); } private static > void rename(net.minecraftforge.registries.IForgeRegistry registry,String oldName,String current) { - net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping old=new net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping<>(registry,registry,new net.minecraft.util.ResourceLocation("minecraft",oldName),30000); - net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping other=new net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping<>(registry,registry,new net.minecraft.util.ResourceLocation("absentfixture",oldName),30001); + 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)); 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.util.ResourceLocation("minecraft",current)),"Native registry rename event not remapped: "+oldName); + 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"); } private static void legacyProfessions() { - net.minecraft.util.ResourceLocation name=new net.minecraft.util.ResourceLocation("minecraft:villagerprofessions"); + net.minecraft.resources.ResourceLocation name=new net.minecraft.resources.ResourceLocation("minecraft:villagerprofessions"); net.minecraftforge.registries.IForgeRegistry registry=net.minecraftforge.registries.RegistryManager.ACTIVE.getRegistry(name); RuntimeChecks.require(registry!=null && registry.getKeys().size()==6,"Retired vanilla profession registry was not preserved"); - RuntimeChecks.require(!registry.containsKey(new net.minecraft.util.ResourceLocation("absentfixture:profession")),"Unknown professions must retain native missing-mapping handling"); + RuntimeChecks.require(!registry.containsKey(new net.minecraft.resources.ResourceLocation("absentfixture:profession")),"Unknown professions must retain native missing-mapping handling"); RuntimeChecks.require(!net.minecraftforge.registries.RegistryManager.ACTIVE.takeSnapshot(true).containsKey(name),"Retired registry must not be written into new saves"); } private static void bed(int source,int marker,int damage) throws Exception { - CompoundNBT input=body("lootablebodies:corpse"); + CompoundTag input=body("lootablebodies:corpse"); input.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").putString("id","minecraft:pig"); - CompoundNBT item=new CompoundNBT();item.putString("id","minecraft:bed");item.putByte("Count",(byte)2);item.putShort("Damage",(short)0);item.putByte("Slot",(byte)41);input.getList("Equipment",10).set(41,item); + CompoundTag item=new CompoundTag();item.putString("id","minecraft:bed");item.putByte("Count",(byte)2);item.putShort("Damage",(short)0);item.putByte("Slot",(byte)41);input.getList("Equipment",10).set(41,item); if(marker>0)input.putInt("LootableBodiesDataVersion",marker); - CompoundNBT result=saved(process(chunk(input,IntNBT.valueOf(source)))); - ListNBT equipment=result.getList("Equipment",10);CompoundNBT found=null; + CompoundTag result=saved(process(chunk(input,IntTag.valueOf(source)))); + ListTag equipment=result.getList("Equipment",10);CompoundTag found=null; for(int i=0;i preservation=null;Object previous=null;Field active=null; try { @@ -203,97 +209,97 @@ private static CompoundNBT processChunk(CompoundNBT source,String minecraft) thr // implemented, activate the same preflight used before world loading. try {preservation=Class.forName("cyano.lootable.upgrade.UpgradePreservation");active=preservation.getDeclaredField("journal");active.setAccessible(true);previous=active.get(null);preservation.getMethod("prepare",Path.class).invoke(null,path);} catch(ClassNotFoundException beforeImplementation) { } - CompoundNBT input=source.copy(); - if(preservation!=null)try{input=(CompoundNBT)preservation.getMethod("chunkContext",CompoundNBT.class).invoke(null,input);}catch(NoSuchMethodException beforeBoundaryFix){} - CompoundNBT result=nativeChunk(input); - if(preservation!=null)try{preservation.getMethod("clearChunkContext",CompoundNBT.class).invoke(null,result);}catch(NoSuchMethodException beforeBoundaryFix){} + CompoundTag input=source.copy(); + if(preservation!=null)try{input=(CompoundTag)preservation.getMethod("chunkContext",CompoundTag.class).invoke(null,input);}catch(NoSuchMethodException beforeBoundaryFix){} + CompoundTag result=nativeChunk(input); + if(preservation!=null)try{preservation.getMethod("clearChunkContext",CompoundTag.class).invoke(null,result);}catch(NoSuchMethodException beforeBoundaryFix){} return result; } finally {if(active!=null)active.set(null,previous);} } - private static CompoundNBT saved(CompoundNBT fixed) { + private static CompoundTag saved(CompoundTag fixed) { RuntimeChecks.require(fixed.getString("id").equals("lootablebodies:corpse"),"legacy identity was not converted before construction"); Entity entity=FixtureApi.loadEntity(fixed.copy(),world); RuntimeChecks.require(entity instanceof EntityLootableBody,"corpse factory returned no body"); - CompoundNBT saved=new CompoundNBT();((EntityLootableBody)entity).writeAdditional(saved);return saved; + CompoundTag saved=new CompoundTag();((EntityLootableBody)entity).addAdditionalSaveData(saved);return saved; } - private static Map inventory(CompoundNBT body) { + private static Map inventory(CompoundTag body) { Map counts=new TreeMap<>();for(String key:Arrays.asList("Equipment","Aux")) { - ListNBT list=body.getList(key,10);for(int i=0;i0,"malformed original fields were lost in vanilla fixing"); } private static void missingItem() throws Exception { - CompoundNBT body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20);raw.putString("id","absentfixture:keepsake"); - CompoundNBT result=saved(process(chunk(body,IntNBT.valueOf(512))));ListNBT recovery=result.getList("LootableBodiesUnresolved",10); - RuntimeChecks.require(recovery.size()==1,"unavailable item not retained exactly once");CompoundNBT record=recovery.getCompound(0); + CompoundTag body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20);raw.putString("id","absentfixture:keepsake"); + CompoundTag result=saved(process(chunk(body,IntTag.valueOf(512))));ListTag recovery=result.getList("LootableBodiesUnresolved",10); + RuntimeChecks.require(recovery.size()==1,"unavailable item not retained exactly once");CompoundTag record=recovery.getCompound(0); RuntimeChecks.require(NameLifetimeChecks.same(raw,record.get("Data")) && record.getCompound("SourceVersions").getInt("DataVersion")==512,"unavailable item lost its original payload or source context"); } private static void repeated() throws Exception { - CompoundNBT raw=body("lootablebodies.Corpse"),fixed=process(chunk(raw,IntNBT.valueOf(512))),once=saved(fixed); - CompoundNBT again=nativeChunk(chunk(fixed.copy(),IntNBT.valueOf(512))).getCompound("Level").getList("Entities",10).getCompound(0); - CompoundNBT twice=saved(again);RuntimeChecks.require(inventory(once).equals(inventory(twice)),"repeated data fixing duplicated or lost items"); + CompoundTag raw=body("lootablebodies.Corpse"),fixed=process(chunk(raw,IntTag.valueOf(512))),once=saved(fixed); + CompoundTag again=nativeChunk(chunk(fixed.copy(),IntTag.valueOf(512))).getCompound("Level").getList("Entities",10).getCompound(0); + CompoundTag twice=saved(again);RuntimeChecks.require(inventory(once).equals(inventory(twice)),"repeated data fixing duplicated or lost items"); } private static void recovery() throws Exception { - CompoundNBT body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20).copy();raw.putString("id","absentfixture:keepsake"); - CompoundNBT record=new CompoundNBT();record.putString("Source","Aux");record.put("Data",raw.copy());ListNBT records=new ListNBT();records.add(record);body.put("LootableBodiesUnresolved",records); - CompoundNBT output=saved(process(chunk(body,IntNBT.valueOf(512))));CompoundNBT kept=output.getList("LootableBodiesUnresolved",10).getCompound(0); + CompoundTag body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20).copy();raw.putString("id","absentfixture:keepsake"); + CompoundTag record=new CompoundTag();record.putString("Source","Aux");record.put("Data",raw.copy());ListTag records=new ListTag();records.add(record);body.put("LootableBodiesUnresolved",records); + CompoundTag output=saved(process(chunk(body,IntTag.valueOf(512))));CompoundTag kept=output.getList("LootableBodiesUnresolved",10).getCompound(0); RuntimeChecks.require(kept.getString("Source").equals("Aux") && NameLifetimeChecks.same(raw,kept.get("Data")),"existing Source/Data payload changed"); RuntimeChecks.require(kept.getCompound("SourceVersions").getInt("DataVersion")==512,"existing recovery extension lost source version context"); } - private static CompoundNBT fullSave(CompoundNBT fixed) { + private static CompoundTag fullSave(CompoundTag fixed) { Entity entity=FixtureApi.loadEntity(fixed.copy(),world);RuntimeChecks.require(entity instanceof EntityLootableBody,"Missing body for full save"); - CompoundNBT result=new CompoundNBT();entity.writeUnlessPassenger(result);return result; + CompoundTag result=new CompoundTag();entity.saveAsPassenger(result);return result; } private static void savedCounter() throws Exception { - CompoundNBT source=body("lootablebodies.Corpse");source.getList("Equipment",10).getCompound(20).getCompound("tag").putInt("upgrade_counter",0); - CompoundNBT once=fullSave(process(chunk(source,IntNBT.valueOf(512)))); - CompoundNBT counted=null; - for(INBT entry:once.getList("Equipment",10))if(((CompoundNBT)entry).getByte("Slot")==20)counted=(CompoundNBT)entry; + CompoundTag source=body("lootablebodies.Corpse");source.getList("Equipment",10).getCompound(20).getCompound("tag").putInt("upgrade_counter",0); + CompoundTag once=fullSave(process(chunk(source,IntTag.valueOf(512)))); + CompoundTag counted=null; + for(Tag entry:once.getList("Equipment",10))if(((CompoundTag)entry).getByte("Slot")==20)counted=(CompoundTag)entry; RuntimeChecks.require(counted!=null && counted.getCompound("tag").getInt("upgrade_counter")==1,"Item fixer did not run exactly once on first load: "+counted); - CompoundNBT twice=fullSave(process(chunk(once,IntNBT.valueOf(922))));RuntimeChecks.require(inventory(once).equals(inventory(twice)),"Old item version was applied again after saving migrated items"); + CompoundTag twice=fullSave(process(chunk(once,IntTag.valueOf(922))));RuntimeChecks.require(inventory(once).equals(inventory(twice)),"Old item version was applied again after saving migrated items"); } - private static CompoundNBT currentStamps(CompoundNBT chunk) {chunk.putInt("DataVersion",UpgradePreservation.MINECRAFT_VERSION);chunk.put("ForgeDataVersion",new CompoundNBT());chunk.getCompound("ForgeDataVersion").putInt("minecraft",-1);return chunk;} + private static CompoundTag currentStamps(CompoundTag chunk) {chunk.putInt("DataVersion",UpgradePreservation.MINECRAFT_VERSION);chunk.put("ForgeDataVersion",new CompoundTag());chunk.getCompound("ForgeDataVersion").putInt("minecraft",-1);return chunk;} private static void migratedDamagedStamp() throws Exception { - CompoundNBT source=body("lootablebodies:corpse");source.putInt("LootableBodiesDataVersion",1);source.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").putString("id","minecraft:pig"); - CompoundNBT result=saved(process(currentStamps(chunk(source,null))));RuntimeChecks.require(expectedInventory(source,922).equals(inventory(result)),"Damaged version stamp truncated an already migrated Equipment list"); + CompoundTag source=body("lootablebodies:corpse");source.putInt("LootableBodiesDataVersion",1);source.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").putString("id","minecraft:pig"); + CompoundTag result=saved(process(currentStamps(chunk(source,null))));RuntimeChecks.require(expectedInventory(source,922).equals(inventory(result)),"Damaged version stamp truncated an already migrated Equipment list"); } private static void forgedModStamp() throws Exception { - CompoundNBT source=body("lootablebodies.Corpse"),expected=source.copy(); - CompoundNBT input=currentStamps(chunk(source,IntNBT.valueOf(512)));input.getCompound("ForgeDataVersion").putInt("minecraft",512); + CompoundTag source=body("lootablebodies.Corpse"),expected=source.copy(); + CompoundTag input=currentStamps(chunk(source,IntTag.valueOf(512)));input.getCompound("ForgeDataVersion").putInt("minecraft",512); RuntimeChecks.require(expectedInventory(expected,512).equals(inventory(saved(process(input)))),"Incorrect mod stamp bypassed custom inventory preservation"); } private static void reusedOutput() throws Exception { - CompoundNBT fixed=process(chunk(body("lootablebodies.Corpse"),IntNBT.valueOf(512))); - EntityLootableBody entity=(EntityLootableBody)FixtureApi.loadEntity(fixed.copy(),world);entity.clear();entity.writeAdditional(fixed); + CompoundTag fixed=process(chunk(body("lootablebodies.Corpse"),IntTag.valueOf(512))); + EntityLootableBody entity=(EntityLootableBody)FixtureApi.loadEntity(fixed.copy(),world);entity.clearContent();entity.addAdditionalSaveData(fixed); EntityLootableBody reloaded=(EntityLootableBody)FixtureApi.loadEntity(fixed.copy(),world); RuntimeChecks.require(reloaded.isEmpty(),"Reused output compound resurrected a consumed upgrade payload"); } private static void allCurrentStamps() throws Exception { - CompoundNBT input=chunk(body("lootablebodies.Corpse"),IntNBT.valueOf(922)),versions=new CompoundNBT(); + CompoundTag input=chunk(body("lootablebodies.Corpse"),IntTag.valueOf(922)),versions=new CompoundTag(); versions.putInt("minecraft",922);versions.putInt("lootablebodies",1);versions.putInt("lootablebodiesbuildtest",1);input.put("ForgeDataVersion",versions); saved(process(input)); } private static void vanillaFalseStamp() throws Exception { - CompoundNBT input=chunk(body("lootablebodies.Corpse"),IntNBT.valueOf(922)),versions=new CompoundNBT(); + CompoundTag input=chunk(body("lootablebodies.Corpse"),IntTag.valueOf(922)),versions=new CompoundTag(); versions.putInt("minecraft",922);versions.putInt("lootablebodies",1);versions.putInt("lootablebodiesbuildtest",1);input.put("ForgeDataVersion",versions); - CompoundNBT chest=new CompoundNBT();chest.putString("id","Chest");chest.putInt("x",2);chest.putInt("y",80);chest.putInt("z",2); - CompoundNBT banner=new CompoundNBT();banner.putString("id","minecraft:banner");banner.putByte("Count",(byte)2);banner.putShort("Damage",(short)0);banner.putByte("Slot",(byte)0); - CompoundNBT tag=new CompoundNBT(),block=new CompoundNBT();block.putInt("Base",5);tag.put("BlockEntityTag",block);banner.put("tag",tag);ListNBT items=new ListNBT();items.add(banner);chest.put("Items",items);ListNBT tiles=new ListNBT();tiles.add(chest);input.getCompound("Level").put("TileEntities",tiles); - CompoundNBT fixed=processChunk(input).getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); + CompoundTag chest=new CompoundTag();chest.putString("id","Chest");chest.putInt("x",2);chest.putInt("y",80);chest.putInt("z",2); + CompoundTag banner=new CompoundTag();banner.putString("id","minecraft:banner");banner.putByte("Count",(byte)2);banner.putShort("Damage",(short)0);banner.putByte("Slot",(byte)0); + CompoundTag tag=new CompoundTag(),block=new CompoundTag();block.putInt("Base",5);tag.put("BlockEntityTag",block);banner.put("tag",tag);ListTag items=new ListTag();items.add(banner);chest.put("Items",items);ListTag tiles=new ListTag();tiles.add(chest);input.getCompound("Level").put("TileEntities",tiles); + CompoundTag fixed=processChunk(input).getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); RuntimeChecks.require(fixed.getString("id").equals("minecraft:purple_banner") && !fixed.contains("Damage") && fixed.getByte("Count")==2,"False version stamps skipped the vanilla banner conversion: "+fixed); } - private static Map expectedInventory(CompoundNBT source,int version) { - CompoundNBT expected=source.copy(); + private static Map expectedInventory(CompoundTag source,int version) { + CompoundTag expected=source.copy(); for(String key:Arrays.asList("Equipment","Aux")) { - ListNBT items=expected.getList(key,10); + ListTag items=expected.getList(key,10); for(int i=0;i(FixtureApi.entities(world()));a.onKillCommand();first=newCorpse();saveId("first",first);phase(2); + a.connection.teleport(96.5,PLAYER_Y,98.5,0,0); + before=new HashSet<>(FixtureApi.entities(world()));a.kill();first=newCorpse();saveId("first",first);phase(2); } else if(phase==2 && both(2)) {mark("both_clients_receive_actual_death_before_respawn");phase(3); } else if(phase==3 && both(3) && a!=null && a.isAlive()) { RuntimeChecks.require(first.isAlive(),"Server lost corpse after respawn"); - a.connection.setPlayerLocation(first.getPosX(),first.getPosY(),first.getPosZ()-2.5,0,25); - b.connection.setPlayerLocation(first.getPosX()+1.5,first.getPosY(),first.getPosZ()-2.5,0,25);phase(4); + a.connection.teleport(first.getX(),first.getY(),first.getZ()-2.5,0,25); + b.connection.teleport(first.getX()+1.5,first.getY(),first.getZ()-2.5,0,25);phase(4); } else if(phase==4 && both(4)) { - RuntimeChecks.require(a.openContainer instanceof cyano.lootable.graphics.CorpseContainer && b.openContainer instanceof cyano.lootable.graphics.CorpseContainer,"GUI ack without server access"); + RuntimeChecks.require(a.containerMenu instanceof cyano.lootable.graphics.CorpseContainer && b.containerMenu instanceof cyano.lootable.graphics.CorpseContainer,"GUI ack without server access"); mark("both_clients_render_and_open_corpse_after_respawn");phase(5); } else if(phase==5 && both(5)) { - a.closeScreen();b.closeScreen();a.connection.setPlayerLocation(160.5,PLAYER_Y,93.5,0,25);phase(6); + a.closeContainer();b.closeContainer();a.connection.teleport(160.5,PLAYER_Y,93.5,0,25);phase(6); } else if(phase==6 && both(6)) { RuntimeChecks.require(FixtureApi.tracked(first).contains(b) && !FixtureApi.tracked(first).contains(a),"Range assertions differ from tracker"); - mark("range_exit_removes_only_distant_client");a.connection.setPlayerLocation(96.5,PLAYER_Y,93.5,0,25);phase(7); + mark("range_exit_removes_only_distant_client");a.connection.teleport(96.5,PLAYER_Y,93.5,0,25);phase(7); } else if(phase==7 && both(7)) {mark("range_return_respawns_same_uuid");phase(8); } else if(phase==8 && a==null) {phase(9); } else if(phase==9 && a!=null && both(9)) { @@ -110,45 +113,45 @@ public VisibilityChecks(MinecraftServer server) throws Exception { phase(120); } else if(phase==120 && both(120)) { mark(Integer.getInteger("lootablebodies.visibility.waitTicks",6200)==6200?"default_corpse_visible_after_6200_ticks_and_310_seconds":"short_fixture_control_default_corpse_visible"); - a.connection.setPlayerLocation(100.5,PLAYER_Y,96.5,0,0);b.connection.setPlayerLocation(102.5,PLAYER_Y,93.5,0,25); - a.inventory.mainInventory.set(9,RuntimeChecks.stack(411));a.container.detectAndSendChanges();phase(13); + a.connection.teleport(100.5,PLAYER_Y,96.5,0,0);b.connection.teleport(102.5,PLAYER_Y,93.5,0,25); + a.getInventory().items.set(9,RuntimeChecks.stack(411));a.containerMenu.broadcastChanges();phase(13); } else if(phase==13 && both(13)) { - before=new HashSet<>(FixtureApi.entities(world()));TNTEntity tnt=new TNTEntity(world(),a.getPosX(),a.getPosY(),a.getPosZ(),null);tnt.setFuse(0); - RuntimeChecks.require(world().addEntity(tnt),"TNT fixture spawn");phase(14); + before=new HashSet<>(FixtureApi.entities(world()));PrimedTnt tnt=new PrimedTnt(world(),a.getX(),a.getY(),a.getZ(),null);tnt.setFuse(0); + RuntimeChecks.require(world().addFreshEntity(tnt),"TNT fixture spawn");phase(14); } else if(phase==14) { if(second==null && a!=null && !a.isAlive()) {second=newCorpse();saveId("second",second);} if(second!=null && both(14)) {RuntimeChecks.require(second.isAlive() && has(second,411),"TNT destroyed default corpse/inventory");mark("tnt_death_after_five_minutes_both_clients_receive_corpse");phase(15);} } else if(phase==15 && both(15) && a!=null && a.isAlive()) { // TNT can displace both the observer and the death position. - a.connection.setPlayerLocation(second.getPosX(),second.getPosY(),second.getPosZ()-2.5,0,25); - b.connection.setPlayerLocation(second.getPosX()+1.5,second.getPosY(),second.getPosZ()-2.5,0,25);phase(16); + a.connection.teleport(second.getX(),second.getY(),second.getZ()-2.5,0,25); + b.connection.teleport(second.getX()+1.5,second.getY(),second.getZ()-2.5,0,25);phase(16); } else if(phase==16 && both(16)) {mark("both_clients_render_and_open_tnt_corpse_after_respawn");phase(17); } else if(phase==17 && both(17) && phaseTicks>20) { RuntimeChecks.require(!has(second,411) && playerHas(a,411),"TNT corpse recovery lost or duplicated tagged stack"); RuntimeChecks.require(has(first,410),"Earlier corpse inventory changed during TNT/reconnect checks"); - a.closeScreen();b.closeScreen();File folder=world().getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile(); + a.closeContainer();b.closeContainer();File folder=world().getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); RuntimeChecks.saveExpected(folder,"visibility-first",first);RuntimeChecks.saveExpected(folder,"visibility-second",second); - CompoundNBT state=new CompoundNBT();state.putUniqueId("First",first.getUniqueID());state.putUniqueId("Second",second.getUniqueID());state.putUniqueId("Player",a.getUniqueID());state.put("Inventory",a.inventory.write(new ListNBT())); - try(FileOutputStream out=new FileOutputStream(new File(folder,"visibility-state.nbt"))){CompressedStreamTools.writeCompressed(state,out);} - server.getPlayerList().saveAllPlayerData();server.save(false,true,false);mark("tagged_inventory_recovered_and_complete_nbt_saved");phase(18); - } else if(phase==18 && server.getPlayerList().getCurrentPlayerCount()==0) { - Files.write(file("visibility.pass").toPath(),new byte[]{1});System.out.println("LOOTABLE_BODIES_VISIBILITY_PASS");finished=true;server.initiateShutdown(false); + CompoundTag state=new CompoundTag();state.putUUID("First",first.getUUID());state.putUUID("Second",second.getUUID());state.putUUID("Player",a.getUUID());state.put("Inventory",a.getInventory().save(new ListTag())); + try(FileOutputStream out=new FileOutputStream(new File(folder,"visibility-state.nbt"))){NbtIo.writeCompressed(state,out);} + server.getPlayerList().saveAll();server.saveAllChunks(false,true,false);mark("tagged_inventory_recovered_and_complete_nbt_saved");phase(18); + } else if(phase==18 && server.getPlayerList().getPlayerCount()==0) { + Files.write(file("visibility.pass").toPath(),new byte[]{1});System.out.println("LOOTABLE_BODIES_VISIBILITY_PASS");finished=true;server.halt(false); } else if(phase==19 && both(19)) { - mark("cold_server_restart_both_clients_render_and_open_saved_corpse");a.closeScreen();b.closeScreen(); - for(int x=397;x<=404;x++)for(int z=397;z<=404;z++)world().setBlockState(new BlockPos(x,GROUND_Y,z),Blocks.OBSIDIAN.getDefaultState()); - a.connection.setPlayerLocation(400.5,PLAYER_Y,400.5,0,25);b.connection.setPlayerLocation(402.5,PLAYER_Y,400.5,0,25);phase(20); - } else if(phase==20 && both(20) && phaseTicks>240 && !world().getChunkProvider().chunkExists(first.chunkCoordX,first.chunkCoordZ) && !world().getChunkProvider().chunkExists(second.chunkCoordX,second.chunkCoordZ)) { + mark("cold_server_restart_both_clients_render_and_open_saved_corpse");a.closeContainer();b.closeContainer(); + for(int x=397;x<=404;x++)for(int z=397;z<=404;z++)world().setBlockAndUpdate(new BlockPos(x,GROUND_Y,z),Blocks.OBSIDIAN.defaultBlockState()); + a.connection.teleport(400.5,PLAYER_Y,400.5,0,25);b.connection.teleport(402.5,PLAYER_Y,400.5,0,25);phase(20); + } else if(phase==20 && both(20) && phaseTicks>240 && !world().getChunkSource().hasChunk(first.chunkPosition().x,first.chunkPosition().z) && !world().getChunkSource().hasChunk(second.chunkPosition().x,second.chunkPosition().z)) { mark("both_clients_leave_and_corpse_chunk_really_unloads"); FixtureApi.flush(server); - a.connection.setPlayerLocation(first.getPosX(),first.getPosY(),first.getPosZ()-2.5,0,25);b.connection.setPlayerLocation(first.getPosX()+1.5,first.getPosY(),first.getPosZ()-2.5,0,25);phase(21); + a.connection.teleport(first.getX(),first.getY(),first.getZ()-2.5,0,25);b.connection.teleport(first.getX()+1.5,first.getY(),first.getZ()-2.5,0,25);phase(21); } else if(phase==21 && both(21)) { - first=RuntimeChecks.find(world(),first.getUniqueID());second=RuntimeChecks.find(world(),second.getUniqueID());verifyInventories(); - mark("chunk_reentry_both_clients_render_same_uuids_and_inventory");server.getPlayerList().saveAllPlayerData();server.save(false,true,false);phase(18); + first=RuntimeChecks.find(world(),first.getUUID());second=RuntimeChecks.find(world(),second.getUUID());verifyInventories(); + mark("chunk_reentry_both_clients_render_same_uuids_and_inventory");server.getPlayerList().saveAll();server.saveAllChunks(false,true,false);phase(18); } - } catch(Throwable failure) {failure.printStackTrace();finished=true;server.initiateShutdown(false);} + } catch(Throwable failure) {failure.printStackTrace();finished=true;server.halt(false);} } private EntityLootableBody newCorpse(){for(Entity e:FixtureApi.entities(world()))if(e instanceof EntityLootableBody&&!before.contains(e))return (EntityLootableBody)e;throw new AssertionError("No server corpse for real death");} - private void saveId(String name,EntityLootableBody b) throws Exception {Files.write(file(name+".uuid").toPath(),b.getUniqueID().toString().getBytes(StandardCharsets.UTF_8));} + private void saveId(String name,EntityLootableBody b) throws Exception {Files.write(file(name+".uuid").toPath(),b.getUUID().toString().getBytes(StandardCharsets.UTF_8));} private void phase(int n) throws Exception { phase=n;phaseTicks=0;File tmp=file("phase.tmp"); Files.write(tmp.toPath(),Integer.toString(n).getBytes(StandardCharsets.UTF_8)); @@ -161,26 +164,26 @@ private void phase(int n) throws Exception { System.out.println("VISIBILITY_PHASE "+n); } private void mark(String n) throws Exception {try(FileWriter out=new FileWriter(file("checks.txt"),true)){out.write("PASS "+n+"\n");}System.out.println("VISIBILITY_CHECK_PASS "+n);} - static boolean has(EntityLootableBody b,int i){for(int s=0;s<48;s++)if(RuntimeChecks.sameStack(b.getStackInSlot(s),RuntimeChecks.stack(i)))return true;return false;} - private static boolean playerHas(ServerPlayerEntity p,int i){int count=0;for(int s=0;s e){if(e.getEntity() instanceof EntityLootableBody)rendered.add(e.getEntity().getUniqueID());} + @SubscribeEvent public void rendered(RenderLivingEvent.Post e){if(e.getEntity() instanceof EntityLootableBody)rendered.add(e.getEntity().getUUID());} @SubscribeEvent public void tick(TickEvent.ClientTickEvent e) { if(e.phase!=TickEvent.Phase.END)return;Minecraft mc=Minecraft.getInstance(); - if(mc.isGameFocused())mc.mouseHelper.ungrabMouse();net.minecraft.client.settings.KeyBinding.unPressAllKeys(); + if(mc.isWindowActive())mc.mouseHandler.releaseMouse();net.minecraft.client.KeyMapping.releaseAll(); try { - if(finished){mc.shutdown();return;} + if(finished){mc.stop();return;} RuntimeChecks.require(++ticks<15000,"Visibility client timeout role="+role+" phase="+phase); RuntimeChecks.require(!VisibilityChecks.file("abort").exists(),"Visibility runner aborted"); int n=Integer.parseInt(new String(Files.readAllBytes(VisibilityChecks.file("phase").toPath()),StandardCharsets.UTF_8).trim()); if(n!=phase){phase=n;frames=0;guiFrames=0;respawnSent=false;rendered.clear();System.out.println("VISIBILITY_CLIENT_PHASE "+role+" "+phase);}frames++; if(FixtureConnections.retry(mc,role))connecting=false; - if(!connecting && FixtureConnections.ready(role) && mc.world==null && mc.currentScreen instanceof MainMenuScreen && (phase==0 || phase==9 || phase==11)) { - connecting=true;mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; + 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; FixtureConnections.connect(mc); } - if(mc.world==null || mc.player==null)return; - if(frames%200==0)System.out.println("VISIBILITY_CLIENT_STATE "+role+" phase="+phase+" position="+mc.player.getPosition()+" rendered="+rendered+" screen="+(mc.currentScreen==null?null:mc.currentScreen.getClass().getSimpleName())+" container="+mc.player.openContainer.getClass().getSimpleName()); + if(mc.level==null || mc.player==null)return; + if(frames%200==0)System.out.println("VISIBILITY_CLIENT_STATE "+role+" phase="+phase+" position="+mc.player.blockPosition()+" rendered="+rendered+" screen="+(mc.screen==null?null:mc.screen.getClass().getSimpleName())+" container="+mc.player.containerMenu.getClass().getSimpleName()); if(phase==12 && frames%200==0) { - EntityLootableBody b=body(mc,"first");RuntimeChecks.require(b!=null&&!b.removed&&!b.isInvisible(),"Corpse disappeared during five-minute wait"); + EntityLootableBody b=body(mc,"first");RuntimeChecks.require(b!=null&&!b.isRemoved()&&!b.isInvisible(),"Corpse disappeared during five-minute wait"); } if(done.contains(phase) || frames<20)return; if(phase==0 || phase==1 || phase==13){ack();return;} if(phase==2 || phase==14) { EntityLootableBody b=body(mc,phase==2?"first":"second");if(b==null)return; - if(role.equals("A") && !(mc.currentScreen instanceof DeathScreen))return; - RuntimeChecks.require(!b.removed&&!b.isInvisible(),"Received corpse is dead/invisible");shot(mc,"death-"+phase+".png");ack(); + if(role.equals("A") && !(mc.screen instanceof DeathScreen))return; + RuntimeChecks.require(!b.isRemoved()&&!b.isInvisible(),"Received corpse is dead/invisible");shot(mc,"death-"+phase+".png");ack(); } else if(phase==3 || phase==15) { if(role.equals("A")) { - if(mc.currentScreen instanceof DeathScreen){if(!respawnSent){mc.player.respawnPlayer();respawnSent=true;}return;} + if(mc.screen instanceof DeathScreen){if(!respawnSent){mc.player.respawn();respawnSent=true;}return;} if(!mc.player.isAlive())return; } if(body(mc,phase==3?"first":"second")!=null)ack(); } else if(phase==4 || phase==16 || phase==19) { EntityLootableBody b=body(mc,phase==16?"second":"first");if(b==null)return; - if(!(mc.currentScreen instanceof CorpseGUIContainer)) { - if(!rendered.contains(b.getUniqueID()))return; - if(frames%10==0)mc.playerController.interactWithEntity(mc.player,b,Hand.MAIN_HAND);return; + if(!(mc.screen instanceof CorpseGUIContainer)) { + if(!rendered.contains(b.getUUID()))return; + if(frames%10==0)mc.gameMode.interact(mc.player,b,InteractionHand.MAIN_HAND);return; } int index=phase==16?411:410;boolean found=false; - for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.openContainer.getSlot(slot).getStack(),RuntimeChecks.stack(index)))found=true; + for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(index)))found=true; if(!found || ++guiFrames<10)return;shot(mc,"corpse-gui-"+phase+".png");ack(); - } else if(phase==5) {mc.player.closeScreen();ack(); + } else if(phase==5) {mc.player.closeContainer();ack(); } else if(phase==6) { boolean present=body(mc,"first")!=null;if(present==role.equals("B"))ack(); } else if(phase==7 || phase==9 || phase==11) { - EntityLootableBody b=body(mc,"first");if(b!=null&&!b.removed&&!b.isInvisible()&&rendered.contains(b.getUniqueID())){shot(mc,"tracked-"+phase+".png");ack();} + EntityLootableBody b=body(mc,"first");if(b!=null&&!b.isRemoved()&&!b.isInvisible()&&rendered.contains(b.getUUID())){shot(mc,"tracked-"+phase+".png");ack();} } else if(phase==8 || phase==10) { - if(role.equals(phase==8?"A":"B")){mc.world.sendQuittingDisconnectingPacket();mc.unloadWorld();mc.displayGuiScreen(new MainMenuScreen());connecting=false;} + if(role.equals(phase==8?"A":"B")){mc.level.disconnect();mc.clearLevel();mc.setScreen(new TitleScreen());connecting=false;} ack(); } else if(phase==12 || phase==120) { - EntityLootableBody b=body(mc,"first");if(b!=null&&!b.removed&&rendered.contains(b.getUniqueID()))ack(); + EntityLootableBody b=body(mc,"first");if(b!=null&&!b.isRemoved()&&rendered.contains(b.getUUID()))ack(); } else if(phase==17) { - if(role.equals("A"))for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.openContainer.getSlot(slot).getStack(),RuntimeChecks.stack(411)))mc.playerController.windowClick(mc.player.openContainer.windowId,slot,0,ClickType.QUICK_MOVE,mc.player); + if(role.equals("A"))for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(411)))mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,0,ClickType.QUICK_MOVE,mc.player); // The following acknowledgement is not inventory proof; the server checks the resulting packet state. ack(); } else if(phase==18) { - new File(mc.gameDir,"visibility-client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_VISIBILITY_CLIENT_PASS role="+role); - mc.player.closeScreen(); - mc.getConnection().getNetworkManager().closeChannel(new net.minecraft.util.text.StringTextComponent("Disposable check complete")); + 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")); finished=true; } else if(phase==20) { if(body(mc,"first")==null&&body(mc,"second")==null)ack(); } else if(phase==21) { - mc.player.rotationYaw=frames*3;mc.player.rotationPitch=25; + mc.player.setYRot(frames*3);mc.player.setXRot(25); EntityLootableBody a=body(mc,"first"),b=body(mc,"second"); - if(a!=null&&b!=null&&!a.removed&&!b.removed&&rendered.contains(a.getUniqueID())&&rendered.contains(b.getUniqueID())) {shot(mc,"chunk-reentry.png");ack();} + if(a!=null&&b!=null&&!a.isRemoved()&&!b.isRemoved()&&rendered.contains(a.getUUID())&&rendered.contains(b.getUUID())) {shot(mc,"chunk-reentry.png");ack();} } - } catch(Throwable error){error.printStackTrace();finished=true;mc.shutdown();} + } catch(Throwable error){error.printStackTrace();finished=true;mc.stop();} } private EntityLootableBody body(Minecraft mc,String name) throws Exception { File file=VisibilityChecks.file(name+".uuid");if(!file.isFile())return null;UUID id=UUID.fromString(new String(Files.readAllBytes(file.toPath()),StandardCharsets.UTF_8)); - for(Entity e:FixtureApi.entities(mc.world))if(e instanceof EntityLootableBody&&e.getUniqueID().equals(id))return (EntityLootableBody)e;return null; + for(Entity e:FixtureApi.entities(mc.level))if(e instanceof EntityLootableBody&&e.getUUID().equals(id))return (EntityLootableBody)e;return null; } private void ack() throws Exception {done.add(phase);Files.write(VisibilityChecks.file(role+".visibility-"+phase).toPath(),new byte[]{1});} - private static void shot(Minecraft mc,String name){ScreenShotHelper.saveScreenshot(mc.gameDir,name,mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} + private static void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java index c84edee..d26dbf9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java @@ -13,23 +13,23 @@ import java.util.concurrent.TimeUnit; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.*; -import net.minecraft.server.management.PlayerProfileCache; -import net.minecraft.tileentity.SkullTileEntity; -import net.minecraft.util.ResourceLocation; +import net.minecraft.server.players.GameProfileCache; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import net.minecraft.resources.ResourceLocation; /** Deterministic service faults; no real account or authentication service required. */ final class VisibilitySkinChecks { static void run(Minecraft mc) throws Exception { RuntimeChecks.require(!LootableBodies.useLocalSkin,"Visibility checks require the production texture default"); - File report=new File(mc.gameDir,"skin-checks.txt"); + File report=new File(mc.gameDirectory,"skin-checks.txt"); List failures=new ArrayList<>(); Field skinField=field(Minecraft.class,SkinManager.class); SkinManager original=(SkinManager)skinField.get(mc); GameProfile profile=new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000201"),"VisibilityFault"); try { - skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDir,"skin-fixture"),mc.getSessionService()) { - @Override public Map loadSkinFromCache(GameProfile p) { + skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture"),mc.getMinecraftSessionService()) { + @Override public Map getInsecureSkinInformation(GameProfile p) { throw new IllegalStateException("injected skin cache failure"); } }); @@ -39,35 +39,43 @@ static void run(Minecraft mc) throws Exception { mark(report,"skin_cache_failure_has_visible_default",true); } catch(Throwable failure) {failures.add("skin cache: "+failure);mark(report,"skin_cache_failure_has_visible_default",false);} } finally {skinField.set(mc,original);} - Field cache=field(SkullTileEntity.class,PlayerProfileCache.class); - Field service=field(SkullTileEntity.class,MinecraftSessionService.class); + Field cache=field(SkullBlockEntity.class,GameProfileCache.class); + Field service=field(SkullBlockEntity.class,MinecraftSessionService.class); Object oldCache=cache.get(null),oldService=service.get(null); AtomicInteger requests=new AtomicInteger(); CountDownLatch failedRequest=new CountDownLatch(1); + CountDownLatch slowDelivered=new CountDownLatch(1); try { - SkullTileEntity.setProfileCache(new PlayerProfileCache(null,new File(mc.gameDir,"fault-usercache.json")) { - @Override public GameProfile getGameProfileForUsername(String name) {return new GameProfile(profile.getId(),name);} - }); - SkullTileEntity.setSessionService((MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ + GameProfileCache faultCache=new GameProfileCache(null,new File(mc.gameDirectory,"fault-usercache.json")) { + @Override public Optional get(String name) {return Optional.of(new GameProfile(profile.getId(),name));} + @Override public void add(GameProfile value) {super.add(value);if(value.getName().equals("VisibilitySlow"))slowDelivered.countDown();} + }; + faultCache.setExecutor(Runnable::run);SkullBlockEntity.setProfileCache(faultCache); + SkullBlockEntity.setSessionService((MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ if(method.getName().equals("fillProfileProperties")) {requests.incrementAndGet();failedRequest.countDown();throw new IllegalStateException("injected profile lookup failure");} throw new UnsupportedOperationException(method.getName()); })); - EntityLootableBody body=new EntityLootableBody(mc.world);body.setUserName("VisibilityFault"); + EntityLootableBody body=new EntityLootableBody(mc.level);body.setUserName("VisibilityFault"); try {body.baseTick();RuntimeChecks.require(body.getGameProfile()!=null,"Failure erased fallback profile");mark(report,"profile_failure_does_not_abort_client_tick",true);} catch(Throwable failure) {failures.add("profile: "+failure);mark(report,"profile_failure_does_not_abort_client_tick",false);} RuntimeChecks.require(failedRequest.await(2,TimeUnit.SECONDS),"Profile failure was never injected"); // Deliberately slow lookup must leave the render/tick thread responsive. - SkullTileEntity.setSessionService((MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ + SkullBlockEntity.setSessionService((MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ if(method.getName().equals("fillProfileProperties")) {requests.incrementAndGet();Thread.sleep(400);return args[0];} throw new UnsupportedOperationException(method.getName()); })); - EntityLootableBody slow=new EntityLootableBody(mc.world);slow.setUserName("VisibilitySlow"); + EntityLootableBody slow=new EntityLootableBody(mc.level);slow.setUserName("VisibilitySlow"); long start=System.nanoTime();slow.baseTick();long millis=(System.nanoTime()-start)/1000000; try {RuntimeChecks.require(millis<150,"Slow profile blocked client tick for "+millis+"ms");mark(report,"delayed_profile_does_not_block_client_tick",true);} catch(Throwable failure) {failures.add("delay: "+failure);mark(report,"delayed_profile_does_not_block_client_tick",false);} boolean local=LootableBodies.useLocalSkin;LootableBodies.useLocalSkin=true; slow.setUserName("VisibilityRenamed");slow.baseTick();LootableBodies.useLocalSkin=local; - Thread.sleep(650); // Only the fault fixture waits; never production code. + RuntimeChecks.require(slowDelivered.await(2,TimeUnit.SECONDS),"Native profile callback was never delivered"); + // Fence the same owned executor after its cache/add/CAS task. + // Waiting for the service alone would not prove the late CAS path. + CountDownLatch callbackFence=new CountDownLatch(1); + ((java.util.concurrent.Executor)field(EntityLootableBody.class,java.util.concurrent.ThreadPoolExecutor.class).get(null)).execute(callbackFence::countDown); + RuntimeChecks.require(callbackFence.await(2,TimeUnit.SECONDS),"Owned profile lookup did not finish"); RuntimeChecks.require(requests.get()==2,"Faults never exercised actual profile services"); RuntimeChecks.require(slow.getGameProfile().getName().equals("VisibilityRenamed"),"Late skin result overwrote renamed corpse"); mark(report,"late_profile_result_preserves_new_name",true); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java index db14516..db4969d 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java @@ -1,5 +1,5 @@ package cyano.lootable.buildtest; -import net.minecraft.client.gui.screen.MainMenuScreen; +import net.minecraft.client.gui.screens.TitleScreen; import java.io.File; import java.lang.reflect.Field; @@ -10,46 +10,46 @@ import cyano.lootable.entities.EntityLootableBody; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.button.Button; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.gui.screen.inventory.InventoryScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; import net.minecraft.client.resources.SkinManager; -import net.minecraft.util.ScreenShotHelper; -import net.minecraft.world.*; +import net.minecraft.client.Screenshot; +import net.minecraft.world.level.GameType; final class VisibilitySkinClientChecks { private int stage,frames; private Field skinField; private SkinManager original; void tick(Minecraft mc) throws Exception { - if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; - FixtureApi.ClientWorlds.create(mc,"skin-fixture","Skin fixture",net.minecraft.world.GameType.SURVIVAL,true);stage=1; - } else if(stage==1 && mc.world!=null && mc.player!=null && ++frames>60) { + if(stage==0 && mc.screen instanceof TitleScreen) { + mc.options.pauseOnLostFocus=false;mc.options.framerateLimit=30;mc.options.renderDistance=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); for(Field field:Minecraft.class.getDeclaredFields())if(field.getType()==SkinManager.class){skinField=field;skinField.setAccessible(true);} original=(SkinManager)skinField.get(mc); - skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDir,"skin-fixture"),mc.getSessionService()) { - @Override public Map loadSkinFromCache(GameProfile p){throw new IllegalStateException("injected render skin cache failure");} + skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture"),mc.getMinecraftSessionService()) { + @Override public Map getInsecureSkinInformation(GameProfile p){throw new IllegalStateException("injected render skin cache failure");} }); - mc.displayGuiScreen(new Panel(mc));stage=2;frames=0; + mc.setScreen(new Panel(mc));stage=2;frames=0; } else if(stage==2 && ++frames>30) { - ScreenShotHelper.saveScreenshot(mc.gameDir,"skin-fallback-models.png",mc.getMainWindow().getFramebufferWidth(),mc.getMainWindow().getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); - skinField.set(mc,original);new File(mc.gameDir,"client.pass").createNewFile(); - System.out.println("LOOTABLE_BODIES_VISIBILITY_SKIN_PASS");mc.shutdown();stage=3; + Screenshot.grab(mc.gameDirectory,"skin-fallback-models.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + skinField.set(mc,original);new File(mc.gameDirectory,"client.pass").createNewFile(); + System.out.println("LOOTABLE_BODIES_VISIBILITY_SKIN_PASS");mc.stop();stage=3; } } private static final class Panel extends Screen { private final EntityLootableBody[] bodies=new EntityLootableBody[3]; - Panel(Minecraft mc){super(new net.minecraft.util.text.StringTextComponent("Skin visibility fixture"));for(int i=0;i<3;i++){bodies[i]=new EntityLootableBody(mc.world);bodies[i].setGameProfile(new GameProfile(new UUID(0,512+i),"VisibilityModel"));}} - @Override public void render(com.mojang.blaze3d.matrix.MatrixStack matrices,int mouseX,int mouseY,float partial) { + 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"));}} + @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.drawEntityOnScreen(x,height*2/3,65,20,-20,bodies[i]); + InventoryScreen.renderEntityInInventory(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 055d10b..5b047e3 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java @@ -1,30 +1,33 @@ package cyano.lootable.buildtest; -import net.minecraft.block.BlockState; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.ByteArrayNBT; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ByteArrayTag; import cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; -import net.minecraft.block.SnowBlock; -import net.minecraft.entity.EntityType; -import net.minecraft.block.Blocks; -import net.minecraft.nbt.*; +import net.minecraft.world.level.block.SnowLayerBlock; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.server.MinecraftServer; -import net.minecraft.util.DamageSource; -import net.minecraft.util.Direction; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.core.Direction; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; /** Real block collision and chunk-provider regressions; never shipped with the mod. */ final class WallEscapeChecks { private interface Check { void run() throws Exception; } - private static ServerWorld world; + private static ServerLevel world; private static int sequence, failures; private static final List results = new ArrayList<>(); - static void run(MinecraftServer server, ServerWorld target, File folder) throws Exception { + static void run(MinecraftServer server, ServerLevel target, File folder) throws Exception { world = target; sequence = 0; failures = 0; results.clear(); check("centred_body_never_uses_NaN_as_destination", WallEscapeChecks::centred); for (Direction direction : Direction.values()) @@ -63,28 +66,28 @@ private static BlockPos site(boolean negative) { private static void terrain(BlockPos centre, boolean solid) { for (int x = -2; x <= 2; x++) for (int y = -2; y <= 3; y++) for (int z = -2; z <= 2; z++) - world.setBlockState(centre.add(x,y,z), (solid || y == -1 ? Blocks.STONE : Blocks.AIR).getDefaultState(), 2); + world.setBlock(centre.offset(x,y,z), (solid || y == -1 ? Blocks.STONE : Blocks.AIR).defaultBlockState(), 2); } private static EntityLootableBody body(BlockPos centre) { EntityLootableBody body = new EntityLootableBody(world); - body.setPosition(centre.getX()+.5, centre.getY()+.015625, centre.getZ()+.5); + body.setPos(centre.getX()+.5, centre.getY()+.015625, centre.getZ()+.5); body.setNoGravity(true); body.setUserName("Wall Escape Fixture"); - for (int slot = 0; slot < 48; slot++) body.setInventorySlotContents(slot, RuntimeChecks.stack(slot)); + for (int slot = 0; slot < 48; slot++) body.setItem(slot, RuntimeChecks.stack(slot)); body.addItem(RuntimeChecks.stack(70)); body.addItem(RuntimeChecks.stack(71)); RuntimeChecks.disableVacuum(body); - CompoundNBT saved = NameLifetimeChecks.saved(body); - CompoundNBT unresolved = new CompoundNBT(); + CompoundTag saved = NameLifetimeChecks.saved(body); + CompoundTag unresolved = new CompoundTag(); unresolved.putString("Source", "FutureFixture"); unresolved.putString("Data", "retain verbatim"); saved.put("LootableBodiesUnresolved", NbtRecoveryChecks.list(unresolved)); - CompoundNBT invalid = new CompoundNBT(); - invalid.putString("Source", "FutureMetadata"); invalid.put("Data", new ByteArrayNBT(new byte[]{4,3,2,1})); + CompoundTag invalid = new CompoundTag(); + invalid.putString("Source", "FutureMetadata"); invalid.put("Data", new ByteArrayTag(new byte[]{4,3,2,1})); saved.put("LootableBodiesInvalidFields", NbtRecoveryChecks.list(invalid)); return (EntityLootableBody) FixtureApi.loadEntity(saved, world); } - private static void unchanged(CompoundNBT before, EntityLootableBody body) { - CompoundNBT after = NameLifetimeChecks.saved(body); + private static void unchanged(CompoundTag before, EntityLootableBody body) { + CompoundTag after = NameLifetimeChecks.saved(body); before = before.copy(); before.remove("Pos"); after.remove("Pos"); RuntimeChecks.require(TypedNbt.sameEntity(before, after), "Escape changed saved fields besides position"); PositionMotionChecks.finite(body); @@ -92,51 +95,51 @@ private static void unchanged(CompoundNBT before, EntityLootableBody body) { private static void clear(EntityLootableBody body) { PositionMotionChecks.finite(body); - RuntimeChecks.require(world.hasNoCollisions(body,body.getBoundingBox()), "Escape left body intersecting terrain"); + RuntimeChecks.require(world.noCollision(body,body.getBoundingBox()), "Escape left body intersecting terrain"); } private static void centred() { - BlockPos centre = site(false); terrain(centre, false); world.setBlockState(centre, Blocks.STONE.getDefaultState(), 2); + BlockPos centre = site(false); terrain(centre, false); world.setBlock(centre, Blocks.STONE.defaultBlockState(), 2); // A zero-vector lookup previously sampled (0,0,0). Test it as air, too. - net.minecraft.block.BlockState origin = world.getBlockState(BlockPos.ZERO); + net.minecraft.world.level.block.state.BlockState origin = world.getBlockState(BlockPos.ZERO); try { world.removeBlock(BlockPos.ZERO,false); - EntityLootableBody body = body(centre); CompoundNBT before = NameLifetimeChecks.saved(body); + EntityLootableBody body = body(centre); CompoundTag before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); - RuntimeChecks.require(body.getPosY() == centre.getY()+1.015625, "Centred body missed the existing upward exit priority"); - } finally { world.setBlockState(BlockPos.ZERO, origin, 2); } + RuntimeChecks.require(body.getY() == centre.getY()+1.015625, "Centred body missed the existing upward exit priority"); + } finally { world.setBlock(BlockPos.ZERO, origin, 2); } } private static void oneExit(Direction direction) { - BlockPos centre = site(false); terrain(centre, true); BlockPos exit = centre.offset(direction); world.removeBlock(exit,false); - EntityLootableBody body = body(centre); CompoundNBT before = NameLifetimeChecks.saved(body); + BlockPos centre = site(false); terrain(centre, true); BlockPos exit = centre.relative(direction); world.removeBlock(exit,false); + EntityLootableBody body = body(centre); CompoundTag before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); - RuntimeChecks.require(body.getPosX() == exit.getX()+.5 && body.getPosY() == exit.getY()+.015625 && body.getPosZ() == exit.getZ()+.5, "Missed sole adjacent exit"); + RuntimeChecks.require(body.getX() == exit.getX()+.5 && body.getY() == exit.getY()+.015625 && body.getZ() == exit.getZ()+.5, "Missed sole adjacent exit"); } private static void overlap() { - BlockPos centre = site(false); terrain(centre, false); world.setBlockState(centre, Blocks.STONE.getDefaultState(), 2); - EntityLootableBody body = body(centre); body.setPosition(centre.getX()+.1, centre.getY()+.015625, centre.getZ()+.5); - CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); + BlockPos centre = site(false); terrain(centre, false); world.setBlock(centre, Blocks.STONE.defaultBlockState(), 2); + EntityLootableBody body = body(centre); body.setPos(centre.getX()+.1, centre.getY()+.015625, centre.getZ()+.5); + CompoundTag before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); } private static void snow() { BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.north(),false); - world.setBlockState(centre.up(), Blocks.SNOW.getDefaultState().with(net.minecraft.block.SnowBlock.LAYERS, 8), 2); + world.setBlock(centre.above(), Blocks.SNOW.defaultBlockState().setValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS, 8), 2); EntityLootableBody body = body(centre); body.jumpOutOfWall(); clear(body); - RuntimeChecks.require(body.getPosition().equals(centre.north()), "Snow collision was ignored"); + RuntimeChecks.require(body.blockPosition().equals(centre.north()), "Snow collision was ignored"); } private static void fence() { BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.north(),false); world.removeBlock(centre.east(),false); - world.setBlockState(centre.north().down(), Blocks.OAK_FENCE.getDefaultState(), 2); + world.setBlock(centre.north().below(), Blocks.OAK_FENCE.defaultBlockState(), 2); EntityLootableBody body = body(centre); body.jumpOutOfWall(); clear(body); - RuntimeChecks.require(body.getPosition().equals(centre.east()), "Tall collision below destination was ignored"); + RuntimeChecks.require(body.blockPosition().equals(centre.east()), "Tall collision below destination was ignored"); } private static void sealed() { BlockPos centre = site(false); terrain(centre, true); EntityLootableBody body = body(centre); - CompoundNBT before = NameLifetimeChecks.saved(body); + CompoundTag before = NameLifetimeChecks.saved(body); for (int i = 0; i < 32; i++) body.jumpOutOfWall(); RuntimeChecks.require(TypedNbt.sameEntity(before, NameLifetimeChecks.saved(body)), "Sealed corpse drifted or changed saved data"); PositionMotionChecks.finite(body); @@ -144,21 +147,21 @@ private static void sealed() { private static void alreadyClear() { BlockPos centre = site(false); terrain(centre, false); EntityLootableBody body = body(centre); - CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); + CompoundTag before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); RuntimeChecks.require(TypedNbt.sameEntity(before, NameLifetimeChecks.saved(body)), "Clear corpse teleported on stale suffocation damage"); } private static void negative() { BlockPos centre = site(true); terrain(centre, true); world.removeBlock(centre.west(),false); - EntityLootableBody body = body(centre); CompoundNBT before = NameLifetimeChecks.saved(body); + EntityLootableBody body = body(centre); CompoundTag before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); - RuntimeChecks.require(body.getPosition().equals(centre.west()), "Negative coordinate exit was rounded incorrectly"); + RuntimeChecks.require(body.blockPosition().equals(centre.west()), "Negative coordinate exit was rounded incorrectly"); } private static void damage() { - BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.up(),false); + BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.above(),false); EntityLootableBody body = body(centre); float health = body.getHealth(); - body.attackEntityFrom(DamageSource.IN_WALL, 1); clear(body); + body.hurt(DamageSource.IN_WALL, 1); clear(body); RuntimeChecks.require(body.isAlive() && body.getHealth() == health, "Default suffocation health rules changed"); } @@ -167,58 +170,60 @@ private static void unloaded(Direction direction) throws Exception { int x = centre.getX() >> 4, z = centre.getZ() >> 4; centre = new BlockPos(x*16+(direction == Direction.WEST ? 0 : direction == Direction.EAST ? 15 : 8), 80, z*16+(direction == Direction.NORTH ? 0 : direction == Direction.SOUTH ? 15 : 8)); - terrain(centre, true); world.removeBlock(centre.offset(direction),false); - int nx = x+direction.getXOffset(), nz = z+direction.getZOffset(); + terrain(centre, true); world.removeBlock(centre.relative(direction),false); + int nx = x+direction.getStepX(), nz = z+direction.getStepZ(); FixtureApi.unload(world,nx,nz); - RuntimeChecks.require(!world.getChunkProvider().chunkExists(nx,nz), "Fixture neighbour did not unload"); - EntityLootableBody body = body(centre); CompoundNBT before = NameLifetimeChecks.saved(body); + RuntimeChecks.require(!world.getChunkSource().hasChunk(nx,nz), "Fixture neighbour did not unload"); + EntityLootableBody body = body(centre); CompoundTag before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); - RuntimeChecks.require(!world.getChunkProvider().chunkExists(nx,nz), "Escape forced an unloaded neighbour to load"); + RuntimeChecks.require(!world.getChunkSource().hasChunk(nx,nz), "Escape forced an unloaded neighbour to load"); RuntimeChecks.require(TypedNbt.sameEntity(before, NameLifetimeChecks.saved(body)), "Escape moved without loaded terrain"); world.getChunk(nx,nz); body.jumpOutOfWall(); clear(body); unchanged(before, body); } private static void ticking() { BlockPos centre = site(false); terrain(centre, true); EntityLootableBody body = body(centre); - CompoundNBT before = NameLifetimeChecks.saved(body); + CompoundTag before = NameLifetimeChecks.saved(body); for (int i = 0; i < 100; i++) body.tick(); - RuntimeChecks.require(body.getPosX() == centre.getX()+.5 && body.getPosY() == centre.getY()+.015625 && body.getPosZ() == centre.getZ()+.5 && body.isAlive(), "Ticking sealed corpse drifted or died"); + RuntimeChecks.require(body.getX() == centre.getX()+.5 && body.getY() == centre.getY()+.015625 && body.getZ() == centre.getZ()+.5 && body.isAlive(), "Ticking sealed corpse drifted or died"); for (String key : new String[]{"Equipment","Aux","LootableBodiesUnresolved","LootableBodiesInvalidFields","DeathTime","DecayTime"}) RuntimeChecks.require(NameLifetimeChecks.same(before.get(key),NameLifetimeChecks.saved(body).get(key)), "Ticking escape changed " + key); - world.removeBlock(centre.up(),false); + world.removeBlock(centre.above(),false); for (int i = 0; i < 30; i++) body.tick(); - clear(body); RuntimeChecks.require(body.getPosition().equals(centre.up()), "Ticking corpse did not use newly opened exit"); + clear(body); RuntimeChecks.require(body.blockPosition().equals(centre.above()), "Ticking corpse did not use newly opened exit"); } private static void seed(File folder) throws Exception { for (boolean trapped : new boolean[]{false,true}) { BlockPos centre = site(false); terrain(centre, true); - if (!trapped) world.removeBlock(centre.up(),false); + if (!trapped) world.removeBlock(centre.above(),false); EntityLootableBody body = body(centre); - body.setUniqueId(UUID.nameUUIDFromBytes(("wall-escape-"+trapped).getBytes(java.nio.charset.StandardCharsets.UTF_8))); - CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); unchanged(before,body); + body.setUUID(UUID.nameUUIDFromBytes(("wall-escape-"+trapped).getBytes(java.nio.charset.StandardCharsets.UTF_8))); + CompoundTag before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); unchanged(before,body); if (!trapped) clear(body); else RuntimeChecks.require(TypedNbt.sameEntity(before,NameLifetimeChecks.saved(body)), "Sealed disk fixture moved"); - RuntimeChecks.awaitChunk(world,body.getPosition().getX()>>4,body.getPosition().getZ()>>4); - RuntimeChecks.require(world.addEntity(body), "Could not save wall fixture"); - CompressedStreamTools.writeCompressed(NameLifetimeChecks.saved(body), new FileOutputStream(new File(folder,"wall-escape-"+trapped+".nbt"))); + RuntimeChecks.awaitChunk(world,body.blockPosition().getX()>>4,body.blockPosition().getZ()>>4); + RuntimeChecks.require(world.addFreshEntity(body), "Could not save wall fixture"); + NbtIo.writeCompressed(NameLifetimeChecks.saved(body), new FileOutputStream(new File(folder,"wall-escape-"+trapped+".nbt"))); } } - static void reload(ServerWorld target, File folder) throws Exception { + static void reload(ServerLevel target, File folder) throws Exception { world = target; for (boolean trapped : new boolean[]{false,true}) { - CompoundNBT expected; - try (InputStream in = new FileInputStream(new File(folder,"wall-escape-"+trapped+".nbt"))) { expected = CompressedStreamTools.readCompressed(in); } - ListNBT pos = expected.getList("Pos",6); - world.getChunk(new BlockPos(pos.getDouble(0),pos.getDouble(1),pos.getDouble(2))); - EntityLootableBody body = RuntimeChecks.find(world, expected.getUniqueId("UUID")); - CompoundNBT actual=NameLifetimeChecks.saved(body); + 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)); + // 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")); + CompoundTag actual=NameLifetimeChecks.saved(body); if(!TypedNbt.sameEntity(expected,actual)) { - try(OutputStream out=new FileOutputStream(new File(folder,"wall-reload-actual-"+trapped+".nbt"))){CompressedStreamTools.writeCompressed(actual,out);} - for(String key:expected.keySet())if(!NameLifetimeChecks.same(expected.get(key),actual.get(key))) + try(OutputStream out=new FileOutputStream(new File(folder,"wall-reload-actual-"+trapped+".nbt"))){NbtIo.writeCompressed(actual,out);} + for(String key:expected.getAllKeys())if(!NameLifetimeChecks.same(expected.get(key),actual.get(key))) System.out.println("WALL_RELOAD_DIFF key="+key+" expected="+expected.get(key)+" actual="+actual.get(key)); - for(String key:actual.keySet())if(!expected.contains(key))System.out.println("WALL_RELOAD_EXTRA key="+key+" value="+actual.get(key)); + for(String key:actual.getAllKeys())if(!expected.contains(key))System.out.println("WALL_RELOAD_EXTRA key="+key+" value="+actual.get(key)); } RuntimeChecks.require(TypedNbt.sameEntity(expected,NameLifetimeChecks.saved(body)), "Cold reload changed wall fixture NBT"); body.jumpOutOfWall(); @@ -230,25 +235,25 @@ static void reload(ServerWorld target, File folder) throws Exception { /** The sealed 2.2.0 Nether fixture that originally exposed random wall drift. */ static void historical(MinecraftServer server, File folder, boolean reload) throws Exception { - world = server.getWorld(FixtureApi.dimensionKey(-1)); + world = server.getLevel(FixtureApi.dimensionKey(-1)); for (int x = 7; x <= 9; x++) for (int z = 7; z <= 9; z++) world.getChunk(x,z); EntityLootableBody body = null; - for (net.minecraft.entity.Entity entity : FixtureApi.entities(world)) - if (entity instanceof EntityLootableBody && Math.abs(entity.getPosX()-128.5)<.001 && Math.abs(entity.getPosZ()-128.5)<.001) + for (net.minecraft.world.entity.Entity entity : FixtureApi.entities(world)) + if (entity instanceof EntityLootableBody && Math.abs(entity.getX()-128.5)<.001 && Math.abs(entity.getZ()-128.5)<.001) body = (EntityLootableBody) entity; RuntimeChecks.require(body != null, "Original Nether corpse not found at its saved position"); - RuntimeChecks.require(!world.hasNoCollisions(body,body.getBoundingBox()), "Historical buried-body terrain was cleared"); - CompoundNBT expected = NameLifetimeChecks.saved(body); + RuntimeChecks.require(!world.noCollision(body,body.getBoundingBox()), "Historical buried-body terrain was cleared"); + CompoundTag expected = NameLifetimeChecks.saved(body); File file = new File(folder,"wall-historical.nbt"); if (reload) { try (InputStream in = new FileInputStream(file)) { - RuntimeChecks.require(TypedNbt.sameEntity(CompressedStreamTools.readCompressed(in),expected), "Historical cold reload changed complete corpse NBT"); + RuntimeChecks.require(TypedNbt.sameEntity(NbtIo.readCompressed(in),expected), "Historical cold reload changed complete corpse NBT"); } } for (int i = 0; i < 32; i++) body.jumpOutOfWall(); RuntimeChecks.require(TypedNbt.sameEntity(expected,NameLifetimeChecks.saved(body)), "Historical sealed corpse drifted or lost saved data"); PositionMotionChecks.finite(body); - if (!reload) try (OutputStream out = new FileOutputStream(file)) { CompressedStreamTools.writeCompressed(expected,out); } - System.out.println("LOOTABLE_BODIES_WALL_HISTORICAL_PASS reload="+reload+" slots="+body.getSizeInventory()); + if (!reload) try (OutputStream out = new FileOutputStream(file)) { NbtIo.writeCompressed(expected,out); } + System.out.println("LOOTABLE_BODIES_WALL_HISTORICAL_PASS reload="+reload+" slots="+body.getContainerSize()); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java index d35b642..1e11071 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java @@ -1,13 +1,13 @@ package cyano.lootable.buildtest; -import net.minecraft.block.Blocks; -import net.minecraft.item.Items; -import net.minecraft.inventory.container.Slot; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.inventory.Inventory; -import net.minecraft.nbt.INBT; -import net.minecraft.nbt.ListNBT; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.dispenser.Position; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.item.Items; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.SimpleContainer; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.PositionImpl; import com.mojang.authlib.GameProfile; import cyano.lootable.LootableBodies; @@ -15,55 +15,59 @@ import cyano.lootable.graphics.CorpseContainer; import java.io.*; import java.util.*; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.inventory.*; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.*; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.management.PlayerInteractionManager; -import net.minecraft.tileentity.TileEntity; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.server.ServerWorld; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraftforge.common.util.FakePlayerFactory; /** Acceptance against worlds written by the actual historical production jars. */ final class WorldUpgradeChecks { - private static CompoundNBT manifest,ledger; + private static CompoundTag manifest,ledger; private static File folder; private static int sourceVersion; private static int checks; static void run(MinecraftServer server,String phase) throws Exception { - folder=server.getWorld(FixtureApi.dimensionKey(0)).getServer().func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile(); + folder=server.getLevel(FixtureApi.dimensionKey(0)).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); manifest=read(new File(folder,"upgrade-fixtures.nbt"));sourceVersion=HistoricalVersions.dataVersion(manifest.getString("Minecraft")); - File stored=new File(folder,"upgrade-recovered.nbt");ledger=stored.isFile()?read(stored):new CompoundNBT();checks=0; - if(!ledger.contains("Recoveries",10))ledger.put("Recoveries",new CompoundNBT()); + File stored=new File(folder,"upgrade-recovered.nbt");ledger=stored.isFile()?read(stored):new CompoundTag();checks=0; + if(!ledger.contains("Recoveries",10))ledger.put("Recoveries",new CompoundTag()); LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1; int wave=phase.startsWith("upgrade-near")?0:phase.startsWith("upgrade-far")?1:phase.equals("upgrade-late")?2:-1; boolean recover=phase.equals("upgrade-near-recover") || phase.equals("upgrade-far-reload") || phase.equals("upgrade-late") || phase.equals("upgrade-return"); - ListNBT bodies=manifest.getList("Bodies",10); + ListTag bodies=manifest.getList("Bodies",10); for(int i=0;i=0 && record.getInt("Wave")!=wave)continue; - ServerWorld world=server.getWorld(FixtureApi.dimensionKey(record.getInt("Dimension")));CompoundNBT source=record.getCompound("Body"); - int x=(int)source.getList("Pos",6).getDouble(0),z=(int)source.getList("Pos",6).getDouble(2);RuntimeChecks.awaitChunk(world,x>>4,z>>4); + CompoundTag record=bodies.getCompound(i);if(wave>=0 && record.getInt("Wave")!=wave)continue; + ServerLevel world=server.getLevel(FixtureApi.dimensionKey(record.getInt("Dimension")));CompoundTag source=record.getCompound("Body"); + int x=net.minecraft.util.Mth.floor(source.getList("Pos",6).getDouble(0)),z=net.minecraft.util.Mth.floor(source.getList("Pos",6).getDouble(2));RuntimeChecks.awaitChunk(world,x>>4,z>>4); UUID uuid=HistoricalUuid.read(source,"UUID");EntityLootableBody body=find(world,uuid);String id=uuid.toString(); if(ledger.getString("Removed").equals(id)) {require(body==null,"Journal resurrected a deliberately removed corpse");continue;} require(body!=null,"Missing historical corpse "+uuid); - require(body.getUniqueID().equals(uuid) && FixtureApi.dimensionId(body.world)==record.getInt("Dimension"),"UUID/dimension changed"); + require(body.getUUID().equals(uuid) && FixtureApi.dimensionId(body.level)==record.getInt("Dimension"),"UUID/dimension changed"); require(body.getCustomNameTag().equals(source.contains("Name",8)?source.getString("Name"):source.getString("CustomName")),"Owner attribution changed"); - require(body.getPosX()==source.getList("Pos",6).getDouble(0) && body.getPosY()==source.getList("Pos",6).getDouble(1) && body.getPosZ()==source.getList("Pos",6).getDouble(2),"Position changed in dimension "+FixtureApi.dimensionId(world)+": "+body.getPositionVec()); - CompoundNBT actual=save(body);require(actual.getInt("LootableBodiesDataVersion")==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Missing corpse migration marker"); + require(body.getX()==source.getList("Pos",6).getDouble(0) && body.getY()==source.getList("Pos",6).getDouble(1) && body.getZ()==source.getList("Pos",6).getDouble(2),"Position changed in dimension "+FixtureApi.dimensionId(world)+": "+body.position()); + CompoundTag actual=save(body);require(actual.getInt("LootableBodiesDataVersion")==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Missing corpse migration marker"); require(NameLifetimeChecks.same(source.get("LootableBodiesInvalidFields"),actual.get("LootableBodiesInvalidFields")),"Original invalid-field evidence changed"); Map expected=expectedInventory(source);Map observed=inventory(actual); - CompoundNBT recovery=ledger.getCompound("Recoveries").getCompound(id); + CompoundTag recovery=ledger.getCompound("Recoveries").getCompound(id); if(!recovery.contains("Storage"))verifySlots(source,actual); Map recovered=storage(world,recovery); merge(observed,recovered);require(expected.equals(observed),"Inventory conservation mismatch "+uuid+" expected="+expected+" actual="+observed); verifyRaw(source,actual); if(recover) { - if(!ledger.getCompound("Recoveries").contains(id,10)) {ledger.getCompound("Recoveries").put(id,new CompoundNBT());recovery=ledger.getCompound("Recoveries").getCompound(id);} + if(!ledger.getCompound("Recoveries").contains(id,10)) {ledger.getCompound("Recoveries").put(id,new CompoundTag());recovery=ledger.getCompound("Recoveries").getCompound(id);} recover(world,body,recovery); require(inventory(save(body)).isEmpty(),"Supported inventory remained after GUI recovery"); require(expected.equals(storage(world,recovery)),"GUI recovery lost or duplicated complete item data"); @@ -76,69 +80,69 @@ static void run(MinecraftServer server,String phase) throws Exception { if(wave==0 || wave<0) verifyVanilla(server); if(phase.equals("upgrade-final") && ledger.getString("Removed").isEmpty()) { for(int i=0;i slots=new TreeMap<>(); - ListNBT entries=source.getList("Equipment",10); + private static void verifySlots(CompoundTag source,CompoundTag actual) { + Map slots=new TreeMap<>(); + ListTag entries=source.getList("Equipment",10); for(int i=0;i=48 || slots.containsKey(slot))continue; item.remove("Slot");slots.put(slot,item); } - Map saved=new TreeMap<>();ListNBT entriesNow=actual.getList("Equipment",10); - for(int i=0;i entry:slots.entrySet()) + Map saved=new TreeMap<>();ListTag entriesNow=actual.getList("Equipment",10); + for(int i=0;i entry:slots.entrySet()) require(Objects.equals(TypedNbt.canonical(canonicalItem(entry.getValue())),TypedNbt.canonical(canonicalItem(saved.get(entry.getKey())))),"Historical inventory slot moved: "+entry.getKey()+" expected="+entry.getValue()+" actual="+saved.get(entry.getKey())); } - private static CompoundNBT canonicalItem(CompoundNBT source) { + private static CompoundTag canonicalItem(CompoundTag source) { if(source==null)return null; - CompoundNBT item=source.copy();String id=item.getString("id"); + CompoundTag item=source.copy();String id=item.getString("id"); String[] colours={"white","orange","magenta","light_blue","yellow","lime","pink","gray","light_gray","cyan","purple","blue","brown","green","red","black"}; for(int colour=0;colour map,CompoundNBT source,boolean indexed) { + private static void add(Map map,CompoundTag source,boolean indexed) { if(!source.contains("id",8) || source.getByte("Count")<=0)return; - CompoundNBT item=canonicalItem(source);int count=item.getByte("Count")&255;item.remove("Count");if(!indexed)item.remove("Slot"); + CompoundTag item=canonicalItem(source);int count=item.getByte("Count")&255;item.remove("Count");if(!indexed)item.remove("Slot"); String key=TypedNbt.canonical(item);map.put(key,map.getOrDefault(key,0)+count); } private static void merge(Map a,Map b){for(Map.Entry item:b.entrySet())a.put(item.getKey(),a.getOrDefault(item.getKey(),0)+item.getValue());} - private static Map inventory(CompoundNBT body){Map map=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}){ListNBT items=body.getList(key,10);for(int i=0;i expectedInventory(CompoundNBT source) { - Map expected=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}) {ListNBT list=source.getList(key,10);for(int i=0;i inventory(CompoundTag body){Map map=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}){ListTag items=body.getList(key,10);for(int i=0;i expectedInventory(CompoundTag source) { + Map expected=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}) {ListTag list=source.getList(key,10);for(int i=0;i expected=new ArrayList<>(); - for(String key:new String[]{"Equipment","Aux"}) {ListNBT list=source.getList(key,10);for(int i=0;i expected=new ArrayList<>(); + for(String key:new String[]{"Equipment","Aux"}) {ListTag list=source.getList(key,10);for(int i=0;i contents(IInventory inventory,boolean indexed) {Map map=new TreeMap<>();for(int slot=0;slot indexed(ListNBT list) {Map map=new TreeMap<>();for(int i=0;i storage(ServerWorld world,CompoundNBT recovery) {Map map=new TreeMap<>();ListNBT list=storageList(recovery);for(int i=0;i contents(Container inventory,boolean indexed) {Map map=new TreeMap<>();for(int slot=0;slot indexed(ListTag list) {Map map=new TreeMap<>();for(int i=0;i storage(ServerLevel world,CompoundTag recovery) {Map map=new TreeMap<>();ListTag list=storageList(recovery);for(int i=0;i0) {destination=(IInventory)world.getTileEntity(position(stored.getCompound(stored.size()-1)));for(int i=0;i0) {destination=(Container)world.getBlockEntity(position(stored.getCompound(stored.size()-1)));for(int i=0;i expected=indexed(source.getList("Items",10)),actual=contents((IInventory)tile,true);if(!expected.equals(actual)){Map missing=new TreeMap<>(expected),unexpected=new TreeMap<>(actual);missing.entrySet().removeAll(actual.entrySet());unexpected.entrySet().removeAll(expected.entrySet());System.out.println("VANILLA_CHEST_DIFF missing="+missing+" unexpected="+unexpected);}require(expected.equals(actual),"Vanilla source chest slots/data changed");} + private static void verifyChest(ServerLevel world,int wave) { + ListTag containers=manifest.getList("Containers",10); + for(int i=0;i expected=indexed(source.getList("Items",10)),actual=contents((Container)tile,true);if(!expected.equals(actual)){Map missing=new TreeMap<>(expected),unexpected=new TreeMap<>(actual);missing.entrySet().removeAll(actual.entrySet());unexpected.entrySet().removeAll(expected.entrySet());System.out.println("VANILLA_CHEST_DIFF missing="+missing+" unexpected="+unexpected);}require(expected.equals(actual),"Vanilla source chest slots/data changed");} } private static void verifyPlayer(MinecraftServer server) throws Exception { - ServerWorld world=server.getWorld(FixtureApi.dimensionKey(0));UUID id=HistoricalUuid.read(manifest,"Player"); - CompoundNBT expected=read(new File(folder,"upgrade-player-source.nbt")); - ServerPlayerEntity player=new ServerPlayerEntity(server,world,new GameProfile(id,"UpgradeFixture"),new PlayerInteractionManager(world));FixtureApi.playerData(server).loadPlayerData(player); + ServerLevel world=server.getLevel(FixtureApi.dimensionKey(0));UUID id=HistoricalUuid.read(manifest,"Player"); + CompoundTag expected=read(new File(folder,"upgrade-player-source.nbt")); + ServerPlayer player=new ServerPlayer(server,world,new GameProfile(id,"UpgradeFixture"));FixtureApi.playerData(server).load(player); // PlayerInventory serializes armour as 100..103 and offhand as 150. - ListNBT actual=player.inventory.write(new ListNBT());require(indexedPlayer(expected,"Inventory").equals(indexedCurrent(actual)),"Player inventory slots, counts or custom data changed"); - require(indexedPlayer(expected,"EnderItems").equals(indexedCurrent(player.getInventoryEnderChest().write())),"Ender chest slots/data changed"); - require(player.getUniqueID().equals(id),"Player UUID changed"); + ListTag actual=player.getInventory().save(new ListTag());require(indexedPlayer(expected,"Inventory").equals(indexedCurrent(actual)),"Player inventory slots, counts or custom data changed"); + require(indexedPlayer(expected,"EnderItems").equals(indexedCurrent(player.getEnderChestInventory().createTag())),"Ender chest slots/data changed"); + require(player.getUUID().equals(id),"Player UUID changed"); if(manifest.contains("Recipe",8)) { require(HistoricalUnlocks.sugarPair(manifest.getString("Recipe"),manifest.getString("Advancement")),"Unexpected historical recipe/advancement fixture"); - net.minecraft.item.crafting.IRecipe recipe=server.getRecipeManager().getRecipe(new ResourceLocation("minecraft:sugar_from_sugar_cane")).orElse(null); - require(recipe!=null && player.getRecipeBook().isUnlocked(recipe),"Native sugar recipe conversion lost its unlock"); - net.minecraft.advancements.Advancement advancement=server.getAdvancementManager().getAdvancement(new ResourceLocation("minecraft:recipes/misc/sugar_from_sugar_cane")); - require(advancement!=null && player.getAdvancements().getProgress(advancement).isDone(),"Native sugar advancement conversion lost its completion"); + net.minecraft.world.item.crafting.Recipe recipe=server.getRecipeManager().byKey(new ResourceLocation("minecraft:sugar_from_sugar_cane")).orElse(null); + require(recipe!=null && player.getRecipeBook().contains(recipe),"Native sugar recipe conversion lost its unlock"); + net.minecraft.advancements.Advancement advancement=server.getAdvancements().getAdvancement(new ResourceLocation("minecraft:recipes/misc/sugar_from_sugar_cane")); + require(advancement!=null && player.getAdvancements().getOrStartProgress(advancement).isDone(),"Native sugar advancement conversion lost its completion"); player.getAdvancements().save(); } - FixtureApi.playerData(server).savePlayerData(player); + FixtureApi.playerData(server).save(player); } - private static void verifyBiomes(ServerWorld world,int wave) { - ListNBT samples=manifest.getList("BiomeSamples",10); + private static void verifyBiomes(ServerLevel world,int wave) { + ListTag samples=manifest.getList("BiomeSamples",10); for(int i=0;i indexedPlayer(CompoundNBT player,String key) { + private static Map indexedPlayer(CompoundTag player,String key) { int version=player.contains("DataVersion",3)?player.getInt("DataVersion"):(sourceVersion); Map map=new TreeMap<>(); - for(INBT entry:player.getList(key,10))add(map,expectedItem((CompoundNBT)entry,version),true); + for(Tag entry:player.getList(key,10))add(map,expectedItem((CompoundTag)entry,version),true); return map; } - private static Map indexedCurrent(ListNBT list){Map map=new TreeMap<>();for(int i=0;i indexedCurrent(ListTag list){Map map=new TreeMap<>();for(int i=0;i>4,((int)pos.getDouble(2))>>4);Entity found=null;for(Entity e:FixtureApi.entities(world))if(e.getUniqueID().equals(HistoricalUuid.read(source,"UUID")))found=e;require(found!=null,"Vanilla entity disappeared");CompoundNBT actual=new CompoundNBT();found.writeUnlessPassenger(actual);require(actual.getString("id").equals(record.getString("ExpectedType")),"Vanilla entity-name conversion failed");require(FixtureApi.dimensionId(found.world)==record.getInt("Dimension"),"Vanilla entity dimension changed");if(record.getString("ExpectedType").equals("minecraft:villager"))verifyVillager(source,actual);if(found instanceof IInventory)require(indexed(source.getList("Items",10)).equals(contents((IInventory)found,true)),"Vanilla minecart inventory changed");if(record.getString("ExpectedType").equals("minecraft:armor_stand"))require(indexedEquipment(source.getList("ArmorItems",10)).equals(indexedNativeEquipment(actual.getList("ArmorItems",10))),"Vanilla armour stand equipment changed");} + ListTag list=manifest.getList("Vanilla",10); + for(int i=0;i>4,net.minecraft.util.Mth.floor(pos.getDouble(2))>>4);Entity found=null;for(Entity e:FixtureApi.entities(world))if(e.getUUID().equals(HistoricalUuid.read(source,"UUID")))found=e;require(found!=null,"Vanilla entity disappeared");CompoundTag actual=new CompoundTag();found.saveAsPassenger(actual);require(actual.getString("id").equals(record.getString("ExpectedType")),"Vanilla entity-name conversion failed");require(FixtureApi.dimensionId(found.level)==record.getInt("Dimension"),"Vanilla entity dimension changed");if(record.getString("ExpectedType").equals("minecraft:villager"))verifyVillager(source,actual);if(found instanceof Container)require(indexed(source.getList("Items",10)).equals(contents((Container)found,true)),"Vanilla minecart inventory changed");if(record.getString("ExpectedType").equals("minecraft:armor_stand"))require(indexedEquipment(source.getList("ArmorItems",10)).equals(indexedNativeEquipment(actual.getList("ArmorItems",10))),"Vanilla armour stand equipment changed");} } - private static void verifyVillager(CompoundNBT source,CompoundNBT actual) { - CompoundNBT expected=cyano.lootable.upgrade.NativeNbt.update(NativeVanillaDataFixer.get(),net.minecraft.util.datafix.TypeReferences.ENTITY,source.copy(),sourceVersion,cyano.lootable.upgrade.UpgradePreservation.MINECRAFT_VERSION); + private static void verifyVillager(CompoundTag source,CompoundTag actual) { + CompoundTag expected=cyano.lootable.upgrade.NativeNbt.update(NativeVanillaDataFixer.get(),net.minecraft.util.datafix.fixes.References.ENTITY,source.copy(),sourceVersion,cyano.lootable.upgrade.UpgradePreservation.MINECRAFT_VERSION); require(NameLifetimeChecks.same(expected.get("VillagerData"),actual.get("VillagerData")),"Vanilla villager profession, career or level changed"); require(indexed(source.getList("Inventory",10)).equals(indexedCurrent(actual.getList("Inventory",10))),"Vanilla villager inventory changed"); } - private static Map indexedNativeEquipment(ListNBT list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(ListNBT list){Map map=new TreeMap<>();for(int i=0;i indexedNativeEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;inew ConfigGuiHandler.ConfigGuiFactory(new CorpseConfigGuiFactory())); FMLJavaModLoadingContext.get().getModEventBus().addListener(ClientProxy::setup); + FMLJavaModLoadingContext.get().getModEventBus().addListener(ClientProxy::renderers); } private static void setup(net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent event) { // The client creates these readers before mod construction. Replace // their cached fixer on the main thread, before opening any world. event.enqueueWork(()-> { net.minecraft.client.Minecraft client=net.minecraft.client.Minecraft.getInstance(); - cyano.lootable.upgrade.ChunkVersionContext.installClient(client,client.getSaveLoader()); + cyano.lootable.upgrade.ChunkVersionContext.installClient(client,client.getLevelSource()); + net.minecraft.client.gui.screens.MenuScreens.register( + LootableBodies.CORPSE_CONTAINER,CorpseGUIContainer::new); }); - RenderingRegistry.registerEntityRenderingHandler(LootableBodies.CORPSE,CorpseRenderer::new); - net.minecraft.client.gui.ScreenManager.registerFactory(LootableBodies.CORPSE_CONTAINER,CorpseGUIContainer::new); + } + private static void renderers(EntityRenderersEvent.RegisterRenderers event) { + event.registerEntityRenderer(LootableBodies.CORPSE,CorpseRenderer::new); } } diff --git a/src/main/java/cyano/lootable/ItemStacks.java b/src/main/java/cyano/lootable/ItemStacks.java index 3eaac03..81ae996 100644 --- a/src/main/java/cyano/lootable/ItemStacks.java +++ b/src/main/java/cyano/lootable/ItemStacks.java @@ -1,5 +1,5 @@ package cyano.lootable; -import net.minecraft.item.ItemStack; +import net.minecraft.world.item.ItemStack; /** Shared guard for legacy integrations and the native empty-stack sentinel. */ public final class ItemStacks { private ItemStacks() { } diff --git a/src/main/java/cyano/lootable/LootableBodies.java b/src/main/java/cyano/lootable/LootableBodies.java index c721d7c..e135da6 100644 --- a/src/main/java/cyano/lootable/LootableBodies.java +++ b/src/main/java/cyano/lootable/LootableBodies.java @@ -6,19 +6,19 @@ import cyano.lootable.events.PlayerDeathEventHandler; import cyano.lootable.upgrade.UpgradePreservation; import cyano.lootable.upgrade.ChunkVersionContext; -import net.minecraft.entity.EntityType; -import net.minecraft.item.Item; -import net.minecraft.item.ItemGroup; -import net.minecraft.item.SpawnEggItem; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.CreativeModeTab; +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.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.DistExecutor; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent; -import net.minecraftforge.fml.event.server.FMLServerStoppedEvent; +import net.minecraftforge.fmlserverevents.FMLServerAboutToStartEvent; +import net.minecraftforge.fmlserverevents.FMLServerStoppedEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.loading.FMLPaths; @@ -26,11 +26,11 @@ 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.7.116051"; + public static final String VERSION = "2.4.8.117011"; public static final ResourceLocation CORPSE_ID = new ResourceLocation("lootablebodies:corpse"); public static EntityType CORPSE; public static Item CORPSE_EGG; - public static net.minecraft.inventory.container.ContainerType CORPSE_CONTAINER; + public static net.minecraft.world.inventory.MenuType CORPSE_CONTAINER; private static LootableBodies INSTANCE; private static LegacyConfiguration configuration; @@ -52,27 +52,27 @@ public final class LootableBodies { public LootableBodies() { INSTANCE=this; - UpgradePreservation.register(net.minecraft.util.datafix.DataFixesManager.getDataFixer()); + UpgradePreservation.register(net.minecraft.util.datafix.DataFixers.getDataFixer()); loadConfiguration(FMLPaths.CONFIGDIR.get().resolve("lootablebodies.cfg").toFile()); net.minecraftforge.eventbus.api.IEventBus bus=FMLJavaModLoadingContext.get().getModEventBus(); bus.addListener((net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent event) -> 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().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.entity.ai.brain.memory.MemoryModuleType.class,cyano.lootable.upgrade.LegacyRegistryMappings::retiredMemories); - bus.addGenericListener(net.minecraft.inventory.container.ContainerType.class,this::registerContainers); + 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); MinecraftForge.EVENT_BUS.register(new PlayerDeathEventHandler()); MinecraftForge.EVENT_BUS.register(this); DistExecutor.runWhenOn(Dist.CLIENT,()->ClientProxy::register); } private static synchronized EntityType corpseType() { if(CORPSE==null) { - EntityType.Builder type=EntityType.Builder.create((entityType,world)->new EntityLootableBody(world),net.minecraft.entity.EntityClassification.MISC) + EntityType.Builder type=EntityType.Builder.of((entityType,level)->new EntityLootableBody(level),net.minecraft.world.entity.MobCategory.MISC) // Native 1.14 expresses tracking range in chunks: two retain 32 blocks. - .size(0.85F,0.75F).setTrackingRange(2).setUpdateInterval(1).setShouldReceiveVelocityUpdates(true); - if(completelyInvulnerable || !hurtByEnvironment)type.immuneToFire(); + .sized(0.85F,0.75F).setTrackingRange(2).setUpdateInterval(1).setShouldReceiveVelocityUpdates(true); + if(completelyInvulnerable || !hurtByEnvironment)type.fireImmune(); CORPSE=type.build(CORPSE_ID.toString()); } return CORPSE; @@ -81,13 +81,13 @@ private void registerEntities(RegistryEvent.Register> event) { event.getRegistry().register(corpseType().setRegistryName(CORPSE_ID)); } private void registerItems(RegistryEvent.Register event) { - CORPSE_EGG=new SpawnEggItem(corpseType(),1,1,new Item.Properties().group(ItemGroup.MISC)) + 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 registerContainers(RegistryEvent.Register> event) { + private void registerContainers(RegistryEvent.Register> event) { CORPSE_CONTAINER=net.minecraftforge.common.extensions.IForgeContainerType.create((window,inventory,buffer)-> { - net.minecraft.entity.Entity body=inventory.player.world.getEntityByID(buffer.readVarInt()); + 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); }); @@ -95,10 +95,10 @@ private void registerContainers(RegistryEvent.Register mainInventory = NonNullList.withSize(6*7, ItemStack.EMPTY); private final Deque auxInventory = new LinkedList<>(); private static final String UNRESOLVED_ITEMS_TAG = "LootableBodiesUnresolved"; - private final List unresolvedInventory = new ArrayList<>(); - private CompoundNBT sourceVersions = new CompoundNBT(); - private CompoundNBT inventoryVersions = new CompoundNBT(); + private final List unresolvedInventory = new ArrayList<>(); + private CompoundTag sourceVersions = new CompoundTag(); + private CompoundTag inventoryVersions = new CompoundTag(); private static final String INVALID_FIELDS_TAG = "LootableBodiesInvalidFields"; // Evidence about metadata is separate from inventory and must not prevent decay. - private final List invalidFields = new ArrayList<>(); + private final List invalidFields = new ArrayList<>(); private final Map defaultAttributeBases = new HashMap<>(); private final Map> defaultAttributeModifiers = new HashMap<>(); private byte vacuumTime = 0; - private boolean positionNeedsChunk; - private boolean malformedPosition; - private INBT loadedPosition; - private boolean missingPositionX, missingPositionZ; private long deathTimestamp; private long decayTimestamp; @@ -90,7 +86,7 @@ public class EntityLootableBody extends MobEntity implements IInventory, net.min private static final int DEATH_COUNTDOWN = 10; - public EntityLootableBody(World w) { + public EntityLootableBody(Level w) { super(LootableBodies.CORPSE,w); // The native type map is registered once; retain the legacy constructor's // configured base and full initial health for each newly created corpse. @@ -98,10 +94,10 @@ public EntityLootableBody(World w) { setHealth(getMaxHealth()); // Keep MobEntity's actual defaults (including follow range 16 and // configured corpse health), rather than the generic attribute defaults. - for(ModifiableAttributeInstance attribute : corpseAttributes()) { + for(AttributeInstance attribute : corpseAttributes()) { String name = attribute.getAttribute().getRegistryName().toString(); defaultAttributeBases.put(name,attribute.getBaseValue()); - defaultAttributeModifiers.put(name,new ArrayList<>(attribute.getModifierListCopy())); + defaultAttributeModifiers.put(name,new ArrayList<>(attribute.getModifiers())); } deathTimestamp = w.getGameTime(); decayTimestamp = deathTimestamp; @@ -114,16 +110,16 @@ public EntityLootableBody(World w) { } - public EntityLootableBody(PlayerEntity player){ - this(player.getEntityWorld()); - this.setPosition(player.getPosX(), player.getPosY()+0.5, player.getPosZ()); + public EntityLootableBody(Player player){ + this(player.getCommandSenderWorld()); + this.setPos(player.getX(), player.getY()+0.5, player.getZ()); - this.setMotion(player.getMotion().add(0D,0.0784000015258789D,0D)); + this.setDeltaMovement(player.getDeltaMovement().add(0D,0.0784000015258789D,0D)); //this.newPosX = getPosX(); // no newPosX? //this.newPosY = getPosY(); // no newPosY? //this.newPosZ = getPosZ(); // no newPosZ? - this.markDirty(); + this.setChanged(); } @@ -139,15 +135,15 @@ private void log(Object o){ private String oldName = null; @Override public void tick() { - if(!getEntityWorld().isRemote) { + if(!getCommandSenderWorld().isClientSide) { // A Forge ticket can tick our centre chunk while neighbouring support // terrain is absent. Never treat those missing collision boxes as air. - AxisAlignedBB bounds = getBoundingBox(); - BlockPos from = new BlockPos(bounds.minX + Math.min(0D,getMotion().x), 0, - bounds.minZ + Math.min(0D,getMotion().z)); - BlockPos to = new BlockPos(bounds.maxX + Math.max(0D,getMotion().x), 0, - bounds.maxZ + Math.max(0D,getMotion().z)); - if(!getEntityWorld().isAreaLoaded(from,to)) return; + AABB bounds = getBoundingBox(); + BlockPos from = new BlockPos(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, + bounds.maxZ + Math.max(0D,getDeltaMovement().z)); + if(!getCommandSenderWorld().hasChunksAt(from,to)) return; } super.tick(); } @@ -156,7 +152,7 @@ public void tick() { public void baseTick(){ //log("position=%s; terminate=%s; time=%s; age=%s; deathTimestamp=%s; decayTimestamp=%s; Health=%s; isDead=%s" ,getPosition(),terminate,getEntityWorld().getTotalWorldTime(),getEntityWorld().getTotalWorldTime() - deathTimestamp, deathTimestamp, decayTimestamp, getHealth(), isDead); super.baseTick(); - final boolean isClient = getEntityWorld().isRemote; + final boolean isClient = getCommandSenderWorld().isClientSide; final boolean isServer = !isClient; if(isServer && terminate >= 0){ @@ -170,11 +166,11 @@ public void baseTick(){ if(terminate > 0) terminate--; if(terminate == 0){ // needs to be manually deleted for some reason - this.remove(); + this.discard(); } return; } - if(isServer && terminate < 0 && (this.getHealth() <= 0 || this.removed)){ + if(isServer && terminate < 0 && (this.getHealth() <= 0 || this.isRemoved())){ terminate = DEATH_COUNTDOWN; } @@ -194,7 +190,7 @@ public void baseTick(){ } // handle decay time - final long currentTime = Math.max(0L,getEntityWorld().getGameTime()); + final long currentTime = Math.max(0L,getCommandSenderWorld().getGameTime()); if(isServer) { // A clock rollback must not suspend decay indefinitely or overflow age. deathTimestamp = checkedTimestamp("DeathTime",deathTimestamp,currentTime); @@ -206,7 +202,7 @@ public void baseTick(){ } long age = currentTime - deathTimestamp; if(age > LootableBodies.corpseDecayTime){ - this.onKillCommand(); + this.kill(); } } @@ -224,10 +220,10 @@ public void baseTick(){ // move items from buffer to main inventory if(isServer && !auxInventory.isEmpty()) { - for (int slot = this.getSizeInventory() - 1; slot >= EQUIPMENT_SLOTS.length; slot--) { - if(emptyStack(this.getStackInSlot(slot))){ + for (int slot = this.getContainerSize() - 1; slot >= EQUIPMENT_SLOTS.length; slot--) { + if(emptyStack(this.getItem(slot))){ if(emptyStack(mergeItem(slot,auxInventory.peekFirst()))) auxInventory.removeFirst(); - this.markDirty(); + this.setChanged(); break; } } @@ -236,50 +232,44 @@ public void baseTick(){ // handle item vacuuming if(isServer && vacuumTime < VACUUM_TIMELIMIT){ vacuumTime++; - List items = getEntityWorld().getEntitiesWithinAABB(ItemEntity.class, new AxisAlignedBB( - getPosX() - VACUUM_RADIUS, getPosY() - VACUUM_RADIUS, getPosZ() - VACUUM_RADIUS, - getPosX() + VACUUM_RADIUS, getPosY() + VACUUM_RADIUS, getPosZ() + VACUUM_RADIUS)); + List items = getCommandSenderWorld().getEntitiesOfClass(ItemEntity.class, new AABB( + getX() - VACUUM_RADIUS, getY() - VACUUM_RADIUS, getZ() - VACUUM_RADIUS, + getX() + VACUUM_RADIUS, getY() + VACUUM_RADIUS, getZ() + VACUUM_RADIUS)); for(ItemEntity item : items){ - if(item.removed) continue; + if(item.isRemoved()) continue; ItemStack stack = item.getItem(); this.addItem(stack); - item.remove(); + item.discard(); } } - if(isServer && LootableBodies.corpseBuoyancy && vacuumTime >= VACUUM_TIMELIMIT && (this.inWater || this.isInLava())) { - this.setMotion(getMotion().x,(getMotion().y+0.025D)*0.9D,getMotion().z); + if(isServer && LootableBodies.corpseBuoyancy && vacuumTime >= VACUUM_TIMELIMIT && (this.wasTouchingWater || this.isInLava())) { + this.setDeltaMovement(getDeltaMovement().x,(getDeltaMovement().y+0.025D)*0.9D,getDeltaMovement().z); } - if(isServer && LootableBodies.voidPlatform && this.getPosY() < 0) { + if(isServer && LootableBodies.voidPlatform && this.getY() < 0) { generateVoidPlatform(); - this.setPositionAndUpdate((int)this.getPosX(), 1.5, (int)this.getPosZ()); + this.teleportTo((int)this.getX(), 1.5, (int)this.getZ()); } } public boolean handleWaterMovement() { - if (this.handleFluidAcceleration(net.minecraft.tags.FluidTags.WATER,0.014D)) { - this.fallDistance = 0.0F; - this.inWater = true; - this.extinguish(); - } else { - this.inWater = false; - } - - return this.inWater; + // Native 1.17 updates both water and lava in this entry point. + // Keep its fluid heights, fire clearing and current pushing together. + return super.updateInWaterStateAndDoFluidPushing(); } private void generateVoidPlatform() { - World world = this.world; - BlockPos testPos = new BlockPos((int)this.getPosX(), 0, (int)this.getPosZ()); + Level level = this.level; + BlockPos testPos = new BlockPos((int)this.getX(), 0, (int)this.getZ()); - BlockState platformBlockstate = getPlatformBlock(world.getDimensionKey()==World.THE_NETHER?-1:world.getDimensionKey()==World.THE_END?1:0); + BlockState platformBlockstate = getPlatformBlock(level.dimension()==Level.NETHER?-1:level.dimension()==Level.END?1:0); for(int j = -1; j <= 1; j++) { for(int k = -1; k <= 1; k++) { - if(world.isAirBlock(testPos.add(j,0,k))) - world.setBlockState(testPos.add(j,0,k), platformBlockstate, 3); + if(level.isEmptyBlock(testPos.offset(j,0,k))) + level.setBlock(testPos.offset(j,0,k), platformBlockstate, 3); } } } @@ -287,17 +277,17 @@ private void generateVoidPlatform() { private BlockState getPlatformBlock(int i) { switch(LootableBodies.dimensionSpecificPlatform ? i : 0) { case -1: - return Blocks.NETHERRACK.getDefaultState(); + return Blocks.NETHERRACK.defaultBlockState(); case 1: - return Blocks.END_STONE.getDefaultState(); + return Blocks.END_STONE.defaultBlockState(); default: - return Blocks.COBBLESTONE.getDefaultState(); + return Blocks.COBBLESTONE.defaultBlockState(); } } private void decayItems(int amount) { - for(int slot = 0; slot < this.getSizeInventory(); slot++){ - this.setInventorySlotContents(slot,this.decayItem(this.getStackInSlot(slot), amount)); + for(int slot = 0; slot < this.getContainerSize(); slot++){ + this.setItem(slot,this.decayItem(this.getItem(slot), amount)); } for(ItemStack item : auxInventory){ decayItem(item, amount); @@ -307,23 +297,23 @@ private void decayItems(int amount) { private ItemStack decayItem(ItemStack item, int amount) { if(!emptyStack(item) && item.getCount() == 1 - && item.isDamageable() - && item.getItem().isDamageable()){ - int durabilityRemaining = item.getMaxDamage() - item.getDamage() - 1; + && item.isDamageableItem() + && item.getItem().isDamageable(item)){ + int durabilityRemaining = item.getMaxDamage() - item.getDamageValue() - 1; if(durabilityRemaining > 0){ - item.damageItem(Math.min(durabilityRemaining,amount),this,broken->{}); + item.hurtAndBreak(Math.min(durabilityRemaining,amount),this,broken->{}); } } return item; } public void addItem(ItemStack stack) { - for(int slot = EQUIPMENT_SLOTS.length; slot < this.getSizeInventory(); slot++){ + for(int slot = EQUIPMENT_SLOTS.length; slot < this.getContainerSize(); slot++){ stack = mergeItem(slot,stack); if(emptyStack(stack)) return; } auxInventory.addLast(stack); - markDirty(); + setChanged(); } /** Retain an item whose container release was rejected. Uses the existing Aux format. */ @@ -337,10 +327,10 @@ private void cancelRemoval() { terminate = -1; deathTime = 0; dead = false; - removed = false; + if(isRemoved())revive(); setHealth(getMaxHealth()); vacuumTime = VACUUM_TIMELIMIT; - markDirty(); + setChanged(); } @@ -360,24 +350,24 @@ public void initializeItems(List inv) { if(i 0) this.setInventorySlotContents(slot,stack.split(amount)); + if(amount > 0) this.setItem(slot,stack.split(amount)); return stack.getCount() > 0 ? stack : ItemStack.EMPTY; } else if(current.getCount() < inventorySizeLimit - && ItemStack.areItemsEqual(stack,current) - && ItemStack.areItemStackTagsEqual(stack,current)){ + && ItemStack.isSame(stack,current) + && ItemStack.tagMatches(stack,current)){ int delta = Math.min(stack.getCount(),inventorySizeLimit - current.getCount()); current.grow(delta); stack.shrink(delta); - this.setInventorySlotContents(slot,current); + this.setItem(slot,current); if(stack.getCount() <= 0) return ItemStack.EMPTY; return stack; } @@ -386,23 +376,23 @@ public ItemStack mergeItem(int slot, ItemStack stack) { @Override - protected void damageEntity(DamageSource src, float amount){ - String type = String.valueOf(src == null ? null : src.getDamageType()); // null protection - if(src instanceof EntityDamageSource && src.getTrueSource() instanceof PlayerEntity){ - PlayerEntity player = (PlayerEntity)src.getTrueSource(); - ItemStack item = player.getHeldItem(Hand.MAIN_HAND); + 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.getItem().getToolTypes(item).contains(net.minecraftforge.common.ToolType.SHOVEL)) { - this.onKillCommand(); + if (item.getItem() instanceof ShovelItem || item.canPerformAction(net.minecraftforge.common.ToolActions.SHOVEL_DIG)) { + this.kill(); } } } - if(type.equals(DamageSource.OUT_OF_WORLD.getDamageType())) { - super.damageEntity(src,amount); // kill command and falling out of the world + 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.getDamageType())){ + if(type.equals(DamageSource.IN_WALL.getMsgId())){ this.jumpOutOfWall(); } @@ -410,17 +400,17 @@ protected void damageEntity(DamageSource src, float amount){ if(LootableBodies.completelyInvulnerable) return; if(src instanceof EntityDamageSource ){ - if(LootableBodies.hurtByAttacks) super.damageEntity(src,amount); + 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.damageEntity(src,amount); + if(LootableBodies.hurtByEnvironment)super.actuallyHurt(src,amount); return; } if(LootableBodies.hurtByMisc){ - super.damageEntity(src,amount); + super.actuallyHurt(src,amount); return; } } @@ -436,23 +426,23 @@ 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.util.text.StringTextComponent(name));} + public void setCustomNameTag(String name){setCustomName(name==null || name.isEmpty()?null:new net.minecraft.network.chat.TextComponent(name));} public void refreshClientSkin() { GameProfile profile = gpSwap.get(); - if (getEntityWorld().isRemote && !LootableBodies.useLocalSkin && profile != null && profile.getId() == null) + if (getCommandSenderWorld().isClientSide && !LootableBodies.useLocalSkin && profile != null && profile.getId() == null) resolveProfile(profile); } private void resolveProfile(GameProfile initial) { + final net.minecraft.server.players.GameProfileCache cache=SkullBlockEntity.profileCache; + final com.mojang.authlib.minecraft.MinecraftSessionService service=SkullBlockEntity.sessionService; try { PROFILE_LOOKUPS.execute(() -> { - try { - GameProfile resolved = SkullTileEntity.updateGameProfile(initial); - // A rename, reload or explicit profile update wins over a late lookup. - if(resolved != null) gpSwap.compareAndSet(initial,resolved); - } catch(RuntimeException unavailable) { - ModLog.fine("Corpse profile lookup unavailable; using default skin"); - } + // Native37's skull helper delegates to another asynchronous worker, + // outside this exception boundary. Keep both native service calls + // on our bounded worker, and let a newer name/profile win the CAS. + GameProfile resolved=cyano.lootable.network.CorpseProfiles.resolve(initial,cache,service); + if(resolved != null)gpSwap.compareAndSet(initial,resolved); }); } catch(RejectedExecutionException busy) { // Keep the already installed default profile when the bounded queue is full. @@ -471,16 +461,16 @@ private void setDeathTimestamp(long timestamp) { } - public static net.minecraft.entity.ai.attributes.AttributeModifierMap.MutableAttribute corpseAttributeDefaults() { - return net.minecraft.entity.MobEntity.func_233666_p_().createMutableAttribute(Attributes.MAX_HEALTH,LootableBodies.corpseHP); + public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder corpseAttributeDefaults() { + return net.minecraft.world.entity.Mob.createMobAttributes().add(Attributes.MAX_HEALTH,LootableBodies.corpseHP); } - private java.util.List corpseAttributes() { - java.util.List result=new java.util.ArrayList<>(); - for(net.minecraft.entity.ai.attributes.Attribute key:net.minecraft.util.registry.Registry.ATTRIBUTE) - if(getAttributeManager().hasAttributeInstance(key))result.add(getAttribute(key)); + 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) + if(getAttributes().hasAttribute(key))result.add(getAttribute(key)); return result; } - private ModifiableAttributeInstance attributeNamed(String name) { + private AttributeInstance attributeNamed(String name) { switch(name) { case "generic.maxHealth":name="minecraft:generic.max_health";break; case "generic.followRange":name="minecraft:generic.follow_range";break; @@ -498,73 +488,73 @@ private ModifiableAttributeInstance attributeNamed(String name) { case "forge.entity_gravity":name="forge:entity_gravity";break; default:break; } - net.minecraft.util.ResourceLocation id=net.minecraft.util.ResourceLocation.tryCreate(name); - net.minecraft.entity.ai.attributes.Attribute key=id==null?null:net.minecraft.util.registry.Registry.ATTRIBUTE.getOptional(id).orElse(null); - return key!=null && getAttributeManager().hasAttributeInstance(key)?getAttribute(key):null; + 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); + return key!=null && getAttributes().hasAttribute(key)?getAttribute(key):null; } public float getRotation(){ - return this.rotationYaw; + return getYRot(); } public void setRotation(float newRot){ - this.rotationYaw = newRot; + setYRot(newRot); } @Override - public boolean canDespawn(double distanceToClosestPlayer) { + public boolean removeWhenFarAway(double distanceToClosestPlayer) { return false; } public void jumpOutOfWall(){ - if(world.isRemote) return; - AxisAlignedBB bounds = getBoundingBox(); + if(level.isClientSide) return; + AABB bounds = getBoundingBox(); // Collision queries omit unloaded chunks. They cannot establish an exit // until the complete local terrain (including tall neighbour shapes) exists. - if(!escapeTerrainLoaded(bounds) || world.hasNoCollisions(this,bounds)) return; - BlockPos currentCoord = getPosition(); - double x = currentCoord.getX()+0.5 - getPosX(); - double z = currentCoord.getZ()+0.5 - getPosZ(); + if(!escapeTerrainLoaded(bounds) || level.noCollision(this,bounds)) return; + BlockPos currentCoord = blockPosition(); + double x = currentCoord.getX()+0.5 - getX(); + double z = currentCoord.getZ()+0.5 - getZ(); double length = Math.hypot(x,z); // At a block centre there is no horizontal direction to normalize. - if(length > 0 && tryWallExit(getPosX()+x/length,getPosY(),getPosZ()+z/length)) return; + if(length > 0 && tryWallExit(getX()+x/length,getY(),getZ()+z/length)) return; // Retain the original adjacent-exit priority, but check the whole body. for(Direction direction : new Direction[]{Direction.UP,Direction.NORTH, Direction.EAST,Direction.SOUTH,Direction.WEST,Direction.DOWN}) { - BlockPos exit = currentCoord.offset(direction); + BlockPos exit = currentCoord.relative(direction); if(tryWallExit(exit.getX()+0.5,exit.getY()+0.015625,exit.getZ()+0.5)) return; } // A sealed corpse must remain recoverable here, rather than wander through // solid terrain or load chunks looking for an unchecked random destination. } - private boolean escapeTerrainLoaded(AxisAlignedBB bounds) { - return world.isAreaLoaded(new BlockPos(bounds.minX-1,0,bounds.minZ-1), + 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)); } private boolean tryWallExit(double x,double y,double z) { - AxisAlignedBB bounds = getBoundingBox().offset(x-getPosX(),y-getPosY(),z-getPosZ()); - if(bounds.minY < 0 || bounds.maxY > world.getHeight() || - !world.getWorldBorder().contains(bounds) || !escapeTerrainLoaded(bounds) || - !world.hasNoCollisions(this,bounds)) return false; - setPosition(x,y,z); + AABB bounds = getBoundingBox().move(x-getX(),y-getY(),z-getZ()); + if(bounds.minY < 0 || bounds.maxY > level.getHeight() || + !level.getWorldBorder().isWithinBounds(bounds) || !escapeTerrainLoaded(bounds) || + !level.noCollision(this,bounds)) return false; + setPos(x,y,z); return true; } @Override - public void onKillCommand() { + public void kill() { if(terminate < 0) terminate = DEATH_COUNTDOWN; - this.attackEntityFrom(DamageSource.OUT_OF_WORLD, this.getMaxHealth()); - this.markDirty(); + this.hurt(DamageSource.OUT_OF_WORLD, this.getMaxHealth()); + this.setChanged(); } @Override - public int getTalkInterval() { + public int getAmbientSoundInterval() { return 1200; } @Override @@ -575,7 +565,7 @@ public void playAmbientSound() { @Override - protected int getExperiencePoints(final PlayerEntity p_getExperiencePoints_1_) { + protected int getExperienceReward(final Player p_getExperiencePoints_1_) { return 0; } @@ -586,23 +576,29 @@ protected SoundEvent getAmbientSound() { } @Override - protected ResourceLocation getLootTable() { - return net.minecraft.loot.LootTables.EMPTY; + protected ResourceLocation getDefaultLootTable() { + return net.minecraft.world.level.storage.loot.BuiltInLootTables.EMPTY; } + @Override + protected void dropFromLootTable(DamageSource source,boolean recentlyHit) { + // Native Mob.getLootTable is final and can select a saved LootTable. + // Corpse contents are released only by the inventory recovery path. + } + @Override - protected void dropSpecialItems(DamageSource source, int lootingModifier, boolean wasRecentlyHit) { + protected void dropCustomDeathLoot(DamageSource source, int lootingModifier, boolean wasRecentlyHit) { // Corpse equipment belongs to the inventory release path, not mob loot. } @Override - public boolean canBeSteered() { + public boolean canBeControlledByRider() { return false; } @Override - public boolean canBeLeashedTo(PlayerEntity player) { + public boolean canBeLeashed(Player player) { return false; // leashing not allowed } @@ -627,7 +623,7 @@ public void setCanPickUpLoot(final boolean p_setCanPickUpLoot_1_) { * Returns the number of slots in the inventory. */ @Override - public int getSizeInventory() { + public int getContainerSize() { return EQUIPMENT_SLOTS.length + mainInventory.size(); } @@ -637,9 +633,9 @@ public int getSizeInventory() { * @param index */ @Override - public ItemStack getStackInSlot(int index) { + public ItemStack getItem(int index) { if(index < EQUIPMENT_SLOTS.length){ - return super.getItemStackFromSlot(EQUIPMENT_SLOTS[index]); + return super.getItemBySlot(EQUIPMENT_SLOTS[index]); } else { return mainInventory.get(index - EQUIPMENT_SLOTS.length); } @@ -652,14 +648,14 @@ public ItemStack getStackInSlot(int index) { * @param stack */ @Override - public void setInventorySlotContents(int index, ItemStack stack) { + public void setItem(int index, ItemStack stack) { if(stack == null) stack = ItemStack.EMPTY; if(index < EQUIPMENT_SLOTS.length){ - super.setItemStackToSlot(EQUIPMENT_SLOTS[index], stack); + super.setItemSlot(EQUIPMENT_SLOTS[index], stack); } else { mainInventory.set(index - EQUIPMENT_SLOTS.length, stack); } - markDirty(); + setChanged(); } /** @@ -668,9 +664,9 @@ public void setInventorySlotContents(int index, ItemStack stack) { * @param index */ @Override - public ItemStack removeStackFromSlot(int index) { - ItemStack i = this.getStackInSlot(index); - this.setInventorySlotContents(index,null); + public ItemStack removeItemNoUpdate(int index) { + ItemStack i = this.getItem(index); + this.setItem(index,null); return i; } @@ -681,14 +677,14 @@ public ItemStack removeStackFromSlot(int index) { * @param count */ @Override - public ItemStack decrStackSize(int index, int count) { - ItemStack i = this.getStackInSlot(index); + public ItemStack removeItem(int index, int count) { + ItemStack i = this.getItem(index); if(emptyStack(i) || count <= 0) return ItemStack.EMPTY; ItemStack result = i.split(count); if (i.getCount() <= 0) { i = ItemStack.EMPTY; } - this.setInventorySlotContents(index, i); + this.setItem(index, i); return result; } @@ -696,7 +692,7 @@ public ItemStack decrStackSize(int index, int count) { * Returns the maximum stack size for a inventory slot. Seems to always be 64, possibly will be extended. */ @Override - public int getInventoryStackLimit() { + public int getMaxStackSize() { return 64; } @@ -705,9 +701,9 @@ public int getInventoryStackLimit() { * hasn't changed and skip it. */ @Override - public void markDirty() { - if(world != null && !world.isRemote && addedToChunk) { - world.markChunkDirty(getPosition(),null); + public void setChanged() { + if(level != null && !level.isClientSide && isAddedToWorld() && level.hasChunkAt(blockPosition())) { + level.getChunkAt(blockPosition()).setUnsaved(true); } } @@ -717,30 +713,30 @@ public void markDirty() { * @param player */ @Override - public boolean isUsableByPlayer(PlayerEntity player) { + public boolean stillValid(Player player) { return player != null && player.isAlive() && this.isAlive() - && terminate < 0 && player.getEntityWorld() == getEntityWorld() - && player.getPosition().distanceSq(this.getPosition()) < 25; + && terminate < 0 && player.getCommandSenderWorld() == getCommandSenderWorld() + && player.blockPosition().distSqr(this.blockPosition()) < 25; } @Override - public net.minecraft.util.ActionResultType getEntityInteractionResult(PlayerEntity player, Hand hand){ - if(isUsableByPlayer(player)) { + public net.minecraft.world.InteractionResult mobInteract(Player player, InteractionHand hand){ + if(stillValid(player)) { GUIHandler.open(player,this); - return net.minecraft.util.ActionResultType.SUCCESS; + return net.minecraft.world.InteractionResult.SUCCESS; } - return net.minecraft.util.ActionResultType.PASS; + return net.minecraft.world.InteractionResult.PASS; } - @Override public net.minecraft.inventory.container.Container createMenu(int windowId, net.minecraft.entity.player.PlayerInventory inventory, PlayerEntity player){return new cyano.lootable.graphics.CorpseContainer(windowId,inventory,this);} + @Override public net.minecraft.world.inventory.AbstractContainerMenu createMenu(int windowId, net.minecraft.world.entity.player.Inventory inventory, Player player){return new cyano.lootable.graphics.CorpseContainer(windowId,inventory,this);} public String getGuiID(){return "lootablebodies:corpse";} @Override - public void openInventory(PlayerEntity player) { + public void startOpen(Player player) { } @Override - public void closeInventory(PlayerEntity player) { + public void stopOpen(Player player) { } /** @@ -750,7 +746,7 @@ public void closeInventory(PlayerEntity player) { * @param stack */ @Override - public boolean isItemValidForSlot(int index, ItemStack stack) { + public boolean canPlaceItem(int index, ItemStack stack) { if(emptyStack(stack) || stack.getCount() <= 0) return false; if(index < 4){ return stack.canEquip(EQUIPMENT_SLOTS[index], this); @@ -772,9 +768,9 @@ public int getFieldCount() { } @Override - public void clear() { + public void clearContent() { for(int i = 0; i < EQUIPMENT_SLOTS.length; i++){ - this.setInventorySlotContents(i,null); + this.setItem(i,null); } mainInventory.clear(); auxInventory.clear(); @@ -785,59 +781,59 @@ public void clear() { public boolean isEmpty(){ if(!auxInventory.isEmpty() || !unresolvedInventory.isEmpty()) return false; - for(int slot = 0; slot < this.getSizeInventory(); slot++){ - if(!emptyStack(this.getStackInSlot(slot))) return false; + for(int slot = 0; slot < this.getContainerSize(); slot++){ + if(!emptyStack(this.getItem(slot))) return false; } return true; } private boolean dropAllItems(){ - for(int slot = 0; slot < getSizeInventory(); slot++){ - ItemStack stack = getStackInSlot(slot); - if(!emptyStack(stack) && getEntityWorld().addEntity( - new ItemEntity(getEntityWorld(),getPosX(),getPosY(),getPosZ(),stack.copy()))) { - setInventorySlotContents(slot,null); + for(int slot = 0; slot < getContainerSize(); slot++){ + ItemStack stack = getItem(slot); + if(!emptyStack(stack) && getCommandSenderWorld().addFreshEntity( + new ItemEntity(getCommandSenderWorld(),getX(),getY(),getZ(),stack.copy()))) { + setItem(slot,null); } } Iterator iterator = auxInventory.iterator(); while(iterator.hasNext()) { ItemStack stack = iterator.next(); - if(emptyStack(stack) || getEntityWorld().addEntity( - new ItemEntity(getEntityWorld(),getPosX(),getPosY(),getPosZ(),stack.copy()))) iterator.remove(); + if(emptyStack(stack) || getCommandSenderWorld().addFreshEntity( + new ItemEntity(getCommandSenderWorld(),getX(),getY(),getZ(),stack.copy()))) iterator.remove(); } - markDirty(); + setChanged(); return isEmpty(); } private static boolean matchesAny(String damageType, DamageSource... list){ for(int i = 0; i < list.length; i++){ - if(damageType.equalsIgnoreCase(list[i].damageType)) return true; + if(damageType.equalsIgnoreCase(list[i].getMsgId())) return true; } return false; } @Override - public CreatureAttribute getCreatureAttribute() + public MobType getMobType() { - return CreatureAttribute.UNDEAD; + return MobType.UNDEAD; } @Override - public void writeAdditional(final CompoundNBT root) { + public void addAdditionalSaveData(final CompoundTag root) { UpgradePreservation.clearTransient(root); root.putInt(UpgradePreservation.VERSION,UpgradePreservation.DATA_VERSION); if(sourceVersions.isEmpty()) root.remove(UpgradePreservation.SOURCES); else root.put(UpgradePreservation.SOURCES,sourceVersions.copy()); root.remove("ActiveEffects"); - super.writeAdditional(root); - final ListNBT equipmentListTag = new ListNBT(); - for (int i = 0; i < this.getSizeInventory(); i++) { - ItemStack item = this.getStackInSlot(i); + super.addAdditionalSaveData(root); + final ListTag equipmentListTag = new ListTag(); + for (int i = 0; i < this.getContainerSize(); i++) { + ItemStack item = this.getItem(i); if (!emptyStack(item)) { - CompoundNBT slotTag = new CompoundNBT(); + CompoundTag slotTag = new CompoundTag(); slotTag.putByte("Slot", (byte)i); - item.write(slotTag); + item.save(slotTag); equipmentListTag.add(slotTag); } } @@ -845,27 +841,27 @@ public void writeAdditional(final CompoundNBT root) { root.remove("Aux"); if(!this.auxInventory.isEmpty()){ - final ListNBT nbtauxtaglist = new ListNBT(); + final ListTag nbtauxtaglist = new ListTag(); Iterator iter = this.auxInventory.iterator(); while (iter.hasNext()) { ItemStack i = iter.next(); if(emptyStack(i)) continue; - final CompoundNBT nbttagcompound1 = new CompoundNBT(); - i.write(nbttagcompound1); + final CompoundTag nbttagcompound1 = new CompoundTag(); + i.save(nbttagcompound1); nbtauxtaglist.add(nbttagcompound1); } root.put("Aux", nbtauxtaglist); } root.remove(UNRESOLVED_ITEMS_TAG); if(!unresolvedInventory.isEmpty()) { - ListNBT retained = new ListNBT(); - for(CompoundNBT record : unresolvedInventory) retained.add(record.copy()); + ListTag retained = new ListTag(); + for(CompoundTag record : unresolvedInventory) retained.add(record.copy()); root.put(UNRESOLVED_ITEMS_TAG,retained); } root.remove(INVALID_FIELDS_TAG); if(!invalidFields.isEmpty()) { - ListNBT retained = new ListNBT(); - for(CompoundNBT record : invalidFields) retained.add(record.copy()); + ListTag retained = new ListTag(); + for(CompoundTag record : invalidFields) retained.add(record.copy()); root.put(INVALID_FIELDS_TAG,retained); } root.remove("Vac"); @@ -880,11 +876,11 @@ public void writeAdditional(final CompoundNBT root) { @Override - public void read(final CompoundNBT supplied) { - final CompoundNBT root=UpgradePreservation.restore(supplied); + public void load(final CompoundTag supplied) { + final CompoundTag root=UpgradePreservation.restore(supplied); // Entity reads geometry before our inventory helper. Sanitize a copy // before it builds bounding boxes or derives previous-tick positions. - CompoundNBT clean = root.copy(); + CompoundTag clean = root.copy(); boolean[] badPosition = new boolean[4], badMotion = new boolean[4]; boolean[] badRotation = new boolean[3]; clean.put("Pos",readVector(root.get("Pos"),false,badPosition)); @@ -896,40 +892,36 @@ public void read(final CompoundNBT supplied) { // Vanilla expects JSON here. Our attribution reader owns compatibility // and retains malformed input without sending it to vanilla's parser. clean.remove("CustomName"); - super.read(clean); + super.load(clean); String fallback=readName(root,"CustomName"),owner=readName(root,"Name"); setUserName(owner!=null?owner:fallback!=null?fallback:""); // A repeated load must not interpolate from obsolete head/body angles. - prevRotationYaw = rotationYaw; prevRotationPitch = rotationPitch; - setRotationYawHead(rotationYaw); prevRotationYawHead = rotationYaw; - setRenderYawOffset(rotationYaw); prevRenderYawOffset = rotationYaw; - positionNeedsChunk = true; - malformedPosition = hasInvalidComponent(badPosition); - loadedPosition = root.contains("Pos") ? root.get("Pos").copy() : null; - missingPositionX = badPosition[0]; missingPositionZ = badPosition[2]; - if(malformedPosition && root.contains("Pos")) retainInvalid("Pos",root.get("Pos")); + yRotO = getYRot(); xRotO = getXRot(); + setYHeadRot(getYRot()); yHeadRotO = getYRot(); + setYBodyRot(getYRot()); yBodyRotO = getYRot(); + if(hasInvalidComponent(badPosition) && root.contains("Pos")) retainInvalid("Pos",root.get("Pos")); if(hasInvalidComponent(badMotion) && root.contains("Motion")) retainInvalid("Motion",root.get("Motion")); if(hasInvalidComponent(badRotation) && root.contains("Rotation")) retainInvalid("Rotation",root.get("Rotation")); if(badFall) retainInvalid("FallDistance",root.get("FallDistance")); } - private static Float finiteFloat(INBT raw) { + private static Float finiteFloat(Tag raw) { Double number = finiteNumber(raw); if(number == null) return null; float value = number.floatValue(); return Float.isFinite(value) && (number == 0D || value != 0F) ? value : null; } - private static ListNBT readRotation(INBT raw, boolean[] invalid) { - ListNBT list = raw instanceof ListNBT ? (ListNBT)raw : null; - invalid[2] = list == null || list.size() != 2 || list.getTagType() < 1 || list.getTagType() > 6; - ListNBT clean = new ListNBT(); + private static ListTag readRotation(Tag raw, boolean[] invalid) { + ListTag list = raw instanceof ListTag ? (ListTag)raw : null; + invalid[2] = list == null || list.size() != 2 || list.getElementType() < 1 || list.getElementType() > 6; + ListTag clean = new ListTag(); for(int axis = 0; axis < 2; axis++) { Float angle = list != null && axis < list.size() ? finiteFloat(list.get(axis)) : null; if(angle == null) { invalid[axis] = true; angle = 0F; } // Preserve the legacy float remainder and the corpse's existing // flat pitch before vanilla copies angles into head/render state. - clean.add(net.minecraft.nbt.FloatNBT.valueOf(axis == 0 ? angle % 360F : 0F)); + clean.add(net.minecraft.nbt.FloatTag.valueOf(axis == 0 ? angle % 360F : 0F)); } return clean; } @@ -939,78 +931,59 @@ private static boolean hasInvalidComponent(boolean[] invalid) { return false; } - private static ListNBT readVector(INBT raw, boolean motion, boolean[] invalid) { - ListNBT list = raw instanceof ListNBT ? (ListNBT)raw : null; - invalid[3] = list == null || list.size() != 3 || list.getTagType() < 1 || list.getTagType() > 6; - ListNBT clean = new ListNBT(); + private static ListTag readVector(Tag raw, boolean motion, boolean[] invalid) { + ListTag list = raw instanceof ListTag ? (ListTag)raw : null; + invalid[3] = list == null || list.size() != 3 || list.getElementType() < 1 || list.getElementType() > 6; + ListTag clean = new ListTag(); for(int axis = 0; axis < 3; axis++) { Double value = list != null && axis < list.size() ? finiteNumber(list.get(axis)) : null; - if(value == null || Math.abs(value) > (motion ? 10D : 3.0E7D)) { + if(value == null || (motion ? Math.abs(value)>10D : !cyano.lootable.upgrade.SavedCorpsePosition.isSafeCoordinate(axis,value))) { invalid[axis] = true; // A lost height must not put recoverable inventory in the void. value = !motion && axis == 1 ? 64D : 0D; } - clean.add(net.minecraft.nbt.DoubleNBT.valueOf(value)); + clean.add(net.minecraft.nbt.DoubleTag.valueOf(value)); } return clean; } - /** Complete a malformed NBT position using the authoritative containing chunk. */ - public void recoverPositionInChunk(int chunkX, int chunkZ) { - if(!positionNeedsChunk) return; - positionNeedsChunk = false; - boolean wrongX = net.minecraft.util.math.MathHelper.floor(getPosX() / 16D) != chunkX; - boolean wrongZ = net.minecraft.util.math.MathHelper.floor(getPosZ() / 16D) != chunkZ; - if(!malformedPosition && !wrongX && !wrongZ) { loadedPosition = null; return; } - if(!malformedPosition && loadedPosition != null) retainInvalid("Pos",loadedPosition); - loadedPosition = null; - double x = missingPositionX || wrongX ? chunkX * 16D + 8.5D : getPosX(); - double z = missingPositionZ || wrongZ ? chunkZ * 16D + 8.5D : getPosZ(); - // Chunk.addEntity flags a location mismatch dead immediately before - // EnteringChunk. Only the first pending NBT admission may undo that - // flag; saved zero health and normal later removals remain effective. - if(wrongX || wrongZ) removed = false; - setLocationAndAngles(x,getPosY(),z,rotationYaw,rotationPitch); - ModLog.warning("Lootable Bodies recovered malformed corpse position in chunk %s,%s",chunkX,chunkZ); - } - @Override - public void readAdditional(final CompoundNBT supplied) { - final CompoundNBT root=UpgradePreservation.restore(supplied); - sourceVersions=root.contains(UpgradePreservation.SOURCES,10) ? root.getCompound(UpgradePreservation.SOURCES).copy() : new CompoundNBT(); + public void readAdditionalSaveData(final CompoundTag supplied) { + final CompoundTag root=UpgradePreservation.restore(supplied); + sourceVersions=root.contains(UpgradePreservation.SOURCES,10) ? root.getCompound(UpgradePreservation.SOURCES).copy() : new CompoundTag(); // Persisted Equipment/Aux are already current after the first save. // Only the transient protected original and retained raw records use old versions. - inventoryVersions=root.getBoolean(UpgradePreservation.ITEMS_PENDING) ? sourceVersions : new CompoundNBT(); + inventoryVersions=root.getBoolean(UpgradePreservation.ITEMS_PENDING) ? sourceVersions : new CompoundTag(); // Replace, rather than append to, an existing inventory on a repeated load. - this.clear(); + this.clearContent(); // These flags are not saved. A reused entity must behave like a fresh // load; its new Health still decides whether normal death handling starts. terminate = -1; dead = false; - removed = false; + if(isRemoved())revive(); // Item readers may update their input tags; the caller's saved data is immutable. - CompoundNBT vanilla = root.copy(); + CompoundTag vanilla = root.copy(); // Our historical DeathTime is a world timestamp, not vanilla's animation. vanilla.putShort("DeathTime",(short)0); invalidFields.clear(); - INBT metadata = root.get(INVALID_FIELDS_TAG); + Tag metadata = root.get(INVALID_FIELDS_TAG); if(isInventoryList(metadata)) { - ListNBT records = (ListNBT)metadata; + ListTag records = (ListTag)metadata; for(int i = 0; i < records.size(); i++) invalidFields.add(records.getCompound(i).copy()); } else if(metadata != null) retainInvalid("Recovery",metadata); // NBT replaces state. Clear the old map without effect callbacks: saved // attributes and absorption are restored below, just as on a fresh entity. - getActivePotionEffects().clear(); - for(ModifiableAttributeInstance attribute : corpseAttributes()) restoreAttribute(attribute); + getActiveEffects().clear(); + for(AttributeInstance attribute : corpseAttributes()) restoreAttribute(attribute); vanilla.put("Attributes",readAttributes(root.get("Attributes"))); vanilla.put("ActiveEffects",readEffects(root.get("ActiveEffects"))); vanilla.putShort("HurtTime",(short)readTimer(root,"HurtTime",Short.MAX_VALUE,0)); // Health must be checked against the loaded, validated effective maximum. vanilla.remove("Health"); vanilla.putFloat("AbsorptionAmount",readLivingFloat(root,"AbsorptionAmount",0F,Float.MAX_VALUE)); - super.readAdditional(vanilla); - updatePotionMetadata(); - for(ModifiableAttributeInstance attribute : corpseAttributes()) { + super.readAdditionalSaveData(vanilla); + updateInvisibilityStatus(); + for(AttributeInstance attribute : corpseAttributes()) { if(!Double.isFinite(attribute.getValue())) { // Also protect against engine ordering/parent-attribute arithmetic. if(root.contains("Attributes")) retainInvalid("Attributes",root.get("Attributes")); @@ -1018,21 +991,21 @@ public void readAdditional(final CompoundNBT supplied) { } } setHealth(readLivingFloat(root,"Health",getMaxHealth(),getMaxHealth())); - INBT equipment = root.get("Equipment"); + Tag equipment = root.get("Equipment"); if(isInventoryList(equipment)) { // Vanilla ArmorItems/HandItems mirror these six slots. The custom // list is authoritative when present, so do not load both copies. - for(int i = 0; i < EQUIPMENT_SLOTS.length; i++) setInventorySlotContents(i,null); + for(int i = 0; i < EQUIPMENT_SLOTS.length; i++) setItem(i,null); } readInventoryTag("Equipment",equipment); readInventoryTag("Aux",root.get("Aux")); - INBT recovery = root.get(UNRESOLVED_ITEMS_TAG); + Tag recovery = root.get(UNRESOLVED_ITEMS_TAG); if(isInventoryList(recovery)) { - ListNBT records = (ListNBT)recovery; + ListTag records = (ListTag)recovery; for(int i = 0; i < records.size(); i++) { - CompoundNBT record = records.getCompound(i); + CompoundTag record = records.getCompound(i); String source = record.getString("Source"); - if((record.keySet().size() == 2 || (record.keySet().size() == 3 && record.contains("SourceVersions",10))) + if((record.getAllKeys().size() == 2 || (record.getAllKeys().size() == 3 && record.contains("SourceVersions",10))) && record.contains("Source",8) && record.contains("Data",10) && (source.equals("Equipment") || source.equals("Aux"))) { readInventoryEntry(source,record.getCompound("Data"),record); @@ -1042,7 +1015,7 @@ public void readAdditional(final CompoundNBT supplied) { if(!unresolvedInventory.isEmpty()) ModLog.warning( "Lootable Bodies retained %s unreadable inventory records for later recovery",unresolvedInventory.size()); - long now = Math.max(0L,getEntityWorld().getGameTime()); + long now = Math.max(0L,getCommandSenderWorld().getGameTime()); this.vacuumTime = (byte)readTimer(root,"Vac",VACUUM_TIMELIMIT,VACUUM_TIMELIMIT); this.setDeathTimestamp(readTimer(root,"DeathTime",now,now)); this.decayTimestamp = readTimer(root,"DecayTime",now,now); @@ -1051,56 +1024,56 @@ public void readAdditional(final CompoundNBT supplied) { this.setUserName(name != null ? name : fallback != null ? fallback : ""); this.setGameProfile(null); this.oldName = null; - this.setRotation(this.rotationYaw, 0); + this.setRot(getYRot(), 0); } - private void retainInvalid(String source, INBT data) { - CompoundNBT record = new CompoundNBT(); + private void retainInvalid(String source, Tag data) { + CompoundTag record = new CompoundTag(); record.putString("Source",source); record.put("Data",data.copy()); if(!invalidFields.contains(record)) invalidFields.add(record); } - private void restoreAttribute(ModifiableAttributeInstance attribute) { + private void restoreAttribute(AttributeInstance attribute) { String name = attribute.getAttribute().getRegistryName().toString(); - for(AttributeModifier modifier : new ArrayList<>(attribute.getModifierListCopy())) attribute.removeModifier(modifier); + for(AttributeModifier modifier : new ArrayList<>(attribute.getModifiers())) attribute.removeModifier(modifier); attribute.setBaseValue(defaultAttributeBases.getOrDefault(name,attribute.getAttribute().getDefaultValue())); List defaults = defaultAttributeModifiers.get(name); - if(defaults != null) for(AttributeModifier modifier : defaults) attribute.applyPersistentModifier(modifier); + if(defaults != null) for(AttributeModifier modifier : defaults) attribute.addPermanentModifier(modifier); } - private ListNBT readEffects(INBT raw) { - ListNBT clean = new ListNBT(); + private ListTag readEffects(Tag raw) { + ListTag clean = new ListTag(); if(raw == null) return clean; if(!isInventoryList(raw)) { retainInvalid("ActiveEffects",raw); return clean; } - Set seen = new HashSet<>(); - ListNBT list = (ListNBT)raw; + Set seen = new HashSet<>(); + ListTag list = (ListTag)raw; for(int i = 0; i < list.size(); i++) { - CompoundNBT record = list.getCompound(i); - INBT rawId = record.get("Id"), rawAmplifier = record.get("Amplifier"); + CompoundTag record = list.getCompound(i); + Tag rawId = record.get("Id"), rawAmplifier = record.get("Amplifier"); Long id = integral(rawId); // IDs are unsigned bytes on disk. Amplifiers are read as signed // bytes by this engine; preserve all canonical byte encodings. if(rawId != null && rawId.getId() == 1) id = id & 255L; Long amplifier = rawAmplifier == null ? Long.valueOf(0) : integral(rawAmplifier); Long duration = record.contains("Duration") ? integral(record.get("Duration")) : Long.valueOf(0); - Effect potion = id != null && id >= 0 && id <= 255 ? Effect.get(id.intValue()) : null; + MobEffect potion = id != null && id >= 0 && id <= 255 ? MobEffect.byId(id.intValue()) : null; if(potion == null || duration == null || duration < 0 || duration > Integer.MAX_VALUE || amplifier == null || amplifier > 127 || (amplifier < 0 && rawAmplifier.getId() != 1) || !seen.add(potion)) { retainInvalid("ActiveEffect",record); continue; } - CompoundNBT accepted = new CompoundNBT(); + CompoundTag accepted = new CompoundTag(); accepted.putByte("Id",id.byteValue()); accepted.putInt("Duration",duration.intValue()); accepted.putByte("Amplifier",amplifier.byteValue()); - boolean changed = !Arrays.asList("Id","Duration","Amplifier","Ambient","ShowParticles","ShowIcon","CurativeItems").containsAll(record.keySet()); + boolean changed = !Arrays.asList("Id","Duration","Amplifier","Ambient","ShowParticles","ShowIcon","CurativeItems").containsAll(record.getAllKeys()); // Forge 1.11 persists curative stacks. Preserve their complete custom // data; an unreadable list uses Forge's defaults and stays recoverable. if(record.contains("CurativeItems")) { - INBT rawCuratives = record.get("CurativeItems"); + Tag rawCuratives = record.get("CurativeItems"); boolean valid = isInventoryList(rawCuratives); if(valid) { - ListNBT items = (ListNBT)rawCuratives; + ListTag items = (ListTag)rawCuratives; for(int c=0; c 6) return null; - double value = ((NumberNBT)raw).getDouble(); + private static Double finiteNumber(Tag raw) { + if(!(raw instanceof NumericTag) || raw.getId() < 1 || raw.getId() > 6) return null; + double value = ((NumericTag)raw).getAsDouble(); return Double.isFinite(value) ? value : null; } - private float readLivingFloat(CompoundNBT root, String key, float fallback, float maximum) { - INBT raw = root.get(key); + private float readLivingFloat(CompoundTag root, String key, float fallback, float maximum) { + Tag raw = root.get(key); if(raw == null) return fallback; Double value = finiteNumber(raw); if(value != null && value >= 0 && value <= maximum) { @@ -1138,15 +1111,15 @@ private float readLivingFloat(CompoundNBT root, String key, float fallback, floa return fallback; } - private ListNBT readAttributes(INBT raw) { - ListNBT clean = new ListNBT(); + private ListTag readAttributes(Tag raw) { + ListTag clean = new ListTag(); if(raw == null) return clean; if(!isInventoryList(raw)) { retainInvalid("Attributes",raw); return clean; } Set seen = new HashSet<>(); - ListNBT list = (ListNBT)raw; + ListTag list = (ListTag)raw; for(int i = 0; i < list.size(); i++) { - CompoundNBT record = list.getCompound(i); - ModifiableAttributeInstance attribute = record.contains("Name",8) + CompoundTag record = list.getCompound(i); + AttributeInstance attribute = record.contains("Name",8) ? attributeNamed(record.getString("Name")) : null; Double base = finiteNumber(record.get("Base")); if(attribute == null || base == null) { retainInvalid("Attribute",record); continue; } @@ -1159,22 +1132,22 @@ private ListNBT readAttributes(INBT raw) { // the effective distance at64; keep the valid saved base unchanged. boolean legacyNameDistance=attribute.getAttribute()==net.minecraftforge.common.ForgeMod.NAMETAG_DISTANCE.get() && base>=0D && base<=Float.MAX_VALUE; - if((attribute.getAttribute().clampValue(base) != base + if((attribute.getAttribute().sanitizeValue(base) != base && !configuredHealth && !legacyNameDistance && !base.equals(defaultAttributeBases.get(name))) || !seen.add(name)) { retainInvalid("Attribute",record); continue; } - boolean changed = !Arrays.asList("Name","Base","Modifiers").containsAll(record.keySet()); - CompoundNBT accepted = new CompoundNBT(); + boolean changed = !Arrays.asList("Name","Base","Modifiers").containsAll(record.getAllKeys()); + CompoundTag accepted = new CompoundTag(); accepted.putString("Name",name); accepted.putDouble("Base",base); - INBT rawModifiers = record.get("Modifiers"); + Tag rawModifiers = record.get("Modifiers"); if(rawModifiers != null) { - ListNBT modifiers = new ListNBT(); + ListTag modifiers = new ListTag(); if(!isInventoryList(rawModifiers)) changed = true; else { Set ids = new HashSet<>(); - ListNBT entries = (ListNBT)rawModifiers; + ListTag entries = (ListTag)rawModifiers; for(int m = 0; m < entries.size(); m++) { - CompoundNBT entry = entries.getCompound(m); + CompoundTag entry = entries.getCompound(m); Long operation = integral(entry.get("Operation")); UUID id=attributeUuid(entry); Double amount = finiteNumber(entry.get("Amount")); @@ -1183,13 +1156,13 @@ private ListNBT readAttributes(INBT raw) { changed = true; continue; } if(ids.contains(id)) { changed = true; continue; } - CompoundNBT modifier = new CompoundNBT(); + CompoundTag modifier = new CompoundTag(); modifier.putString("Name",entry.getString("Name")); modifier.putDouble("Amount",amount); - modifier.putInt("Operation",operation.intValue()); modifier.putUniqueId("UUID",id); - ListNBT candidate = modifiers.copy(); candidate.add(modifier); + modifier.putInt("Operation",operation.intValue()); modifier.putUUID("UUID",id); + ListTag candidate = modifiers.copy(); candidate.add(modifier); if(!finiteAttributeValue(base,candidate)) { changed = true; continue; } ids.add(id); modifiers.add(modifier); - if(!Arrays.asList("Name","Amount","Operation","UUIDMost","UUIDLeast","UUID").containsAll(entry.keySet())) changed = true; + if(!Arrays.asList("Name","Amount","Operation","UUIDMost","UUIDLeast","UUID").containsAll(entry.getAllKeys())) changed = true; } } accepted.put("Modifiers",modifiers); @@ -1200,11 +1173,11 @@ private ListNBT readAttributes(INBT raw) { return clean; } - private static UUID attributeUuid(CompoundNBT entry) { + private static UUID attributeUuid(CompoundTag entry) { UUID current=null,legacy=null; if(entry.contains("UUID")) { - if(!entry.hasUniqueId("UUID"))return null; - current=entry.getUniqueId("UUID"); + if(!entry.hasUUID("UUID"))return null; + current=entry.getUUID("UUID"); } if(entry.contains("UUIDMost") || entry.contains("UUIDLeast")) { Long most=integral(entry.get("UUIDMost")),least=integral(entry.get("UUIDLeast")); @@ -1213,28 +1186,28 @@ private static UUID attributeUuid(CompoundNBT entry) { } return current!=null?(legacy!=null && !current.equals(legacy)?null:current):legacy; } - private static boolean finiteAttributeValue(double base, ListNBT modifiers) { + private static boolean finiteAttributeValue(double base, ListTag modifiers) { double value = base; for(int i = 0; i < modifiers.size(); i++) { - CompoundNBT m = modifiers.getCompound(i); + CompoundTag m = modifiers.getCompound(i); if(m.getInt("Operation") == 0) value += m.getDouble("Amount"); } if(!Double.isFinite(value)) return false; double additive = value; for(int i = 0; i < modifiers.size(); i++) { - CompoundNBT m = modifiers.getCompound(i); + CompoundTag m = modifiers.getCompound(i); if(m.getInt("Operation") == 1) value += additive * m.getDouble("Amount"); } if(!Double.isFinite(value)) return false; for(int i = 0; i < modifiers.size(); i++) { - CompoundNBT m = modifiers.getCompound(i); + CompoundTag m = modifiers.getCompound(i); if(m.getInt("Operation") == 2) value *= 1D + m.getDouble("Amount"); } return Double.isFinite(value); } - private long readTimer(CompoundNBT root, String key, long maximum, long fallback) { - INBT raw = root.get(key); + private long readTimer(CompoundTag root, String key, long maximum, long fallback) { + Tag raw = root.get(key); if(raw == null) return fallback; Long value = integral(raw); if(value != null && value >= 0 && value <= maximum) return value; @@ -1244,22 +1217,22 @@ private long readTimer(CompoundNBT root, String key, long maximum, long fallback private long checkedTimestamp(String key, long timestamp, long now) { if(timestamp >= 0 && timestamp <= now) return timestamp; - retainInvalid(key,net.minecraft.nbt.LongNBT.valueOf(timestamp)); - markDirty(); + retainInvalid(key,net.minecraft.nbt.LongTag.valueOf(timestamp)); + setChanged(); return now; } - private String readName(CompoundNBT root,String key) { - INBT raw=root.get(key);if(raw==null)return null; + private String readName(CompoundTag root,String key) { + Tag raw=root.get(key);if(raw==null)return null; if(raw.getId()==8) { String name=root.getString(key); if(key.equals("CustomName") && root.getInt(UpgradePreservation.VERSION)>=3) { try { - net.minecraft.util.text.ITextComponent text=net.minecraft.util.text.ITextComponent.Serializer.getComponentFromJson(name); + net.minecraft.network.chat.Component text=net.minecraft.network.chat.Component.Serializer.fromJson(name); if(text==null){retainInvalid(key,raw);return null;}name=text.getString(); }catch(RuntimeException malformed){retainInvalid(key,raw);return null;} } - String packet=net.minecraft.util.text.ITextComponent.Serializer.toJson(new net.minecraft.util.text.StringTextComponent(name)); + String packet=net.minecraft.network.chat.Component.Serializer.toJson(new net.minecraft.network.chat.TextComponent(name)); if(name.length()<=32767 && name.getBytes(StandardCharsets.UTF_8).length<=32767 && packet.getBytes(StandardCharsets.UTF_8).length<=32767)return name.trim().isEmpty()?"":name; } @@ -1268,33 +1241,33 @@ private String readName(CompoundNBT root,String key) { - private static boolean isInventoryList(INBT data) { - return data instanceof ListNBT && (((ListNBT)data).size() == 0 || ((ListNBT)data).getTagType() == 10); + private static boolean isInventoryList(Tag data) { + return data instanceof ListTag && (((ListTag)data).size() == 0 || ((ListTag)data).getElementType() == 10); } - private void retainRaw(String source, INBT data) { - CompoundNBT record = new CompoundNBT(); + private void retainRaw(String source, Tag data) { + CompoundTag record = new CompoundTag(); record.putString("Source",source); record.put("Data",data.copy()); if(!inventoryVersions.isEmpty()) record.put("SourceVersions",inventoryVersions.copy()); unresolvedInventory.add(record); } - private void readInventoryTag(String source, INBT data) { + private void readInventoryTag(String source, Tag data) { if(data == null) return; if(!isInventoryList(data)) { retainRaw(source + "List",data); return; } - ListNBT list = (ListNBT)data; + ListTag list = (ListTag)data; for(int i = 0; i < list.size(); i++) readInventoryEntry(source,list.getCompound(i),null); } - private static Long integral(INBT data) { - return data instanceof NumberNBT && data.getId() >= 1 && data.getId() <= 4 ? ((NumberNBT)data).getLong() : null; + private static Long integral(Tag data) { + return data instanceof NumericTag && data.getId() >= 1 && data.getId() <= 4 ? ((NumericTag)data).getAsLong() : null; } - private ItemStack decodeInventoryItem(CompoundNBT raw) { + private ItemStack decodeInventoryItem(CompoundTag raw) { // Ambiguous fields, unavailable IDs and unhandled capability data stay // verbatim. Never guess an item, truncate a count or erase custom data. - for(String key : raw.keySet()) if(!Arrays.asList("id","Count","Damage","tag","ForgeCaps","Slot").contains(key)) return null; + for(String key : raw.getAllKeys()) if(!Arrays.asList("id","Count","Damage","tag","ForgeCaps","Slot").contains(key)) return null; if(!raw.contains("id",8)) return null; Long count = integral(raw.get("Count")); Long legacyDamage = raw.contains("Damage") ? integral(raw.get("Damage")) : Long.valueOf(0L); @@ -1307,25 +1280,25 @@ private ItemStack decodeInventoryItem(CompoundNBT raw) { ResourceLocation id = new ResourceLocation(raw.getString("id")); if(!net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(id)) return null; Item registered = net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(id); - if(registered == null || Item.getIdFromItem(registered) == 0) return null; - CompoundNBT input = raw.copy(); + if(registered == null || Item.getId(registered) == 0) return null; + CompoundTag input = raw.copy(); input.putString("id",id.toString()); input.putByte("Count",count.byteValue()); if(raw.contains("Damage"))input.putShort("Damage",legacyDamage.shortValue()); - ItemStack stack = ItemStack.read(input); + ItemStack stack = ItemStack.of(input); if(emptyStack(stack) || stack.getItem() != registered || stack.getCount() != count.intValue() - || stack.getDamage() != damage.intValue() || stack.getMaxStackSize() <= 0) return null; + || stack.getDamageValue() != damage.intValue() || stack.getMaxStackSize() <= 0) return null; // Forge16 lazily echoes capability NBT before gathering providers. // Resolve them before comparing: unsupported payloads must remain raw, // rather than disappearing later during an inventory transaction. if(raw.contains("ForgeCaps")) stack.getCapability(net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY); - CompoundNBT encoded = stack.write(new CompoundNBT()); - INBT expectedTag=raw.get("tag"); + CompoundTag encoded = stack.save(new CompoundTag()); + Tag expectedTag=raw.get("tag"); // Native16 writes an explicit zero for missing tool durability. // Allow only that canonical default, retaining every other byte. if(registered.isDamageable(stack) && !raw.getCompound("tag").contains("Damage") && damage==0L) { - CompoundNBT canonical=raw.getCompound("tag").copy();canonical.putInt("Damage",0);expectedTag=canonical; + CompoundTag canonical=raw.getCompound("tag").copy();canonical.putInt("Damage",0);expectedTag=canonical; } if(!Objects.equals(expectedTag,encoded.get("tag")) || !Objects.equals(raw.get("ForgeCaps"),encoded.get("ForgeCaps"))) return null; @@ -1333,26 +1306,26 @@ private ItemStack decodeInventoryItem(CompoundNBT raw) { } catch(RuntimeException unreadable) { return null; } } - private void readInventoryEntry(String source, CompoundNBT raw, CompoundNBT retained) { - CompoundNBT context=retained!=null && retained.contains("SourceVersions",10) ? retained.getCompound("SourceVersions") : inventoryVersions; - CompoundNBT converted=ItemMigration.convert(raw,context); + private void readInventoryEntry(String source, CompoundTag raw, CompoundTag retained) { + CompoundTag context=retained!=null && retained.contains("SourceVersions",10) ? retained.getCompound("SourceVersions") : inventoryVersions; + CompoundTag converted=ItemMigration.convert(raw,context); ItemStack stack = converted==null ? null : decodeInventoryItem(converted); if(emptyStack(stack)) { if(retained == null) retainRaw(source,raw); else { - CompoundNBT copy=retained.copy(); + CompoundTag copy=retained.copy(); if(!copy.contains("SourceVersions") && !context.isEmpty()) copy.put("SourceVersions",context.copy()); unresolvedInventory.add(copy); } return; } - int limit = Math.min(stack.getMaxStackSize(),getInventoryStackLimit()); + int limit = Math.min(stack.getMaxStackSize(),getMaxStackSize()); if(source.equals("Equipment")) { Long number = integral(raw.get("Slot")); - if(number != null && number >= 0 && number < getSizeInventory()) { + if(number != null && number >= 0 && number < getContainerSize()) { int slot = number.intValue(); - if(emptyStack(getStackInSlot(slot)) && isItemValidForSlot(slot,stack)) { - setInventorySlotContents(slot,stack.split(Math.min(stack.getCount(),slot < 4 ? 1 : limit))); + if(emptyStack(getItem(slot)) && canPlaceItem(slot,stack)) { + setItem(slot,stack.split(Math.min(stack.getCount(),slot < 4 ? 1 : limit))); } } } diff --git a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java index d336573..101914a 100644 --- a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java +++ b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java @@ -5,13 +5,13 @@ import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.Items; -import net.minecraft.inventory.EquipmentSlotType; -import net.minecraft.item.ItemStack; -import net.minecraft.util.HandSide; -import net.minecraft.world.World; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Items; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.entity.HumanoidArm; +import net.minecraft.world.level.Level; import net.minecraftforge.event.entity.living.LivingDeathEvent; import net.minecraftforge.event.entity.EntityEvent; import net.minecraftforge.event.entity.living.LivingDropsEvent; @@ -31,32 +31,26 @@ public class PlayerDeathEventHandler { - private static final Map> equipmentCache = new WeakHashMap<>(); - - @SubscribeEvent(priority= EventPriority.HIGHEST) - public void corpseEnteringChunk(EntityEvent.EnteringChunk e) { - if(e.getEntity() instanceof EntityLootableBody && !e.getEntity().getEntityWorld().isRemote) - ((EntityLootableBody)e.getEntity()).recoverPositionInChunk(e.getNewChunkX(),e.getNewChunkZ()); - } + private static final Map> equipmentCache = new WeakHashMap<>(); @SubscribeEvent(priority= EventPriority.LOW, receiveCanceled = true) public void entityDeathEvent(LivingDeathEvent e){ - if(e.getEntity() instanceof PlayerEntity - && !e.getEntity().getEntityWorld().isRemote) { - final PlayerEntity player = (PlayerEntity)e.getEntity(); + if(e.getEntity() instanceof Player + && !e.getEntity().getCommandSenderWorld().isClientSide) { + final Player player = (Player)e.getEntity(); equipmentCache.remove(player); if(e.isCanceled() || e.getResult() == Event.Result.DENY || player.isSpectator() - || player.getEntityWorld().getGameRules().getBoolean(net.minecraft.world.GameRules.KEEP_INVENTORY)) return; - Map cache = new IdentityHashMap<>(); - for(EquipmentSlotType slot : EntityLootableBody.EQUIPMENT_SLOTS){ - ItemStack stack = player.getItemStackFromSlot(slot); + || player.getCommandSenderWorld().getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY)) return; + Map cache = new IdentityHashMap<>(); + for(EquipmentSlot slot : EntityLootableBody.EQUIPMENT_SLOTS){ + ItemStack stack = player.getItemBySlot(slot); if(!emptyStack(stack)) cache.put(stack,slot); } - if(player.getPrimaryHand() == HandSide.LEFT){ + if(player.getMainArm() == HumanoidArm.LEFT){ // swap main and off hand items (easier than messing with the rendering code) - cache.put(player.getItemStackFromSlot(EquipmentSlotType.MAINHAND),EquipmentSlotType.OFFHAND); - cache.put(player.getItemStackFromSlot(EquipmentSlotType.OFFHAND),EquipmentSlotType.MAINHAND); + cache.put(player.getItemBySlot(EquipmentSlot.MAINHAND),EquipmentSlot.OFFHAND); + cache.put(player.getItemBySlot(EquipmentSlot.OFFHAND),EquipmentSlot.MAINHAND); } equipmentCache.put(player,cache); } @@ -64,28 +58,28 @@ public void entityDeathEvent(LivingDeathEvent e){ @SubscribeEvent(priority= EventPriority.LOWEST, receiveCanceled = true) public void entityDropEvent(LivingDropsEvent e){ - if(e.getEntity() instanceof PlayerEntity - && !e.getEntity().getEntityWorld().isRemote) { - final PlayerEntity player = (PlayerEntity)e.getEntity(); - Map cache = equipmentCache.remove(player); + if(e.getEntity() instanceof Player + && !e.getEntity().getCommandSenderWorld().isClientSide) { + final Player player = (Player)e.getEntity(); + Map cache = equipmentCache.remove(player); if(e.isCanceled() || e.getResult() == Event.Result.DENY || player.isSpectator() - || player.getEntityWorld().getGameRules().getBoolean(net.minecraft.world.GameRules.KEEP_INVENTORY)) return; + || player.getCommandSenderWorld().getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY)) return; if(cache == null) cache = Collections.emptyMap(); - final World w = player.getEntityWorld(); + final Level w = player.getCommandSenderWorld(); EntityLootableBody corpse = new EntityLootableBody(player); corpse.setUserName(player.getGameProfile().getName()); - corpse.setRotation(player.rotationYaw); + corpse.setRotation(player.getYRot()); List items = new ArrayList<>(); - Set assignedSlots = EnumSet.noneOf(EquipmentSlotType.class); + Set assignedSlots = EnumSet.noneOf(EquipmentSlot.class); for (ItemEntity itemEntity : e.getDrops()) { ItemStack item = itemEntity.getItem(); if(emptyStack(item) || item.getCount() <= 0) continue; - EquipmentSlotType slot = cache.get(item); + EquipmentSlot slot = cache.get(item); // Stage independent stacks so a rejected spawn cannot mutate vanilla drops. if (slot != null && assignedSlots.add(slot)) { - corpse.setItemStackToSlot(slot,item.copy()); + corpse.setItemSlot(slot,item.copy()); } else { items.add(item.copy()); } @@ -93,11 +87,11 @@ public void entityDropEvent(LivingDropsEvent e){ corpse.initializeItems(items.toArray(new ItemStack[0])); if(LootableBodies.addBonesToCorpse){ - corpse.addItem(new ItemStack(Items.BONE,1+w.rand.nextInt(3))); - corpse.addItem(new ItemStack(Items.ROTTEN_FLESH,1+w.rand.nextInt(3))); + corpse.addItem(new ItemStack(Items.BONE,1+w.random.nextInt(3))); + corpse.addItem(new ItemStack(Items.ROTTEN_FLESH,1+w.random.nextInt(3))); } - if(w.addEntity(corpse)) { + if(w.addFreshEntity(corpse)) { e.getDrops().clear(); } else { ModLog.warning("%s: Corpse spawn rejected; original death drops retained", LootableBodies.MODID); diff --git a/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java b/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java index e60ba4c..c22f05f 100644 --- a/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java +++ b/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java @@ -2,7 +2,7 @@ import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import net.minecraft.client.Minecraft; -import net.minecraft.entity.Entity; +import net.minecraft.world.entity.Entity; import net.minecraftforge.api.distmarker.*; @OnlyIn(Dist.CLIENT) public final class ClientConfigChanges { @@ -10,7 +10,7 @@ private ClientConfigChanges() { } public static void apply() { boolean wasLocal=LootableBodies.useLocalSkin;LootableBodies.saveClientConfiguration(); Minecraft mc=Minecraft.getInstance(); - if(wasLocal && !LootableBodies.useLocalSkin && mc.world!=null) - for(Entity entity:mc.world.getAllEntities())if(entity instanceof EntityLootableBody)((EntityLootableBody)entity).refreshClientSkin(); + if(wasLocal && !LootableBodies.useLocalSkin && mc.level!=null) + for(Entity entity:mc.level.entitiesForRendering())if(entity instanceof EntityLootableBody)((EntityLootableBody)entity).refreshClientSkin(); } } diff --git a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java index db61082..bed51b6 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java @@ -4,10 +4,10 @@ import cyano.lootable.config.LegacyConfiguration.Property; import java.util.*; import net.minecraft.client.gui.*; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.button.Button; -import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.resources.I18n; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.resources.language.I18n; import net.minecraftforge.api.distmarker.*; /** The ordinary Mods-list settings screen, retaining the existing CFG choices. */ @@ -18,45 +18,45 @@ public final class CorpseConfigGui extends Screen { private final Screen parent; private final String group; private final Map pending; - private final Map fields=new LinkedHashMap<>(); + private final Map fields=new LinkedHashMap<>(); private final Map rows=new LinkedHashMap<>(); 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.util.text.StringTextComponent(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(new net.minecraft.network.chat.TextComponent(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 addButton(new SettingButton(id,x,y,width,label,click));} + 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.util.text.StringTextComponent(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,new net.minecraft.network.chat.TextComponent(label),button->action.run());this.id=id;} } @Override protected void init(){ fields.clear();rows.clear();int left=width/2-155; if(group==null){ - button(1,left,65,310,I18n.format("lootablebodies.config.appearance"),()->minecraft.displayGuiScreen(new CorpseConfigGui(this,"appearance",pending,0))); - button(2,left,95,310,I18n.format("lootablebodies.config.rules"),()->minecraft.displayGuiScreen(new CorpseConfigGui(this,"rules",pending,0))).active=minecraft.world==null; + button(1,left,65,310,I18n.get("lootablebodies.config.appearance"),()->minecraft.setScreen(new CorpseConfigGui(this,"appearance",pending,0))); + button(2,left,95,310,I18n.get("lootablebodies.config.rules"),()->minecraft.setScreen(new CorpseConfigGui(this,"rules",pending,0))).active=minecraft.level==null; }else{ String[] keys=group.equals("appearance")?APPEARANCE:RULES;perPage=Math.max(2,(height-112)/26);page=Math.min(page,(keys.length-1)/perPage); int start=page*perPage,end=Math.min(keys.length,start+perPage); for(int i=start;i{capture();pending.put(key,Boolean.toString(!Boolean.parseBoolean(pending.get(key))));init(minecraft,width,height);}); - else {TextFieldWidget field=new TextFieldWidget(font,width/2+60,y+1,95,18,new net.minecraft.util.text.TranslationTextComponent(property(key).getLanguageKey()));field.setMaxStringLength(64);field.setText(pending.get(key));fields.put(key,field);children.add(field);} + 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);} } if(page>0)button(3,left,height-56,80,"<",()->changePage(-1)); if(end",()->changePage(1)); - button(5,width/2-65,height-56,130,I18n.format("controls.reset"),()->{for(String key:keys)pending.put(key,property(key).getDefault());init(minecraft,width,height);}); + button(5,width/2-65,height-56,130,I18n.get("controls.reset"),()->{for(String key:keys)pending.put(key,property(key).getDefault());init(minecraft,width,height);}); } - button(200,width/2-155,height-28,150,I18n.format("gui.done"),()->{ + button(200,width/2-155,height-28,150,I18n.get("gui.done"),()->{ capture();if(!validPending()){invalid=true;return;} - if(group==null){for(String key:keys())property(key).set(pending.get(key));ClientConfigChanges.apply();}else if(parent instanceof CorpseConfigGui)((CorpseConfigGui)parent).pending.putAll(pending);minecraft.displayGuiScreen(parent); + if(group==null){for(String key:keys())property(key).set(pending.get(key));ClientConfigChanges.apply();}else if(parent instanceof CorpseConfigGui)((CorpseConfigGui)parent).pending.putAll(pending);minecraft.setScreen(parent); }); - button(201,width/2+5,height-28,150,I18n.format("gui.cancel"),()->minecraft.displayGuiScreen(parent)); + button(201,width/2+5,height-28,150,I18n.get("gui.cancel"),()->minecraft.setScreen(parent)); } - private void capture(){for(Map.Entry entry:fields.entrySet())pending.put(entry.getKey(),entry.getValue().getText());} + private void capture(){for(Map.Entry entry:fields.entrySet())pending.put(entry.getKey(),entry.getValue().getValue());} private void changePage(int change){capture();page+=change;init(minecraft,width,height);} - private String booleanLabel(String key){return I18n.format(Boolean.parseBoolean(pending.get(key))?"options.on":"options.off");} + private String booleanLabel(String key){return I18n.get(Boolean.parseBoolean(pending.get(key))?"options.on":"options.off");} private boolean validPending(){ for(String key:keys()){ Property property=property(key);String value=pending.get(key); @@ -64,21 +64,21 @@ private boolean validPending(){ else if(property.getType()=='D' || property.getType()=='I')try{double number=Double.parseDouble(value);if(!Double.isFinite(number)||numberproperty.getMax())return false;}catch(NumberFormatException error){return false;} }return true; } - @Override public void tick(){for(TextFieldWidget field:fields.values())field.tick();} - @Override public void render(com.mojang.blaze3d.matrix.MatrixStack matrices,int mouseX,int mouseY,float partialTicks){ + @Override public void tick(){for(EditBox field:fields.values())field.tick();} + @Override public void render(com.mojang.blaze3d.vertex.PoseStack matrices,int mouseX,int mouseY,float partialTicks){ renderBackground(matrices);drawCenteredString(matrices,font,LootableBodies.NAME,width/2,12,0xffffff); - drawCenteredString(matrices,font,I18n.format(group==null?"lootablebodies.config.local_file":"lootablebodies.config."+group),width/2,28,0xcccccc); + drawCenteredString(matrices,font,I18n.get(group==null?"lootablebodies.config.local_file":"lootablebodies.config."+group),width/2,28,0xcccccc); for(Map.Entry row:rows.entrySet()){ - String text=I18n.format(property(row.getKey()).getLanguageKey());drawString(matrices,font,font.trimStringToWidth(text,205),width/2-155,row.getValue()+6,0xffffff); + String text=I18n.get(property(row.getKey()).getLanguageKey());drawString(matrices,font,font.plainSubstrByWidth(text,205),width/2-155,row.getValue()+6,0xffffff); } - super.render(matrices,mouseX,mouseY,partialTicks);for(TextFieldWidget field:fields.values())field.render(matrices,mouseX,mouseY,partialTicks); + 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) - renderTooltip(matrices,font.trimStringToWidth(new net.minecraft.util.text.TranslationTextComponent("lootablebodies.config.rules.tooltip"),300),mouseX,mouseY); - if(invalid)drawCenteredString(matrices,font,I18n.format("lootablebodies.config.invalid"),width/2,height-72,0xff5555); + renderComponentTooltip(matrices,java.util.Collections.singletonList(new net.minecraft.network.chat.TranslatableComponent("lootablebodies.config.rules.tooltip")),mouseX,mouseY); + if(invalid)drawCenteredString(matrices,font,I18n.get("lootablebodies.config.invalid"),width/2,height-72,0xff5555); } - @Override public void closeScreen(){capture();minecraft.displayGuiScreen(parent);} + @Override public void onClose(){capture();minecraft.setScreen(parent);} public static boolean isValidDecayTime(String value) { if (!value.matches("\\s*[0-9]+\\s*:\\s*[0-9]+\\s*(?::\\s*[0-9]+\\s*)?")) return false; String[] parts = value.trim().split(":"); diff --git a/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java b/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java index ecf6cc5..9802060 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java @@ -1,7 +1,7 @@ package cyano.lootable.graphics; import java.util.function.BiFunction; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screens.Screen; import net.minecraftforge.api.distmarker.*; /** The regular Forge Mods-list Config button entry point. */ @OnlyIn(Dist.CLIENT) diff --git a/src/main/java/cyano/lootable/graphics/CorpseContainer.java b/src/main/java/cyano/lootable/graphics/CorpseContainer.java index 407a186..aee83e5 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseContainer.java @@ -3,17 +3,17 @@ import static cyano.lootable.ItemStacks.emptyStack; import cyano.lootable.entities.EntityLootableBody; -import net.minecraft.entity.Entity; -import net.minecraft.entity.item.ItemEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.entity.player.ServerPlayerEntity; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.inventory.EquipmentSlotType; -import net.minecraft.inventory.IInventory; -import net.minecraft.inventory.container.Slot; -import net.minecraft.inventory.container.ClickType; -import net.minecraft.item.ArmorItem; -import net.minecraft.item.ItemStack; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ArmorItem; +import net.minecraft.world.item.ItemStack; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.common.MinecraftForge; @@ -22,29 +22,29 @@ /** * Created by Chris on 4/12/2016. */ -public class CorpseContainer extends net.minecraft.inventory.container.Container { +public class CorpseContainer extends net.minecraft.world.inventory.AbstractContainerMenu { - private static final EquipmentSlotType[] EQUIPMENT_SLOTS = EntityLootableBody.EQUIPMENT_SLOTS; + private static final EquipmentSlot[] EQUIPMENT_SLOTS = EntityLootableBody.EQUIPMENT_SLOTS; - private final IInventory targetInventory; + private final Container targetInventory; private boolean dragging; - public CorpseContainer(PlayerInventory playerItems, IInventory entity){this(0,playerItems,entity);} - public CorpseContainer(int windowId, PlayerInventory playerItems, IInventory entity){ + public CorpseContainer(Inventory playerItems, Container entity){this(0,playerItems,entity);} + public CorpseContainer(int windowId, Inventory playerItems, Container entity){ super(cyano.lootable.LootableBodies.CORPSE_CONTAINER,windowId); this.targetInventory = entity; int index = 0; while(index < 4) { // armor final int i = index; - this.addSlot(new net.minecraft.inventory.container.Slot(targetInventory, i, 8, 8 + 18*i) { + this.addSlot(new net.minecraft.world.inventory.Slot(targetInventory, i, 8, 8 + 18*i) { @Override - public int getSlotStackLimit() { + public int getMaxStackSize() { return 1; } @Override - public boolean isItemValid(ItemStack item) { - return !emptyStack(item) && item.getCount() > 0 && super.isItemValid(item) && item.canEquip(EQUIPMENT_SLOTS[i], (Entity) targetInventory); + public boolean mayPlace(ItemStack item) { + return !emptyStack(item) && item.getCount() > 0 && super.mayPlace(item) && item.canEquip(EQUIPMENT_SLOTS[i], (Entity) targetInventory); } @OnlyIn(Dist.CLIENT) public String getSlotTexture() @@ -57,7 +57,7 @@ public String getSlotTexture() while(index < EQUIPMENT_SLOTS.length){ // held items final int i = index; - this.addSlot(new net.minecraft.inventory.container.Slot(targetInventory, i, 8 + 18 * (i - 4), 98 ) { + this.addSlot(new net.minecraft.world.inventory.Slot(targetInventory, i, 8 + 18 * (i - 4), 98 ) { @OnlyIn(Dist.CLIENT) public String getSlotTexture() { @@ -67,7 +67,7 @@ public String getSlotTexture() index++; } - while(index < targetInventory.getSizeInventory()){ + while(index < targetInventory.getContainerSize()){ int n = index - EQUIPMENT_SLOTS.length; int x = n % 6; int y = n / 6; @@ -83,140 +83,140 @@ public String getSlotTexture() @Override - public boolean canInteractWith(PlayerEntity entityplayer) { - return targetInventory.isUsableByPlayer(entityplayer); + public boolean stillValid(Player entityplayer) { + return targetInventory.stillValid(entityplayer); } @Override - public ItemStack slotClick(int slotId, int button, ClickType type, PlayerEntity player) { + public void clicked(int slotId, int button, ClickType type, Player player) { // A click can arrive before the next player tick closes a stale window. - if(!canInteractWith(player)) { - resetDrag(); - return ItemStack.EMPTY; + if(!stillValid(player)) { + resetQuickCraft(); + return; } if(type != ClickType.QUICK_CRAFT && dragging) { - resetDrag(); - return ItemStack.EMPTY; + resetQuickCraft(); + return; } - if(type == ClickType.QUICK_CRAFT && getDragEvent(button) == 0 - && !emptyStack(player.inventory.getItemStack()) && isValidDragMode(extractDragMode(button),player)) dragging = true; - if(type == ClickType.THROW && emptyStack(player.inventory.getItemStack()) - && slotId >= 0 && slotId < inventorySlots.size()) { + if(type == ClickType.QUICK_CRAFT && getQuickcraftHeader(button) == 0 + && !emptyStack(getCarried()) && isValidQuickcraftType(getQuickcraftType(button),player)) dragging = true; + if(type == ClickType.THROW && emptyStack(getCarried()) + && slotId >= 0 && slotId < slots.size()) { Slot slot = getSlot(slotId); - if(slot.getHasStack() && slot.canTakeStack(player)) { - ItemStack original = slot.getStack().copy(); - ItemStack removed = slot.decrStackSize(button == 0 ? 1 : original.getCount()); + if(slot.hasItem() && slot.mayPickup(player)) { + ItemStack original = slot.getItem().copy(); + ItemStack removed = slot.remove(button == 0 ? 1 : original.getCount()); slot.onTake(player,removed); if(!tryRelease(player,removed,true)) { - slot.putStack(original); + slot.set(original); resync(player); } } - return ItemStack.EMPTY; + return; } if((type == ClickType.PICKUP || type == ClickType.QUICK_MOVE) && slotId == -999 && (button == 0 || button == 1)) { - ItemStack cursor = player.inventory.getItemStack(); + ItemStack cursor = getCarried(); if(!emptyStack(cursor)) { ItemStack original = cursor.copy(); ItemStack removed = cursor.split(button == 0 ? cursor.getCount() : 1); - if(cursor.getCount() == 0) player.inventory.setItemStack(ItemStack.EMPTY); + if(cursor.getCount() == 0) setCarried(ItemStack.EMPTY); if(!tryRelease(player,removed,true)) { - player.inventory.setItemStack(original); + setCarried(original); resync(player); } } - return ItemStack.EMPTY; + return; } if(type == ClickType.SWAP && button >= 0 && button < 9 - && slotId >= 0 && slotId < targetInventory.getSizeInventory()) { + && slotId >= 0 && slotId < targetInventory.getContainerSize()) { Slot slot = getSlot(slotId); - ItemStack incoming = player.inventory.getStackInSlot(button); - ItemStack displaced = slot.getStack(); - if(!emptyStack(incoming) && !emptyStack(displaced) && slot.canTakeStack(player) && slot.isItemValid(incoming)) { - int limit = Math.min(slot.getItemStackLimit(incoming),incoming.getMaxStackSize()); + ItemStack incoming = player.getInventory().getItem(button); + ItemStack displaced = slot.getItem(); + if(!emptyStack(incoming) && !emptyStack(displaced) && slot.mayPickup(player) && slot.mayPlace(incoming)) { + int limit = Math.min(slot.getMaxStackSize(incoming),incoming.getMaxStackSize()); if(limit > 0 && incoming.getCount() > limit) { - slot.putStack(incoming.split(limit)); + slot.set(incoming.split(limit)); slot.onTake(player,displaced); - storeInPlayerInventory(player.inventory,displaced); + storeInPlayerInventory(player.getInventory(),displaced); if(displaced.getCount() > 0) releaseOrRetain(player,displaced); - return ItemStack.EMPTY; + return; } } } - return super.slotClick(slotId,button,type,player); + super.clicked(slotId,button,type,player); } @Override - public void onContainerClosed(PlayerEntity player) { - ItemStack cursor = player.inventory.getItemStack(); + public void removed(Player player) { + ItemStack cursor = getCarried(); if(!emptyStack(cursor)) { releaseOrRetain(player,cursor); - player.inventory.setItemStack(ItemStack.EMPTY); + setCarried(ItemStack.EMPTY); } } @Override - protected void resetDrag() { + protected void resetQuickCraft() { dragging = false; - super.resetDrag(); + super.resetQuickCraft(); } - private void resync(PlayerEntity player) { + private void resync(Player player) { // Accepted transaction replies assume client prediction succeeded. A // rejected toss needs an explicit source/cursor correction as well. - if(player instanceof ServerPlayerEntity) ((ServerPlayerEntity)player).sendContainerToPlayer(this); + if(player instanceof ServerPlayer) broadcastFullState(); } - private void releaseOrRetain(PlayerEntity player, ItemStack stack) { + private void releaseOrRetain(Player player, ItemStack stack) { if(emptyStack(stack) || stack.getCount() <= 0 || tryRelease(player,stack,false)) return; ItemStack remainder = stack.copy(); // PlayerInventory discards overflow in creative mode. Retain real // corpse/cursor items even when every player slot is full. - storeInPlayerInventory(player.inventory,remainder); + storeInPlayerInventory(player.getInventory(),remainder); if(remainder.getCount() > 0) ((EntityLootableBody)targetInventory).retainItem(remainder); } - private void storeInPlayerInventory(PlayerInventory inventory, ItemStack remainder) { + private void storeInPlayerInventory(Inventory inventory, ItemStack remainder) { // Keep vanilla's selected-slot, off-hand, main-slot and first-empty // priority, without PlayerInventory's creative overflow discard. while(remainder.getCount() > 0) { int destination = -1; if(!remainder.isDamaged()) { - if(canStore(inventory,inventory.currentItem,remainder)) destination = inventory.currentItem; + if(canStore(inventory,inventory.selected,remainder)) destination = inventory.selected; else if(canStore(inventory,40,remainder)) destination = 40; - else for(int i = 0; i < inventory.mainInventory.size(); i++) { + else for(int i = 0; i < inventory.items.size(); i++) { if(canStore(inventory,i,remainder)) { destination = i; break; } } } - if(destination < 0) destination = inventory.getFirstEmptyStack(); + if(destination < 0) destination = inventory.getFreeSlot(); if(destination < 0) return; - ItemStack current = inventory.getStackInSlot(destination); - int limit = Math.min(remainder.getMaxStackSize(),inventory.getInventoryStackLimit()); + ItemStack current = inventory.getItem(destination); + int limit = Math.min(remainder.getMaxStackSize(),inventory.getMaxStackSize()); int amount = Math.min(remainder.getCount(),Math.max(0,limit - (emptyStack(current) ? 0 : current.getCount()))); if(amount == 0) return; if(emptyStack(current)) { - inventory.setInventorySlotContents(destination,remainder.split(amount)); - current = inventory.getStackInSlot(destination); + inventory.setItem(destination,remainder.split(amount)); + current = inventory.getItem(destination); } else { current.grow(amount); remainder.shrink(amount); } - current.setAnimationsToGo(5); - inventory.markDirty(); + current.setPopTime(5); + inventory.setChanged(); } } - private boolean canStore(PlayerInventory inventory, int slot, ItemStack incoming) { - ItemStack current = inventory.getStackInSlot(slot); + private boolean canStore(Inventory inventory, int slot, ItemStack incoming) { + ItemStack current = inventory.getItem(slot); return !emptyStack(current) && current.isStackable() && current.getItem() == incoming.getItem() - && ItemStack.areItemStackTagsEqual(current,incoming) - && current.getCount() < Math.min(current.getMaxStackSize(),inventory.getInventoryStackLimit()); + && ItemStack.tagMatches(current,incoming) + && current.getCount() < Math.min(current.getMaxStackSize(),inventory.getMaxStackSize()); } - private boolean tryRelease(PlayerEntity player, ItemStack stack, boolean includeName) { + private boolean tryRelease(Player player, ItemStack stack, boolean includeName) { if(emptyStack(stack) || stack.getCount() <= 0) return false; - if(player.world.isRemote) { - player.dropItem(stack.copy(),includeName); + if(player.level.isClientSide) { + player.drop(stack.copy(),includeName); return true; } // Forge's legacy toss helper clears the caller's captured drops and @@ -224,7 +224,7 @@ private boolean tryRelease(PlayerEntity player, ItemStack stack, boolean include java.util.Collection capturing = player.captureDrops(new java.util.ArrayList<>()); ItemEntity dropped; try { - dropped = player.dropItem(stack.copy(),false,includeName); + dropped = player.drop(stack.copy(),false,includeName); if(dropped != null) player.captureDrops().remove(dropped); } finally { player.captureDrops(capturing); @@ -236,30 +236,30 @@ private boolean tryRelease(PlayerEntity player, ItemStack stack, boolean include capturing.add(event.getEntityItem()); return true; } - return player.world.addEntity(event.getEntityItem()); + return player.level.addFreshEntity(event.getEntityItem()); } @Override - protected boolean mergeItemStack(ItemStack stack, int start, int end, boolean reverse) { + protected boolean moveItemStackTo(ItemStack stack, int start, int end, boolean reverse) { boolean changed = false; int step = reverse ? -1 : 1; for(int pass = 0; pass < 2 && stack.getCount() > 0; pass++) { for(int i = reverse ? end - 1 : start; i >= start && i < end && stack.getCount() > 0; i += step) { Slot slot = getSlot(i); - if(!slot.isItemValid(stack)) continue; - ItemStack current = slot.getStack(); - int limit = Math.min(slot.getItemStackLimit(stack),stack.getMaxStackSize()); + if(!slot.mayPlace(stack)) continue; + ItemStack current = slot.getItem(); + int limit = Math.min(slot.getMaxStackSize(stack),stack.getMaxStackSize()); if(pass == 0 && stack.isStackable() && !emptyStack(current) - && ItemStack.areItemsEqual(stack,current) && ItemStack.areItemStackTagsEqual(stack,current)) { + && ItemStack.isSame(stack,current) && ItemStack.tagMatches(stack,current)) { int amount = Math.min(stack.getCount(),Math.max(0,limit - current.getCount())); if(amount > 0) { current.grow(amount); stack.shrink(amount); - slot.onSlotChanged(); + slot.setChanged(); changed = true; } } else if(pass == 1 && emptyStack(current) && limit > 0) { - slot.putStack(stack.split(Math.min(stack.getCount(),limit))); + slot.set(stack.split(Math.min(stack.getCount(),limit))); changed = true; } } @@ -269,30 +269,30 @@ protected boolean mergeItemStack(ItemStack stack, int start, int end, boolean re @Override - public ItemStack transferStackInSlot(PlayerEntity player, int slot) { - int hostSize = targetInventory.getSizeInventory(); + public ItemStack quickMoveStack(Player player, int slot) { + int hostSize = targetInventory.getContainerSize(); ItemStack stack = ItemStack.EMPTY; - Slot slotObject = (Slot) inventorySlots.get(slot); + Slot slotObject = (Slot) slots.get(slot); //null checks and checks if the item can be stacked (maxStackSize > 1) - if (slotObject != null && slotObject.getHasStack()) { - ItemStack stackInSlot = slotObject.getStack(); + if (slotObject != null && slotObject.hasItem()) { + ItemStack stackInSlot = slotObject.getItem(); stack = stackInSlot.copy(); //merges the item into player inventory since its in the tileEntity if (slot < hostSize) { - if (!this.mergeItemStack(stackInSlot, hostSize, 36+hostSize, true)) { + if (!this.moveItemStackTo(stackInSlot, hostSize, 36+hostSize, true)) { return ItemStack.EMPTY; } } //places it into the tileEntity if possible since it's in the player inventory - else if (!this.mergeItemStack(stackInSlot, 0, hostSize, false)) { + else if (!this.moveItemStackTo(stackInSlot, 0, hostSize, false)) { return ItemStack.EMPTY; } if (stackInSlot.getCount() == 0) { - slotObject.putStack(ItemStack.EMPTY); + slotObject.set(ItemStack.EMPTY); } else { - slotObject.onSlotChanged(); + slotObject.setChanged(); } if (stackInSlot.getCount() == stack.getCount()) { @@ -303,7 +303,7 @@ else if (!this.mergeItemStack(stackInSlot, 0, hostSize, false)) { return stack; } - protected void bindPlayerInventory(PlayerInventory playerInv, int yOffset) { + protected void bindPlayerInventory(Inventory playerInv, int yOffset) { for (int row = 0; row < 3; row++) { for (int col = 0; col < 9; col++) { addSlot(new Slot(playerInv, col + row * 9 + 9, 8 + col * 18, yOffset + row * 18)); diff --git a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java index b273d11..dfabd24 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java @@ -1,30 +1,31 @@ package cyano.lootable.graphics; import cyano.lootable.LootableBodies; -import net.minecraft.entity.player.PlayerInventory; -import net.minecraft.inventory.IInventory; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.Container; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.mojang.blaze3d.platform.GlStateManager; -import net.minecraft.util.text.ITextComponent; -import net.minecraft.util.text.StringTextComponent; +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. */ @OnlyIn(Dist.CLIENT) -public class CorpseGUIContainer extends net.minecraft.client.gui.screen.inventory.ContainerScreen{ +public class CorpseGUIContainer extends net.minecraft.client.gui.screens.inventory.AbstractContainerScreen{ private final ResourceLocation image = new ResourceLocation(LootableBodies.MODID+":textures/gui/corpse.png"); - public CorpseGUIContainer(PlayerInventory playerItems, IInventory entity) { - this(new CorpseContainer(playerItems,entity),playerItems,entity instanceof net.minecraft.entity.Entity?((net.minecraft.entity.Entity)entity).getDisplayName():new StringTextComponent(LootableBodies.NAME)); + 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)); } - public CorpseGUIContainer(CorpseContainer container, PlayerInventory playerItems, ITextComponent title){ + public CorpseGUIContainer(CorpseContainer container, Inventory playerItems, Component title){ super(container,playerItems,title); - this.xSize = 176; - this.ySize = 222; + this.imageWidth = 176; + this.imageHeight = 222; } /** @@ -36,16 +37,17 @@ public CorpseGUIContainer(CorpseContainer container, PlayerInventory playerItems * @param mouseY Mouse y coordinate */ @Override - protected void drawGuiContainerBackgroundLayer(com.mojang.blaze3d.matrix.MatrixStack matrices, float partialTicks, int mouseX, int mouseY) { - GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F); - int x = (width - xSize) / 2; - int y = (height - ySize) / 2; - this.minecraft.getTextureManager().bindTexture(image); - this.blit(matrices, x, y, 0, 0, this.xSize, this.ySize); // x, y, textureOffsetX, textureOffsetY, width, height) + protected void renderBg(com.mojang.blaze3d.vertex.PoseStack matrices, float partialTicks, int mouseX, int mouseY) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + RenderSystem.setShaderTexture(0,image); + this.blit(matrices, x, y, 0, 0, this.imageWidth, this.imageHeight); // x, y, textureOffsetX, textureOffsetY, width, height) } @Override - protected void drawGuiContainerForegroundLayer(com.mojang.blaze3d.matrix.MatrixStack matrices,int mouseX,int mouseY) { + protected void renderLabels(com.mojang.blaze3d.vertex.PoseStack matrices,int mouseX,int mouseY) { // This seven-row layout has no label strip. Keep the original screen's // foreground; the new vanilla labels would overlap corpse item slots. } diff --git a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java index e0efc20..81df845 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java @@ -2,62 +2,66 @@ import com.mojang.authlib.GameProfile; import com.mojang.authlib.minecraft.MinecraftProfileTexture; -import com.mojang.blaze3d.matrix.MatrixStack; +import com.mojang.blaze3d.vertex.PoseStack; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import java.util.Map; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.IRenderTypeBuffer; -import net.minecraft.util.math.vector.Vector3f; -import net.minecraft.client.renderer.entity.EntityRendererManager; -import net.minecraft.client.renderer.entity.LivingRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import com.mojang.math.Vector3f; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.model.geom.ModelLayers; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.layers.ArrowLayer; -import net.minecraft.client.renderer.entity.layers.BipedArmorLayer; -import net.minecraft.client.renderer.entity.layers.HeadLayer; -import net.minecraft.client.renderer.entity.layers.HeldItemLayer; -import net.minecraft.client.renderer.entity.model.BipedModel; -import net.minecraft.client.renderer.entity.model.PlayerModel; +import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; +import net.minecraft.client.renderer.entity.layers.CustomHeadLayer; +import net.minecraft.client.renderer.entity.layers.ItemInHandLayer; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.client.model.PlayerModel; import net.minecraft.client.resources.DefaultPlayerSkin; -import net.minecraft.util.ResourceLocation; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; /** Corpse renderer, originally created by Chris on 4/10/2016. */ @OnlyIn(Dist.CLIENT) -public class CorpseRenderer extends LivingRenderer> { +public class CorpseRenderer extends LivingEntityRenderer> { private static final ResourceLocation localSkin = new ResourceLocation(LootableBodies.MODID, "textures/entity/corpse/corpse.png"); protected final PlayerModel thickArmsModel; protected final PlayerModel thinArmsModel; - public CorpseRenderer(EntityRendererManager manager) { - super(manager, new FrozenPlayerModel(true), 0.5F); - thinArmsModel = entityModel; - thickArmsModel = new FrozenPlayerModel(false); - addLayer(new BipedArmorLayer<>(this, new FrozenArmorModel(0.5F), new FrozenArmorModel(1F))); - addLayer(new HeldItemLayer<>(this)); - addLayer(new ArrowLayer<>(this)); - addLayer(new HeadLayer<>(this)); + public CorpseRenderer(EntityRendererProvider.Context context) { + super(context, new FrozenPlayerModel(context.bakeLayer(ModelLayers.PLAYER_SLIM),true), 0.5F); + thinArmsModel = model; + 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)); + addLayer(new ArrowLayer<>(context,this)); + addLayer(new CustomHeadLayer<>(this,context.getModelSet())); } - public PlayerModel getMainModel() { return getEntityModel(); } - public void setModel(boolean thinArms) { entityModel = thinArms ? thinArmsModel : thickArmsModel; } + public PlayerModel getMainModel() { return getModel(); } + public void setModel(boolean thinArms) { model = thinArms ? thinArmsModel : thickArmsModel; } - @Override public ResourceLocation getEntityTexture(EntityLootableBody entity) { + @Override public ResourceLocation getTextureLocation(EntityLootableBody entity) { if (LootableBodies.useLocalSkin) return localSkin; GameProfile profile = entity.getGameProfile(); - return profile != null && profile.getId() != null ? getSkin(profile) : DefaultPlayerSkin.getDefaultSkinLegacy(); + return profile != null && profile.getId() != null ? getSkin(profile) : DefaultPlayerSkin.getDefaultSkin(); } public static ResourceLocation getSkin(GameProfile profile) { ResourceLocation fallback = profile == null || profile.getId() == null - ? DefaultPlayerSkin.getDefaultSkinLegacy() : DefaultPlayerSkin.getDefaultSkin(profile.getId()); + ? DefaultPlayerSkin.getDefaultSkin() : DefaultPlayerSkin.getDefaultSkin(profile.getId()); if (profile == null || profile.getId() == null) return fallback; try { Minecraft minecraft = Minecraft.getInstance(); - Map textures = minecraft.getSkinManager().loadSkinFromCache(profile); + Map textures = minecraft.getSkinManager().getInsecureSkinInformation(profile); return textures.containsKey(MinecraftProfileTexture.Type.SKIN) - ? minecraft.getSkinManager().loadSkin(textures.get(MinecraftProfileTexture.Type.SKIN), MinecraftProfileTexture.Type.SKIN) + ? minecraft.getSkinManager().registerTexture(textures.get(MinecraftProfileTexture.Type.SKIN), MinecraftProfileTexture.Type.SKIN) : fallback; } catch (RuntimeException unavailable) { return fallback; @@ -65,7 +69,7 @@ public static ResourceLocation getSkin(GameProfile profile) { } @Override public void render(EntityLootableBody entity, float yaw, float partialTicks, - MatrixStack matrices, IRenderTypeBuffer buffers, int light) { + PoseStack matrices, MultiBufferSource buffers, int light) { setModel(entity.useThinArms()); try { super.render(entity, 0F, partialTicks, matrices, buffers, light); @@ -74,42 +78,42 @@ public static ResourceLocation getSkin(GameProfile profile) { } } - @Override protected void applyRotations(EntityLootableBody entity, MatrixStack matrices, + @Override protected void setupRotations(EntityLootableBody entity, PoseStack matrices, float age, float yaw, float partialTicks) { - matrices.rotate(Vector3f.XP.rotationDegrees(90F)); - matrices.rotate(Vector3f.ZP.rotationDegrees(entity.getRotation())); + matrices.mulPose(Vector3f.XP.rotationDegrees(90F)); + matrices.mulPose(Vector3f.ZP.rotationDegrees(entity.getRotation())); matrices.translate(0D, -0.85D, -0.125D); - super.applyRotations(entity, matrices, 0F, 0F, partialTicks); + super.setupRotations(entity, matrices, 0F, 0F, partialTicks); } - @Override protected float getSwingProgress(EntityLootableBody entity, float partialTicks) { return 0F; } - @Override protected float handleRotationFloat(EntityLootableBody entity, float partialTicks) { return 0F; } - @Override protected boolean canRenderName(EntityLootableBody entity) { - return LootableBodies.displayNameTag && super.canRenderName(entity); + @Override protected float getAttackAnim(EntityLootableBody entity, float partialTicks) { return 0F; } + @Override protected float getBob(EntityLootableBody entity, float partialTicks) { return 0F; } + @Override protected boolean shouldShowName(EntityLootableBody entity) { + return LootableBodies.displayNameTag && super.shouldShowName(entity); } - @Override protected void renderName(EntityLootableBody entity, net.minecraft.util.text.ITextComponent name, MatrixStack matrices, - IRenderTypeBuffer buffers, int light) { - if (LootableBodies.displayNameTag) super.renderName(entity, name, matrices, buffers, light); + @Override protected void renderNameTag(EntityLootableBody entity, net.minecraft.network.chat.Component name, PoseStack matrices, + MultiBufferSource buffers, int light) { + if (LootableBodies.displayNameTag) super.renderNameTag(entity, name, matrices, buffers, light); } private static final class FrozenPlayerModel extends PlayerModel { - FrozenPlayerModel(boolean thin) { super(0F, thin); } - @Override public void setLivingAnimations(EntityLootableBody body, float swing, float amount, float partial) { - super.setLivingAnimations(body, 0F, 0F, 0F); + FrozenPlayerModel(ModelPart root,boolean thin) { super(root, thin); } + @Override public void prepareMobModel(EntityLootableBody body, float swing, float amount, float partial) { + super.prepareMobModel(body, 0F, 0F, 0F); } - @Override public void setRotationAngles(EntityLootableBody body, float swing, float amount, + @Override public void setupAnim(EntityLootableBody body, float swing, float amount, float age, float yaw, float pitch) { - super.setRotationAngles(body, 0F, 0F, 0F, 0F, 0F); + super.setupAnim(body, 0F, 0F, 0F, 0F, 0F); } } - private static final class FrozenArmorModel extends BipedModel { - FrozenArmorModel(float scale) { super(scale); } - @Override public void setLivingAnimations(EntityLootableBody body, float swing, float amount, float partial) { - super.setLivingAnimations(body, 0F, 0F, 0F); + private static final class FrozenArmorModel extends HumanoidModel { + FrozenArmorModel(ModelPart root) { super(root); } + @Override public void prepareMobModel(EntityLootableBody body, float swing, float amount, float partial) { + super.prepareMobModel(body, 0F, 0F, 0F); } - @Override public void setRotationAngles(EntityLootableBody body, float swing, float amount, + @Override public void setupAnim(EntityLootableBody body, float swing, float amount, float age, float yaw, float pitch) { - super.setRotationAngles(body, 0F, 0F, 0F, 0F, 0F); + super.setupAnim(body, 0F, 0F, 0F, 0F, 0F); } } } diff --git a/src/main/java/cyano/lootable/graphics/GUIHandler.java b/src/main/java/cyano/lootable/graphics/GUIHandler.java index 6e8a13f..d202336 100644 --- a/src/main/java/cyano/lootable/graphics/GUIHandler.java +++ b/src/main/java/cyano/lootable/graphics/GUIHandler.java @@ -1,16 +1,17 @@ package cyano.lootable.graphics; import cyano.lootable.entities.EntityLootableBody; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.*; -import net.minecraft.world.World; +import net.minecraft.world.entity.Entity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; import net.minecraftforge.api.distmarker.*; -import net.minecraftforge.fml.network.NetworkHooks; +import net.minecraftforge.fmllegacy.network.NetworkHooks; /** Native Forge container opening, with the historical local helper kept internal. */ public final class GUIHandler { private static final GUIHandler INSTANCE=new GUIHandler(); private GUIHandler() { } public static GUIHandler getInstance(){return INSTANCE;} - public static void open(PlayerEntity player,EntityLootableBody corpse){if(player instanceof ServerPlayerEntity)NetworkHooks.openGui((ServerPlayerEntity)player,corpse,buf->buf.writeVarInt(corpse.getEntityId()));} - public Object getServerGuiElement(int id,PlayerEntity player,World world,int x,int y,int z){Entity entity=world.getEntityByID(x);return id==0 && entity instanceof EntityLootableBody?new CorpseContainer(player.inventory,(EntityLootableBody)entity):null;} - @OnlyIn(Dist.CLIENT) public Object getClientGuiElement(int id,PlayerEntity player,World world,int x,int y,int z){Entity entity=world.getEntityByID(x);return id==0 && entity instanceof EntityLootableBody?new CorpseGUIContainer(player.inventory,(EntityLootableBody)entity):null;} + public static void open(Player player,EntityLootableBody corpse){if(player instanceof ServerPlayer)NetworkHooks.openGui((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/network/ClientLoginReadGuard.java b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java index d72a76a..66bc010 100644 --- a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java +++ b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java @@ -3,13 +3,13 @@ import io.netty.channel.Channel; import java.util.concurrent.RejectedExecutionException; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screen.ConnectingScreen; -import net.minecraft.network.ProtocolType; -import net.minecraft.network.NetworkManager; +import net.minecraft.client.gui.screens.ConnectScreen; +import net.minecraft.network.ConnectionProtocol; +import net.minecraft.network.Connection; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.ObfuscationReflectionHelper; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import net.minecraftforge.event.TickEvent; /** Repairs the pinned Netty's stale read-clear task during client login. */ @@ -18,9 +18,9 @@ public final class ClientLoginReadGuard { @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) { if(event.phase!=TickEvent.Phase.END)return; Minecraft minecraft=Minecraft.getInstance(); - if(!(minecraft.currentScreen instanceof ConnectingScreen))return; - NetworkManager manager=ObfuscationReflectionHelper.getPrivateValue( - ConnectingScreen.class,(ConnectingScreen)minecraft.currentScreen,"field_146371_g"); + if(!(minecraft.screen instanceof ConnectScreen))return; + Connection manager=ObfuscationReflectionHelper.getPrivateValue( + ConnectScreen.class,(ConnectScreen)minecraft.screen,"f_95684_"); if(manager!=null)resumeReads(manager.channel()); } @@ -33,7 +33,7 @@ public static void resumeReads(Channel channel) { // A queued clearReadPending from the connector thread can have // removed OP_READ even though autoRead has already become true. if(channel.isActive() && channel.config().isAutoRead() - && channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).get()==ProtocolType.LOGIN) + && channel.attr(Connection.ATTRIBUTE_PROTOCOL).get()==ConnectionProtocol.LOGIN) channel.read(); }); }catch(RejectedExecutionException closed) { diff --git a/src/main/java/cyano/lootable/network/CorpseProfiles.java b/src/main/java/cyano/lootable/network/CorpseProfiles.java new file mode 100644 index 0000000..e9f13bc --- /dev/null +++ b/src/main/java/cyano/lootable/network/CorpseProfiles.java @@ -0,0 +1,26 @@ +package cyano.lootable.network; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import net.minecraft.server.players.GameProfileCache; + +/** Synchronous native services; callers own the bounded background worker. */ +public final class CorpseProfiles { + private CorpseProfiles() { } + public static GameProfile resolve(GameProfile initial,GameProfileCache cache,MinecraftSessionService service) { + if(initial==null || cache==null || service==null)return initial; + try { + GameProfile cached=cache.get(initial.getName()).orElse(initial); + if(cached.getId()==null)return initial; + GameProfile resolved=new GameProfile(cached.getId(),cached.getName()); + resolved.getProperties().putAll(cached.getProperties()); + if(!resolved.getProperties().containsKey("textures"))resolved=service.fillProfileProperties(resolved,true); + if(resolved==null)return initial; + cache.add(resolved); + return resolved; + }catch(RuntimeException unavailable) { + cyano.lootable.ModLog.fine("Corpse profile lookup unavailable; using default skin"); + return initial; + } + } +} diff --git a/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java b/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java index 125a91b..ae5e47c 100644 --- a/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java +++ b/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java @@ -3,11 +3,11 @@ import java.lang.reflect.*; import java.util.*; import java.util.function.UnaryOperator; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.server.ServerWorld; -import net.minecraft.world.chunk.storage.ChunkLoader; -import net.minecraft.world.gen.feature.structure.LegacyStructureDataUtil; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler; /** Installs preservation before native save handlers and chunk readers run. */ public final class ChunkVersionContext { private ChunkVersionContext() { } @@ -16,12 +16,12 @@ public static void installClient(Object client,Object saveFormat) { } public static void installServer(MinecraftServer server) { installFixer(server,null); - installFixer(related(server,net.minecraft.world.storage.PlayerData.class),null); + installFixer(related(server,net.minecraft.world.level.storage.PlayerDataStorage.class),null); } - public static void installWorld(ServerWorld world) { - Object loader=world.getChunkProvider().chunkManager; - if(!(loader instanceof ChunkLoader))throw new IllegalStateException("Unsupported chunk reader; refusing unprotected world loading"); - installFixer(loader,chunk->LegacyStructureDataUtil.func_236992_a_(world.getDimensionKey(),world.getServer().getWorld(net.minecraft.world.World.OVERWORLD).getSavedData()).func_212181_a(chunk)); + public static void installWorld(ServerLevel world) { + Object loader=world.getChunkSource().chunkMap; + if(!(loader instanceof ChunkStorage))throw new IllegalStateException("Unsupported chunk reader; refusing unprotected world loading"); + installFixer(loader,chunk->LegacyStructureDataHandler.getLegacyStructureHandler(world.dimension(),world.getServer().getLevel(net.minecraft.world.level.Level.OVERWORLD).getDataStorage()).updateFromLegacy(chunk)); } private static Object related(Object owner,Class expected) { java.util.List matches=new java.util.ArrayList<>(); @@ -31,7 +31,7 @@ private static Object related(Object owner,Class expected) { try {Field field=matches.get(0);field.setAccessible(true);return field.get(owner);} catch(ReflectiveOperationException failure){throw new IllegalStateException("Cannot access native save reader",failure);} } - static void installFixer(Object owner,UnaryOperator structures) { + static void installFixer(Object owner,UnaryOperator structures) { List fields=new ArrayList<>(); for(Class type=owner.getClass();type!=null;type=type.getSuperclass())for(Field field:type.getDeclaredFields()) if(!Modifier.isStatic(field.getModifiers()) && field.getType()==DataFixer.class)fields.add(field); diff --git a/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java b/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java index 9d0016d..6663aa2 100644 --- a/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java +++ b/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java @@ -1,17 +1,17 @@ package cyano.lootable.upgrade; import java.util.UUID; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.CompoundTag; /** Reads both native UUID encodings without inventing a saved entity identity. */ final class CorpseIdentity { private CorpseIdentity() { } - static UUID read(CompoundNBT data) { + static UUID read(CompoundTag data) { UUID current=null,legacy=null; if(data.contains("UUID")) { if(!data.contains("UUID",11) || data.getIntArray("UUID").length!=4) throw new IllegalArgumentException("Malformed corpse UUID array"); - current=data.getUniqueId("UUID"); + current=data.getUUID("UUID"); } if(data.contains("UUIDMost") || data.contains("UUIDLeast")) { if(!data.contains("UUIDMost",4) || !data.contains("UUIDLeast",4)) @@ -23,7 +23,7 @@ static UUID read(CompoundNBT data) { if(current==null && legacy==null)throw new IllegalArgumentException("Missing saved corpse UUID"); return current==null?legacy:current; } - static boolean present(CompoundNBT data) { + static boolean present(CompoundTag data) { return data.contains("UUID") || data.contains("UUIDMost") || data.contains("UUIDLeast"); } } diff --git a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java index 3fa1a16..8b99c1b 100644 --- a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java +++ b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java @@ -4,28 +4,28 @@ import java.util.Collections; import java.util.Map; import java.util.WeakHashMap; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.world.storage.FolderName; -import net.minecraft.world.storage.IServerConfiguration; -import net.minecraft.world.storage.SaveFormat; -import net.minecraftforge.fml.WorldPersistenceHooks; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.level.storage.WorldData; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraftforge.fmllegacy.WorldPersistenceHooks; /** Preserve old chunks before Forge rewrites the level header on startup. */ public final class EarlyWorldPreservation implements WorldPersistenceHooks.WorldPersistenceHook { private static boolean installed; - private final Map metadata=Collections.synchronizedMap(new WeakHashMap<>()); + private final Map metadata=Collections.synchronizedMap(new WeakHashMap<>()); private EarlyWorldPreservation(){ } public static synchronized void install() { if(!installed){WorldPersistenceHooks.addHook(new EarlyWorldPreservation());installed=true;} } @Override public String getModId(){return "lootablebodies";} - @Override public void readData(SaveFormat.LevelSave save,IServerConfiguration info,CompoundNBT tag) { + @Override public void readData(LevelStorageSource.LevelStorageAccess save,WorldData info,CompoundTag tag) { metadata.put(save,tag.copy()); - try {UpgradePreservation.prepareEarly(save.resolveFilePath(FolderName.DOT));} + try {UpgradePreservation.prepareEarly(save.getLevelPath(LevelResource.ROOT));} catch(IOException unsafe){throw new UnsafeWorldUpgrade(unsafe);} } - @Override public CompoundNBT getDataForWriting(SaveFormat.LevelSave save,IServerConfiguration info) { - CompoundNBT original=metadata.get(save);return original==null?new CompoundNBT():original.copy(); + @Override public CompoundTag getDataForWriting(LevelStorageSource.LevelStorageAccess save,WorldData info) { + CompoundTag original=metadata.get(save);return original==null?new CompoundTag():original.copy(); } private static final class UnsafeWorldUpgrade extends Error { private static final long serialVersionUID=1L; diff --git a/src/main/java/cyano/lootable/upgrade/ItemMigration.java b/src/main/java/cyano/lootable/upgrade/ItemMigration.java index 84c7010..05070b0 100644 --- a/src/main/java/cyano/lootable/upgrade/ItemMigration.java +++ b/src/main/java/cyano/lootable/upgrade/ItemMigration.java @@ -1,8 +1,11 @@ package cyano.lootable.upgrade; import java.util.*; -import net.minecraft.item.Item; -import net.minecraft.nbt.*; -import net.minecraft.util.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; import com.mojang.datafixers.DataFixer; /** Runs native item fixes on copies, retaining ambiguous originals. */ public final class ItemMigration { @@ -10,19 +13,19 @@ public final class ItemMigration { private ItemMigration() { } static void register(DataFixer fixer){pipeline=fixer;} /** Null means that conversion is ambiguous; retain the original raw record. */ - public static CompoundNBT convert(CompoundNBT raw,CompoundNBT versions) { + public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { if(versions==null || versions.isEmpty())return raw.copy(); - CompoundNBT fixed=convertNbt(raw,versions); + CompoundTag fixed=convertNbt(raw,versions); return fixed!=null && knownNestedItems(fixed)?fixed:null; } /** NBT conversion is independent of live registry resolution. */ - static CompoundNBT convertNbt(CompoundNBT raw,CompoundNBT versions) { + static CompoundTag convertNbt(CompoundTag raw,CompoundTag versions) { if(versions==null || versions.isEmpty()) return raw.copy(); if(versions.getBoolean("Ambiguous")) { - CompoundNBT agreed=null; + CompoundTag agreed=null; for(int candidate:versions.getIntArray("CandidateDataVersions")) { - CompoundNBT context=versions.copy();context.remove("Ambiguous");context.remove("CandidateDataVersions");context.putInt("DataVersion",candidate); - CompoundNBT converted=convertNbt(raw,context); + CompoundTag context=versions.copy();context.remove("Ambiguous");context.remove("CandidateDataVersions");context.putInt("DataVersion",candidate); + CompoundTag converted=convertNbt(raw,context); if(converted==null || (agreed!=null && !agreed.equals(converted)))return null; agreed=converted; } @@ -32,89 +35,89 @@ static CompoundNBT convertNbt(CompoundNBT raw,CompoundNBT versions) { int source=versions.getInt("DataVersion"); if(source<512 || source>UpgradePreservation.MINECRAFT_VERSION || integral(raw.get("Count"))==null)return null; try { - CompoundNBT item=cyano.lootable.upgrade.NativeNbt.update(pipeline,net.minecraft.util.datafix.TypeReferences.ITEM_STACK,raw.copy(),source,UpgradePreservation.MINECRAFT_VERSION); + CompoundTag item=cyano.lootable.upgrade.NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK,raw.copy(),source,UpgradePreservation.MINECRAFT_VERSION); if(!Objects.equals(integral(raw.get("Count")),integral(item.get("Count"))) || !Objects.equals(raw.get("Slot"),item.get("Slot")) || !Objects.equals(raw.get("ForgeCaps"),item.get("ForgeCaps")))return null; if(!flatteningSurvives(raw,item,source))return null; - for(String key:raw.keySet())if(!Arrays.asList("id","Count","Damage","Slot","tag").contains(key) && !Objects.equals(raw.get(key),item.get(key)))return null; + for(String key:raw.getAllKeys())if(!Arrays.asList("id","Count","Damage","Slot","tag").contains(key) && !Objects.equals(raw.get(key),item.get(key)))return null; return nestedInventoriesSurvive(raw,item)?item:null; }catch(RuntimeException invalid){return null;} } - private static boolean flatteningSurvives(INBT before,INBT after,int source) { - if(before instanceof CompoundNBT) { - if(!(after instanceof CompoundNBT))return false; - CompoundNBT a=(CompoundNBT)before,b=(CompoundNBT)after; + private static boolean flatteningSurvives(Tag before,Tag after,int source) { + if(before instanceof CompoundTag) { + if(!(after instanceof CompoundTag))return false; + CompoundTag a=(CompoundTag)before,b=(CompoundTag)after; if(a.contains("id") && a.contains("Count") && source<1451) { Long damage=a.contains("Damage")?integral(a.get("Damage")):0L; if(damage==null || damage<0 || damage>32767)return false; if(damage!=0) { - String mapped=net.minecraft.util.datafix.fixes.ItemStackDataFlattening.updateItem(a.getString("id"),damage.intValue()); - String base=net.minecraft.util.datafix.fixes.ItemStackDataFlattening.updateItem(a.getString("id"),0); + String mapped=net.minecraft.util.datafix.fixes.ItemStackTheFlatteningFix.updateItem(a.getString("id"),damage.intValue()); + String base=net.minecraft.util.datafix.fixes.ItemStackTheFlatteningFix.updateItem(a.getString("id"),0); boolean durability=Objects.equals(damage,integral(b.getCompound("tag").get("Damage"))); // Vanilla silently falls back to variant zero for bad metadata. // Unknown mod metadata has no safe automatic interpretation. if(!durability && (mapped==null || mapped.equals(base)))return false; } } - for(String key:a.keySet()) { - INBT old=a.get(key),next=b.get(key); + for(String key:a.getAllKeys()) { + Tag old=a.get(key),next=b.get(key); if(key.equals("ench") || key.equals("StoredEnchantments")) { if(!enchantmentsSurvive(old,b.get(key.equals("ench")?"Enchantments":key)))return false; } else if(next!=null && !flatteningSurvives(old,next,source))return false; } - } else if(before instanceof ListNBT && after instanceof ListNBT) { - ListNBT a=(ListNBT)before,b=(ListNBT)after; + } else if(before instanceof ListTag && after instanceof ListTag) { + ListTag a=(ListTag)before,b=(ListTag)after; if(a.size()!=b.size())return false; for(int i=0;i=1 && nbt.getId()<=4 ? ((NumberNBT)nbt).getLong() : null; + private static Long integral(Tag nbt) { + return nbt instanceof NumericTag && nbt.getId()>=1 && nbt.getId()<=4 ? ((NumericTag)nbt).getAsLong() : null; } - private static boolean knownNestedItems(INBT tag) { - if(tag instanceof CompoundNBT) { - CompoundNBT compound=(CompoundNBT)tag; + private static boolean knownNestedItems(Tag tag) { + if(tag instanceof CompoundTag) { + CompoundTag compound=(CompoundTag)tag; if(compound.contains("id") && compound.contains("Count")) { if(!compound.contains("id",8)) return false; ResourceLocation id=new ResourceLocation(compound.getString("id")); - if(!net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(id) || Item.getIdFromItem(net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(id))==0) return false; + if(!net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(id) || Item.getId(net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(id))==0) return false; } - for(String key:compound.keySet()) if(!key.equals("ForgeCaps") && !knownNestedItems(compound.get(key))) return false; - } else if(tag instanceof ListNBT) { - ListNBT list=(ListNBT)tag;for(int i=0;i> event) { - event.getRegistry().register(new net.minecraft.entity.ai.brain.memory.MemoryModuleType(java.util.Optional.empty()) + 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")); } - @SubscribeEvent public static void items(RegistryEvent.MissingMappings event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.item.Item.class))return; - for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { + @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); 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.block.Block.class))return; - for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { + @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); if (replacement != null) mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.BLOCKS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.BLOCKS.getValue(replacement))); } } - @SubscribeEvent public static void entities(RegistryEvent.MissingMappings> event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.entity.EntityType.class))return; - for(RegistryEvent.MissingMappings.Mapping> mapping:event.getAllMappings()) { + @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))); } } - @SubscribeEvent public static void memories(RegistryEvent.MissingMappings> event) { - if(!event.getRegistry().getRegistrySuperType().equals(net.minecraft.entity.ai.brain.memory.MemoryModuleType.class))return; - for(RegistryEvent.MissingMappings.Mapping> mapping:event.getAllMappings()) + @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"))) 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")))); } diff --git a/src/main/java/cyano/lootable/upgrade/NativeNbt.java b/src/main/java/cyano/lootable/upgrade/NativeNbt.java index bc9ccbc..4631620 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeNbt.java +++ b/src/main/java/cyano/lootable/upgrade/NativeNbt.java @@ -3,14 +3,14 @@ import com.mojang.datafixers.DSL; import com.mojang.datafixers.DataFixer; import com.mojang.serialization.Dynamic; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.nbt.NBTDynamicOps; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; /** Native data-fixer entry point for explicit vanilla type references. */ public final class NativeNbt { private NativeNbt() { } - public static CompoundNBT update(DataFixer fixer, DSL.TypeReference type, - CompoundNBT input, int source, int target) { - return (CompoundNBT) fixer.update(type, new Dynamic<>(NBTDynamicOps.INSTANCE, input), source, target).getValue(); + public static CompoundTag update(DataFixer fixer, DSL.TypeReference type, + CompoundTag input, int source, int target) { + return (CompoundTag) fixer.update(type, new Dynamic<>(NbtOps.INSTANCE, input), source, target).getValue(); } } diff --git a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java index 7d732fd..612090c 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java +++ b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java @@ -6,22 +6,27 @@ import java.io.IOException; import java.util.*; import java.util.function.UnaryOperator; -import net.minecraft.nbt.*; -import net.minecraft.util.datafix.TypeReferences; -import net.minecraft.util.text.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +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 { final DataFixer delegate; - private final UnaryOperator structures; - NativeSafeFixer(DataFixer delegate,UnaryOperator structures){this.delegate=delegate;this.structures=structures;} + private final UnaryOperator structures; + private final UpgradeJournal preserved; + NativeSafeFixer(DataFixer delegate,UnaryOperator structures){this(delegate,structures,null);} + NativeSafeFixer(DataFixer delegate,UnaryOperator structures,UpgradeJournal preserved){this.delegate=delegate;this.structures=structures;this.preserved=preserved;} @Override public Schema getSchema(int key){return delegate.getSchema(key);} @Override @SuppressWarnings("unchecked") public Dynamic update(DSL.TypeReference type,Dynamic input,int version,int target) { - if(!(input.getValue() instanceof CompoundNBT))return delegate.update(type,input,version,target); - CompoundNBT raw=(CompoundNBT)input.getValue();String kind=type.typeName(); + if(!(input.getValue() instanceof CompoundTag))return delegate.update(type,input,version,target); + CompoundTag raw=(CompoundTag)input.getValue();String kind=type.typeName(); try { - int source=kind.equals("chunk")?chunkVersion(raw,version):version; - CompoundNBT result; + int source=kind.equals("chunk")?chunkVersion(raw,version):kind.equals("entity_chunk")?entityChunkVersion(raw):version; + CompoundTag result; if(kind.equals("chunk") && source<1493 && target>1493) { result=convert(type,raw,source,1493); if(result.getCompound("Level").getBoolean("hasLegacyStructureData") && structures!=null)result=structures.apply(result); @@ -39,18 +44,18 @@ private static final class UnsafeChunkUpgrade extends Error { private static final long serialVersionUID=1L; UnsafeChunkUpgrade(Throwable cause){super("Lootable Bodies stopped an unsafe chunk conversion. Keep the original world and upgrade journal.",cause);} } - private CompoundNBT convert(DSL.TypeReference type,CompoundNBT input,int source,int target) { - CompoundNBT copy=input.copy();Tokens tokens=new Tokens();String kind=type.typeName(); + private CompoundTag convert(DSL.TypeReference type,CompoundTag input,int source,int target) { + CompoundTag copy=input.copy();Tokens tokens=new Tokens();String kind=type.typeName(); // Current world settings are already a registry codec payload. Replaying // pre-1.16 fixes on a damaged stamp otherwise replaces the seed and // dimensions. Shield this field alone; vanilla still fixes the level. - if(kind.equals("world_gen_settings") && currentWorldSettings(copy))return copy; - INBT currentSettings=kind.equals("level") && copy.contains("WorldGenSettings",10) + if(kind.equals("world_gen_settings") && currentWorldSettings(copy))return updateCurrentWorldSettings(copy,source,target); + Tag currentSettings=kind.equals("level") && copy.contains("WorldGenSettings",10) && currentWorldSettings(copy.getCompound("WorldGenSettings"))?copy.get("WorldGenSettings").copy():null; if(currentSettings!=null)copy.remove("WorldGenSettings"); - INBT currentBiomes=null; + Tag currentBiomes=null; if(kind.equals("chunk")) { - CompoundNBT level=copy.getCompound("Level"); + CompoundTag level=copy.getCompound("Level"); // A damaged stamp can replay the old 2D biome expansion on an // already-current 3D array. Protect that field alone so older // vanilla entities and containers still receive their own fixes. @@ -64,14 +69,17 @@ private CompoundNBT convert(DSL.TypeReference type,CompoundNBT input,int source, level.putBoolean("TerrainPopulated",true);level.putBoolean("LightPopulated",true); } shieldList(level.getList("Entities",10),source,tokens,level.getInt("xPos"),level.getInt("zPos"),true); + } else if(kind.equals("entity_chunk")) { + int[] position=copy.getIntArray("Position"); + shieldList(copy.getList("Entities",10),source,tokens,position[0],position[1],true); } else if(kind.equals("entity") || kind.equals("entity_tree"))copy=shieldEntity(copy,source,tokens,0,0,false); else if(kind.equals("player")) { - CompoundNBT vehicle=copy.getCompound("RootVehicle"); + CompoundTag vehicle=copy.getCompound("RootVehicle"); if(vehicle.contains("Entity",10))vehicle.put("Entity",shieldEntity(vehicle.getCompound("Entity"),source,tokens,0,0,false)); for(String shoulder:new String[]{"ShoulderEntityLeft","ShoulderEntityRight"})if(copy.contains(shoulder,10))copy.put(shoulder,shieldEntity(copy.getCompound(shoulder),source,tokens,0,0,false)); } protectEggs(copy,source,tokens); - CompoundNBT result; + CompoundTag result; if(source<704 && target>=705) { // The old item hook writes Chest/Banner/Pig names back while the // 704/705 schemas expect namespaced IDs. Cross that boundary on @@ -80,198 +88,235 @@ else if(kind.equals("player")) { bridgeIdentifiers(result,kind); result=cyano.lootable.upgrade.NativeNbt.update(delegate,type,result,705,target); } else result=source0 && ((ListTag)entities).getElementType()!=10)) + throw new IllegalStateException("Malformed saved entity chunk; refusing unsafe construction"); + int version=root.contains("DataVersion",3)?root.getInt("DataVersion"):-1; + if(version>UpgradePreservation.MINECRAFT_VERSION) + throw new IllegalStateException("Saved entity chunk comes from a newer Minecraft version"); + // Terrain Sections/Biomes cannot identify the separate entity format. + // The owned current writer marker proves the native version even when + // an outer stamp was removed or downgraded. + for(UpgradeJournal.LocatedCorpse body:UpgradeJournal.corpses((ListTag)entities)) + if(body.entity.getString("id").equals("lootablebodies:corpse") + && body.entity.contains(UpgradePreservation.VERSION,3) + && body.entity.getInt(UpgradePreservation.VERSION)==UpgradePreservation.DATA_VERSION) + return UpgradePreservation.MINECRAFT_VERSION; + if(version<512)throw new IllegalStateException("Cannot prove saved entity chunk source version"); + return version; + } + private void bridgeIdentifiers(CompoundTag nbt,String kind) { switch(kind) { case "chunk": - CompoundNBT level=nbt.getCompound("Level"); + CompoundTag level=nbt.getCompound("Level"); bridgeList(level,"Entities","entity_tree");bridgeList(level,"TileEntities","block_entity");break; case "player": bridgeList(nbt,"Inventory","item_stack");bridgeList(nbt,"EnderItems","item_stack"); bridgeField(nbt.getCompound("RootVehicle"),"Entity","entity_tree"); bridgeField(nbt,"ShoulderEntityLeft","entity_tree");bridgeField(nbt,"ShoulderEntityRight","entity_tree");break; case "item_stack": - CompoundNBT tag=nbt.getCompound("tag"); + CompoundTag tag=nbt.getCompound("tag"); bridgeField(tag,"EntityTag","entity_tree");bridgeField(tag,"BlockEntityTag","block_entity");break; case "entity": case "entity_tree": - renameIdentifier(nbt,TypeReferences.ENTITY_TYPE); + renameIdentifier(nbt,References.ENTITY_TREE); bridgeList(nbt,"Passengers","entity_tree");bridgeField(nbt,"Riding","entity_tree"); for(String key:new String[]{"Equipment","ArmorItems","HandItems","Items","Inventory"})bridgeList(nbt,key,"item_stack"); for(String key:new String[]{"Item","FireworksItem","Potion","SaddleItem","ArmorItem","DecorItem"})bridgeField(nbt,key,"item_stack"); bridgeField(nbt,"TileEntityData","block_entity"); - for(INBT entry:nbt.getCompound("Offers").getList("Recipes",10)) - for(String key:new String[]{"buy","buyB","sell"})bridgeField((CompoundNBT)entry,key,"item_stack"); + for(Tag entry:nbt.getCompound("Offers").getList("Recipes",10)) + for(String key:new String[]{"buy","buyB","sell"})bridgeField((CompoundTag)entry,key,"item_stack"); bridgeSpawner(nbt);break; case "block_entity": - renameIdentifier(nbt,TypeReferences.BLOCK_ENTITY); + renameIdentifier(nbt,References.BLOCK_ENTITY); bridgeList(nbt,"Items","item_stack");bridgeField(nbt,"RecordItem","item_stack");bridgeSpawner(nbt);break; case "structure": - for(INBT entry:nbt.getList("entities",10))bridgeField((CompoundNBT)entry,"nbt","entity_tree"); - for(INBT entry:nbt.getList("blocks",10))bridgeField((CompoundNBT)entry,"nbt","block_entity");break; + for(Tag entry:nbt.getList("entities",10))bridgeField((CompoundTag)entry,"nbt","entity_tree"); + for(Tag entry:nbt.getList("blocks",10))bridgeField((CompoundTag)entry,"nbt","block_entity");break; default: break; } } - private void renameIdentifier(CompoundNBT nbt,DSL.TypeReference type) { + private void renameIdentifier(CompoundTag nbt,DSL.TypeReference type) { if(!nbt.contains("id",8))return; - CompoundNBT id=new CompoundNBT();id.putString("id",nbt.getString("id")); - CompoundNBT fixed=cyano.lootable.upgrade.NativeNbt.update(delegate,type,id,703,705); + CompoundTag id=new CompoundTag();id.putString("id",nbt.getString("id")); + CompoundTag fixed=cyano.lootable.upgrade.NativeNbt.update(delegate,type,id,703,705); if(!fixed.contains("id",8))throw new IllegalStateException("Vanilla identifier conversion lost its type"); nbt.putString("id",fixed.getString("id")); } - private void bridgeField(CompoundNBT parent,String key,String kind) { + private void bridgeField(CompoundTag parent,String key,String kind) { if(parent.contains(key,10))bridgeIdentifiers(parent.getCompound(key),kind); } - private void bridgeList(CompoundNBT parent,String key,String kind) { - for(INBT entry:parent.getList(key,10))bridgeIdentifiers((CompoundNBT)entry,kind); + private void bridgeList(CompoundTag parent,String key,String kind) { + for(Tag entry:parent.getList(key,10))bridgeIdentifiers((CompoundTag)entry,kind); } - private void bridgeSpawner(CompoundNBT nbt) { + private void bridgeSpawner(CompoundTag nbt) { bridgeField(nbt,"SpawnData","entity_tree"); - for(INBT entry:nbt.getList("SpawnPotentials",10))bridgeField((CompoundNBT)entry,"Entity","entity_tree"); + for(Tag entry:nbt.getList("SpawnPotentials",10))bridgeField((CompoundTag)entry,"Entity","entity_tree"); } - private static boolean nonempty(CompoundNBT nbt,String key){return nbt.contains(key,9) && !nbt.getList(key,10).isEmpty();} - private void shieldList(ListNBT entities,int source,Tokens tokens,int x,int z,boolean saved) { + private static boolean nonempty(CompoundTag nbt,String key){return nbt.contains(key,9) && !nbt.getList(key,10).isEmpty();} + private void shieldList(ListTag entities,int source,Tokens tokens,int x,int z,boolean saved) { for(int i=0;iUpgradePreservation.DATA_VERSION)throw new IllegalStateException("This corpse was saved by a newer Lootable Bodies format"); - CompoundNBT safe=raw.copy();boolean current=raw.getString("id").equals("lootablebodies:corpse") && raw.contains(UpgradePreservation.VERSION,3) && raw.getInt(UpgradePreservation.VERSION)==UpgradePreservation.DATA_VERSION; + CompoundTag safe=raw.copy();boolean current=raw.getString("id").equals("lootablebodies:corpse") && raw.contains(UpgradePreservation.VERSION,3) && raw.getInt(UpgradePreservation.VERSION)==UpgradePreservation.DATA_VERSION; if(CorpseIdentity.present(raw)) { - safe.putUniqueId("UUID",CorpseIdentity.read(raw));safe.remove("UUIDMost");safe.remove("UUIDLeast"); + safe.putUUID("UUID",CorpseIdentity.read(raw));safe.remove("UUIDMost");safe.remove("UUIDLeast"); } safe.putString("id","lootablebodies:corpse");safe.putInt(UpgradePreservation.VERSION,UpgradePreservation.DATA_VERSION); if(current)return safe; safe.put(UpgradePreservation.SOURCES,context.copy()); - ListNBT records=new ListNBT();boolean custom=list(raw.get("Equipment")); + ListTag records=new ListTag();boolean custom=list(raw.get("Equipment")); retainInventory(records,"Equipment",raw.get("Equipment"),context); retainInventory(records,"Aux",raw.get("Aux"),context); if(!custom) { retainMirror(records,raw.getList("ArmorItems",10),new int[]{3,2,1,0},context); retainMirror(records,raw.getList("HandItems",10),new int[]{4,5},context); } - INBT recovery=raw.get("LootableBodiesUnresolved"); - if(list(recovery))for(INBT value:(ListNBT)recovery) { - CompoundNBT record=((CompoundNBT)value).copy(); + Tag recovery=raw.get("LootableBodiesUnresolved"); + if(list(recovery))for(Tag value:(ListTag)recovery) { + CompoundTag record=((CompoundTag)value).copy(); if(record.contains("Source",8) && record.contains("Data",10) && !record.contains("SourceVersions") && (record.getString("Source").equals("Equipment") || record.getString("Source").equals("Aux")))record.put("SourceVersions",context.copy()); records.add(record); }else if(recovery!=null)retain(records,"Recovery",recovery,context); - safe.put("Equipment",new ListNBT());safe.remove("Aux");safe.remove("ArmorItems");safe.remove("HandItems"); + safe.put("Equipment",new ListTag());safe.remove("Aux");safe.remove("ArmorItems");safe.remove("HandItems"); if(!records.isEmpty())safe.put("LootableBodiesUnresolved",records);else safe.remove("LootableBodiesUnresolved"); // These are durable existing recovery records, not a transient envelope: // 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",ITextComponent.Serializer.toJson(new StringTextComponent(safe.getString("CustomName")))); + safe.putString("CustomName",Component.Serializer.toJson(new TextComponent(safe.getString("CustomName")))); return safe; } - private static boolean list(INBT nbt){return nbt instanceof ListNBT && (((ListNBT)nbt).isEmpty() || ((ListNBT)nbt).getTagType()==10);} - private static void retainInventory(ListNBT records,String source,INBT data,CompoundNBT context) { + private static boolean list(Tag nbt){return nbt instanceof ListTag && (((ListTag)nbt).isEmpty() || ((ListTag)nbt).getElementType()==10);} + private static void retainInventory(ListTag records,String source,Tag data,CompoundTag context) { if(data==null)return; - if(list(data))for(INBT entry:(ListNBT)data)retain(records,source,entry,context); + if(list(data))for(Tag entry:(ListTag)data)retain(records,source,entry,context); else retain(records,source+"List",data,context); } - private static void retainMirror(ListNBT records,ListNBT items,int[] slots,CompoundNBT context) { + private static void retainMirror(ListTag records,ListTag items,int[] slots,CompoundTag context) { for(int i=0;i(item.keySet()))protectEggs(item.get(key),source,tokens); - }else if(value instanceof ListNBT)for(INBT entry:(ListNBT)value)protectEggs(entry,source,tokens); + for(String key:new ArrayList<>(item.getAllKeys()))protectEggs(item.get(key),source,tokens); + }else if(value instanceof ListTag)for(Tag entry:(ListTag)value)protectEggs(entry,source,tokens); } - private INBT restore(INBT value,Tokens tokens) { - if(value instanceof CompoundNBT) { - CompoundNBT nbt=(CompoundNBT)value; + private Tag restore(Tag value,Tokens tokens) { + if(value instanceof CompoundTag) { + CompoundTag nbt=(CompoundTag)value; if(nbt.contains(tokens.key,8)) { String id=nbt.getString(tokens.key);Protected original=tokens.values.get(id);if(original==null)throw new IllegalStateException("Unknown preservation token"); tokens.restored.merge(id,1,Integer::sum); if(!original.egg)return original.data.copy(); - nbt.remove(tokens.key);CompoundNBT tag=nbt.getCompound("tag");tag.put("EntityTag",original.data.copy());nbt.put("tag",tag); + nbt.remove(tokens.key);CompoundTag tag=nbt.getCompound("tag");tag.put("EntityTag",original.data.copy());nbt.put("tag",tag); } - for(String key:new ArrayList<>(nbt.keySet()))nbt.put(key,restore(nbt.get(key),tokens));return nbt; + for(String key:new ArrayList<>(nbt.getAllKeys()))nbt.put(key,restore(nbt.get(key),tokens));return nbt; } - if(value instanceof ListNBT){ListNBT list=(ListNBT)value;for(int i=0;i values=new HashMap<>();final Map restored=new HashMap<>(); - String put(CompoundNBT data,boolean egg){String id=Integer.toString(values.size());values.put(id,new Protected(data,egg));return id;} + String put(CompoundTag data,boolean egg){String id=Integer.toString(values.size());values.put(id,new Protected(data,egg));return id;} } - private static boolean sections(CompoundNBT root,String key,int type) { - ListNBT sections=root.getCompound("Level").getList("Sections",10);for(int i=0;i=512 && version<=UpgradePreservation.MINECRAFT_VERSION && (!legacy || version<1451) && (!palette || version>=1451); } - private static int chunkVersion(CompoundNBT chunk,int requested) { + private static int chunkVersion(CompoundTag chunk,int requested) { boolean legacy=sections(chunk,"Blocks",7),palette=sections(chunk,"Palette",9); if(legacy && palette)throw new IllegalStateException("Mixed old and flattened sections cannot be converted safely"); int source=chunk.contains("DataVersion",3)?chunk.getInt("DataVersion"):requested; - UpgradeJournal journal=UpgradePreservation.journal();CompoundNBT level=chunk.getCompound("Level"); + UpgradeJournal journal=UpgradePreservation.journal();CompoundTag level=chunk.getCompound("Level"); int preserved=-1; for(UpgradeJournal.LocatedCorpse located:UpgradeJournal.corpses(level.getList("Entities",10))) { try { - CompoundNBT original=journal==null || !CorpseIdentity.present(located.entity)?null:journal.originalFor(CorpseIdentity.read(located.entity),level.getInt("xPos"),level.getInt("zPos")); + CompoundTag original=journal==null || !CorpseIdentity.present(located.entity)?null:journal.originalFor(CorpseIdentity.read(located.entity),level.getInt("xPos"),level.getInt("zPos")); if(original!=null) { - CompoundNBT context=original.getCompound("SourceVersions");int candidate=UpgradePreservation.chunkSourceVersion(context); + CompoundTag context=original.getCompound("SourceVersions");int candidate=UpgradePreservation.chunkSourceVersion(context); if(!context.getBoolean("Ambiguous") && compatible(candidate,legacy,palette)) { if(preserved>=0 && preserved!=candidate)throw new IllegalStateException("Conflicting preserved chunk versions"); preserved=candidate; @@ -283,7 +328,7 @@ private static int chunkVersion(CompoundNBT chunk,int requested) { // the matching section schema so a saved native chunk is never flattened twice. if(preserved>=0)source=preserved; else if(journal!=null) { - CompoundNBT world=journal.worldVersions();int candidate=UpgradePreservation.chunkSourceVersion(world); + CompoundTag world=journal.worldVersions();int candidate=UpgradePreservation.chunkSourceVersion(world); if(!compatible(candidate,legacy,palette))candidate=world.getInt("WorldDataVersion"); if(!compatible(candidate,legacy,palette) && world.getCompound("WorldVersion").contains("Id",3))candidate=world.getCompound("WorldVersion").getInt("Id"); if(compatible(candidate,legacy,palette) && (!compatible(source,legacy,palette) || candidate2)throw new IllegalArgumentException("Invalid position axis"); + return Double.isFinite(value) && Math.abs(value)<=(axis==1?2.0E7D:3.0E7D); + } + static void prepare(ListTag roots,int chunkX,int chunkZ) { + for(Tag root:roots)visit((CompoundTag)root,chunkX,chunkZ,true,0); + } + private static void visit(CompoundTag entity,int x,int z,boolean root,int depth) { + if(depth>512)throw new IllegalStateException("Saved corpse passenger tree is too deep"); + if(UpgradeJournal.corpse(entity))repair(entity,x,z,root); + for(Tag rider:entity.getList("Passengers",10))visit((CompoundTag)rider,x,z,false,depth+1); + } + private static void repair(CompoundTag entity,int x,int z,boolean root) { + Tag raw=entity.get("Pos");ListTag list=raw instanceof ListTag?(ListTag)raw:null; + boolean changed=list==null || list.size()!=3; + double[] position=new double[3]; + for(int axis=0;axis<3;axis++) { + Tag value=list!=null && axis active=new HashMap<>(); - private CompoundNBT worldContext=new CompoundNBT(); + private CompoundTag worldContext=new CompoundTag(); private UpgradeJournal(Path world) throws IOException { this.world=world.toRealPath(); @@ -33,7 +40,7 @@ public static UpgradeJournal scan(Path world) throws IOException { UpgradeJournal journal=new UpgradeJournal(world); journal.inspect(); for(Path original:journal.active.values()) { - CompoundNBT corpse=journal.readSnapshot(original).getCompound("Entity"); + CompoundTag corpse=journal.readSnapshot(original).getCompound("Entity"); if(corpse.contains(UpgradePreservation.VERSION,3) && corpse.getInt(UpgradePreservation.VERSION)>UpgradePreservation.DATA_VERSION) throw new IOException("This world contains a newer Lootable Bodies corpse format; originals have been preserved"); } @@ -41,39 +48,68 @@ public static UpgradeJournal scan(Path world) throws IOException { } public int size() { return active.size(); } - CompoundNBT worldVersions(){return worldContext.copy();} + CompoundTag worldVersions(){return worldContext.copy();} + + /** Validate immutable copies before native async entity reads can become PENDING. */ + void validateEntityChunks(com.mojang.datafixers.DataFixer fixer) throws IOException { + Set checked=new HashSet<>(); + List originals=new ArrayList<>(active.values());originals.sort(Comparator.comparing(Path::toString)); + for(Path original:originals) { + CompoundTag snapshot=readSnapshot(original); + Path relative=Paths.get(snapshot.getString("Region")); + if(relative.getParent()==null || !relative.getParent().getFileName().toString().equals("entities"))continue; + String key=relative.toString()+":"+snapshot.getString("OriginalChunk"); + if(!checked.add(key))continue; + Path region=world.resolve(relative).normalize(); + if(relative.isAbsolute() || !region.startsWith(world))throw new IOException("Entity region is outside the world"); + String expected=snapshot.getString("RegionDigest"); + if(!expected.equals(fileDigest(region)))throw new IOException("Entity region changed before conversion validation: "+relative); + byte[] bytes=Files.readAllBytes(chunks.resolve(snapshot.getString("OriginalChunk")+".bin")); + InputStream stream=snapshot.getByte("OriginalCompression")==1?new GZIPInputStream(new ByteArrayInputStream(bytes)): + new InflaterInputStream(new ByteArrayInputStream(bytes)); + CompoundTag root; + try(DataInputStream input=new DataInputStream(stream)){root=NbtIo.read(input,new NbtAccounter(MAX_NBT));} + try { + NativeNbt.update(fixer,net.minecraft.util.datafix.fixes.References.ENTITY_CHUNK,root, + root.contains("DataVersion",3)?root.getInt("DataVersion"):-1,UpgradePreservation.MINECRAFT_VERSION); + } catch(RuntimeException unsafe) { + throw new IOException("Cannot safely convert preserved entity chunk before world loading: "+relative,unsafe); + } + if(!expected.equals(fileDigest(region)))throw new IOException("Entity region changed during conversion validation: "+relative); + } + } /** Only an entity already present in this chunk may consume its original. */ - public CompoundNBT originalFor(UUID id,int chunkX,int chunkZ) throws IOException { + public CompoundTag originalFor(UUID id,int chunkX,int chunkZ) throws IOException { Path path=active.get(id); if(path==null) return null; - CompoundNBT original=readSnapshot(path); + CompoundTag original=readSnapshot(path); return original.getInt("ChunkX")==chunkX && original.getInt("ChunkZ")==chunkZ ? original : null; } - CompoundNBT originalFor(UUID id) throws IOException { + CompoundTag originalFor(UUID id) throws IOException { Path path=active.get(id); return path==null ? null : readSnapshot(path); } - static boolean corpse(CompoundNBT entity) { + static boolean corpse(CompoundTag entity) { String id=entity.getString("id"); return id.equals("lootablebodies:corpse") || id.equals("lootablebodies.Corpse"); } static final class LocatedCorpse { - final CompoundNBT entity;final int root;final int[] passengers; - LocatedCorpse(CompoundNBT entity,int root,int[] passengers){this.entity=entity;this.root=root;this.passengers=passengers;} + final CompoundTag entity;final int root;final int[] passengers; + LocatedCorpse(CompoundTag entity,int root,int[] passengers){this.entity=entity;this.root=root;this.passengers=passengers;} } - static List corpses(ListNBT roots) { + static List corpses(ListTag roots) { List result=new ArrayList<>(); for(int root=0;root result) { + private static void collect(CompoundTag entity,int root,int[] path,List result) { if(path.length>512)throw new IllegalArgumentException("Corpse passenger tree is too deep"); if(corpse(entity))result.add(new LocatedCorpse(entity,root,path)); - ListNBT passengers=entity.getList("Passengers",10); + ListTag passengers=entity.getList("Passengers",10); for(int i=0;i it=paths.iterator();it.hasNext();) { Path path=it.next(); if(Files.isSymbolicLink(path)) throw new IOException("Cannot preserve a world containing linked paths: "+world.relativize(path)); - if(Files.isRegularFile(path) && path.getParent().getFileName().toString().equals("region") + if(Files.isRegularFile(path) && (path.getParent().getFileName().toString().equals("region") + || path.getParent().getFileName().toString().equals("entities")) && REGION.matcher(path.getFileName().toString()).matches()) regions.add(path); } } @@ -99,23 +136,23 @@ private void inspect() throws IOException { try(DirectoryStream files=Files.newDirectoryStream(snapshots,"*.nbt")) { for(Path path:files) readSnapshot(path); } - Map previous=readIndex(); - CompoundNBT worldVersions=readWorldVersions();worldContext=worldVersions.copy(); - ListNBT updated=new ListNBT(); + Map previous=readIndex(); + CompoundTag worldVersions=readWorldVersions();worldContext=worldVersions.copy(); + ListTag updated=new ListTag(); for(Path region:regions) { String relative=world.relativize(region).toString().replace('\\','/'); String hash=fileDigest(region); - CompoundNBT record=previous.get(relative); + CompoundTag record=previous.get(relative); if(record==null || !hash.equals(record.getString("Digest"))) record=scanRegion(region,relative,hash,worldVersions,record); else record=record.copy(); - ListNBT originals=record.getList("Snapshots",8); + ListTag originals=record.getList("Snapshots",8); for(int i=0;i readIndex() throws IOException { - Map records=new HashMap<>(); + private Map readIndex() throws IOException { + Map records=new HashMap<>(); Path index=directory.resolve("index.nbt"); if(!Files.exists(index)) return records; - CompoundNBT envelope=readCompressed(index); + CompoundTag envelope=readCompressed(index); byte[] payload=envelope.getByteArray("Payload"); if(!envelope.contains("Format",3) || envelope.getInt("Format")!=FORMAT || !envelope.contains("Digest",8) || !envelope.contains("Payload",7) || !digest(payload).equals(envelope.getString("Digest"))) throw new IOException("Lootable Bodies upgrade index checksum mismatch"); - CompoundNBT nbt; + CompoundTag nbt; try(DataInputStream input=new DataInputStream(new GZIPInputStream(new ByteArrayInputStream(payload)))) { - nbt=CompressedStreamTools.read(input,new NBTSizeTracker(MAX_NBT)); + nbt=NbtIo.read(input,new NbtAccounter(MAX_NBT)); } if(!nbt.contains("Format",3) || nbt.getInt("Format")!=FORMAT || !compoundList(nbt.get("Regions"))) throw new IOException("Invalid Lootable Bodies upgrade index"); - ListNBT regions=nbt.getList("Regions",10); + ListTag regions=nbt.getList("Regions",10); for(int i=0;i0 && list.getTagType()!=8) throw new IOException("Invalid recovery snapshot list"); + || !(record.get("Snapshots") instanceof ListTag)) throw new IOException("Invalid recovery region record"); + ListTag list=(ListTag)record.get("Snapshots"); + if(list.size()>0 && list.getElementType()!=8) throw new IOException("Invalid recovery snapshot list"); if(records.put(record.getString("Path"),record)!=null) throw new IOException("Duplicate recovery region record"); for(int j=0;jInteger.MAX_VALUE/32 || rzInteger.MAX_VALUE/32) throw new IOException("Region coordinates out of range"); - ListNBT preserved=new ListNBT(); - Map prior=new HashMap<>(); - Map priorLevels=new HashMap<>(); + ListTag preserved=new ListTag(); + Map prior=new HashMap<>(); + Map priorLevels=new HashMap<>(); Map matchingChunks=new HashMap<>(); if(previous!=null) { - ListNBT originals=previous.getList("Snapshots",8); + ListTag originals=previous.getList("Snapshots",8); for(int i=0;iUpgradePreservation.chunkSourceVersion(selected.getCompound("SourceVersions")))prior.put(key,original); } } @@ -198,21 +236,31 @@ private CompoundNBT scanRegion(Path file,String relative,String hash,CompoundNBT InputStream stream=compression==1 ? new GZIPInputStream(new ByteArrayInputStream(payload)) : compression==2 ? new InflaterInputStream(new ByteArrayInputStream(payload)) : null; if(stream==null) throw new IOException("Unknown chunk compression: "+relative); - CompoundNBT chunk; - try(DataInputStream nbt=new DataInputStream(stream)) { chunk=CompressedStreamTools.read(nbt,new NBTSizeTracker(MAX_NBT)); } + CompoundTag chunk; + try(DataInputStream nbt=new DataInputStream(stream)) { chunk=NbtIo.read(nbt,new NbtAccounter(MAX_NBT)); } catch(RuntimeException damaged) { throw new IOException("Unreadable chunk in "+relative,damaged); } - if(!chunk.contains("Level",10)) throw new IOException("Missing chunk Level: "+relative); - CompoundNBT level=chunk.getCompound("Level");int x=(int)rx*32+(i%32),z=(int)rz*32+(i/32); - if(!level.contains("xPos",3) || !level.contains("zPos",3) || level.getInt("xPos")!=x || level.getInt("zPos")!=z) - throw new IOException("Chunk coordinate mismatch in "+relative); + int x=(int)rx*32+(i%32),z=(int)rz*32+(i/32); + CompoundTag level; + if(entityRegion) { + int[] position=chunk.getIntArray("Position"); + if(!chunk.contains("Position",11) || position.length!=2 || position[0]!=x || position[1]!=z) + throw new IOException("Entity chunk Position mismatch in "+relative); + level=chunk; + } else { + if(!chunk.contains("Level",10))throw new IOException("Missing chunk Level: "+relative); + level=chunk.getCompound("Level"); + if(!level.contains("xPos",3) || !level.contains("zPos",3) || level.getInt("xPos")!=x || level.getInt("zPos")!=z) + throw new IOException("Chunk coordinate mismatch in "+relative); + } + if(entityRegion && !level.contains("Entities"))throw new IOException("Missing native entity list in "+relative); if(!level.contains("Entities")) continue; if(!compoundList(level.get("Entities"))) throw new IOException("Unreadable entity list in "+relative); - ListNBT entities=level.getList("Entities",10); + ListTag entities=level.getList("Entities",10); String originalChunk=digest(payload); for(LocatedCorpse located:corpses(entities)) { - CompoundNBT entity=located.entity;int e=located.root; + CompoundTag entity=located.entity;int e=located.root; uuid(entity); // An ambiguous identity cannot safely select a backup. - CompoundNBT snapshot=new CompoundNBT();snapshot.putInt("Format",FORMAT);snapshot.putString("Region",relative);snapshot.putString("RegionDigest",hash); + CompoundTag snapshot=new CompoundTag();snapshot.putInt("Format",FORMAT);snapshot.putString("Region",relative);snapshot.putString("RegionDigest",hash); Path chunkFile=chunks.resolve(originalChunk+".bin"); if(!Files.exists(chunkFile)) atomic(chunkFile,payload,false); else if(!Arrays.equals(payload,Files.readAllBytes(chunkFile))) throw new IOException("Original chunk checksum collision"); @@ -220,20 +268,20 @@ private CompoundNBT scanRegion(Path file,String relative,String hash,CompoundNBT if(located.passengers.length>0)snapshot.putIntArray("PassengerPath",located.passengers); snapshot.putString("Dimension",world.relativize(file.getParent().getParent()).toString().replace('\\','/')); snapshot.putInt("ChunkX",x);snapshot.putInt("ChunkZ",z);snapshot.put("Entity",entity.copy()); - CompoundNBT versions=sourceVersions(chunk,worldVersions,entity),priorSnapshot=prior.get(chunkKey(x,z)); + CompoundTag versions=sourceVersions(chunk,worldVersions,entity),priorSnapshot=prior.get(chunkKey(x,z)); if(priorSnapshot!=null) { String savedChunk=priorSnapshot.getString("OriginalChunk"); - CompoundNBT oldLevel=priorLevels.get(savedChunk); + CompoundTag oldLevel=priorLevels.get(savedChunk); if(oldLevel==null) { byte[] originalBytes=Files.readAllBytes(chunks.resolve(savedChunk+".bin")); InputStream originalStream=priorSnapshot.getByte("OriginalCompression")==1?new GZIPInputStream(new ByteArrayInputStream(originalBytes)):new InflaterInputStream(new ByteArrayInputStream(originalBytes)); - try(DataInputStream originalInput=new DataInputStream(originalStream)){oldLevel=CompressedStreamTools.read(originalInput,new NBTSizeTracker(MAX_NBT)).getCompound("Level");} + try(DataInputStream originalInput=new DataInputStream(originalStream)){oldLevel=comparisonPayload(NbtIo.read(originalInput,new NbtAccounter(MAX_NBT)),entityRegion);} priorLevels.put(savedChunk,oldLevel); } - // Only metadata outside Level may differ. Never let an older - // journal replace changed terrain, inventory or live entities. + // Only the outer version stamp may differ for entity chunks. + // Never substitute old entities, Position or unknown payloads. long key=chunkKey(x,z);Boolean same=matchingChunks.get(key); - if(same==null){same=sameNbt(oldLevel,level);matchingChunks.put(key,same);} + if(same==null){same=sameNbt(oldLevel,comparisonPayload(chunk,entityRegion));matchingChunks.put(key,same);} if(same)versions.putInt("VerifiedChunkDataVersion",UpgradePreservation.chunkSourceVersion(priorSnapshot.getCompound("SourceVersions"))); } snapshot.put("SourceVersions",versions); @@ -242,21 +290,25 @@ private CompoundNBT scanRegion(Path file,String relative,String hash,CompoundNBT else atomic(destination,original,false); if(entity.contains("LootableBodiesUpgradePayload") || entity.contains("LootableBodiesInventorySource")) throw new IOException("Unexpected intermediate corpse migration data on disk; original chunk has been preserved"); - preserved.add(StringNBT.valueOf(filename)); + preserved.add(StringTag.valueOf(filename)); } } } catch(RuntimeException damaged) { throw new IOException("Cannot safely preserve "+relative,damaged); } return regionRecord(relative,hash,preserved); } - private static CompoundNBT regionRecord(String relative,String hash,ListNBT preserved) { - CompoundNBT record=new CompoundNBT();record.putString("Path",relative);record.putString("Digest",hash);record.put("Snapshots",preserved);return record; + private static CompoundTag regionRecord(String relative,String hash,ListTag preserved) { + CompoundTag record=new CompoundTag();record.putString("Path",relative);record.putString("Digest",hash);record.put("Snapshots",preserved);return record; + } + private static CompoundTag comparisonPayload(CompoundTag chunk,boolean entityRegion) { + if(!entityRegion)return chunk.getCompound("Level"); + CompoundTag payload=chunk.copy();payload.remove("DataVersion");return payload; } - private CompoundNBT readWorldVersions() throws IOException { - CompoundNBT context=new CompoundNBT(); + private CompoundTag readWorldVersions() throws IOException { + CompoundTag context=new CompoundTag(); Path level=world.resolve("level.dat"); if(Files.isRegularFile(level)) { - CompoundNBT root=readCompressed(level),data=root.getCompound("Data"); + CompoundTag root=readCompressed(level),data=root.getCompound("Data"); if(data.contains("DataVersion")) context.put("WorldDataVersion",data.get("DataVersion").copy()); if(data.contains("Version"))context.put("WorldVersion",data.get("Version").copy()); if(root.contains("FML")) context.put("WorldFML",root.get("FML").copy()); @@ -267,8 +319,8 @@ private CompoundNBT readWorldVersions() throws IOException { return context; } - private static CompoundNBT sourceVersions(CompoundNBT chunk,CompoundNBT world,CompoundNBT entity) { - CompoundNBT context=world.copy(); + private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,CompoundTag entity) { + CompoundTag context=world.copy(); if(chunk.contains("DataVersion")) context.put("ChunkDataVersion",chunk.get("DataVersion").copy()); if(chunk.contains("ForgeDataVersion")) context.put("ForgeDataVersion",chunk.get("ForgeDataVersion").copy()); int version=chunk.contains("DataVersion",3) ? chunk.getInt("DataVersion") : -1; @@ -283,15 +335,17 @@ private static CompoundNBT sourceVersions(CompoundNBT chunk,CompoundNBT world,Co else if(marker==4)version=1976; else if(marker==5)version=2230; else if(marker==6)version=2586; + else if(marker==7)version=2730; if(legacy) {if(version!=512)context.putBoolean("Inferred",true);version=512;} if(legacy || (marker>=1 && marker<=UpgradePreservation.DATA_VERSION)) context.putInt("CorpseWriterDataVersion",version); int family=-1; - ListNBT mods=UpgradePreservation.modList(world.getCompound("WorldFML")); + ListTag mods=UpgradePreservation.modList(world.getCompound("WorldFML")); for(int i=0;i32)throw new IOException("Recursive native converter during entity preflight"); + delegate=((NativeSafeFixer)delegate).delegate; + } + complete.validateEntityChunks(new NativeSafeFixer(delegate,null,complete)); + } public static synchronized void close(){journal=null;earlyWorld=null;} static UpgradeJournal journal(){return journal;} public static DataFixer wrapper(DataFixer delegate){return delegate instanceof NativeSafeFixer?delegate:new NativeSafeFixer(delegate,null);} - public static CompoundNBT chunkContext(CompoundNBT chunk){return chunk;} - public static void clearChunkContext(CompoundNBT chunk){chunk.remove("LootableBodiesChunkContext");} - public static CompoundNBT restore(CompoundNBT input) { + public static CompoundTag chunkContext(CompoundTag chunk){return chunk;} + public static void clearChunkContext(CompoundTag chunk){chunk.remove("LootableBodiesChunkContext");} + public static CompoundTag restore(CompoundTag input) { if(input.contains(PAYLOAD,10))return input.getCompound(PAYLOAD).copy();return input; } - public static void clearTransient(CompoundNBT output){output.remove(PAYLOAD);output.remove(ITEMS_PENDING);} - static int chunkSourceVersion(CompoundNBT versions) { + public static void clearTransient(CompoundTag output){output.remove(PAYLOAD);output.remove(ITEMS_PENDING);} + static int chunkSourceVersion(CompoundTag versions) { if(versions.contains("VerifiedChunkDataVersion",3))return versions.getInt("VerifiedChunkDataVersion"); int version=versions.getInt("DataVersion"),family=-1; - ListNBT mods=modList(versions.getCompound("WorldFML")); + ListTag mods=modList(versions.getCompound("WorldFML")); for(int m=0;m event) {event.getRegistry().register(keepsake);} + } + private static final class Stored implements ICapabilitySerializable { + private CompoundNBT data=new CompoundNBT(); + Stored(){data.putString("owner","Disposable fixture");data.putLong("number",9876543210123L);data.putByteArray("opaque",new byte[]{0,1,-1,17});} + public boolean hasCapability(Capability capability,Direction side){return false;} + public net.minecraftforge.common.util.LazyOptional getCapability(Capability capability,Direction side){return net.minecraftforge.common.util.LazyOptional.empty();} + public CompoundNBT serializeNBT(){return data.copy();} + public void deserializeNBT(CompoundNBT nbt){data=nbt.copy();} + } +} diff --git a/src/nativeSeed116/java/cyano/lootable/fixture/NativeWorldSeeder116.java b/src/nativeSeed116/java/cyano/lootable/fixture/NativeWorldSeeder116.java new file mode 100644 index 0000000..e6f13de --- /dev/null +++ b/src/nativeSeed116/java/cyano/lootable/fixture/NativeWorldSeeder116.java @@ -0,0 +1,180 @@ +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.entity.item.ArmorStandEntity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.item.minecart.ChestMinecartEntity; +import net.minecraft.entity.passive.PigEntity; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.block.Blocks; +import net.minecraft.item.Items; +import net.minecraft.enchantment.Enchantments; +import net.minecraft.inventory.EquipmentSlotType; +import net.minecraft.inventory.IInventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.*; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.management.PlayerInteractionManager; +import net.minecraft.tileentity.ChestTileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.server.ServerWorld; +import net.minecraftforge.fml.common.*; +import net.minecraftforge.fml.event.server.*; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +/** Uses the installed historical mod's own writer; never part of a release jar. */ +@Mod("lootablebodiesbuildtest") +public final class NativeWorldSeeder116 { + private static final UUID PLAYER=UUID.fromString("00000000-2222-4222-8222-000000000001"); + private static boolean oldMinecraft,native112; + public NativeWorldSeeder116(){FixtureItems.register(net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus());net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);} + @SubscribeEvent public void started(FMLServerStartedEvent event) { + MinecraftServer server=net.minecraftforge.fml.server.ServerLifecycleHooks.getCurrentServer(); + try {seed(server);} catch(Throwable error) {error.printStackTrace();} finally {server.initiateShutdown(false);} + } + private static void seed(MinecraftServer server) throws Exception { + String minecraft=server.getMinecraftVersion(),version=net.minecraftforge.fml.ModList.get().getModContainerById("lootablebodies").get().getModInfo().getVersion().toString(); + oldMinecraft=minecraft.equals("1.10.2");native112=minecraft.equals("1.12.2"); + if(!minecraft.equals("1.16.5") || !version.equals("2.4.7.116051")) throw new AssertionError("Wrong fixture Minecraft "+minecraft); + if(!version.equals(System.getProperty("lootablebodies.fixture.version"))) throw new AssertionError("Wrong historical mod "+version); + LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1; + CompoundNBT manifest=new CompoundNBT();manifest.putString("Minecraft",minecraft);manifest.putString("ModVersion",version); + ListNBT bodies=new ListNBT(),containers=new ListNBT(),vanilla=new ListNBT(),biomes=new ListNBT(); + for(int dimension:new int[]{0,-1,1}) { + ServerWorld world=server.getWorld(dimensionKey(dimension)); + for(int wave=0;wave<3;wave++) { + int coordinate=wave==0?128:wave==1?4096:8192; + awaitChunk(world,coordinate>>4,coordinate>>4); + net.minecraft.world.biome.Biome[] sourceBiomes=biomes(world.getChunk(coordinate>>4,coordinate>>4).getBiomes()); + net.minecraft.util.registry.Registry registry=world.func_241828_r().getRegistry(net.minecraft.util.registry.Registry.BIOME_KEY); + net.minecraft.world.biome.Biome[] choices={registry.getOrThrow(net.minecraft.world.biome.Biomes.PLAINS),registry.getOrThrow(net.minecraft.world.biome.Biomes.DESERT),registry.getOrThrow(net.minecraft.world.biome.Biomes.FOREST),registry.getOrThrow(net.minecraft.world.biome.Biomes.TAIGA)}; + int[] savedBiomes=new int[sourceBiomes.length]; + for(int b=0;b>4);biomeRecord.putInt("ChunkZ",coordinate>>4);biomeRecord.putIntArray("Data",savedBiomes);biomes.add(biomeRecord); + EntityLootableBody body=new EntityLootableBody(world); + body.setUniqueId(UUID.nameUUIDFromBytes((minecraft+"/"+version+"/"+dimension+"/"+wave).getBytes("UTF-8"))); + body.setPosition(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.setInventorySlotContents(slot,item(slot)); + for(int slot=70;slot<73;slot++)body.addItem(item(slot)); + if(Boolean.getBoolean("lootablebodies.fixture.recovery")) { + if(!version.equals("2.4.1.110021") && !version.equals("2.4.2.111021") && !version.equals("2.4.3.112021") && !version.equals("2.4.7.116051")) throw new AssertionError("Recovery fixtures require a checksum-pinned maintained writer"); + CompoundNBT damaged=new CompoundNBT();body.writeUnlessPassenger(damaged); + ListNBT equipment=damaged.getList("Equipment",10); + equipment.getCompound(20).putString("id","lootablebodiesbuildtest:returning_item"); + CompoundNBT duplicate=equipment.getCompound(21).copy();duplicate.putByte("Count",(byte)9);duplicate.getCompound("tag").putString("extension","duplicate slot");equipment.add(duplicate); + CompoundNBT 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.read(damaged);body.setPosition(coordinate+.5,80,coordinate+.5); + } + java.lang.reflect.Field vacuum=EntityLootableBody.class.getDeclaredField("vacuumTime");vacuum.setAccessible(true);vacuum.setByte(body,(byte)20); + if(!world.addEntity(body))throw new AssertionError("Historical fixture corpse spawn"); + CompoundNBT record=new CompoundNBT(),payload=new CompoundNBT(); + if(!body.writeUnlessPassenger(payload))throw new AssertionError("Historical corpse writer"); + record.putInt("Dimension",dimension);record.putInt("Wave",wave);record.put("Body",payload);bodies.add(record); + BlockPos chestPos=new BlockPos(coordinate+2,80,coordinate+2);world.setBlockState(chestPos,Blocks.CHEST.getDefaultState()); + ChestTileEntity chest=(ChestTileEntity)world.getTileEntity(chestPos);for(int slot=0;slot<16;slot++)chest.setInventorySlotContents(slot,item(slot+6)); + CompoundNBT chestRecord=new CompoundNBT(),chestNbt=new CompoundNBT(); + chest.write(chestNbt); + chestRecord.putInt("Dimension",dimension);chestRecord.putInt("Wave",wave);chestRecord.put("Tile",chestNbt);containers.add(chestRecord); + if(wave==0) { + for(int profession=0;profession<6;profession++) { + net.minecraft.entity.merchant.villager.VillagerEntity villager=new net.minecraft.entity.merchant.villager.VillagerEntity(EntityType.VILLAGER,world); + villager.setUniqueId(UUID.nameUUIDFromBytes((minecraft+"/villager/"+dimension+"/"+profession).getBytes("UTF-8"))); + villager.setPosition(coordinate+8+profession,82,coordinate+8);villager.setNoGravity(true);villager.setNoAI(true);villager.setVillagerData(villager.getVillagerData().withProfession(new net.minecraft.entity.merchant.villager.VillagerProfession[]{net.minecraft.entity.merchant.villager.VillagerProfession.FARMER,net.minecraft.entity.merchant.villager.VillagerProfession.LIBRARIAN,net.minecraft.entity.merchant.villager.VillagerProfession.CLERIC,net.minecraft.entity.merchant.villager.VillagerProfession.ARMORER,net.minecraft.entity.merchant.villager.VillagerProfession.BUTCHER,net.minecraft.entity.merchant.villager.VillagerProfession.NITWIT}[profession]).withLevel(3)); + CompoundNBT data=new CompoundNBT();villager.writeUnlessPassenger(data); + ListNBT inventory=new ListNBT();CompoundNBT carried=item(7).write(new CompoundNBT());inventory.add(carried);data.put("Inventory",inventory);villager.read(data); + if(!world.addEntity(villager))throw new AssertionError("Historical villager spawn"); + CompoundNBT recordVillager=new CompoundNBT(),savedVillager=new CompoundNBT();villager.writeUnlessPassenger(savedVillager); + recordVillager.putInt("Dimension",dimension);recordVillager.putString("ExpectedType","minecraft:villager");recordVillager.put("Entity",savedVillager);vanilla.add(recordVillager); + } + PigEntity pig=new PigEntity(EntityType.PIG,world);pig.setUniqueId(UUID.nameUUIDFromBytes((minecraft+"/pig/"+dimension).getBytes("UTF-8")));pig.setPosition(coordinate+4,82,coordinate+4);pig.setNoGravity(true);pig.setNoAI(true);pig.setCustomName(new net.minecraft.util.text.StringTextComponent("Saved pig"));world.addEntity(pig); + CompoundNBT p=new CompoundNBT();pig.writeUnlessPassenger(p);CompoundNBT v=new CompoundNBT();v.putInt("Dimension",dimension);v.putString("ExpectedType","minecraft:pig");v.put("Entity",p);vanilla.add(v); + ArmorStandEntity stand=new ArmorStandEntity(EntityType.ARMOR_STAND,world);stand.setUniqueId(UUID.nameUUIDFromBytes((minecraft+"/stand/"+dimension).getBytes("UTF-8")));stand.setPosition(coordinate+5,82,coordinate+5);stand.setNoGravity(true);stand.setItemStackToSlot(EquipmentSlotType.HEAD,item(0));world.addEntity(stand); + p=new CompoundNBT();stand.writeUnlessPassenger(p);v=new CompoundNBT();v.putInt("Dimension",dimension);v.putString("ExpectedType","minecraft:armor_stand");v.put("Entity",p);vanilla.add(v); + ChestMinecartEntity cart=new ChestMinecartEntity(world,coordinate+6,82,coordinate+6);cart.setUniqueId(UUID.nameUUIDFromBytes((minecraft+"/cart/"+dimension).getBytes("UTF-8")));cart.setNoGravity(true);for(int slot=0;slot<8;slot++)cart.setInventorySlotContents(slot,item(slot+6));world.addEntity(cart); + p=new CompoundNBT();cart.writeUnlessPassenger(p);v=new CompoundNBT();v.putInt("Dimension",dimension);v.putString("ExpectedType","minecraft:chest_minecart");v.put("Entity",p);vanilla.add(v); + } + } + } + ServerWorld world=server.getWorld(net.minecraft.world.World.OVERWORLD); + ServerPlayerEntity player=new ServerPlayerEntity(server,world,new GameProfile(PLAYER,"UpgradeFixture"),new PlayerInteractionManager(world));player.setPosition(136.5,82,136.5); + for(int slot=0;slot<36;slot++)player.inventory.setInventorySlotContents(slot,item(slot+6)); + for(int slot=0;slot<4;slot++)player.setItemStackToSlot(new EquipmentSlotType[]{EquipmentSlotType.HEAD,EquipmentSlotType.CHEST,EquipmentSlotType.LEGS,EquipmentSlotType.FEET}[slot],item(slot)); + player.setItemStackToSlot(EquipmentSlotType.OFFHAND,item(5));for(int slot=0;slot<27;slot++)player.getInventoryEnderChest().setInventorySlotContents(slot,item(slot+6)); + player.connection=new net.minecraft.network.play.ServerPlayNetHandler(server,new net.minecraft.network.NetworkManager(net.minecraft.network.PacketDirection.SERVERBOUND),player) { + @Override public void sendPacket(net.minecraft.network.IPacket packet){ } + }; + net.minecraft.item.crafting.IRecipe sugar=server.getRecipeManager().getRecipe(new net.minecraft.util.ResourceLocation("minecraft:sugar_from_sugar_cane")).orElseThrow(()->new AssertionError("Missing historical15sugar recipe")); + player.unlockRecipes(java.util.Collections.singleton(sugar)); + net.minecraft.advancements.Advancement advancement=server.getAdvancementManager().getAdvancement(new net.minecraft.util.ResourceLocation("minecraft:recipes/misc/sugar_from_sugar_cane")); + if(advancement==null)throw new AssertionError("Missing historical15sugar advancement"); + for(String criterion:advancement.getCriteria().keySet())player.getAdvancements().grantCriterion(advancement,criterion); + player.getAdvancements().save(); + playerData(server).savePlayerData(player); + manifest.put("BiomeSamples",biomes);manifest.putString("Recipe","minecraft:sugar_from_sugar_cane");manifest.putString("Advancement","minecraft:recipes/misc/sugar_from_sugar_cane"); + manifest.putUniqueId("Player",PLAYER);manifest.put("Bodies",bodies);manifest.put("Containers",containers);manifest.put("Vanilla",vanilla); + server.save(true,true,false); + File folder=server.func_240776_a_(net.minecraft.world.storage.FolderName.DOT).toFile();try(FileOutputStream file=new FileOutputStream(new File(folder,"upgrade-fixtures.nbt"))){CompressedStreamTools.writeCompressed(manifest,file);} + try(FileWriter marker=new FileWriter(new File(folder,"legacy-seed.pass"))){marker.write(minecraft+" "+version+"\n");} + System.out.println("LOOTABLE_BODIES_LEGACY_SEED_PASS minecraft="+minecraft+" mod="+version+" corpses=9 dimensions=3"); + } + private static net.minecraft.util.RegistryKey dimensionKey(int id) { + if(id==0)return net.minecraft.world.World.OVERWORLD; + if(id==-1)return net.minecraft.world.World.THE_NETHER; + if(id==1)return net.minecraft.world.World.THE_END; + throw new IllegalArgumentException("Unknown fixture dimension "+id); + } + private static net.minecraft.world.storage.PlayerData 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 field:owner.getDeclaredFields()) + if(field.getType()==net.minecraft.world.storage.PlayerData.class) { + if(found!=null)throw new IllegalStateException("Ambiguous native116 player data");found=field; + } + if(found==null)throw new IllegalStateException("Missing native116 player data"); + found.setAccessible(true);return (net.minecraft.world.storage.PlayerData)found.get(server); + } + private static net.minecraft.world.biome.Biome[] biomes(net.minecraft.world.biome.BiomeContainer container) throws Exception { + java.lang.reflect.Field field=null; + for(java.lang.reflect.Field candidate:net.minecraft.world.biome.BiomeContainer.class.getDeclaredFields()) + if(candidate.getType()==net.minecraft.world.biome.Biome[].class) { + if(field!=null)throw new AssertionError("Ambiguous historical biome payload");field=candidate; + } + if(field==null)throw new AssertionError("Missing historical biome payload"); + field.setAccessible(true);return (net.minecraft.world.biome.Biome[])field.get(container); + } + private static boolean minecraftLongArrays(){return true;} + private static void awaitChunk(ServerWorld world,int x,int z)throws Exception { + world.getChunkProvider().getChunk(x,z,net.minecraft.world.chunk.ChunkStatus.FULL,true); + if(!world.getChunkProvider().chunkExists(x,z))throw new AssertionError("Historical15native chunk admission failed"); + } + 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,1); + 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); + CompoundNBT tag=new CompoundNBT();tag.putString("fixture_slot","original-"+slot);tag.putLong("typed_long",1234567890123L);tag.putByteArray("typed_bytes",new byte[]{0,-1,17}); + if(slot==6){CompoundNBT pig=new CompoundNBT();pig.putString("id",oldMinecraft?"Pig":"minecraft:pig");pig.putString("CustomName",net.minecraft.util.text.ITextComponent.Serializer.toJson(new net.minecraft.util.text.StringTextComponent("Saved pig egg")));tag.put("EntityTag",pig);} + if(slot==7){CompoundNBT tile=new CompoundNBT();ListNBT items=new ListNBT();CompoundNBT nested=new ItemStack(Items.EMERALD,17).write(new CompoundNBT());nested.putByte("Slot",(byte)3);items.add(nested);tile.put("Items",items);tag.put("BlockEntityTag",tile);} + if(slot==8 && oldMinecraft){CompoundNBT banner=new CompoundNBT();banner.putInt("Base",5);tag.put("BlockEntityTag",banner);} + if(minecraftLongArrays()) { + tag.put("typed_longs",new LongArrayNBT(new long[]{Long.MIN_VALUE,slot,Long.MAX_VALUE})); + } + stack.setTag(tag);if(slot<6)stack.setDamage(slot<4?slot+2:slot==4?17:9);stack.setDisplayName(new net.minecraft.util.text.StringTextComponent("Original keepsake "+slot));if(slot==4)stack.addEnchantment(Enchantments.UNBREAKING,2);return stack; + } +} diff --git a/src/nativeSeed116/resources/META-INF/mods.toml b/src/nativeSeed116/resources/META-INF/mods.toml new file mode 100644 index 0000000..88fff75 --- /dev/null +++ b/src/nativeSeed116/resources/META-INF/mods.toml @@ -0,0 +1,13 @@ +modLoader="javafml" +loaderVersion="[36,)" +license="LGPL-2.1-only" +[[mods]] +modId="lootablebodiesbuildtest" +version="1" +displayName="Disposable historical world writer" +[[dependencies.lootablebodiesbuildtest]] +modId="lootablebodies" +mandatory=true +versionRange="[2.4.7.116051]" +ordering="AFTER" +side="BOTH" diff --git a/src/nativeSeed116/resources/pack.mcmeta b/src/nativeSeed116/resources/pack.mcmeta new file mode 100644 index 0000000..43cd02c --- /dev/null +++ b/src/nativeSeed116/resources/pack.mcmeta @@ -0,0 +1 @@ +{"pack":{"pack_format":6,"description":"Disposable historical world writer"}} diff --git a/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java b/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java index d10661c..f7c7247 100644 --- a/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java +++ b/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java @@ -1,18 +1,18 @@ package cyano.lootable.buildtest; import java.util.UUID; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.CompoundTag; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class HistoricalUuidTest { private final UUID id=UUID.fromString("13572468-abcd-4f01-9234-56789abcdef0"); - private CompoundNBT legacy(String name){CompoundNBT n=new CompoundNBT();n.putLong(name+"Most",id.getMostSignificantBits());n.putLong(name+"Least",id.getLeastSignificantBits());return n;} - @Test void readsUnmodifiedHistoricalEntityAndPlayerPairs(){for(String key:new String[]{"UUID","Player"}){CompoundNBT n=legacy(key),before=n.copy();assertEquals(id,HistoricalUuid.read(n,key));assertEquals(before,n);}} - @Test void readsNativeArraysWithoutChangingTheirBytes(){CompoundNBT n=new CompoundNBT();n.putUniqueId("UUID",id);int[] before=n.getIntArray("UUID").clone();assertEquals(id,HistoricalUuid.read(n,"UUID"));assertArrayEquals(before,n.getIntArray("UUID"));} + private CompoundTag legacy(String name){CompoundTag n=new CompoundTag();n.putLong(name+"Most",id.getMostSignificantBits());n.putLong(name+"Least",id.getLeastSignificantBits());return n;} + @Test void readsUnmodifiedHistoricalEntityAndPlayerPairs(){for(String key:new String[]{"UUID","Player"}){CompoundTag n=legacy(key),before=n.copy();assertEquals(id,HistoricalUuid.read(n,key));assertEquals(before,n);}} + @Test void readsNativeArraysWithoutChangingTheirBytes(){CompoundTag n=new CompoundTag();n.putUUID("UUID",id);int[] before=n.getIntArray("UUID").clone();assertEquals(id,HistoricalUuid.read(n,"UUID"));assertArrayEquals(before,n.getIntArray("UUID"));} @Test void malformedAndPartialSourceIdentitiesCannotBeGuessed(){ - CompoundNBT n=legacy("UUID");n.remove("UUIDLeast");assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID")); + CompoundTag n=legacy("UUID");n.remove("UUIDLeast");assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID")); n.putString("UUIDLeast","opaque");assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID")); - CompoundNBT array=new CompoundNBT();array.putIntArray("UUID",new int[]{1,2,3});assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(array,"UUID")); + CompoundTag array=new CompoundTag();array.putIntArray("UUID",new int[]{1,2,3});assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(array,"UUID")); } - @Test void conflictingLegacyAndCurrentSourcesAreRejected(){CompoundNBT n=legacy("UUID");n.putUniqueId("UUID",new UUID(0,1));assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID"));} - @Test void zeroAndSignedBitsArePreserved(){for(UUID value:new UUID[]{new UUID(0,0),new UUID(Long.MIN_VALUE,Long.MAX_VALUE)}){CompoundNBT n=new CompoundNBT();n.putUniqueId("UUID",value);assertEquals(value,HistoricalUuid.read(n,"UUID"));}} + @Test void conflictingLegacyAndCurrentSourcesAreRejected(){CompoundTag n=legacy("UUID");n.putUUID("UUID",new UUID(0,1));assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID"));} + @Test void zeroAndSignedBitsArePreserved(){for(UUID value:new UUID[]{new UUID(0,0),new UUID(Long.MIN_VALUE,Long.MAX_VALUE)}){CompoundTag n=new CompoundTag();n.putUUID("UUID",value);assertEquals(value,HistoricalUuid.read(n,"UUID"));}} } diff --git a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java index 13ab125..93e2a47 100644 --- a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java @@ -2,35 +2,35 @@ import io.netty.buffer.Unpooled; import java.lang.reflect.Field; -import net.minecraft.network.PacketBuffer; -import net.minecraft.network.play.server.SRespawnPacket; -import net.minecraft.world.GameType; -import net.minecraft.world.World; -import net.minecraft.world.DimensionType; -import net.minecraft.util.RegistryKey; -import net.minecraft.util.registry.Registry; -import net.minecraft.util.registry.DynamicRegistries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.resources.ResourceKey; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class Native115PacketTest { - static { net.minecraft.util.registry.Bootstrap.register(); } + static { net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap(); } private static final long SEED=0x13572468ABCDEF01L; - private SRespawnPacket packet(RegistryKey dimension,long seed) { - DimensionType type=DynamicRegistries.func_239770_b_().getRegistry(Registry.DIMENSION_TYPE_KEY).getValueForKey(DimensionType.OVERWORLD); - return new SRespawnPacket(type,dimension,seed,GameType.SURVIVAL,GameType.CREATIVE,false,true,true); + private ClientboundRespawnPacket packet(ResourceKey dimension,long seed) { + DimensionType type=RegistryAccess.builtin().registryOrThrow(Registry.DIMENSION_TYPE_REGISTRY).getOrThrow(DimensionType.OVERWORLD_LOCATION); + return new ClientboundRespawnPacket(type,dimension,seed,GameType.SURVIVAL,GameType.CREATIVE,false,true,true); } @Test void native115HashedSeedSurvivesDecodedPacketObservation() throws Exception { - PacketBuffer bytes=new PacketBuffer(Unpooled.buffer()); + FriendlyByteBuf bytes=new FriendlyByteBuf(Unpooled.buffer()); try { - SRespawnPacket outgoing=packet(World.THE_NETHER,SEED); - outgoing.writePacketData(bytes); + ClientboundRespawnPacket outgoing=packet(Level.NETHER,SEED); + outgoing.write(bytes); byte[] before=new byte[bytes.readableBytes()];bytes.getBytes(0,before); - SRespawnPacket incoming=new SRespawnPacket();incoming.readPacketData(bytes); + ClientboundRespawnPacket incoming=new ClientboundRespawnPacket(bytes); String expected="RESPAWN dimension=minecraft:the_nether hashedSeed="+SEED; assertEquals(expected,NativeRespawnDescription.describe(outgoing)); assertEquals(expected,NativeRespawnDescription.describe(incoming)); - for(Field field:SRespawnPacket.class.getDeclaredFields()) if(field.getType()==DimensionType.class) { + for(Field field:ClientboundRespawnPacket.class.getDeclaredFields()) if(field.getType()==DimensionType.class) { field.setAccessible(true);assertNotNull(field.get(incoming),"Native codec dimension type was lost"); } assertEquals(before.length,bytes.readerIndex()); @@ -38,23 +38,23 @@ private SRespawnPacket packet(RegistryKey dimension,long seed) { } finally {bytes.release();} } @Test void differentNative115SeedsCannotHaveTheSameDescription() { - assertNotEquals(NativeRespawnDescription.describe(packet(World.OVERWORLD,SEED)), - NativeRespawnDescription.describe(packet(World.OVERWORLD,SEED+1))); + assertNotEquals(NativeRespawnDescription.describe(packet(Level.OVERWORLD,SEED)), + NativeRespawnDescription.describe(packet(Level.OVERWORLD,SEED+1))); } @Test void allSignedSeedValuesRemainVisible() { for(long seed:new long[]{0,-1,Long.MIN_VALUE,Long.MAX_VALUE}) assertEquals("RESPAWN dimension=minecraft:overworld hashedSeed="+seed, - NativeRespawnDescription.describe(packet(World.OVERWORLD,seed))); + NativeRespawnDescription.describe(packet(Level.OVERWORLD,seed))); } @Test void nativeDimensionDistinctionIsRetained() { - assertNotEquals(NativeRespawnDescription.describe(packet(World.OVERWORLD,SEED)), - NativeRespawnDescription.describe(packet(World.THE_NETHER,SEED))); + assertNotEquals(NativeRespawnDescription.describe(packet(Level.OVERWORLD,SEED)), + NativeRespawnDescription.describe(packet(Level.NETHER,SEED))); } @Test void observationKeepsUnknownDimensionKeysAndEveryDecodedField() throws Exception { - RegistryKey key=RegistryKey.getOrCreateKey(Registry.WORLD_KEY,new net.minecraft.util.ResourceLocation("fixture:unhandled")); - SRespawnPacket packet=packet(key,SEED); + ResourceKey key=ResourceKey.create(Registry.DIMENSION_REGISTRY,new net.minecraft.resources.ResourceLocation("fixture:unhandled")); + ClientboundRespawnPacket packet=packet(key,SEED); java.util.Map before=new java.util.HashMap<>(); - for(Field field:SRespawnPacket.class.getDeclaredFields()){field.setAccessible(true);before.put(field,field.get(packet));} + for(Field field:ClientboundRespawnPacket.class.getDeclaredFields()){field.setAccessible(true);before.put(field,field.get(packet));} assertEquals("RESPAWN dimension=fixture:unhandled hashedSeed="+SEED,NativeRespawnDescription.describe(packet)); for(java.util.Map.Entry field:before.entrySet())assertEquals(field.getValue(),field.getKey().get(packet)); } diff --git a/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java b/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java new file mode 100644 index 0000000..909784b --- /dev/null +++ b/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java @@ -0,0 +1,105 @@ +package cyano.lootable.buildtest; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.entity.*; +import net.minecraft.world.phys.AABB; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** Real native manager/inbox; controlled storage futures are fixture seams. */ +class Native117EntityInboxTest { + private static final ChunkPos POS = new ChunkPos(-4, 9); + private static final class Saved implements EntityAccess { + final UUID id = UUID.fromString("00000000-1171-4171-8171-000000000088"); + public int getId() { return 88; } + public UUID getUUID() { return id; } + public BlockPos blockPosition() { return new BlockPos(-63, 80, 145); } + public AABB getBoundingBox() { return new AABB(blockPosition()); } + public void setLevelCallback(EntityInLevelCallback callback) { } + public Stream getSelfAndPassengers() { return Stream.of(this); } + public Stream getPassengersAndSelf() { return Stream.of(this); } + public void setRemoved(Entity.RemovalReason reason) { } + public boolean shouldBeSaved() { return true; } + public boolean isAlwaysTicking() { return false; } + } + private static final class Storage implements EntityPersistentStorage { + final CompletableFuture> read = new CompletableFuture<>(); + int loads, stores; + public CompletableFuture> loadEntities(ChunkPos pos) { + assertEquals(POS, pos); loads++; return read; + } + public void storeEntities(ChunkEntities data) { stores++; } + public void flush(boolean sync) { } + } + private static final class Fixture { + final Storage storage = new Storage(); + final AtomicInteger tracked = new AtomicInteger(); + final PersistentEntitySectionManager manager = new PersistentEntitySectionManager<>( + Saved.class, new LevelCallback() { + public void onCreated(Saved e) { } + public void onDestroyed(Saved e) { } + public void onTickingStart(Saved e) { } + public void onTickingEnd(Saved e) { } + public void onTrackingStart(Saved e) { tracked.incrementAndGet(); } + public void onTrackingEnd(Saved e) { } + }, storage); + Fixture() { manager.updateChunkStatus(POS, Visibility.TRACKED); } + } + @Test void completedReadRequiresActualNativeInboxAdmission() { + Fixture f = new Fixture(); Saved entity = new Saved(); + f.storage.read.complete(new ChunkEntities<>(POS, Collections.singletonList(entity))); + assertFalse(f.manager.areEntitiesLoaded(POS.toLong())); + NativeEntityInbox.await(f.manager, POS.toLong(), () -> false, () -> 0, 10); + assertTrue(f.manager.areEntitiesLoaded(POS.toLong())); + assertEquals(1, f.tracked.get()); assertEquals(1, f.storage.loads); assertEquals(0, f.storage.stores); + } + @Test void delayedReadProcessesNativeInboxAfterPollingTasks() { + Fixture f = new Fixture(); AtomicInteger polls = new AtomicInteger(); AtomicLong clock = new AtomicLong(); + NativeEntityInbox.await(f.manager, POS.toLong(), () -> { + polls.incrementAndGet(); f.storage.read.complete(new ChunkEntities<>(POS, Collections.singletonList(new Saved()))); return true; + }, clock::getAndIncrement, 10); + assertEquals(1, polls.get()); assertEquals(1, f.tracked.get()); assertTrue(f.manager.areEntitiesLoaded(POS.toLong())); + } + @Test void pendingReadRefusesAtDeadlineWithoutInventingEmptySuccessOrSaving() { + Fixture f = new Fixture(); AtomicLong clock = new AtomicLong(); + IllegalStateException failure = assertThrows(IllegalStateException.class, () -> + NativeEntityInbox.await(f.manager, POS.toLong(), () -> false, clock::getAndIncrement, 3)); + assertTrue(failure.getMessage().contains("deadline")); + assertFalse(f.manager.areEntitiesLoaded(POS.toLong())); assertFalse(f.storage.read.isDone()); + assertEquals(1, f.storage.loads); assertEquals(0, f.storage.stores); assertEquals(0, f.tracked.get()); + } + @Test void genuineEmptyStorageStillPassesThroughNativeInbox() { + Fixture f = new Fixture(); f.storage.read.complete(new ChunkEntities<>(POS, Collections.emptyList())); + assertFalse(f.manager.areEntitiesLoaded(POS.toLong())); + NativeEntityInbox.await(f.manager, POS.toLong(), () -> false, () -> 0, 10); + assertTrue(f.manager.areEntitiesLoaded(POS.toLong())); assertEquals(0, f.tracked.get()); assertEquals(0, f.storage.stores); + } + private static final class BeforeGameLoop extends net.minecraft.util.thread.BlockableEventLoop { + private final Thread owner=Thread.currentThread(); + BeforeGameLoop(){super("NativeFixtureBeforeGameLoop");} + protected Runnable wrapRunnable(Runnable task){return task;} + protected boolean shouldRun(Runnable task){return false;} + protected Thread getRunningThread(){return owner;} + } + @Test void nativeManagedBlockAdmitsCompletionsGatedBeforeTheGameLoop() { + Fixture f=new Fixture();BeforeGameLoop loop=new BeforeGameLoop();AtomicLong clock=new AtomicLong(); + loop.tell(()->f.storage.read.complete(new ChunkEntities<>(POS,Collections.singletonList(new Saved())))); + assertFalse(loop.pollTask(),"Native event-loop gate must reproduce the pre-loop condition"); + NativeEntityInbox.await(f.manager,POS.toLong(),loop::pollTask,clock::getAndIncrement,10,loop::managedBlock); + assertTrue(f.manager.areEntitiesLoaded(POS.toLong()));assertEquals(1,f.tracked.get());assertEquals(0,loop.getPendingTasksCount()); + loop.tell(()->{});assertFalse(loop.pollTask(),"Managed blocking must not leave the native task gate bypassed"); + } + @Test void nativeManagedBlockRestoresItsGateAfterAPendingDeadline() { + Fixture f=new Fixture();BeforeGameLoop loop=new BeforeGameLoop();AtomicLong clock=new AtomicLong(); + assertThrows(IllegalStateException.class,()->NativeEntityInbox.await(f.manager,POS.toLong(),loop::pollTask, + clock::getAndIncrement,3,loop::managedBlock)); + loop.tell(()->{});assertFalse(loop.pollTask());assertFalse(f.manager.areEntitiesLoaded(POS.toLong()));assertEquals(0,f.storage.stores); + } +} diff --git a/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java new file mode 100644 index 0000000..5a1ed01 --- /dev/null +++ b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java @@ -0,0 +1,50 @@ +package cyano.lootable.buildtest; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.toml.TomlParser; +import java.nio.file.*; +import java.util.*; +import net.minecraftforge.common.ForgeConfig; +import net.minecraftforge.common.ForgeConfigSpec; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** Execute the pinned native37 spec; config corrections precede mod world hooks. */ +class Native117ForgeConfigTest { + private ForgeConfigSpec spec() throws Exception { + java.lang.reflect.Field field=ForgeConfig.class.getDeclaredField("serverSpec"); + field.setAccessible(true);return (ForgeConfigSpec)field.get(null); + } + private CommentedConfig old() throws Exception { + return new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge36-server-default.toml"))); + } + private Set keys(CommentedConfig config) { + return ((CommentedConfig)config.get("server")).valueMap().keySet(); + } + @Test void actualNativeSpecDropsOnlyItsRetiredKeysAndAddsBlockEntityDefault() throws Exception { + CommentedConfig config=old();Set previous=new HashSet<>(keys(config)); + spec().correct(config); + Set removed=new HashSet<>(previous);removed.removeAll(keys(config)); + assertEquals(new HashSet<>(Arrays.asList("removeErroringEntities","removeErroringTileEntities", + "logCascadingWorldGeneration","fixVanillaCascading","dimensionUnloadQueueDelay")),removed); + assertEquals(new HashSet<>(Arrays.asList("removeErroringBlockEntities","fullBoundingBoxLadders", + "zombieBaseSummonChance","zombieBabyChance","treatEmptyTagsAsAir","fixAdvancementLoading")),keys(config)); + assertEquals(false,config.get("server.removeErroringBlockEntities")); + assertEquals(0,spec().correct(config),"Native correction must be stable"); + } + @Test void actualNativeSpecRetainsAllSupportedNondefaultChoices() throws Exception { + CommentedConfig config=old();config.set("server.fullBoundingBoxLadders",true); + 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); + 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")); + } + @Test void actualNativeDefaultMatchesRetainedPackagedOutput() throws Exception { + CommentedConfig config=CommentedConfig.inMemory();spec().correct(config); + CommentedConfig expected=new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge37-server-default.toml"))); + assertEquals(expected,config); + } +} diff --git a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java new file mode 100644 index 0000000..2a0af5c --- /dev/null +++ b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java @@ -0,0 +1,58 @@ +package cyano.lootable.buildtest; + +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.util.Arrays; +import net.minecraft.core.NonNullList; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.*; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +class Native117MenuProtocolTest { + @BeforeAll static void bootstrap(){net.minecraft.server.Bootstrap.bootStrap();} + private ItemStack stack(int count) {ItemStack item=new ItemStack(Items.DIAMOND,count);CompoundTag tag=new CompoundTag();tag.putString("keep","native packet");item.setTag(tag);return item;} + private byte[] bytes(Packet packet) throws Exception { + FriendlyByteBuf buffer=new FriendlyByteBuf(Unpooled.buffer()); + try {packet.write(buffer);byte[] bytes=new byte[buffer.readableBytes()];buffer.getBytes(0,bytes);return bytes;}finally{buffer.release();} + } + @Test void fullContentCarriesStateAndCursorWithoutMutatingThePacket() throws Exception { + NonNullList slots=NonNullList.withSize(84,ItemStack.EMPTY);slots.set(47,stack(31));ItemStack cursor=stack(17); + ClientboundContainerSetContentPacket packet=new ClientboundContainerSetContentPacket(7,321,slots,cursor);byte[] before=bytes(packet); + NativeMenuObservation observed=NativeMenuObservation.decode(packet);assertNotNull(observed);assertTrue(observed.content); + assertEquals(7,observed.containerId);assertEquals(321,observed.stateId);assertEquals(84,observed.items.size()); + assertTrue(ItemStack.matches(slots.get(47),observed.items.get(47)));assertTrue(ItemStack.matches(cursor,observed.carried));assertArrayEquals(before,bytes(packet)); + } + @Test void slotAndCursorCorrectionsUseTheirActualNativeWindowAndState() throws Exception { + for(int window:new int[]{7,-1,-2}) { + ClientboundContainerSetSlotPacket packet=new ClientboundContainerSetSlotPacket(window,123,window==-1?-1:47,stack(19));byte[] before=bytes(packet); + NativeMenuObservation observed=NativeMenuObservation.decode(packet);assertNotNull(observed);assertFalse(observed.content); + assertEquals(window,observed.containerId);assertEquals(123,observed.stateId);assertEquals(window==-1?-1:47,observed.slot); + assertTrue(ItemStack.matches(stack(19),observed.item));assertArrayEquals(before,bytes(packet)); + } + } + @Test void clickCodecRetainsStatePredictedSlotsAndCarriedStack() throws Exception { + Int2ObjectOpenHashMap changed=new Int2ObjectOpenHashMap<>();changed.put(6,stack(13));changed.put(47,ItemStack.EMPTY); + ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket(7,32766,6,1,ClickType.PICKUP,stack(9),changed); + FriendlyByteBuf buffer=new FriendlyByteBuf(Unpooled.buffer()); + try { + outgoing.write(buffer);byte[] before=new byte[buffer.readableBytes()];buffer.getBytes(0,before); + ServerboundContainerClickPacket decoded=new ServerboundContainerClickPacket(buffer); + assertEquals(7,decoded.getContainerId());assertEquals(32766,decoded.getStateId());assertEquals(6,decoded.getSlotNum());assertEquals(1,decoded.getButtonNum()); + assertEquals(ClickType.PICKUP,decoded.getClickType());assertTrue(ItemStack.matches(stack(9),decoded.getCarriedItem())); + assertEquals(changed.keySet(),decoded.getChangedSlots().keySet());assertTrue(ItemStack.matches(stack(13),decoded.getChangedSlots().get(6)));assertTrue(decoded.getChangedSlots().get(47).isEmpty()); + assertEquals(before.length,buffer.readerIndex());byte[] after=new byte[before.length];buffer.getBytes(0,after);assertArrayEquals(before,after); + }finally{buffer.release();} + } + @Test void unrelatedPacketsAreIgnoredWithoutSerializingThem() { + Packet other=new Packet() { + public void write(FriendlyByteBuf buffer){throw new AssertionError("Observer serialized an unrelated packet");} + public void handle(ClientGamePacketListener listener){throw new AssertionError("Observer handled a live packet");} + }; + assertNull(NativeMenuObservation.decode(other)); + } +} diff --git a/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java new file mode 100644 index 0000000..1ab6b09 --- /dev/null +++ b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java @@ -0,0 +1,33 @@ +package cyano.lootable.buildtest; + +import java.nio.file.*; +import java.util.*; +import java.util.zip.ZipFile; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** Native37 loads mods as distinct Java modules: split packages are fatal. */ +class Native117ProbeIsolationTest { + private Set packages(String archive) throws Exception { + Set result=new TreeSet<>(); + try(ZipFile zip=new ZipFile(archive)) { + for(java.util.Enumeration entries=zip.entries();entries.hasMoreElements();) { + java.util.zip.ZipEntry entry=entries.nextElement();String path=entry.getName(); + if(!path.endsWith(".class"))continue; + byte[] code=zip.getInputStream(entry).readAllBytes(); + assertEquals(60,((code[6]&255)<<8)|(code[7]&255),path); + result.add(path.substring(0,path.lastIndexOf('/'))); + } + } + assertFalse(result.isEmpty(),archive);return result; + } + @Test void integrationModuleHasNoProductionPackageDefinitions() throws Exception { + Set main=packages("build/libs-dev/LootableBodies-2.4.8.117011-deobf.jar"); + Set probe=packages("build/integration-test-mod/lootablebodies-build-tests.jar"); + probe.retainAll(main);assertTrue(probe.isEmpty(),"Native37 module split packages: "+probe); + } + @Test void independentControlHasNoModOrIntegrationPackages() throws Exception { + Set control=packages("build/control-test-mod/forge-platform-control.jar"); + assertTrue(control.stream().allMatch(name->name.equals("buildcontrol")),control.toString()); + } +} diff --git a/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java b/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java new file mode 100644 index 0000000..397e451 --- /dev/null +++ b/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java @@ -0,0 +1,12 @@ +package cyano.lootable.buildtest; + +import org.junit.platform.launcher.LauncherSession; +import org.junit.platform.launcher.LauncherSessionListener; + +/** Native registries must be ready before any test-class static initializer. */ +public final class Native117TestEnvironment implements LauncherSessionListener { + @Override public void launcherSessionOpened(LauncherSession session) { + net.minecraft.SharedConstants.tryDetectVersion(); + net.minecraft.server.Bootstrap.bootStrap(); + } +} diff --git a/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java b/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java index ee329ea..44c6853 100644 --- a/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java +++ b/src/test/java/cyano/lootable/buildtest/NativeVanillaDataFixerTest.java @@ -11,13 +11,13 @@ class NativeVanillaDataFixerTest { assertSame(vanilla,NativeVanillaDataFixer.unwrap(vanilla)); } @Test void capturedVanillaOracleStaysIndependentWhenASyntheticCounterWrapsTheGlobalReader(){ - DataFixer nativeOracle=NativeVanillaDataFixer.get();DataFixer previous=net.minecraft.util.datafix.DataFixesManager.getDataFixer(); + DataFixer nativeOracle=NativeVanillaDataFixer.get();DataFixer previous=net.minecraft.util.datafix.DataFixers.getDataFixer(); try { - net.minecraft.util.datafix.DataFixesManager.DATA_FIXER=new DataFixer(){ + net.minecraft.util.datafix.DataFixers.DATA_FIXER=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.DataFixesManager.DATA_FIXER=previous;} + }finally{net.minecraft.util.datafix.DataFixers.DATA_FIXER=previous;} } } diff --git a/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java b/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java index 920b9b0..bbbe77d 100644 --- a/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java +++ b/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java @@ -1,72 +1,78 @@ package cyano.lootable.buildtest; -import net.minecraft.nbt.*; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class TypedNbtTest { - private CompoundNBT compound(boolean reverse) { - CompoundNBT value = new CompoundNBT(); + private CompoundTag compound(boolean reverse) { + CompoundTag value = new CompoundTag(); // These names deliberately collide in Java's hash map. if (reverse) { value.putInt("BB", 2); value.putInt("Aa", 1); } else { value.putInt("Aa", 1); value.putInt("BB", 2); } return value; } @Test void compoundInsertionOrderDoesNotInventAMutation() { - CompoundNBT left = compound(false), right = compound(true); + CompoundTag left = compound(false), right = compound(true); assertEquals(left, right); assertTrue(TypedNbt.same(left, right)); } @Test void compoundOrderInsideListsAlsoDoesNotInventAMutation() { - ListNBT left = new ListNBT(), right = new ListNBT(); + ListTag left = new ListTag(), right = new ListTag(); left.add(compound(false)); right.add(compound(true)); assertTrue(TypedNbt.same(left, right)); } @Test void listOrderAndNumericTypesStillMatter() { - ListNBT left = new ListNBT(), right = new ListNBT(); - left.add(IntNBT.valueOf(1)); left.add(IntNBT.valueOf(2)); - right.add(IntNBT.valueOf(2)); right.add(IntNBT.valueOf(1)); + ListTag left = new ListTag(), right = new ListTag(); + left.add(IntTag.valueOf(1)); left.add(IntTag.valueOf(2)); + right.add(IntTag.valueOf(2)); right.add(IntTag.valueOf(1)); assertFalse(TypedNbt.same(left, right)); - assertFalse(TypedNbt.same(IntNBT.valueOf(7), ByteNBT.valueOf((byte)7))); + assertFalse(TypedNbt.same(IntTag.valueOf(7), ByteTag.valueOf((byte)7))); } @Test void arraysAndChangedNestedPayloadsStillMatter() { - assertFalse(TypedNbt.same(new IntArrayNBT(new int[]{1,2}), new IntArrayNBT(new int[]{2,1}))); - CompoundNBT left = compound(false), right = compound(true); right.putInt("Aa", 3); + assertFalse(TypedNbt.same(new IntArrayTag(new int[]{1,2}), new IntArrayTag(new int[]{2,1}))); + CompoundTag left = compound(false), right = compound(true); right.putInt("Aa", 3); assertFalse(TypedNbt.same(left, right)); } @Test void nonFiniteValuesKeepTheirTypesAndSigns() throws Exception { - assertTrue(TypedNbt.same(DoubleNBT.valueOf(Double.NaN), DoubleNBT.valueOf(Double.NaN))); - assertFalse(TypedNbt.same(DoubleNBT.valueOf(Double.NaN), FloatNBT.valueOf(Float.NaN))); - assertFalse(TypedNbt.same(DoubleNBT.valueOf(Double.POSITIVE_INFINITY), DoubleNBT.valueOf(Double.NEGATIVE_INFINITY))); + assertTrue(TypedNbt.same(DoubleTag.valueOf(Double.NaN), DoubleTag.valueOf(Double.NaN))); + assertFalse(TypedNbt.same(DoubleTag.valueOf(Double.NaN), FloatTag.valueOf(Float.NaN))); + assertFalse(TypedNbt.same(DoubleTag.valueOf(Double.POSITIVE_INFINITY), DoubleTag.valueOf(Double.NEGATIVE_INFINITY))); // The native factory and reader canonicalise both zero signs. Construct // this fixture directly so the assertion really compares different bits. - java.lang.reflect.Constructor constructor = DoubleNBT.class.getDeclaredConstructor(double.class); + java.lang.reflect.Constructor constructor = DoubleTag.class.getDeclaredConstructor(double.class); constructor.setAccessible(true); - assertFalse(TypedNbt.same(DoubleNBT.valueOf(0D), constructor.newInstance(-0D))); + assertFalse(TypedNbt.same(DoubleTag.valueOf(0D), constructor.newInstance(-0D))); } @Test void comparisonDoesNotAlterEitherInput() { - CompoundNBT left = compound(false), before = left.copy(), right = compound(true); + CompoundTag left = compound(false), before = left.copy(), right = compound(true); TypedNbt.same(left, right); assertEquals(before, left); assertEquals(before, right); } - private CompoundNBT entity(boolean reverse) { - CompoundNBT body=new CompoundNBT(),health=new CompoundNBT(),speed=new CompoundNBT(); + private CompoundTag entity(boolean reverse) { + CompoundTag body=new CompoundTag(),health=new CompoundTag(),speed=new CompoundTag(); health.putString("Name","minecraft:generic.max_health");health.putDouble("Base",20); speed.putString("Name","minecraft:generic.movement_speed");speed.putDouble("Base",.7); - ListNBT attributes=new ListNBT();attributes.add(reverse?speed:health);attributes.add(reverse?health:speed);body.put("Attributes",attributes); - ListNBT equipment=new ListNBT();equipment.add(compound(false));equipment.add(compound(true));body.put("Equipment",equipment);return body; + ListTag attributes=new ListTag();attributes.add(reverse?speed:health);attributes.add(reverse?health:speed);body.put("Attributes",attributes); + ListTag equipment=new ListTag();equipment.add(compound(false));equipment.add(compound(true));body.put("Equipment",equipment);return body; } @Test void nativeAttributeMapOrderDoesNotInventAnEntityMutation() { - CompoundNBT before=entity(false),other=entity(true),copy=before.copy(); + CompoundTag before=entity(false),other=entity(true),copy=before.copy(); assertFalse(TypedNbt.same(before,other));assertTrue(TypedNbt.sameEntity(before,other)); assertTrue(TypedNbt.same(before,copy));assertTrue(TypedNbt.same(other,entity(true))); } @Test void entityComparisonStillRequiresEveryTypedAttributeAndDuplicate() { - CompoundNBT left=entity(false),right=entity(true);right.getList("Attributes",10).getCompound(0).putFloat("Base",.7F); + CompoundTag left=entity(false),right=entity(true);right.getList("Attributes",10).getCompound(0).putFloat("Base",.7F); assertFalse(TypedNbt.sameEntity(left,right));right=entity(true);right.getList("Attributes",10).add(right.getList("Attributes",10).getCompound(0).copy()); assertFalse(TypedNbt.sameEntity(left,right)); } @Test void inventoryAndRecoveryListOrderRemainStrictInsideEntityComparison() { - CompoundNBT left=entity(false),right=entity(true);ListNBT items=new ListNBT();items.add(IntNBT.valueOf(1));items.add(IntNBT.valueOf(2));left.put("Equipment",items); - ListNBT reversed=new ListNBT();reversed.add(IntNBT.valueOf(2));reversed.add(IntNBT.valueOf(1));right.put("Equipment",reversed); + CompoundTag left=entity(false),right=entity(true);ListTag items=new ListTag();items.add(IntTag.valueOf(1));items.add(IntTag.valueOf(2));left.put("Equipment",items); + ListTag reversed=new ListTag();reversed.add(IntTag.valueOf(2));reversed.add(IntTag.valueOf(1));right.put("Equipment",reversed); assertFalse(TypedNbt.sameEntity(left,right));left.remove("Equipment");right.remove("Equipment");left.put("LootableBodiesUnresolved",items);right.put("LootableBodiesUnresolved",reversed); assertFalse(TypedNbt.sameEntity(left,right)); } diff --git a/src/test/java/cyano/lootable/contracts/BuildContractTest.java b/src/test/java/cyano/lootable/contracts/BuildContractTest.java index 6222967..323a807 100644 --- a/src/test/java/cyano/lootable/contracts/BuildContractTest.java +++ b/src/test/java/cyano/lootable/contracts/BuildContractTest.java @@ -50,9 +50,9 @@ 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.16.5]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); + assertTrue(info.contains("versionRange=\"[1.17.1]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); assertFalse(Files.exists(Paths.get("build/resources/main/mcmod.info"))); - assertEquals(6,new com.google.gson.JsonParser().parse(text("build/resources/main/pack.mcmeta")).getAsJsonObject().getAsJsonObject("pack").get("pack_format").getAsInt()); + assertEquals(7,new com.google.gson.JsonParser().parse(text("build/resources/main/pack.mcmeta")).getAsJsonObject().getAsJsonObject("pack").get("pack_format").getAsInt()); } @Test void everyOriginalTranslationSurvivesTheNativeJsonFormat() throws Exception { try(DirectoryStream locales=Files.newDirectoryStream(Paths.get("src/test/resources/legacy-translations"),"*.lang")){ @@ -69,7 +69,7 @@ private String text(String path) throws Exception { for (String key : Arrays.asList("Equipment", "Aux", "Slot", "Vac", "Name", "DeathTime", "DecayTime")) assertTrue(source.contains("\"" + key + "\""), key); assertTrue(source.contains("NonNullList.withSize(6*7, ItemStack.EMPTY)")); - assertTrue(source.contains("EquipmentSlotType.HEAD, EquipmentSlotType.CHEST, EquipmentSlotType.LEGS, EquipmentSlotType.FEET, EquipmentSlotType.MAINHAND, EquipmentSlotType.OFFHAND")); + assertTrue(source.contains("EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET, EquipmentSlot.MAINHAND, EquipmentSlot.OFFHAND")); } @Test void configurationNamesAndDefaultsRemainStable() throws Exception { String source = text("src/main/java/cyano/lootable/LootableBodies.java"); diff --git a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java index b66490e..36dfd7a 100644 --- a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java +++ b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java @@ -110,7 +110,7 @@ private void save() throws Exception { @Test void cancelledChildEditsCannotLeakIntoTheRootDraft()throws Exception { load("");cyano.lootable.graphics.CorpseConfigGui root=new cyano.lootable.graphics.CorpseConfigGui(null); java.lang.reflect.Field pending=root.getClass().getDeclaredField("pending");pending.setAccessible(true); - java.lang.reflect.Constructor ctor=root.getClass().getDeclaredConstructor(net.minecraft.client.gui.screen.Screen.class,String.class,Map.class,int.class);ctor.setAccessible(true); + java.lang.reflect.Constructor ctor=root.getClass().getDeclaredConstructor(net.minecraft.client.gui.screens.Screen.class,String.class,Map.class,int.class);ctor.setAccessible(true); @SuppressWarnings("unchecked") Map original=(Map)pending.get(root); Object child=ctor.newInstance(root,"rules",original,0); @SuppressWarnings("unchecked") Map edits=(Map)pending.get(child); @@ -121,16 +121,16 @@ private void save() throws Exception { load("");cyano.lootable.graphics.CorpseConfigGui root=new cyano.lootable.graphics.CorpseConfigGui(null); java.lang.reflect.Field pending=root.getClass().getDeclaredField("pending");pending.setAccessible(true); @SuppressWarnings("unchecked") Map edits=(Map)pending.get(root); - edits.put("corpse_HP","31");root.onClose(); + edits.put("corpse_HP","31");root.removed(); assertEquals("31",edits.get("corpse_HP"));assertEquals(20,LootableBodies.corpseHP); } @Test void corpseLayoutKeepsItsOriginalUnlabelledForeground()throws Exception { cyano.lootable.graphics.CorpseGUIContainer screen=new cyano.lootable.graphics.CorpseGUIContainer( - (cyano.lootable.graphics.CorpseContainer)null,null,new net.minecraft.util.text.StringTextComponent("Saved owner")); - java.lang.reflect.Method foreground=net.minecraft.client.gui.screen.inventory.ContainerScreen.class.getDeclaredMethod( - "drawGuiContainerForegroundLayer",com.mojang.blaze3d.matrix.MatrixStack.class,int.class,int.class);foreground.setAccessible(true); - // The original layout uses all seven rows. Vanilla16 labels would draw + (cyano.lootable.graphics.CorpseContainer)null,new net.minecraft.world.entity.player.Inventory(null),new net.minecraft.network.chat.TextComponent("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 // over those slots; its original foreground needs no font or item state. - foreground.invoke(screen,new com.mojang.blaze3d.matrix.MatrixStack(),0,0); + foreground.invoke(screen,new com.mojang.blaze3d.vertex.PoseStack(),0,0); } } diff --git a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java index 7f256c9..969480c 100644 --- a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java +++ b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java @@ -7,8 +7,12 @@ import java.nio.file.*; import java.util.*; import java.util.zip.DeflaterOutputStream; -import net.minecraft.nbt.*; -import net.minecraft.util.datafix.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; import org.junit.jupiter.api.*; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; @@ -17,100 +21,100 @@ class NativeUpgradeBoundaryTest { @TempDir Path world; private static final UUID ID=UUID.fromString("00000000-3333-4333-8333-000000000001"); - private static final DataFixer VANILLA=DataFixesManager.getDataFixer(); + private static final DataFixer VANILLA=DataFixers.getDataFixer(); @AfterEach void close(){UpgradePreservation.close();} - private CompoundNBT item(String id,int count,int damage,int slot) { - CompoundNBT item=new CompoundNBT();item.putString("id",id);item.putByte("Count",(byte)count);item.putShort("Damage",(short)damage);if(slot>=0)item.putByte("Slot",(byte)slot); - CompoundNBT tag=new CompoundNBT();tag.putString("keepsake","slot-"+slot);item.put("tag",tag);return item; - } - private CompoundNBT corpse() { - CompoundNBT body=new CompoundNBT();body.putString("id","lootablebodies.Corpse");body.putUniqueId("UUID",ID);body.putString("Name","Saved owner");body.putString("CustomName","Saved owner");body.putLong("DeathTime",700); - ListNBT equipment=new ListNBT();for(int i=0;i<48;i++)equipment.add(item("minecraft:stone",i+1,1,i));body.put("Equipment",equipment); - ListNBT aux=new ListNBT();aux.add(item("minecraft:gold_ingot",17,0,-1));body.put("Aux",aux);body.putDouble("FallDistance",Double.NaN);return body; - } - private CompoundNBT chunk(CompoundNBT entity,boolean populated) { - CompoundNBT root=new CompoundNBT(),level=new CompoundNBT();root.putInt("DataVersion",512);root.put("Level",level);level.putInt("xPos",0);level.putInt("zPos",0);level.putBoolean("TerrainPopulated",populated);level.putBoolean("LightPopulated",populated);level.putByteArray("Biomes",new byte[256]);level.putIntArray("HeightMap",new int[256]); - ListNBT entities=new ListNBT();if(entity!=null)entities.add(entity.copy());level.put("Entities",entities); - CompoundNBT section=new CompoundNBT();section.putByte("Y",(byte)0);byte[] blocks=new byte[4096];blocks[0]=54;section.putByteArray("Blocks",blocks);section.putByteArray("Data",new byte[2048]);section.putByteArray("BlockLight",new byte[2048]);section.putByteArray("SkyLight",new byte[2048]);ListNBT sections=new ListNBT();sections.add(section);level.put("Sections",sections); - CompoundNBT chest=new CompoundNBT();chest.putString("id","Chest");chest.putInt("x",0);chest.putInt("y",0);chest.putInt("z",0);ListNBT contents=new ListNBT();contents.add(item("minecraft:stone",23,1,0));chest.put("Items",contents);ListNBT tiles=new ListNBT();tiles.add(chest);level.put("TileEntities",tiles);return root; - } - private byte[] encoded(CompoundNBT nbt)throws Exception{ByteArrayOutputStream bytes=new ByteArrayOutputStream();CompressedStreamTools.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} - private void preserve(CompoundNBT nbt)throws Exception { - ByteArrayOutputStream bytes=new ByteArrayOutputStream();try(DataOutputStream output=new DataOutputStream(new DeflaterOutputStream(bytes))){CompressedStreamTools.write(nbt,output);} + private CompoundTag item(String id,int count,int damage,int slot) { + CompoundTag item=new CompoundTag();item.putString("id",id);item.putByte("Count",(byte)count);item.putShort("Damage",(short)damage);if(slot>=0)item.putByte("Slot",(byte)slot); + CompoundTag tag=new CompoundTag();tag.putString("keepsake","slot-"+slot);item.put("tag",tag);return item; + } + private CompoundTag corpse() { + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies.Corpse");body.putUUID("UUID",ID);body.putString("Name","Saved owner");body.putString("CustomName","Saved owner");body.putLong("DeathTime",700); + ListTag equipment=new ListTag();for(int i=0;i<48;i++)equipment.add(item("minecraft:stone",i+1,1,i));body.put("Equipment",equipment); + ListTag aux=new ListTag();aux.add(item("minecraft:gold_ingot",17,0,-1));body.put("Aux",aux);body.putDouble("FallDistance",Double.NaN);return body; + } + private CompoundTag chunk(CompoundTag entity,boolean populated) { + CompoundTag root=new CompoundTag(),level=new CompoundTag();root.putInt("DataVersion",512);root.put("Level",level);level.putInt("xPos",0);level.putInt("zPos",0);level.putBoolean("TerrainPopulated",populated);level.putBoolean("LightPopulated",populated);level.putByteArray("Biomes",new byte[256]);level.putIntArray("HeightMap",new int[256]); + ListTag entities=new ListTag();if(entity!=null)entities.add(entity.copy());level.put("Entities",entities); + CompoundTag section=new CompoundTag();section.putByte("Y",(byte)0);byte[] blocks=new byte[4096];blocks[0]=54;section.putByteArray("Blocks",blocks);section.putByteArray("Data",new byte[2048]);section.putByteArray("BlockLight",new byte[2048]);section.putByteArray("SkyLight",new byte[2048]);ListTag sections=new ListTag();sections.add(section);level.put("Sections",sections); + CompoundTag chest=new CompoundTag();chest.putString("id","Chest");chest.putInt("x",0);chest.putInt("y",0);chest.putInt("z",0);ListTag contents=new ListTag();contents.add(item("minecraft:stone",23,1,0));chest.put("Items",contents);ListTag tiles=new ListTag();tiles.add(chest);level.put("TileEntities",tiles);return root; + } + private byte[] encoded(CompoundTag nbt)throws Exception{ByteArrayOutputStream bytes=new ByteArrayOutputStream();NbtIo.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} + private void preserve(CompoundTag nbt)throws Exception { + ByteArrayOutputStream bytes=new ByteArrayOutputStream();try(DataOutputStream output=new DataOutputStream(new DeflaterOutputStream(bytes))){NbtIo.write(nbt,output);} int sectors=(bytes.size()+5+4095)/4096;ByteBuffer region=ByteBuffer.allocate(8192+sectors*4096);region.putInt((2<<8)|sectors);region.position(8192);region.putInt(bytes.size()+1);region.put((byte)2);region.put(bytes.toByteArray());Path file=world.resolve("region/r.0.0.mca");Files.createDirectories(file.getParent());Files.write(file,region.array());UpgradePreservation.register(VANILLA);UpgradePreservation.prepare(world); } - private CompoundNBT update(CompoundNBT chunk)throws Exception { + private CompoundTag update(CompoundTag chunk)throws Exception { preserve(chunk);DataFixer safe=UpgradePreservation.wrapper(VANILLA); - CompoundNBT first=cyano.lootable.upgrade.NativeNbt.update(safe,TypeReferences.CHUNK,chunk.copy(),chunk.getInt("DataVersion"),1493); - return cyano.lootable.upgrade.NativeNbt.update(safe,TypeReferences.CHUNK,first,1493,1976); + CompoundTag first=cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,chunk.copy(),chunk.getInt("DataVersion"),1493); + return cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,first,1493,1976); } - private CompoundNBT findCorpse(CompoundNBT root) { - CompoundNBT found=find(root);assertNotNull(found,"The original corpse must remain present");return found; + private CompoundTag findCorpse(CompoundTag root) { + CompoundTag found=find(root);assertNotNull(found,"The original corpse must remain present");return found; } - private CompoundNBT find(INBT value) { - if(value instanceof CompoundNBT){CompoundNBT nbt=(CompoundNBT)value;if(nbt.getString("id").equals("lootablebodies:corpse") || nbt.getString("id").equals("lootablebodies.Corpse"))return nbt;for(String key:nbt.keySet()){CompoundNBT found=find(nbt.get(key));if(found!=null)return found;}} - if(value instanceof ListNBT){for(INBT entry:(ListNBT)value){CompoundNBT found=find(entry);if(found!=null)return found;}}return null; + private CompoundTag find(Tag value) { + if(value instanceof CompoundTag){CompoundTag nbt=(CompoundTag)value;if(nbt.getString("id").equals("lootablebodies:corpse") || nbt.getString("id").equals("lootablebodies.Corpse"))return nbt;for(String key:nbt.getAllKeys()){CompoundTag found=find(nbt.get(key));if(found!=null)return found;}} + if(value instanceof ListTag){for(Tag entry:(ListTag)value){CompoundTag found=find(entry);if(found!=null)return found;}}return null; } @Test void legacyIdentityAndEveryRawInventoryEntrySurviveBothAnvilPasses()throws Exception { - CompoundNBT body=corpse(),source=chunk(body,true);byte[] before=encoded(source);CompoundNBT result=update(source),saved=findCorpse(result); - assertEquals("lootablebodies:corpse",saved.getString("id"));assertEquals(6,saved.getInt("LootableBodiesDataVersion"));assertEquals(ID,saved.getUniqueId("UUID")); - ListNBT records=saved.getList("LootableBodiesUnresolved",10);assertEquals(49,records.size()); - for(int i=0;i<48;i++){CompoundNBT 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"));} + 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(7,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")); } @Test void occupiedPartlyGeneratedChunksKeepTerrainAndVanillaContainers()throws Exception { - CompoundNBT result=update(chunk(corpse(),false)),level=result.getCompound("Level"); + CompoundTag result=update(chunk(corpse(),false)),level=result.getCompound("Level"); assertEquals(1,level.getList("Sections",10).size());assertTrue(level.getList("Sections",10).getCompound(0).contains("Palette",9)); - ListNBT tiles=level.getList("TileEntities",10);assertEquals(1,tiles.size());CompoundNBT contents=tiles.getCompound(0).getList("Items",10).getCompound(0);assertEquals("minecraft:granite",contents.getString("id"));assertEquals(23,contents.getByte("Count"));assertNotEquals("empty",level.getString("Status")); + ListTag tiles=level.getList("TileEntities",10);assertEquals(1,tiles.size());CompoundTag contents=tiles.getCompound(0).getList("Items",10).getCompound(0);assertEquals("minecraft:granite",contents.getString("id"));assertEquals(23,contents.getByte("Count"));assertNotEquals("empty",level.getString("Status")); } @Test void vanillaContainersAreProtectedEvenWithoutACorpseInTheirChunk()throws Exception { - CompoundNBT result=update(chunk(null,false));assertEquals(1,result.getCompound("Level").getList("Sections",10).size());assertEquals(1,result.getCompound("Level").getList("TileEntities",10).size()); + CompoundTag result=update(chunk(null,false));assertEquals(1,result.getCompound("Level").getList("Sections",10).size());assertEquals(1,result.getCompound("Level").getList("TileEntities",10).size()); } @Test void storedTerrainSurvivesAnIncompleteChunkWithoutEntitiesOrContainers()throws Exception { - CompoundNBT source=chunk(null,false);source.getCompound("Level").put("TileEntities",new ListNBT()); - CompoundNBT level=update(source).getCompound("Level"); + CompoundTag source=chunk(null,false);source.getCompound("Level").put("TileEntities",new ListTag()); + CompoundTag level=update(source).getCompound("Level"); assertEquals(1,level.getList("Sections",10).size()); assertTrue(level.getList("Sections",10).getCompound(0).contains("Palette",9)); } @Test void repeatedConversionCannotDuplicateOrResurrectRemovedCorpses()throws Exception { - CompoundNBT result=update(chunk(corpse(),true));DataFixer safe=UpgradePreservation.wrapper(VANILLA);CompoundNBT again=cyano.lootable.upgrade.NativeNbt.update(safe,TypeReferences.CHUNK,result.copy(),1976,1976); + CompoundTag result=update(chunk(corpse(),true));DataFixer safe=UpgradePreservation.wrapper(VANILLA);CompoundTag again=cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,result.copy(),1976,1976); assertEquals(49,findCorpse(again).getList("LootableBodiesUnresolved",10).size()); - again.getCompound("Level").put("Entities",new ListNBT());assertNull(find(cyano.lootable.upgrade.NativeNbt.update(safe,TypeReferences.CHUNK,again,1976,1976))); + again.getCompound("Level").put("Entities",new ListTag());assertNull(find(cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,again,1976,1976))); } @Test void vanillaMinecartParentsAndNestedCorpsesBothUpgrade()throws Exception { - CompoundNBT cart=new CompoundNBT();cart.putString("id","MinecartRideable");cart.putUniqueId("UUID",UUID.fromString("00000000-3333-4333-8333-000000000099"));ListNBT riders=new ListNBT();riders.add(corpse());cart.put("Passengers",riders); - CompoundNBT result=update(chunk(cart,true));assertEquals("minecraft:minecart",result.getCompound("Level").getList("Entities",10).getCompound(0).getString("id"));assertEquals(49,findCorpse(result).getList("LootableBodiesUnresolved",10).size()); + CompoundTag cart=new CompoundTag();cart.putString("id","MinecartRideable");cart.putUUID("UUID",UUID.fromString("00000000-3333-4333-8333-000000000099"));ListTag riders=new ListTag();riders.add(corpse());cart.put("Passengers",riders); + CompoundTag result=update(chunk(cart,true));assertEquals("minecraft:minecart",result.getCompound("Level").getList("Entities",10).getCompound(0).getString("id"));assertEquals(49,findCorpse(result).getList("LootableBodiesUnresolved",10).size()); } @Test void corpseEggsKeepTheirTypeCountAndEntityPayload()throws Exception { for(String id:new String[]{"lootablebodies.Corpse","lootablebodies:corpse"}){ - CompoundNBT egg=item("minecraft:spawn_egg",7,0,4),entity=new CompoundNBT();entity.putString("id",id);entity.putString("Name","Egg owner");egg.getCompound("tag").put("EntityTag",entity);byte[] before=encoded(egg); - CompoundNBT fixed=cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.ITEM_STACK,egg.copy(),512,1976); + CompoundTag egg=item("minecraft:spawn_egg",7,0,4),entity=new CompoundTag();entity.putString("id",id);entity.putString("Name","Egg owner");egg.getCompound("tag").put("EntityTag",entity);byte[] before=encoded(egg); + CompoundTag fixed=cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),References.ITEM_STACK,egg.copy(),512,1976); assertEquals("lootablebodies:corpse_spawn_egg",fixed.getString("id"));assertEquals(7,fixed.getByte("Count"));assertEquals("lootablebodies:corpse",fixed.getCompound("tag").getCompound("EntityTag").getString("id"));assertEquals("Egg owner",fixed.getCompound("tag").getCompound("EntityTag").getString("Name"));assertEquals(4,fixed.getByte("Slot"));assertArrayEquals(before,encoded(egg)); } } @Test void corpseEggsInsideVanillaPlayerInventoriesAlsoUpgrade()throws Exception { - CompoundNBT egg=item("minecraft:spawn_egg",11,0,0),entity=new CompoundNBT();entity.putString("id","lootablebodies.Corpse");egg.getCompound("tag").put("EntityTag",entity);ListNBT items=new ListNBT();items.add(egg);CompoundNBT player=new CompoundNBT();player.put("Inventory",items); - CompoundNBT result=cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.PLAYER,player.copy(),512,1976);CompoundNBT fixed=result.getList("Inventory",10).getCompound(0);assertEquals("lootablebodies:corpse_spawn_egg",fixed.getString("id"));assertEquals(11,fixed.getByte("Count")); + CompoundTag egg=item("minecraft:spawn_egg",11,0,0),entity=new CompoundTag();entity.putString("id","lootablebodies.Corpse");egg.getCompound("tag").put("EntityTag",entity);ListTag items=new ListTag();items.add(egg);CompoundTag player=new CompoundTag();player.put("Inventory",items); + CompoundTag result=cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),References.PLAYER,player.copy(),512,1976);CompoundTag fixed=result.getList("Inventory",10).getCompound(0);assertEquals("lootablebodies:corpse_spawn_egg",fixed.getString("id"));assertEquals(11,fixed.getByte("Count")); } @Test void aFalseCurrentMarkerCannotBypassLegacyIdentityConversion()throws Exception { - CompoundNBT body=corpse();body.putInt("LootableBodiesDataVersion",3); - CompoundNBT result=update(chunk(body,true)); + CompoundTag body=corpse();body.putInt("LootableBodiesDataVersion",3); + CompoundTag result=update(chunk(body,true)); assertEquals(49,findCorpse(result).getList("LootableBodiesUnresolved",10).size()); assertEquals(512,findCorpse(result).getCompound("LootableBodiesSourceVersions").getInt("DataVersion")); } @Test void futureCorpseFormatsAreNeverOverwritten() { - CompoundNBT body=corpse();body.putString("id","lootablebodies:corpse");body.putInt("LootableBodiesDataVersion",99); - assertThrows(IllegalStateException.class,()->cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.ENTITY_TYPE,body,1343,1976)); + CompoundTag body=corpse();body.putString("id","lootablebodies:corpse");body.putInt("LootableBodiesDataVersion",99); + assertThrows(IllegalStateException.class,()->cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),References.ENTITY_TREE,body,1343,1976)); } @Test void chunkConversionFailuresCannotBeMistakenForAbsentChunks() { UpgradePreservation.close(); - assertThrows(Error.class,()->cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.CHUNK,chunk(corpse(),true),512,1976)); + assertThrows(Error.class,()->cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),References.CHUNK,chunk(corpse(),true),512,1976)); } @Test void aPlausibleFalseStampCannotSkipVanillaBannerConversion()throws Exception { - CompoundNBT source=chunk(corpse(),true);source.putInt("DataVersion",922); - CompoundNBT banner=item("minecraft:banner",2,0,0),block=new CompoundNBT();block.putInt("Base",5);banner.getCompound("tag").put("BlockEntityTag",block); - ListNBT items=new ListNBT();items.add(banner);source.getCompound("Level").getList("TileEntities",10).getCompound(0).put("Items",items); - CompoundNBT converted=update(source).getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); + CompoundTag source=chunk(corpse(),true);source.putInt("DataVersion",922); + CompoundTag banner=item("minecraft:banner",2,0,0),block=new CompoundTag();block.putInt("Base",5);banner.getCompound("tag").put("BlockEntityTag",block); + ListTag items=new ListTag();items.add(banner);source.getCompound("Level").getList("TileEntities",10).getCompound(0).put("Items",items); + CompoundTag converted=update(source).getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); assertEquals("minecraft:purple_banner",converted.getString("id"));assertEquals(2,converted.getByte("Count")); } } diff --git a/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java b/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java index 112ea3e..9e8ae81 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.7.116051", version); + assertEquals("2.4.8.117011", 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.16.5]\"")); + assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.17.1]\"")); } @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 df18196..1e6e670 100644 --- a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java +++ b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java @@ -6,7 +6,9 @@ import java.security.MessageDigest; import java.util.*; import java.util.zip.DeflaterOutputStream; -import net.minecraft.nbt.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; @@ -19,136 +21,136 @@ private Object scan() throws Exception { try { return Class.forName("cyano.lootable.upgrade.UpgradeJournal").getMethod("scan",Path.class).invoke(null,world); } catch(InvocationTargetException error) { throw (Exception)error.getCause(); } } - private CompoundNBT original(Object journal,UUID id,int x,int z) throws Exception { - return (CompoundNBT)journal.getClass().getMethod("originalFor",UUID.class,int.class,int.class).invoke(journal,id,x,z); + private CompoundTag original(Object journal,UUID id,int x,int z) throws Exception { + return (CompoundTag)journal.getClass().getMethod("originalFor",UUID.class,int.class,int.class).invoke(journal,id,x,z); } - private CompoundNBT body(UUID id) { - CompoundNBT body=new CompoundNBT();body.putString("id","lootablebodies.Corpse");body.putUniqueId("UUID",id); + private CompoundTag body(UUID id) { + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies.Corpse");body.putUUID("UUID",id); body.putDouble("FallDistance",Double.NaN);body.putString("Name","Saved owner");body.putLong("DeathTime",700); - ListNBT equipment=new ListNBT(); - for(int i=0;i<48;i++) {CompoundNBT item=new CompoundNBT();item.putString("id","minecraft:diamond");item.putByte("Count",(byte)(i+1));item.putShort("Damage",(short)i);item.putByte("Slot",(byte)i);CompoundNBT custom=new CompoundNBT();custom.putString("keepsake","slot-"+i);item.put("tag",custom);equipment.add(item);} + ListTag equipment=new ListTag(); + for(int i=0;i<48;i++) {CompoundTag item=new CompoundTag();item.putString("id","minecraft:diamond");item.putByte("Count",(byte)(i+1));item.putShort("Damage",(short)i);item.putByte("Slot",(byte)i);CompoundTag custom=new CompoundTag();custom.putString("keepsake","slot-"+i);item.put("tag",custom);equipment.add(item);} body.put("Equipment",equipment); - CompoundNBT record=new CompoundNBT();record.putString("Source","Equipment");record.put("Data",equipment.getCompound(47).copy()); - ListNBT recovery=new ListNBT();recovery.add(record);body.put("LootableBodiesUnresolved",recovery); + CompoundTag record=new CompoundTag();record.putString("Source","Equipment");record.put("Data",equipment.getCompound(47).copy()); + ListTag recovery=new ListTag();recovery.add(record);body.put("LootableBodiesUnresolved",recovery); return body; } - private Path region(String dimension,CompoundNBT... bodies) throws Exception { + private Path region(String dimension,CompoundTag... bodies) throws Exception { return regionVersion(dimension,512,bodies); } - private Path regionVersion(String dimension,int version,CompoundNBT... bodies) throws Exception { - CompoundNBT chunk=new CompoundNBT(),level=new CompoundNBT();if(version>=0)chunk.putInt("DataVersion",version);chunk.put("Level",level);level.putInt("xPos",0);level.putInt("zPos",0); - ListNBT entities=new ListNBT();for(CompoundNBT body:bodies)entities.add(body.copy());level.put("Entities",entities); + private Path regionVersion(String dimension,int version,CompoundTag... bodies) throws Exception { + CompoundTag chunk=new CompoundTag(),level=new CompoundTag();if(version>=0)chunk.putInt("DataVersion",version);chunk.put("Level",level);level.putInt("xPos",0);level.putInt("zPos",0); + ListTag entities=new ListTag();for(CompoundTag body:bodies)entities.add(body.copy());level.put("Entities",entities); ByteArrayOutputStream bytes=new ByteArrayOutputStream(); - try(DataOutputStream compressed=new DataOutputStream(new DeflaterOutputStream(bytes))){CompressedStreamTools.write(chunk,compressed);} + try(DataOutputStream compressed=new DataOutputStream(new DeflaterOutputStream(bytes))){NbtIo.write(chunk,compressed);} int sectors=(bytes.size()+5+4095)/4096;byte[] contents=new byte[8192+sectors*4096]; java.nio.ByteBuffer buffer=java.nio.ByteBuffer.wrap(contents);buffer.putInt((2<<8)|sectors);buffer.position(8192);buffer.putInt(bytes.size()+1);buffer.put((byte)2);buffer.put(bytes.toByteArray()); Path region=world.resolve(dimension).resolve("region/r.0.0.mca");Files.createDirectories(region.getParent());Files.write(region,contents);return region; } - private byte[] encoded(CompoundNBT nbt) throws Exception {ByteArrayOutputStream bytes=new ByteArrayOutputStream();CompressedStreamTools.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} + private byte[] encoded(CompoundTag nbt) throws Exception {ByteArrayOutputStream bytes=new ByteArrayOutputStream();NbtIo.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} @Test void native112VersionIsNotDowngradedTo111() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1343,b); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1343,b); assertEquals(1343,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); } @Test void native116MarkerOverridesMissingAndOlderOuterStamps() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",6); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",6); regionVersion("",512,b); - CompoundNBT saved=original(scan(),ID,0,0); + CompoundTag saved=original(scan(),ID,0,0); assertEquals(2586,saved.getCompound("SourceVersions").getInt("DataVersion")); assertArrayEquals(encoded(b),encoded(saved.getCompound("Entity"))); } @Test void released116MetadataIdentifiesUnstampedNativeItems() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); - CompoundNBT root=new CompoundNBT(),fml=new CompoundNBT(),mod=new CompoundNBT(); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); + CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag(); mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.7.116051"); - ListNBT mods=new ListNBT();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundNBT()); - try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){CompressedStreamTools.writeCompressed(root,output);} + ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); + try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} assertEquals(2586,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); } @Test void ambiguousItemContextsIncludeTheNative116Schema() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); int[] versions=original(scan(),ID,0,0).getCompound("SourceVersions").getIntArray("CandidateDataVersions"); assertTrue(java.util.Arrays.stream(versions).anyMatch(version->version==2586)); } @Test void nativeLowercaseFmlIdentifiesUnstampedItemsWithoutChangingOriginals() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); - CompoundNBT root=new CompoundNBT(),fml=new CompoundNBT(),mod=new CompoundNBT(); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); + CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag(); mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.6.115021"); - ListNBT mods=new ListNBT();mods.add(mod);fml.put("LoadingModList",mods);root.put("fml",fml);root.put("Data",new CompoundNBT()); - try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){CompressedStreamTools.writeCompressed(root,output);} + ListTag mods=new ListTag();mods.add(mod);fml.put("LoadingModList",mods);root.put("fml",fml);root.put("Data",new CompoundTag()); + try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} byte[] header=Files.readAllBytes(world.resolve("level.dat")); - CompoundNBT preserved=original(scan(),ID,0,0),context=preserved.getCompound("SourceVersions"); + CompoundTag preserved=original(scan(),ID,0,0),context=preserved.getCompound("SourceVersions"); assertEquals(2230,context.getInt("DataVersion"));assertFalse(context.getBoolean("Ambiguous")); assertEquals(fml,context.getCompound("WorldFML")); assertArrayEquals(encoded(b),encoded(preserved.getCompound("Entity"))); assertArrayEquals(header,Files.readAllBytes(world.resolve("level.dat"))); } @Test void native113MarkerAndStampSurvivePreservation() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",3);regionVersion("",1631,b); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",3);regionVersion("",1631,b); assertEquals(1631,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); assertNotNull(original(scan(),ID,0,0)); } @Test void released112MarkerDoesNotBorrowAFalselyNative113Stamp() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",2);regionVersion("",1631,b); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",2);regionVersion("",1631,b); assertEquals(1343,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); } @Test void native113MarkerDisambiguatesMissingMinecraftStamps() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",3);regionVersion("",-1,b); - CompoundNBT versions=original(scan(),ID,0,0).getCompound("SourceVersions"); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",3);regionVersion("",-1,b); + CompoundTag versions=original(scan(),ID,0,0).getCompound("SourceVersions"); assertEquals(1631,versions.getInt("DataVersion"));assertFalse(versions.getBoolean("Ambiguous")); } @Test void actual112WriterMetadataRejectsAFalselyFlattenedStamp() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1631,b); - CompoundNBT root=new CompoundNBT(),fml=new CompoundNBT(),mod=new CompoundNBT();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.0");ListNBT mods=new ListNBT();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundNBT()); - try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){CompressedStreamTools.writeCompressed(root,output);} + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1631,b); + CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.0");ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); + try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} int source=original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion");assertTrue(source>=1139 && source<=1343); } @Test void corpsesSavedAsVanillaVehiclePassengersArePreserved() throws Exception { - CompoundNBT cart=new CompoundNBT();cart.putString("id","minecraft:minecart");cart.putUniqueId("UUID",UUID.fromString("00000000-1111-4111-8111-000000000099")); - ListNBT riders=new ListNBT();riders.add(body(ID));cart.put("Passengers",riders);region("",cart); - CompoundNBT saved=original(scan(),ID,0,0);assertNotNull(saved);assertArrayEquals(encoded(body(ID)),encoded(saved.getCompound("Entity"))); + CompoundTag cart=new CompoundTag();cart.putString("id","minecraft:minecart");cart.putUUID("UUID",UUID.fromString("00000000-1111-4111-8111-000000000099")); + ListTag riders=new ListTag();riders.add(body(ID));cart.put("Passengers",riders);region("",cart); + CompoundTag saved=original(scan(),ID,0,0);assertNotNull(saved);assertArrayEquals(encoded(body(ID)),encoded(saved.getCompound("Entity"))); region("",cart.copy());assertNotNull(original(scan(),ID,0,0)); cart.remove("Passengers");region("",cart);assertNull(original(scan(),ID,0,0)); } @Test void current112MarkerCanBePreservedAndReloaded() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",2);regionVersion("",1343,b); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",2);regionVersion("",1343,b); assertNotNull(original(scan(),ID,0,0));assertNotNull(original(scan(),ID,0,0)); } @Test void nativeIdentityWithoutAnyVersionEvidenceIsExplicitlyAmbiguous() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); assertTrue(original(scan(),ID,0,0).getCompound("SourceVersions").getBoolean("Ambiguous")); } @Test void old111MarkerProvesItsOwnSourceDespiteAnIncorrectNewChunkStamp() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",1);regionVersion("",1343,b); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",1);regionVersion("",1343,b); assertEquals(922,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); } @Test void rootFmlMetadataDistinguishesAnUnstampedOriginal112World() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); - CompoundNBT root=new CompoundNBT(),fml=new CompoundNBT(),mod=new CompoundNBT();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.0");ListNBT mods=new ListNBT();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundNBT()); - try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){CompressedStreamTools.writeCompressed(root,output);} - CompoundNBT context=original(scan(),ID,0,0).getCompound("SourceVersions");assertTrue(context.getInt("DataVersion")>=1139);assertFalse(context.getBoolean("Ambiguous"));assertEquals(fml,context.getCompound("WorldFML")); + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); + CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.0");ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); + try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} + CompoundTag context=original(scan(),ID,0,0).getCompound("SourceVersions");assertTrue(context.getInt("DataVersion")>=1139);assertFalse(context.getBoolean("Ambiguous"));assertEquals(fml,context.getCompound("WorldFML")); } @Test void unmarkedOriginal111CorpseWithFalselyNewStampStillUses111ItemRules() throws Exception { - CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1343,b); - CompoundNBT root=new CompoundNBT(),fml=new CompoundNBT(),mod=new CompoundNBT();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.3.0");ListNBT mods=new ListNBT();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundNBT()); - try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){CompressedStreamTools.writeCompressed(root,output);} + CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1343,b); + CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.3.0");ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); + try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} assertEquals(922,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); } @Test void stampOnlyChangesRetainPreviouslyVerifiedChunkVersion() throws Exception { - CompoundNBT b=body(ID);regionVersion("",512,b);scan(); + CompoundTag b=body(ID);regionVersion("",512,b);scan(); regionVersion("",-1,b); - CompoundNBT versions=original(scan(),ID,0,0).getCompound("SourceVersions"); + CompoundTag versions=original(scan(),ID,0,0).getCompound("SourceVersions"); assertEquals(512,versions.getInt("VerifiedChunkDataVersion")); } @Test void changedChunkPayloadCannotReusePreviousVersionEvidence() throws Exception { - CompoundNBT b=body(ID);regionVersion("",512,b);scan(); - CompoundNBT added=new CompoundNBT();added.putString("id","minecraft:pig"); + CompoundTag b=body(ID);regionVersion("",512,b);scan(); + CompoundTag added=new CompoundTag();added.putString("id","minecraft:pig"); regionVersion("",-1,b,added); assertFalse(original(scan(),ID,0,0).getCompound("SourceVersions").contains("VerifiedChunkDataVersion")); } private List snapshots() throws Exception {try(java.util.stream.Stream files=Files.walk(world.resolve("data/lootablebodies-upgrade/snapshots"))){List result=new ArrayList<>();files.filter(p->p.toString().endsWith(".nbt")).forEach(result::add);return result;}} @Test void preservesCompleteTypedPayloadWithoutWritingSourceRegions() throws Exception { - CompoundNBT body=body(ID);Path region=region("",body);byte[] before=Files.readAllBytes(region);java.nio.file.attribute.FileTime stamp=Files.getLastModifiedTime(region); - Object journal=scan();CompoundNBT saved=original(journal,ID,0,0); + CompoundTag body=body(ID);Path region=region("",body);byte[] before=Files.readAllBytes(region);java.nio.file.attribute.FileTime stamp=Files.getLastModifiedTime(region); + Object journal=scan();CompoundTag saved=original(journal,ID,0,0); assertArrayEquals(encoded(body),encoded(saved.getCompound("Entity")));assertEquals(48,saved.getCompound("Entity").getList("Equipment",10).size()); assertEquals(512,saved.getCompound("SourceVersions").getInt("DataVersion")); assertArrayEquals(before,Files.readAllBytes(region));assertEquals(stamp,Files.getLastModifiedTime(region)); @@ -159,7 +161,7 @@ private Path regionVersion(String dimension,int version,CompoundNBT... bodies) t assertNull(original(journal,ID,0,0));assertNotNull(original(journal,replacement,0,0));assertTrue(snapshots().containsAll(first));assertEquals(2,snapshots().size()); } @Test void staleIndexCannotOverwriteNewInventory() throws Exception { - CompoundNBT body=body(ID);region("",body);scan();Path index=world.resolve("data/lootablebodies-upgrade/index.nbt");byte[] stale=Files.readAllBytes(index); + CompoundTag body=body(ID);region("",body);scan();Path index=world.resolve("data/lootablebodies-upgrade/index.nbt");byte[] stale=Files.readAllBytes(index); body.getList("Equipment",10).getCompound(40).putByte("Count",(byte)99);region("",body);scan();Files.write(index,stale); assertArrayEquals(encoded(body),encoded(original(scan(),ID,0,0).getCompound("Entity"))); } @@ -171,17 +173,17 @@ private Path regionVersion(String dimension,int version,CompoundNBT... bodies) t @Test void corruptIndexAbortsPreservation() throws Exception {region("",body(ID));scan();Files.write(world.resolve("data/lootablebodies-upgrade/index.nbt"),new byte[]{0,1,2});assertThrows(IOException.class,()->scan());} @Test void readableIndexDamageCannotSilentlyOmitPreservedCorpses() throws Exception { region("",body(ID));scan();Path path=world.resolve("data/lootablebodies-upgrade/index.nbt"); - CompoundNBT index=CompressedStreamTools.readCompressed(Files.newInputStream(path)); + CompoundTag index=NbtIo.readCompressed(Files.newInputStream(path)); if(index.contains("Payload",7)) {byte[] payload=index.getByteArray("Payload");payload[payload.length/2]^=1;index.putByteArray("Payload",payload);} - else index.getList("Regions",10).getCompound(0).put("Snapshots",new ListNBT()); - try(OutputStream output=Files.newOutputStream(path)){CompressedStreamTools.writeCompressed(index,output);} + else index.getList("Regions",10).getCompound(0).put("Snapshots",new ListTag()); + try(OutputStream output=Files.newOutputStream(path)){NbtIo.writeCompressed(index,output);} assertThrows(IOException.class,()->scan(),"A readable damaged index must not be accepted as a complete scan"); } @Test void truncatedRegionAbortsBeforeLoading() throws Exception {Path region=region("",body(ID));Files.write(region,new byte[8193]);assertThrows(IOException.class,()->scan());} @Test void nativeEmptyRegionPlaceholdersRemainReadOnlyAndAreScannedWhenPopulated()throws Exception { Path folder=world.resolve("region");Files.createDirectories(folder);Path file=folder.resolve("r.0.0.mca"); - try(net.minecraft.world.chunk.storage.RegionFile nativeRegion=new net.minecraft.world.chunk.storage.RegionFile(file.toFile(),folder.toFile(),false)) { - assertNull(nativeRegion.func_222666_a(new net.minecraft.util.math.ChunkPos(0,0))); + try(net.minecraft.world.level.chunk.storage.RegionFile nativeRegion=new net.minecraft.world.level.chunk.storage.RegionFile(file.toFile(),folder.toFile(),false)) { + assertNull(nativeRegion.getChunkDataInputStream(new net.minecraft.world.level.ChunkPos(0,0))); } assertEquals(0,Files.size(file));assertNull(original(scan(),ID,0,0));assertEquals(0,Files.size(file)); region("",body(ID));assertNotNull(original(scan(),ID,0,0)); @@ -192,19 +194,19 @@ private Path regionVersion(String dimension,int version,CompoundNBT... bodies) t @Test void chunkCoordinateMismatchCannotApplyAnOldPayload() throws Exception {region("",body(ID));assertNull(original(scan(),ID,32,0));} @Test void retainsExactCompressedChunkAsWellAsDecodedCorpse() throws Exception { Path region=region("",body(ID));byte[] source=Files.readAllBytes(region);int size=java.nio.ByteBuffer.wrap(source,8192,4).getInt(); - byte[] originalChunk=Arrays.copyOfRange(source,8197,8192+4+size);CompoundNBT snapshot=original(scan(),ID,0,0); + byte[] originalChunk=Arrays.copyOfRange(source,8197,8192+4+size);CompoundTag snapshot=original(scan(),ID,0,0); assertTrue(snapshot.contains("OriginalChunk",8),"A decoded NBT copy cannot retain every original byte encoding"); assertEquals(2,snapshot.getByte("OriginalCompression"));assertEquals(0,snapshot.getInt("EntityIndex")); assertArrayEquals(originalChunk,Files.readAllBytes(world.resolve("data/lootablebodies-upgrade/chunks/"+snapshot.getString("OriginalChunk")+".bin"))); } @Test void diskCannotSupplyAProcessedTemporaryPayload() throws Exception { - CompoundNBT body=body(ID),fake=body(ID);fake.putInt("LootableBodiesDataVersion",1);fake.put("Equipment",new ListNBT());body.put("LootableBodiesUpgradePayload",fake); + CompoundTag body=body(ID),fake=body(ID);fake.putInt("LootableBodiesDataVersion",1);fake.put("Equipment",new ListTag());body.put("LootableBodiesUpgradePayload",fake); Path region=region("",body);byte[] before=Files.readAllBytes(region); IOException failure=assertThrows(IOException.class,()->scan());assertTrue(failure.getMessage().contains("intermediate"));assertArrayEquals(before,Files.readAllBytes(region)); assertFalse(snapshots().isEmpty(),"Unsafe input must still have a forensic original"); } @Test void unsupportedFutureCorpseFormatsStopBeforeWorldLoading()throws Exception { - CompoundNBT corpse=body(ID);corpse.putString("id","lootablebodies:corpse");corpse.putInt("LootableBodiesDataVersion",99);Path file=region("",corpse);byte[] before=Files.readAllBytes(file); + CompoundTag corpse=body(ID);corpse.putString("id","lootablebodies:corpse");corpse.putInt("LootableBodiesDataVersion",99);Path file=region("",corpse);byte[] before=Files.readAllBytes(file); assertThrows(IOException.class,()->scan());assertArrayEquals(before,Files.readAllBytes(file)); assertTrue(Files.list(world.resolve("data/lootablebodies-upgrade/snapshots")).findAny().isPresent(),"Original future data must be preserved before stopping"); } diff --git a/src/test/java/cyano/lootable/network/LoginReadGuardTest.java b/src/test/java/cyano/lootable/network/LoginReadGuardTest.java index d624a0d..5d8894b 100644 --- a/src/test/java/cyano/lootable/network/LoginReadGuardTest.java +++ b/src/test/java/cyano/lootable/network/LoginReadGuardTest.java @@ -8,8 +8,7 @@ import java.io.*; import java.net.*; import java.util.concurrent.*; -import net.minecraft.network.ProtocolType; -import net.minecraft.network.NetworkManager; +import net.minecraft.network.ConnectionProtocol; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -44,7 +43,7 @@ protected void channelRead0(ChannelHandlerContext context,ByteBuf input) { } }).connect(server.getLocalSocketAddress()).sync().channel(); peer=accepted.get(5,TimeUnit.SECONDS); - channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).set(ProtocolType.LOGIN); + channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).set(ConnectionProtocol.LOGIN); drain(); } void drain()throws Exception {channel.eventLoop().submit(()->{}).get(5,TimeUnit.SECONDS);} @@ -85,10 +84,10 @@ public void close()throws Exception { } } @Test void otherProtocolStatesAreNotChanged()throws Exception { - for(ProtocolType state:new ProtocolType[]{ProtocolType.HANDSHAKING,ProtocolType.STATUS,ProtocolType.PLAY}) { + for(ConnectionProtocol state:new ConnectionProtocol[]{ConnectionProtocol.HANDSHAKING,ConnectionProtocol.STATUS,ConnectionProtocol.PLAY}) { try(Connection c=new Connection()) { - c.stall();c.channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).set(state);guard(c.channel);c.drain(); - assertEquals(0,c.channel.readInterest());assertEquals(state,c.channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).get()); + c.stall();c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).set(state);guard(c.channel);c.drain(); + assertEquals(0,c.channel.readInterest());assertEquals(state,c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).get()); } } } @@ -117,9 +116,9 @@ public void close()throws Exception { try {if(!resume.await(5,TimeUnit.SECONDS))throw new AssertionError("Fixture synchronization timed out");} catch(InterruptedException failure){throw new AssertionError(failure);} }); - try {assertTrue(entered.await(5,TimeUnit.SECONDS));guard(c.channel);c.channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).set(ProtocolType.PLAY);} + try {assertTrue(entered.await(5,TimeUnit.SECONDS));guard(c.channel);c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).set(ConnectionProtocol.PLAY);} finally {resume.countDown();} - c.drain();assertEquals(0,c.channel.readInterest());assertEquals(ProtocolType.PLAY,c.channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).get()); + c.drain();assertEquals(0,c.channel.readInterest());assertEquals(ConnectionProtocol.PLAY,c.channel.attr(net.minecraft.network.Connection.ATTRIBUTE_PROTOCOL).get()); } } } diff --git a/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java new file mode 100644 index 0000000..24b977e --- /dev/null +++ b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java @@ -0,0 +1,59 @@ +package cyano.lootable.network; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.minecraft.MinecraftSessionService; +import com.mojang.authlib.properties.Property; +import java.lang.reflect.Proxy; +import java.nio.file.Path; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; +import net.minecraft.server.players.GameProfileCache; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.*; + +class Native117ProfileLookupTest { + @TempDir Path directory; + final GameProfile initial=new GameProfile(null,"Owner"); + GameProfile cached=new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000201"),"Owner"); + GameProfile added; + GameProfileCache cache(boolean broken) { + return new GameProfileCache(null,directory.resolve("unused.json").toFile()) { + @Override public Optional get(String name) { + if(broken)throw new IllegalStateException("injected cache failure"); + return Optional.of(cached); + } + @Override public void add(GameProfile value) {added=value;} + }; + } + MinecraftSessionService service(UnaryOperator action) { + return (MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class}, + (proxy,method,args)->{ + if(method.getName().equals("fillProfileProperties"))return action.apply((GameProfile)args[0]); + throw new AssertionError("Unexpected native service "+method.getName()); + }); + } + @Test void cacheFailureKeepsTheOriginalFallback() { + assertSame(initial,CorpseProfiles.resolve(initial,cache(true),service(profile->{throw new AssertionError("Service must not run");}))); + } + @Test void sessionFailureKeepsTheOriginalFallback() { + assertSame(initial,CorpseProfiles.resolve(initial,cache(false),service(profile->{throw new IllegalStateException("injected service failure");}))); + assertNull(added); + } + @Test void resolvedProfileKeepsNativeIdentityAndTexturesWithoutMutatingCachedInput() { + GameProfile resolved=CorpseProfiles.resolve(initial,cache(false),service(profile->{ + profile.getProperties().put("textures",new Property("textures","native-fixture"));return profile; + })); + assertEquals(cached.getId(),resolved.getId());assertEquals("Owner",resolved.getName()); + assertEquals("native-fixture",resolved.getProperties().get("textures").iterator().next().getValue()); + assertFalse(cached.getProperties().containsKey("textures"));assertSame(resolved,added); + } + @Test void warmTexturesSkipTheServiceAndMissingServicesKeepFallback() { + cached.getProperties().put("textures",new Property("textures","cached"));AtomicInteger requests=new AtomicInteger(); + GameProfile resolved=CorpseProfiles.resolve(initial,cache(false),service(profile->{requests.incrementAndGet();return profile;})); + assertEquals(0,requests.get());assertEquals(cached.getId(),resolved.getId()); + assertSame(initial,CorpseProfiles.resolve(initial,null,null)); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java index 365d480..379a22a 100644 --- a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java +++ b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java @@ -3,9 +3,10 @@ import cyano.lootable.buildtest.NativeVanillaDataFixer; import java.io.*; import java.nio.file.*; -import net.minecraft.nbt.*; -import net.minecraft.world.storage.SaveFormat; -import net.minecraftforge.fml.WorldPersistenceHooks; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraftforge.fmllegacy.WorldPersistenceHooks; import org.junit.jupiter.api.*; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; @@ -13,19 +14,19 @@ class EarlyWorldPreservationTest { @TempDir Path directory; @AfterEach void close(){UpgradePreservation.close();} - private CompoundNBT level(int version) { - CompoundNBT root=new CompoundNBT(),data=new CompoundNBT();data.putInt("DataVersion",version);root.put("Data",data); - CompoundNBT owned=new CompoundNBT();owned.putString("unknown","retained world metadata");root.put("lootablebodies",owned);return root; + private CompoundTag level(int version) { + CompoundTag root=new CompoundTag(),data=new CompoundTag();data.putInt("DataVersion",version);root.put("Data",data); + CompoundTag owned=new CompoundTag();owned.putString("unknown","retained world metadata");root.put("lootablebodies",owned);return root; } - private void write(Path world,CompoundNBT root)throws Exception { - Files.createDirectories(world);try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){CompressedStreamTools.writeCompressed(root,output);} + private void write(Path world,CompoundTag root)throws Exception { + Files.createDirectories(world);try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} } @Test void nativeReadHookAbortsBeforeFallbackOrLevelWrites()throws Exception { - Path world=directory.resolve("fixture");CompoundNBT root=level(2230);write(world,root); + Path world=directory.resolve("fixture");CompoundTag root=level(2230);write(world,root); Path journal=world.resolve("data/lootablebodies-upgrade");Files.createDirectories(journal);Files.write(journal.resolve("index.nbt"),new byte[]{1,2,3}); byte[] before=Files.readAllBytes(world.resolve("level.dat"));EarlyWorldPreservation.install(); - SaveFormat format=new SaveFormat(directory,directory.resolve("backups"),NativeVanillaDataFixer.get()); - try(SaveFormat.LevelSave save=format.getLevelSave("fixture")) { + LevelStorageSource format=new LevelStorageSource(directory,directory.resolve("backups"),NativeVanillaDataFixer.get()); + try(LevelStorageSource.LevelStorageAccess save=format.createAccess("fixture")) { Error failure=assertThrows(Error.class,()->WorldPersistenceHooks.handleWorldDataLoad(save,null,root)); assertTrue(failure.getMessage().contains("Lootable Bodies could not preserve this world's corpses")); } @@ -33,23 +34,23 @@ private void write(Path world,CompoundNBT root)throws Exception { assertArrayEquals(new byte[]{1,2,3},Files.readAllBytes(journal.resolve("index.nbt"))); } @Test void startupKeepsItsEarlySourceContextAndUnknownOwnedMetadata()throws Exception { - Path world=directory.resolve("fixture");CompoundNBT root=level(2230);write(world,root);EarlyWorldPreservation.install(); - SaveFormat format=new SaveFormat(directory,directory.resolve("backups"),NativeVanillaDataFixer.get()); - try(SaveFormat.LevelSave save=format.getLevelSave("fixture")) { + Path world=directory.resolve("fixture");CompoundTag root=level(2230);write(world,root);EarlyWorldPreservation.install(); + LevelStorageSource format=new LevelStorageSource(directory,directory.resolve("backups"),NativeVanillaDataFixer.get()); + try(LevelStorageSource.LevelStorageAccess save=format.createAccess("fixture")) { WorldPersistenceHooks.handleWorldDataLoad(save,null,root); UpgradeJournal early=UpgradePreservation.journal();assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); write(world,level(2586));assertSame(early,UpgradePreservation.prepareForStart(world)); assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); - CompoundNBT output=new CompoundNBT();WorldPersistenceHooks.handleWorldDataSave(save,null,output); + CompoundTag output=new CompoundTag();WorldPersistenceHooks.handleWorldDataSave(save,null,output); assertEquals(root.get("lootablebodies"),output.get("lootablebodies")); output.getCompound("lootablebodies").putString("unknown","changed copy"); - CompoundNBT again=new CompoundNBT();WorldPersistenceHooks.handleWorldDataSave(save,null,again); + CompoundTag again=new CompoundTag();WorldPersistenceHooks.handleWorldDataSave(save,null,again); assertEquals(root.get("lootablebodies"),again.get("lootablebodies")); } } @Test void nativeSessionLockOnlyOverwritesItsThreeByteMarker()throws Exception { byte[] old=new byte[]{0,0,1,2,3,4,5,6};Files.write(directory.resolve("session.lock"),old); - try(net.minecraft.server.SessionLockManager ignored=net.minecraft.server.SessionLockManager.func_232998_a_(directory)) { + try(net.minecraft.util.DirectoryLock ignored=net.minecraft.util.DirectoryLock.create(directory)) { assertTrue(Files.isRegularFile(directory.resolve("session.lock"))); } // Windows denies a separate reader while this native lock is held. diff --git a/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java b/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java index 823bc2c..bae0e3e 100644 --- a/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java +++ b/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java @@ -4,10 +4,10 @@ import java.lang.reflect.Field; import java.util.*; import java.util.concurrent.*; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.world.storage.IServerConfiguration; -import net.minecraft.world.storage.SaveFormat; -import net.minecraftforge.fml.WorldPersistenceHooks; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.storage.WorldData; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraftforge.fmllegacy.WorldPersistenceHooks; import org.junit.jupiter.api.Test; import org.objectweb.asm.*; import static org.junit.jupiter.api.Assertions.*; @@ -18,8 +18,8 @@ private static final class Hook implements WorldPersistenceHooks.WorldPersistenc private final String id; Hook(String id){this.id=id;} public String getModId(){return id;} - public void readData(SaveFormat.LevelSave save,IServerConfiguration info,CompoundNBT tag){} - public CompoundNBT getDataForWriting(SaveFormat.LevelSave save,IServerConfiguration info){return new CompoundNBT();} + public void readData(LevelStorageSource.LevelStorageAccess save,WorldData info,CompoundTag tag){} + public CompoundTag getDataForWriting(LevelStorageSource.LevelStorageAccess save,WorldData info){return new CompoundTag();} } @Test void serialRegistrationKeepsEveryNativeHookAndRemainsIdempotent() throws Exception { Field registry=WorldPersistenceHooks.class.getDeclaredField("worldPersistenceHooks");registry.setAccessible(true); @@ -35,8 +35,8 @@ public void readData(SaveFormat.LevelSave save,IServerConfiguration info,Compoun assertEquals(3,hooks.size());assertFalse(hooks.contains(null)); Set ids=new HashSet<>();for(WorldPersistenceHooks.WorldPersistenceHook hook:hooks)ids.add(hook.getModId()); assertEquals(new HashSet<>(Arrays.asList("forge","fml","lootablebodies")),ids); - CompoundNBT saved=new CompoundNBT();WorldPersistenceHooks.handleWorldDataSave(null,null,saved); - assertEquals(ids,saved.keySet()); + CompoundTag saved=new CompoundTag();WorldPersistenceHooks.handleWorldDataSave(null,null,saved); + assertEquals(ids,saved.getAllKeys()); } } finally {registry.set(null,original);installed.setBoolean(null,wasInstalled);} } diff --git a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java index 9de06ab..e8a0f8a 100644 --- a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java +++ b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java @@ -1,66 +1,69 @@ package cyano.lootable.upgrade; import java.io.*; -import net.minecraft.nbt.*; -import net.minecraft.util.datafix.DataFixesManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixers; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; /** Actual native item-fixer contracts; registry resolution is tested in Forge. */ class ItemNbtConversionTest { - @BeforeEach void pipeline(){ItemMigration.register(UpgradePreservation.wrapper(DataFixesManager.getDataFixer()));} - private CompoundNBT context(int version){CompoundNBT nbt=new CompoundNBT();nbt.putInt("DataVersion",version);return nbt;} - private CompoundNBT item(String id,int damage){CompoundNBT nbt=new CompoundNBT();nbt.putString("id",id);nbt.putByte("Count",(byte)7);nbt.putShort("Damage",(short)damage);nbt.putByte("Slot",(byte)47);CompoundNBT custom=new CompoundNBT();custom.putString("keepsake","saved");nbt.put("tag",custom);return nbt;} - private byte[] bytes(CompoundNBT nbt)throws Exception{ByteArrayOutputStream bytes=new ByteArrayOutputStream();CompressedStreamTools.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} + @BeforeEach void pipeline(){ItemMigration.register(UpgradePreservation.wrapper(DataFixers.getDataFixer()));} + private CompoundTag context(int version){CompoundTag nbt=new CompoundTag();nbt.putInt("DataVersion",version);return nbt;} + private CompoundTag item(String id,int damage){CompoundTag nbt=new CompoundTag();nbt.putString("id",id);nbt.putByte("Count",(byte)7);nbt.putShort("Damage",(short)damage);nbt.putByte("Slot",(byte)47);CompoundTag custom=new CompoundTag();custom.putString("keepsake","saved");nbt.put("tag",custom);return nbt;} + private byte[] bytes(CompoundTag nbt)throws Exception{ByteArrayOutputStream bytes=new ByteArrayOutputStream();NbtIo.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} @Test void blockVariantsFlattenWithoutLosingTheirCountSlotOrCustomTag()throws Exception { - CompoundNBT raw=item("minecraft:stone",1);byte[] before=bytes(raw);CompoundNBT result=ItemMigration.convertNbt(raw,context(512));assertNotNull(result);assertEquals("minecraft:granite",result.getString("id"));assertEquals(7,result.getByte("Count"));assertEquals(47,result.getByte("Slot"));assertEquals("saved",result.getCompound("tag").getString("keepsake"));assertArrayEquals(before,bytes(raw)); + CompoundTag raw=item("minecraft:stone",1);byte[] before=bytes(raw);CompoundTag result=ItemMigration.convertNbt(raw,context(512));assertNotNull(result);assertEquals("minecraft:granite",result.getString("id"));assertEquals(7,result.getByte("Count"));assertEquals(47,result.getByte("Slot"));assertEquals("saved",result.getCompound("tag").getString("keepsake"));assertArrayEquals(before,bytes(raw)); } @Test void durabilityAndEnchantmentsUseTheirNativeFields() { - CompoundNBT raw=item("minecraft:diamond_sword",123);CompoundNBT enchantment=new CompoundNBT();enchantment.putShort("id",(short)16);enchantment.putShort("lvl",(short)3);ListNBT enchantments=new ListNBT();enchantments.add(enchantment);raw.getCompound("tag").put("ench",enchantments); - CompoundNBT result=ItemMigration.convertNbt(raw,context(1343));assertNotNull(result);assertEquals(123,result.getCompound("tag").getInt("Damage"));CompoundNBT sharpness=result.getCompound("tag").getList("Enchantments",10).getCompound(0);assertEquals("minecraft:sharpness",sharpness.getString("id"));assertEquals(3,sharpness.getShort("lvl")); + CompoundTag raw=item("minecraft:diamond_sword",123);CompoundTag enchantment=new CompoundTag();enchantment.putShort("id",(short)16);enchantment.putShort("lvl",(short)3);ListTag enchantments=new ListTag();enchantments.add(enchantment);raw.getCompound("tag").put("ench",enchantments); + CompoundTag result=ItemMigration.convertNbt(raw,context(1343));assertNotNull(result);assertEquals(123,result.getCompound("tag").getInt("Damage"));CompoundTag sharpness=result.getCompound("tag").getList("Enchantments",10).getCompound(0);assertEquals("minecraft:sharpness",sharpness.getString("id"));assertEquals(3,sharpness.getShort("lvl")); } @Test void nestedInventoriesAndCapabilitiesRemainComplete()throws Exception { - CompoundNBT raw=item("minecraft:blue_shulker_box",0),nested=item("minecraft:stone",1);nested.putByte("Count",(byte)29);nested.putByte("Slot",(byte)3);ListNBT contents=new ListNBT();contents.add(nested);CompoundNBT block=new CompoundNBT();block.putString("id","minecraft:shulker_box");block.put("Items",contents);raw.getCompound("tag").put("BlockEntityTag",block);CompoundNBT caps=new CompoundNBT();caps.putByteArray("retained",new byte[]{1,4,9});raw.put("ForgeCaps",caps); - CompoundNBT result=ItemMigration.convertNbt(raw,context(922));assertNotNull(result);assertArrayEquals(bytes(caps),bytes(result.getCompound("ForgeCaps")));CompoundNBT recovered=result.getCompound("tag").getCompound("BlockEntityTag").getList("Items",10).getCompound(0);assertEquals("minecraft:granite",recovered.getString("id"));assertEquals(29,recovered.getByte("Count"));assertEquals(3,recovered.getByte("Slot")); + CompoundTag raw=item("minecraft:blue_shulker_box",0),nested=item("minecraft:stone",1);nested.putByte("Count",(byte)29);nested.putByte("Slot",(byte)3);ListTag contents=new ListTag();contents.add(nested);CompoundTag block=new CompoundTag();block.putString("id","minecraft:shulker_box");block.put("Items",contents);raw.getCompound("tag").put("BlockEntityTag",block);CompoundTag caps=new CompoundTag();caps.putByteArray("retained",new byte[]{1,4,9});raw.put("ForgeCaps",caps); + CompoundTag result=ItemMigration.convertNbt(raw,context(922));assertNotNull(result);assertArrayEquals(bytes(caps),bytes(result.getCompound("ForgeCaps")));CompoundTag recovered=result.getCompound("tag").getCompound("BlockEntityTag").getList("Items",10).getCompound(0);assertEquals("minecraft:granite",recovered.getString("id"));assertEquals(29,recovered.getByte("Count"));assertEquals(3,recovered.getByte("Slot")); } @Test void currentNativeItemsAreNotFlattenedAgain()throws Exception { - CompoundNBT raw=item("minecraft:granite",0);raw.remove("Damage");assertArrayEquals(bytes(raw),bytes(ItemMigration.convertNbt(raw,context(UpgradePreservation.MINECRAFT_VERSION)))); + CompoundTag raw=item("minecraft:granite",0);raw.remove("Damage");assertArrayEquals(bytes(raw),bytes(ItemMigration.convertNbt(raw,context(UpgradePreservation.MINECRAFT_VERSION)))); } @Test void bedColoursStillUseTheCorrectHistoricalContext() { - CompoundNBT old=ItemMigration.convertNbt(item("minecraft:bed",0),context(922)),native112=ItemMigration.convertNbt(item("minecraft:bed",0),context(1343));assertNotNull(old);assertNotNull(native112);assertEquals("minecraft:red_bed",old.getString("id"));assertEquals("minecraft:white_bed",native112.getString("id")); + CompoundTag old=ItemMigration.convertNbt(item("minecraft:bed",0),context(922)),native112=ItemMigration.convertNbt(item("minecraft:bed",0),context(1343));assertNotNull(old);assertNotNull(native112);assertEquals("minecraft:red_bed",old.getString("id"));assertEquals("minecraft:white_bed",native112.getString("id")); } @Test void ambiguousConversionsRemainRawAndRetriable() { - CompoundNBT 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)); + 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() { - CompoundNBT raw=item("minecraft:stone",1);raw.putString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(2587))); + CompoundTag raw=item("minecraft:stone",1);raw.putString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(2731))); } @Test void unrecognisedMetadataAndEnchantmentsRemainRecoverable() { assertNull(ItemMigration.convertNbt(item("minecraft:stone",99),context(1343))); assertNull(ItemMigration.convertNbt(item("missing:variant",4),context(1343))); - CompoundNBT raw=item("minecraft:diamond_sword",0),enchantment=new CompoundNBT(); + CompoundTag raw=item("minecraft:diamond_sword",0),enchantment=new CompoundTag(); enchantment.putShort("id",(short)200);enchantment.putShort("lvl",(short)5); - ListNBT list=new ListNBT();list.add(enchantment);raw.getCompound("tag").put("ench",list); + ListTag list=new ListTag();list.add(enchantment);raw.getCompound("tag").put("ench",list); assertNull(ItemMigration.convertNbt(raw,context(1343))); } - private CompoundNBT original110(int slot)throws Exception { + private CompoundTag original110(int slot)throws Exception { try(InputStream in=getClass().getResourceAsStream("/legacy-items/original-110.nbt")) { - ListNBT list=CompressedStreamTools.readCompressed(in).getList("Items",10); - for(INBT value:list){CompoundNBT item=(CompoundNBT)value;if(item.getByte("Slot")==slot)return item;} + ListTag list=NbtIo.readCompressed(in).getList("Items",10); + for(Tag value:list){CompoundTag item=(CompoundTag)value;if(item.getByte("Slot")==slot)return item;} }throw new AssertionError("Missing sealed historical item"); } @Test void actualOriginalPigEggKeepsItsTypeAndSavedEntityName()throws Exception { - CompoundNBT raw=original110(6);byte[] before=bytes(raw);CompoundNBT fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); + CompoundTag raw=original110(6);byte[] before=bytes(raw);CompoundTag fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); assertEquals("minecraft:pig_spawn_egg",fixed.getString("id"));assertEquals(raw.getByte("Count"),fixed.getByte("Count"));assertEquals("minecraft:pig",fixed.getCompound("tag").getCompound("EntityTag").getString("id")); - assertEquals("Saved pig egg",net.minecraft.util.text.ITextComponent.Serializer.getComponentFromJson(fixed.getCompound("tag").getCompound("EntityTag").getString("CustomName")).getString());assertArrayEquals(before,bytes(raw)); + assertEquals("Saved pig egg",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("EntityTag").getString("CustomName")).getString());assertArrayEquals(before,bytes(raw)); } @Test void actualOriginalChestKeepsItsNestedInventoryInNativeFormat()throws Exception { - CompoundNBT raw=original110(7),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); - CompoundNBT nested=fixed.getCompound("tag").getCompound("BlockEntityTag");assertEquals("minecraft:chest",nested.getString("id"));assertEquals("minecraft:emerald",nested.getList("Items",10).getCompound(0).getString("id"));assertEquals(17,nested.getList("Items",10).getCompound(0).getByte("Count"));assertFalse(fixed.contains("Damage")); - assertEquals("Original keepsake 7",net.minecraft.util.text.ITextComponent.Serializer.getComponentFromJson(fixed.getCompound("tag").getCompound("display").getString("Name")).getString()); + CompoundTag raw=original110(7),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); + CompoundTag nested=fixed.getCompound("tag").getCompound("BlockEntityTag");assertEquals("minecraft:chest",nested.getString("id"));assertEquals("minecraft:emerald",nested.getList("Items",10).getCompound(0).getString("id"));assertEquals(17,nested.getList("Items",10).getCompound(0).getByte("Count"));assertFalse(fixed.contains("Damage")); + assertEquals("Original keepsake 7",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("display").getString("Name")).getString()); } @Test void actualOriginalBannerKeepsItsBaseColour()throws Exception { - CompoundNBT raw=original110(8),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed);assertEquals("minecraft:purple_banner",fixed.getString("id"));assertEquals(raw.getByte("Count"),fixed.getByte("Count"));// Vanilla 1.13 leaves its inverted, redundant Base field behind. + CompoundTag raw=original110(8),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed);assertEquals("minecraft:purple_banner",fixed.getString("id"));assertEquals(raw.getByte("Count"),fixed.getByte("Count"));// Vanilla 1.13 leaves its inverted, redundant Base field behind. assertEquals(10,fixed.getCompound("tag").getCompound("BlockEntityTag").getInt("Base")); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java index a3a155f..4d3290e 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java @@ -1,9 +1,9 @@ package cyano.lootable.upgrade; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.TypeReferences; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -14,8 +14,8 @@ class Native114RegistryRenameTest { ResourceLocation old = new ResourceLocation("minecraft", names[0]); ResourceLocation current = new ResourceLocation("minecraft", names[1]); assertEquals(current, LegacyRegistryMappings.itemReplacement(old)); - CompoundNBT input = new CompoundNBT(); input.putString("id", old.toString()); input.putByte("Count", (byte)11); - CompoundNBT converted = NativeNbt.update(DataFixesManager.getDataFixer(), TypeReferences.ITEM_STACK, input, 1631, 1976); + CompoundTag input = new CompoundTag(); input.putString("id", old.toString()); input.putByte("Count", (byte)11); + CompoundTag converted = NativeNbt.update(DataFixers.getDataFixer(), References.ITEM_STACK, input, 1631, 1976); assertEquals(current.toString(), converted.getString("id")); assertEquals(11, converted.getByte("Count")); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java index adb03af..8167ba2 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java @@ -1,45 +1,46 @@ package cyano.lootable.upgrade; import java.util.UUID; -import net.minecraft.nbt.*; -import net.minecraft.util.datafix.DataFixesManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.util.datafix.DataFixers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class Native114UpgradeTest { @AfterEach void close() { UpgradePreservation.close(); } - private CompoundNBT context(int version) { - CompoundNBT context = new CompoundNBT(); context.putInt("DataVersion", version); return context; + private CompoundTag context(int version) { + CompoundTag context = new CompoundTag(); context.putInt("DataVersion", version); return context; } - private CompoundNBT item(int slot) { - CompoundNBT item = new CompoundNBT(); item.putString("id", "minecraft:sign"); + private CompoundTag item(int slot) { + CompoundTag item = new CompoundTag(); item.putString("id", "minecraft:sign"); item.putByte("Count", (byte) 11); item.putByte("Slot", (byte) slot); - CompoundNBT tag = new CompoundNBT(); tag.putString("keepsake", "original-113"); - CompoundNBT nested = new CompoundNBT(); nested.putIntArray("typed", new int[]{7, 8, 9}); + CompoundTag tag = new CompoundTag(); tag.putString("keepsake", "original-113"); + CompoundTag nested = new CompoundTag(); nested.putIntArray("typed", new int[]{7, 8, 9}); tag.put("nested", nested); item.put("tag", tag); return item; } - private CompoundNBT body(int marker) { - CompoundNBT body = new CompoundNBT(); body.putString("id", "lootablebodies:corpse"); - body.putUniqueId("UUID", UUID.fromString("00000000-1144-4144-8144-000000000001")); + private CompoundTag body(int marker) { + CompoundTag body = new CompoundTag(); body.putString("id", "lootablebodies:corpse"); + body.putUUID("UUID", UUID.fromString("00000000-1144-4144-8144-000000000001")); body.putInt(UpgradePreservation.VERSION, marker); - ListNBT equipment = new ListNBT(); for (int i=0; i<48; i++) equipment.add(item(i)); - body.put("Equipment", equipment); ListNBT aux = new ListNBT(); aux.add(item(47)); body.put("Aux", aux); + ListTag equipment = new ListTag(); for (int i=0; i<48; i++) equipment.add(item(i)); + body.put("Equipment", equipment); ListTag aux = new ListTag(); aux.add(item(47)); body.put("Aux", aux); return body; } - @Test void currentFormatNamesThe115VanillaSchema() { - assertEquals(2586, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(6, UpgradePreservation.DATA_VERSION); + @Test void currentFormatNamesThe117VanillaSchema() { + assertEquals(2730, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(7, UpgradePreservation.DATA_VERSION); } @Test void released113EquipmentAndOverflowAreRetainedBeforeNativeConversion() { - CompoundNBT original = body(3), before = original.copy(); - CompoundNBT staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(6, staged.getInt(UpgradePreservation.VERSION)); - assertEquals(original.getUniqueId("UUID"), staged.getUniqueId("UUID")); + CompoundTag original = body(3), before = original.copy(); + CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); + assertEquals(7, staged.getInt(UpgradePreservation.VERSION)); + assertEquals(original.getUUID("UUID"), staged.getUUID("UUID")); assertTrue(staged.getList("Equipment", 10).isEmpty()); - ListNBT records = staged.getList("LootableBodiesUnresolved", 10); assertEquals(49, records.size()); + ListTag records = staged.getList("LootableBodiesUnresolved", 10); assertEquals(49, records.size()); for (int i=0; i<48; i++) { - CompoundNBT record = records.getCompound(i); + CompoundTag record = records.getCompound(i); assertEquals("Equipment", record.getString("Source")); assertEquals(original.getList("Equipment", 10).get(i), record.get("Data")); assertEquals(1631, record.getCompound("SourceVersions").getInt("DataVersion")); @@ -47,23 +48,23 @@ private CompoundNBT body(int marker) { assertEquals("Aux", records.getCompound(48).getString("Source")); assertEquals(before, original); } @Test void olderRecoveryPayloadsKeepTheirOriginalSourceAndData() { - CompoundNBT original = body(3), record = new CompoundNBT(); + CompoundTag original = body(3), record = new CompoundTag(); record.putString("Source", "Equipment"); record.put("Data", item(47)); - record.put("SourceVersions", context(922)); ListNBT recovery = new ListNBT(); recovery.add(record.copy()); + record.put("SourceVersions", context(922)); ListTag recovery = new ListTag(); recovery.add(record.copy()); original.put("LootableBodiesUnresolved", recovery); - CompoundNBT staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(6, staged.getInt(UpgradePreservation.VERSION)); + CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); + assertEquals(7, staged.getInt(UpgradePreservation.VERSION)); assertEquals(record, staged.getList("LootableBodiesUnresolved", 10).getCompound(49)); assertEquals(record, original.getList("LootableBodiesUnresolved", 10).getCompound(0)); } @Test void currentRecordsAreNotWrappedOrDuplicatedAgain() { - CompoundNBT original = body(6); + CompoundTag original = body(7); assertEquals(original, NativeSafeFixer.stageCorpse(original, context(1976))); } @Test void vanilla114ItemRenamesRunForReleased113InventoryCopies() { - UpgradePreservation.register(DataFixesManager.getDataFixer()); - CompoundNBT original = item(47), before = original.copy(); - CompoundNBT fixed = ItemMigration.convertNbt(original, context(1631)); + UpgradePreservation.register(DataFixers.getDataFixer()); + CompoundTag original = item(47), before = original.copy(); + CompoundTag fixed = ItemMigration.convertNbt(original, context(1631)); assertNotNull(fixed); assertEquals("minecraft:oak_sign", fixed.getString("id")); assertEquals(11, fixed.getByte("Count")); assertEquals(47, fixed.getByte("Slot")); assertEquals(original.get("tag"), fixed.get("tag")); assertEquals(before, original); diff --git a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java index 384ec96..e9ca4db 100644 --- a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java @@ -1,8 +1,13 @@ package cyano.lootable.upgrade; -import net.minecraft.nbt.*; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.DefaultTypeReferences; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.DataFixTypes; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -10,49 +15,49 @@ class Native115UpgradeTest { @AfterEach void close() { UpgradePreservation.close(); } - private CompoundNBT chunk(int[] biomes) { - CompoundNBT root = new CompoundNBT(), level = new CompoundNBT(); + private CompoundTag chunk(int[] biomes) { + CompoundTag root = new CompoundTag(), level = new CompoundTag(); root.putInt("DataVersion", 1976); level.putInt("xPos", 0); level.putInt("zPos", 0); level.putString("Status", "full"); - level.put("Sections", new ListNBT()); level.put("Entities", new ListNBT()); - level.put("TileEntities", new ListNBT()); level.putIntArray("Biomes", biomes); + level.put("Sections", new ListTag()); level.put("Entities", new ListTag()); + level.put("TileEntities", new ListTag()); level.putIntArray("Biomes", biomes); root.put("Level", level); return root; } - private CompoundNBT convert(CompoundNBT root, int source) { - return NBTUtil.update(UpgradePreservation.wrapper(DataFixesManager.getDataFixer()), - DefaultTypeReferences.CHUNK, root, source, 2586); + private CompoundTag convert(CompoundTag root, int source) { + return NbtUtils.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), + DataFixTypes.CHUNK, 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 currentFormatUsesThe115SchemaAndItsOwnMarker() { - assertEquals(2586, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(6, UpgradePreservation.DATA_VERSION); + @Test void currentFormatUsesThe117SchemaAndItsOwnMarker() { + assertEquals(2730, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(7, UpgradePreservation.DATA_VERSION); } @Test void currentBiomeValuesSurvivePlausibleOlderStampsAndRepeatedConversion() { - int[] values = currentBiomes(); CompoundNBT original = chunk(values), before = original.copy(); - CompoundNBT first = convert(original, 1976); + int[] values = currentBiomes(); CompoundTag original = chunk(values), before = original.copy(); + CompoundTag first = convert(original, 1976); assertArrayEquals(values, first.getCompound("Level").getIntArray("Biomes")); assertEquals(before, original); - CompoundNBT restamped = first.copy(); restamped.putInt("DataVersion", 1976); + CompoundTag restamped = first.copy(); restamped.putInt("DataVersion", 1976); assertArrayEquals(values, convert(restamped, 1976).getCompound("Level").getIntArray("Biomes")); } @Test void currentBiomeValuesSurviveMissingAndWronglyTypedStamps() { - for (INBT stamp : new INBT[]{null, StringNBT.valueOf("damaged"), IntNBT.valueOf(-1)}) { - int[] values = currentBiomes(); CompoundNBT original = chunk(values); + for (Tag stamp : new Tag[]{null, StringTag.valueOf("damaged"), IntTag.valueOf(-1)}) { + int[] values = currentBiomes(); CompoundTag original = chunk(values); if (stamp == null) original.remove("DataVersion"); else original.put("DataVersion", stamp); assertArrayEquals(values, convert(original, 1631).getCompound("Level").getIntArray("Biomes")); } } @Test void biomeProtectionStillRunsOlderVanillaEntityFixes() { - int[] values = currentBiomes(); CompoundNBT original = chunk(values); + int[] values = currentBiomes(); CompoundTag original = chunk(values); original.putInt("DataVersion", 1631); - CompoundNBT villager = new CompoundNBT(); villager.putString("id", "minecraft:villager"); + CompoundTag villager = new CompoundTag(); villager.putString("id", "minecraft:villager"); villager.putInt("Profession", 1); villager.putInt("Career", 1); villager.putInt("CareerLevel", 3); original.getCompound("Level").getList("Entities", 10).add(villager); - CompoundNBT before = original.copy(), fixed = convert(original, 1631); + CompoundTag before = original.copy(), fixed = convert(original, 1631); assertArrayEquals(values, fixed.getCompound("Level").getIntArray("Biomes")); assertEquals("minecraft:librarian", fixed.getCompound("Level").getList("Entities", 10) .getCompound(0).getCompound("VillagerData").getString("profession")); @@ -66,27 +71,27 @@ private int[] currentBiomes() { assertEquals(values[(z*4+2)*16+x*4+2], fixed[y*16+z*4+x]); } @Test void released114MarkerKeepsItsOriginalItemContext() { - CompoundNBT raw = new CompoundNBT(), item = new CompoundNBT(), context = new CompoundNBT(); + CompoundTag raw = new CompoundTag(), item = new CompoundTag(), context = new CompoundTag(); raw.putString("id", "lootablebodies:corpse"); raw.putInt(UpgradePreservation.VERSION, 4); item.putString("id", "minecraft:oak_sign"); item.putByte("Count", (byte) 9); item.putByte("Slot", (byte) 47); item.putIntArray("CustomTyped", new int[]{8, 5, 3}); - ListNBT items = new ListNBT(); items.add(item); raw.put("Equipment", items); - context.putInt("DataVersion", 1976); CompoundNBT before = raw.copy(); - CompoundNBT staged = NativeSafeFixer.stageCorpse(raw, context); - assertEquals(6, staged.getInt(UpgradePreservation.VERSION)); - CompoundNBT record = staged.getList("LootableBodiesUnresolved", 10).getCompound(0); + 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(7, 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")); assertEquals(before, raw); } @Test void released114MarkerWinsOverAnOlderOuterEntityContext() { - CompoundNBT raw = new CompoundNBT(), item = new CompoundNBT(); + CompoundTag raw = new CompoundTag(), item = new CompoundTag(); raw.putString("id", "lootablebodies:corpse"); raw.putInt(UpgradePreservation.VERSION, 4); item.putString("id", "minecraft:oak_sign"); item.putByte("Count", (byte) 7); - item.putByte("Slot", (byte) 47); ListNBT equipment = new ListNBT(); equipment.add(item); - raw.put("Equipment", equipment); CompoundNBT before = raw.copy(); - CompoundNBT fixed = NativeNbt.update(UpgradePreservation.wrapper(DataFixesManager.getDataFixer()), - net.minecraft.util.datafix.TypeReferences.ENTITY_TYPE, raw, 1631, 2586); + item.putByte("Slot", (byte) 47); ListTag equipment = new ListTag(); equipment.add(item); + raw.put("Equipment", equipment); CompoundTag before = raw.copy(); + CompoundTag fixed = NativeNbt.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), + net.minecraft.util.datafix.fixes.References.ENTITY_TREE, raw, 1631, 2586); assertEquals(1976, fixed.getList("LootableBodiesUnresolved", 10).getCompound(0) .getCompound("SourceVersions").getInt("DataVersion")); assertEquals(before, raw); diff --git a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java index e7370e6..723cf15 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java @@ -5,52 +5,54 @@ import java.nio.ByteBuffer; import java.nio.file.*; import java.util.zip.DeflaterOutputStream; -import net.minecraft.nbt.*; -import net.minecraft.util.datafix.TypeReferences; -import net.minecraft.world.World; -import net.minecraft.world.chunk.storage.ChunkLoader; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.storage.ChunkStorage; import org.junit.jupiter.api.*; import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.*; /** Captured from the sealed released 1.15 writer; never candidate-generated. */ class Native116DamagedStampTest { - static {net.minecraft.util.registry.Bootstrap.register();} + static {net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap();} @TempDir Path world; @AfterEach void close(){UpgradePreservation.close();} - private CompoundNBT original()throws Exception { + private CompoundTag original()throws Exception { Path fixture=Paths.get("src/test/resources/native115-release-chunk.nbt"); byte[] hash=java.security.MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(fixture));StringBuilder text=new StringBuilder();for(byte value:hash)text.append(String.format("%02X",value)); assertEquals("B6D03C7E0962715745FCD93A31EA27C01B4601020A51F542898BCE09F14C10C2",text.toString()); - try(InputStream input=Files.newInputStream(fixture)){return CompressedStreamTools.readCompressed(input);} + try(InputStream input=Files.newInputStream(fixture)){return NbtIo.readCompressed(input);} } - private void preserve(CompoundNBT chunk)throws Exception { - CompoundNBT header=new CompoundNBT(),data=new CompoundNBT(),fml=new CompoundNBT(); + private void preserve(CompoundTag chunk)throws Exception { + CompoundTag header=new CompoundTag(),data=new CompoundTag(),fml=new CompoundTag(); data.putInt("DataVersion",2586);header.put("Data",data); - ListNBT mods=new ListNBT();CompoundNBT minecraft=new CompoundNBT();minecraft.putString("ModId","minecraft");minecraft.putString("ModVersion","1.16.5");mods.add(minecraft); + ListTag mods=new ListTag();CompoundTag minecraft=new CompoundTag();minecraft.putString("ModId","minecraft");minecraft.putString("ModVersion","1.16.5");mods.add(minecraft); fml.put("LoadingModList",mods);header.put("fml",fml); - try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){CompressedStreamTools.writeCompressed(header,output);} - ByteArrayOutputStream bytes=new ByteArrayOutputStream();try(DataOutputStream output=new DataOutputStream(new DeflaterOutputStream(bytes))){CompressedStreamTools.write(chunk,output);} + try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(header,output);} + ByteArrayOutputStream bytes=new ByteArrayOutputStream();try(DataOutputStream output=new DataOutputStream(new DeflaterOutputStream(bytes))){NbtIo.write(chunk,output);} int sectors=(bytes.size()+5+4095)/4096;ByteBuffer region=ByteBuffer.allocate(8192+sectors*4096); - CompoundNBT level=chunk.getCompound("Level");int index=(level.getInt("xPos")&31)+(level.getInt("zPos")&31)*32; + CompoundTag level=chunk.getCompound("Level");int index=(level.getInt("xPos")&31)+(level.getInt("zPos")&31)*32; region.putInt(index*4,(2<<8)|sectors);region.position(8192);region.putInt(bytes.size()+1);region.put((byte)2);region.put(bytes.toByteArray()); Path file=world.resolve("region/r.0.0.mca");Files.createDirectories(file.getParent());Files.write(file,region.array());UpgradePreservation.prepare(world); } @Test void currentHeaderCannotOverrideThePreservedWriterOfAnUnstampedChunk() { - CompoundNBT context=new CompoundNBT(),fml=new CompoundNBT(),minecraft=new CompoundNBT(); + CompoundTag context=new CompoundTag(),fml=new CompoundTag(),minecraft=new CompoundTag(); context.putInt("DataVersion",2230);context.putInt("CorpseWriterDataVersion",2230);minecraft.putString("ModId","minecraft");minecraft.putString("ModVersion","1.16.5"); - ListNBT mods=new ListNBT();mods.add(minecraft);fml.put("LoadingModList",mods);context.put("WorldFML",fml); + ListTag mods=new ListTag();mods.add(minecraft);fml.put("LoadingModList",mods);context.put("WorldFML",fml); assertEquals(2230,UpgradePreservation.chunkSourceVersion(context)); context.putInt("ChunkDataVersion",1343);assertEquals(2230,UpgradePreservation.chunkSourceVersion(context)); context.putInt("ChunkDataVersion",2586);assertEquals(2586,UpgradePreservation.chunkSourceVersion(context)); context.putInt("VerifiedChunkDataVersion",2586);assertEquals(2586,UpgradePreservation.chunkSourceVersion(context)); } @Test void bothNativeAnvilPassesRetainNestedChestDataWithMissingChunkStamps()throws Exception { - CompoundNBT source=original();assertEquals(2230,source.getInt("DataVersion")); - CompoundNBT expected=NativeNbt.update(NativeVanillaDataFixer.get(),TypeReferences.CHUNK,source.copy(),2230,2586); - CompoundNBT damaged=source.copy();damaged.remove("DataVersion");preserve(damaged); - try(ChunkLoader loader=new ChunkLoader(world.resolve("reader").toFile(),UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),false)) { - CompoundNBT actual=loader.func_235968_a_(World.OVERWORLD,()->{throw new AssertionError("Current source must not replay legacy structure conversion");},damaged.copy()); + CompoundTag source=original();assertEquals(2230,source.getInt("DataVersion")); + CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,source.copy(),2230,2586); + CompoundTag damaged=source.copy();damaged.remove("DataVersion");preserve(damaged); + try(ChunkStorage loader=new ChunkStorage(world.resolve("reader").toFile(),UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),false)) { + CompoundTag actual=loader.upgradeChunkTag(Level.OVERWORLD,()->{throw new AssertionError("Current source must not replay legacy structure conversion");},damaged.copy()); assertEquals(expected.getCompound("Level").get("TileEntities"),actual.getCompound("Level").get("TileEntities")); assertEquals(source.getCompound("Level").get("TileEntities"),original().getCompound("Level").get("TileEntities")); } diff --git a/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java index 52102b2..88ca03e 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java @@ -1,8 +1,8 @@ package cyano.lootable.upgrade; -import net.minecraft.nbt.CompoundNBT; -import net.minecraft.util.ResourceLocation; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.TypeReferences; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class Native116RegistryRenameTest { @@ -10,10 +10,10 @@ class Native116RegistryRenameTest { ResourceLocation old=new ResourceLocation("minecraft:zombie_pigman"),current=new ResourceLocation("minecraft:zombified_piglin"); assertEquals(current,LegacyRegistryMappings.entityReplacement(old)); assertEquals(new ResourceLocation("minecraft:zombified_piglin_spawn_egg"),LegacyRegistryMappings.itemReplacement(new ResourceLocation("minecraft:zombie_pigman_spawn_egg"))); - CompoundNBT entity=new CompoundNBT();entity.putString("id",old.toString()); - assertEquals(current.toString(),NativeNbt.update(DataFixesManager.getDataFixer(),TypeReferences.ENTITY_TYPE,entity,2230,2586).getString("id")); - CompoundNBT item=new CompoundNBT();item.putString("id","minecraft:zombie_pigman_spawn_egg");item.putByte("Count",(byte)7); - assertEquals("minecraft:zombified_piglin_spawn_egg",NativeNbt.update(DataFixesManager.getDataFixer(),TypeReferences.ITEM_STACK,item,2230,2586).getString("id")); + CompoundTag entity=new CompoundTag();entity.putString("id",old.toString()); + assertEquals(current.toString(),NativeNbt.update(DataFixers.getDataFixer(),References.ENTITY_TREE,entity,2230,2586).getString("id")); + CompoundTag item=new CompoundTag();item.putString("id","minecraft:zombie_pigman_spawn_egg");item.putByte("Count",(byte)7); + assertEquals("minecraft:zombified_piglin_spawn_egg",NativeNbt.update(DataFixers.getDataFixer(),References.ITEM_STACK,item,2230,2586).getString("id")); } @Test void onlyExactRetiredVanillaSoundsAndBiomesAreRenamed(){ for(String sound:new String[]{"ambient","angry","death","hurt"})assertEquals(new ResourceLocation("minecraft:entity.zombified_piglin."+sound),LegacyRegistryMappings.soundReplacement(new ResourceLocation("minecraft:entity.zombie_pigman."+sound))); @@ -22,7 +22,7 @@ class Native116RegistryRenameTest { assertNull(LegacyRegistryMappings.soundReplacement(new ResourceLocation("othermod:music.nether"))); } @Test void nativeBiomesUseTheirOwnDynamicRegistry() { - assertEquals(new ResourceLocation("minecraft:worldgen/biome"),net.minecraftforge.registries.ForgeRegistries.Keys.BIOMES.getLocation()); + assertEquals(new ResourceLocation("minecraft:worldgen/biome"),net.minecraftforge.registries.ForgeRegistries.Keys.BIOMES.location()); } @Test void failedMappingsEscapeTheNativeLevelReaderFallback() { Error absent=assertThrows(Error.class,()->LegacyRegistryMappings.requireMappingTarget(()->null)); diff --git a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java index 97bef46..4ba7020 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java @@ -5,56 +5,60 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.UUID; -import net.minecraft.nbt.*; -import net.minecraft.util.datafix.DataFixesManager; -import net.minecraft.util.datafix.TypeReferences; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.TagParser; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; /** Native UUID and world-settings preservation, including damaged save stamps. */ class Native116UpgradeTest { private static final UUID CORPSE=UUID.fromString("00000000-1111-4111-8111-000000000001"); - private CompoundNBT settings() throws Exception { - return JsonToNBT.getTagFromJson(new String(Files.readAllBytes( + private CompoundTag settings() throws Exception { + return TagParser.parseTag(new String(Files.readAllBytes( Paths.get("src/test/resources/native116-worldgen.snbt")),StandardCharsets.UTF_8)); } - private CompoundNBT legacy() { - CompoundNBT body=new CompoundNBT();body.putString("id","lootablebodies:corpse"); + private CompoundTag legacy() { + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse"); body.putInt(UpgradePreservation.VERSION,5); body.putLong("UUIDMost",CORPSE.getMostSignificantBits());body.putLong("UUIDLeast",CORPSE.getLeastSignificantBits()); - ListNBT inventory=new ListNBT(); + ListTag inventory=new ListTag(); for(int slot=0;slot<48;slot++) { - CompoundNBT item=new CompoundNBT();item.putByte("Slot",(byte)slot);item.putString("id","minecraft:diamond"); - item.putByte("Count",(byte)(slot+1));CompoundNBT tag=new CompoundNBT();tag.putLong("typed",1234567890123L+slot); + CompoundTag item=new CompoundTag();item.putByte("Slot",(byte)slot);item.putString("id","minecraft:diamond"); + item.putByte("Count",(byte)(slot+1));CompoundTag tag=new CompoundTag();tag.putLong("typed",1234567890123L+slot); item.put("tag",tag);inventory.add(item); } body.put("Equipment",inventory);return body; } - @Test void currentFormatUsesThe116SchemaAndMarker() { - assertEquals(2586,UpgradePreservation.MINECRAFT_VERSION);assertEquals(6,UpgradePreservation.DATA_VERSION); + @Test void currentFormatUsesThe117SchemaAndMarker() { + assertEquals(2730,UpgradePreservation.MINECRAFT_VERSION);assertEquals(7,UpgradePreservation.DATA_VERSION); } @Test void legacyCorpseUuidSurvivesShieldingWithoutTruncatingCustomInventory() { - CompoundNBT body=legacy(),before=body.copy(),context=new CompoundNBT();context.putInt("DataVersion",2230); - CompoundNBT fixed=NativeSafeFixer.stageCorpse(body,context); - assertTrue(fixed.hasUniqueId("UUID"));assertEquals(CORPSE,fixed.getUniqueId("UUID")); + CompoundTag body=legacy(),before=body.copy(),context=new CompoundTag();context.putInt("DataVersion",2230); + CompoundTag fixed=NativeSafeFixer.stageCorpse(body,context); + assertTrue(fixed.hasUUID("UUID"));assertEquals(CORPSE,fixed.getUUID("UUID")); assertEquals(48,fixed.getList("LootableBodiesUnresolved",10).size()); for(int slot=0;slot<48;slot++)assertEquals(body.getList("Equipment",10).getCompound(slot), fixed.getList("LootableBodiesUnresolved",10).getCompound(slot).getCompound("Data")); assertEquals(before,body); } @Test void released115MarkerKeepsItsOwnItemContextUnderAnOlderOuterStamp() { - CompoundNBT body=legacy(),before=body.copy(); - CompoundNBT fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixesManager.getDataFixer()), - TypeReferences.ENTITY_TYPE,body,512,2586); + CompoundTag body=legacy(),before=body.copy(); + CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), + References.ENTITY_TREE,body,512,2586); assertEquals(2230,fixed.getList("LootableBodiesUnresolved",10).getCompound(0) .getCompound("SourceVersions").getInt("DataVersion")); assertEquals(before,body); } @Test void actualNativeCurrentSettingsSurviveOldAndMissingStampContexts() throws Exception { for(int source:new int[]{-1,0,512,922,1343,1631,1976,2230,2586}) { - CompoundNBT raw=settings(),before=raw.copy(); - CompoundNBT fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixesManager.getDataFixer()), - TypeReferences.WORLD_GEN_SETTINGS,raw,source,2586); + CompoundTag raw=settings(),before=raw.copy(); + CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), + References.WORLD_GEN_SETTINGS,raw,source,2586); assertEquals(before,fixed,"Source version "+source);assertEquals(before,raw); assertEquals(4267321L,fixed.getLong("seed")); assertEquals("minecraft:flat",fixed.getCompound("dimensions").getCompound("minecraft:overworld") @@ -62,31 +66,31 @@ private CompoundNBT legacy() { } } @Test void nonSingletonNbtOpsArePreservedWithCurrentWorldSettings() throws Exception { - NBTDynamicOps ops=new NBTDynamicOps(){};CompoundNBT raw=settings(),before=raw.copy(); - Dynamic result=UpgradePreservation.wrapper(DataFixesManager.getDataFixer()).update( - TypeReferences.WORLD_GEN_SETTINGS,new Dynamic(ops,raw),-1,2586); + NbtOps ops=new NbtOps(){};CompoundTag raw=settings(),before=raw.copy(); + Dynamic result=UpgradePreservation.wrapper(DataFixers.getDataFixer()).update( + References.WORLD_GEN_SETTINGS,new Dynamic(ops,raw),-1,2586); assertSame(ops,result.getOps());assertEquals(before,result.getValue());assertEquals(before,raw); } @Test void levelEnvelopeProtectsCurrentSettingsWithoutMutatingTheOriginal() throws Exception { - CompoundNBT root=new CompoundNBT();root.putString("LevelName","Disposable fixture"); - root.put("WorldGenSettings",settings());CompoundNBT before=root.copy(); - CompoundNBT fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixesManager.getDataFixer()), - net.minecraft.util.datafix.DefaultTypeReferences.LEVEL.getTypeReference(),root,-1,2586); + 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); assertEquals(before.get("WorldGenSettings"),fixed.get("WorldGenSettings"));assertEquals(before,root); } @Test void registrationProtectsTheNativeLevelReaderBeforeServerCreation() throws Exception { - UpgradePreservation.register(DataFixesManager.getDataFixer()); - CompoundNBT raw=settings(),before=raw.copy(); - CompoundNBT fixed=NativeNbt.update(DataFixesManager.getDataFixer(), - TypeReferences.WORLD_GEN_SETTINGS,raw,-1,2586); + UpgradePreservation.register(DataFixers.getDataFixer()); + CompoundTag raw=settings(),before=raw.copy(); + CompoundTag fixed=NativeNbt.update(DataFixers.getDataFixer(), + References.WORLD_GEN_SETTINGS,raw,-1,2586); assertEquals(before,fixed,"The world reader obtains its fixer before server-about-to-start"); assertEquals(before,raw); } @Test void nativeLoadingModListSuppliesChunkContextWhenStampsAreDamaged() { - CompoundNBT versions=new CompoundNBT(),fml=new CompoundNBT(),mod=new CompoundNBT(); + CompoundTag versions=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag(); mod.putString("ModId","minecraft");mod.putString("ModVersion","1.15.2"); - ListNBT mods=new ListNBT();mods.add(mod);fml.put("LoadingModList",mods);versions.put("WorldFML",fml); + ListTag mods=new ListTag();mods.add(mod);fml.put("LoadingModList",mods);versions.put("WorldFML",fml); versions.putInt("DataVersion",922);versions.putString("ChunkDataVersion","damaged"); - CompoundNBT before=versions.copy();assertEquals(2230,UpgradePreservation.chunkSourceVersion(versions));assertEquals(before,versions); + CompoundTag before=versions.copy();assertEquals(2230,UpgradePreservation.chunkSourceVersion(versions));assertEquals(before,versions); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java new file mode 100644 index 0000000..f5afb69 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java @@ -0,0 +1,107 @@ +package cyano.lootable.upgrade; + +import java.nio.file.*; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import com.mojang.datafixers.*; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.serialization.Dynamic; +import net.minecraft.nbt.*; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; +import cyano.lootable.buildtest.NativeVanillaDataFixer; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.*; +import static org.junit.jupiter.api.Assertions.*; +import static cyano.lootable.upgrade.Native117RegionFixtures.*; + +class Native117EntityChunkTest { + @TempDir(cleanup=CleanupMode.ON_SUCCESS) Path world; + 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,2730); + } + @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)); + ListTag passengers=new ListTag();passengers.add(corpse);cart.put("Passengers",passengers); + 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(7,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")); + assertEquals(corpse.getList("LootableBodiesUnresolved",10).getCompound(0),records.getCompound(49)); + assertEquals(before,raw);assertArrayEquals(bytes,Files.readAllBytes(region)); + } + @Test void currentMarkerProtectsNativeRootAgainstDamagedStamps() { + CompoundTag corpse=body(ID,7); + for(int version:new int[]{-1,0,512,2230,2586,2730}) { + CompoundTag raw=chunk(version,corpse),before=raw.copy(); + CompoundTag fixed=convert(raw,version); + assertEquals(corpse,fixed.getList("Entities",10).getCompound(0));assertEquals(before,raw); + } + } + @Test void malformedEntityChunkCannotReachNativeConstruction() { + CompoundTag raw=chunk(2730,body(ID,7));raw.putString("Position","0,0");CompoundTag before=raw.copy(); + assertThrows(IllegalStateException.class,()->convert(raw,2730));assertEquals(before,raw); + } + @Test void unpreservedSavedEntityChunkRefusesInsteadOfGuessingItsOriginal() { + UpgradePreservation.close();CompoundTag raw=chunk(2586,body(ID,6)); + assertThrows(IllegalStateException.class,()->convert(raw,2586)); + } + private DataFixer observe(DataFixer nativeFixer,AtomicInteger calls,UpgradeJournal expectedGlobal,boolean reject) { + return new DataFixer() { + public Schema getSchema(int version){return nativeFixer.getSchema(version);} + public Dynamic update(DSL.TypeReference type,Dynamic input,int from,int to) { + if(type.typeName().equals("entity_chunk")) { + calls.incrementAndGet();assertSame(expectedGlobal,UpgradePreservation.journal(),"Preflight must not temporarily substitute the runtime journal"); + if(reject)throw new IllegalStateException("Injected fixture conversion refusal; not a native Minecraft failure"); + } + return nativeFixer.update(type,input,from,to); + } + }; + } + @Test void startupRefusesBeforeWorldConstructionWithoutChangingGlobalOrOriginals() throws Exception { + DataFixer original=DataFixers.getDataFixer(),nativeFixer=NativeVanillaDataFixer.get(); + UpgradeJournal previous=UpgradePreservation.prepare(world.resolve("previous")); + Path candidate=world.resolve("candidate"),region=write(candidate,chunk(2586,body(ID,6)));byte[] bytes=Files.readAllBytes(region); + AtomicInteger calls=new AtomicInteger(); + try { + DataFixers.DATA_FIXER=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;} + } + @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); + 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;} + } + @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); + 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;} + } + @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); + 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;} + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java new file mode 100644 index 0000000..054ba35 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java @@ -0,0 +1,115 @@ +package cyano.lootable.upgrade; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.file.*; +import java.util.*; +import java.util.zip.InflaterInputStream; +import net.minecraft.nbt.*; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.*; + +/** Native RegionFile codec and entity-chunk shapes; not historical-world qualification. */ +class Native117EntityRegionTest { + @TempDir 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); + body.putInt(UpgradePreservation.VERSION,7);body.putString("Name","Native 17 owner"); + ListTag inventory=new ListTag(); + for(int slot=0;slot<48;slot++) { + CompoundTag item=new CompoundTag();item.putByte("Slot",(byte)slot);item.putString("id","minecraft:diamond"); + item.putByte("Count",(byte)(slot+1));CompoundTag custom=new CompoundTag();custom.putString("keep","slot-"+slot);item.put("tag",custom);inventory.add(item); + } + body.put("Equipment",inventory);return body; + } + private CompoundTag chunk(int x,int z,CompoundTag... entities) { + CompoundTag root=new CompoundTag();root.putInt("DataVersion",2730);root.putIntArray("Position",new int[]{x,z}); + ListTag list=new ListTag();for(CompoundTag entity:entities)list.add(entity.copy());root.put("Entities",list);return root; + } + private Path write(String dimension,String storage,int x,int z,CompoundTag root) throws Exception { + Path directory=world.resolve(dimension).resolve(storage);Files.createDirectories(directory); + Path path=directory.resolve("r."+(x>>5)+"."+(z>>5)+".mca"); + try(RegionFile region=new RegionFile(path.toFile(),directory.toFile(),false)) { + try(DataOutputStream out=region.getChunkDataOutputStream(new ChunkPos(x,z))) {NbtIo.write(root,out);} + } + return path; + } + private CompoundTag original(UpgradeJournal journal,UUID id,int x,int z) throws Exception { + CompoundTag saved=journal.originalFor(id,x,z);assertNotNull(saved,"Saved native entity chunk was skipped");return saved; + } + @Test void nativeEntityRegionsPreserveEverySlotAcrossAllDimensions() throws Exception { + String[] dimensions={"","DIM-1","DIM1","dimensions/fixture/realm"};Map before=new LinkedHashMap<>(); + List ids=new ArrayList<>(); + for(int i=0;i file:before.entrySet())assertArrayEquals(file.getValue(),Files.readAllBytes(file.getKey())); + } + @Test void retainsExactCompressedEntityChunkAndRootContext() throws Exception { + CompoundTag root=chunk(0,0,body(ID));Path file=write("","entities",0,0,root);byte[] before=Files.readAllBytes(file); + CompoundTag saved=original(UpgradeJournal.scan(world),ID,0,0); + int offset=ByteBuffer.wrap(before).getInt()>>>8,base=offset*4096,size=ByteBuffer.wrap(before,base,4).getInt(); + byte[] original=Arrays.copyOfRange(before,base+5,base+4+size); + assertArrayEquals(original,Files.readAllBytes(world.resolve("data/lootablebodies-upgrade/chunks/"+saved.getString("OriginalChunk")+".bin"))); + try(DataInputStream input=new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(original)))) { + assertEquals(root,NbtIo.read(input,new NbtAccounter(64L*1024*1024))); + } + assertArrayEquals(before,Files.readAllBytes(file)); + } + @Test void rejectsWrongTypedOrMismatchedPositionBeforeOpeningTheWorld() throws Exception { + for(Tag position:new Tag[]{StringTag.valueOf("0,0"),new IntArrayTag(new int[]{0}),new IntArrayTag(new int[]{32,0})}) { + CompoundTag root=chunk(0,0,body(ID));root.put("Position",position); + Path file=write("","entities",0,0,root);byte[] before=Files.readAllBytes(file); + assertThrows(IOException.class,()->UpgradeJournal.scan(world));assertArrayEquals(before,Files.readAllBytes(file)); + } + } + @Test void rejectsMissingPositionAndWrongEntityListType() throws Exception { + CompoundTag root=chunk(0,0,body(ID));root.remove("Position");write("","entities",0,0,root); + assertThrows(IOException.class,()->UpgradeJournal.scan(world)); + root=chunk(0,0);ListTag invalid=new ListTag();invalid.add(StringTag.valueOf("not an entity"));root.put("Entities",invalid);write("","entities",0,0,root); + assertThrows(IOException.class,()->UpgradeJournal.scan(world)); + } + @Test void duplicateUuidAcrossActiveTerrainAndEntityRegionsCannotBeGuessed() throws Exception { + CompoundTag terrain=new CompoundTag(),level=new CompoundTag();terrain.putInt("DataVersion",2586);terrain.put("Level",level); + level.putInt("xPos",0);level.putInt("zPos",0);ListTag entities=new ListTag();entities.add(body(ID));level.put("Entities",entities); + write("","region",0,0,terrain);write("","entities",0,0,chunk(0,0,body(ID))); + assertThrows(IOException.class,()->UpgradeJournal.scan(world)); + } + @Test void removedNativeBodyNeverReappearsFromAnArchivedSnapshot() throws Exception { + write("","entities",0,0,chunk(0,0,body(ID)));assertNotNull(UpgradeJournal.scan(world).originalFor(ID,0,0)); + write("","entities",0,0,chunk(0,0));assertEquals(0,UpgradeJournal.scan(world).size()); + assertNull(UpgradeJournal.scan(world).originalFor(ID,0,0)); + try(java.util.stream.Stream saved=Files.list(world.resolve("data/lootablebodies-upgrade/snapshots"))) {assertTrue(saved.findAny().isPresent());} + } + @Test void nestedPassengersArePreservedBeforeConstruction() throws Exception { + CompoundTag cart=new CompoundTag();cart.putString("id","minecraft:chest_minecart");cart.putUUID("UUID",new UUID(7,8)); + ListTag passengers=new ListTag();passengers.add(body(ID));cart.put("Passengers",passengers); + write("","entities",0,0,chunk(0,0,cart));CompoundTag saved=original(UpgradeJournal.scan(world),ID,0,0); + assertEquals(body(ID),saved.getCompound("Entity"));assertArrayEquals(new int[]{0},saved.getIntArray("PassengerPath")); + } + @Test void unsupportedExternalStreamsRefuseSafelyAndRetainTheSidecar() throws Exception { + Path file=write("","entities",0,0,chunk(0,0,body(ID)));byte[] raw=Files.readAllBytes(file); + int base=(ByteBuffer.wrap(raw).getInt()>>>8)*4096,size=ByteBuffer.wrap(raw,base,4).getInt(); + Path sidecar=file.getParent().resolve("c.0.0.mcc");byte[] payload=Arrays.copyOfRange(raw,base+5,base+4+size);Files.write(sidecar,payload); + ByteBuffer.wrap(raw,base,4).putInt(1);raw[base+4]=(byte)0x82;Files.write(file,raw); + assertThrows(IOException.class,()->UpgradeJournal.scan(world)); + assertArrayEquals(raw,Files.readAllBytes(file));assertArrayEquals(payload,Files.readAllBytes(sidecar)); + } + @Test void nativeEmptyEntityRegionPlaceholderStaysEmpty() throws Exception { + Path directory=world.resolve("entities");Files.createDirectories(directory);Path file=directory.resolve("r.0.0.mca"); + try(RegionFile region=new RegionFile(file.toFile(),directory.toFile(),false)) {assertNull(region.getChunkDataInputStream(new ChunkPos(0,0)));} + byte[] before=Files.readAllBytes(file);assertEquals(0,UpgradeJournal.scan(world).size());assertArrayEquals(before,Files.readAllBytes(file)); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java new file mode 100644 index 0000000..5d3e3d1 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java @@ -0,0 +1,56 @@ +package cyano.lootable.upgrade; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.file.*; +import java.security.MessageDigest; +import java.util.*; +import net.minecraft.nbt.*; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.*; + +/** Actual native oversized-chunk writer, never historical-world provenance. */ +class Native117ExternalSidecarTest { + @TempDir Path temporary; + @Test void actualNativeExternalChunkRefusesConservativelyWithAllOriginalBytesIntact() throws Exception { + String exported=System.getProperty("lootablebodies.exportNativeExternalWorld"); + Path world=exported==null?temporary.resolve("world"):Paths.get(exported); + assertFalse(Files.exists(world),"Refusing an existing native external-chunk fixture"); + Path directory=world.resolve("entities");Files.createDirectories(directory); + Path header=Paths.get("tools/fixtures/native117-flat-level.dat"); + String headerHash=String.format(Locale.ROOT,"%064X",new java.math.BigInteger(1,MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(header)))); + assertEquals("358E4B91F290AFB219D85B09435B4A641035412686E31A09CF5C69F5A28515CB",headerHash); + Files.copy(header,world.resolve("level.dat")); + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse"); + UUID identity=UUID.fromString("00000000-1171-4171-8171-000000000099");body.putUUID("UUID",identity); + body.putInt(UpgradePreservation.VERSION,7);body.putString("Name","Native external control"); + ListTag equipment=new ListTag(); + for(int slot=0;slot<48;slot++) { + CompoundTag item=new CompoundTag();item.putByte("Slot",(byte)slot);item.putString("id","minecraft:diamond");item.putByte("Count",(byte)1);equipment.add(item); + } + body.put("Equipment",equipment); + byte[] opaque=new byte[2*1024*1024];new Random(1170199L).nextBytes(opaque);body.putByteArray("NativeExternalControl",opaque); + CompoundTag chunk=new CompoundTag();chunk.putInt("DataVersion",2730);chunk.putIntArray("Position",new int[]{0,0}); + ListTag entities=new ListTag();entities.add(body);chunk.put("Entities",entities); + Path file=directory.resolve("r.0.0.mca"); + try(RegionFile region=new RegionFile(file.toFile(),directory.toFile(),false)) { + try(DataOutputStream out=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(chunk,out);} + } + Path sidecar=directory.resolve("c.0.0.mcc");assertTrue(Files.isRegularFile(sidecar));assertTrue(Files.size(sidecar)>1024*1024); + byte[] raw=Files.readAllBytes(file),payload=Files.readAllBytes(sidecar); + int location=ByteBuffer.wrap(raw).getInt(),offset=(location>>>8)*4096; + assertEquals(0x82,raw[offset+4]&255,"Native writer must actually select external zlib storage"); + try(RegionFile region=new RegionFile(file.toFile(),directory.toFile(),false); + DataInputStream in=region.getChunkDataInputStream(new ChunkPos(0,0))) { + assertEquals(chunk,NbtIo.read(in),"Native reader must decode the original external entity chunk"); + } + IOException refusal=assertThrows(IOException.class,()->UpgradeJournal.scan(world)); + assertTrue(refusal.getMessage().startsWith("Invalid compressed chunk size:"), + "The existing conservative reader must reject the native one-byte external stub before decoding any entity"); + assertArrayEquals(raw,Files.readAllBytes(file));assertArrayEquals(payload,Files.readAllBytes(sidecar)); + assertArrayEquals(Files.readAllBytes(header),Files.readAllBytes(world.resolve("level.dat"))); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java new file mode 100644 index 0000000..d7bfbb2 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java @@ -0,0 +1,28 @@ +package cyano.lootable.upgrade; + +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class Native117RegistryRenameTest { + final ResourceLocation oldPath=new ResourceLocation("minecraft:grass_path"); + final ResourceLocation dirtPath=new ResourceLocation("minecraft:dirt_path"); + @Test void grassPathBlockUsesTheActualNativeDirtPath() { + assertEquals(dirtPath,LegacyRegistryMappings.blockReplacement(oldPath)); + assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH,Registry.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)); + 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)); + assertNull(LegacyRegistryMappings.soundReplacement(new ResourceLocation("anothermod:item.sweet_berries.pick_from_bush"))); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native117SavedPositionTest.java b/src/test/java/cyano/lootable/upgrade/Native117SavedPositionTest.java new file mode 100644 index 0000000..dadbc15 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native117SavedPositionTest.java @@ -0,0 +1,80 @@ +package cyano.lootable.upgrade; + +import java.util.UUID; +import net.minecraft.nbt.*; +import net.minecraft.util.datafix.fixes.References; +import cyano.lootable.buildtest.NativeVanillaDataFixer; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static cyano.lootable.upgrade.Native117RegionFixtures.*; + +/** Saved containing coordinates must reach the conversion before construction. */ +class Native117SavedPositionTest { + private static final UUID ID=new UUID(117,171); + private static final String RECOVERY="LootableBodiesInvalidFields"; + private ListTag pos(double x,double y,double z) { + ListTag list=new ListTag();for(double v:new double[]{x,y,z})list.add(DoubleTag.valueOf(v));return list; + } + private CompoundTag fixed(CompoundTag raw,boolean separate) { + return NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), + separate?References.ENTITY_CHUNK:References.CHUNK,raw,2730,2730); + } + private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { + CompoundTag root=chunk(2730,body); + if(separate)root.putIntArray("Position",new int[]{x,z}); + else { + CompoundTag level=new CompoundTag();level.putInt("xPos",x);level.putInt("zPos",z);level.put("Entities",root.get("Entities")); + root.remove("Entities");root.remove("Position");root.put("Level",level); + } + return root; + } + private CompoundTag first(CompoundTag root,boolean separate){return (separate?root:root.getCompound("Level")).getList("Entities",10).getCompound(0);} + @Test void malformedAxisUsesContainingRegionBeforeEitherNativeLoaderConstructsAnEntity() { + for(boolean separate:new boolean[]{false,true}) { + CompoundTag body=body(ID,7);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")); + for(String key:new String[]{"UUID","Name","Equipment","Aux","LootableBodiesUnresolved"})assertEquals(body.get(key),actual.get(key),key); + assertEquals(converted,fixed(converted,separate),"Conversion accumulated records or moved an already repaired body"); + } + } + @Test void finiteMismatchedRootAndMissingPositionCannotBeIndexedInTheWrongSection() { + CompoundTag body=body(ID,7);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),7);rider.put("Pos",pos(17,80,8)); + CompoundTag body=body(ID,7);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,7);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")); + assertEquals("Pos",records.getCompound(1).getString("Source"));assertEquals(StringTag.valueOf("opaque position"),records.getCompound(1).get("Data")); + assertEquals(result,fixed(result,true)); + } + @Test void impossibleContainingCoordinatesRefuseBeforeNativeConstruction() { + CompoundTag raw=root(body(ID,7),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,7);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")); + for(String key:new String[]{"UUID","Equipment","Aux","LootableBodiesUnresolved"})assertEquals(body.get(key),actual.get(key)); + } + } + @Test void nativeHorizontalAndVerticalBoundaryValuesRemainExact() { + for(boolean separate:new boolean[]{false,true})for(double y:new double[]{-2e7,2e7}) { + CompoundTag body=body(ID,7);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/Native117TargetTest.java b/src/test/java/cyano/lootable/upgrade/Native117TargetTest.java new file mode 100644 index 0000000..d0c73f4 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native117TargetTest.java @@ -0,0 +1,55 @@ +package cyano.lootable.upgrade; + +import java.io.DataInputStream; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class Native117TargetTest { + @Test void targetMatchesTheActualNativeWorldVersion() { + net.minecraft.SharedConstants.tryDetectVersion(); + assertEquals(2730,net.minecraft.SharedConstants.getCurrentVersion().getWorldVersion()); + assertEquals(2730,UpgradePreservation.MINECRAFT_VERSION); + assertEquals(7,UpgradePreservation.DATA_VERSION); + } + @Test void released16MarkerIsMigratedWithAllSlotsAndOriginalsRetained() { + assertEquals(2586,cyano.lootable.buildtest.HistoricalVersions.dataVersion("1.16.5")); + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse"); + body.putInt(UpgradePreservation.VERSION,6); + ListTag items=new ListTag(); + for(int slot=0;slot<48;slot++) { + CompoundTag item=new CompoundTag();item.putByte("Slot",(byte)slot); + item.putString("id","minecraft:stone");item.putByte("Count",(byte)(slot+1));items.add(item); + } + body.put("Equipment",items);CompoundTag before=body.copy(); + CompoundTag source=new CompoundTag();source.putInt("DataVersion",2586); + CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); + assertEquals(7,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); + assertEquals(items.getCompound(slot),record.getCompound("Data")); + assertEquals("Equipment",record.getString("Source")); + assertEquals(2586,record.getCompound("SourceVersions").getInt("DataVersion")); + } + assertEquals(before,body); + } + @Test void productionClassesUseJava16() throws Exception { + try(DataInputStream bytes=new DataInputStream(getClass().getResourceAsStream("/cyano/lootable/entities/EntityLootableBody.class"))) { + assertEquals(0xCAFEBABE,bytes.readInt());bytes.readUnsignedShort();assertEquals(60,bytes.readUnsignedShort()); + } + } + @Test void marker6OverridesADamagedOuterEntityStamp() { + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse"); + body.putInt(UpgradePreservation.VERSION,6); + 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 for this pass-through control");} + 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,2730); + assertEquals(2586,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(6,body.getInt(UpgradePreservation.VERSION)); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native117VanillaTest.java b/src/test/java/cyano/lootable/upgrade/Native117VanillaTest.java new file mode 100644 index 0000000..161c02a --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native117VanillaTest.java @@ -0,0 +1,50 @@ +package cyano.lootable.upgrade; + +import java.nio.file.Files; +import java.nio.file.Paths; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.util.datafix.fixes.References; +import cyano.lootable.buildtest.NativeVanillaDataFixer; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class Native117VanillaTest { + private CompoundTag flatSettings() throws Exception { + CompoundTag settings=TagParser.parseTag(Files.readString(Paths.get("src/test/resources/native116-worldgen.snbt"))); + settings.getCompound("dimensions").getCompound("minecraft:overworld").getCompound("generator") + .getCompound("settings").getList("layers",10).getCompound(2).putString("block","minecraft:grass_path"); + return settings; + } + private CompoundTag nativeSettings(CompoundTag input) { + CompoundTag fixed=NativeNbt.update(NativeVanillaDataFixer.get(),References.WORLD_GEN_SETTINGS,input,2586,2730); + assertEquals("minecraft:dirt_path",fixed.getCompound("dimensions").getCompound("minecraft:overworld") + .getCompound("generator").getCompound("settings").getList("layers",10).getCompound(2).getString("block")); + return fixed; + } + @Test void completeSettingsMigrateFlatLayersEvenWithDamagedStamps() throws Exception { + CompoundTag raw=flatSettings(),before=raw.copy(),expected=nativeSettings(raw); + for(int source:new int[]{-1,0,512,2230,2586}) { + CompoundTag actual=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), + References.WORLD_GEN_SETTINGS,raw,source,2730); + assertEquals(expected,actual,"source="+source);assertEquals(before,raw); + } + } + @Test void protectedLevelFieldStillReceivesNative17WorldgenChanges() throws Exception { + CompoundTag raw=flatSettings(),expected=nativeSettings(raw),level=new CompoundTag(); + level.put("WorldGenSettings",raw);level.putString("CustomKeepTag","retain me"); + CompoundTag before=level.copy(); + CompoundTag actual=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.LEVEL,level,-1,2730); + assertEquals(expected,actual.getCompound("WorldGenSettings")); + assertEquals("retain me",actual.getString("CustomKeepTag"));assertEquals(before,level); + } + @Test void vanillaBlockAndItemRenamesUseTheActualNativeOracle() { + CompoundTag block=new CompoundTag();block.putString("Name","minecraft:grass_path"); + CompoundTag item=new CompoundTag();item.putString("id","minecraft:grass_path");item.putByte("Count",(byte)3); + CompoundTag blockBefore=block.copy(),itemBefore=item.copy(); + com.mojang.datafixers.DataFixer safe=UpgradePreservation.wrapper(NativeVanillaDataFixer.get()); + assertEquals("minecraft:dirt_path",NativeNbt.update(safe,References.BLOCK_STATE,block,2586,2730).getString("Name")); + assertEquals("minecraft:dirt_path",NativeNbt.update(safe,References.ITEM_STACK,item,2586,2730).getString("id")); + assertEquals(blockBefore,block);assertEquals(itemBefore,item); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/NativeFixerInstallationTest.java b/src/test/java/cyano/lootable/upgrade/NativeFixerInstallationTest.java index f917ba9..723490c 100644 --- a/src/test/java/cyano/lootable/upgrade/NativeFixerInstallationTest.java +++ b/src/test/java/cyano/lootable/upgrade/NativeFixerInstallationTest.java @@ -1,12 +1,12 @@ package cyano.lootable.upgrade; import com.mojang.datafixers.DataFixer; -import net.minecraft.util.datafix.DataFixesManager; +import net.minecraft.util.datafix.DataFixers; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class NativeFixerInstallationTest { - static class Parent {private final DataFixer fixer=DataFixesManager.getDataFixer();DataFixer value(){return fixer;}} + static class Parent {private final DataFixer fixer=DataFixers.getDataFixer();DataFixer value(){return fixer;}} static class Child extends Parent { } - static class Duplicate extends Parent {private final DataFixer second=DataFixesManager.getDataFixer();} + static class Duplicate extends Parent {private final DataFixer second=DataFixers.getDataFixer();} @Test void wrapsTheUniqueInheritedFixerAndDoesNotDoubleWrap() { Child owner=new Child();ChunkVersionContext.installFixer(owner,null);DataFixer fixed=owner.value();assertInstanceOf(NativeSafeFixer.class,fixed);ChunkVersionContext.installFixer(owner,null);assertSame(fixed,owner.value()); } diff --git a/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener new file mode 100644 index 0000000..0b7619a --- /dev/null +++ b/src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener @@ -0,0 +1 @@ +cyano.lootable.buildtest.Native117TestEnvironment diff --git a/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java b/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java index a5d8aee..b73fe6b 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java +++ b/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java @@ -1,10 +1,10 @@ package cyano.lootable.buildtest; import java.util.UUID; -import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.CompoundTag; /** Independent source-fixture identity reader; never rewrites historical NBT. */ public final class HistoricalUuid { private HistoricalUuid() { } - public static UUID read(CompoundNBT n,String key) { + public static UUID read(CompoundTag n,String key) { UUID array=null,pair=null; if(n.contains(key)) { if(!n.contains(key,11) || n.getIntArray(key).length!=4)throw new IllegalArgumentException("Malformed fixture identity array"); diff --git a/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java b/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java index 4095e14..564eaad 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java +++ b/src/testSupport/java/cyano/lootable/buildtest/HistoricalVersions.java @@ -10,6 +10,7 @@ public static int dataVersion(String minecraft) { if ("1.13.2".equals(minecraft)) return 1631; if ("1.14.4".equals(minecraft)) return 1976; if ("1.15.2".equals(minecraft)) return 2230; + if ("1.16.5".equals(minecraft)) return 2586; throw new IllegalArgumentException("Unsupported historical Minecraft " + minecraft); } } diff --git a/src/testSupport/java/cyano/lootable/buildtest/NativeEntityInbox.java b/src/testSupport/java/cyano/lootable/buildtest/NativeEntityInbox.java new file mode 100644 index 0000000..0171ce2 --- /dev/null +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeEntityInbox.java @@ -0,0 +1,32 @@ +package cyano.lootable.buildtest; + +import java.util.function.BooleanSupplier; +import java.util.function.LongSupplier; +import java.util.function.Consumer; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; + +/** Isolated fixture readiness at the real native entity-storage boundary. */ +public final class NativeEntityInbox { + private NativeEntityInbox() { } + + public static void await(PersistentEntitySectionManager manager, long chunk, + BooleanSupplier pollTask, LongSupplier clock, long deadline, + Consumer managedBlock) { + managedBlock.accept(()->{ + if(manager.areEntitiesLoaded(chunk))return true; + manager.tick(); + if(manager.areEntitiesLoaded(chunk))return true; + if(clock.getAsLong()>=deadline) + throw new IllegalStateException("Native entity inbox deadline for "+chunk); + if(!pollTask.getAsBoolean())Thread.yield(); + return false; + }); + if(!manager.areEntitiesLoaded(chunk)) + throw new IllegalStateException("Native managed wait returned before entity admission for "+chunk); + } + + public static void await(PersistentEntitySectionManager manager, long chunk, + BooleanSupplier pollTask, LongSupplier clock, long deadline) { + await(manager,chunk,pollTask,clock,deadline,ready->{while(!ready.getAsBoolean())Thread.yield();}); + } +} diff --git a/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java new file mode 100644 index 0000000..0a030cc --- /dev/null +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java @@ -0,0 +1,42 @@ +package cyano.lootable.buildtest; + +import java.util.List; +import java.util.ArrayList; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.world.item.ItemStack; + +/** Receiving-side menu observations from independent native serialization. */ +public final class NativeMenuObservation { + public final boolean content; + public final int containerId,stateId,slot; + public final List items; + public final ItemStack carried,item; + NativeMenuObservation(boolean content,int containerId,int stateId,int slot,List items,ItemStack carried,ItemStack item) { + this.content=content;this.containerId=containerId;this.stateId=stateId;this.slot=slot;this.items=items;this.carried=carried;this.item=item; + } + public static NativeMenuObservation decode(Packet packet) { + boolean content=packet instanceof ClientboundContainerSetContentPacket; + if(!content && !(packet instanceof ClientboundContainerSetSlotPacket))return null; + FriendlyByteBuf wire=new FriendlyByteBuf(Unpooled.buffer()); + try { + // Receiving-side getters can be stripped from a dedicated build. + // Serialize independently instead of touching live packet state. + packet.write(wire); + int window=content?wire.readUnsignedByte():wire.readByte();int state=wire.readVarInt(); + NativeMenuObservation result; + if(content) { + int count=wire.readVarInt();List items=new ArrayList<>(); + if(count<0 || count>4096)throw new IllegalStateException("Unexpected native menu slot count"); + for(int index=0;index dimension=null; + ResourceKey dimension=null; long seed=0; - for(Field field:SRespawnPacket.class.getDeclaredFields()) { - if(field.getType()==RegistryKey.class) { + for(Field field:ClientboundRespawnPacket.class.getDeclaredFields()) { + if(field.getType()==ResourceKey.class) { if(dimension!=null)throw new AssertionError("Ambiguous respawn world key"); - field.setAccessible(true);dimension=(RegistryKey)field.get(packet); + field.setAccessible(true);dimension=(ResourceKey)field.get(packet); } else if(field.getType()==long.class) { field.setAccessible(true);seed=field.getLong(packet); } } if(dimension==null)throw new AssertionError("Missing respawn world key"); - return "RESPAWN dimension="+dimension.getLocation()+" hashedSeed="+seed; + return "RESPAWN dimension="+dimension.location()+" hashedSeed="+seed; } catch(ReflectiveOperationException invalid) {throw new AssertionError("Fixture respawn observation",invalid);} } } diff --git a/src/testSupport/java/cyano/lootable/buildtest/NativeVanillaDataFixer.java b/src/testSupport/java/cyano/lootable/buildtest/NativeVanillaDataFixer.java index 4ca6cee..12a2ae6 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/NativeVanillaDataFixer.java +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeVanillaDataFixer.java @@ -7,7 +7,7 @@ private NativeVanillaDataFixer() { } private static DataFixer captured; public static synchronized DataFixer get() { if(captured!=null)return captured; - DataFixer result=unwrap(net.minecraft.util.datafix.DataFixesManager.getDataFixer()); + DataFixer result=unwrap(net.minecraft.util.datafix.DataFixers.getDataFixer()); if(!result.getClass().getName().equals("com.mojang.datafixers.DataFixerUpper"))throw new AssertionError("Historical oracle is not the native fixer"); // Keep this independent reference when a synthetic counter later wraps // the global reader. Never accept that test wrapper as the oracle. diff --git a/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java b/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java index 58d6817..d880bd8 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java +++ b/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java @@ -1,46 +1,52 @@ package cyano.lootable.buildtest; import java.util.TreeSet; -import net.minecraft.nbt.*; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.FloatTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; /** Compound order is immaterial; NBT types, list order and every array byte matter. */ public final class TypedNbt { private TypedNbt() { } - public static boolean same(INBT left, INBT right) { + public static boolean same(Tag left, Tag right) { return canonical(left).equals(canonical(right)); } /** Native Attributes is a map; inventory and recovery lists stay ordered. */ - public static boolean sameEntity(CompoundNBT left,CompoundNBT right) { + public static boolean sameEntity(CompoundTag left,CompoundTag right) { return same(entityCopy(left),entityCopy(right)); } - private static CompoundNBT entityCopy(CompoundNBT input) { - CompoundNBT copy=input.copy(); + private static CompoundTag entityCopy(CompoundTag input) { + CompoundTag copy=input.copy(); if(!copy.contains("Attributes",9))return copy; - ListNBT attributes=copy.getList("Attributes",10); - if(attributes.size()!=((ListNBT)copy.get("Attributes")).size())return copy; - java.util.List entries=new java.util.ArrayList<>(); - for(INBT value:attributes) { - if(!(value instanceof CompoundNBT) || !((CompoundNBT)value).contains("Name",8))return copy; + ListTag attributes=copy.getList("Attributes",10); + if(attributes.size()!=((ListTag)copy.get("Attributes")).size())return copy; + java.util.List entries=new java.util.ArrayList<>(); + for(Tag value:attributes) { + if(!(value instanceof CompoundTag) || !((CompoundTag)value).contains("Name",8))return copy; entries.add(value); } entries.sort(java.util.Comparator.comparing(TypedNbt::canonical)); - ListNBT sorted=new ListNBT();entries.forEach(sorted::add);copy.put("Attributes",sorted);return copy; + ListTag sorted=new ListTag();entries.forEach(sorted::add);copy.put("Attributes",sorted);return copy; } - public static String canonical(INBT tag) { + public static String canonical(Tag tag) { if(tag==null)return "null"; StringBuilder text=new StringBuilder().append(tag.getId()).append(':'); - if(tag instanceof CompoundNBT) { - CompoundNBT compound=(CompoundNBT)tag; - for(String key:new TreeSet<>(compound.keySet()))text.append(key.length()).append(':').append(key).append('=').append(canonical(compound.get(key))).append(';'); - } else if(tag instanceof ListNBT) { - ListNBT list=(ListNBT)tag;text.append(list.getTagType()).append('/').append(list.size()).append('['); + if(tag instanceof CompoundTag) { + CompoundTag compound=(CompoundTag)tag; + for(String key:new TreeSet<>(compound.getAllKeys()))text.append(key.length()).append(':').append(key).append('=').append(canonical(compound.get(key))).append(';'); + } else if(tag instanceof ListTag) { + ListTag list=(ListTag)tag;text.append(list.getElementType()).append('/').append(list.size()).append('['); for(int i=0;i=0)chunk.putInt("DataVersion",version);chunk.putIntArray("Position",new int[]{0,0}); + ListTag entities=new ListTag();for(CompoundTag root:roots)entities.add(root.copy());chunk.put("Entities",entities);return chunk; + } + public static Path write(Path world,CompoundTag chunk) throws Exception { + Path folder=world.resolve("entities");Files.createDirectories(folder);Path path=folder.resolve("r.0.0.mca"); + try(RegionFile region=new RegionFile(path.toFile(),folder.toFile(),false)) { + try(DataOutputStream output=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(chunk,output);} + } + return path; + } +} diff --git a/tools/evidence_policy.py b/tools/evidence_policy.py new file mode 100644 index 0000000..5a58f3f --- /dev/null +++ b/tools/evidence_policy.py @@ -0,0 +1,53 @@ +"""Fail-closed planning for reuse of an explicitly approved qualification receipt. + +This plans work; it never starts a build, sets hosted check statuses or publishes. +The approved receipt digest and server-fetched run metadata must come from the +trusted release controller, not from the downloaded receipt itself. +""" +import hashlib,json +from pathlib import Path + +IDENTITY_FIELDS=('target','source_tree','artifacts','probe','test_tools','toolchains', + 'sdk','fixture_bundle','source_worlds','fixture_environment','platform','coverage') + +def sha(path):return hashlib.sha256(Path(path).read_bytes()).hexdigest().upper() + +def identity(subject): + if any(name not in subject or not subject[name] for name in IDENTITY_FIELDS): + raise ValueError('Incomplete qualification identity') + return hashlib.sha256(json.dumps({name:subject[name] for name in IDENTITY_FIELDS}, + sort_keys=True,separators=(',',':')).encode()).hexdigest().upper() + +def plan(subject,receipt_path,approved_sha,evidence_root,api_runs,required_gates): + def require(value,reason): + if not value:raise ValueError(reason) + try: + root=Path(evidence_root).resolve();path=Path(receipt_path).resolve() + require(path.is_relative_to(root) and path.is_file(),'Receipt missing or outside owned evidence') + require(sha(path)==approved_sha,'Receipt differs from independently approved digest') + receipt=json.loads(path.read_text()) + require(receipt['schema']==1,'Unknown qualification receipt schema') + require(receipt['identity']==identity(receipt['subject'])==identity(subject),'Qualification inputs changed') + required=set(required_gates) + require(required and set(receipt['required_gates'])==required and len(receipt['required_gates'])==len(required),'Coverage contract changed') + require(set(receipt['gates'])==required and all(receipt['gates'][gate] is True for gate in required),'Missing or failed functional gate') + allowed={'.github/workflows/'+name for name in ('build.yml','migration-matrix.yml','codeql-analysis.yml','gradle-wrapper-validation.yml','release-workflow-checks.yml')} + requested=receipt['runs'];actual={run['run']:run for run in api_runs} + require(requested and len(actual)==len(api_runs)==len(requested),'Missing or duplicate server run metadata') + require(len({run['run'] for run in requested})==len(requested),'Duplicate qualification run') + for recorded in requested: + run=actual.get(recorded['run']) + require(run is not None and run==recorded,'Run provenance changed') + require(run['repository']=='MinecraftModDevelopmentMods/LootableBodies','Untrusted qualification repository') + require(run['head']==receipt['qualified_commit'] and run['workflow'] in allowed,'Stale source or unreviewed workflow') + require(run['status']=='completed' and run['conclusion']=='success','Qualification run is not successful') + require(receipt['evidence'],'Missing actual qualification artifacts') + for name,expected in receipt['evidence'].items(): + target=(root/name).resolve() + require(target.is_relative_to(root) and target.is_file() and sha(target)==expected,'Missing, escaped or changed qualification artifact: '+name) + return {'action':'reuse','reason':'All qualification inputs and independently approved evidence match', + 'fresh_execution':False,'evidence_label':'reused original qualification', + 'qualified_commit':receipt['qualified_commit'],'qualification_run_ids':[run['run'] for run in requested], + 'fresh_merge_and_publication_checks_required':True} + except (OSError,KeyError,TypeError,ValueError) as error: + return {'action':'run','reason':str(error),'fresh_execution':True} diff --git a/tools/fixture_login.py b/tools/fixture_login.py index ca5d020..d8c895d 100644 --- a/tools/fixture_login.py +++ b/tools/fixture_login.py @@ -41,8 +41,8 @@ def planned_disconnect(home, role, phase): for line in plain(log.read_text(encoding='utf-8',errors='replace')).splitlines()) class LoginMonitor: - def __init__(self, home): - self.home=Path(home);self.seen=0;self.counts={'A':0,'B':0};self.visibility_phase=None + def __init__(self, home, legacy_recovery=True): + self.home=Path(home);self.seen=0;self.counts={'A':0,'B':0};self.visibility_phase=None;self.legacy_recovery=legacy_recovery def poll(self): text=(self.home/'server.log').read_text(encoding='utf-8',errors='replace') lines=plain(text[:text.rfind('\n')+1]).splitlines() @@ -54,6 +54,7 @@ def poll(self): (self.home/'sync/client-A-joined').touch() match=FAILURE.fullmatch(line) if match: + if not self.legacy_recovery:raise RuntimeError('Legacy login recovery is disabled for native17: '+line) role=match[1];self.counts[role]+=1 if self.counts[role]>LIMIT:raise RuntimeError('Forge login retry limit: '+role) (self.home/'sync'/('login-retry-'+role)).write_text(str(self.counts[role])) diff --git a/tools/fixtures/forge37-server-default.toml b/tools/fixtures/forge37-server-default.toml new file mode 100644 index 0000000..3fe21d6 --- /dev/null +++ b/tools/fixtures/forge37-server-default.toml @@ -0,0 +1,18 @@ + +#Server configuration settings +[server] + #Base zombie summoning spawn chance. Allows changing the bonus zombie summoning mechanic. + #Range: 0.0 ~ 1.0 + zombieBaseSummonChance = 0.1 + #Chance that a zombie (or subclass) is a baby. Allows changing the zombie spawning mechanic. + #Range: 0.0 ~ 1.0 + zombieBabyChance = 0.05 + #Set this to true to check the entire entity's collision bounding box for ladders instead of just the block they are in. Causes noticeable differences in mechanics so default is vanilla behavior. Default: false. + fullBoundingBoxLadders = false + #Fix advancement loading to use a proper topological sort. This may have visibility side-effects and can thus be turned off if needed for data-pack compatibility. + fixAdvancementLoading = true + #Vanilla will treat crafting recipes using empty tags as air, and allow you to craft with nothing in that slot. This changes empty tags to use BARRIER as the item. To prevent crafting with air. + treatEmptyTagsAsAir = false + #Set this to true to remove any BlockEntity that throws an error in its update method instead of closing the server and reporting a crash log. BE WARNED THIS COULD SCREW UP EVERYTHING USE SPARINGLY WE ARE NOT RESPONSIBLE FOR DAMAGES. + removeErroringBlockEntities = false + diff --git a/tools/fixtures/native117-flat-level.dat b/tools/fixtures/native117-flat-level.dat new file mode 100644 index 0000000..dee525e Binary files /dev/null and b/tools/fixtures/native117-flat-level.dat differ diff --git a/tools/fixtures/native37-delayed-movement-stack.txt b/tools/fixtures/native37-delayed-movement-stack.txt new file mode 100644 index 0000000..4875a6e --- /dev/null +++ b/tools/fixtures/native37-delayed-movement-stack.txt @@ -0,0 +1,21 @@ +java.lang.NullPointerException: Cannot invoke "it.unimi.dsi.fastutil.objects.ObjectSet.remove(Object)" because "objectset" is null + at net.minecraft.server.level.DistanceManager.m_140828_(DistanceManager.java:230) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.level.ChunkMap.m_140184_(ChunkMap.java:863) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.level.ServerChunkCache.m_8385_(ServerChunkCache.java:433) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.network.ServerGamePacketListenerImpl.m_7185_(ServerGamePacketListenerImpl.java:849) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.network.protocol.game.ServerboundMovePlayerPacket.m_5797_(SourceFile:114) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.network.protocol.game.ServerboundMovePlayerPacket$Pos.m_5797_(SourceFile:42) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.network.protocol.PacketUtils.m_131356_(SourceFile:21) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.TickTask.run(SourceFile:18) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.util.thread.BlockableEventLoop.m_6367_(SourceFile:151) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.util.thread.ReentrantBlockableEventLoop.m_6367_(SourceFile:23) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.MinecraftServer.m_6367_(MinecraftServer.java:776) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.MinecraftServer.m_6367_(MinecraftServer.java:162) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.util.thread.BlockableEventLoop.m_7245_(SourceFile:125) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.MinecraftServer.m_129961_(MinecraftServer.java:759) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.MinecraftServer.m_7245_(MinecraftServer.java:753) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.util.thread.BlockableEventLoop.m_18701_(SourceFile:134) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.MinecraftServer.m_130012_(MinecraftServer.java:739) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.MinecraftServer.m_130011_(MinecraftServer.java:688) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at net.minecraft.server.MinecraftServer.m_177918_(MinecraftServer.java:258) ~[server-1.17.1-20210706.113038-srg.jar%2327!:?] + at java.lang.Thread.run(Thread.java:831) [?:?] diff --git a/tools/migration_ci.py b/tools/migration_ci.py index ec4d383..043889d 100644 --- a/tools/migration_ci.py +++ b/tools/migration_ci.py @@ -18,6 +18,20 @@ def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest().upper() +def native_input_paths(project, expected=None): + from native_runtime import SERVER_INPUTS + native=project/'build/native-server-distribution' + expected=SERVER_INPUTS if expected is None else expected + if not expected:raise ValueError('Missing native SDK manifest') + paths=[] + for name,sha in sorted(expected.items()): + path=native/name + if not path.resolve().is_relative_to(native.resolve()) or path.is_symlink() or not path.is_file() or digest(path)!=sha: + raise ValueError('Missing or changed pinned native SDK input: '+name) + paths.append(safe_name(path.relative_to(project).as_posix())) + return paths + + def partition(origins, shard, total=6): if total < 1 or not 0 <= shard < total or len(origins) != len(set(origins)): raise ValueError('Invalid or duplicate migration shard') @@ -129,7 +143,7 @@ def run_shard(project, java, shard, commit, total=12): if digest(project / name) != manifest['files'][name]['sha256']: raise ValueError('Prepared candidate/probe changed') args = SimpleNamespace(java=java, origins=origins, variants=['normal','stamps'], seeds=[seeds], - forge116_home=project/'build/native-server-distribution', output=project/'build/migration-results') + forge117_home=project/'build/native-server-distribution', prepared_sources=None, output=project/'build/migration-results') verify(args, project) abort_root = project / 'build/migration-abort';abort_root.mkdir(exist_ok=False) aborts = {} @@ -142,7 +156,7 @@ def run_shard(project, java, shard, commit, total=12): shutil.copytree(source, selected);changes = patch_stamps(selected, abort_root) with (abort_root / (name + '.log')).open('x') as log: subprocess.run([sys.executable,str(project/'tools/verify_upgrade_abort.py'),'--java',str(java), - '--forge-home',str(args.forge116_home),'--source-world',str(selected), + '--forge-home',str(args.forge117_home),'--source-world',str(selected), '--output',str(abort_root/name)],cwd=project,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=600) result = json.loads((abort_root/name/'result.json').read_text()) aborts[name] = {'conditions':result['cases'], 'stamp_mutations':changes} @@ -156,6 +170,10 @@ def run_shard(project, java, shard, commit, total=12): report = dict(positive, head=commit, aborts=aborts, source_seals={origin:seed_report[origin]['world'] for origin in origins}, elapsed_seconds=round(time.monotonic()-started,3)) aggregate([report],origins,commit) + from native_evidence import audit_shard + report['native_audit']=audit_shard(project/'build',report) + if len(report['native_audit']['receipts'])!=len(report['cases'])*12: + raise ValueError('Every native positive/refusal receipt is required') destination = project/'build/migration-ci/report.json';destination.parent.mkdir(exist_ok=True) destination.write_text(json.dumps(report,indent=2)+'\n') print('MIGRATION_SHARD_PASS',shard,'cases='+str(len(report['cases'])),'seconds='+str(report['elapsed_seconds']),flush=True) @@ -174,9 +192,7 @@ def main(): if actual != args.commit:raise ValueError('Checkout does not match the declared input commit') if args.mode=='seal': paths=['build/libs/'+MAIN_JAR,'build/integration-test-mod/lootablebodies-build-tests-reobf.jar','build/migration-seeds/seed-report.json'] - native=project/'build/native-server-distribution' - paths += [p.relative_to(project).as_posix() for p in native.glob('*.jar') if 'installer' not in p.name] - paths += [p.relative_to(project).as_posix() for p in (native/'libraries').rglob('*.jar')] + paths += native_input_paths(project) report=json.loads((project/'build/migration-seeds/seed-report.json').read_text())['origins'] if set(report)!=set(ORIGINS):raise ValueError('Missing prepared historical worlds') for origin,details in report.items(): @@ -189,9 +205,16 @@ def main(): if args.java is None or args.shard is None:parser.error('--java and --shard are required') run_shard(project,args.java.resolve(),args.shard,args.commit,total=args.shards) else: - reports=[json.loads(p.read_text()) for p in sorted(args.folder.glob('*/migration-ci/report.json'))] + reports=[];audits=[] + from native_evidence import audit_shard + for path in sorted(args.folder.glob('*/migration-ci/report.json')): + report=json.loads(path.read_text());proof=audit_shard(path.parent.parent,report,relocated=True) + if len(proof['receipts'])!=len(report['cases'])*12:raise ValueError('Missing downloaded native receipts') + reports.append(report);audits.append(proof) if len(reports)!=args.shards:raise ValueError('Every declared migration shard is required') result=aggregate(reports,list(ORIGINS),args.commit) + result['native_audits']=audits + if sum(len(proof['receipts']) for proof in audits)!=len(ORIGINS)*24:raise ValueError('Incomplete native receipt total') output=project/'build/migration-summary.json';output.write_text(json.dumps(result,indent=2)+'\n') print('MIGRATION_MATRIX_PASS cases='+str(len(result['cases']))+' phases='+str(result['jvm_phases'])+' unsafe_aborts='+str(result['abort_cases'])) diff --git a/tools/native117_ci.py b/tools/native117_ci.py new file mode 100644 index 0000000..a494517 --- /dev/null +++ b/tools/native117_ci.py @@ -0,0 +1,126 @@ +"""Execute explicit native117 CI stages once, retaining every failed command.""" +import argparse,hashlib,json,os,shutil,subprocess,sys,tempfile,time +from pathlib import Path +sys.dont_write_bytecode=True +from command_logs import run_build_input_logged +from release_identity import MAIN_JAR,MOD_VERSION +from native_runtime import digest +PROJECT=Path(__file__).resolve().parents[1] + +def check_bundle(bundle): + expected=json.loads((PROJECT/'.github/release117.json').read_text()) + if expected['version']!=MOD_VERSION:raise ValueError('Wrong approved release version') + actual={p.name:digest(p) for p in bundle.iterdir() if p.is_file()} + if actual!=expected['files']:raise ValueError('Built six-file bundle differs from accepted release') + return actual + +def run(name,args,*,timeout=1800): + path=PROJECT/'build/ci117-commands'/name;path.parent.mkdir(parents=True,exist_ok=True) + if path.exists():raise FileExistsError(path) + with path.open('x') as output: + result=subprocess.run([str(x) for x in args],cwd=PROJECT,stdout=output,stderr=subprocess.STDOUT,timeout=timeout,env=dict(os.environ,PYTHONDONTWRITEBYTECODE='1')) + if result.returncode:raise RuntimeError('Failed command; retained '+str(path)) + +def python(name,tool,*args,timeout=1800):run(name,[sys.executable,'-B',str(PROJECT/'tools'/tool),*args],timeout=timeout) + +def gradle(name,*tasks,offline=False): + args=[str(PROJECT/('gradlew.bat' if os.name=='nt' else 'gradlew')),*tasks,'-PminecraftAssetsDirectory=build/native-assets','--no-daemon','--console=plain','--stacktrace','--no-build-cache'] + if offline:args.append('--offline') + # Only exact pre-task input transport failures receive the established + # bounded sequential retry. Runtime and test failures are never retried. + run_build_input_logged(args,cwd=PROJECT,path=PROJECT/'build/ci117-commands'/name,timeout=3600) + +def build(): + cache=Path(os.environ['GRADLE_USER_HOME']) + if (cache/'caches').exists() or (PROJECT/'.gradle').exists():raise ValueError('The authoritative first build requires a fresh Gradle cache and checkout') + first=Path(os.environ.get('RUNNER_TEMP',tempfile.gettempdir()))/'lootablebodies117-first-build' + first.mkdir(exist_ok=False) + # Commands survive clean; no previous evidence directory is overwritten. + tasks=('clean','build','javadoc','verifyEclipseProductionClasspath','preparePackagedIntegrationProbe','preparePackagedForgeControl','prepareReleaseBundle') + for number in (1,2): + log=first/('build-'+str(number)+'.log') + args=[str(PROJECT/('gradlew.bat' if os.name=='nt' else 'gradlew')),*tasks,'--no-daemon','--console=plain','--stacktrace','--no-build-cache'] + if number==2:args.append('--offline') + run_build_input_logged(args,cwd=PROJECT,path=log,timeout=3600) + bundle=PROJECT/'build/release/bundle';current=check_bundle(bundle) + target=first/str(number);shutil.copytree(PROJECT/'build/test-results',target/'test-results') + shutil.copytree(bundle,target/'bundle') + import xml.etree.ElementTree as ET + xml=list((target/'test-results/test').glob('TEST-*.xml'));assert xml + tests=sum(int(ET.parse(f).getroot().attrib['tests']) for f in xml) + assert tests>=223 and all(int(ET.parse(f).getroot().attrib.get(k,0))==0 for f in xml for k in ('failures','errors','skipped')) + if number==1:expected=current + else:assert current==expected,'Two clean uncached six-file builds differ' + target=PROJECT/'build/ci117-build-proof';shutil.copytree(first,target) + (target/'result.json').write_text(json.dumps({'two_clean_uncached_builds':True,'second_build_offline':True,'junit_each':tests,'files':expected},indent=2)+'\n') + +def prepare(): + java=Path(os.environ['JAVA_HOME_16_X64'])/'bin/java' + java8=Path(os.environ['JAVA_HOME_8_X64'])/'bin/java' + python('native-server-install.log','native_runtime.py','--java',java,'--output','build/native-server-distribution') + python('native-client-install.log','native_runtime.py','--java',java,'--output','build/native-client-distribution','--client') + from native_runtime import prepare_server,run_java,HISTORICAL_JVM_ARGS + from audit_runtime_logs import audit + control=PROJECT/'build/control-test-mod/forge-platform-control-reobf.jar' + resource_proof=[] + for number in range(4): + home=PROJECT/'build/native37-resource-budget'/str(number) + prepare_server(home,PROJECT/'build/native-server-distribution',probe=control) + result=run_java(java,home,'resource-50',jvm_args=[*HISTORICAL_JVM_ARGS,'-Dlootablebodies.control.resourceReloads=50','-Dlog4j.configurationFile='+(PROJECT/'src/integrationTest/resources/log4j2.xml').as_uri()]) + assert result.returncode==0 and (home/'resource-control.pass').is_file() and not (home/'world').exists() + logs=[home/('resource-50'+suffix) for suffix in ('.log','.latest.log','.debug.log')] + resource_proof.append({'rounds':50,'no_world':True,'logs':[audit(f,native=True) for f in logs],'receipt_sha256':digest(home/'resource-50.diagnostics/result.json')}) + (PROJECT/'build/native37-resource-budget/result.json').write_text(json.dumps({'actual_native_reloads':200,'jvms':4,'platform':sys.platform,'control_jar':digest(control),'proof':resource_proof},indent=2)+'\n') + gradle('assets-and-mapped.log','prepareMinecraftAssets','integrationTest','upgradeContractTest','clientIntegrationTest') + python('ordinary-development.log','verify_normal_server.py','--java',java,'--forge-home','build/native-server-distribution','--output','build/ordinary-development','--development',*flat()) + python('native116-writer.log','prepare_native116_tools.py','--java17-home',os.environ['JAVA_HOME'],'--java8-home',os.environ['JAVA_HOME_8_X64'],'--java25-home',os.environ['JAVA_HOME_25_X64']) + python('native116-originals.log','prepare_native117_history.py','--download','build/prior116-download','--java8',java8) + commit=subprocess.check_output(['git','rev-parse','HEAD'],cwd=PROJECT,text=True).strip() + python('seal-history.log','migration_ci.py','seal','--commit',commit) + from migration_ci import seal + roots=['build/libs','build/release/bundle','build/integration-test-mod','build/control-test-mod','build/native-server-distribution','build/native-client-distribution','build/native-assets'] + paths=[p.relative_to(PROJECT).as_posix() for root in roots for p in (PROJECT/root).rglob('*') if p.is_file()] + seal(PROJECT,paths,PROJECT/'build/ci117-platform-inputs',commit) + +def flat(): + p=PROJECT/'tools/fixtures/native117-flat-level.dat' + return ['--flat-header',str(p),'--flat-header-sha256',digest(p)] + +def scenario(group): + java=Path(os.environ['JAVA_HOME_16_X64'])/'bin/java' + server=['--java',java,'--forge-home','build/native-server-distribution'] + client=['--forge-client-home','build/native-client-distribution','--client-inputs','build/native-client-distribution/prepared-inputs','--assets','build/native-assets'] + root=Path('build/ci117-scenarios')/group + if root.exists():raise FileExistsError(root) + root.mkdir(parents=True) + if group=='servers': + python('packaged.log','verify_packaged.py',*server,*flat(),'--output',root/'packaged') + python('ordinary-packaged.log','verify_normal_server.py',*server,*flat(),'--output',root/'ordinary') + python('malformed-recovery.log','verify_nbt_recovery.py',*server,*flat(),'--output',root/'nbt') + elif group in ('default','config','skin','issues','names','rejected'): + mode='issues' if group=='rejected' else group + extra=['--rejected-displaced-viewer'] if group=='rejected' else [] + python(group+'.log','verify_packaged_client.py','--java',java,*client,'--mode',mode,'--output',root/'client',*extra) + elif group=='multiplayer':python('multiplayer.log','verify_multiplayer.py',*server,*client,*flat(),'--output',root/'run') + elif group=='visibility': + python('visibility.log','verify_visibility.py',*server,*client,*flat(),'--output',root/'live','--mode','visibility',timeout=1800) + python('visibility-restart.log','verify_visibility.py',*server,*client,*flat(),'--output',root/'restart','--mode','visibility','--source-server',root/'live/server',timeout=1800) + elif group=='control': + log=PROJECT/'build/ci117-commands/native-platform-control.log';log.parent.mkdir(parents=True,exist_ok=True) + with log.open('x') as output: + result=subprocess.run([sys.executable,'-B',str(PROJECT/'tools/verify_native_respawn_control.py'),*[str(x) for x in server+client+flat()], '--output',str(root/'run')],cwd=PROJECT,stdout=output,stderr=subprocess.STDOUT,timeout=900) + from native37_control_evidence import classify + classify(PROJECT/root/'run',PROJECT/'build/control-test-mod/forge-platform-control-reobf.jar',result.returncode) + elif group=='respawn': + for variant,extra in [('ordinary',[]),('simultaneous',['--simultaneous']),('collision',['--counter-collision']),('restart',['--simultaneous','--counter-collision','--source-server',root/'ordinary/server'])]: + python('respawn-'+variant+'.log','verify_visibility.py',*server,*client,*flat(),'--output',root/variant,'--mode','respawn','--cases','16',*extra,timeout=1800) + else:raise ValueError(group) + (root/'complete.json').write_text(json.dumps({'group':group,'candidate':digest(PROJECT/'build/libs'/MAIN_JAR),'complete':True})+'\n') + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('mode',choices=['build','prepare','scenario']);p.add_argument('--group');a=p.parse_args() + if a.mode=='build':build() + elif a.mode=='prepare':prepare() + else:scenario(a.group) + +if __name__=='__main__':main() diff --git a/tools/native117_client.py b/tools/native117_client.py new file mode 100644 index 0000000..062f78c --- /dev/null +++ b/tools/native117_client.py @@ -0,0 +1,164 @@ +"""Pinned native37 client preparation and descriptor-driven Java16 launch.""" +import hashlib +import json +import os +from pathlib import Path +import platform +import re +import zipfile + +BASE_VERSION = '1.17.1' +VERSION = '1.17.1-forge-37.1.1' +BASE_SHA1 = 'e0e7ab5ed6f55bbd874ef95be3c9356d67e64b57' +FORGE_SHA256 = '3D4E955ED77A397615C9AC9593E442C44ACD0E7D6C1AA775593E54588E087584' + + +def descriptors(runtime): + runtime = Path(runtime).resolve() + base = runtime/'versions'/BASE_VERSION/(BASE_VERSION+'.json') + forge = runtime/'versions'/VERSION/(VERSION+'.json') + if not base.is_file() or hashlib.sha1(base.read_bytes()).hexdigest() != BASE_SHA1: + raise RuntimeError('Incorrect official native1.17.1 client descriptor') + if not forge.is_file() or hashlib.sha256(forge.read_bytes()).hexdigest().upper() != FORGE_SHA256: + raise RuntimeError('Incorrect installed native37 client descriptor') + return json.loads(base.read_text()), json.loads(forge.read_text()) + + +def prepare(runtime, output): + """Download only missing official client inputs into a NEW owned root. + + The installer SDK and shared Gradle cache remain read-only. Its generated + Forge modules are reused in place; no cache or installation is copied. + """ + from native_runtime import allowed, _download, digest, validate_client_paths + runtime, output = Path(runtime).resolve(), Path(output).resolve() + base, forge = descriptors(runtime) + output.mkdir(parents=True, exist_ok=False) + system = {'Windows':'windows', 'Darwin':'osx', 'Linux':'linux'}[platform.system()] + architecture = 'x86' if platform.architecture()[0] == '32bit' else 'amd64' + natives = output/'natives'; natives.mkdir() + modules, inputs = {}, {} + + def artifact(record, *, generated=False): + relative = Path(record['path']) + if relative.is_absolute() or '..' in relative.parts: + raise ValueError('Unsafe descriptor artifact path') + original = runtime/'libraries'/relative + target = original if original.is_file() else output/'libraries'/relative + if target == original or generated: + if not target.is_file() or hashlib.sha1(target.read_bytes()).hexdigest() != record['sha1']: + raise RuntimeError('Missing or changed installer input: '+str(original)) + else: + _download(record, target) + inputs[str(target)] = digest(target) + return target + + for metadata in (base, forge): + for library in metadata['libraries']: + if not allowed(library, system, architecture): + continue + downloads = library.get('downloads', {}) + if downloads.get('artifact'): + path = artifact(downloads['artifact'], generated=metadata is forge) + parts = library['name'].split(':') + modules[':'.join(parts[:2]+parts[3:])] = 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) + 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: + raise RuntimeError('Missing or changed official client jar') + paths = list(modules.values())+[client] + validate_client_paths(paths) + for path in [client, *natives.iterdir(), runtime/'versions'/BASE_VERSION/(BASE_VERSION+'.json'), runtime/'versions'/VERSION/(VERSION+'.json')]: + inputs[str(path)] = digest(path) + record = {'version':VERSION, 'system':system, 'architecture':architecture, + 'sdk':str(runtime), 'library_directory':str(runtime/'libraries'), + 'classpath':[str(p) for p in paths], 'natives':str(natives), 'files':inputs} + (output/'inputs.json').write_text(json.dumps(record,indent=2)+'\n') + return record + + +def load(runtime, prepared): + from native_runtime import digest, validate_client_paths + base, forge = descriptors(runtime) + record = json.loads((Path(prepared)/'inputs.json').read_text()) + if record['version'] != VERSION or Path(record['sdk']) != Path(runtime).resolve(): + raise RuntimeError('Prepared client belongs to another SDK') + system = {'Windows':'windows', 'Darwin':'osx', 'Linux':'linux'}[platform.system()] + architecture = 'x86' if platform.architecture()[0] == '32bit' else 'amd64' + if (record['system'], record['architecture']) != (system, architecture): + raise RuntimeError('Prepared client belongs to another platform') + for name, expected in record['files'].items(): + if not Path(name).is_file() or digest(Path(name)) != expected: + raise RuntimeError('Changed prepared native client input: '+name) + paths = [Path(p) for p in record['classpath']] + if any(str(path) not in record['files'] for path in paths): + raise RuntimeError('Unsealed client classpath entry') + validate_client_paths(paths) + return base, forge, record + + +def expand(arguments, values, *, system, architecture, os_version): + result = [] + for argument in arguments: + if isinstance(argument, dict): + permitted = not argument.get('rules') + for rule in argument.get('rules', []): + target = rule.get('os', {}) + matches = target.get('name',system) == system and target.get('arch',architecture) == architecture + if 'version' in target: + matches = matches and re.search(target['version'], os_version) is not None + for feature, expected in rule.get('features', {}).items(): + matches = matches and {'is_demo_user':False,'has_custom_resolution':True}.get(feature,False) == expected + if matches: + permitted = rule['action'] == 'allow' + if not permitted: + continue + argument = argument['value'] + for part in argument if isinstance(argument,list) else [argument]: + rendered = re.sub(r'\$\{([^}]+)\}',lambda match:values[match[1]],part) + if '${' in rendered: + raise RuntimeError('Unexpanded launcher argument') + result.append(rendered) + return result + + +def command(java, home, runtime, assets, prepared, *, username, extra=()): + java, home, assets = Path(java).resolve(), Path(home).resolve(), Path(assets).resolve() + release = dict(line.split('=',1) for line in (java.parent.parent/'release').read_text().splitlines() if '=' in line) + if release.get('SEMANTIC_VERSION') != '"16.0.2+7"' or release.get('IMPLEMENTOR') != '"Eclipse Foundation"': + raise RuntimeError('Native client requires exact Temurin16.0.2+7') + 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.17 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', + # Native37 explicitly selects OfflineSocialInteractions for0. + 'auth_access_token':'0','user_type':'legacy','version_type':'release', + 'resolution_width':'854','resolution_height':'480'} + options = dict(system=record['system'],architecture=record['architecture'],os_version=platform.version()) + # The inherited profile's game version differs from the actual vanilla jar + # filename. Forge's JVM ignoreList must exclude that precise base jar from + # the transforming module layer, which already contains the native SRG jar. + jvm_values = dict(values,version_name=BASE_VERSION) + jvm = expand(base['arguments']['jvm']+forge['arguments']['jvm'],jvm_values,**options) + game = expand(base['arguments']['game']+forge['arguments']['game'],values,**options) + return [str(java),'-Xmx2G',*jvm,*extra,forge['mainClass'],*game] diff --git a/tools/native117_persistence.py b/tools/native117_persistence.py new file mode 100644 index 0000000..163ea21 --- /dev/null +++ b/tools/native117_persistence.py @@ -0,0 +1,40 @@ +"""Read-only persistence checks for native17 output, not old-writer evidence.""" +from nbt_regions import Tag, read_region +import re +import struct + +def count_persisted(world,wave): + if wave not in (-1,0,1,2):raise ValueError('Invalid loaded 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') + # Loaded native17 bodies must leave terrain storage, including legacy IDs. + for region in world.rglob('region/r.*.mca'): + if not selected(region):continue + for chunk in read_region(region).values(): + level=chunk['root'].value.get('Level') + if level is None:raise ValueError('Unexpected target terrain shape') + for entity in level.value.get('Entities',Tag(9,(10,[]))).value[1]: + if entity.value.get('id',Tag(8,'')).value in names: + raise ValueError('Loaded corpse remained in terrain storage') + count=0;seen=set() + for region in world.rglob('entities/r.*.mca'): + if not selected(region):continue + coordinates=re.fullmatch(r'r\.(-?[0-9]+)\.(-?[0-9]+)\.mca',region.name) + if coordinates is None:raise ValueError('Unknown entity region name') + rx,rz=map(int,coordinates.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,2730):raise ValueError('Wrong native17 entity-chunk data version') + if position is None or position.kind!=11 or len(position.value)!=8 or struct.unpack('>ii',position.value)!=(rx*32+(index&31),rz*32+(index>>5)): + raise ValueError('Wrong native entity-chunk Position/index') + if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native 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,7): + raise ValueError('Wrong native17 persisted corpse identity/marker') + identity=entity.value.get('UUID') + if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native17 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/native117_server_inputs.json b/tools/native117_server_inputs.json new file mode 100644 index 0000000..b561376 --- /dev/null +++ b/tools/native117_server_inputs.json @@ -0,0 +1,81 @@ +{ + "libraries/com/electronwill/night-config/core/3.6.3/core-3.6.3.jar": "14E9554E9C48ABFE7449E4451E597BFC0D85074FC6C80A377488C067C313FD7C", + "libraries/com/electronwill/night-config/toml/3.6.3/toml-3.6.3.jar": "721B9484F2C35E1A051D5D72254B76AB98AE799858BD5DAA8217A7124EF728DF", + "libraries/com/github/jponge/lzma-java/1.3/lzma-java-1.3.jar": "75CCC0E24D37ED5B8916E1BCFFAD1E067C2B1CFA21B635287F968DB5C3C1963A", + "libraries/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar": "766AD2A0783F2687962C8AD74CEECC38A28B9F72A2D085EE438B7813E928D0C7", + "libraries/com/google/code/gson/gson/2.8.0/gson-2.8.0.jar": "C6221763BD79C4F1C3DC7F750B5F29A0BB38B367B81314C4F71896E340C40825", + "libraries/com/google/code/gson/gson/2.8.7/gson-2.8.7.jar": "67BD19C510ED227D8A9DC5F67F1B4B2F3426853F5EFF02E1D9EA7E95F4923BA0", + "libraries/com/google/errorprone/error_prone_annotations/2.1.3/error_prone_annotations-2.1.3.jar": "03D0329547C13DA9E17C634D1049EA2EAD093925E290567E1A364FD6B1FC7FF8", + "libraries/com/google/errorprone/error_prone_annotations/2.3.4/error_prone_annotations-2.3.4.jar": "BAF7D6EA97CE606C53E11B6854BA5F2CE7EF5C24DDDF0AFA18D1260BD25B002C", + "libraries/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar": "A171EE4C734DD2DA837E4B16BE9DF4661AFAB72A41ADAF31EB84DFDAF936CA26", + "libraries/com/google/guava/guava/20.0/guava-20.0.jar": "36A666E3B71AE7F0F0DCA23654B67E086E6C93D192F60BA5DFD5519DB6C288C8", + "libraries/com/google/guava/guava/21.0/guava-21.0.jar": "972139718ABC8A4893FA78CBA8CF7B2C903F35C97AAF44FA3031B0669948B480", + "libraries/com/google/guava/guava/25.1-jre/guava-25.1-jre.jar": "6DB0C3A244C397429C2E362EA2837C3622D5B68BB95105D37C21C36E5BC70ABF", + "libraries/com/google/guava/guava/30.1-android/guava-30.1-android.jar": "EA0B5A62B707482EEE5CF305C8F35EF91CF4CEAFF504F011A5C49C41355F5781", + "libraries/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar": "B372A037D4230AA57FBEFFDEF30FD6123F9C0C2DB85D0ACED00C91B974F33F99", + "libraries/com/google/j2objc/j2objc-annotations/1.1/j2objc-annotations-1.1.jar": "2994A7EB78F2710BD3D3BFB639B2C94E219CEDAC0D4D084D516E78C16DDDECF6", + "libraries/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar": "21AF30C92267BD6122C0E0B4D20CCCB6641A37EAF956C6540EC471D584E64A7B", + "libraries/com/google/jimfs/jimfs/1.2/jimfs-1.2.jar": "DE16D5C8489729A8512F1A02FBD81F58F89249B72066987DA4CC5C87ECB9F72D", + "libraries/com/nothome/javaxdelta/2.0.1/javaxdelta-2.0.1.jar": "A661DFCB749053FC26EADA2A0A8E619A8EAD1CA9EDA29E1698687AA2CDD80152", + "libraries/com/opencsv/opencsv/4.4/opencsv-4.4.jar": "5D8969C14C7ACECC318A9321D34B1289AFD1BDD41C0E772667FD0A4F53E291A0", + "libraries/commons-beanutils/commons-beanutils/1.9.3/commons-beanutils-1.9.3.jar": "C058E39C7C64203D3A448F3ADB588CB03D6378ED808485618F26E137F29DAE73", + "libraries/commons-collections/commons-collections/3.2.2/commons-collections-3.2.2.jar": "EEEAE917917144A68A741D4C0DFF66AA5C5C5FD85593FF217BCED3FC8CA783B8", + "libraries/commons-io/commons-io/2.4/commons-io-2.4.jar": "CC6A41DC3EAACC9E440A6BD0D2890B20D36B4EE408FE2D67122F328BB6E01581", + "libraries/commons-logging/commons-logging/1.2/commons-logging-1.2.jar": "DADDEA1EA0BE0F56978AB3006B8AC92834AFEEFBD9B7E4E6316FCA57DF0FA636", + "libraries/cpw/mods/bootstraplauncher/0.1.17/bootstraplauncher-0.1.17.jar": "D57A076D6F6053F3B73790FD41CA8A5BD9FDCE619A3073C7310D9593ED510A8A", + "libraries/cpw/mods/modlauncher/9.0.7/modlauncher-9.0.7.jar": "1CC15015960778CAFB777CC08C7A6BF15B363FFDD7272BBD97CE8F84313B31E7", + "libraries/cpw/mods/securejarhandler/0.9.54/securejarhandler-0.9.54.jar": "823C9FF565C3F29013AB17D20A03E5BA178675F1F0D0A0E2B7B8355BBADB07DB", + "libraries/de/siegmar/fastcsv/2.0.0/fastcsv-2.0.0.jar": "665D52CB6F35CA236919BE9AC81653C208628507F91D6BF13479BC11496DB616", + "libraries/net/jodah/typetools/0.8.3/typetools-0.8.3.jar": "F853E21F7DE6BD192F0CE09E64A6C8B0E154C9CF9F34E62F08D5E26F73CE3B19", + "libraries/net/md-5/SpecialSource/1.10.0/SpecialSource-1.10.0.jar": "AF22317CF6A4B3929BF52ECA6E21229491DD5846F17AC9288BD42BDCFC586587", + "libraries/net/minecraft/server/1.17.1/server-1.17.1.jar": "E8C211B41317A9F5A780C98A89592ECB72EB39A6E475D4AC9657E5BC9FFAF55F", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar": "713AC4E3F19EC5347BDEC01CB168C7568C348952AF389C705D33AB10E30511B6", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-slim.jar": "AA6008E0FCB8A8514B43AEA5F23129F6E4D889AEE6361A8B884F7F874D3C3AAF", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-srg.jar": "A00E5004F190A0F221D8001B2E04609CE744A129F7764181AD10FBEFB1237EAA", + "libraries/net/minecraftforge/accesstransformers/8.0.4/accesstransformers-8.0.4.jar": "3225E2A513F658DF686FE8B2AE5B4808525851A2C238CE65E38847CC4FFC6441", + "libraries/net/minecraftforge/binarypatcher/1.0.12/binarypatcher-1.0.12.jar": "84672EF03A51325CD50AA22A141D3302C4F25915F8BD9188C927AE4308DAB4AC", + "libraries/net/minecraftforge/coremods/5.0.1/coremods-5.0.1.jar": "A0678F4D444C9D9AC816F91638308604576AA1297168E33A154538D1ACC0C1AC", + "libraries/net/minecraftforge/eventbus/5.0.3/eventbus-5.0.3.jar": "81F34DEA30F014AA45EC172370AE712476A6AA7B887428D9A85F9E2BD8EDA2F9", + "libraries/net/minecraftforge/fmlcore/1.17.1-37.1.1/fmlcore-1.17.1-37.1.1.jar": "400D3D56C4CC77132A5A169303C977846A7B50AE3E10290CCECCB70986005BB7", + "libraries/net/minecraftforge/fmlloader/1.17.1-37.1.1/fmlloader-1.17.1-37.1.1.jar": "1165B695D41A6A6DFA3BBE5DA7BE3A9E208EAA6ACB4FCB1669217CDFE5A9D300", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-server.jar": "35DB91BCD011CAD8B812BA165CD7F5AD934CEEEE9050621423F9E5D227D7F895", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-universal.jar": "2CC032AE95D60F03E62BE4929F3ECD66354E6ED135FA3CF676CED2B2F7910A6D", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/unix_args.txt": "60E81E217CB6B9C26FBCAFD2AEE9091C527EC411DD49D23EB5D7E855E7D881FE", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt": "B0E2D21DBA2E7C55A7DD9BBF914ADA99B124692810158F970F8F7C20543C2311", + "libraries/net/minecraftforge/forgespi/4.0.10/forgespi-4.0.10.jar": "5EC3C46F6B3EF3671AB4984D45807AE7B907E285FD6CF93B74E3626EB369397F", + "libraries/net/minecraftforge/installertools/1.2.7/installertools-1.2.7.jar": "F6F40D6ABD53F90FFB5B9A5E9B46E6436ACB9C1AC84E87B91DA24B520F5BF315", + "libraries/net/minecraftforge/jarsplitter/1.1.4/jarsplitter-1.1.4.jar": "7B7B2882921900B74CAD59008761CB18F7DA1A5BCA17CD7648251E0A65070067", + "libraries/net/minecraftforge/javafmllanguage/1.17.1-37.1.1/javafmllanguage-1.17.1-37.1.1.jar": "96D176680D936A459154AACBAE625B11FB7045A2CD40196D6B865D07F285E75D", + "libraries/net/minecraftforge/lex/vignette/0.2.0.16/vignette-0.2.0.16.jar": "747497B8E473D926C35937CF163971736B60E698D8F42F4CB1C602CF29B6646F", + "libraries/net/minecraftforge/mclanguage/1.17.1-37.1.1/mclanguage-1.17.1-37.1.1.jar": "7F68D4C69267ADD84B8FC14BB0C5D8B765E46A0B74B45CADA9FA8CA8C56D9826", + "libraries/net/minecraftforge/srgutils/0.4.3/srgutils-0.4.3.jar": "94027D592A75D9437845C5C5A9B21708697E48EC2143B991BD264B4C7D2C0C51", + "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/org/antlr/antlr4-runtime/4.9.1/antlr4-runtime-4.9.1.jar": "A80502C3140AE7ACFBB1F57847C8EB1C101461A969215EC38C3F1EBDFF61AC29", + "libraries/org/apache/commons/commons-collections4/4.2/commons-collections4-4.2.jar": "6A594721D51444FD97B3EAEFC998A77F606DEDB03DEF494F74755AEAD3C9DF3E", + "libraries/org/apache/commons/commons-lang3/3.8.1/commons-lang3-3.8.1.jar": "DAC807F65B07698FF39B1B07BFEF3D87AE3FD46D91BBF8A2BC02B2A831616F68", + "libraries/org/apache/commons/commons-text/1.3/commons-text-1.3.jar": "8185B3A5311092D83ED1F184C2D093B3105D726BBD76867C32B3511542BB99A8", + "libraries/org/apache/logging/log4j/log4j-api/2.15.0/log4j-api-2.15.0.jar": "C8C33E7E8E05496DAE69CF0CAAC8C3092CFFD937A164526E92922D2D566D0A55", + "libraries/org/apache/logging/log4j/log4j-core/2.15.0/log4j-core-2.15.0.jar": "419A8512895971B7B4F4F33E620D361254E5C9552B904B0474B09DDD4A6A220B", + "libraries/org/apache/logging/log4j/log4j-slf4j18-impl/2.15.0/log4j-slf4j18-impl-2.15.0.jar": "59FE1BAE704100690335039688CE1EDCD483201BA84D3CFEDED1D49C155B8862", + "libraries/org/apache/maven/maven-artifact/3.6.3/maven-artifact-3.6.3.jar": "61B20DA4E1E3DB837630D0810A6A7A5AC9FB7EA31B0A55638062AAD6E479C09C", + "libraries/org/cadixdev/atlas/0.2.2/atlas-0.2.2.jar": "F86933211EFC3CDE9747838BD3096FD2E01119A365B2143AF959EA3A11EB0ECD", + "libraries/org/cadixdev/bombe/0.3.5/bombe-0.3.5.jar": "288081906A4502B80FB0ED1FBF745516F08C111DB5CC79B90C97F10FBB07522D", + "libraries/org/cadixdev/bombe-asm/0.3.5/bombe-asm-0.3.5.jar": "87045EAE6D0C1BAD52E69FE14CD13B72AA1ABB7BDE1635DFAC5119DE2BD2DE54", + "libraries/org/cadixdev/lorenz/0.5.7/lorenz-0.5.7.jar": "223022F33FEF6D5D88903B7C4FC8D7096AD6FD869ACBC0FD241B0FCF8391D031", + "libraries/org/cadixdev/lorenz-asm/0.5.7/lorenz-asm-0.5.7.jar": "A1AD19FBBBAAAA7E6397D32A7EB7C7E35AF9FF0269AAE855D9B37E111EBF2C23", + "libraries/org/checkerframework/checker-compat-qual/2.5.5/checker-compat-qual-2.5.5.jar": "11D134B245E9CACC474514D2D66B5B8618F8039A1465CDC55BBC0B34E0008B7A", + "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/openjdk/nashorn/nashorn-core/15.3/nashorn-core-15.3.jar": "EFC4F1E6164377A6A347DAE255CF764FB3E5461CAFD859ABBDFB39E2DC831B62", + "libraries/org/ow2/asm/asm/9.1/asm-9.1.jar": "CDA4DE455FAB48FF0BCB7C48B4639447D4DE859A7AFC30A094A986F0936BEBA2", + "libraries/org/ow2/asm/asm-analysis/9.1/asm-analysis-9.1.jar": "81A88041B1B8BEDA5A8A99646098046C48709538270C49DEF68ABFF25AC3BE34", + "libraries/org/ow2/asm/asm-commons/9.1/asm-commons-9.1.jar": "AFCB26DC1FC12C0C4A99ADA670908DD82E18DFC488CAF5EE92546996B470C00C", + "libraries/org/ow2/asm/asm-tree/9.1/asm-tree-9.1.jar": "FD00AFA49E9595D7646205B09CECB4A776A8FF0BA06F2D59B8F7BF9C704B4A73", + "libraries/org/ow2/asm/asm-util/9.1/asm-util-9.1.jar": "380E2ECD16F7CC0F1A76BA9BA049179B5760A57B282A87A4C653CAEFF2CD5BD6", + "libraries/org/spongepowered/mixin/0.8.4/mixin-0.8.4.jar": "751118C1D01800C0000A253D10A66F8C1B755805B2B13BFB0C21624FAC905661", + "libraries/trove/trove/1.0.2/trove-1.0.2.jar": "AD68A86CFD0255A2BC0525CE11EC49640CAEA7E34FF91EB89C3193DCCCD77F05" +} diff --git a/tools/native117_worker_budget.json b/tools/native117_worker_budget.json new file mode 100644 index 0000000..36f5de3 --- /dev/null +++ b/tools/native117_worker_budget.json @@ -0,0 +1,158 @@ +{ + "minecraft": "1.17.1", + "forge": "37.1.1", + "java": "Temurin16.0.2+7", + "platform": "Windows amd64", + "reloads": 200, + "jvms": 4, + "per_process_deadline": 240, + "max_concurrent_workers": 2, + "visible_cpus": 2, + "native_preparation_workers": 1, + "no_candidate": true, + "no_worlds": true, + "source_and_sdk_seals_unchanged": true, + "frozen_inputs": { + "command_logs.py": "267A8D06BBF5C88CD97B9464A93E081D6B6687B2A5162F7E305E18632A3917E2", + "forge-platform-control-reobf.jar": "86297C57BDE37BFF67B3652B5F898D96B77262D1F6854991C1DFA0243FC76111", + "ForgeControl.java": "13E925FBF8B8A29D12ED1FDCAB3051AA86DCCB6B5DDC76A6E17A14364D13824C", + "native117_server_inputs.json": "5E49675E201355212E305D2910A7EE3A89387008D3ECA878F16A0ED9B1B5493E", + "native_runtime.py": "908CEBB40DB34C41AC9BF112BCAE3711ACCAAEAFD3A1478263FD01725230E091", + "ResourceControl.java": "EA36E101D13E2563C7422CB4CDCC323D1685BA7E212350C0D9801AB6BB73E0DB", + "runtime_diagnostics.py": "9C37400512A24BA3FE857FAC3F1EBF0ABE1D37CAE287900251700E52FC0E561F" + }, + "receipts": [ + { + "label": "shard-01", + "elapsed_seconds": 169.98400000000402, + "files": { + "banned-ips.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "banned-players.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "control-result.json": "60E393A01B8A6352B4BAD3CDE5485F50BCFA9156BC872A28AD37FCD6D9CB2F9E", + "eula.txt": "E4DB182AA77B5E31657F4B83F8973C79A8C65217A4383200F9F59F7C94752FAD", + "ops.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "resource-50.latest.log": "2FF82591F45C6B18B4F857938E40DE77DBF703A453F119ABC5831E26C41E4AD8", + "resource-50.log": "A5314267629B4E2D3462A9F280C01C4502D8C15DDE62076C5B356A83B8C74BDD", + "resource-control.pass": "0F63A3BDBF846F50BE54563380E5C050D634A599EAE9CF816E3B6F230E32538A", + "server.properties": "00E0E14D563CB92E2D7328ECCFFFA4E568B706E44EE535E555FA99B79336C27A", + "whitelist.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "config/fml.toml": "E6EA643B9E374575DBBDCBB8100B7B6F063BC28D9FC7FDA1B0ECB1B3EFD1A112", + "config/forge-common.toml": "7DA7A8A2043C5146B6C941631B040A945281943B9FF75A25E9743291959BD2BF", + "logs/latest.log": "2FF82591F45C6B18B4F857938E40DE77DBF703A453F119ABC5831E26C41E4AD8", + "resource-50.diagnostics/gc.txt": "D729AEF3EB9609B904FB643C1BFBEA759BF18EA0522E40C8E4D9EBABB6893914", + "resource-50.diagnostics/heap-gc-01.txt": "E431BF5A5A9676969606B0CEAB548712641D007FAC99A980DB1EA803DC95FF77", + "resource-50.diagnostics/heap-gc-02.txt": "BDA32C21BB3D60B36FD6DEBE5954373FB3228B6C5492F6DC6A287AB5F941C7B7", + "resource-50.diagnostics/result.json": "05537B3A686164FA6B1803326FE0D4FEEAB8F83F9D32D00816BFDCA69B43C4F4", + "resource-50.diagnostics/threads-01.txt": "A7D8463D77676837F6CFE5BF5FEAE8C46397AC4F03370FC644E04EF483D4DD2A", + "resource-50.diagnostics/threads-02.txt": "6049E47E1ABFCF40D243A1E82EE95F3FE3CBFF8C5BD949DC13828CC318F776AB", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings-merged.txt": "D161BED6B61E76AB9665DA72B440D5D07CA04EA3F022B5F5454FB35A3E6DE90B", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings.txt": "A538173EE059072A1570749ABC6A6E896FAE30CBC7748240E0A1A482D7D382F4", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038.zip": "507F59441CCDBA03C526F5484CD40559CF958340C0D8B7C0D6A0DFC37F511413", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar.cache": "A743FC18ECCC9F32DF31C440A051720D9A08044D1B4EA41E1EDAFB96022006B0", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-mappings.txt": "EDE01B6B10B554AE3146D2C084A8BFA88F0CA4DA3C6F51B7CAE7A154F40F3457", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-slim.jar.cache": "60F94FAB5E3D9117025B973634924AFE408A207A8FA216C329E76E99B7BDF50C", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/unix_args.txt": "60E81E217CB6B9C26FBCAFD2AEE9091C527EC411DD49D23EB5D7E855E7D881FE", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt": "B0E2D21DBA2E7C55A7DD9BBF914ADA99B124692810158F970F8F7C20543C2311" + } + }, + { + "label": "shard-02", + "elapsed_seconds": 170.06200000000536, + "files": { + "banned-ips.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "banned-players.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "control-result.json": "0DA68AF25BE04CA094B6CB8273123ABC51D78E2E529FFB72B451F95CCF6543E1", + "eula.txt": "E4DB182AA77B5E31657F4B83F8973C79A8C65217A4383200F9F59F7C94752FAD", + "ops.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "resource-50.latest.log": "64B1FD04DA141C2DCB91F4AF95241493D0F99FC18763344F96A9414C573E772A", + "resource-50.log": "FA069FFF59F8D78598F1686D45FC0D0E3A74A772879202A8258253998F03A95F", + "resource-control.pass": "0F63A3BDBF846F50BE54563380E5C050D634A599EAE9CF816E3B6F230E32538A", + "server.properties": "00E0E14D563CB92E2D7328ECCFFFA4E568B706E44EE535E555FA99B79336C27A", + "whitelist.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "config/fml.toml": "E6EA643B9E374575DBBDCBB8100B7B6F063BC28D9FC7FDA1B0ECB1B3EFD1A112", + "config/forge-common.toml": "7DA7A8A2043C5146B6C941631B040A945281943B9FF75A25E9743291959BD2BF", + "logs/latest.log": "64B1FD04DA141C2DCB91F4AF95241493D0F99FC18763344F96A9414C573E772A", + "resource-50.diagnostics/gc.txt": "F67DC0C082E8F94EFDA129A988ADE9E007BD5BBC0934EE7CBD492A08A4B96E96", + "resource-50.diagnostics/heap-gc-01.txt": "18CB5E1A91EA7A052189DC0AD739E4F2BA0ECFB5E6C5708B5F2941F701A6F829", + "resource-50.diagnostics/heap-gc-02.txt": "AABD32DF06C84E981B5B59A3BA7E1D8DC24554B963545D52515732C1FD9F59BD", + "resource-50.diagnostics/result.json": "747D02114635F05F1CE61F374DC18537101165EF8A626CFCE29B7163182A74A3", + "resource-50.diagnostics/threads-01.txt": "9E67D43D6930CE7F983232FE1F584C8BDB801A6F944F96068F99AB729F0D4B55", + "resource-50.diagnostics/threads-02.txt": "18C77C504C79D9EBE09846DD3345B23B3E79F467885B97D3C76F6E918839ED52", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings-merged.txt": "D161BED6B61E76AB9665DA72B440D5D07CA04EA3F022B5F5454FB35A3E6DE90B", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings.txt": "A538173EE059072A1570749ABC6A6E896FAE30CBC7748240E0A1A482D7D382F4", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038.zip": "507F59441CCDBA03C526F5484CD40559CF958340C0D8B7C0D6A0DFC37F511413", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar.cache": "A743FC18ECCC9F32DF31C440A051720D9A08044D1B4EA41E1EDAFB96022006B0", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-mappings.txt": "EDE01B6B10B554AE3146D2C084A8BFA88F0CA4DA3C6F51B7CAE7A154F40F3457", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-slim.jar.cache": "60F94FAB5E3D9117025B973634924AFE408A207A8FA216C329E76E99B7BDF50C", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/unix_args.txt": "60E81E217CB6B9C26FBCAFD2AEE9091C527EC411DD49D23EB5D7E855E7D881FE", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt": "B0E2D21DBA2E7C55A7DD9BBF914ADA99B124692810158F970F8F7C20543C2311" + } + }, + { + "label": "shard-03", + "elapsed_seconds": 166.93699999999808, + "files": { + "banned-ips.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "banned-players.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "control-result.json": "1F8A07E20F853B02786F8243B1153A9A489A883F7B6310C191E856B66B9B781E", + "eula.txt": "E4DB182AA77B5E31657F4B83F8973C79A8C65217A4383200F9F59F7C94752FAD", + "ops.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "resource-50.latest.log": "1737DD187BBED5D34BCB66E4447D74D4C6EE187C38416CD5A01D299F56E93C30", + "resource-50.log": "A1353F1454535111A8AD1568CEDA8F7EBA2B511572362990D9ADFCBF00A65CB8", + "resource-control.pass": "0F63A3BDBF846F50BE54563380E5C050D634A599EAE9CF816E3B6F230E32538A", + "server.properties": "00E0E14D563CB92E2D7328ECCFFFA4E568B706E44EE535E555FA99B79336C27A", + "whitelist.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "config/fml.toml": "E6EA643B9E374575DBBDCBB8100B7B6F063BC28D9FC7FDA1B0ECB1B3EFD1A112", + "config/forge-common.toml": "7DA7A8A2043C5146B6C941631B040A945281943B9FF75A25E9743291959BD2BF", + "logs/latest.log": "1737DD187BBED5D34BCB66E4447D74D4C6EE187C38416CD5A01D299F56E93C30", + "resource-50.diagnostics/gc.txt": "9F15B49A9D9D4871C2B17B4BF7ED5111A2A066C223BEE6D7D7EB04489BABB4D8", + "resource-50.diagnostics/heap-gc-01.txt": "A5C7FEF4CC18B99ADE1E206F67D48BCD8519763E52B0A2AF404266487241EB20", + "resource-50.diagnostics/heap-gc-02.txt": "3F520EB932A3AD472EA756CCCAF33D51F7BDF8DA9D8D1DEE78D0D0E8E051964C", + "resource-50.diagnostics/result.json": "0FF7E194BC38A150E4738AECF21CAD991490A555E986E43ACE735439A174B520", + "resource-50.diagnostics/threads-01.txt": "3E74C67B19CC495FF41C4729F2823E9979EEC2EFE17AD8EEC4C800582E14962E", + "resource-50.diagnostics/threads-02.txt": "2E1D8163D0DD670B75746C70BF743710546692CE869B792B371DAF8D832A55AE", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings-merged.txt": "D161BED6B61E76AB9665DA72B440D5D07CA04EA3F022B5F5454FB35A3E6DE90B", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings.txt": "A538173EE059072A1570749ABC6A6E896FAE30CBC7748240E0A1A482D7D382F4", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038.zip": "507F59441CCDBA03C526F5484CD40559CF958340C0D8B7C0D6A0DFC37F511413", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar.cache": "A743FC18ECCC9F32DF31C440A051720D9A08044D1B4EA41E1EDAFB96022006B0", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-mappings.txt": "EDE01B6B10B554AE3146D2C084A8BFA88F0CA4DA3C6F51B7CAE7A154F40F3457", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-slim.jar.cache": "60F94FAB5E3D9117025B973634924AFE408A207A8FA216C329E76E99B7BDF50C", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/unix_args.txt": "60E81E217CB6B9C26FBCAFD2AEE9091C527EC411DD49D23EB5D7E855E7D881FE", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt": "B0E2D21DBA2E7C55A7DD9BBF914ADA99B124692810158F970F8F7C20543C2311" + } + }, + { + "label": "shard-04", + "elapsed_seconds": 166.5460000000021, + "files": { + "banned-ips.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "banned-players.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "control-result.json": "C4AFDAE55A794DEF8FF1BD913FFFCB9619BE175E827FDBF8CA92D73E9C481A76", + "eula.txt": "E4DB182AA77B5E31657F4B83F8973C79A8C65217A4383200F9F59F7C94752FAD", + "ops.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "resource-50.latest.log": "615AE975332FFB95C1DE4EDADA7102015B3BF6EDCE7A691761A5E37CAD9232A9", + "resource-50.log": "7F930B15058E1A006428291A0A953887B52BDB32E2583C467B502D463ABFE176", + "resource-control.pass": "0F63A3BDBF846F50BE54563380E5C050D634A599EAE9CF816E3B6F230E32538A", + "server.properties": "00E0E14D563CB92E2D7328ECCFFFA4E568B706E44EE535E555FA99B79336C27A", + "whitelist.json": "A5338D955B09046EC0B16F3A9625B7955C763AAE07DC722E474E6078745F932F", + "config/fml.toml": "E6EA643B9E374575DBBDCBB8100B7B6F063BC28D9FC7FDA1B0ECB1B3EFD1A112", + "config/forge-common.toml": "7DA7A8A2043C5146B6C941631B040A945281943B9FF75A25E9743291959BD2BF", + "logs/latest.log": "615AE975332FFB95C1DE4EDADA7102015B3BF6EDCE7A691761A5E37CAD9232A9", + "resource-50.diagnostics/gc.txt": "E9F501268E757F6D33E5C6B20FC0CA8778971B4D71F2A5C46098FB16758E2010", + "resource-50.diagnostics/heap-gc-01.txt": "8C7F9487AB5526205B2DB9953E47886CC7656E87482EBD9918F690EC61ABE843", + "resource-50.diagnostics/heap-gc-02.txt": "B5500A591E752E4C609C0641505EEC6D889AA14B683B7B94A8405FB8FE0A977C", + "resource-50.diagnostics/result.json": "CEACD83A43732F96994C2EEED1CB1B9CF90F856FC142F8E635E5E902CB47F986", + "resource-50.diagnostics/threads-01.txt": "05D702306B47C6755659B369B3DD20AA1B3DD2A7C50DC9900964361F093AD055", + "resource-50.diagnostics/threads-02.txt": "2F3CFB666D59E9248B45963B87447F4D0FF9A9C598C3CF12C787B044D2E1DD0D", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings-merged.txt": "D161BED6B61E76AB9665DA72B440D5D07CA04EA3F022B5F5454FB35A3E6DE90B", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038-mappings.txt": "A538173EE059072A1570749ABC6A6E896FAE30CBC7748240E0A1A482D7D382F4", + "libraries/de/oceanlabs/mcp/mcp_config/1.17.1-20210706.113038/mcp_config-1.17.1-20210706.113038.zip": "507F59441CCDBA03C526F5484CD40559CF958340C0D8B7C0D6A0DFC37F511413", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-extra.jar.cache": "A743FC18ECCC9F32DF31C440A051720D9A08044D1B4EA41E1EDAFB96022006B0", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-mappings.txt": "EDE01B6B10B554AE3146D2C084A8BFA88F0CA4DA3C6F51B7CAE7A154F40F3457", + "libraries/net/minecraft/server/1.17.1-20210706.113038/server-1.17.1-20210706.113038-slim.jar.cache": "60F94FAB5E3D9117025B973634924AFE408A207A8FA216C329E76E99B7BDF50C", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/unix_args.txt": "60E81E217CB6B9C26FBCAFD2AEE9091C527EC411DD49D23EB5D7E855E7D881FE", + "libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt": "B0E2D21DBA2E7C55A7DD9BBF914ADA99B124692810158F970F8F7C20543C2311" + } + } + ] +} diff --git a/tools/native37_control_evidence.py b/tools/native37_control_evidence.py new file mode 100644 index 0000000..2fc2515 --- /dev/null +++ b/tools/native37_control_evidence.py @@ -0,0 +1,57 @@ +"""Classify the deliberate mod-free control separately from ordinary clean logs.""" +import json,re +from pathlib import Path +from audit_runtime_logs import audit +from native_runtime import digest + +def normalized_stack(text): + # The module-loader enumeration is an incidental JVM identity. Every + # exception, frame, native line, jar name and thread line remains exact. + return re.sub(r'%23[0-9]+!',r'%23MODULE!',text.strip()) + +def residual(text): + expected=(Path(__file__).parent/'fixtures/native37-delayed-movement-stack.txt').read_text() + lines=text.splitlines();out=[];count=0;i=0 + while i=receipt['timeout']: + 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') + 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 (')): + raise ValueError('World loading started after unsafe preservation') + comparison=json.loads((home/'comparison.json').read_text()) + 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') + 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'), + 'native_files':len(receipt['evidence']),'returncode':receipt['returncode'],'logs':logs} + +def verify_saved_audit(root,report): + def exact(path,sha): + path=Path(path).resolve() + return path.is_relative_to(root.resolve()) and path.is_file() and digest(path)==sha + try: + for item in report['receipts']: + path=Path(item['receipt']) + if not exact(path,item['sha256']):return False + receipt=json.loads(path.read_text()) + for name,sha in receipt['evidence'].items(): + if Path(name).name!=name or not exact(path.parent/name,sha):return False + for log,sha in item['logs'].items(): + if not exact(log,sha):return False + return all(exact(item['path'],item['sha256']) for item in report['positive_complete_logs']) + except (OSError,KeyError,TypeError,ValueError): + return False + +def audit_case_mods(home,report): + from release_identity import MAIN_JAR + expected={MAIN_JAR:report['candidate_sha256'],'lootablebodies-build-tests-reobf.jar':report['probe_sha256']} + mods=home/'mods' + if mods.is_symlink() or not mods.is_dir():raise ValueError('Missing or aliased native mods directory') + archives=[p for p in mods.iterdir() if p.suffix.lower()=='.jar'] + if any(p.is_symlink() or not p.is_file() for p in archives):raise ValueError('Aliased or invalid native mod archive') + actual={p.name:digest(p) for p in archives} + if actual!=expected:raise ValueError('Actual native process mods differ from qualified candidate/probe: '+str(home)) + return {str(home/'mods'/name):sha for name,sha in expected.items()} + + +def audit_shard(folder,report,*,relocated=False): + positive=folder/'migration-results';aborts=folder/'migration-abort' + records=[];logs=[] + from audit_runtime_logs import audit + for case,details in report['cases'].items(): + home=positive/case + mods=audit_case_mods(home,report) + for phase in details['phases']: + record=audit_phase(home,phase,relocated=relocated);record['logs'].update(mods);records.append(record) + for case,details in report['aborts'].items(): + for condition in details['conditions']: + home=aborts/case/condition;mods=audit_case_mods(home,report) + record=audit_phase(home,'abort',refusal=True,relocated=relocated);record['logs'].update(mods);records.append(record) + declared={Path(item['receipt']).resolve() for item in records} + actual={path.resolve() for root in (positive,aborts) for path in root.glob('**/*.diagnostics/result.json')} + 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)}) + 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 d86569d..b463df5 100644 --- a/tools/native_runtime.py +++ b/tools/native_runtime.py @@ -1,4 +1,4 @@ -"""Disposable official Forge 36 launches, separate from the development bootstrap.""" +"""Disposable official Forge 37 launches, separate from the development bootstrap.""" import hashlib import json import os @@ -12,12 +12,16 @@ from command_logs import run_logged from runtime_diagnostics import run_monitored -FORGE = 'forge-1.16.5-36.2.42' -INSTALLER_SHA256 = 'DFA90C681F30BE2406548B6429BCCB68D95AF4F64AB19B8C58F6128FD50FF857' -SERVER_INPUTS = {'forge-1.16.5-36.2.42.jar': 'DFEAB9C7D582FCE5B1112E9D3038FA0E7075296E467031CE1E2C0BA9FDCF441F', 'libraries/net/minecraftforge/forge/1.16.5-36.2.42/forge-1.16.5-36.2.42-server.jar': '487536E867F5D4287121BAB75600C980A8313E458DE2B2C392C7201EB5648EA1', 'minecraft_server.1.16.5.jar': '58F329C7D2696526F948470AA6FD0B45545039B64CB75015E64C12194B373DA6'} -# Native 1.16 Main applies resource-stage completions directly to an -# unsynchronised HashSet. Two visible CPUs give its preparation pool one worker. -# Apply this budget only to disposable historical preservation/refusal fixtures. +FORGE = 'forge-1.17.1-37.1.1' +INSTALLER_SHA256 = '31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B' +SERVER_INPUTS = json.loads(Path(__file__).with_name('native117_server_inputs.json').read_text(encoding='utf-8')) +# Qualified by 200 actual packaged native37 resource reloads in four bounded +# JVMs, without this mod or a world. Ordinary gameplay retains native defaults. +WORKER_BUDGET_PROOF = json.loads(Path(__file__).with_name('native117_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['native_preparation_workers']) != ('1.17.1', '37.1.1', 200, 4, 1): + raise RuntimeError('Missing native37 worker-budget qualification') HISTORICAL_JVM_ARGS = ('-XX:ActiveProcessorCount=2',) @@ -50,18 +54,16 @@ def seed_loader_defaults(home, deterministic=False, include_default_path=True): def prepare_server(home, distribution, candidate=None, probe=None): home = home.resolve() distribution = distribution.resolve() - launcher = distribution / (FORGE + '.jar') libraries = distribution / 'libraries' - if not launcher.is_file() or not libraries.is_dir(): - raise FileNotFoundError('Incomplete official Forge 36 server installation') + if not libraries.is_dir(): + raise FileNotFoundError('Incomplete official Forge 37 server installation') for name, expected in SERVER_INPUTS.items(): artifact = distribution / name if not artifact.is_file() or digest(artifact) != expected: - raise RuntimeError('Incorrect official Forge 36 server input: ' + name) + raise RuntimeError('Incorrect official Forge 37 server input: ' + name) home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() seed_loader_defaults(home, deterministic=probe is not None) - shutil.copy2(launcher, home / launcher.name) # Share this disposable installation's immutable libraries, not a user # installation or Gradle cache. Every matrix run fingerprints the inputs. link = home / 'libraries' @@ -80,8 +82,11 @@ def prepare_server(home, distribution, candidate=None, probe=None): raise FileNotFoundError(artifact) shutil.copy2(artifact, home / 'mods' / artifact.name) (home / 'eula.txt').write_text('eula=true\n', encoding='utf-8') + # Native37's flat server-property codec uses bare JsonOps and logs errors. + # Existing copied headers remain authoritative. Fresh flat fixtures need + # an explicitly prepared native-codec header before their first launch. (home / 'server.properties').write_text( - 'level-name=world\nlevel-seed=4267321\nlevel-type=FLAT\nonline-mode=false\n' + 'level-name=world\nlevel-seed=4267321\nlevel-type=DEFAULT\nonline-mode=false\n' 'server-ip=127.0.0.1\nserver-port=0\ngenerate-structures=false\n' 'spawn-animals=false\nspawn-monsters=false\nspawn-protection=0\n' 'max-tick-time=120000\n', encoding='utf-8') @@ -93,6 +98,13 @@ def run_java(java, home, phase, *, timeout=240, jvm_args=()): """Run one owned Forge JVM, retaining failure evidence before returning.""" java = Path(java).resolve() home = home.resolve() + release = java.parent.parent / 'release' + if not release.is_file(): + raise RuntimeError('Native runtime requires exact Temurin 16.0.2+7 JDK metadata') + version = dict(line.split('=', 1) for line in release.read_text(encoding='utf-8').splitlines() if '=' in line) + if (version.get('SEMANTIC_VERSION') != '"16.0.2+7"' or + version.get('IMPLEMENTOR') != '"Eclipse Foundation"'): + raise RuntimeError('Native runtime requires exact Temurin 16.0.2+7') log = home / (phase + '.log') diagnostics = home / (phase + '.diagnostics') copies = [home / (phase + '.' + name) for name in ('latest.log', 'debug.log')] @@ -103,11 +115,11 @@ def run_java(java, home, phase, *, timeout=240, jvm_args=()): for tool in (jstack, jstat): if not tool.is_file(): raise FileNotFoundError('Native runtime diagnostics require the pinned JDK tool: '+str(tool)) - gc = diagnostics / 'gc.txt' - command = [str(java), '-XX:+PrintGCDetails', '-XX:+PrintGCTimeStamps', - '-XX:+PrintGCDateStamps', '-XX:+PrintGCApplicationStoppedTime', - '-Xloggc:' + str(gc), *jvm_args, - '-jar', FORGE + '.jar', '--nogui'] + # 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), '-Xlog:gc*,safepoint:file=' + gc.as_posix() + ':time,uptime,level,tags', + *jvm_args, '@libraries/net/minecraftforge/forge/1.17.1-37.1.1/' + arguments, '--nogui'] try: completed = run_monitored(command, cwd=home, log=log, timeout=timeout, diagnostics=diagnostics, @@ -122,6 +134,35 @@ def run_java(java, home, phase, *, timeout=240, jvm_args=()): return completed +def seed_flat_fixture(home, header, expected_sha256): + """Copy one sealed native-codec header into a new owned fixture world.""" + import gzip + from nbt_regions import decode + home, header = Path(home).resolve(), Path(header).resolve() + if not (home/'server.properties').is_file(): + raise RuntimeError('Prepare the owned server home before seeding its header') + world = home/'world' + if world.exists(): + raise FileExistsError('Refusing to overwrite an existing fixture world') + if not header.is_file() or digest(header) != expected_sha256: + raise RuntimeError('Native flat fixture header checksum mismatch') + _, root = decode(gzip.decompress(header.read_bytes())) + data = root.value.get('Data') + try: + version = data.value['DataVersion'] + generator = data.value['WorldGenSettings'].value['dimensions'].value[ + 'minecraft:overworld'].value['generator'].value['type'] + if version.kind != 3 or version.value != 2730 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 native2730 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':2730, 'generator':'minecraft:flat', + 'only_header_copied':True}, indent=2)+'\n', encoding='utf-8') + + def run_server(java, home, phase, *, timeout=240, extra=(), marker=None, message=None): home = home.resolve() log = home / (phase + '.log') @@ -189,83 +230,14 @@ def validate_client_paths(paths): raise RuntimeError('Development, testing or incompatible legacy library in packaged client: '+str(path)) -def client_inputs(runtime): - runtime = runtime.resolve() - base_path = runtime / 'versions/1.16.5/1.16.5.json' - if not base_path.is_file() or hashlib.sha1(base_path.read_bytes()).hexdigest() != 'fba9f7833e858a1257d810d21a3a9e3c967f9077': - raise RuntimeError('Missing or incorrect official Minecraft 1.16.5 descriptor') - base = json.loads(base_path.read_text(encoding='utf-8')) - forge = json.loads((runtime / 'versions/1.16.5-forge-36.2.42/1.16.5-forge-36.2.42.json').read_text(encoding='utf-8')) - system = {'Windows': 'windows', 'Darwin': 'osx', 'Linux': 'linux'}[platform.system()] - architecture = 'x86' if platform.architecture()[0] == '32bit' else 'amd64' - native_directory = runtime / ('natives-' + system + '-' + architecture) - native_directory.mkdir(exist_ok=True) - modules = {} - for metadata in (base, forge): - for library in metadata['libraries']: - if not allowed(library, system, architecture): - continue - downloads = library.get('downloads', {}) - artifact = downloads.get('artifact') - if artifact: - path = runtime / 'libraries' / artifact['path'] - _download(artifact, path) - parts = library['name'].split(':') - key = ':'.join(parts[:2] + parts[3:]) - modules[key] = path - classifier = library.get('natives', {}).get(system) - if classifier: - classifier = classifier.replace('${arch}', '32' if architecture == 'x86' else '64') - native = downloads['classifiers'][classifier] - archive = runtime / 'libraries' / native['path'] - _download(native, archive) - 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 = native_directory / Path(entry.filename).name - content = jar.read(entry) - if target.exists(): - if target.read_bytes() != content: - raise RuntimeError('Conflicting native runtime input: ' + target.name) - else: - target.write_bytes(content) - base_jar = runtime / 'versions/1.16.5/1.16.5.jar' - _download(base['downloads']['client'], base_jar) - paths = list(modules.values()) + [base_jar] - validate_client_paths(paths) - return base, forge, paths, native_directory +def client_inputs(runtime, prepared): + from native117_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): - """Hydrate owned assets from official hashes, reusing verified cache objects.""" - from concurrent.futures import ThreadPoolExecutor - runtime = runtime.resolve() - base = json.loads((runtime / 'versions/1.16.5/1.16.5.json').read_text(encoding='utf-8')) - assets = runtime / 'assets' - index = assets / 'indexes/1.16.json' - _download(base['assetIndex'], index) - objects = json.loads(index.read_text(encoding='utf-8'))['objects'] - unique = {item['hash'] for item in objects.values()} - - def hydrate(checksum): - relative = checksum[:2] + '/' + checksum - destination = assets / 'objects' / relative - if not destination.exists() and cache is not None: - previous = cache / 'objects' / relative - if previous.is_file() and hashlib.sha1(previous.read_bytes()).hexdigest() == checksum: - destination.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(previous, destination) - _download({'sha1': checksum, 'url': 'https://resources.download.minecraft.net/' + relative}, destination) - - with ThreadPoolExecutor(max_workers=8) as pool: - for count, _ in enumerate(pool.map(hydrate, sorted(unique)), 1): - if count % 250 == 0: - print('Verified native asset objects', count, '/', len(unique), flush=True) - print('Official native assets ready:', len(unique), 'objects', flush=True) - return assets + raise RuntimeError('Use the pinned native1.17 prepareMinecraftAssets task; inherited cache-copy hydration is disabled') def prepare_client(home, candidate=None, probe=None): @@ -281,41 +253,34 @@ def prepare_client(home, candidate=None, probe=None): (home / 'options.txt').write_text('realmsNotifications:false\npauseOnLostFocus:false\nrenderDistance:4\nmaxFps:30\nsoundCategory_master:0.0\n', encoding='utf-8') -def client_command(java, home, runtime, assets, *, username='LootableTest', extra=()): - base, forge, paths, natives = client_inputs(runtime) - index = assets / 'indexes/1.16.json' - if not index.is_file() or hashlib.sha1(index.read_bytes()).hexdigest() != base['assetIndex']['sha1']: - raise RuntimeError('Missing or incorrect official Minecraft asset index') - return [str(java), '-Xmx2G', '-Djava.library.path=' + str(natives), - '-DlibraryDirectory=' + str(runtime.resolve() / 'libraries'), *extra, - '-cp', os.pathsep.join(str(path) for path in paths), forge['mainClass'], - *forge['arguments']['game'], '--username', username, - '--version', '1.16.5-forge-36.2.42', '--gameDir', str(home.resolve()), - '--assetsDir', str(assets.resolve()), '--assetIndex', '1.16', - '--uuid', '00000000-0000-4000-8000-000000000001', '--accessToken', 'disposable-test', - '--userType', 'legacy', '--versionType', 'release', '--width', '854', '--height', '480'] +def client_command(java, home, runtime, assets, *, username='LootableTest', extra=(), prepared=None): + from native117_client import command + if prepared is None: + prepared=Path(__file__).resolve().parents[1]/'.gradle/native117-client-inputs' + return command(java,home,runtime,assets,prepared,username=username,extra=extra) -def install(java,home,client=False): - """Install an official pinned distribution into a new, owned directory.""" +def install(java, home, client=False): + """Install the pinned native37 distribution into a new owned directory.""" 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.16.5-36.2.42/'+installer.name,installer,INSTALLER_SHA256) + fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.17.1-37.1.1/'+installer.name,installer,INSTALLER_SHA256) if client: - base=home/'versions/1.16.5/1.16.5.json' - _download({'sha1':'fba9f7833e858a1257d810d21a3a9e3c967f9077','url':'https://piston-meta.mojang.com/v1/packages/fba9f7833e858a1257d810d21a3a9e3c967f9077/1.16.5.json'},base) + from native117_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.17.1.json'},base) metadata=json.loads(base.read_text());_download(metadata['downloads']['client'],base.with_suffix('.jar')) (home/'launcher_profiles.json').write_text('{"profiles":{}}\n') run_logged([str(java),'-Djava.awt.headless=true','-jar',installer.name, - '--installClient' if client else '--installServer',str(home)], - cwd=home,path=home/'install.log',timeout=600) + '--installClient' if client else '--installServer',str(home)], + cwd=home,path=home/'install.log',timeout=600) if client: - base,forge,paths,natives=client_inputs(home) - inputs={str(path.relative_to(home)):digest(path) for path in paths} + from native117_client import prepare + inputs=prepare(home,home/'prepared-inputs')['files'] else: inputs={name:digest(home/name) for name in SERVER_INPUTS} - if inputs!=SERVER_INPUTS:raise RuntimeError('Official Forge server installation checksum mismatch') + if inputs!=SERVER_INPUTS:raise RuntimeError('Official Forge37 server installation checksum mismatch') (home/'runtime-inputs.json').write_text(json.dumps({'installer_sha256':INSTALLER_SHA256,'client':client,'files':inputs},indent=2)+'\n') return home diff --git a/tools/owned_process.py b/tools/owned_process.py new file mode 100644 index 0000000..83a0267 --- /dev/null +++ b/tools/owned_process.py @@ -0,0 +1,71 @@ +"""Concurrent owned processes using the same bounded diagnostic collector.""" +from concurrent.futures import ThreadPoolExecutor +import os +from pathlib import Path +import subprocess +import threading +import time +from runtime_diagnostics import run_monitored + + +class OwnedProcess: + def __init__(self,command,*,cwd,log,timeout,diagnostic_command,metric_command=None,console_input=False): + self.cwd=Path(cwd).resolve();self.log=Path(log).resolve() + if self.log.parent!=self.cwd.parent or self.log.exists(): + raise ValueError('Group console alias must be a new file in the owned parent') + self._stop=threading.Event();self._pool=ThreadPoolExecutor(max_workers=1) + self._process=None;self._started=threading.Event() + self._console=self.cwd/self.log.name + self._future=self._pool.submit(run_monitored,command,cwd=self.cwd,log=self._console, + timeout=timeout,diagnostics=self.cwd/(self.log.stem+'.diagnostics'), + diagnostic_command=diagnostic_command,metric_command=metric_command, + stop_requested=self._stop.is_set,stdin=subprocess.PIPE if console_input else None, + process_started=self._publish) + # A hard link exposes the very same live stdout to the group login + # monitor while all receipt paths remain inside their native home. + try: + while not self._console.exists(): + if self._future.done():self._future.result() + time.sleep(.01) + os.link(self._console,self.log) + except BaseException: + self._stop.set() + try:self._future.result(timeout=5) + except BaseException:pass + self._pool.shutdown(wait=False) + raise + def poll(self): + if not self._future.done():return None + try:return self._future.result().returncode + except BaseException:return 1 + def _publish(self,process):self._process=process;self._started.set() + @property + def stdin(self): + while not self._started.wait(.01): + if self._future.done():self._future.result() + return self._process.stdin + @property + def returncode(self):return self.poll() + def terminate(self):self._stop.set() + def kill(self):self._stop.set() + def wait(self,timeout=None): + from concurrent.futures import TimeoutError + try: + result=self._future.result(timeout=timeout) + return result.returncode + except TimeoutError as error:raise subprocess.TimeoutExpired(str(self.log),timeout) from error + finally: + if self._future.done():self._pool.shutdown(wait=True) + + +def native_process(command,*,cwd,log,timeout,console_input=False): + java=Path(command[0]).resolve();metadata=java.parent.parent/'release' + values=dict(line.split('=',1) for line in metadata.read_text().splitlines() if '=' in line) + if values.get('SEMANTIC_VERSION')!='"16.0.2+7"' or values.get('IMPLEMENTOR')!='"Eclipse Foundation"': + raise RuntimeError('Group runtime requires exact Temurin16.0.2+7') + suffix='.exe' if java.suffix=='.exe' else '' + stack=java.with_name('jstack'+suffix);metric=java.with_name('jstat'+suffix) + if not stack.is_file() or not metric.is_file():raise FileNotFoundError('Missing pinned native JDK diagnostics') + label=Path(log).stem + launch=[command[0],'-Xlog:gc*,safepoint:file='+label+'.diagnostics/gc.txt:time,uptime,level,tags',*command[1:]] + return OwnedProcess(launch,cwd=cwd,log=log,timeout=timeout,diagnostic_command=[str(stack),'-l'],metric_command=[str(metric),'-gc'],console_input=console_input) diff --git a/tools/prepare_native116_tools.py b/tools/prepare_native116_tools.py new file mode 100644 index 0000000..47c9bdb --- /dev/null +++ b/tools/prepare_native116_tools.py @@ -0,0 +1,65 @@ +"""Build historical fixtures against the exact released 1.16 source and API.""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import shutil +from prepare_legacy_tools import prepare, digest +from command_logs import run_build_input_logged as run_logged + +SOURCE = '9edcc246bc156d93dabd51970b5a832d7d53a511' +OUTPUT = 'native116-seed/lootablebodies-native116-world-fixtures-reobf.jar' +PROBE = 'integration-test-mod/lootablebodies-build-tests-reobf.jar' + + +def historical_runtime(project): + """Load checksum-verified release tooling, not the 1.17 candidate's tooling.""" + root=project/'.gradle/legacy-tools/released-116' + source=root/'source/tools/native_runtime.py' + original=json.loads((root/'source.json').read_text()) + if original['commit'] != SOURCE or digest(source) != original['files']['tools/native_runtime.py']: + raise RuntimeError('Missing or changed released 1.16 runtime tools; run prepare_native116_tools.py') + spec=importlib.util.spec_from_file_location('released116_runtime', source) + module=importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path) + parser.add_argument('--java17-home', type=Path, required=True) + parser.add_argument('--java8-home', type=Path, required=True) + parser.add_argument('--java25-home', type=Path, required=True) + args=parser.parse_args() + project=Path(__file__).resolve().parents[1] + output=(args.output or project/'.gradle/legacy-tools/released-116').resolve() + source=prepare(project, output, SOURCE) + environment=dict(os.environ, JAVA_HOME=str(args.java17_home), + LOOTABLEBODIES_JAVA8_HOME=str(args.java8_home), + LOOTABLEBODIES_MAVENIZER_JAVA_HOME=str(args.java25_home)) + wrapper=source/('gradlew.bat' if os.name=='nt' else 'gradlew') + if os.name!='nt': wrapper.chmod(0o755) + run_logged([str(wrapper), 'prepareNative116WorldFixtures', 'assemble', + '--init-script='+str(project/'gradle/native116-writer.init.gradle'), + '-PhistoricalWriterSource='+str(project), '--no-daemon', '--console=plain'], + cwd=source, env=environment, path=output/'build.log', timeout=1800) + artifact=source/'build'/OUTPUT + for path in (artifact,): + if not path.is_file() or not path.stat().st_size: raise FileNotFoundError(path) + files=[p for p in (project/'src/nativeSeed116').rglob('*') if p.is_file()] + public_main=source/'build/libs/LootableBodies-2.4.7.116051.jar' + public_sha='E545CF725650E8A01C5863A4C92BCB2D23437D61C9441C2F5D27A5272EDDA229' + if digest(public_main)!=public_sha:raise RuntimeError('Native116 writer must use exact public E545CF main bytes') + record={'public_main_sha256':public_sha,'source': SOURCE, 'artifact': OUTPUT, 'sha256': digest(artifact), + 'probe': 'pinned original hosted116 input archive; never rebuilt', + 'writer_sources':{p.relative_to(project).as_posix():digest(p) for p in files}} + (output/'tools.json').write_text(json.dumps(record, indent=2)+'\n', encoding='utf-8') + target=project/'build'/OUTPUT + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(artifact, target) + print('Historical 1.16 writer built from released source '+SOURCE, flush=True) + + +if __name__=='__main__': main() diff --git a/tools/prepare_native117_history.py b/tools/prepare_native117_history.py new file mode 100644 index 0000000..ac734c1 --- /dev/null +++ b/tools/prepare_native117_history.py @@ -0,0 +1,82 @@ +"""Carry original sealed 1.16 inputs into native 1.17 qualification, never caches.""" +import argparse,hashlib,importlib.util,json,os,shutil,subprocess,sys,tarfile +from pathlib import Path,PurePosixPath + +SOURCE='9edcc246bc156d93dabd51970b5a832d7d53a511' +PRIOR_RUN=40909860288 +PRIOR_MANIFEST='1AE69CAD824DF6AD61A90715E5BEDC158B7A14EE311D59ECF7C4A9EE023B3AEC' +PRIOR_ARCHIVE='6D3A380A7998E1DE756E9CBCF3C345FE3F862117B0697935D91E440E1E21E6F1' +PUBLIC_MAIN='E545CF725650E8A01C5863A4C92BCB2D23437D61C9441C2F5D27A5272EDDA229' +PUBLIC_PROBE='142A5E48E9EA0DB4A0C40E08804319BC628B9E2F97704033EA007A20E1E28452' + +def sha(path): + h=hashlib.sha256() + with path.open('rb') as f: + for data in iter(lambda:f.read(1048576),b''):h.update(data) + return h.hexdigest().upper() + +def extract_sealed(folder,output,manifest_sha,archive_sha): + folder,output=Path(folder),Path(output) + if sha(folder/'inputs.json')!=manifest_sha or sha(folder/'inputs.tar.gz')!=archive_sha: + raise ValueError('Pinned prior inputs changed') + record=json.loads((folder/'inputs.json').read_text()) + if record['head']!=SOURCE or record['archive_sha256']!=archive_sha:raise ValueError('Wrong prior source') + with tarfile.open(folder/'inputs.tar.gz','r:gz') as t: + members=t.getmembers();names=[m.name for m in members] + if len(names)!=len(set(names)) or set(names)!=set(record['files']):raise ValueError('Unexpected archive members') + for m in members: + path=PurePosixPath(m.name) + if path.is_absolute() or '..' in path.parts or path.parts[:1]!=('build',) or '\\' in m.name or not m.isfile():raise ValueError('Unsafe prior input') + expected=record['files'][m.name] + if m.size!=expected['size'] or hashlib.sha256(t.extractfile(m).read()).hexdigest().upper()!=expected['sha256']: + raise ValueError('Changed prior archive entry') + output.mkdir(parents=True,exist_ok=False) + for m in members: + target=output/m.name;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(t.extractfile(m).read()) + return record + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--download',type=Path,required=True) + parser.add_argument('--java8',type=Path,required=True) + args=parser.parse_args();project=Path(__file__).resolve().parents[1] + from verify_world_upgrades import ORIGINS,fingerprint,preserve_historical_markers + from prepare_native116_tools import historical_runtime + from native_runtime import seed_loader_defaults + prior=project/'build/prior116';record=extract_sealed(args.download,prior,PRIOR_MANIFEST,PRIOR_ARCHIVE) + old=prior/'build/migration-seeds';seeds=project/'build/migration-seeds';seeds.mkdir(exist_ok=False) + report=json.loads((old/'seed-report.json').read_text());assert len(report['origins'])==30 + for name,details in report['origins'].items(): + source=old/name/'world';assert fingerprint(source)==details['world'] + target=seeds/name/'world';target.parent.mkdir();shutil.copytree(source,target) + assert fingerprint(target)==details['world'] + runtime=historical_runtime(project) + sdk=prior/'build/native-server-distribution' + mainjar=prior/'build/libs/LootableBodies-2.4.7.116051.jar' + probe=prior/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' + writer=project/'build/native116-seed/lootablebodies-native116-world-fixtures-reobf.jar' + assert sha(mainjar)==PUBLIC_MAIN and sha(probe)==PUBLIC_PROBE + build=json.loads((project/'.gradle/legacy-tools/released-116/tools.json').read_text()) + assert build['source']==SOURCE and sha(writer)==build['sha256'] + java=args.java8.resolve() + for name,recovery in [('released-116',False),('recovery-116',True)]: + home=seeds/name;runtime.prepare_server(home,sdk,mainjar,writer);seed_loader_defaults(home,deterministic=True) + runtime.run_server(java,home,'seed',extra=[*runtime.HISTORICAL_JVM_ARGS,'-Dlootablebodies.fixture.version=2.4.7.116051','-Dlootablebodies.fixture.recovery='+str(recovery).lower()],marker='world/legacy-seed.pass',message='LOOTABLE_BODIES_LEGACY_SEED_PASS minecraft=1.16.5 mod=2.4.7.116051 corpses=9 dimensions=3') + report['origins'][name]={'minecraft':'1.16.5','mod_version':'2.4.7.116051','jar_sha256':PUBLIC_MAIN,'writer_sha256':sha(writer),'world':fingerprint(home/'world')} + (seeds/'seed-report.json').write_text(json.dumps(report,indent=2)+'\n') + for suffix,oldname in [('110','original-110'),('111','released-111'),('112','released-112'),('113','released-113'),('114','released-114'),('115','released-115')]: + name='via116-'+suffix;source=seeds/oldname/'world';details=report['origins'][oldname];assert fingerprint(source)==details['world'] + home=seeds/name;runtime.prepare_server(home,sdk,mainjar,probe);seed_loader_defaults(home,deterministic=True) + shutil.copytree(source,home/'world');world=home/'world';markers=preserve_historical_markers(home) + players=list((world/'playerdata').glob('*.dat'));assert len(players)==1;shutil.copy2(players[0],world/'upgrade-player-source.nbt') + far={p.relative_to(world).as_posix():sha(p) for p in world.rglob('region/r.*.mca') if p.name in ('r.8.8.mca','r.16.16.mca')};assert len(far)==6 + runtime.run_server(java,home,'upgrade-near',extra=runtime.HISTORICAL_JVM_ARGS,message='LOOTABLE_BODIES_WORLD_UPGRADE_PASS phase=upgrade-near') + assert fingerprint(source)==details['world'] and all(sha(world/p)==h for p,h in far.items()) + report['origins'][name]={'minecraft':details['minecraft']+' via 1.16.5','mod_version':details['mod_version']+' via 2.4.7.116051','jar_sha256':details['jar_sha256'],'intermediate_jar_sha256':PUBLIC_MAIN,'transition_probe_sha256':PUBLIC_PROBE,'world':fingerprint(world),'distant_regions_unchanged':far,'historical_markers':markers} + (seeds/'seed-report.json').write_text(json.dumps(report,indent=2)+'\n') + assert set(report['origins'])==set(ORIGINS) + for name,details in report['origins'].items():assert fingerprint(seeds/name/'world')==details['world'] + (seeds/'provenance.json').write_text(json.dumps({'original_30_reused_from_run':PRIOR_RUN,'original_manifest':PRIOR_MANIFEST,'original_archive':PRIOR_ARCHIVE,'eight_new_native116_sources':True,'original_worlds_regenerated':False,'all38_sealed':True},indent=2)+'\n') + print('NATIVE117_HISTORY_PREPARED original30_reused=30 new_native116=8',flush=True) + +if __name__=='__main__':main() diff --git a/tools/prepared_worlds.py b/tools/prepared_worlds.py new file mode 100644 index 0000000..faa4478 --- /dev/null +++ b/tools/prepared_worlds.py @@ -0,0 +1,37 @@ +"""Validate sealed, already-written historical worlds without regenerating them.""" +import json,re +from pathlib import Path +from native_runtime import digest,fingerprint + + +def load(path,expected,requested): + path=Path(path).resolve() + def unique(pairs): + result={} + for name,value in pairs: + if name in result:raise ValueError('Duplicate prepared-manifest key: '+name) + 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')!=2730: + raise ValueError('Wrong native17 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()] + if len(set(paths))!=len(paths):raise ValueError('Prepared origins must identify distinct source worlds') + if not requested or len(set(requested))!=len(requested) or not set(requested)<=set(expected): + raise ValueError('Invalid prepared origin selection') + result={} + for name in requested: + details=origins[name];world=Path(details['world']) + minecraft,version,jar=expected[name] + if details.get('jar_sha256')!=jar or details.get('minecraft','').split(' via ')[0]!=minecraft or details.get('mod_version','').split(' via ')[0]!=version: + raise ValueError('Historical source identity mismatch: '+name) + files=details.get('files',{}) + if not files or not all(isinstance(n,str) and not Path(n).is_absolute() and '..' not in Path(n).parts and re.fullmatch('[A-F0-9]{64}',sha or '') for n,sha in files.items()): + raise ValueError('Missing or invalid prepared file seal: '+name) + if not world.is_absolute() or not world.is_dir() or world.is_symlink() or any(p.is_symlink() for p in world.rglob('*')): + raise ValueError('Prepared world must be an absolute ordinary directory: '+name) + if fingerprint(world)!=files:raise ValueError('Sealed prepared source changed: '+name) + result[name]=(world.resolve(),dict(details,world=files)) + return result,{'path':str(path),'sha256':digest(path),'schema':1,'all_origins':len(origins)} diff --git a/tools/runtime_diagnostics.py b/tools/runtime_diagnostics.py index eaad505..590c819 100644 --- a/tools/runtime_diagnostics.py +++ b/tools/runtime_diagnostics.py @@ -12,7 +12,8 @@ def run_monitored(command, *, cwd, log, timeout, diagnostics, - diagnostic_command, metric_command=None): + diagnostic_command, metric_command=None, stop_requested=None, + stdin=None, process_started=None): if timeout <= 0: raise ValueError('Runtime deadline must be positive') cwd, log, diagnostics = Path(cwd), Path(log), Path(diagnostics) @@ -80,11 +81,22 @@ def launch_collector(kind, command, now): try: with log.open('x', encoding='utf-8') as output: process = subprocess.Popen(command, cwd=cwd, stdout=output, - stderr=subprocess.STDOUT) + stderr=subprocess.STDOUT, stdin=stdin, + text=stdin is not None) record['pid'] = process.pid save() + if process_started is not None:process_started(process) while process.poll() is None: now = time.monotonic() + if stop_requested is not None and stop_requested(): + record['stopped_reason'] = 'owned_group_abort' + end_collector('owned_group_abort') + process.terminate() + try: + process.wait(timeout=min(2,max(.01,deadline-now))) + except subprocess.TimeoutExpired: + process.kill();process.wait() + break if now >= deadline: record['timed_out'] = True end_collector('runtime_deadline') diff --git a/tools/shard_plan.py b/tools/shard_plan.py new file mode 100644 index 0000000..c404eee --- /dev/null +++ b/tools/shard_plan.py @@ -0,0 +1,19 @@ +"""Deterministic whole-origin scheduling; every normal/stamp/abort case is kept.""" +import math + +def validate(plan,origins,shards): + flat=[origin for group in plan for origin in group] + if len(plan)!=shards or any(not group for group in plan) or len(flat)!=len(origins) or len(flat)!=len(set(flat)) or set(flat)!=set(origins): + raise ValueError('Missing, duplicate or unexpected origin in shard plan') + return plan + +def schedule(origins,costs,shards): + if not origins or len(origins)!=len(set(origins)) or not 1<=shards<=len(origins) or set(costs)!=set(origins): + raise ValueError('Invalid origin/shard/cost coverage') + if any(type(value) not in (int,float) or not math.isfinite(value) or value<=0 for value in costs.values()): + raise ValueError('Every origin needs a finite positive measured/declared cost') + groups=[[] for _ in range(shards)];loads=[0.0]*shards + for origin in sorted(origins,key=lambda name:(-costs[name],name)): + index=min(range(shards),key=lambda value:(loads[value],len(groups[value]),value)) + groups[index].append(origin);loads[index]+=costs[origin] + return validate(groups,origins,shards) diff --git a/tools/test_evidence_policy.py b/tools/test_evidence_policy.py new file mode 100644 index 0000000..23f86cf --- /dev/null +++ b/tools/test_evidence_policy.py @@ -0,0 +1,60 @@ +from pathlib import Path +import copy,json,tempfile,unittest +import evidence_policy as p + +class EvidenceReuseTests(unittest.TestCase): + def fixture(self,root): + report=root/'report.json';report.write_text('{"all_pass":true}') + log=root/'complete.log';log.write_text('actual retained complete log') + subject={name:name+'-sealed' for name in p.IDENTITY_FIELDS} + subject.update(source_commit='reviewed-commit',target='forge-1.16.5',source_tree='same-tree', + artifacts={'main':'qualified-main','sources':'qualified-sources'},platform='linux-x86_64') + runs=[{'repository':'MinecraftModDevelopmentMods/LootableBodies','run':123,'head':'reviewed-commit','status':'completed','conclusion':'success','workflow':'.github/workflows/build.yml'}] + receipt={'schema':1,'subject':subject,'identity':p.identity(subject),'qualified_commit':'reviewed-commit', + 'label':'qualified original run, not rerun','required_gates':['gameplay','migration'], + 'gates':{'gameplay':True,'migration':True},'runs':copy.deepcopy(runs), + 'evidence':{'report.json':p.sha(report),'complete.log':p.sha(log)}} + path=root/'receipt.json';path.write_text(json.dumps(receipt));return subject,path,p.sha(path),runs,receipt + def invoke(self,subject,path,pin,runs,root):return p.plan(subject,path,pin,root,runs,{'gameplay','migration'}) + def test_same_tree_new_merge_commit_reuses_original_proof_honestly(self): + with tempfile.TemporaryDirectory() as folder: + root=Path(folder);subject,path,pin,runs,receipt=self.fixture(root);subject['source_commit']='merged-commit' + value=self.invoke(subject,path,pin,runs,root) + self.assertEqual('reuse',value['action']);self.assertFalse(value['fresh_execution']);self.assertEqual('reviewed-commit',value['qualified_commit']) + self.assertTrue(value['fresh_merge_and_publication_checks_required']) + def test_every_changed_identity_component_requires_fresh_qualification(self): + for key in p.IDENTITY_FIELDS: + with self.subTest(key=key),tempfile.TemporaryDirectory() as folder: + root=Path(folder);subject,path,pin,runs,receipt=self.fixture(root);subject[key]={'changed':True} + self.assertEqual('run',self.invoke(subject,path,pin,runs,root)['action']) + def test_missing_or_tampered_evidence_and_receipt_cannot_reuse(self): + for mutation in ('missing-log','changed-log','changed-receipt','missing-identity','failed-gate','missing-gate','extra-required-gate','no-files','path-escape'): + with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as folder: + root=Path(folder);subject,path,pin,runs,receipt=self.fixture(root) + if mutation=='missing-log':(root/'complete.log').unlink() + if mutation=='changed-log':(root/'complete.log').write_text('tampered') + if mutation=='changed-receipt':path.write_text('{}') + if mutation=='missing-identity':del subject['sdk'] + if mutation=='failed-gate':receipt['gates']['gameplay']=False + if mutation=='missing-gate':del receipt['gates']['migration'] + if mutation=='extra-required-gate':receipt['required_gates'].append('new-test') + if mutation=='no-files':receipt['evidence']={} + if mutation=='path-escape':receipt['evidence']['../outside.log']='0'*64 + if mutation in ('failed-gate','missing-gate','extra-required-gate','no-files','path-escape'): + path.write_text(json.dumps(receipt));pin=p.sha(path) + self.assertEqual('run',self.invoke(subject,path,pin,runs,root)['action']) + def test_fork_failed_stale_missing_or_changed_api_run_cannot_authorise_reuse(self): + for mutation in ('fork','failed','pending','other-head','other-workflow','missing','other-run','receipt-self-approval'): + with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as folder: + root=Path(folder);subject,path,pin,runs,receipt=self.fixture(root) + if mutation=='fork':runs[0]['repository']='SkyBlade1978/LootableBodies' + if mutation=='failed':runs[0]['conclusion']='failure' + if mutation=='pending':runs[0]['status']='in_progress' + if mutation=='other-head':runs[0]['head']='old-commit' + if mutation=='other-workflow':runs[0]['workflow']='unreviewed.yml' + if mutation=='missing':runs=[] + if mutation=='other-run':runs[0]['run']=999 + if mutation=='receipt-self-approval':pin='0'*64 + self.assertEqual('run',self.invoke(subject,path,pin,runs,root)['action']) + +if __name__=='__main__':unittest.main(verbosity=2) diff --git a/tools/test_inventory_client_isolation.py b/tools/test_inventory_client_isolation.py index 60b02e7..b40b093 100644 --- a/tools/test_inventory_client_isolation.py +++ b/tools/test_inventory_client_isolation.py @@ -13,19 +13,20 @@ class Prepared(Exception):pass jar=project/'build/libs'/client.MAIN_JAR jar.parent.mkdir(parents=True);jar.write_bytes(b'unchanged production bytes') def prepare(root,*args):root.mkdir() - def launch(command,**options): - self.assertEqual(home,options['cwd']) + def launch(command,root,label,timeout): + self.assertEqual(home,root) + self.assertEqual(('client',180),(label,timeout)) config=(home/'config/lootablebodies.cfg').read_text() self.assertIn('B:local_texture='+expected,config) self.assertEqual(b'unchanged production bytes',jar.read_bytes()) raise Prepared() argv=['verify_packaged_client','--java',str(project/'java'), - '--forge-client-home',str(project/'sdk'),'--output',str(home),'--mode',mode] + '--forge-client-home',str(project/'sdk'),'--client-inputs',str(project/'prepared'),'--output',str(home),'--mode',mode] with patch.object(client,'__file__',str(project/'tools/verify_packaged_client.py')),\ patch.object(client,'prepare_client',prepare),\ patch.object(client,'project_assets',return_value=project/'assets'),\ patch.object(client,'client_command',return_value=['owned-java']),\ - patch.object(subprocess,'run',side_effect=launch),patch('sys.argv',argv),\ + patch.object(client,'run_client',side_effect=launch),patch('sys.argv',argv),\ self.assertRaises(Prepared): client.main() diff --git a/tools/test_migration_ci.py b/tools/test_migration_ci.py index 016110f..694d8c4 100644 --- a/tools/test_migration_ci.py +++ b/tools/test_migration_ci.py @@ -36,10 +36,13 @@ 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_600_phases_and_120_refusals_are_required(self): + def test_all_760_phases_and_152_refusals_are_required(self): result = ci.aggregate(self.reports(), list(ORIGINS), 'a'*40) - self.assertEqual((60,600,120),(len(result['cases']),result['jvm_phases'],result['abort_cases'])) - earlier=[name for name in result['cases'] if not name.startswith(('released-115-','recovery-115-','via115-'))] + self.assertEqual((76,760,152),(len(result['cases']),result['jvm_phases'],result['abort_cases'])) + native16=[name for name in result['cases'] if not name.startswith(('released-116-','recovery-116-','via116-'))] + self.assertEqual(60,len(native16));self.assertEqual(600,sum(len(result['cases'][name]['phases']) for name in native16)) + self.assertEqual(120,sum(len(result['aborts'][name]['conditions']) for name in native16)) + earlier=[name for name in native16 if not name.startswith(('released-115-','recovery-115-','via115-'))] self.assertEqual(46,len(earlier)) self.assertEqual(460,sum(len(result['cases'][name]['phases']) for name in earlier)) self.assertEqual(92,sum(len(result['aborts'][name]['conditions']) for name in earlier)) @@ -100,5 +103,28 @@ def test_changed_test_tool_cannot_reuse_prepared_inputs(self): (source/'tools/check.py').write_text('different check') with self.assertRaises(ValueError):ci.restore(source,bundle,'a'*40) + def native_inputs(self,root): + folder=root/'build/native-server-distribution';expected={} + for name in ['libraries/forge/server.jar','libraries/forge/win_args.txt','libraries/forge/unix_args.txt']: + path=folder/name;path.parent.mkdir(parents=True,exist_ok=True);path.write_bytes(name.encode());expected[name]=ci.digest(path) + return folder,expected + + def test_native_preparation_seals_both_module_argument_files(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);folder,expected=self.native_inputs(root) + paths=ci.native_input_paths(root,expected) + self.assertEqual({(folder/name).relative_to(root).as_posix() for name in expected},set(paths)) + bundle=root/'bundle';ci.seal(root,paths,bundle,'a'*40) + restored=root/'restored';restored.mkdir();ci.restore(restored,bundle,'a'*40) + self.assertEqual(set(expected),{p.relative_to(restored/'build/native-server-distribution').as_posix() for p in (restored/'build/native-server-distribution').rglob('*') if p.is_file()}) + + def test_missing_or_changed_native_preparation_input_is_fatal(self): + for mutation in ['missing','changed']: + with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as directory: + root=Path(directory);folder,expected=self.native_inputs(root);path=folder/'libraries/forge/unix_args.txt' + if mutation=='missing':path.unlink() + else:path.write_bytes(b'other native arguments') + with self.assertRaises(ValueError):ci.native_input_paths(root,expected) + if __name__=='__main__':unittest.main() diff --git a/tools/test_multiplayer_reload_evidence.py b/tools/test_multiplayer_reload_evidence.py new file mode 100644 index 0000000..b3afc13 --- /dev/null +++ b/tools/test_multiplayer_reload_evidence.py @@ -0,0 +1,24 @@ +"""A native reload must retain console/latest/debug, without replaying clients.""" +from pathlib import Path +import tempfile,unittest +from unittest.mock import patch +from verify_multiplayer import reload_server + + +class MultiplayerReloadEvidenceTests(unittest.TestCase): + def test_explicit_native_logging_and_complete_logs_are_required(self): + for missing in [None,'multiplayer-reload.log','multiplayer-reload.latest.log','multiplayer-reload.debug.log']: + with self.subTest(missing=missing),tempfile.TemporaryDirectory() as folder: + home=Path(folder);logging=home/'fixture-log4j.xml';logging.write_text('synthetic logging fixture') + for name in ['multiplayer-reload.log','multiplayer-reload.latest.log','multiplayer-reload.debug.log']: + if name!=missing:(home/name).write_text('synthetic positive log') + with patch('verify_multiplayer.run') as run,patch('verify_multiplayer.audit') as audit: + if missing is None: + reload_server(Path('fixture-java'),home,logging) + self.assertEqual(3,audit.call_count) + self.assertEqual(['-Dlog4j.configurationFile='+logging.resolve().as_uri()],run.call_args.kwargs['extra']) + else: + with self.assertRaises(FileNotFoundError):reload_server(Path('fixture-java'),home,logging) + + +if __name__=='__main__':unittest.main() diff --git a/tools/test_native116_infrastructure.py b/tools/test_native116_infrastructure.py index 23d0da0..18ea517 100644 --- a/tools/test_native116_infrastructure.py +++ b/tools/test_native116_infrastructure.py @@ -5,19 +5,29 @@ class Native116InfrastructureTest(unittest.TestCase): def test_native_distribution_and_every_server_input_use_the_target(self): - self.assertEqual('forge-1.16.5-36.2.42',runtime.FORGE) - self.assertEqual('DFA90C681F30BE2406548B6429BCCB68D95AF4F64AB19B8C58F6128FD50FF857',runtime.INSTALLER_SHA256) - self.assertEqual({'forge-1.16.5-36.2.42.jar','minecraft_server.1.16.5.jar', - 'libraries/net/minecraftforge/forge/1.16.5-36.2.42/forge-1.16.5-36.2.42-server.jar'},set(runtime.SERVER_INPUTS)) + self.assertEqual('forge-1.17.1-37.1.1',runtime.FORGE) + self.assertEqual('31F2319B1B9CA491AACCC020887240F8524A4FF2A8E0F8B795E17E889CBC043B',runtime.INSTALLER_SHA256) + self.assertEqual(79,len(runtime.SERVER_INPUTS)) + self.assertEqual('35DB91BCD011CAD8B812BA165CD7F5AD934CEEEE9050621423F9E5D227D7F895', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-server.jar']) + self.assertEqual('B0E2D21DBA2E7C55A7DD9BBF914ADA99B124692810158F970F8F7C20543C2311', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt']) + self.assertFalse(any('1.16.5' in path for path in runtime.SERVER_INPUTS)) def test_development_assets_use_the_native_index(self): text=(Path(__file__).resolve().parents[1]/'gradle/assets.gradle').read_text() - self.assertIn('indexes/1.16.json',text) + self.assertIn('indexes/1.17.json',text) self.assertNotIn('e9d0adb8f642abe422909ede50f651b2b58a3573',text) def test_every_released115_history_extends_the_complete_earlier_matrix(self): from verify_world_upgrades import ORIGINS,PHASES names={'released-115','recovery-115','via115-110','via115-111','via115-112','via115-113','via115-114'} - self.assertTrue(names.issubset(ORIGINS));self.assertEqual(30,len(ORIGINS)) - self.assertEqual(23,len(set(ORIGINS)-names));self.assertEqual(10,len(PHASES)) + earlier={name for name in ORIGINS if not name.startswith(('released-116','recovery-116','via116-'))} + self.assertTrue(names.issubset(earlier));self.assertEqual(30,len(earlier)) + self.assertEqual(23,len(earlier-names));self.assertEqual(10,len(PHASES)) self.assertEqual('DC7798F3E165EC369D8092324C9F85F51D7E633824D1BD04D946A60ADA3DDDA7',ORIGINS['released-115'][2]) + def test_public116_sources_extend_every_prior_origin_without_replacing_it(self): + from verify_world_upgrades import ORIGINS + names={'released-116','recovery-116',*(f'via116-{version}' for version in range(110,116))} + self.assertEqual(38,len(ORIGINS));self.assertTrue(names.issubset(ORIGINS)) + self.assertEqual('E545CF725650E8A01C5863A4C92BCB2D23437D61C9441C2F5D27A5272EDDA229',ORIGINS['released-116'][2]) if __name__=='__main__':unittest.main() diff --git a/tools/test_native117_ci.py b/tools/test_native117_ci.py new file mode 100644 index 0000000..a952e97 --- /dev/null +++ b/tools/test_native117_ci.py @@ -0,0 +1,30 @@ +import hashlib,json,tempfile,unittest +from pathlib import Path +from unittest.mock import patch +import native117_ci as ci + +class Native117CITests(unittest.TestCase): + def test_only_the_six_accepted_bytes_can_be_promoted(self): + with tempfile.TemporaryDirectory() as d,patch.object(ci,'PROJECT',Path(d)): + target=Path(d)/'bundle';target.mkdir();expected={} + for name in ['main.jar','sources.jar','javadoc.jar','release.pom','SHA256SUMS','CHANGELOG.txt']: + data=('approved '+name).encode();(target/name).write_bytes(data);expected[name]=hashlib.sha256(data).hexdigest().upper() + metadata=Path(d)/'.github';metadata.mkdir() + (metadata/'release117.json').write_text(json.dumps({'version':ci.MOD_VERSION,'files':expected})) + self.assertEqual(expected,ci.check_bundle(target)) + for name in expected: + f=target/name;data=f.read_bytes();f.write_bytes(data+b'changed') + with self.assertRaises(ValueError):ci.check_bundle(target) + f.write_bytes(data) + (target/'extra.txt').write_text('unexpected') + with self.assertRaises(ValueError):ci.check_bundle(target) + def test_unknown_scenario_fails_before_a_game_command(self): + with tempfile.TemporaryDirectory() as d,patch.object(ci,'PROJECT',Path(d)),patch.object(ci,'python') as game,patch.dict('os.environ',{'JAVA_HOME_16_X64':d}): + # Scenario outputs are confined to the checkout, including failures. + import os + previous=Path.cwd() + try: + os.chdir(d) + with self.assertRaises(ValueError):ci.scenario('unknown') + finally:os.chdir(previous) + game.assert_not_called() diff --git a/tools/test_native117_client.py b/tools/test_native117_client.py new file mode 100644 index 0000000..618d7ff --- /dev/null +++ b/tools/test_native117_client.py @@ -0,0 +1,58 @@ +import json +import hashlib +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +import native117_client +import native_runtime + + +class Native117ClientTests(unittest.TestCase): + def test_native_module_metadata_is_used_instead_of_legacy_classpath_only_launch(self): + with patch('native117_client.command',return_value=['native37-command']) as launch: + result=native_runtime.client_command(Path('java'),Path('home'),Path('sdk'),Path('assets'),prepared=Path('prepared'),username='Player',extra=['-Dprobe=true']) + self.assertEqual(['native37-command'],result) + self.assertEqual(Path('prepared'),launch.call_args.args[4]) + self.assertEqual('Player',launch.call_args.kwargs['username']) + + def test_feature_os_and_version_rules_do_not_enable_demo_or_wrong_platform_flags(self): + arguments=[{'rules':[{'action':'allow','features':{'is_demo_user':True}}],'value':'--demo'}, + {'rules':[{'action':'allow','features':{'has_custom_resolution':True}}],'value':['--width','${width}']}, + {'rules':[{'action':'allow','os':{'name':'windows','version':'^10\\.'}}],'value':'-Dos.version=10.0'}, + {'rules':[{'action':'allow','os':{'name':'osx'}}],'value':'-XstartOnFirstThread'}] + self.assertEqual(['--width','854','-Dos.version=10.0'],native117_client.expand(arguments,{'width':'854'},system='windows',architecture='amd64',os_version='10.0.26200')) + + def test_unknown_placeholder_fails_before_a_process_can_be_started(self): + with self.assertRaises(KeyError): + native117_client.expand(['${unrecognised}'],{},system='windows',architecture='amd64',os_version='10.0') + + def test_jvm_filters_the_actual_base_jar_while_game_keeps_its_forge_profile_version(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);java=root/'jdk/bin/java.exe';java.parent.mkdir(parents=True) + (java.parent.parent/'release').write_text('SEMANTIC_VERSION="16.0.2+7"\nIMPLEMENTOR="Eclipse Foundation"\n') + index=root/'assets/indexes/1.17.json';index.parent.mkdir(parents=True);index.write_bytes(b'{}') + base={'assetIndex':{'id':'1.17','sha1':hashlib.sha1(b'{}').hexdigest()},'arguments':{'jvm':['-cp','${classpath}'],'game':['--version','${version_name}','--accessToken','${auth_access_token}']}} + forge={'arguments':{'jvm':['-DignoreList=forge-,${version_name}.jar'],'game':['--launchTarget','forgeclient']},'mainClass':'cpw.mods.bootstraplauncher.BootstrapLauncher'} + record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'1.17.1.jar')],'natives':str(root/'natives'),'system':'windows','architecture':'amd64'} + with patch('native117_client.load',return_value=(base,forge,record)): + result=native117_client.command(java,root/'home',root/'sdk',root/'assets',root/'prepared',username='Fixture') + self.assertIn('-DignoreList=forge-,1.17.1.jar',result) + self.assertEqual('1.17.1-forge-37.1.1',result[result.index('--version')+1]) + self.assertEqual('0',result[result.index('--accessToken')+1]) + + def test_unsealed_classpath_and_changed_native_bytes_are_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);library=root/'native.dll';library.write_bytes(b'original') + record={'version':native117_client.VERSION,'sdk':str(root.resolve()),'system':'windows','architecture':'amd64', + 'files':{str(library):native_runtime.digest(library)},'classpath':[str(root/'unsealed.jar')]} + (root/'inputs.json').write_text(json.dumps(record)) + with patch('native117_client.descriptors',return_value=({},{})),patch('native117_client.platform.system',return_value='Windows'),patch('native117_client.platform.architecture',return_value=('64bit','')): + with self.assertRaisesRegex(RuntimeError,'Unsealed'): + native117_client.load(root,root) + library.write_bytes(b'changed') + with self.assertRaisesRegex(RuntimeError,'Changed prepared'): + native117_client.load(root,root) + + +if __name__=='__main__':unittest.main() diff --git a/tools/test_native117_flat_header.py b/tools/test_native117_flat_header.py new file mode 100644 index 0000000..b4cea8e --- /dev/null +++ b/tools/test_native117_flat_header.py @@ -0,0 +1,38 @@ +import gzip +import hashlib +from pathlib import Path +import tempfile +import unittest +from native_runtime import seed_flat_fixture +from nbt_regions import Tag,encode + + +class Native117FlatHeaderTests(unittest.TestCase): + def header(self,root,version=2730,generator='minecraft:flat'): + settings=Tag(10,{'dimensions':Tag(10,{'minecraft:overworld':Tag(10,{ + 'generator':Tag(10,{'type':Tag(8,generator)})})})}) + data=encode('',Tag(10,{'Data':Tag(10,{'DataVersion':Tag(3,version),'WorldGenSettings':settings})})) + path=root/'native-header.dat';path.write_bytes(gzip.compress(data,mtime=0));return path,hashlib.sha256(path.read_bytes()).hexdigest().upper() + def home(self,root): + home=root/'home';home.mkdir();(home/'server.properties').touch();return home + def test_only_the_explicit_sealed_header_is_copied(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);header,digest=self.header(root);before=header.read_bytes();home=self.home(root) + seed_flat_fixture(home,header,digest) + self.assertEqual(before,(home/'world/level.dat').read_bytes());self.assertEqual(before,header.read_bytes()) + self.assertEqual(['level.dat'],[p.name for p in (home/'world').iterdir()]) + def test_wrong_hash_target_or_generator_refuses_before_creating_world(self): + for version,generator,bad_hash in [(2730,'minecraft:flat',True),(2586,'minecraft:flat',False),(2730,'minecraft:noise',False)]: + with self.subTest(version=version,generator=generator,bad_hash=bad_hash),tempfile.TemporaryDirectory() as directory: + root=Path(directory);header,digest=self.header(root,version,generator);home=self.home(root) + with self.assertRaises(RuntimeError):seed_flat_fixture(home,header,'0'*64 if bad_hash else digest) + self.assertFalse((home/'world').exists()) + def test_existing_world_is_never_overwritten(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);header,digest=self.header(root);home=self.home(root);(home/'world').mkdir() + protected=home/'world/level.dat';protected.write_bytes(b'existing world bytes') + with self.assertRaises(FileExistsError):seed_flat_fixture(home,header,digest) + self.assertEqual(b'existing world bytes',protected.read_bytes()) + + +if __name__=='__main__':unittest.main() diff --git a/tools/test_native117_history_inputs.py b/tools/test_native117_history_inputs.py new file mode 100644 index 0000000..4527f29 --- /dev/null +++ b/tools/test_native117_history_inputs.py @@ -0,0 +1,33 @@ +"""Exercise the actual sealed-archive reader before hosted preparation.""" +import hashlib,io,json,tarfile,tempfile,unittest +from pathlib import Path +import prepare_native117_history as history + +class Native117HistoryInputsTests(unittest.TestCase): + def fixture(self,root,names=None): + names=names or {'build/migration-seeds/original-110/world/level.dat':b'actual-original'} + folder=root/'download';folder.mkdir();archive=folder/'inputs.tar.gz';files={} + with tarfile.open(archive,'w:gz') as t: + for name,data in names.items(): + i=tarfile.TarInfo(name);i.size=len(data);t.addfile(i,io.BytesIO(data)) + files[name]={'size':len(data),'sha256':hashlib.sha256(data).hexdigest().upper()} + record={'head':history.SOURCE,'archive_sha256':history.sha(archive),'files':files} + (folder/'inputs.json').write_text(json.dumps(record)) + return folder,history.sha(folder/'inputs.json'),history.sha(archive) + def test_original_bytes_are_restored_once_without_regeneration(self): + with tempfile.TemporaryDirectory() as d: + root=Path(d);folder,manifest,archive=self.fixture(root);out=root/'original' + history.extract_sealed(folder,out,manifest,archive) + self.assertEqual(b'actual-original',(out/'build/migration-seeds/original-110/world/level.dat').read_bytes()) + with self.assertRaises(FileExistsError):history.extract_sealed(folder,out,manifest,archive) + def test_changed_manifest_or_archive_fail_before_extraction(self): + for target in ('inputs.json','inputs.tar.gz'): + with self.subTest(target=target),tempfile.TemporaryDirectory() as d: + root=Path(d);folder,manifest,archive=self.fixture(root);p=folder/target;p.write_bytes(p.read_bytes()+b'changed') + with self.assertRaisesRegex(ValueError,'Pinned'):history.extract_sealed(folder,root/'out',manifest,archive) + self.assertFalse((root/'out').exists()) + def test_traversal_and_unknown_archive_members_are_refused(self): + for name in ('../outside','build/../../outside','C:/outside'): + with self.subTest(name=name),tempfile.TemporaryDirectory() as d: + root=Path(d);folder,manifest,archive=self.fixture(root,{name:b'bad'}) + with self.assertRaises(ValueError):history.extract_sealed(folder,root/'out',manifest,archive) diff --git a/tools/test_native117_launch.py b/tools/test_native117_launch.py new file mode 100644 index 0000000..29a47b5 --- /dev/null +++ b/tools/test_native117_launch.py @@ -0,0 +1,80 @@ +import subprocess +import hashlib +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch +from types import SimpleNamespace +import native_runtime + + +class Native117LaunchTests(unittest.TestCase): + def test_posix_native_diagnostics_fixture_uses_the_posix_tools(self): + with patch.object(native_runtime,'os',SimpleNamespace(name='posix')): + self.test_packaged_server_uses_installer_module_arguments_and_java16_gc() + + def jdk(self, root, version='16.0.2+7'): + suffix='.exe' if native_runtime.os.name=='nt' else '' + java = root/('jdk/bin/java'+suffix) + java.parent.mkdir(parents=True) + for name in ('java', 'jstack', 'jstat'): + (java.parent/(name+suffix)).touch() + (java.parent.parent/'release').write_text( + 'SEMANTIC_VERSION="'+version+'"\nIMPLEMENTOR="Eclipse Foundation"\n') + return java + + def test_packaged_server_uses_installer_module_arguments_and_java16_gc(self): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory); java=self.jdk(home) + with patch('native_runtime.run_monitored', return_value=subprocess.CompletedProcess([],0)) as run: + native_runtime.run_java(java,home,'fixture',timeout=240) + command=run.call_args.args[0] + self.assertNotIn('-jar',command) + self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/1.17.1-37.1.1/') for arg in command)) + gc=next((arg for arg in command if arg.startswith('-Xlog:')),None) + self.assertIsNotNone(gc) + self.assertIn('gc*,safepoint',gc) + self.assertFalse(any('PrintGC' in arg or 'Xloggc' in arg for arg in command)) + self.assertEqual(240,run.call_args.kwargs['timeout']) + suffix='.exe' if native_runtime.os.name=='nt' else '' + self.assertEqual([str(java.with_name('jstack'+suffix)),'-l'],run.call_args.kwargs['diagnostic_command']) + self.assertEqual([str(java.with_name('jstat'+suffix)),'-gc'],run.call_args.kwargs['metric_command']) + + def test_runtime_rejects_a_different_jdk_before_launch(self): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory); java=self.jdk(home,'17.0.1+12') + with patch('native_runtime.run_monitored') as run: + with self.assertRaisesRegex(RuntimeError,'16.0.2'): + native_runtime.run_java(java,home,'fixture') + run.assert_not_called() + + def test_server_inputs_pin_native37_instead_of_the_old_launcher(self): + self.assertEqual('forge-1.17.1-37.1.1',native_runtime.FORGE) + for path in ('libraries/net/minecraftforge/forge/1.17.1-37.1.1/win_args.txt', + 'libraries/net/minecraftforge/forge/1.17.1-37.1.1/unix_args.txt', + 'libraries/net/minecraftforge/forge/1.17.1-37.1.1/forge-1.17.1-37.1.1-server.jar'): + self.assertIn(path,native_runtime.SERVER_INPUTS) + self.assertFalse(any('1.16.5' in path for path in native_runtime.SERVER_INPUTS)) + + def test_historical_cpu_budget_requires_its_own_native37_validation(self): + proof=native_runtime.WORKER_BUDGET_PROOF + self.assertEqual(('1.17.1','37.1.1',200,4,1),tuple(proof[key] for key in + ('minecraft','forge','reloads','jvms','native_preparation_workers'))) + self.assertTrue(proof['no_candidate'] and proof['no_worlds'] and proof['source_and_sdk_seals_unchanged']) + self.assertEqual(4,len(proof['receipts'])) + self.assertEqual(('-XX:ActiveProcessorCount=2',),native_runtime.HISTORICAL_JVM_ARGS) + + def test_owned_server_does_not_trigger_native_bare_json_flat_settings_fallback(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory); sdk=root/'sdk';(sdk/'libraries').mkdir(parents=True) + artifact=sdk/'libraries/pinned-fixture';artifact.write_bytes(b'owned-sdk-input') + pins={'libraries/pinned-fixture':hashlib.sha256(artifact.read_bytes()).hexdigest().upper()} + with patch.object(native_runtime,'SERVER_INPUTS',pins),patch('native_runtime.subprocess.run'): + native_runtime.prepare_server(root/'home',sdk) + properties=(root/'home/server.properties').read_text() + self.assertIn('level-type=DEFAULT\n',properties) + self.assertNotIn('level-type=FLAT',properties) + self.assertFalse((root/'home/world').exists(),'Historical/native-codec worlds are copied separately') + + +if __name__=='__main__':unittest.main() diff --git a/tools/test_native117_persistence.py b/tools/test_native117_persistence.py new file mode 100644 index 0000000..9f75b1d --- /dev/null +++ b/tools/test_native117_persistence.py @@ -0,0 +1,39 @@ +"""Synthetic codec inputs test the inspector; these are not historical worlds.""" +from pathlib import Path +import struct,tempfile,unittest +from nbt_regions import Tag,write_disposable_region +from native117_persistence import count_persisted + +class Native117PersistenceTest(unittest.TestCase): + def body(self,number=1,marker=7): + 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=2730,position=None): + return {'name':'','root':Tag(10,{'DataVersion':Tag(3,version),'Position':position or Tag(11,struct.pack('>ii',8,8)), + '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_actual_target_shape_counts_all_dimensions_without_writing(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_target_output_rejects_old_marker_and_old_native_version(self): + for marker,version in [(6,2730),(7,2586)]: + with self.subTest(marker=marker,version=version),tempfile.TemporaryDirectory() as directory: + world=Path(directory);self.write(world,self.chunk([self.body(marker=marker)],version=version)) + with self.assertRaises(ValueError):count_persisted(world,0) + def test_wrong_native_position_type_or_index_is_fatal(self): + for position in [Tag(9,(3,[Tag(3,8),Tag(3,8)])),Tag(11,struct.pack('>ii',9,8))]: + with tempfile.TemporaryDirectory() as directory: + world=Path(directory);self.write(world,self.chunk([self.body()],position=position)) + with self.assertRaises(ValueError):count_persisted(world,0) + def test_duplicate_persisted_corpse_identity_is_fatal(self): + with tempfile.TemporaryDirectory() as directory: + world=Path(directory);self.write(world,self.chunk([self.body(),self.body()])) + with self.assertRaises(ValueError):count_persisted(world,0) + def test_loaded_corpse_cannot_be_left_only_in_terrain_storage(self): + with tempfile.TemporaryDirectory() as directory: + world=Path(directory);path=world/'region/r.0.0.mca';path.parent.mkdir() + write_disposable_region(path,{264:{'name':'','root':Tag(10,{'Level':Tag(10,{'Entities':Tag(9,(10,[self.body(marker=6)]))})}),'compression':2,'timestamp':bytes(4)}}) + with self.assertRaises(ValueError):count_persisted(world,0) + +if __name__=='__main__':unittest.main() diff --git a/tools/test_native37_control_evidence.py b/tools/test_native37_control_evidence.py new file mode 100644 index 0000000..8446295 --- /dev/null +++ b/tools/test_native37_control_evidence.py @@ -0,0 +1,18 @@ +from pathlib import Path +import unittest +from native37_control_evidence import residual + +class Native37ControlEvidenceTests(unittest.TestCase): + def sample(self): + stack=(Path(__file__).parent/'fixtures/native37-delayed-movement-stack.txt').read_text() + return '[12:00:00] [Server thread/FATAL] [net.minecraft.util.thread.BlockableEventLoop]: Error executing task on Server\n'+stack + def test_exact_eight_native_control_blocks_are_identified_without_changing_raw_logs(self): + text=self.sample()*8+'[12:00:01] [Server thread/INFO] [minecraft/MinecraftServer]: Stopping server\n' + original=text;clean,count=residual(text) + self.assertEqual(8,count);self.assertEqual(original,text);self.assertNotIn('NullPointerException',clean) + def test_other_frames_exception_text_or_incomplete_counts_fail(self): + for text in (self.sample()*7,(self.sample()*8).replace('DistanceManager.java:230','DistanceManager.java:231'),(self.sample()*8).replace('objectset','somethingelse')): + with self.subTest(text=text[:40]),self.assertRaises(ValueError):residual(text) + def test_a_clean_native_platform_does_not_require_manufactured_errors(self): + text='[12:00:00] [Server thread/INFO] [minecraft/MinecraftServer]: Clean control\n' + self.assertEqual((text,0),residual(text)) diff --git a/tools/test_native37_platform_config.py b/tools/test_native37_platform_config.py new file mode 100644 index 0000000..ed2dca7 --- /dev/null +++ b/tools/test_native37_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_native37_platform_additions + +class Native37ConfigTest(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/forge37-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_native37_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/forge37-server-default.toml').read_bytes().replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true')) + self.assertEqual(['serverconfig/forge-server.toml'],allowed_native37_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/forge37-server-default.toml').read_bytes() + if change=='value':target=target.replace(b'fullBoundingBoxLadders = false',b'fullBoundingBoxLadders = true') + path.write_bytes(target) + self.assertEqual([],allowed_native37_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 new file mode 100644 index 0000000..d298728 --- /dev/null +++ b/tools/test_native_evidence.py @@ -0,0 +1,82 @@ +"""Synthetic receipts exercise fail-closed auditing, not native JVM execution.""" +import json,tempfile,unittest,os +from pathlib import Path +from native_runtime import digest +from native_evidence import audit_phase,verify_saved_audit,audit_case_mods + +class NativeEvidenceTest(unittest.TestCase): + def fixture(self,home): + phase='upgrade-near';directory=home/(phase+'.diagnostics');directory.mkdir() + (directory/'gc.txt').write_text('synthetic GC fixture') + for name in [phase+'.log',phase+'.latest.log',phase+'.debug.log']: + (home/name).write_text('LOOTABLE_BODIES_WORLD_UPGRADE_PASS phase='+phase+'\n') + record={'timed_out':False,'returncode':0,'pid':123,'timeout':240,'cwd':str(home), + 'command':['fixture-java','-XX:ActiveProcessorCount=2','-Dlootablebodies.phase='+phase, + '-Xlog:gc*,safepoint:file=upgrade-near.diagnostics/gc.txt:time,uptime,level,tags', + '@libraries/net/minecraftforge/forge/1.17.1-37.1.1/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], + 'evidence':{'gc.txt':digest(directory/'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): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory);self.fixture(home);record=audit_phase(home,'upgrade-near') + proof={'receipts':[record],'positive_complete_logs':[]};self.assertTrue(verify_saved_audit(home,proof)) + (home/'upgrade-near.diagnostics/gc.txt').write_text('changed') + self.assertFalse(verify_saved_audit(home,proof)) + with self.assertRaises(ValueError):audit_phase(home,'upgrade-near') + def test_missing_receipt_log_or_declared_diagnostic_is_fatal(self): + for name in ['upgrade-near.diagnostics/result.json','upgrade-near.debug.log','upgrade-near.diagnostics/gc.txt']: + with self.subTest(name=name),tempfile.TemporaryDirectory() as directory: + home=Path(directory);self.fixture(home);(home/name).unlink() + with self.assertRaises((ValueError,OSError)):audit_phase(home,'upgrade-near') + def test_wrong_case_native_distribution_deadline_or_failed_process_is_fatal(self): + for field in ['cwd','timeout','command','returncode','timed_out','captures']: + with self.subTest(field=field),tempfile.TemporaryDirectory() as directory: + home=Path(directory);folder,record=self.fixture(home) + record[field]={'cwd':str(home/'other'),'timeout':241,'command':['java','-jar','old.jar'], + 'returncode':1,'timed_out':True,'captures':[{'file':'missing.txt','returncode':None,'started_seconds':1}]}[field] + (folder/'result.json').write_text(json.dumps(record)) + with self.assertRaises(ValueError):audit_phase(home,'upgrade-near') + def test_relocated_artifact_requires_the_actual_case_suffix(self): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory)/'migration-results/case';home.mkdir(parents=True);folder,record=self.fixture(home) + record['cwd']='/original/runner/build/migration-results/case';(folder/'result.json').write_text(json.dumps(record)) + with self.assertRaises(ValueError):audit_phase(home,'upgrade-near') + audit_phase(home,'upgrade-near',relocated=True) + record['cwd']='/original/runner/build/migration-results/other';(folder/'result.json').write_text(json.dumps(record)) + with self.assertRaises(ValueError):audit_phase(home,'upgrade-near',relocated=True) + def test_native_process_must_have_exact_qualified_mod_copies(self): + from release_identity import MAIN_JAR + with tempfile.TemporaryDirectory() as directory: + home=Path(directory);mods=home/'mods';mods.mkdir();main=mods/MAIN_JAR;probe=mods/'lootablebodies-build-tests-reobf.jar' + main.write_bytes(b'synthetic candidate');probe.write_bytes(b'synthetic probe') + report={'candidate_sha256':digest(main),'probe_sha256':digest(probe)};self.assertEqual(2,len(audit_case_mods(home,report))) + main.write_bytes(b'changed') + with self.assertRaises(ValueError):audit_case_mods(home,report) + main.write_bytes(b'synthetic candidate');extra=mods/'unexpected.jar';extra.write_bytes(b'extra') + with self.assertRaises(ValueError):audit_case_mods(home,report) + extra.unlink();probe.unlink() + with self.assertRaises(ValueError):audit_case_mods(home,report) + + def test_downloaded_linux_receipt_is_audited_independently_of_auditor_os(self): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory)/'migration-results/case';home.mkdir(parents=True);folder,record=self.fixture(home) + 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.17.1-37.1.1/win_args.txt') + (folder/'result.json').write_text(json.dumps(record)) + with self.assertRaises(ValueError):audit_phase(home,'upgrade-near',relocated=True) + + def test_mod_aliases_cannot_hide_an_extra_loader_archive(self): + from unittest.mock import patch + from release_identity import MAIN_JAR + with tempfile.TemporaryDirectory() as directory: + home=Path(directory);mods=home/'mods';mods.mkdir();main=mods/MAIN_JAR;probe=mods/'lootablebodies-build-tests-reobf.jar' + main.write_bytes(b'synthetic candidate');probe.write_bytes(b'synthetic probe') + report={'candidate_sha256':digest(main),'probe_sha256':digest(probe)} + (mods/'unexpected.jar').write_bytes(b'extra') + original=Path.is_symlink + with patch.object(Path,'is_symlink',lambda path:path.name=='unexpected.jar' or original(path)): + with self.assertRaises(ValueError):audit_case_mods(home,report) + +if __name__=='__main__':unittest.main() diff --git a/tools/test_owned_process.py b/tools/test_owned_process.py new file mode 100644 index 0000000..ae05004 --- /dev/null +++ b/tools/test_owned_process.py @@ -0,0 +1,60 @@ +from pathlib import Path +import json +import subprocess +import sys +import tempfile +import time +import unittest +from unittest.mock import patch +from owned_process import OwnedProcess + + +class OwnedProcessTest(unittest.TestCase): + def start(self,root,script,timeout): + home=root/'home';home.mkdir() + return OwnedProcess([sys.executable,'-u','-c',script],cwd=home,log=root/'client.log',timeout=timeout, + diagnostic_command=[sys.executable,'-c','print("owned capture")']) + def test_completed_process_preserves_same_stdout_alias_and_receipt(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);p=self.start(root,'print("real owned output")',5) + self.assertEqual(0,p.wait(timeout=5));self.assertEqual(0,p.poll()) + self.assertEqual((root/'client.log').read_bytes(),(root/'home/client.log').read_bytes()) + record=json.loads((root/'home/client.diagnostics/result.json').read_text()) + self.assertEqual(0,record['returncode']);self.assertFalse(record['timed_out']) + def test_group_abort_reaps_its_owned_child_without_changing_runtime_deadline(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);p=self.start(root,'import time; print("ready"); time.sleep(30)',30) + p.terminate();p.wait(timeout=5) + record=json.loads((root/'home/client.diagnostics/result.json').read_text()) + self.assertEqual(30,record['timeout']);self.assertEqual('owned_group_abort',record['stopped_reason']) + self.assertFalse(record['timed_out']);self.assertIsNotNone(record['returncode']) + def test_deadline_failure_still_keeps_a_failed_receipt_and_complete_stdout(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);p=self.start(root,'import time; print("before deadline"); time.sleep(30)',.3) + with self.assertRaises(subprocess.TimeoutExpired):p.wait(timeout=5) + record=json.loads((root/'home/client.diagnostics/result.json').read_text()) + self.assertTrue(record['timed_out']);self.assertNotEqual(0,record['returncode']) + self.assertIn('before deadline',(root/'client.log').read_text()) + def test_console_alias_failure_does_not_leave_an_unowned_running_child(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory) + with patch('owned_process.os.link',side_effect=OSError('injected alias failure')): + with self.assertRaisesRegex(OSError,'injected alias'): + self.start(root,'import time; print("ready"); time.sleep(.5)',3) + receipt=root/'home/client.diagnostics/result.json';deadline=time.monotonic()+4 + while time.monotonic()deadline: raise RuntimeError('Disposable server did not start') time.sleep(.2) - monitor=LoginMonitor(root) + monitor=LoginMonitor(root,legacy_recovery=False) start_clients(start);deadline=time.monotonic()+180 while any(p.poll() is None for p in processes.values()): monitor.poll() @@ -89,7 +105,7 @@ def start(name): if not (home/'screenshots'/shot).is_file(): raise RuntimeError('Missing screenshot '+role+' '+shot) (server/'server.properties').write_text(properties.replace('server-port='+str(port),'server-port=0')+'view-distance=4\n') archive_live_logs(root) - run(java,server,'multiplayer-reload',message='LOOTABLE_BODIES_RUNTIME_PASS phase=multiplayer-reload') + reload_server(java,server,project/'src/integrationTest/resources/log4j2.xml') if digest(jar)!=before: raise RuntimeError('Candidate changed') (root/'result.json').write_text(json.dumps({'candidate_sha256':before,'two_real_tcp_clients':True, 'checks':(shared/'checks.txt').read_text().splitlines(),'corpse_and_real_player_disk_reload':True},indent=2)+'\n') diff --git a/tools/verify_native_respawn_control.py b/tools/verify_native_respawn_control.py index 6191def..2b4ee5d 100644 --- a/tools/verify_native_respawn_control.py +++ b/tools/verify_native_respawn_control.py @@ -1,10 +1,12 @@ -"""Reproduce the pinned Forge 36 delayed-movement error without Lootable Bodies.""" -import argparse,json,socket,subprocess,time +"""Exercise native37 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 native_runtime import prepare_server,prepare_client,client_command,project_assets,FORGE,digest +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) _,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') @@ -16,12 +18,17 @@ def main(): parser.add_argument('--java',type=Path,required=True) parser.add_argument('--forge-home',type=Path,required=True) parser.add_argument('--forge-client-home',type=Path,required=True) + parser.add_argument('--client-inputs',type=Path,required=True) + parser.add_argument('--assets',type=Path) + parser.add_argument('--flat-header',type=Path,required=True) + parser.add_argument('--flat-header-sha256',required=True) parser.add_argument('--hidden-agent',type=Path) parser.add_argument('--output',type=Path,required=True) args=parser.parse_args();project=Path(__file__).resolve().parents[1] root=args.output.resolve();root.mkdir(parents=True,exist_ok=False) probe=project/'build/control-test-mod/forge-platform-control-reobf.jar' prepare_server(root/'server',args.forge_home.resolve(),probe=probe) + seed_flat_fixture(root/'server',args.flat_header,args.flat_header_sha256) prepare_client(root/'client',probe=probe) for home in (root/'server',root/'client'): if list((home/'mods').glob('*.jar'))!=[home/'mods'/probe.name]: @@ -34,14 +41,15 @@ def main(): '-Dlog4j.configurationFile='+(project/'src/integrationTest/resources/log4j2.xml').as_uri()] if args.hidden_agent:extra.append('-javaagent:'+str(args.hidden_agent.resolve())) commands={'server':[str(args.java.resolve()),'-Xmx1G','-Dlootablebodies.control.tcpServer=true', - '-Dlootablebodies.control.deaths=true','-jar',FORGE+'.jar','--nogui'], + '-Dlootablebodies.control.deaths=true', + '-Dlog4j.configurationFile='+(project/'src/integrationTest/resources/log4j2.xml').as_uri(), + '@libraries/net/minecraftforge/forge/1.17.1-37.1.1/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], 'client':client_command(args.java.resolve(),root/'client',args.forge_client_home.resolve(), - project_assets(project),username='ControlOnly',extra=extra)} + args.assets.resolve() if args.assets else project_assets(project),username='ControlOnly',extra=extra,prepared=args.client_inputs.resolve())} processes={};logs=[] def start(name): - log=(root/(name+'.log')).open('w');logs.append(log) - processes[name]=subprocess.Popen(commands[name],cwd=root/name,stdin=subprocess.PIPE, - stdout=log,stderr=subprocess.STDOUT,text=True) + processes[name]=native_process(commands[name],cwd=root/name,log=root/(name+'.log'), + timeout=420 if name=='server' else 240,console_input=name=='server') try: start('server');deadline=time.monotonic()+120 while 'FORGE_PLATFORM_CONTROL_READY' not in (root/'server.log').read_text(errors='replace'): @@ -66,11 +74,12 @@ def start(name): 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)) - count,additional=control_error_counts(root/'server.log') - if count!=8 or 'NATIVE_FIFO_DELAY_RELEASE packets=' not in client: - raise RuntimeError('Native control did not reproduce the expected upstream error') - result={'lootable_bodies_installed':False,'respawns':8,'native_error_reproductions':count, - 'additional_native_rotation_errors':additional, + 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('Native37 control did not exercise all eight real delayed respawn/FIFO actions') + result={'lootable_bodies_installed':False,'respawns':8,'native37_delayed_fifo_batches':batches, + 'legacy36_error_matcher_used':False,'native37_complete_logs_clean':True, 'control_jar_sha256':digest(probe),'native_outbound_packets_delayed_fifo':True,'log_audits':reports} (root/'result.json').write_text(json.dumps(result,indent=2)+'\n') print('NATIVE_RESPAWN_CONTROL_PASS',json.dumps(result),flush=True) diff --git a/tools/verify_nbt_recovery.py b/tools/verify_nbt_recovery.py index efd0af9..81e6a0a 100644 --- a/tools/verify_nbt_recovery.py +++ b/tools/verify_nbt_recovery.py @@ -2,15 +2,19 @@ import argparse,json,shutil from pathlib import Path from verify_packaged import MAIN,prepare,run,digest,fingerprint +from native_runtime import seed_flat_fixture def main(): p=argparse.ArgumentParser(description=__doc__) p.add_argument('--java',type=Path,required=True);p.add_argument('--forge-home',type=Path,required=True) + p.add_argument('--flat-header',type=Path,required=True);p.add_argument('--flat-header-sha256',required=True) p.add_argument('--baseline-jar',type=Path);p.add_argument('--output',type=Path,required=True);a=p.parse_args() project=Path(__file__).resolve().parents[1];root=a.output.resolve();root.mkdir(parents=True,exist_ok=False) java=a.java.resolve();distribution=a.forge_home.resolve();main=project/'build/libs'/MAIN;baseline=a.baseline_jar.resolve() if a.baseline_jar else main;probe=project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' original=digest(baseline);candidate=digest(main) - seed=root/'sealed-seed';prepare(seed,distribution,baseline,probe);run(java,seed,'nbt-seed');sealed=fingerprint(seed/'world') + seed=root/'sealed-seed';prepare(seed,distribution,baseline,probe) + seed_flat_fixture(seed,a.flat_header,a.flat_header_sha256) + run(java,seed,'nbt-seed');sealed=fingerprint(seed/'world') home=root/'candidate';prepare(home,distribution,main,probe);shutil.copytree(seed/'world',home/'world') far=home/'world/region/r.8.8.mca';deferred=digest(far) run(java,home,'nbt-reload');assert digest(far)==deferred,'Unopened malformed region changed' diff --git a/tools/verify_normal_server.py b/tools/verify_normal_server.py index 24a5c6f..7afc9ba 100644 --- a/tools/verify_normal_server.py +++ b/tools/verify_normal_server.py @@ -5,15 +5,18 @@ from pathlib import Path import subprocess import time +import re +import shutil from audit_runtime_logs import audit from verify_packaged import prepare, digest, FORGE, MAIN -from native_runtime import seed_loader_defaults +from native_runtime import seed_loader_defaults,seed_flat_fixture +from owned_process import native_process def prepare_development_home(home): home.mkdir(exist_ok=True);(home/'eula.txt').write_text('eula=true\n') seed_loader_defaults(home) - (home/'server.properties').write_text('level-name=world\nlevel-type=FLAT\nserver-ip=127.0.0.1\nserver-port=0\nonline-mode=false\n') + (home/'server.properties').write_text('level-name=world\nlevel-type=DEFAULT\nserver-ip=127.0.0.1\nserver-port=0\nonline-mode=false\n') for name in ['ops','whitelist','banned-players','banned-ips']: (home/(name+'.json')).write_text('[]\n') @@ -22,6 +25,8 @@ def main(): parser.add_argument('--java',type=Path,required=True) parser.add_argument('--forge-home',type=Path,required=True) parser.add_argument('--output',type=Path,required=True) + parser.add_argument('--flat-header',type=Path,required=True) + parser.add_argument('--flat-header-sha256',required=True) parser.add_argument('--development',action='store_true') args=parser.parse_args(); project=Path(__file__).resolve().parents[1] root=args.output.resolve();root.mkdir(parents=True,exist_ok=False) @@ -30,35 +35,74 @@ def main(): prepare_development_home(home) init=root/'ordinary.init.gradle' path=home.as_posix().replace("'","\\'") - init.write_text("gradle.projectsEvaluated { def p=gradle.rootProject; p.minecraft.runs.getByName('server').workingDir.set(new File('"+path+"')); p.tasks.named('runServer') { workingDir new File('"+path+"') } }\n") - command=[str(project/('gradlew.bat' if os.name=='nt' else 'gradlew')),'runServer','--init-script',str(init),'--no-daemon','--console=plain'] + logging=(project/'src/integrationTest/resources/log4j2.xml').as_uri() + init.write_text("gradle.projectsEvaluated { def p=gradle.rootProject; p.minecraft.runs.getByName('server').workingDir.set(new File('"+path+"')); p.tasks.named('runServer') { workingDir new File('"+path+"'); systemProperty 'log4j.configurationFile','"+logging+"'; jvmArgs '-Xlog:gc*,safepoint:file=ordinary-game.gc.txt:time,uptime,pid,level,tags' } }\n") + command=[str(project/('gradlew.bat' if os.name=='nt' else 'gradlew')),'runServer','--init-script',str(init),'-PgameRunDirectory='+str(home),'--no-daemon','--console=plain'] cwd=project else: prepare(home,args.forge_home.resolve(),jar) - command=[str(args.java.resolve()),'-Xmx1G','-jar',FORGE+'.jar','--nogui'];cwd=home + command=[str(args.java.resolve()),'-Xmx1G', + '-Dlog4j.configurationFile='+(project/'src/integrationTest/resources/log4j2.xml').as_uri(), + '@libraries/net/minecraftforge/forge/1.17.1-37.1.1/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'];cwd=home + seed_flat_fixture(home,args.flat_header,args.flat_header_sha256) results=[] for phase in ['fresh','reload']: + started=time.monotonic();whole_deadline=started+210 log=root/(phase+'.log') - with log.open('w',encoding='utf-8') as output: - process=subprocess.Popen(command,cwd=cwd,stdin=subprocess.PIPE,stdout=output,stderr=subprocess.STDOUT,text=True) - try: - deadline=time.monotonic()+150 - while 'Done (' not in log.read_text(encoding='utf-8',errors='replace'): - if process.poll() is not None or time.monotonic()>deadline: raise RuntimeError('Ordinary server did not start: '+str(log)) - time.sleep(.2) - process.stdin.write('stop\n');process.stdin.flush();process.wait(timeout=60) - finally: - if process.poll() is None: process.terminate();process.wait(timeout=15) + output=None + if args.development: + output=log.open('x',encoding='utf-8') + process=subprocess.Popen(command,cwd=cwd,stdin=subprocess.PIPE,stdout=output,stderr=subprocess.STDOUT,text=True,start_new_session=os.name!='nt') + else:process=native_process(command,cwd=home,log=log,timeout=210,console_input=True) + try: + deadline=started+150 + while 'Done (' not in log.read_text(encoding='utf-8',errors='replace'): + if process.poll() is not None or time.monotonic()>deadline: raise RuntimeError('Ordinary server did not start: '+str(log)) + time.sleep(.2) + if args.development: + gc=home/'ordinary-game.gc.txt' + matches=re.findall(r'\[([0-9]+)\]\[(?:info|debug|trace|warning|error)\]',gc.read_text()) + if not matches or len(set(matches))!=1:raise RuntimeError('Cannot identify the actual owned development game JVM from its new GC log') + pid=int(matches[0]) + else:pid=json.loads((home/(phase+'.diagnostics/result.json')).read_text())['pid'] + diagnostics={} + for label,tool,option in [('threads','jstack','-l'),('heap-gc','jstat','-gc')]: + suffix='.exe' if os.name=='nt' else '';binary=args.java.resolve().with_name(tool+suffix);target=root/(phase+'.'+label+'.txt') + with target.open('x') as stream: + remaining=whole_deadline-time.monotonic() + if remaining<=0:raise subprocess.TimeoutExpired(command,210) + receipt=subprocess.run([str(binary),option,str(pid)],stdout=stream,stderr=subprocess.STDOUT,timeout=min(10,remaining)) + if receipt.returncode or not target.stat().st_size:raise RuntimeError('Actual native16 diagnostic failed: '+str(target)) + diagnostics[target.name]=digest(target) + process.stdin.write('stop\n');process.stdin.flush() + remaining=whole_deadline-time.monotonic() + if remaining<=0:raise subprocess.TimeoutExpired(command,210) + process.wait(timeout=min(60,remaining)) + finally: + if process.poll() is None: + if args.development and os.name=='nt': + # This exact Popen-created launcher tree is owned by this run. + subprocess.run(['taskkill','/PID',str(process.pid),'/T','/F'],stdout=subprocess.DEVNULL,stderr=subprocess.STDOUT,timeout=10) + elif args.development: + import signal + os.killpg(process.pid,signal.SIGKILL) + else:process.terminate() + process.wait(timeout=15) + if output is not None:output.close() text=log.read_text(encoding='utf-8',errors='replace') if process.returncode or 'Stopping server' not in text: raise RuntimeError('Ordinary server failed clean startup/stop: '+str(log)) - if 'lootablebodiesbuildtest' in text or list((home/'crash-reports').glob('*')): raise RuntimeError('Probe leak or crash') + if any(name in text for name in ['lootablebodiesbuildtest','forgeplatformcontrol']) or list((home/'crash-reports').glob('*')): raise RuntimeError('Probe leak or crash') audit(log,development=args.development,native=True) for name in ('latest.log','debug.log'): complete=home/'logs'/name if not complete.is_file():raise FileNotFoundError(complete) audit(complete,development=args.development,native=True) - if 'lootablebodiesbuildtest' in complete.read_text():raise RuntimeError('Probe in ordinary runtime') + if any(name in complete.read_text() for name in ['lootablebodiesbuildtest','forgeplatformcontrol']):raise RuntimeError('Probe in ordinary runtime') + shutil.copy2(complete,root/(phase+'.'+name)) + if args.development: + shutil.copy2(home/'ordinary-game.gc.txt',root/(phase+'.game-gc.txt')) + (root/(phase+'.native-game-receipt.json')).write_text(json.dumps({'actual_game_pid':pid,'pinned_java':str(args.java.resolve()),'game_gc':phase+'.game-gc.txt','diagnostics':diagnostics,'gradle_launcher_pid':process.pid,'startup_timeout':150,'shutdown_timeout':60,'source':'Actual native16 GC PID decorator from this newly owned home; no global process enumeration'},indent=2)+'\n') if not args.development and (len(list((home/'mods').glob('*.jar')))!=1 or digest(home/'mods'/MAIN)!=digest(jar)): raise RuntimeError('Ordinary packaged mod set changed') results.append(phase);print('ORDINARY_SERVER_PASS',phase,'development='+str(args.development),flush=True) diff --git a/tools/verify_packaged.py b/tools/verify_packaged.py index ca5dc4c..db4f628 100644 --- a/tools/verify_packaged.py +++ b/tools/verify_packaged.py @@ -1,13 +1,15 @@ -"""Verify the exact production jar in disposable native Forge 36 servers.""" +"""Verify the exact production jar in disposable native Forge 37 servers.""" import argparse import json from pathlib import Path -from native_runtime import FORGE,digest,fingerprint,prepare_server as prepare,run_server,install +from native_runtime import FORGE,digest,fingerprint,prepare_server as prepare,run_server,seed_flat_fixture from release_identity import MAIN_JAR as MAIN def run(java,home,phase): - run_server(java,home,phase,message='LOOTABLE_BODIES_RUNTIME_PASS phase='+phase) + logging=Path(__file__).resolve().parents[1]/'src/integrationTest/resources/log4j2.xml' + run_server(java,home,phase,message='LOOTABLE_BODIES_RUNTIME_PASS phase='+phase, + extra=['-Dlog4j.configurationFile='+logging.as_uri()]) print('PACKAGED_PASS',home.name,phase,flush=True) @@ -15,23 +17,25 @@ def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--java',type=Path,required=True) parser.add_argument('--output',type=Path,required=True) - parser.add_argument('--forge-home',type=Path) + parser.add_argument('--forge-home',type=Path,required=True) + parser.add_argument('--flat-header',type=Path,required=True) + parser.add_argument('--flat-header-sha256',required=True) args=parser.parse_args();project=Path(__file__).resolve().parents[1] main_jar=project/'build/libs'/MAIN probe=project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' output=args.output.resolve();output.mkdir(parents=True,exist_ok=False) - distribution=args.forge_home.resolve() if args.forge_home else output/'forge-distribution' - if not args.forge_home:install(args.java.resolve(),distribution) + distribution=args.forge_home.resolve() fresh = output / "candidate-fresh" prepare(fresh, distribution, main_jar, probe) + seed_flat_fixture(fresh,args.flat_header,args.flat_header_sha256) run(args.java, fresh, "fresh") - far = fresh / "world/region/r.8.8.mca" - before = digest(far) + far = [fresh/('world/'+kind+'/r.8.8.mca') for kind in ('region','entities')] + before = {path.relative_to(fresh).as_posix():digest(path) for path in far} run(args.java, fresh, "reload") - if digest(far) != before: - raise RuntimeError("Unopened far region changed") + if any(digest(path)!=before[path.relative_to(fresh).as_posix()] for path in far): + raise RuntimeError("Unopened far terrain or entity region changed") run(args.java, fresh, "deferred") - report = {"candidate_sha256": digest(main_jar), "deferred_region_unchanged": before} + report = {"candidate_sha256": digest(main_jar), "deferred_regions_unchanged": before} (output / "result.json").write_text(json.dumps(report, indent=2) + "\n") diff --git a/tools/verify_packaged_client.py b/tools/verify_packaged_client.py index 2cc7fcd..bd609cc 100644 --- a/tools/verify_packaged_client.py +++ b/tools/verify_packaged_client.py @@ -1,4 +1,4 @@ -"""Run the exact production jar and isolated verifier in official Forge 36.""" +"""Run the exact production jar and isolated verifier in official Forge 37.""" import argparse import hashlib import json @@ -8,6 +8,22 @@ from audit_runtime_logs import audit from native_runtime import project_assets,prepare_client,client_command from release_identity import MAIN_JAR +from runtime_diagnostics import run_monitored + + +def run_client(command,root,label,timeout): + diagnostics=root/(label+'.diagnostics') + gc='-Xlog:gc*,safepoint:file='+label+'.diagnostics/gc.txt:time,uptime,level,tags' + launch=[command[0],gc,*command[1:]] + java=Path(command[0]);suffix='.exe' if java.suffix=='.exe' else '' + try: + return run_monitored(launch,cwd=root,log=root/(label+'.log'),timeout=timeout, + diagnostics=diagnostics,diagnostic_command=[str(java.with_name('jstack'+suffix)),'-l'], + metric_command=[str(java.with_name('jstat'+suffix)),'-gc']) + finally: + for name in ('latest.log','debug.log'): + log=root/'logs'/name + if log.is_file():shutil.copy2(log,root/(label+'.'+name)) def audit_client(root,console): @@ -21,9 +37,14 @@ def main(): parser.add_argument('--java',type=Path,required=True) parser.add_argument('--forge-client-home',type=Path,required=True) parser.add_argument('--output',type=Path,required=True) + parser.add_argument('--client-inputs',type=Path,required=True) + parser.add_argument('--assets',type=Path) + parser.add_argument('--rejected-displaced-viewer',action='store_true') parser.add_argument('--hidden-agent',type=Path) parser.add_argument('--mode',choices=['default','config','skin','issues','names'],default='default') args=parser.parse_args();project=Path(__file__).resolve().parents[1] + if args.rejected_displaced_viewer and args.mode!='issues': + raise ValueError('Rejected/displaced viewer control requires issues mode') root=args.output.resolve();main=project/'build/libs'/MAIN_JAR probe=project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' candidate_hash=hashlib.sha256(main.read_bytes()).hexdigest().upper() @@ -33,13 +54,14 @@ def main(): local='true' if args.mode in ('default','names','issues') else 'false' (root/'config/lootablebodies.cfg').write_text('options {\n B:local_texture='+local+'\n}\n') extra=['-Dlootablebodies.clientCheck=true','-Dlog4j.configurationFile='+(project/'src/integrationTest/resources/log4j2.xml').as_uri()] + if args.rejected_displaced_viewer: + extra+=['-Dlootablebodies.rejectFirstCorpseOpen=true','-Dlootablebodies.issueClient.offsetViewer=true'] if args.hidden_agent:extra.append('-javaagent:'+str(args.hidden_agent.resolve())) if args.mode!='default': option={'config':'configUiCheck','skin':'visibilitySkinCheck','issues':'issueClientCheck','names':'namelessClientCheck'}[args.mode] extra.append('-Dlootablebodies.'+option+'=true') - command=client_command(args.java.resolve(),root,args.forge_client_home.resolve(),project_assets(project),extra=extra) - with (root/'client.log').open('w',encoding='utf-8') as log: - result=subprocess.run(command,cwd=root,stdout=log,stderr=subprocess.STDOUT,timeout=180) + command=client_command(args.java.resolve(),root,args.forge_client_home.resolve(),args.assets.resolve() if args.assets else project_assets(project),extra=extra,prepared=args.client_inputs.resolve()) + result=run_client(command,root,'client',180) text=(root/'client.log').read_text(errors='replace') expected={'default':'LOOTABLE_BODIES_CLIENT_PASS','config':'LOOTABLE_BODIES_CONFIG_CLIENT_PASS', 'skin':'LOOTABLE_BODIES_VISIBILITY_SKIN_PASS','issues':'LOOTABLE_BODIES_ISSUE5_CLIENT_PASS','names':'LOOTABLE_BODIES_NAME_TRANSLATION_PASS'}[args.mode] @@ -55,16 +77,20 @@ def main(): raise RuntimeError('Missing graphical mode assertions: '+str(report)) if args.mode=='config': command.insert(2,'-Dlootablebodies.configUiReload=true') - with (root/'restart.log').open('w',encoding='utf-8') as log: - restart=subprocess.run(command,cwd=root,stdout=log,stderr=subprocess.STDOUT,timeout=120) + restart=run_client(command,root,'restart',120) if restart.returncode or not (root/'config-restart.pass').is_file(): raise RuntimeError('Configuration did not survive process restart') audit_client(root,'restart.log') + if args.rejected_displaced_viewer: + if text.count('CORPSE_GUI_FIRST_INTERACTION_REJECTED_CONTROL')!=1: + raise RuntimeError('Missing or duplicate first-open rejection control') + for token in ('ISSUE5_VIEWER_APPROACH corpse=','ISSUE5_DISTANT_VIEWER_CONFIRMED corpse='): + if token not in text:raise RuntimeError('Missing rejected/displaced control evidence: '+token) if list((root/'crash-reports').glob('*')) or hashlib.sha256(main.read_bytes()).hexdigest().upper()!=candidate_hash: raise RuntimeError('Crash or candidate mutation in graphical mode') (root/'result.json').write_text(json.dumps({'candidate_sha256':candidate_hash,'mode':args.mode, 'checks':report.read_text().splitlines(),'screenshots':sorted(p.name for p in (root/'screenshots').glob('*.png')), - 'computer_control':'none'},indent=2)+'\n') + 'computer_control':'none','rejected_displaced_control':args.rejected_displaced_viewer},indent=2)+'\n') print('PACKAGED_CLIENT_MODE_PASS',args.mode,flush=True) return checks=['connected_player_death_and_respawn','network_shift_click_exact_nbt', diff --git a/tools/verify_upgrade_abort.py b/tools/verify_upgrade_abort.py index b5b4ba9..40b9771 100644 --- a/tools/verify_upgrade_abort.py +++ b/tools/verify_upgrade_abort.py @@ -74,6 +74,32 @@ def allowed_native_session_lock(before, after, world, original=None): return [name] if actual==marker+original[3:] else [] +def allowed_native37_platform_additions(before,after,world,original_config=None): + """Exact pinned Forge37 spec corrections, proved with the actual native spec.""" + 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/forge37-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={'removeErroringEntities':bool,'removeErroringTileEntities':bool, + 'logCascadingWorldGeneration':bool,'fixVanillaCascading':bool, + 'dimensionUnloadQueueDelay':int,'clumpingThreshold':int} + 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) @@ -98,8 +124,10 @@ def inject_unsafe_journal(world, case): def run_abort(java, home): from native_runtime import run_java, HISTORICAL_JVM_ARGS + logging=Path(__file__).resolve().parents[1]/'src/integrationTest/resources/log4j2.xml' return run_java(java, home, 'abort', timeout=180, - jvm_args=['-Xmx1G', *HISTORICAL_JVM_ARGS]) + jvm_args=['-Xmx1G', *HISTORICAL_JVM_ARGS, + '-Dlog4j.configurationFile='+logging.as_uri()]) def main(): @@ -137,7 +165,7 @@ def main(): raise RuntimeError('World loading continued after unsafe preservation') after = source_files(world) raw_after = dict(after) - platform_additions = allowed_platform_additions(before, after, world, original_config) + platform_additions = allowed_native37_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 f7fb941..1344b8d 100644 --- a/tools/verify_visibility.py +++ b/tools/verify_visibility.py @@ -6,6 +6,9 @@ import time from pathlib import Path import shutil +import os +from native_runtime import seed_flat_fixture +from owned_process import native_process from audit_runtime_logs import audit from fixture_login import LoginMonitor, start_clients, write_receipts, archive_live_logs from verify_multiplayer import client_command @@ -17,6 +20,10 @@ def main(): parser.add_argument('--java',type=Path,required=True) parser.add_argument('--forge-home',type=Path,required=True) parser.add_argument('--forge-client-home',type=Path,required=True) + parser.add_argument('--client-inputs',type=Path,required=True) + parser.add_argument('--assets',type=Path) + parser.add_argument('--flat-header',type=Path,required=True) + parser.add_argument('--flat-header-sha256',required=True) parser.add_argument('--hidden-agent',type=Path) parser.add_argument('--output',type=Path,required=True) parser.add_argument('--mode',choices=['visibility','respawn'],required=True) @@ -38,17 +45,20 @@ def main(): from verify_world_upgrades import fingerprint source_before=fingerprint(args.source_server/'world') shutil.copytree(args.source_server/'world',home/'server/world') + else: + seed_flat_fixture(home/'server',args.flat_header,args.flat_header_sha256) with socket.socket() as reservation: reservation.bind(('127.0.0.1',0)); port=reservation.getsockname()[1] props=(home/'server/server.properties').read_text().replace('server-port=0','server-port='+str(port))+'view-distance=4\n' (home/'server/server.properties').write_text(props) commands={'server':[str(java),'-Xmx1G','-Dlootablebodies.'+args.mode+'=true', + '-Dlog4j.configurationFile='+(project/'src/integrationTest/resources/log4j2.xml').as_uri(), '-Dlootablebodies.multiplayer.sync='+str(shared),'-Dlootablebodies.respawn.cases='+str(args.cases), - '-jar',FORGE+'.jar','--nogui']} + '@libraries/net/minecraftforge/forge/1.17.1-37.1.1/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui']} if args.source_server: commands['server'].insert(2,'-Dlootablebodies.visibility.reload=true') if args.simultaneous: commands['server'].insert(2,'-Dlootablebodies.respawn.simultaneous=true') for role in ['A','B']: - command=client_command(project,java,args.forge_client_home.resolve(),home/('client'+role),None,shared,port,role,args.hidden_agent,probe) + command=client_command(project,java,args.forge_client_home.resolve(),home/('client'+role),None,shared,port,role,args.hidden_agent,probe,prepared=args.client_inputs.resolve(),assets=args.assets.resolve() if args.assets else None) command.insert(2,'-Dlootablebodies.'+args.mode+'=true') if args.simultaneous: command.insert(2,'-Dlootablebodies.respawn.simultaneous=true') if args.counter_collision: command.insert(2,'-Dlootablebodies.respawn.counterCollision=true') @@ -57,14 +67,13 @@ def main(): (home/('client'+role)/'config/lootablebodies.cfg').write_text('options {\n B:local_texture=false\n}\n') processes={}; logs=[] def start(name): - log=(home/(name+'.log')).open('w',encoding='utf-8');logs.append(log) - processes[name]=subprocess.Popen(commands[name],cwd=home/name,stdout=log,stderr=subprocess.STDOUT) + processes[name]=native_process(commands[name],cwd=home/name,log=home/(name+'.log'),timeout=710 if name=='server' else 650) try: start('server');deadline=time.monotonic()+60 while not (shared/'server.ready').is_file(): if processes['server'].poll() is not None or time.monotonic()>deadline: raise RuntimeError('Server did not start') time.sleep(.2) - monitor=LoginMonitor(home) + monitor=LoginMonitor(home,legacy_recovery=False) start_clients(start);deadline=time.monotonic()+650 while any(process.poll() is None for process in processes.values()): monitor.poll() diff --git a/tools/verify_world_upgrades.py b/tools/verify_world_upgrades.py index f4e5935..10280df 100644 --- a/tools/verify_world_upgrades.py +++ b/tools/verify_world_upgrades.py @@ -45,6 +45,16 @@ 'original-112': ('1.12.2', '2.4.0', 'DE701F657A03BFFE08D6F9FEB1516B37B23B0171B91716087C7C84D9C19B9B5C'), 'recovery-111': ('1.11.2', '2.4.2.111021', '59ACF5652505F08A7F011307B3CD68F3754FCFC14C4C330BD952475351AEC932'), }) +ORIGINS.update({ + 'released-116': ('1.16.5','2.4.7.116051','E545CF725650E8A01C5863A4C92BCB2D23437D61C9441C2F5D27A5272EDDA229'), + 'recovery-116': ('1.16.5','2.4.7.116051','E545CF725650E8A01C5863A4C92BCB2D23437D61C9441C2F5D27A5272EDDA229'), + 'via116-110': ORIGINS['original-110'], + 'via116-111': ORIGINS['released-111'], + 'via116-112': ORIGINS['released-112'], + 'via116-113': ORIGINS['released-113'], + 'via116-114': ORIGINS['released-114'], + 'via116-115': ORIGINS['released-115'], +}) 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'} @@ -159,21 +169,23 @@ def seed_native115(java, home, recovery=False): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--java', type=Path, required=True) - parser.add_argument('--forge110-home', type=Path, required=True) - parser.add_argument('--forge111-home', type=Path, required=True) - parser.add_argument('--forge112-home', type=Path, required=True) + parser.add_argument('--forge110-home', type=Path) + parser.add_argument('--forge111-home', type=Path) + parser.add_argument('--forge112-home', type=Path) parser.add_argument('--forge113-home', type=Path) parser.add_argument('--forge114-home', type=Path) parser.add_argument('--forge115-home', type=Path) parser.add_argument('--forge116-home', type=Path) + parser.add_argument('--forge117-home', type=Path) for origin in ORIGINS: if origin.startswith('recovery-') or origin.startswith('via'): continue - parser.add_argument('--' + origin, type=Path, required=True) + parser.add_argument('--' + origin, type=Path) parser.add_argument('--origins', nargs='+', choices=list(ORIGINS), default=list(ORIGINS)) parser.add_argument('--output', type=Path, required=True) parser.add_argument('--seed-only', action='store_true') parser.add_argument('--seeds', nargs='+', type=Path) + parser.add_argument('--prepared-sources', type=Path, help='Exact sealed38-origin paths; reuse original writer worlds') parser.add_argument('--variants', nargs='+', choices=['normal', 'stamps'], default=['normal', 'stamps']) parser.add_argument('--control-report', type=Path) args = parser.parse_args() @@ -181,6 +193,8 @@ def main(): if not args.seed_only: verify(args, project) return + if any(name in args.origins for name in ('released-116','recovery-116')) or any(name.startswith('via116-') for name in args.origins): + raise ValueError('Native16 sources require the genuine public16 writer/transition preparation, then --prepared-sources') root = args.output.resolve() root.mkdir(parents=True, exist_ok=False) seeder = project / 'build/legacy-seed/lootablebodies-world-fixtures-reobf.jar' @@ -449,7 +463,8 @@ def seed_transition112(args,project,root,seeder,origin): def run_upgrade(java, home, phase): from native_runtime import run_server, HISTORICAL_JVM_ARGS - run_server(java,home,phase,extra=HISTORICAL_JVM_ARGS, + logging=Path(__file__).resolve().parents[1]/'src/integrationTest/resources/log4j2.xml' + run_server(java,home,phase,extra=[*HISTORICAL_JVM_ARGS,'-Dlog4j.configurationFile='+logging.as_uri()], message='LOOTABLE_BODIES_WORLD_UPGRADE_PASS phase='+phase) @@ -516,18 +531,23 @@ def preserve_historical_markers(home): def verify(args, project): - if not args.seeds: - raise ValueError('--seeds is required for the upgrade matrix') - if not args.forge116_home: - raise ValueError('--forge116-home is required for the native upgrade matrix') + prepared=getattr(args,'prepared_sources',None) + if not args.seeds and not prepared: + 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.forge117_home: + raise ValueError('--forge117-home is required for the native17 upgrade matrix') root = args.output.resolve() root.mkdir(parents=True, exist_ok=False) candidate = project / 'build/libs' / MAIN_JAR probe = project / 'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' candidate_hash = digest(candidate) probe_hash = digest(probe) - sources = {} - for seed_root in args.seeds: + sources = {};prepared_proof=None + if prepared: + from prepared_worlds import load + sources,prepared_proof=load(prepared,ORIGINS,args.origins) + for seed_root in args.seeds or []: seed_root = seed_root.resolve() report = json.loads((seed_root / 'seed-report.json').read_text(encoding='utf-8')) for name, details in report['origins'].items(): @@ -541,14 +561,14 @@ def verify(args, project): sources[name] = (source, details) if set(sources) != set(args.origins): raise RuntimeError('Missing requested historical fixture') - report = {'candidate_sha256': candidate_hash, 'probe_sha256':probe_hash, 'cases': {}} + report = {'candidate_sha256': candidate_hash, 'probe_sha256':probe_hash, 'prepared_sources':prepared_proof, 'cases': {}} for origin in args.origins: source,details=sources[origin] for variant in args.variants: name = origin + '-' + variant home = root / name from native_runtime import prepare_server - prepare_server(home,args.forge116_home.resolve(),candidate,probe) + prepare_server(home,args.forge117_home.resolve(),candidate,probe) shutil.copytree(source, home / 'world') world = home / 'world' historical_markers=preserve_historical_markers(home) @@ -565,18 +585,8 @@ def verify(args, project): 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 - persisted = 0 - for region in world.rglob('region/r.*.mca'): - if wave >= 0 and region.name != ['r.0.0.mca', 'r.8.8.mca', 'r.16.16.mca'][wave]: - continue - for chunk in read_region(region).values(): - for entity in chunk['root'].value['Level'].value.get('Entities', Tag(9, (10, []))).value[1]: - if entity.value.get('id', Tag(8, '')).value not in ('lootablebodies.Corpse', 'lootablebodies:corpse'): - continue - if wave >= 0 or phase in ('upgrade-return', 'upgrade-final', 'upgrade-final-reload'): - if entity.value['id'].value != 'lootablebodies:corpse' or entity.value.get('LootableBodiesDataVersion') != Tag(3, 6): - raise RuntimeError('A loaded corpse was not actually saved in the new format') - persisted += 1 + from native117_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') for relative, original in distant.items():