diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 7e2e1d8..038768b 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -20,6 +20,9 @@ MATRIX113 = MATRIX112 + [origin + ' / ' + stamps for origin in ['released-112', 'recovery-112', 'via112-110', 'via112-111'] for stamps in ['normal', 'stamps']] +MATRIX114 = MATRIX113 + [origin + ' / ' + stamps for origin in + ['released-113', 'recovery-113', 'via113-110', 'via113-111', 'via113-112'] + for stamps in ['normal', 'stamps']] MATRIX = [origin + ' / ' + stamps for origin in ['original-110', 'released-110', 'original-111', 'recovery-110'] for stamps in ['normal', 'stamps']] @@ -53,7 +56,8 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.10.2': ('110021', '1.10.2', '12.18.3.2511'), '1.11.2': ('111021', '1.11.2-dev', '13.20.1.2588'), '1.12.2': ('112021', 'master-1.12', '14.23.5.2859'), - '1.13.2': ('113021', 'master-1.13.2', '25.0.223')}[minecraft] + '1.13.2': ('113021', 'master-1.13.2', '25.0.223'), + '1.14.4': ('114041', 'master-1.14.4', '28.2.26')}[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', @@ -62,7 +66,8 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w if mutation: values.update(mutation) checks = CORE + {'1.11.2': MATRIX, '1.12.2': MATRIX112, - '1.13.2': MATRIX113 + ['Validate release workflow']}.get(minecraft, []) if names is None else names + '1.13.2': MATRIX113 + ['Validate release workflow'], + '1.14.4': MATRIX114 + ['Validate release workflow']}.get(minecraft, []) if names is None else names def command(args, **options): if args[0] == 'git': return SHA + '\n' @@ -85,8 +90,13 @@ 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_115_candidate_cannot_dispatch_publication(self): + code, values = self.select('2.4.6.115021') + self.assertNotEqual(0, code) + self.assertEqual({}, values) + def test_routes_all_supported_versions(self): - for suffix, minecraft, branch in [('110021', '1.10.2', '1.10.2'), ('111021', '1.11.2', '1.11.2-dev'), ('112021', '1.12.2', 'master-1.12'), ('113021', '1.13.2', 'master-1.13.2')]: + for suffix, minecraft, branch in [('110021', '1.10.2', '1.10.2'), ('111021', '1.11.2', '1.11.2-dev'), ('112021', '1.12.2', 'master-1.12'), ('113021', '1.13.2', 'master-1.13.2'), ('114041', '1.14.4', 'master-1.14.4')]: with self.subTest(minecraft=minecraft): status, values = self.select('2.4.2.' + suffix) self.assertEqual(0, status) @@ -94,7 +104,7 @@ def test_routes_all_supported_versions(self): self.assertEqual(branch, values['source_branch']) def test_invalid_versions_are_rejected(self): - for version in ['2.4.2', '2.4.2.114041', '02.4.2.111021', '2.4.2.111021;exit 0']: + for version in ['2.4.2', '2.4.2.114021', '02.4.2.111021', '2.4.2.111021;exit 0']: with self.subTest(version=version): self.assertNotEqual(0, self.select(version)[0]) @@ -143,6 +153,23 @@ def test_113_audit_receives_the_registered_shared_ci_completion(self): event_names = audit.split('workflows: [', 1)[1].split(']', 1)[0].split(',') self.assertIn('LootableBodies 1.12 CI', [name.strip() for name in event_names]) + def test_114_requires_every_upgrade_case_and_workflow_validation(self): + self.validate('1.14.4') + required = MATRIX114 + ['Validate release workflow'] + self.assertEqual(34, len(MATRIX114)) + for missing in required: + with self.subTest(missing=missing), self.assertRaises(SystemExit): + self.validate('1.14.4', names=CORE + [name for name in required if name != missing]) + + def test_114_requires_an_independent_audit_of_its_exact_artifact(self): + with self.assertRaises(SystemExit): + self.validate('1.14.4', audit_success=False) + + def test_114_audit_receives_the_registered_shared_ci_completion(self): + audit = WORKFLOW.with_name('artifact-audit-1.14.yml').read_text(encoding='utf-8') + event_names = audit.split('workflows: [', 1)[1].split(']', 1)[0].split(',') + self.assertIn('LootableBodies 1.12 CI', [name.strip() for name in event_names]) + 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.14.yml b/.github/workflows/artifact-audit-1.14.yml new file mode 100644 index 0000000..090ebe8 --- /dev/null +++ b/.github/workflows/artifact-audit-1.14.yml @@ -0,0 +1,64 @@ +name: Audit LootableBodies 1.14 artifacts +run-name: Audit 1.14.4 artifacts from ${{ github.event.workflow_run.head_sha }} + +on: + workflow_run: + workflows: [LootableBodies 1.12 CI, LootableBodies 1.14 CI] + types: [completed] + +permissions: + contents: read + actions: read + +jobs: + audit: + # Pull-request merge trees are audited inside CI. Push artifacts use the + # workflow run's exact head SHA, which is also their declared artifact name. + if: >- + github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && + (github.event.workflow_run.head_branch == 'master-1.14.4') + 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: '8.0.502+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.14.4-${{ github.event.workflow_run.head_sha }} + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + path: downloaded + - name: Require exact inputs and assemble an immutable audit bundle + run: | + set -euo pipefail + mkdir audited + for name in LootableBodies-2.4.5.114041.jar LootableBodies-2.4.5.114041-sources.jar LootableBodies-2.4.5.114041-javadoc.jar; do + test -s "downloaded/build/libs/$name" + cp "downloaded/build/libs/$name" audited/ + done + test -s downloaded/build/release/SHA256SUMS + test -s downloaded/build/publications/mavenJava/pom-default.xml + test -s downloaded/CHANGELOG.txt + cp downloaded/build/release/SHA256SUMS audited/ + cp downloaded/build/publications/mavenJava/pom-default.xml audited/LootableBodies-2.4.5.114041.pom + cp downloaded/CHANGELOG.txt audited/ + - name: Audit original bytes without recompiling the jars + env: + LOOTABLEBODIES_JAVA8_HOME: ${{ env.JAVA_HOME_8_X64 }} + LOOTABLEBODIES_MAVENIZER_JAVA_HOME: ${{ env.JAVA_HOME_25_X64 }} + run: | + chmod +x ./gradlew + ./gradlew verifyPreparedReleaseArtifacts -PpreparedReleaseDir=audited --no-daemon diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd57f3a..d66f5b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,19 +1,19 @@ -name: LootableBodies 1.13 CI +name: LootableBodies 1.14 CI on: push: branches: - - master-1.13.2 + - master-1.14.4 - 'feature/**' pull_request: branches: - - master-1.13.2 + - master-1.14.4 permissions: contents: read concurrency: - group: lootablebodies-1.13-${{ github.workflow }}-${{ github.ref }} + group: lootablebodies-1.14-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: @@ -141,6 +141,9 @@ jobs: python3 tools/native_runtime.py --java "$JAVA_HOME_8_X64/bin/java" --output build/native-server-distribution python3 tools/native_runtime.py --java "$JAVA_HOME_8_X64/bin/java" --output build/native-client-distribution --client + - name: Verify the independent native respawn control + run: xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 tools/verify_native_respawn_control.py --java "$JAVA_HOME_8_X64/bin/java" --forge-home build/native-server-distribution --forge-client-home build/native-client-distribution --output build/native-respawn-control + - name: Verify packaged servers and malformed inventory recovery run: | python3 tools/verify_packaged.py --java "$JAVA_HOME_8_X64/bin/java" --forge-home build/native-server-distribution --output build/packaged-runtime @@ -157,7 +160,23 @@ jobs: xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 tools/verify_packaged_client.py --java "$JAVA_HOME_8_X64/bin/java" --forge-client-home build/native-client-distribution --output "build/packaged-client-$mode" --mode "$mode" done + - name: Verify recovery after a rejected first interaction + env: + JAVA_TOOL_OPTIONS: -Dlootablebodies.rejectFirstCorpseOpen=true -Dlootablebodies.issueClient.offsetViewer=true + run: | + xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 tools/verify_packaged_client.py --java "$JAVA_HOME_8_X64/bin/java" --forge-client-home build/native-client-distribution --output build/packaged-client-rejected-interaction --mode issues + python3 - <<'PY' + from pathlib import Path + log = Path('build/packaged-client-rejected-interaction/client.log').read_text() + assert log.count('CORPSE_GUI_FIRST_INTERACTION_REJECTED_CONTROL') == 1 + assert 'ISSUE5_VIEWER_APPROACH corpse=' in log + assert 'ISSUE5_DISTANT_VIEWER_CONFIRMED corpse=' in log + assert 'LOOTABLE_BODIES_ISSUE5_CLIENT_PASS' in log + PY + - name: Verify two-player inventory, death and visibility + env: + JAVA_TOOL_OPTIONS: -Dlootablebodies.visibility.offsetFirstDeath=true run: | xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 tools/verify_multiplayer.py --java "$JAVA_HOME_8_X64/bin/java" --forge-home build/native-server-distribution --forge-client-home build/native-client-distribution --output build/packaged-multiplayer xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 tools/verify_visibility.py --java "$JAVA_HOME_8_X64/bin/java" --forge-home build/native-server-distribution --forge-client-home build/native-client-distribution --output build/packaged-visibility --mode visibility @@ -173,17 +192,17 @@ jobs: xvfb-run -a -s '-screen 0 1280x720x24 +extension GLX +render -noreset' python3 tools/verify_visibility.py --java "$JAVA_HOME_8_X64/bin/java" --forge-home build/native-server-distribution --forge-client-home build/native-client-distribution --output build/packaged-respawn-restart --mode respawn --cases 16 --source-server build/packaged-respawn-ordinary/server --simultaneous --counter-collision - name: Audit complete runtime logs - run: python3 tools/audit_runtime_logs.py --native --recursive build/integration-run build/upgrade-contract-run build/client-integration-run build/packaged-runtime build/packaged-nbt build/ordinary-development build/ordinary-packaged build/packaged-client-default build/packaged-client-config build/packaged-client-skin build/packaged-client-issues build/packaged-client-names build/packaged-multiplayer build/packaged-visibility build/packaged-visibility-restart build/packaged-respawn-ordinary build/packaged-respawn-simultaneous build/packaged-respawn-restart build/packaged-respawn-counter-collision + run: python3 tools/audit_runtime_logs.py --native --recursive build/native-respawn-control build/integration-run build/upgrade-contract-run build/client-integration-run build/packaged-runtime build/packaged-nbt build/ordinary-development build/ordinary-packaged build/packaged-client-default build/packaged-client-config build/packaged-client-skin build/packaged-client-issues build/packaged-client-names build/packaged-multiplayer build/packaged-visibility build/packaged-visibility-restart build/packaged-respawn-ordinary build/packaged-respawn-simultaneous build/packaged-respawn-restart build/packaged-respawn-counter-collision - name: Compare a second complete build run: | mkdir -p .gradle/repro-first - for name in LootableBodies-2.4.4.113021.jar LootableBodies-2.4.4.113021-sources.jar LootableBodies-2.4.4.113021-javadoc.jar; do + for name in LootableBodies-2.4.5.114041.jar LootableBodies-2.4.5.114041-sources.jar LootableBodies-2.4.5.114041-javadoc.jar; do cp "build/libs/$name" .gradle/repro-first/ done cp build/release/SHA256SUMS .gradle/repro-first/ ./gradlew build javadoc prepareReleaseBundle --rerun-tasks --no-build-cache --no-daemon - for name in LootableBodies-2.4.4.113021.jar LootableBodies-2.4.4.113021-sources.jar LootableBodies-2.4.4.113021-javadoc.jar; do + for name in LootableBodies-2.4.5.114041.jar LootableBodies-2.4.5.114041-sources.jar LootableBodies-2.4.5.114041-javadoc.jar; do cmp ".gradle/repro-first/$name" "build/libs/$name" done cmp .gradle/repro-first/SHA256SUMS build/release/SHA256SUMS @@ -191,23 +210,35 @@ jobs: - name: Require all declared reports and artifacts run: | - for path in build/libs/LootableBodies-2.4.4.113021.jar build/libs/LootableBodies-2.4.4.113021-sources.jar build/libs/LootableBodies-2.4.4.113021-javadoc.jar build/release/SHA256SUMS build/publications/mavenJava/pom-default.xml CHANGELOG.txt build/packaged-runtime/result.json build/packaged-nbt/result.json build/ordinary-development/result.json build/ordinary-packaged/result.json build/packaged-client-default/result.json build/packaged-client-config/result.json build/packaged-client-skin/result.json build/packaged-client-issues/result.json build/packaged-client-names/result.json build/packaged-multiplayer/result.json build/packaged-visibility/result.json build/packaged-visibility-restart/result.json build/packaged-respawn-ordinary/result.json build/packaged-respawn-simultaneous/result.json build/packaged-respawn-restart/result.json build/packaged-respawn-counter-collision/result.json build/release-contracts/result.json; do + for path in build/native-respawn-control/result.json build/libs/LootableBodies-2.4.5.114041.jar build/libs/LootableBodies-2.4.5.114041-sources.jar build/libs/LootableBodies-2.4.5.114041-javadoc.jar build/release/SHA256SUMS build/publications/mavenJava/pom-default.xml CHANGELOG.txt build/packaged-runtime/result.json build/packaged-nbt/result.json build/ordinary-development/result.json build/ordinary-packaged/result.json build/packaged-client-default/result.json build/packaged-client-config/result.json build/packaged-client-skin/result.json build/packaged-client-issues/result.json build/packaged-client-names/result.json build/packaged-multiplayer/result.json build/packaged-visibility/result.json build/packaged-visibility-restart/result.json build/packaged-respawn-ordinary/result.json build/packaged-respawn-simultaneous/result.json build/packaged-respawn-restart/result.json build/packaged-respawn-counter-collision/result.json build/release-contracts/result.json; do test -s "$path" done test -s build/test-results/test/TEST-cyano.lootable.contracts.NativeUpgradeBoundaryTest.xml test -s build/test-results/test/TEST-cyano.lootable.network.LoginReadGuardTest.xml test -s build/upgrade-contract-run/world/upgrade-contracts.pass + - name: Keep native control evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: Native-Forge-28-control-${{ github.sha }} + if-no-files-found: error + retention-days: 30 + path: | + build/native-respawn-control/result.json + build/native-respawn-control/*.log + build/native-respawn-control/server/logs/*.log + build/native-respawn-control/client/logs/*.log + - name: Upload audited local candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.13.2-${{ github.sha }} + name: LootableBodies-1.14.4-${{ github.sha }} if-no-files-found: error retention-days: 30 path: | - build/libs/LootableBodies-2.4.4.113021.jar - build/libs/LootableBodies-2.4.4.113021-sources.jar - build/libs/LootableBodies-2.4.4.113021-javadoc.jar + build/libs/LootableBodies-2.4.5.114041.jar + build/libs/LootableBodies-2.4.5.114041-sources.jar + build/libs/LootableBodies-2.4.5.114041-javadoc.jar build/release/SHA256SUMS build/publications/mavenJava/pom-default.xml CHANGELOG.txt @@ -216,7 +247,8 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.13.2-diagnostics-${{ github.sha }} + name: LootableBodies-1.14.4-diagnostics-${{ github.sha }} + include-hidden-files: true if-no-files-found: error retention-days: 14 path: | @@ -227,3 +259,9 @@ jobs: build/**/screenshots/*.png build/**/result.json build/**/*checks.txt + .gradle/legacy-tools/released-112/build.log + .gradle/legacy-tools/released-113/build.log + build/packaged-client-issues/config/*.toml + build/packaged-client-issues/saves/issue-fixture/level.dat + build/packaged-client-issues/saves/issue-fixture/region/*.mca + build/packaged-client-issues/saves/issue-fixture/playerdata/*.dat diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5d740bf..c3b05bb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-1.13.2 + - master-1.14.4 - 'feature/**' pull_request: branches: - - master-1.13.2 + - master-1.14.4 schedule: - cron: '43 7 * * 4' diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index c1f6100..122e410 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 or 113021. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021 or 114041. required: true type: string curseforge_release_level: @@ -48,12 +48,13 @@ 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)$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(110021|111021|112021|113021|114041)$ ]] 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 ;; *.112021) minecraft_version=1.12.2; source_branch=master-1.12 ;; *.113021) minecraft_version=1.13.2; source_branch=master-1.13.2 ;; + *.114041) minecraft_version=1.14.4; source_branch=master-1.14.4 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -81,7 +82,8 @@ jobs: targets = {'1.10.2': ('1.10.2', '12.18.3.2511', '110021'), '1.11.2': ('1.11.2-dev', '13.20.1.2588', '111021'), '1.12.2': ('master-1.12', '14.23.5.2859', '112021'), - '1.13.2': ('master-1.13.2', '25.0.223', '113021')} + '1.13.2': ('master-1.13.2', '25.0.223', '113021'), + '1.14.4': ('master-1.14.4', '28.2.26', '114041')} branch, forge, suffix = targets[minecraft] if os.environ['SOURCE_BRANCH'] != branch: raise SystemExit('The selected branch does not match its Minecraft version') @@ -101,19 +103,21 @@ jobs: checks = json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/commits/{sha}/check-runs?per_page=100'], text=True))['check_runs'] required = {'Cold Forge bootstrap', 'Build, test, and audit', 'Analyze Java', 'Validation'} origins = ['original-110', 'released-110', 'original-111', 'recovery-110'] - if minecraft in ('1.12.2', '1.13.2'): + if minecraft in ('1.12.2', '1.13.2', '1.14.4'): origins += ['released-111', 'original-112', 'recovery-111', 'via111-110'] - if minecraft == '1.13.2': + if minecraft in ('1.13.2', '1.14.4'): origins += ['released-112', 'recovery-112', 'via112-110', 'via112-111'] required.add('Validate release workflow') - if minecraft in ('1.11.2', '1.12.2', '1.13.2'): + if minecraft == '1.14.4': + origins += ['released-113', 'recovery-113', 'via113-110', 'via113-111', 'via113-112'] + if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4'): required.update(origin + ' / ' + stamps for origin in origins for stamps in ['normal', 'stamps']) 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'): + if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4'): target = minecraft.rsplit('.', 1)[0] audits = json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/actions/workflows/artifact-audit-{target}.yml/runs?event=workflow_run&per_page=100'], text=True))['workflow_runs'] diff --git a/.github/workflows/migration-matrix.yml b/.github/workflows/migration-matrix.yml index 8a6a44c..a0aa1ad 100644 --- a/.github/workflows/migration-matrix.yml +++ b/.github/workflows/migration-matrix.yml @@ -1,10 +1,10 @@ -name: LootableBodies 1.13 world upgrades +name: LootableBodies 1.14 world upgrades on: push: - branches: [master-1.13.2] + branches: [master-1.14.4] pull_request: - branches: [master-1.13.2] + branches: [master-1.14.4] workflow_dispatch: permissions: @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - origin: [original-110, released-110, original-111, recovery-110, released-111, original-112, recovery-111, via111-110, released-112, recovery-112, via112-110, via112-111] + origin: [original-110, released-110, original-111, recovery-110, released-111, original-112, recovery-111, via111-110, released-112, recovery-112, via112-110, via112-111, released-113, recovery-113, via113-110, via113-111, via113-112] stamps: [normal, stamps] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -44,6 +44,8 @@ jobs: chmod +x ./gradlew - name: Build historical fixture tools against the released API run: python3 tools/prepare_legacy_tools.py --java17-home "$JAVA_HOME" --java8-home "$JAVA_HOME_8_X64" --java25-home "$JAVA_HOME_25_X64" + - name: Build the released 1.13 fixture tools + run: python3 tools/prepare_native113_tools.py --java17-home "$JAVA_HOME" --java8-home "$JAVA_HOME_8_X64" --java25-home "$JAVA_HOME_25_X64" - name: Build candidate and isolated fixture writers run: ./gradlew build prepareLegacyWorldFixtures preparePackagedIntegrationProbe preparePackagedForgeControl --no-daemon - name: Download and verify historical artifacts @@ -53,18 +55,18 @@ jobs: - name: Create worlds with historical mod writers run: >- python3 tools/verify_world_upgrades.py --java "$JAVA_HOME_8_X64/bin/java" - --forge110-home build/migration-inputs/forge110 --forge111-home build/migration-inputs/forge111 --forge112-home build/migration-inputs/forge112 + --forge110-home build/migration-inputs/forge110 --forge111-home build/migration-inputs/forge111 --forge112-home build/migration-inputs/forge112 --forge113-home build/migration-inputs/forge113 --original-110 build/migration-inputs/original-110.jar --released-110 build/migration-inputs/released-110.jar - --original-111 build/migration-inputs/original-111.jar --released-111 build/migration-inputs/released-111.jar --original-112 build/migration-inputs/original-112.jar --released-112 build/migration-inputs/released-112.jar --origins ${{ matrix.origin }} + --original-111 build/migration-inputs/original-111.jar --released-111 build/migration-inputs/released-111.jar --original-112 build/migration-inputs/original-112.jar --released-112 build/migration-inputs/released-112.jar --released-113 build/migration-inputs/released-113.jar --origins ${{ matrix.origin }} --seed-only --output build/migration-seeds - name: Verify first load, recovery, deferred dimensions and restarts run: >- python3 tools/verify_world_upgrades.py --java "$JAVA_HOME_8_X64/bin/java" - --forge110-home build/migration-inputs/forge110 --forge111-home build/migration-inputs/forge111 --forge112-home build/migration-inputs/forge112 + --forge110-home build/migration-inputs/forge110 --forge111-home build/migration-inputs/forge111 --forge112-home build/migration-inputs/forge112 --forge113-home build/migration-inputs/forge113 --original-110 build/migration-inputs/original-110.jar --released-110 build/migration-inputs/released-110.jar - --original-111 build/migration-inputs/original-111.jar --released-111 build/migration-inputs/released-111.jar --original-112 build/migration-inputs/original-112.jar --released-112 build/migration-inputs/released-112.jar --origins ${{ matrix.origin }} + --original-111 build/migration-inputs/original-111.jar --released-111 build/migration-inputs/released-111.jar --original-112 build/migration-inputs/original-112.jar --released-112 build/migration-inputs/released-112.jar --released-113 build/migration-inputs/released-113.jar --origins ${{ matrix.origin }} --seeds build/migration-seeds --variants ${{ matrix.stamps }} - --forge113-home build/native-server-distribution --output build/migration-results + --forge114-home build/native-server-distribution --output build/migration-results - name: Verify unsafe preservation stops world loading run: >- python3 tools/verify_upgrade_abort.py --java "$JAVA_HOME_8_X64/bin/java" @@ -77,7 +79,7 @@ jobs: done - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.13.2-upgrade-${{ matrix.origin }}-${{ matrix.stamps }}-${{ github.sha }} + name: LootableBodies-1.14.4-upgrade-${{ matrix.origin }}-${{ matrix.stamps }}-${{ github.sha }} if-no-files-found: error path: | build/migration-inputs/inputs.json @@ -89,9 +91,14 @@ jobs: if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.13.2-upgrade-failure-${{ matrix.origin }}-${{ matrix.stamps }}-${{ github.sha }} + name: LootableBodies-1.14.4-upgrade-failure-${{ matrix.origin }}-${{ matrix.stamps }}-${{ github.sha }} + include-hidden-files: true if-no-files-found: error path: | build/reports/** build/migration-*/**/*.log build/migration-*/**/crash-reports/** + build/migration-abort/**/comparison.json + build/native-server-distribution/**/*.log + .gradle/legacy-tools/released-112/build.log + .gradle/legacy-tools/released-113/build.log diff --git a/.github/workflows/release-workflow-checks.yml b/.github/workflows/release-workflow-checks.yml index d435641..fb40b1b 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.13.2'] + branches: ['feature/**', 'master-1.14.4'] pull_request: - branches: ['master-1.13.2'] + branches: ['master-1.14.4'] 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/deploy-release.yml .github/workflows/release-workflow-checks.yml .github/workflows/artifact-audit-1.13.yml .github/workflows/artifact-audit-1.14.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 2984ea1..f67a3fa 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,10 +1,9 @@ -2.4.4.113021 - Minecraft 1.13.2, Forge +2.4.5.114041 - Minecraft 1.14.4, Forge -Updated for Minecraft 1.13.2. +Updated for Minecraft 1.14.4. -Includes the inventory, corpse and config improvements from 1.12.2. -Fixed occasional stalls when joining a server. -Protects bodies and their contents when upgrading older 1.10.2, 1.11.2 -and 1.12.2 worlds, including chunks visited later. +Includes the inventory, corpse and config improvements from 1.13.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. diff --git a/README.md b/README.md index 5bc9d51..54a9137 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lootable Bodies for Minecraft 1.13.2 +# Lootable Bodies for Minecraft 1.14.4 Your items stay on a corpse when you die. Right-click the body to recover them. -Use Forge 25.0.223 and Java 8. Install `LootableBodies-2.4.4.113021.jar` +Use Forge 28.2.26 and Java 8. Install `LootableBodies-2.4.5.114041.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 and 1.12.2 corpses are preserved when upgrading, +Existing 1.10.2, 1.11.2, 1.12.2 and 1.13.2 corpses are preserved when upgrading, including bodies in chunks you visit later. Keep the whole world backup, including its `data` folder. If preservation cannot finish safely, world loading stops with an explanation in the log. Items from missing mods are retained for diff --git a/build.gradle b/build.gradle index ffb816e..f4c1a43 100644 --- a/build.gradle +++ b/build.gradle @@ -116,7 +116,7 @@ minecraft { providers.gradleProperty('gameRunDirectory').getOrElse('run')) systemProperty 'log4j.configurationFile', file('gradle/log4j2.xml').toURI().toString() } - register('client') { client=true; args '--assetsDir', minecraftAssets.asFile.absolutePath, '--assetIndex', '1.13.1' } + register('client') { client=true; args '--assetsDir', minecraftAssets.asFile.absolutePath, '--assetIndex', '1.14' } register('server') { client=false; args '--nogui' } } } @@ -129,11 +129,11 @@ minecraft.runs.configureEach { options -> 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('forge13-development/main').get().asFile.absolutePath + String root=layout.buildDirectory.dir('forge14-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.13.1') + environment.put('assetIndex','1.14') environment.put('nativesDirectory',new File(gradle.gradleUserHomeDir,"caches/minecraftforge/forgegradle/slime-launcher/cache/net/minecraftforge/forge/${minecraft_version}-${forge_version}/natives").absolutePath) } } @@ -146,12 +146,12 @@ tasks.withType(JavaExec).configureEach { run -> environment 'FORGE_VERSION', forge_version environment 'FORGE_GROUP', 'net.minecraftforge' environment 'MC_VERSION', minecraft_version - String root=layout.buildDirectory.dir('forge13-development/main').get().asFile.absolutePath + String root=layout.buildDirectory.dir('forge14-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.13.1' + environment 'assetIndex','1.14' environment 'nativesDirectory',new File(cache,"minecraftforge/forgegradle/slime-launcher/cache/net/minecraftforge/forge/${minecraft_version}-${forge_version}/natives").absolutePath } } @@ -170,7 +170,7 @@ tasks.register('prepareProductionRunDirectory') { } // Legacy FML discovers classes and processed resources from a single root. -ext.developmentOutput = layout.buildDirectory.dir('forge13-development/main') +ext.developmentOutput = layout.buildDirectory.dir('forge14-development/main') tasks.register('syncDevelopmentOutput', Sync) { dependsOn 'classes' from tasks.named('compileJava', JavaCompile).flatMap { it.destinationDirectory } diff --git a/gradle.properties b/gradle.properties index 2a34606..c93d868 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,10 +5,10 @@ org.gradle.caching=true org.gradle.parallel=false org.gradle.workers.max=2 net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.13.2 -forge_version=25.0.223 +minecraft_version=1.14.4 +forge_version=28.2.26 mapping_channel=snapshot -mapping_version=20180921-1.13 +mapping_version=20190719-1.14.3 # Release identity and dispatcher target metadata. loader_name=forge @@ -25,7 +25,7 @@ mod_vendor=Cyanobacterium mod_license=LGPL-2.1-only mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=2.4.4.113021 +mod_version=2.4.5.114041 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=20190213.203750 +mcp_version=20190829.143755 diff --git a/gradle/assets.gradle b/gradle/assets.gradle index 4e608b6..507e0b7 100644 --- a/gradle/assets.gradle +++ b/gradle/assets.gradle @@ -16,10 +16,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.13.2.json') - download('https://piston-meta.mojang.com/v1/packages/fa3ddc22146c46bfeb0e9d322c6f83b937e25005/1.13.2.json','fa3ddc22146c46bfeb0e9d322c6f83b937e25005',descriptor) + File descriptor=new File(root,'1.14.4.json') + download('https://piston-meta.mojang.com/v1/packages/be146d5f66a3627ed0a87c234c4d8dde8ab35098/1.14.4.json','be146d5f66a3627ed0a87c234c4d8dde8ab35098',descriptor) def version=new groovy.json.JsonSlurper().parse(descriptor) - File index=new File(root,'indexes/1.13.1.json') + File index=new File(root,'indexes/1.14.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 14b81f5..f2bea4b 100644 --- a/gradle/audit.gradle +++ b/gradle/audit.gradle @@ -60,7 +60,7 @@ 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.13.2]"'].each { + ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.14.4]"'].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!=4) @@ -72,7 +72,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_70014_b')) + if (!entity.contains('func_70037_a') || !entity.contains('func_213281_b')) 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/historical-writers.init.gradle b/gradle/historical-writers.init.gradle index 526906c..e1cfb18 100644 --- a/gradle/historical-writers.init.gradle +++ b/gradle/historical-writers.init.gradle @@ -1,4 +1,4 @@ -// Compile fixture writers against the pinned historical API, not native 1.13. +// Compile fixture writers against the pinned historical API, not native 1.14. gradle.projectsEvaluated { def p=gradle.rootProject File writerRoot=p.file(p.property('historicalWriterSource')) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 64ada9d..d8034c2 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -65,14 +65,14 @@ tasks.register('isolateEclipseProductionRuns') { 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.13.1"') + contents=contents.replace('value="{asset_index}"','value="1.14"') .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}\"") 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.13.2"') + contents = contents.replace('key="MC_VERSION" value="${MC_VERSION}"', 'key="MC_VERSION" value="1.14.4"') contents = contents.replaceAll(//, '') contents = contents.replace('', ' \n') @@ -126,7 +126,7 @@ 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.13.2"')) + if (!xml.contains('key="MC_VERSION" value="1.14.4"')) 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() @@ -140,7 +140,7 @@ tasks.register('verifyEclipseProductionClasspath') { if (file(directory).canonicalFile != file(providers.gradleProperty('gameRunDirectory').getOrElse('run')).canonicalFile) throw new GradleException("${name} must use the production run directory, found ${directory}") if (name == 'runServer.launch' && !xml.contains('--nogui')) - throw new GradleException('Forge 25 dedicated server needs --nogui') + throw new GradleException('Forge 28 dedicated server needs --nogui') } } } diff --git a/gradle/native113-writer.init.gradle b/gradle/native113-writer.init.gradle new file mode 100644 index 0000000..1a8c5a1 --- /dev/null +++ b/gradle/native113-writer.init.gradle @@ -0,0 +1,26 @@ +// Build a separate fixture writer against the immutable released 1.13 API. +gradle.projectsEvaluated { + def p = gradle.rootProject + File writerRoot = p.file(p.property('historicalWriterSource')) + def classes = p.layout.buildDirectory.dir('native113-seed/classes') + def compile = p.tasks.register('compileNative113Seed', JavaCompile) { + dependsOn 'classes' + source p.fileTree(new File(writerRoot, 'src/nativeSeed113/java')) + classpath = p.files(p.sourceSets.main.output, p.sourceSets.main.compileClasspath) + destinationDirectory = classes + options.compilerArgs.add('-proc:none') + } + def jar = p.tasks.register('native113SeedJar', Jar) { + dependsOn compile + archiveFileName = 'lootablebodies-native113-world-fixtures.jar' + destinationDirectory = p.layout.buildDirectory.dir('native113-seed') + from classes + from new File(writerRoot, 'src/nativeSeed113/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('native113-seed/lootablebodies-native113-world-fixtures-reobf.jar') + } + p.tasks.register('prepareNative113WorldFixtures') { dependsOn packaged } +} diff --git a/gradle/release.gradle b/gradle/release.gradle index 8e28f2c..707dbda 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,8 +5,8 @@ def expectedIdentity = [ mod_id: 'lootablebodies', mod_artifact: 'LootableBodies', mod_group: 'zone.moddev.mc.lootablebodies', mod_vendor: 'Cyanobacterium', mod_name: "DrCyano's Lootable Bodies", mod_authors: 'Cyanobacterium,SkyBlade1978', - minecraft_version: '1.13.2', forge_version: '25.0.223', - mapping_channel: 'snapshot', mapping_version: '20180921-1.13', + minecraft_version: '1.14.4', forge_version: '28.2.26', + mapping_channel: 'snapshot', mapping_version: '20190719-1.14.3', loader_name: 'forge', loader_code: '1', curseforge_project_id: '240968', java_version: '8', java_toolchain_version: '8.0.502+7', gradle_java_version: '17', mod_license: 'LGPL-2.1-only', mod_license_name: 'GNU Lesser General Public License, Version 2.1', diff --git a/gradle/runtime.gradle b/gradle/runtime.gradle index b793a2e..eb27959 100644 --- a/gradle/runtime.gradle +++ b/gradle/runtime.gradle @@ -17,6 +17,14 @@ def probeJar = tasks.register('integrationTestModJar', Jar) { destinationDirectory = layout.buildDirectory.dir('integration-test-mod') from probeClasses from 'src/integrationTest/resources' + // Native 1.14 stitches every registered effect icon, including the isolated + // unsigned-ID fixtures. Keep their transparent placeholders inside the probe. + (0..<200).each { id -> + from('src/integrationTest/resources/assets/lootablebodiesbuildtest/textures/mob_effect/extended_effect.png') { + into 'assets/lootablebodiesbuildtest/textures/mob_effect' + rename { "padding_${id}.png" } + } + } } def packagedProbe = renamer.classes(probeJar) { map.from minecraft.dependency.toSrgFile @@ -95,6 +103,8 @@ def phaseTask = { String phase, Object prerequisite -> File previousDir = options.workingDir.get().asFile def previousProperties = new LinkedHashMap(run.systemProperties) def previousClasspath = files(run.classpath) + def previousArgs = new ArrayList(run.args) + def previousMain = run.mainClass.orNull try { options.workingDir.set(runDirectory) run.workingDir(runDirectory) @@ -108,6 +118,8 @@ def phaseTask = { String phase, Object prerequisite -> run.workingDir(previousDir) run.setSystemProperties(previousProperties) run.setClasspath(previousClasspath) + run.setArgs(previousArgs) + run.mainClass.set(previousMain) } File marker = new File(runDirectory.get().asFile,"world/${phase}.pass") if (!marker.isFile()) throw new GradleException("Missing ${phase} corpse preservation marker") @@ -170,7 +182,7 @@ tasks.register('clientIntegrationTest') { if(root.exists()) throw new GradleException('Preserve the previous client evidence before rerunning') new File(root,'mods').mkdirs() new File(root,'config').mkdirs() - new File(root,'options.txt').text='realmsNotifications:false\n' + new File(root,'options.txt').text='realmsNotifications:false\nsoundCategory_master:0.0\n' copy { from probeJar; into new File(root,'mods') } new File(root,'config/lootablebodies.cfg').text = tasks.getByName('runClient').systemProperties['lootablebodies.configUiCheck']=='true' ? '' : 'options {\n B:local_texture=true\n}\n' if(providers.gradleProperty('clientIntegrationConfiguration').isPresent()) { @@ -239,7 +251,7 @@ minecraft.runs.register('platformControl') { client=false args '--nogui' workingDir=layout.buildDirectory.dir('forge-control-run') - String root=layout.buildDirectory.dir('forge13-development/control').get().asFile.absolutePath + String root=layout.buildDirectory.dir('forge14-development/control').get().asFile.absolutePath environment.put('MOD_CLASSES',"forgeplatformcontrol%%${root}${File.pathSeparator}forgeplatformcontrol%%${root}") } tasks.register('forgeControlServer') { @@ -254,7 +266,7 @@ tasks.register('forgeControlServer') { history.parentFile.mkdirs();if(!root.renameTo(history))throw new GradleException('Cannot preserve previous control evidence') } new File(root,'mods').mkdirs() - File controlOutput=layout.buildDirectory.dir('forge13-development/control').get().asFile + File controlOutput=layout.buildDirectory.dir('forge14-development/control').get().asFile 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' diff --git a/src/controlTest/java/buildcontrol/ForgeControl.java b/src/controlTest/java/buildcontrol/ForgeControl.java index 6eedd01..d872872 100644 --- a/src/controlTest/java/buildcontrol/ForgeControl.java +++ b/src/controlTest/java/buildcontrol/ForgeControl.java @@ -2,7 +2,7 @@ import net.minecraftforge.fml.common.Mod; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.event.server.FMLServerStartedEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; /** Independent native Forge control, without a Lootable Bodies dependency. */ @Mod("forgeplatformcontrol") public final class ForgeControl { @@ -14,16 +14,38 @@ public ForgeControl() { 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();} + else {System.out.println("FORGE_PLATFORM_CONTROL_PASS");event.getServer().initiateShutdown(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"); + if(p==null || !p.isAlive())return; + if(++checkTicks==1) { + p.setGameType(net.minecraft.world.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.setSpawnPoint(new net.minecraft.util.math.BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),true); + p.connection.setPlayerLocation(x,81,z,0,0); + } + if(checkTicks==50){ + int px=(int)Math.floor(p.posX),pz=(int)Math.floor(p.posZ); + 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.onKillCommand();checkTicks=0;System.out.println("NATIVE_DEATH "+(++checkDeaths));} + } public static final class ClientControl { private int stage,ticks; @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) { 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.GuiMainMenu) { + if(stage==0 && mc.currentScreen instanceof net.minecraft.client.gui.screen.MainMenuScreen) { mc.gameSettings.pauseOnLostFocus=false; if(Boolean.getBoolean("lootablebodies.control.world")) { mc.launchIntegratedServer("platform-fixture","Platform fixture",new net.minecraft.world.WorldSettings(4267321,net.minecraft.world.GameType.CREATIVE,false,false,net.minecraft.world.WorldType.FLAT));stage=1; @@ -34,18 +56,42 @@ public static final class ClientControl { }catch(java.io.IOException failure){throw new IllegalStateException(failure);} stage=4; }else if(stage==4 && ++ticks>=80) { - mc.world.sendQuittingDisconnectingPacket();mc.loadWorld(null);stage=2; + mc.world.sendQuittingDisconnectingPacket();mc.func_213254_o();stage=2; }else if(stage==2 && mc.world==null && !mc.isIntegratedServerRunning())finish(mc); } - private int connections,wait; + private int connections,wait,deathTicks,respawns; + private volatile boolean hold; + private boolean installed; + private io.netty.channel.ChannelHandlerContext delayContext; + private final java.util.List delayed=new java.util.ArrayList<>(); + 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(){ + @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;} + super.write(ctx,message,promise); + } + }); + } + boolean next=mc.currentScreen instanceof net.minecraft.client.gui.screen.DeathScreen && deathTicks<40; + boolean release=hold && !next;hold=next; + if(release)delayContext.executor().execute(()->{ + if(hold)return; + System.out.println("NATIVE_FIFO_DELAY_RELEASE packets="+delayed.size()); + for(Object[] pending:delayed)delayContext.write(pending[0],(io.netty.channel.ChannelPromise)pending[1]); + delayed.clear();delayContext.flush(); + }); + } private net.minecraft.client.network.ServerPinger pinger; private net.minecraft.client.multiplayer.ServerData destination; private void tcp(net.minecraft.client.Minecraft mc) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.limitFramerate=30; + mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.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.GuiConnecting) { + if(stage==1 && wait%40==0 && mc.currentScreen instanceof net.minecraft.client.gui.screen.ConnectingScreen) { try { - for(java.lang.reflect.Field field:net.minecraft.client.gui.GuiConnecting.class.getDeclaredFields()) + 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); if(manager!=null && manager.channel()!=null) { @@ -54,22 +100,30 @@ private void tcp(net.minecraft.client.Minecraft mc) { } }catch(ReflectiveOperationException failure){throw new IllegalStateException(failure);} } - if(stage==0 && mc.currentScreen instanceof net.minecraft.client.gui.GuiMainMenu) { + if(stage==0 && mc.currentScreen instanceof net.minecraft.client.gui.screen.MainMenuScreen) { 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);} stage=5; - }else {mc.displayGuiScreen(new net.minecraft.client.gui.GuiConnecting(new net.minecraft.client.gui.GuiMainMenu(),mc,destination));stage=1;} + }else {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==5) { pinger.pingPendingNetworks(); - if(destination.pingToServer>=0){pinger.clearPendingNetworks();mc.displayGuiScreen(new net.minecraft.client.gui.GuiConnecting(new net.minecraft.client.gui.GuiMainMenu(),mc,destination));stage=1;wait=0;} + 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) { + 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));} + }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.loadWorld(null);mc.displayGuiScreen(new net.minecraft.client.gui.GuiMainMenu());stage=0;wait=0; - }else if(stage==1 && mc.currentScreen instanceof net.minecraft.client.gui.GuiDisconnected) { + mc.world.sendQuittingDisconnectingPacket();mc.func_213254_o();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) { throw new AssertionError("NATIVE_FORGE_TCP_LOGIN_FAILURE joins="+connections); } } diff --git a/src/controlTest/resources/META-INF/mods.toml b/src/controlTest/resources/META-INF/mods.toml index 672afbb..1a57f4d 100644 --- a/src/controlTest/resources/META-INF/mods.toml +++ b/src/controlTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[25,)" +loaderVersion="[28,)" license="LGPL-2.1-only" [[mods]] modId="forgeplatformcontrol" diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java index 3f1a7c5..c955b85 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java @@ -1,19 +1,23 @@ 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 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.GuiMainMenu; +import net.minecraft.client.gui.screen.MainMenuScreen; import net.minecraft.entity.Entity; -import net.minecraft.util.EnumHand; +import net.minecraft.util.Hand; import net.minecraft.util.ScreenShotHelper; import net.minecraft.world.*; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; /** Automated real-client world, packet interaction, GUI render and reload check. */ public final class ClientChecks { @@ -58,15 +62,15 @@ public void register() { 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 GuiMainMenu) { + if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { mc.gameSettings.pauseOnLostFocus=false; - mc.gameSettings.limitFramerate=30; + mc.gameSettings.framerateLimit=30; mc.gameSettings.renderDistanceChunks=4; stage=1; mc.launchIntegratedServer("build-fixture", "Build fixture",new WorldSettings(4267321,GameType.CREATIVE,false,false,WorldType.FLAT)); } else if(stage==1 && mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null) { stage=2; - pending=mc.getIntegratedServer().addScheduledTask(() -> { + pending=mc.getIntegratedServer().runAsync(() -> { try { RuntimeChecks.run(mc.getIntegratedServer(),"fresh"); } catch(Exception error) { throw new RuntimeException(error); } positionViewer(mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID())); @@ -74,12 +78,12 @@ public void register() { } else if(stage==2 && pending.isDone()) { pending.get(); NamelessCorpseClientChecks.englishContract(mc); - for(Entity entity : mc.world.loadedEntityList) if(entity instanceof EntityLootableBody && entity.getUniqueID().equals(RuntimeChecks.NEAR)) { - mc.playerController.interactWithEntity(mc.player,entity,EnumHand.MAIN_HAND); + 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); stage=3; break; } } else if(stage==3 && mc.currentScreen instanceof CorpseGUIContainer && ++frames>20) { - RuntimeChecks.require(((CorpseGUIContainer)mc.currentScreen).inventorySlots.inventorySlots.size()==84,"rendered GUI slots"); + RuntimeChecks.require(((CorpseGUIContainer)mc.currentScreen).getContainer().inventorySlots.size()==84,"rendered GUI slots"); ScreenShotHelper.saveScreenshot(mc.gameDir,"corpse-gui-fresh.png",mc.mainWindow.getFramebufferWidth(),mc.mainWindow.getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString())); mc.player.closeScreen(); stage=4; FixtureConnections.disconnect(mc); @@ -87,15 +91,15 @@ public void register() { stage=5; mc.launchIntegratedServer("build-fixture","Build fixture",null); } else if(stage==5 && mc.player!=null && mc.world!=null && mc.getIntegratedServer()!=null) { stage=6; - pending=mc.getIntegratedServer().addScheduledTask(() -> { + pending=mc.getIntegratedServer().runAsync(() -> { try { RuntimeChecks.run(mc.getIntegratedServer(),"deferred"); } catch(Exception error) { throw new RuntimeException(error); } positionViewer(mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID())); }); } else if(stage==6 && pending.isDone()) { pending.get(); - for(Entity entity:mc.world.loadedEntityList) if(entity instanceof EntityLootableBody && entity.getUniqueID().equals(RuntimeChecks.NEAR)) { - mc.playerController.interactWithEntity(mc.player,entity,EnumHand.MAIN_HAND); + 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); stage=7; frames=0; break; } } else if(stage==7 && mc.currentScreen instanceof CorpseGUIContainer && ++frames>20) { @@ -116,12 +120,12 @@ public void register() { } } catch(Throwable error) { fail(error); } } - private static void positionViewer(net.minecraft.entity.player.EntityPlayerMP player) { - WorldServer world=player.getServerWorld(); + private static void positionViewer(net.minecraft.entity.player.ServerPlayerEntity player) { + ServerWorld world=player.getServerWorld(); 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.init.Blocks.STONE.getDefaultState()); - RuntimeChecks.require(world.getBlockState(new net.minecraft.util.math.BlockPos(8,79,7)).getBlock()==net.minecraft.init.Blocks.STONE, + 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, "Graphical recovery needs a stable floor below the viewing player"); player.setGameType(GameType.CREATIVE); player.abilities.allowFlying=true;player.abilities.isFlying=true;player.sendPlayerAbilities(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java index 8b22c3c..e563122 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java @@ -1,4 +1,5 @@ package cyano.lootable.buildtest; +import net.minecraft.client.gui.screen.MainMenuScreen; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseConfigGui; @@ -8,6 +9,9 @@ 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.GuiModList; @@ -17,8 +21,8 @@ final class ConfigClientChecks { 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 GuiMainMenu) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.limitFramerate=30;mc.gameSettings.renderDistanceChunks=4; + if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { + mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=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; @@ -50,9 +54,9 @@ void tick(Minecraft mc)throws Exception { check(LootableBodies.corpseHP==20 && LootableBodies.corpseDecayTime==72000 && LootableBodies.ticksPerItemDecay==6000,"gameplay_unchanged_until_restart"); mc.displayGuiScreen(null);mc.launchIntegratedServer("config-fixture","Config fixture",new WorldSettings(4267321,GameType.CREATIVE,false,false,WorldType.FLAT));stage=3; } else if(stage==3 && mc.world!=null && mc.player!=null) { - pending=mc.getIntegratedServer().addScheduledTask(()->{EntityLootableBody body=RuntimeChecks.fixture(mc.getIntegratedServer().getWorld(net.minecraft.world.dimension.DimensionType.OVERWORLD),UUID.fromString("00000000-0000-4000-8000-000000000941"),8,true);body.setCustomNameVisible(false);try{RuntimeChecks.saveExpected(mc.getIntegratedServer().getWorld(net.minecraft.world.dimension.DimensionType.OVERWORLD).getSaveHandler().getWorldDirectory(),"config-body",body);}catch(Exception failure){throw new RuntimeException(failure);}});stage=4; + pending=mc.getIntegratedServer().runAsync(()->{EntityLootableBody body=RuntimeChecks.fixture(mc.getIntegratedServer().getWorld(net.minecraft.world.dimension.DimensionType.OVERWORLD),UUID.fromString("00000000-0000-4000-8000-000000000941"),8,true);body.setCustomNameVisible(false);try{RuntimeChecks.saveExpected(mc.getIntegratedServer().getWorld(net.minecraft.world.dimension.DimensionType.OVERWORLD).getSaveHandler().getWorldDirectory(),"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).enabled,"world_rules_disabled_while_world_open");stage=6; + 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); check(!LootableBodies.useLocalSkin && LootableBodies.displayNameTag,"appearance_changes_while_world_open"); @@ -60,16 +64,16 @@ void tick(Minecraft mc)throws Exception { } else if(stage==5 && FixtureConnections.stopped(mc))finish(mc); } private void openMods(Minecraft mc)throws Exception { - mods=new GuiModList(new GuiMainMenu());mc.displayGuiScreen(mods);List list=(List)get(mods,"mods"); - for(int i=0;i list=(List)get(mods,"mods"); + for(int i=0;i)entries.children()).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,0,0,0F));check(button(mods,20).active,"mods_config_button_enabled");return;} throw new AssertionError("Lootable Bodies missing from Mods list"); } private void openConfig(Minecraft mc)throws Exception {net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.GuiScreenEvent.DrawScreenEvent.Pre(mods,0,0,0F));click(mods,20);check(mc.currentScreen instanceof CorpseConfigGui,"mods_button_opens_regular_config");root=(CorpseConfigGui)mc.currentScreen;} private static Object get(Object target,String name)throws Exception {for(Class type=target.getClass();type!=null;type=type.getSuperclass())try{Field f=type.getDeclaredField(name);f.setAccessible(true);return f.get(target);}catch(NoSuchFieldException absent){}throw new NoSuchFieldException(name);} - private static GuiButton button(GuiScreen screen,int id)throws Exception {for(Object value:screen.getChildren())if(value instanceof GuiButton && ((GuiButton)value).id==id)return (GuiButton)value;throw new AssertionError("Missing GUI button "+id);} - private static void click(GuiScreen screen,int id)throws Exception {GuiButton button=button(screen,id);RuntimeChecks.require(button.enabled,"Disabled GUI button "+id);button.onClick(button.x+1,button.y+1);} - @SuppressWarnings("unchecked") private static GuiTextField field(CorpseConfigGui screen,String key)throws Exception { - for(int attempt=0;attempt<10;attempt++){GuiTextField 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;} + private static Button button(Screen screen,int id)throws Exception {if(screen instanceof GuiModList && 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;} 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);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java index bc91c55..3979d63 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java @@ -1,4 +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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -6,16 +11,16 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGameOver; -import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.screen.DeathScreen; +import net.minecraft.client.gui.screen.MainMenuScreen; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; -import net.minecraft.inventory.ClickType; +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.server.MinecraftServer; -import net.minecraft.util.EnumHand; +import net.minecraft.util.Hand; import net.minecraft.util.ScreenShotHelper; import net.minecraft.util.math.BlockPos; import net.minecraft.world.GameType; @@ -26,7 +31,7 @@ private interface Action { void run() throws Exception; } private int stage, frames; private Future pending; private UUID playerId, corpseId; - private NBTTagCompound visibleExpected; + private CompoundNBT visibleExpected; private File report; void start(Minecraft mc) { @@ -38,12 +43,12 @@ boolean tick(Minecraft mc) throws Exception { frames++; if(stage==0) { schedule(mc,() -> prepareDeath(mc)); next(1); - } else if(stage==1 && done() && mc.currentScreen instanceof GuiGameOver && frames>20) { + } else if(stage==1 && done() && mc.currentScreen 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 GuiGameOver)) { + } else if(stage==2 && mc.player!=null && mc.player.isAlive() && !(mc.currentScreen instanceof DeathScreen)) { schedule(mc,() -> { - EntityPlayerMP player=player(mc); EntityLootableBody corpse=corpse(mc); + ServerPlayerEntity player=player(mc); EntityLootableBody corpse=corpse(mc); RuntimeChecks.require(player.isAlive(),"Connected respawn failed"); player.connection.setPlayerLocation(24.5,80,7.5,0,0); RuntimeChecks.saveExpected(folder(mc),"connected-death-before",corpse); @@ -117,35 +122,35 @@ boolean tick(Minecraft mc) throws Exception { return false; } private void prepareDeath(Minecraft mc) throws Exception { - MinecraftServer server=mc.getIntegratedServer(); EntityPlayerMP player=player(mc); + MinecraftServer server=mc.getIntegratedServer(); ServerPlayerEntity 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().setOrCreateGameRule("keepInventory","false",null); - player.world.getGameRules().setOrCreateGameRule("doMobSpawning","false",null); + 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.inventoryContainer.detectAndSendChanges(); player.onKillCommand(); + player.container.detectAndSendChanges(); player.onKillCommand(); RuntimeChecks.require(!player.isAlive(),"Connected player did not die"); EntityLootableBody corpse=null; - for(Entity entity:player.world.loadedEntityList) + for(Entity entity:FixtureApi.entities(player.world)) 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.motionX=0;corpse.motionY=0;corpse.motionZ=0; + 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); 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 NBTTagList()).size()==0,"Death retained player inventory alongside corpse"); + RuntimeChecks.require(player.inventory.write(new ListNBT()).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); - NBTTagCompound expected=read(folder(mc),before),actual=new NBTTagCompound(); - NBTTagList equipment=expected.getList("Equipment",10); - for(int i=equipment.size()-1;i>=0;i--)if(equipment.getCompound(i).getByte("Slot")==slot)equipment.removeTag(i); + CompoundNBT expected=read(folder(mc),before),actual=new CompoundNBT(); + ListNBT 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); RuntimeChecks.saveExpected(folder(mc),after,corpse);visibleExpected=actual.copy(); } @@ -153,23 +158,23 @@ private void verifyReload(Minecraft mc,String corpseName,String playerName) thro mc.getIntegratedServer().getWorld(net.minecraft.world.dimension.DimensionType.getById(0)).getChunk(1,0); EntityLootableBody corpse=corpse(mc);RuntimeChecks.require(corpse.isAlive(),"Saved corpse is no longer accessible"); corpse.baseTick();RuntimeChecks.verifyExpected(folder(mc),corpseName,corpse); - NBTTagCompound expected=read(folder(mc),playerName); - RuntimeChecks.require(expected.getTag("Inventory").equals(player(mc).inventory.write(new NBTTagList())),"Recovered player inventory changed on disk reload"); + 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); visibleExpected=read(folder(mc),corpseName); } private void savePlayer(Minecraft mc,String name) throws Exception { - NBTTagCompound expected=new NBTTagCompound();expected.setTag("Inventory",player(mc).inventory.write(new NBTTagList())); + 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().saveAllWorlds(false); + mc.getIntegratedServer().getPlayerList().saveAllPlayerData();mc.getIntegratedServer().save(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];NBTTagList 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.getIntegratedServer().runAsync(() -> {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 boolean open(Minecraft mc){ - for(Entity entity:mc.world.loadedEntityList)if(entity instanceof EntityLootableBody && entity.getUniqueID().equals(corpseId)){ - mc.playerController.interactWithEntity(mc.player,entity,EnumHand.MAIN_HAND);return true; + 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; } return false; } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java index ad33197..6fc62a8 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java @@ -1,4 +1,16 @@ 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 com.mojang.authlib.GameProfile; import cyano.lootable.entities.EntityLootableBody; @@ -7,9 +19,9 @@ import java.io.*; import java.util.*; import net.minecraft.entity.Entity; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; +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.*; @@ -27,12 +39,12 @@ final class ContainerInteractionChecks { private interface Check { void run() throws Exception; } private static MinecraftServer server; - private static WorldServer world; + private static ServerWorld world; private static int sequence, failures; private static short transaction; private static final List results=new ArrayList<>(); - static void run(MinecraftServer instance,WorldServer target,File folder) throws Exception { + static void run(MinecraftServer instance,ServerWorld 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); @@ -68,7 +80,7 @@ static void run(MinecraftServer instance,WorldServer target,File folder) throws System.out.println("LOOTABLE_BODIES_CONTAINER_PASS checks="+results.size()); } - static void runDrops(MinecraftServer instance,WorldServer target,File folder) throws Exception { + static void runDrops(MinecraftServer instance,ServerWorld 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++) { @@ -105,23 +117,23 @@ static void runDrops(MinecraftServer instance,WorldServer 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();EntityPlayerMP p=player();open(p,b);b.setInventorySlotContents(4,item(4,1));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);b.setInventorySlotContents(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"); } private static void interruptDrag(boolean outside) throws Exception { - EntityLootableBody b=body();EntityPlayerMP 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();ServerPlayerEntity p=player();open(p,b);b.setInventorySlotContents(6,item(120,17));p.inventory.setItemStack(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); } private static void throwing(boolean creative,int source,int button,int rejection,boolean capture) throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL); + EntityLootableBody b=body();ServerPlayerEntity p=player();p.setGameType(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(capture){p.captureDrops(new java.util.ArrayList<>());p.captureDrops().add(new EntityItem(world,96.5,80,96.5,item(121,11)));} + 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; MinecraftForge.EVENT_BUS.register(reject); @@ -134,12 +146,12 @@ private static void throwing(boolean creative,int source,int button,int rejectio if(rejection!=0)RuntimeChecks.require(connection.resyncs>initialSyncs,"Rejected predicted drop did not resync initiating client"); if(capture)RuntimeChecks.require(p.captureDrops().size()==2,"Toss discarded pre-existing captured item"); if(rejection==0 && !capture){ - int dropped=0;for(Entity e:world.loadedEntityList)if(e instanceof EntityItem && !e.removed){equal(((EntityItem)e).getItem(),item(120,amount),"Accepted drop NBT differs");dropped++;} + 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++;} 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();EntityPlayerMP p=player();fill(b);fill(p);p.setGameType(GameType.CREATIVE);open(p,b); + 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)); Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=toss;reject.join=!toss;MinecraftForge.EVENT_BUS.register(reject); @@ -147,7 +159,7 @@ private static void creativeFallback(boolean toss,boolean swap) throws Exception 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();EntityPlayerMP p=player(),q=player();open(p,b);open(q,b); + 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)); @@ -162,41 +174,41 @@ private static void collect(int mode) throws Exception { conserved(before,b,p,q); } private static void fallbackOrder(boolean offhand) throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();open(p,b); + 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); 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); } private static void cloneStack(boolean creative,boolean occupied) throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setInventorySlotContents(6,item(120,17)); + 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); 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); } private static void creativeDrag(boolean creative) throws Exception { - EntityLootableBody b=body();EntityPlayerMP 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();ServerPlayerEntity p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);p.inventory.setItemStack(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); } private static void creativePacket(boolean creative,boolean cancelled) throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();p.setGameType(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setInventorySlotContents(6,item(120,17));NBTTagCompound unchanged=data(b); + 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));CPacketCreativeInventoryAction packet=new CPacketCreativeInventoryAction();packet.readPacketData(wire);p.connection.processCreativeInventoryAction(packet);} + try{wire.writeShort(cancelled?-1:9);wire.writeItemStack(item(121,23));CCreativeInventoryActionPacket packet=new CCreativeInventoryActionPacket();packet.readPacketData(wire);p.connection.processCreativeInventoryAction(packet);} 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(world.loadedEntityList.stream().noneMatch(e->e instanceof EntityItem && !e.removed),"Cancelled creative minted drop spawned anyway"); + if(cancelled)RuntimeChecks.require(FixtureApi.entities(world).stream().noneMatch(e->e instanceof ItemEntity && !e.removed),"Cancelled creative minted drop spawned anyway"); } private static void check(String name,Check test) { - Set before=new HashSet<>(world.loadedEntityList); + Set before=new HashSet<>(FixtureApi.entities(world)); try {test.run();results.add("PASS "+name);System.out.println("CONTAINER_CHECK_PASS "+name);} catch(Throwable error){failures++;results.add("FAIL "+name+": "+error);System.out.println("CONTAINER_CHECK_FAIL "+name+": "+error);error.printStackTrace();} - finally{for(Entity entity:new ArrayList(world.loadedEntityList))if(!before.contains(entity))world.removeEntityDangerously(entity);} + finally{for(Entity entity:new ArrayList(FixtureApi.entities(world)))if(!before.contains(entity))world.removeEntity(entity);} } - private static EntityPlayerMP player() { - EntityPlayerMP p=new EntityPlayerMP(server,world, + 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); @@ -204,62 +216,62 @@ private static EntityPlayerMP player() { } 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.spawnEntity(b),"Container corpse spawn");return b; + world.getChunk(6,6);RuntimeChecks.require(world.addEntity(b),"Container corpse spawn");return b; } - private static CorpseContainer open(EntityPlayerMP p,EntityLootableBody b){ - CorpseContainer c=new CorpseContainer(p.inventory,b);c.windowId=7;p.openContainer=c;c.addListener(p);return c; + 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 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(EntityPlayerMP p){for(int i=0;i<36;i++)p.inventory.mainInventory.set(i,item(100+i,64));} + 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(EntityPlayerMP p,int slot,int button,ClickType type,ItemStack predicted) throws Exception { + 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); - CPacketClickWindow packet=new CPacketClickWindow();packet.readPacketData(wire);p.connection.processClickWindow(packet); + CClickWindowPacket packet=new CClickWindowPacket();packet.readPacketData(wire);p.connection.processClickWindow(packet); } finally{wire.release();} } - private static void click(EntityPlayerMP p,int slot,int button,ClickType type) throws Exception { + private static void click(ServerPlayerEntity 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(); } packet(p,slot,button,type,predicted); } - private static void close(EntityPlayerMP p) throws Exception { + private static void close(ServerPlayerEntity p) throws Exception { PacketBuffer wire=new PacketBuffer(Unpooled.buffer()); - try{wire.writeByte(p.openContainer.windowId);CPacketCloseWindow packet=new CPacketCloseWindow();packet.readPacketData(wire);p.connection.processCloseWindow(packet);} + try{wire.writeByte(p.openContainer.windowId);CCloseWindowPacket packet=new CCloseWindowPacket();packet.readPacketData(wire);p.connection.processCloseWindow(packet);} finally{wire.release();} } - private static void drag(EntityPlayerMP p,int mode,int... slots) throws Exception { + private static void drag(ServerPlayerEntity 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 NBTTagCompound data(EntityLootableBody b){NBTTagCompound n=new NBTTagCompound();b.writeAdditional(n);return n;} + private static CompoundNBT data(EntityLootableBody b){CompoundNBT n=new CompoundNBT();b.writeAdditional(n);return n;} private static void add(Map all,ItemStack s){ - if((s == null || s.isEmpty()) || s.getCount()<=0)return;NBTTagCompound n=s.write(new NBTTagCompound());n.removeTag("Count"); + if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundNBT n=s.write(new CompoundNBT());n.remove("Count"); String key=n.toString();all.put(key,all.getOrDefault(key,0)+s.getCount()); } - private static Map total(EntityLootableBody b,EntityPlayerMP... players){ + private static Map total(EntityLootableBody b,ServerPlayerEntity... players){ Map all=new TreeMap<>(); - for(int i=0;i<48;i++)add(all,b.getStackInSlot(i));NBTTagList aux=data(b).getList("Aux",10); + 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:world.loadedEntityList)if(e instanceof EntityItem && !e.removed)add(all,((EntityItem)e).getItem()); + for(ServerPlayerEntity p:players){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()); return all; } - private static void conserved(Map expected,EntityLootableBody b,EntityPlayerMP... players){ + private static void conserved(Map expected,EntityLootableBody b,ServerPlayerEntity... players){ RuntimeChecks.require(expected.equals(total(b,players)),"Item count/type/damage/custom NBT lost or duplicated"); - NBTTagCompound before=data(b),immutable=before.copy();EntityLootableBody copy=new EntityLootableBody(world);copy.readAdditional(before); - RuntimeChecks.require(Objects.equals(before.getTag("Equipment"),data(copy).getTag("Equipment")) && Objects.equals(before.getTag("Aux"),data(copy).getTag("Aux")),"Post-interaction NBT round trip changed inventory"); + CompoundNBT before=data(b),immutable=before.copy();EntityLootableBody copy=new EntityLootableBody(world);copy.readAdditional(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();EntityPlayerMP p=player();open(p,b); + EntityLootableBody b=body();ServerPlayerEntity 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"); @@ -270,78 +282,78 @@ private static void hotbar() throws Exception { } } private static void armorSwap() throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();open(p,b);p.inventory.mainInventory.set(0,item(4,1)); + 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); } private static void wearableShift() throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();p.inventory.mainInventory.set(9,pumpkins(64));open(p,b); + EntityLootableBody b=body();ServerPlayerEntity p=player();p.inventory.mainInventory.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"); conserved(before,b,p); } private static void dragDistribution(int mode) throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();open(p,b);p.inventory.setItemStack(item(19,10));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(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); } 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)); - EntityPlayerMP p=player();open(p,b);p.inventory.setItemStack(item(19,12));Map before=total(b,p); + 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); } private static void dragArmor() throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();open(p,b);p.inventory.setItemStack(pumpkins(64));Map before=total(b,p); + 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); } private static void dragCancel() throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();open(p,b);p.inventory.setItemStack(item(19,10));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(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); } private static void fullPlayer() throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));EntityPlayerMP p=player();fill(p);open(p,b); + 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); } private static void partialPlayer() throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));EntityPlayerMP p=player();fill(p);p.inventory.mainInventory.set(35,item(19,63));open(p,b); + 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); } private static void fullCorpse() throws Exception { - EntityLootableBody b=body();fill(b);EntityPlayerMP p=player();p.inventory.mainInventory.set(9,item(110,23));open(p,b); - NBTTagCompound unchanged=data(b);Map before=total(b,p);packet(p,48,0,ClickType.QUICK_MOVE,null);b.baseTick(); - RuntimeChecks.require(unchanged.getTag("Equipment").equals(data(b).getTag("Equipment")) && unchanged.getTag("Aux").equals(data(b).getTag("Aux")),"Full corpse overwrote indexed/Aux contents");conserved(before,b,p); + 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(); + 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 { boolean join,toss; - @SubscribeEvent public void join(EntityJoinWorldEvent e){if(join && e.getEntity() instanceof EntityItem)e.setCanceled(true);} + @SubscribeEvent public void join(EntityJoinWorldEvent e){if(join && e.getEntity() instanceof ItemEntity)e.setCanceled(true);} @SubscribeEvent public void toss(ItemTossEvent e){if(toss)e.setCanceled(true);} } private static void displacement(boolean rejectJoin,boolean rejectToss) throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(0,item(0,1));EntityPlayerMP p=player();fill(p);p.inventory.mainInventory.set(0,pumpkins(64));open(p,b);Map before=total(b,p); + 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); 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(world.loadedEntityList.stream().anyMatch(e->e instanceof EntityItem && !e.removed),"Successful displacement no longer drops normally"); + if(!rejectJoin && !rejectToss)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.removed),"Successful displacement no longer drops normally"); } private static void closeCursor(boolean rejected,boolean full) throws Exception { - EntityLootableBody b=body();EntityPlayerMP 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();ServerPlayerEntity p=player();if(full){fill(b);fill(p);}open(p,b);p.inventory.setItemStack(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(world.loadedEntityList.stream().anyMatch(e->e instanceof EntityItem && !e.removed),"Cursor close no longer performs normal accepted drop"); + if(!rejected)RuntimeChecks.require(FixtureApi.entities(world).stream().anyMatch(e->e instanceof ItemEntity && !e.removed),"Cursor close no longer performs normal accepted drop"); } private static void capturedCursor() throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();open(p,b);p.inventory.setItemStack(item(120,17)); - p.captureDrops(new java.util.ArrayList<>());p.captureDrops().add(new EntityItem(world,96.5,80,96.5,item(121,11)));Map before=total(b,p); + EntityLootableBody b=body();ServerPlayerEntity p=player();open(p,b);p.inventory.setItemStack(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();EntityPlayerMP p=player();fill(p);open(p,b);p.inventory.setItemStack(item(120,17)); + 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); RejectedDrops reject=new RejectedDrops();reject.join=true;MinecraftForge.EVENT_BUS.register(reject); try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} @@ -351,8 +363,8 @@ 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));EntityPlayerMP a=player(),c=player();open(a,b);open(c,b); - EntityPlayerMP first=order==0?a:c,second=order==0?c:a;Map before=total(b,a,c); + 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"); @@ -364,38 +376,38 @@ private static void twoPlayerShift() throws Exception { } } private static void interleavedDrag(boolean swap) throws Exception { - EntityLootableBody b=body();EntityPlayerMP a=player(),c=player();open(a,b);open(c,b);a.inventory.setItemStack(item(19,10)); + 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); 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); } private static void twoPlayerClose() throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));EntityPlayerMP a=player(),c=player();fill(a);open(a,b);open(c,b);Map before=total(b,a,c); + 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); 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); } private static void stale(boolean removed) throws Exception { - EntityLootableBody b=body();b.setInventorySlotContents(6,item(19,23));EntityPlayerMP p=player();open(p,b);Map before=total(b,p); + 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); } private static void staleDrag() throws Exception { - EntityLootableBody b=body();EntityPlayerMP p=player();open(p,b);p.inventory.setItemStack(item(19,10));Map before=total(b,p); + 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 NetHandlerPlayServer { + private static final class RecordingConnection extends ServerPlayNetHandler { final Map slots=new HashMap<>();int resyncs,rejections; - RecordingConnection(EntityPlayerMP p){super(server,new NetworkManager(EnumPacketDirection.SERVERBOUND),p);} - @Override public void sendPacket(Packet packet){ - if(!(packet instanceof SPacketWindowItems || packet instanceof SPacketSetSlot || packet instanceof SPacketConfirmTransaction))return; + 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 SPacketWindowItems){int n=wire.readShort();for(int i=0;i=0)slots.put(slot,s);} + 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();} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java index 5a311bb..35a5d75 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java @@ -1,4 +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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -6,15 +10,15 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.screen.MainMenuScreen; import net.minecraft.entity.Entity; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; -import net.minecraft.inventory.ClickType; +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.EnumHand; +import net.minecraft.util.Hand; import net.minecraft.util.ScreenShotHelper; import net.minecraft.util.math.BlockPos; import net.minecraft.world.GameType; @@ -28,8 +32,8 @@ private interface Action {void run() throws Exception;} private int stage,frames; private Future pending; private UUID playerId; - private NBTTagCompound savedBody; - private NBTTagList savedPlayer; + private CompoundNBT savedBody; + private ListNBT savedPlayer; void start(Minecraft mc){playerId=mc.player.getUniqueID();} boolean tick(Minecraft mc) throws Exception { frames++; @@ -72,44 +76,44 @@ else if(stage==16 && frames>20){ } else if(stage==18 && done() && frames>20){equal(mc.player.inventory.getItemStack(),item(122,17),"Creative close cursor fixture");mc.player.closeScreen();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");NBTTagCompound data=new NBTTagCompound();body(mc).writeAdditional(data);NBTTagList aux=data.getList("Aux",10);int count=0; + 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;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 NBTTagList())),"Creative player inventory changed on disk reload");player(mc).connection.setPlayerLocation(40.5,80,7.5,0,0); + 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); 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];NBTTagList list=savedBody.getList("Equipment",10);for(int i=0;i(player(mc).world.loadedEntityList))if(e instanceof EntityItem && !e.removed){equal(((EntityItem)e).getItem(),expected,"Accepted world drop NBT/count");count++;player(mc).world.removeEntityDangerously(e);}RuntimeChecks.require(count==1,"Expected exactly one accepted drop");} + private void drop(Minecraft mc,ItemStack expected){int count=0;for(Entity e:new ArrayList(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:mc.world.loadedEntityList)if(e.getUniqueID().equals(bodyId)){mc.playerController.interactWithEntity(mc.player,e,EnumHand.MAIN_HAND);return true;}return false;} - private EntityPlayerMP player(Minecraft mc){return mc.getIntegratedServer().getPlayerList().getPlayerByUUID(playerId);} + 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(net.minecraft.world.dimension.DimensionType.getById(0)),bodyId);} - private File folder(Minecraft mc){return player(mc).world.getSaveHandler().getWorldDirectory();} + private File folder(Minecraft mc){return player(mc).getServerWorld().getSaveHandler().getWorldDirectory();} 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().addScheduledTask(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} + private void schedule(Minecraft mc,Action action){pending=mc.getIntegratedServer().runAsync(()->{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.mainWindow.getFramebufferWidth(),mc.mainWindow.getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java index 17f1057..b62c875 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java @@ -1,51 +1,72 @@ 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 cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; import net.minecraft.entity.SharedMonsterAttributes; -import net.minecraft.init.MobEffects; +import net.minecraft.potion.Effects; import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.potion.*; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; /** Real legacy readers, ticking and round trips; no production test dependency. */ final class EffectHurtChecks { private interface Check {void run() throws Exception;} - private static WorldServer world; + private static ServerWorld world; private static int failures; private static final List results=new ArrayList<>(); - private static Potion extended; + private static Effect extended; static void registerPotion(){ - RuntimeChecks.require(Potion.getPotionById(200)==null,"Fixture potion ID already occupied"); - extended=new Potion(false,0x123456){}; + RuntimeChecks.require(Effect.get(200)==null,"Fixture potion ID already occupied"); + extended=new Effect(net.minecraft.potion.EffectType.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;Potion.getPotionById(199)==null && i<200;i++) - net.minecraftforge.registries.ForgeRegistries.POTIONS.register(new Potion(false,0){}.setRegistryName("lootablebodiesbuildtest:padding_"+i)); + 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(Potion.getIdFromPotion(extended)==200,"Fixture potion must exercise unsigned byte ID 200"); + RuntimeChecks.require(Effect.getId(extended)==200,"Fixture potion must exercise unsigned byte ID 200"); + // Older probes saved a different number of padding effects. Retain their + // registry names during upgrades without changing fresh-world ID tests. + if(System.getProperty("lootablebodies.phase","").startsWith("upgrade-")) + 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)); + } } - static void run(MinecraftServer server,WorldServer target,File folder) throws Exception { + static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { world=target;failures=0;results.clear(); - INBTBase[] wrong={new NBTTagString("opaque"),new NBTTagCompound(),new NBTTagByteArray(new byte[]{1}),new NBTTagIntArray(new int[]{1}),new NBTTagList()}; - for(int i=0;ibadHurt(raw));} - for(INBTBase raw:new INBTBase[]{new NBTTagShort((short)-1),new NBTTagInt(32768),new NBTTagLong(65537),new NBTTagLong(Long.MAX_VALUE),new NBTTagFloat(2),new NBTTagDouble(2.5),new NBTTagDouble(Double.NaN),new NBTTagDouble(Double.POSITIVE_INFINITY)})check("HurtTime_invalid_"+raw,()->badHurt(raw)); - for(INBTBase raw:new INBTBase[]{new NBTTagByte((byte)7),new NBTTagShort((short)10),new NBTTagInt(11),new NBTTagLong(32767)})check("HurtTime_lossless_"+raw,()->validHurt(raw)); - check("missing_hurt_time_defaults_zero",()->{NBTTagCompound n=base();n.removeTag("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",()->{NBTTagList l=new NBTTagList();l.add(new NBTTagString("opaque"));badList(l);}); - check("empty_and_missing_effects_keep_defaults",()->{for(boolean empty:new boolean[]{false,true}){NBTTagCompound n=base();if(empty)n.setTag("ActiveEffects",new NBTTagList());EntityLootableBody b=read(n);require(b.getActivePotionEffects().isEmpty(),"Unexpected default effect");require(!saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Healthy empty effects quarantined");stable(b);}}); - for(String key:new String[]{"Id","Duration","Amplifier"})for(int i=0;ibadEffect(k,raw));} - for(INBTBase raw:new INBTBase[]{new NBTTagInt(-1),new NBTTagInt(256),new NBTTagLong(4294967297L),new NBTTagDouble(1),new NBTTagByte((byte)201)})check("Id_invalid_or_absent_registry_"+raw,()->badEffect("Id",raw)); + INBT[] wrong={new StringNBT("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[]{new ShortNBT((short)-1),new IntNBT(32768),new LongNBT(65537),new LongNBT(Long.MAX_VALUE),new FloatNBT(2),new DoubleNBT(2.5),new DoubleNBT(Double.NaN),new DoubleNBT(Double.POSITIVE_INFINITY)})check("HurtTime_invalid_"+raw,()->badHurt(raw)); + for(INBT raw:new INBT[]{new ByteNBT((byte)7),new ShortNBT((short)10),new IntNBT(11),new LongNBT(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(new StringNBT("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[]{new IntNBT(-1),new IntNBT(256),new LongNBT(4294967297L),new DoubleNBT(1),new ByteNBT((byte)201)})check("Id_invalid_or_absent_registry_"+raw,()->badEffect("Id",raw)); check("Id_missing_retains_record",()->badEffect("Id",null)); - for(INBTBase raw:new INBTBase[]{new NBTTagInt(-1),new NBTTagLong(2147483648L),new NBTTagLong(4294967496L),new NBTTagFloat(100),new NBTTagDouble(Double.NaN),new NBTTagDouble(Double.NEGATIVE_INFINITY)})check("Duration_invalid_"+raw,()->badEffect("Duration",raw)); - for(INBTBase raw:new INBTBase[]{new NBTTagShort((short)-1),new NBTTagInt(128),new NBTTagInt(256),new NBTTagLong(4294967296L),new NBTTagFloat(1),new NBTTagDouble(Double.POSITIVE_INFINITY)})check("Amplifier_invalid_"+raw,()->badEffect("Amplifier",raw)); + for(INBT raw:new INBT[]{new IntNBT(-1),new LongNBT(2147483648L),new LongNBT(4294967496L),new FloatNBT(100),new DoubleNBT(Double.NaN),new DoubleNBT(Double.NEGATIVE_INFINITY)})check("Duration_invalid_"+raw,()->badEffect("Duration",raw)); + for(INBT raw:new INBT[]{new ShortNBT((short)-1),new IntNBT(128),new IntNBT(256),new LongNBT(4294967296L),new FloatNBT(1),new DoubleNBT(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(INBTBase raw:new INBTBase[]{new NBTTagByte((byte)-1),new NBTTagInt(2),new NBTTagLong(256),new NBTTagFloat(1),new NBTTagDouble(Double.NaN)})check(key+"_invalid_"+raw,()->badFlag(key,raw)); + for(int i=0;ibadFlag(k,raw));} + for(INBT raw:new INBT[]{new ByteNBT((byte)-1),new IntNBT(2),new LongNBT(256),new FloatNBT(1),new DoubleNBT(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); @@ -64,43 +85,43 @@ static void run(MinecraftServer server,WorldServer 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",()->{NBTTagCompound e=effect();e.setBoolean("ShowIcon",false);EntityLootableBody b=read(with(e));require(!b.getActivePotionEffect(MobEffects.NIGHT_VISION).isShowIcon(),"Native icon visibility changed");require(!saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Native icon visibility quarantined");stable(b);}); - check("missing_icon_flag_follows_particle_default",()->{NBTTagCompound e=effect();e.removeTag("ShowIcon");e.setBoolean("ShowParticles",false);EntityLootableBody b=read(with(e));require(!b.getActivePotionEffect(MobEffects.NIGHT_VISION).isShowIcon(),"Missing native icon default changed");stable(b);}); + 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("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 NBTTagCompound saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} - private static NBTTagCompound 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(NBTTagCompound n){NBTTagCompound before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.read(n);require(NameLifetimeChecks.same(before,n),"Input mutated");return b;} - static NBTTagCompound effect(Potion p,int duration,int amplifier){return new PotionEffect(p,duration,amplifier,false,true).write(new NBTTagCompound());} - private static NBTTagCompound effect(){return effect(MobEffects.NIGHT_VISION,200,1);} - private static NBTTagCompound with(NBTTagCompound e){NBTTagCompound n=base();n.setTag("ActiveEffects",NbtRecoveryChecks.list(e));return n;} - private static void stable(EntityLootableBody b){NBTTagCompound 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.getTag(k),z.getTag(k)),"Round trip changed "+k);require(RuntimeChecks.sameStack(b.getStackInSlot(6),NbtRecoveryChecks.item(120,17)),"Complete known inventory changed");} - private static void badHurt(INBTBase raw){NBTTagCompound n=base();n.setTag("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(INBTBase raw){NBTTagCompound n=base();n.setTag("HurtTime",raw);EntityLootableBody b=read(n);int v=((NBTPrimitive)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(INBTBase raw){NBTTagCompound n=base();n.setTag("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,INBTBase raw){NBTTagCompound e=effect();if(raw==null)e.removeTag(key);else e.setTag(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,INBTBase raw){NBTTagCompound e=effect();e.setTag(key,raw.copy());EntityLootableBody b=read(with(e));PotionEffect p=b.getActivePotionEffect(MobEffects.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++){NBTTagCompound e=effect();INBTBase[] ids={new NBTTagByte((byte)16),new NBTTagShort((short)16),new NBTTagInt(16),new NBTTagLong(16)};INBTBase[] one={new NBTTagByte((byte)1),new NBTTagShort((short)1),new NBTTagInt(1),new NBTTagLong(1)};e.setTag("Id",ids[type]);e.setTag("Duration",one[type]);e.setTag("Amplifier",one[type].copy());e.setTag("Ambient",one[type].copy());e.setTag("ShowParticles",one[type].copy());EntityLootableBody b=read(with(e));PotionEffect p=b.getActivePotionEffect(MobEffects.NIGHT_VISION);require(p!=null && p.getDuration()==1 && p.getAmplifier()==1 && p.isAmbient() && p.doesShowParticles(),"Lossless integral effect changed");require(!saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Valid numbers quarantined");stable(b);}} - private static void missingOptional(){NBTTagCompound e=effect();for(String k:new String[]{"Duration","Amplifier","Ambient","ShowParticles"})e.removeTag(k);EntityLootableBody b=read(with(e));PotionEffect p=b.getActivePotionEffect(MobEffects.NIGHT_VISION);require(p!=null && p.getDuration()==0 && p.getAmplifier()==0 && !p.isAmbient() && p.doesShowParticles(),"Legacy defaults changed");require(!saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Missing optional fields quarantined");stable(b);} - private static void validDuration(int d){EntityLootableBody b=read(with(effect(MobEffects.NIGHT_VISION,d,0)));b.baseTick();PotionEffect p=b.getActivePotionEffect(MobEffects.NIGHT_VISION);require(d==0?p==null:p!=null && p.getDuration()==d-1,"Valid duration tick/expiry changed");stable(b);} - private static void signedAmplifiers(){for(int a:new int[]{-128,-1,0,127}){EntityLootableBody b=read(with(effect(MobEffects.NIGHT_VISION,200,a)));require(b.getActivePotionEffect(MobEffects.NIGHT_VISION).getAmplifier()==Math.max(0,a),"Native 1.11 byte amplifier clamping changed");require(!saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Canonical signed byte quarantined");stable(b);}} - private static void curativeItems(){NBTTagCompound e=effect();NBTTagCompound item=NbtRecoveryChecks.item(120,7).write(new NBTTagCompound());NBTTagList list=new NBTTagList();list.add(item);e.setTag("CurativeItems",list);EntityLootableBody b=read(with(e));require(b.getActivePotionEffect(MobEffects.NIGHT_VISION).getCurativeItems().size()==1 && RuntimeChecks.sameStack(b.getActivePotionEffect(MobEffects.NIGHT_VISION).getCurativeItems().get(0),ItemStack.read(item)),"Forge curative item NBT changed");require(!saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Healthy Forge curative items quarantined");stable(b);} - private static void badCurativeItems(){NBTTagCompound e=effect();e.setString("CurativeItems","opaque");EntityLootableBody b=read(with(e));NameLifetimeChecks.retained(b,"ActiveEffect",e);require(!b.getActivePotionEffect(MobEffects.NIGHT_VISION).getCurativeItems().isEmpty(),"Malformed curative list did not use native defaults");stable(b);} - private static void extendedId(){for(boolean wide:new boolean[]{false,true}){NBTTagCompound e=effect(extended,200,1);if(wide)e.setInt("Id",200);EntityLootableBody b=read(with(e));require(b.getActivePotionEffect(extended)!=null && b.getActivePotionEffect(extended).getDuration()==200,"Registered unsigned ID changed");require(!saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Valid extended ID quarantined");stable(b);}} - private static void alias(){NBTTagCompound first=effect(extended,200,1),second=effect(extended,900,3),n=base();second.setLong("Id",200);n.setTag("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(){NBTTagCompound bad=effect();bad.setString("Duration","opaque");NBTTagCompound n=base();n.setTag("ActiveEffects",NbtRecoveryChecks.list(bad,effect()));EntityLootableBody b=read(n);require(b.getActivePotionEffect(MobEffects.NIGHT_VISION).getDuration()==200,"Bad first effect hid valid tail");NameLifetimeChecks.retained(b,"ActiveEffect",bad);stable(b);} - private static void duplicate(){NBTTagCompound first=effect(),second=effect(MobEffects.NIGHT_VISION,900,3),n=base();n.setTag("ActiveEffects",NbtRecoveryChecks.list(first,second));EntityLootableBody b=read(n);require(b.getActivePotionEffect(MobEffects.NIGHT_VISION).getDuration()==200,"Duplicate overwrote first effect");NameLifetimeChecks.retained(b,"ActiveEffect",second);stable(b);} - private static void unknown(){NBTTagCompound e=effect();e.setString("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(MobEffects.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 PotionEffect(MobEffects.HEALTH_BOOST,200,1));original.addPotionEffect(new PotionEffect(MobEffects.SPEED,200,0));original.addPotionEffect(new PotionEffect(MobEffects.ABSORPTION,200,0));original.setHealth(original.getMaxHealth());NBTTagCompound 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).hasKey(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 PotionEffect(MobEffects.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());NBTTagCompound n=with(effect());b.writeAdditional(n);require(!n.hasKey("ActiveEffects"),"Writer retained obsolete ActiveEffects");stable(b);} - private static void inventory(){NBTTagCompound n=base(),missing=NbtRecoveryChecks.entry(121,11,7);missing.setString("id","absentfixture:missing");n.getList("Equipment",10).add(missing);NBTTagCompound raw=malformedFields();for(String k:raw.keySet())n.setTag(k,raw.getTag(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(){NBTTagCompound n=base();n.setTag("Equipment",new NBTTagList());n.setString("HurtTime","opaque");EntityLootableBody b=read(n);require(b.isEmpty(),"Metadata counts as inventory");NameLifetimeChecks.retained(b,"HurtTime",n.getTag("HurtTime"));} - static NBTTagCompound malformedFields(){NBTTagCompound n=new NBTTagCompound();n.setLong("HurtTime",65537);NBTTagCompound bad=effect();bad.setString("Duration","opaque saved duration");NBTTagCompound unknown=effect();unknown.setInt("Id",250);unknown.setString("Opaque","missing effect data");n.setTag("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");NBTTagCompound raw=malformedFields();NameLifetimeChecks.retained(b,"HurtTime",raw.getTag("HurtTime"));NBTTagList list=raw.getList("ActiveEffects",10);for(int i=0;i entities(World world) { + if (world instanceof ServerWorld) return ((ServerWorld) world).getEntities().collect(Collectors.toList()); + return ClientEntities.read(world); + } + static Entity loadEntity(CompoundNBT data, World world) { + return net.minecraft.entity.EntityType.func_220335_a(data, world, entity -> entity); + } + static Entity readChunkEntity(CompoundNBT data, ServerWorld world, net.minecraft.world.chunk.Chunk 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); + 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})) { + 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; + }catch(ReflectiveOperationException failure){throw new IllegalStateException("Native chunk admission failed",failure);} + } + static void awaitTicking(Entity entity) { + ServerWorld world=(ServerWorld)entity.world;int x=net.minecraft.util.math.MathHelper.floor(entity.posX)>>4,z=net.minecraft.util.math.MathHelper.floor(entity.posZ)>>4; + world.forceChunk(x,z,true); + net.minecraft.world.server.ServerChunkProvider provider=world.getChunkProvider(); + long deadline=System.nanoTime()+java.util.concurrent.TimeUnit.SECONDS.toNanos(30); + while(!provider.isChunkLoaded(entity)) { + RuntimeChecks.require(System.nanoTime()true); + for(int task=0;task<1000 && provider.func_217234_d();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); + } + static void flush(MinecraftServer server) { server.save(true, true, false); } + @SuppressWarnings("unchecked") + static java.util.Set tracked(Entity entity) { + try { + Object manager=((ServerWorld)entity.world).getChunkProvider().chunkManager; + 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())) { + if(map!=null)throw new IllegalStateException("Ambiguous native tracking map"); map=field; + } + 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()); + if(tracker==null)return java.util.Collections.emptySet(); + java.lang.reflect.Field players=null; + for(java.lang.reflect.Field field:tracker.getClass().getDeclaredFields()) + if(java.util.Set.class.isAssignableFrom(field.getType())) { + if(players!=null)throw new IllegalStateException("Ambiguous native tracking set");players=field; + } + if(players==null)throw new IllegalStateException("Missing native tracking set"); + players.setAccessible(true); + return new java.util.HashSet<>((java.util.Set)players.get(tracker)); + }catch(ReflectiveOperationException failure){throw new IllegalStateException("Cannot inspect native tracking",failure);} + } + @OnlyIn(Dist.CLIENT) + private static final class ClientEntities { + static List read(World world) { + List result = new ArrayList<>(); + ((net.minecraft.client.world.ClientWorld) world).getAllEntities().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 711348c..e523508 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java @@ -4,20 +4,20 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiDisconnected; -import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.screen.DisconnectedScreen; +import net.minecraft.client.gui.screen.MainMenuScreen; import net.minecraft.client.multiplayer.ServerData; -import net.minecraft.client.gui.GuiConnecting; +import net.minecraft.client.gui.screen.ConnectingScreen; /** 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().getServerThread(); + if(mc.getIntegratedServer()!=null)retiringServer=mc.getIntegratedServer().getExecutionThread(); if(mc.player!=null)mc.player.closeScreen(); if(mc.world!=null)mc.world.sendQuittingDisconnectingPacket(); - mc.loadWorld(null);mc.displayGuiScreen(new GuiMainMenu()); + mc.func_213254_o();mc.displayGuiScreen(new MainMenuScreen()); } static boolean stopped(Minecraft mc) { return ServerStopGate.ready(mc.world==null,mc.isIntegratedServerRunning(),retiringServer); @@ -27,10 +27,10 @@ static boolean ready(String role) { } static void connect(Minecraft mc) { String address="127.0.0.1:"+Integer.parseInt(System.getProperty("lootablebodies.multiplayer.port")); - mc.displayGuiScreen(new GuiConnecting(new GuiMainMenu(),mc,new ServerData("Disposable check",address,false))); + mc.displayGuiScreen(new ConnectingScreen(new MainMenuScreen(),mc,new ServerData("Disposable check",address,false))); } static boolean retry(Minecraft mc,String role) throws Exception { - if(!(mc.currentScreen instanceof GuiDisconnected)||mc.world!=null)return false; + if(!(mc.currentScreen instanceof DisconnectedScreen)||mc.world!=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 +39,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 GuiMainMenu());return true; + mc.displayGuiScreen(new MainMenuScreen());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 77724d4..21705dc 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java @@ -2,8 +2,8 @@ import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.EnumFacing; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.util.Direction; import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; @@ -15,7 +15,7 @@ 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,NBTTagCompound nbt){return new Stored();} + @Override public ICapabilityProvider initCapabilities(ItemStack stack,CompoundNBT 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 NBTTagCompound data=new NBTTagCompound(); - Stored(){data.setString("owner","Disposable fixture");data.setLong("number",9876543210123L);data.setByteArray("opaque",new byte[]{0,1,-1,17});} - public boolean hasCapability(Capability capability,EnumFacing side){return false;} - public net.minecraftforge.common.util.LazyOptional getCapability(Capability capability,EnumFacing side){return net.minecraftforge.common.util.LazyOptional.empty();} - public NBTTagCompound serializeNBT(){return data.copy();} - public void deserializeNBT(NBTTagCompound nbt){data=nbt.copy();} + 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/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java index 5a8d66c..155d9ac 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java @@ -1,4 +1,17 @@ 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 cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; @@ -10,51 +23,51 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; /** 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 WorldServer world; + private static ServerWorld 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,WorldServer target,File folder) throws Exception { + static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { world=target;results.clear();failures=0; - INBTBase[] wrong={new NBTTagString("opaque"),new NBTTagCompound(),new NBTTagList(),new NBTTagByteArray(new byte[]{1}),new NBTTagIntArray(new int[]{1})}; + INBT[] wrong={new StringNBT("opaque"),new CompoundNBT(),new ListNBT(),new ByteArrayNBT(new byte[]{1}),new IntArrayNBT(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,new NBTTagDouble(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,new DoubleNBT(value))); } - check("health_above_effective_max_retained",()->badScalar("Health",new NBTTagFloat(21))); + check("health_above_effective_max_retained",()->badScalar("Health",new FloatNBT(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",()->{NBTTagCompound n=base();n.setTag("Attributes",new NBTTagList());EntityLootableBody b=read(n);value(b.getMaxHealth(),20,"Empty list changed max");RuntimeChecks.require(!NameLifetimeChecks.saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Valid empty list quarantined");stable(b);}); - check("Attributes_wrong_element_type",()->{NBTTagList raw=new NBTTagList();raw.add(new NBTTagString("opaque"));badList(raw);}); + 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(new StringNBT("opaque"));badList(raw);}); check("attribute_missing_name",()->badAttribute("Name",null)); - check("attribute_wrong_name_type",()->badAttribute("Name",new NBTTagInt(1))); - check("attribute_unknown_name_retained",()->badAttribute("Name",new NBTTagString("absentfixture:attribute"))); + check("attribute_wrong_name_type",()->badAttribute("Name",new IntNBT(1))); + check("attribute_unknown_name_retained",()->badAttribute("Name",new StringNBT("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",new NBTTagDouble(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",new DoubleNBT(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(new NBTTagString("opaque"))); - check("Modifiers_wrong_element_type_keeps_base",()->{NBTTagList l=new NBTTagList();l.add(new NBTTagInt(3));badModifierList(l);}); + check("Modifiers_wrong_list_type_keeps_base",()->badModifierList(new StringNBT("opaque"))); + check("Modifiers_wrong_element_type_keeps_base",()->{ListNBT l=new ListNBT();l.add(new IntNBT(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")?new NBTTagInt(1):new NBTTagString("opaque"))); + check("modifier_wrong_type_"+field,()->badModifier(field,field.equals("Name")?new IntNBT(1):new StringNBT("opaque"))); } - check("modifier_blank_name",()->badModifier("Name",new NBTTagString(""))); - for(double value:new double[]{Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY})check("modifier_nonfinite_amount_"+value,()->badModifier("Amount",new NBTTagDouble(value))); - for(INBTBase value:new INBTBase[]{new NBTTagInt(-1),new NBTTagInt(3),new NBTTagLong(4294967296L),new NBTTagFloat(1F)})check("modifier_invalid_operation_"+value,()->badModifier("Operation",value)); - check("modifier_fractional_UUID_not_coerced",()->badModifier("UUIDMost",new NBTTagDouble(1.5))); + check("modifier_blank_name",()->badModifier("Name",new StringNBT(""))); + for(double value:new double[]{Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY})check("modifier_nonfinite_amount_"+value,()->badModifier("Amount",new DoubleNBT(value))); + for(INBT value:new INBT[]{new IntNBT(-1),new IntNBT(3),new LongNBT(4294967296L),new FloatNBT(1F)})check("modifier_invalid_operation_"+value,()->badModifier("Operation",value)); + check("modifier_fractional_UUID_not_coerced",()->badModifier("UUIDMost",new DoubleNBT(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); @@ -72,59 +85,59 @@ static void run(MinecraftServer server,WorldServer target,File folder) throws Ex RuntimeChecks.require(failures==0,"Health/attribute regressions: "+failures+" / "+results.size());System.out.println("LOOTABLE_BODIES_HEALTH_ATTRIBUTE_PASS checks="+results.size()); } private static void check(String name,Check test){ - float hp=LootableBodies.corpseHP;Set before=new HashSet<>(world.loadedEntityList); + float hp=LootableBodies.corpseHP;Set before=new HashSet<>(FixtureApi.entities(world)); try{test.run();results.add("PASS "+name);System.out.println("HEALTH_ATTRIBUTE_PASS "+name);} catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("HEALTH_ATTRIBUTE_FAIL "+name+": "+e);e.printStackTrace();} - finally{LootableBodies.corpseHP=hp;for(Entity e:new ArrayList(world.loadedEntityList))if(!before.contains(e))world.removeEntityDangerously(e);} + finally{LootableBodies.corpseHP=hp;for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))world.removeEntity(e);} } - static NBTTagCompound 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(NBTTagCompound n){NBTTagCompound before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.read(n);RuntimeChecks.require(NameLifetimeChecks.same(before,n),"Input NBT mutated");return b;} - static NBTTagCompound attribute(String name,double base){NBTTagCompound n=new NBTTagCompound();n.setString("Name",name);n.setDouble("Base",base);return n;} - static NBTTagCompound modifier(double amount,int operation){NBTTagCompound n=new NBTTagCompound();n.setString("Name","Fixture modifier");n.setDouble("Amount",amount);n.setInt("Operation",operation);n.setUniqueId("UUID",MOD);return n;} - static NBTTagCompound with(NBTTagCompound a){NBTTagCompound n=base();n.setTag("Attributes",NbtRecoveryChecks.list(a));return n;} + 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;} + static CompoundNBT modifier(double amount,int operation){CompoundNBT n=new CompoundNBT();n.putString("Name","Fixture modifier");n.putDouble("Amount",amount);n.putInt("Operation",operation);n.putUniqueId("UUID",MOD);return n;} + static CompoundNBT with(CompoundNBT a){CompoundNBT 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){NBTTagCompound 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.getTag(key),z.getTag(key)),"Round trip changed "+key);inventoryKept(b);} - private static void retained(EntityLootableBody b,String key,INBTBase raw){NameLifetimeChecks.retained(b,key,raw);} - private static void badScalar(String key,INBTBase raw){NBTTagCompound n=base();n.setTag(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){INBTBase[] values={new NBTTagByte((byte)1),new NBTTagShort((short)1),new NBTTagInt(1),new NBTTagLong(1),new NBTTagFloat(1),new NBTTagDouble(1)};NBTTagCompound n=base();n.setTag("Health",values[type]);n.setTag("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(){NBTTagCompound n=base();n.setFloat("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(){NBTTagCompound n=with(attribute("generic.maxHealth",30));n.removeTag("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;NBTTagCompound n=base();n.removeTag("Attributes");n.removeTag("Health");EntityLootableBody b=read(n);value(b.getMaxHealth(),37,"Config health changed");value(b.getAttribute(SharedMonsterAttributes.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);NBTTagCompound n=NameLifetimeChecks.saved(original);EntityLootableBody b=read(n);value(b.getMaxHealth(),original.getMaxHealth(),"Existing configured health clamp changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).hasKey(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));NBTTagCompound 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).hasKey(NameLifetimeChecks.RECOVERY),"Healthy historical config quarantined");stable(b);} - private static void badList(INBTBase raw){NBTTagCompound n=base();n.setTag("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,INBTBase raw){NBTTagCompound a=attribute("generic.maxHealth",30);if(raw==null)a.removeTag(field);else a.setTag(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(){NBTTagCompound a=attribute("generic.maxHealth",Double.NaN),n=base();n.setTag("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(SharedMonsterAttributes.FOLLOW_RANGE).getBaseValue(),24,"Valid tail lost");retained(b,"Attribute",a);stable(b);} - private static void duplicateAttribute(){NBTTagCompound duplicate=attribute("generic.maxHealth",40),n=base();n.setTag("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(){NBTTagCompound a=attribute("generic.maxHealth",30);a.setString("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(INBTBase raw){NBTTagCompound a=attribute("generic.maxHealth",30);a.setTag("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,INBTBase raw){NBTTagCompound m=modifier(5,0);if(raw==null)m.removeTag(field);else m.setTag(field,raw.copy());NBTTagCompound a=attribute("generic.maxHealth",30);a.setTag("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(){NBTTagCompound a=attribute("generic.maxHealth",20);a.setTag("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(){NBTTagCompound bad=modifier(Double.NaN,0),good=modifier(5,0),a=attribute("generic.maxHealth",30);good.setUniqueId("UUID",new UUID(0,142));a.setTag("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(){NBTTagCompound m=modifier(5,0),a=attribute("generic.maxHealth",30);m.setString("Opaque","retain");a.setTag("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(){NBTTagCompound a=attribute("generic.maxHealth",30);a.setTag("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(){NBTTagCompound a=attribute("generic.maxHealth",30);NBTTagCompound m0=modifier(-2,0),m1=modifier(.5,1),m2=modifier(.25,2);m1.setUniqueId("UUID",new UUID(0,142));m2.setUniqueId("UUID",new UUID(0,143));a.setTag("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).hasKey(NameLifetimeChecks.RECOVERY),"Valid modifiers quarantined");stable(b);} - private static void zeroUuid(){NBTTagCompound a=attribute("generic.maxHealth",30),m=modifier(5,0);m.setLong("UUIDMost",0);m.setLong("UUIDLeast",0);m.setByte("Operation",(byte)0);m.setInt("Amount",5);a.setTag("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(INBTBase raw:new INBTBase[]{new NBTTagByte((byte)30),new NBTTagShort((short)30),new NBTTagInt(30),new NBTTagLong(30),new NBTTagFloat(30.5F),new NBTTagDouble(30.5)}){NBTTagCompound a=attribute("generic.maxHealth",30);a.setTag("Base",raw);NBTTagCompound n=with(a);n.setDouble("Health",12.5);EntityLootableBody b=read(n);value(b.getMaxHealth(),((NBTPrimitive)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)));NBTTagCompound n=base();n.removeTag("Attributes");n.removeTag("Health");n.removeTag("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(){NBTTagCompound a=attribute("generic.maxHealth",30);a.setTag("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(){NBTTagCompound n=base();n.setFloat("Health",0);EntityLootableBody b=read(n);b.baseTick();n=base();n.setDouble("Health",Double.NaN);b.read(n);b.baseTick();RuntimeChecks.require(b.isAlive(),"Repaired owner still dead");inventoryKept(b);stable(b);n=base();n.setFloat("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(){NBTTagCompound n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.setString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);n.setDouble("Health",Double.NaN);NBTTagCompound a=attribute("generic.maxHealth",Double.NaN);n.setTag("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.getTag("Health"));stable(b);stable(read(NameLifetimeChecks.saved(b)));} - private static void empty(){NBTTagCompound n=base();n.setTag("Equipment",new NBTTagList());n.setDouble("Health",Double.NaN);EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Health evidence counted as inventory");retained(b,"Health",n.getTag("Health"));} - private static void healthy(){NBTTagCompound n=base();EntityLootableBody b=read(n);RuntimeChecks.require(!NameLifetimeChecks.saved(b).hasKey(NameLifetimeChecks.RECOVERY),"Healthy save gained recovery records");stable(b);} + 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={new ByteNBT((byte)1),new ShortNBT((short)1),new IntNBT(1),new LongNBT(1),new FloatNBT(1),new DoubleNBT(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(SharedMonsterAttributes.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(SharedMonsterAttributes.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);good.putUniqueId("UUID",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);m1.putUniqueId("UUID",new UUID(0,142));m2.putUniqueId("UUID",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[]{new ByteNBT((byte)30),new ShortNBT((short)30),new IntNBT(30),new LongNBT(30),new FloatNBT(30.5F),new DoubleNBT(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);} - static NBTTagCompound malformedFields(){ - NBTTagCompound n=new NBTTagCompound();n.setDouble("Health",Double.NaN);n.setString("AbsorptionAmount","opaque saved absorption"); - NBTTagCompound max=attribute("generic.maxHealth",Double.NaN),follow=attribute("generic.followRange",24),unknown=attribute("absentfixture:attribute",7); - NBTTagCompound bad=modifier(Double.NaN,0),good=modifier(2,0);good.setUniqueId("UUID",new UUID(0,144)); - follow.setTag("Modifiers",NbtRecoveryChecks.list(bad,good));unknown.setString("Opaque","retain original attribute"); - n.setTag("Attributes",NbtRecoveryChecks.list(max,follow,unknown));return n; + static CompoundNBT malformedFields(){ + CompoundNBT n=new CompoundNBT();n.putDouble("Health",Double.NaN);n.putString("AbsorptionAmount","opaque saved absorption"); + CompoundNBT max=attribute("generic.maxHealth",Double.NaN),follow=attribute("generic.followRange",24),unknown=attribute("absentfixture:attribute",7); + CompoundNBT bad=modifier(Double.NaN,0),good=modifier(2,0);good.putUniqueId("UUID",new UUID(0,144)); + follow.put("Modifiers",NbtRecoveryChecks.list(bad,good));unknown.putString("Opaque","retain original attribute"); + n.put("Attributes",NbtRecoveryChecks.list(max,follow,unknown));return n; } static void verifyWorldFields(EntityLootableBody b){ value(b.getHealth(),20,"Disk health unsafe");value(b.getMaxHealth(),20,"Disk max health unsafe");value(b.getAbsorptionAmount(),0,"Disk absorption unsafe"); value(b.getAttribute(SharedMonsterAttributes.FOLLOW_RANGE).getValue(),26,"Valid attribute/modifier tail lost on disk"); - NBTTagCompound raw=malformedFields();retained(b,"Health",raw.getTag("Health"));retained(b,"AbsorptionAmount",raw.getTag("AbsorptionAmount")); - NBTTagList entries=raw.getList("Attributes",10);for(int i=0;i results = new ArrayList<>(); private static int failures; private static int sequence; - private static WorldServer world; + private static ServerWorld world; private static MinecraftServer server; - public static void run(MinecraftServer instance, WorldServer target, File folder) throws Exception { + public static void run(MinecraftServer instance, ServerWorld 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); @@ -61,12 +69,12 @@ public static void run(MinecraftServer instance, WorldServer target, File folder System.out.println("LOOTABLE_BODIES_INVENTORY_PASS checks="+results.size()); } private static void check(String name,Check check) { - List before=new ArrayList<>(world.loadedEntityList); - String keep=world.getGameRules().get("keepInventory").getString(); - String mobLoot=world.getGameRules().get("doMobLoot").getString(); + 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(); try { - world.getGameRules().setOrCreateGameRule("keepInventory","false",null); - world.getGameRules().setOrCreateGameRule("doMobLoot","false",null); + 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); check.run(); results.add("PASS "+name); System.out.println("INVENTORY_CHECK_PASS "+name); } catch(Throwable error) { @@ -74,18 +82,18 @@ private static void check(String name,Check check) { System.out.println("INVENTORY_CHECK_FAIL "+name+": "+error); error.printStackTrace(); } finally { - world.getGameRules().setOrCreateGameRule("keepInventory",keep,null); - world.getGameRules().setOrCreateGameRule("doMobLoot",mobLoot,null); - for(Entity entity:new ArrayList(world.loadedEntityList)) - if(!before.contains(entity)) world.removeEntityDangerously(entity); + 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); + for(Entity entity:new ArrayList(FixtureApi.entities(world))) + if(!before.contains(entity)) world.removeEntity(entity); } } - private static EntityPlayerMP player() { - EntityPlayerMP player=new EntityPlayerMP(server,world, + 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.NetHandlerPlayServer(server, - new net.minecraft.network.NetworkManager(net.minecraft.network.EnumPacketDirection.SERVERBOUND),player); + 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); return player; } @@ -98,45 +106,45 @@ private static EntityLootableBody body() { private static ItemStack item(int id,int count) { ItemStack stack=RuntimeChecks.stack(id); stack.setCount(count); return stack; } - private static List drops(EntityPlayerMP player,ItemStack... stacks) { - List result=new ArrayList<>(); - for(ItemStack stack:stacks) result.add(new EntityItem(player.getEntityWorld(),player.posX,player.posY,player.posZ,stack)); + private static List drops(ServerPlayerEntity player,ItemStack... stacks) { + List result=new ArrayList<>(); + for(ItemStack stack:stacks) result.add(new ItemEntity(player.getEntityWorld(),player.posX,player.posY,player.posZ,stack)); return result; } - private static void handle(PlayerDeathEventHandler handler,EntityPlayerMP player,List drops) { + private static void handle(PlayerDeathEventHandler handler,ServerPlayerEntity player,List drops) { handler.entityDropEvent(new LivingDropsEvent(player,DamageSource.OUT_OF_WORLD,drops,0,false)); } - private static NBTTagCompound data(EntityLootableBody body) { - NBTTagCompound result=new NBTTagCompound(); body.writeAdditional(result); return result; + private static CompoundNBT data(EntityLootableBody body) { + CompoundNBT result=new CompoundNBT(); body.writeAdditional(result); return result; } - private static NBTTagCompound encoded(ItemStack stack) { return stack.write(new NBTTagCompound()); } + private static CompoundNBT encoded(ItemStack stack) { return stack.write(new CompoundNBT()); } 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; - NBTTagCompound tag=encoded(stack); tag.removeTag("Count"); + CompoundNBT 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:world.loadedEntityList) if(entity instanceof EntityItem && !entity.removed) add(result,((EntityItem)entity).getItem()); + for(Entity entity:FixtureApi.entities(world)) if(entity instanceof ItemEntity && !entity.removed) add(result,((ItemEntity)entity).getItem()); return result; } private static EntityLootableBody newest(Set before) { - for(Entity entity:world.loadedEntityList) if(entity instanceof EntityLootableBody && !before.contains(entity)) return (EntityLootableBody)entity; + for(Entity entity:FixtureApi.entities(world)) if(entity instanceof EntityLootableBody && !before.contains(entity)) return (EntityLootableBody)entity; throw new AssertionError("No corpse spawned"); } private static void failedSpawn() { - EntityPlayerMP player=player(); RejectingWorld rejected=new RejectingWorld(world); + 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.TextComponentString("Saved bones")); - List drops=drops(player,bones,item(19,23)); - List before=new ArrayList<>(); for(EntityItem e:drops) before.add(encoded(e.getItem())); + ItemStack bones=new ItemStack(Items.BONE,9); bones.setDisplayName(new net.minecraft.util.text.StringTextComponent("Saved bones")); + List drops=drops(player,bones,item(19,23)); + List before=new ArrayList<>(); for(ItemEntity e:drops) before.add(encoded(e.getItem())); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); handle(handler,player,drops); @@ -151,24 +159,24 @@ public static final class RejectCorpse { } } private static void cancelledSpawn() { - EntityPlayerMP player=player(); player.inventory.mainInventory.set(0,item(4,1)); player.inventory.mainInventory.set(9,item(19,23)); + 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)); - Set before=new HashSet<>(world.loadedEntityList); + 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); } RuntimeChecks.require(reject.attempts==1,"Corpse cancellation not exercised"); - for(Entity entity:world.loadedEntityList) RuntimeChecks.require(before.contains(entity) || !(entity instanceof EntityLootableBody),"Cancelled corpse spawn bypassed Forge cancellation"); + 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() { - EntityPlayerMP player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); - ItemStack previous=item(0,1); player.setItemStackToSlot(EntityEquipmentSlot.HEAD,previous); + ServerPlayerEntity player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); + ItemStack previous=item(0,1); player.setItemStackToSlot(EquipmentSlotType.HEAD,previous); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); handle(handler,player,drops(player,previous)); - ItemStack current=item(1,1); player.setItemStackToSlot(EntityEquipmentSlot.HEAD,current); + ItemStack current=item(1,1); player.setItemStackToSlot(EquipmentSlotType.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<>(world.loadedEntityList); + Set before=new HashSet<>(FixtureApi.entities(world)); handle(handler,player,drops(player,current,previous)); EntityLootableBody corpse=newest(before); Map actual=inventory(corpse); @@ -181,20 +189,20 @@ private static void staleEquipment() { return (Map)field.get(handler); } private static void cacheLifecycle() throws Exception { - EntityPlayerMP player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); - player.setItemStackToSlot(EntityEquipmentSlot.HEAD,item(0,1)); + ServerPlayerEntity player=player(); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); + player.setItemStackToSlot(EquipmentSlotType.HEAD,item(0,1)); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); - handle(handler,player,drops(player,player.getItemStackFromSlot(EntityEquipmentSlot.HEAD))); + handle(handler,player,drops(player,player.getItemStackFromSlot(EquipmentSlotType.HEAD))); RuntimeChecks.require(!cache(handler).containsKey(player),"Equipment cache retains completed death"); - world.getGameRules().setOrCreateGameRule("keepInventory","true",null); + world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).set(true,null); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); RuntimeChecks.require(!cache(handler).containsKey(player),"keepInventory death cached equipment"); - world.getGameRules().setOrCreateGameRule("keepInventory","false",null); + world.getGameRules().get(net.minecraft.world.GameRules.KEEP_INVENTORY).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"); handler.entityDeathEvent(new LivingDeathEvent(player,DamageSource.OUT_OF_WORLD)); - List original=drops(player,item(19,23)); + List original=drops(player,item(19,23)); LivingDropsEvent canceledDrops=new LivingDropsEvent(player,DamageSource.OUT_OF_WORLD,original,0,false); canceledDrops.setCanceled(true);handler.entityDropEvent(canceledDrops); RuntimeChecks.require(!cache(handler).containsKey(player) && original.size()==1 @@ -204,29 +212,29 @@ public static final class RejectItems { boolean rejectAll; String rejectFixture; @SubscribeEvent public void join(EntityJoinWorldEvent event) { - if(event.getEntity() instanceof EntityItem) { - ItemStack stack=((EntityItem)event.getEntity()).getItem(); + if(event.getEntity() instanceof ItemEntity) { + ItemStack stack=((ItemEntity)event.getEntity()).getItem(); if(rejectAll || stack.hasTag() && rejectFixture.equals(stack.getTag().getString("fixture"))) event.setCanceled(true); } } } private static void failedRelease() throws Exception { EntityLootableBody corpse=body(); corpse.setInventorySlotContents(6,item(19,23)); - RuntimeChecks.require(world.spawnEntity(corpse),"Rejected-release owner spawn"); + RuntimeChecks.require(world.addEntity(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(); } finally { MinecraftForge.EVENT_BUS.unregister(reject); } RuntimeChecks.require(expected.equals(inventory(corpse)),"Rejected item spawn erased corpse inventory"); - RuntimeChecks.require(corpse.isAlive() && world.loadedEntityList.contains(corpse),"Rejected release removed the only inventory owner"); - NBTTagCompound saved=data(corpse); EntityLootableBody loaded=body();loaded.readAdditional(saved); + 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); RuntimeChecks.require(expected.equals(inventory(loaded)),"Rejected items do not survive NBT reload"); corpse.onKillCommand();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.spawnEntity(corpse),"Partial-release owner spawn"); + RuntimeChecks.require(world.addEntity(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(); } @@ -238,9 +246,9 @@ private static void partialRelease() { RuntimeChecks.require(expected.equals(dropped()),"Partial retry duplicated previously accepted drops"); } private static void vanillaEquipment() { - world.getGameRules().setOrCreateGameRule("doMobLoot","true",null); - EntityLootableBody corpse=body();corpse.setInventorySlotContents(0,item(0,1));corpse.setDropChance(EntityEquipmentSlot.HEAD,2F); - RuntimeChecks.require(world.spawnEntity(corpse),"Equipment-release owner spawn"); + 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"); Map expected=inventory(corpse); corpse.onKillCommand();for(int i=0;i<12;i++)corpse.baseTick(); RuntimeChecks.require(expected.equals(dropped()),"Vanilla mob equipment path duplicates corpse inventory"); @@ -249,7 +257,7 @@ 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);} - EntityItem entity=new EntityItem(world,64.5,80,64.5,item(19,23));RuntimeChecks.require(world.spawnEntity(entity),"Vacuum input spawn"); + ItemEntity entity=new ItemEntity(world,64.5,80,64.5,item(19,23));RuntimeChecks.require(world.addEntity(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)); @@ -258,7 +266,7 @@ 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)); NBTTagCompound expected=encoded(corpse.getStackInSlot(6)); + 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"); @@ -266,16 +274,16 @@ private static void emptyRemoval() { } 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)); - NBTTagCompound input=data(source),immutable=input.copy(); + CompoundNBT input=data(source),immutable=input.copy(); EntityLootableBody target=body(); target.readAdditional(input); target.readAdditional(input); - RuntimeChecks.require(data(target).getTag("Equipment").equals(input.getTag("Equipment")) && data(target).getTag("Aux").equals(input.getTag("Aux")),"Repeated read appended overflow or changed slots"); - NBTTagCompound sparse=new NBTTagCompound();NBTTagList list=new NBTTagList();NBTTagCompound slot=encoded(item(19,23));slot.setByte("Slot",(byte)47);list.add(slot);sparse.setTag("Equipment",list); + 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).hasKey("Aux"),"Sparse reread kept stale inventory/overflow"); + RuntimeChecks.require((target.getStackInSlot(6)).isEmpty() && RuntimeChecks.sameStack(target.getStackInSlot(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() { - EntityPlayerMP player=player();EntityLootableBody corpse=body();CorpseContainer container=new CorpseContainer(player.inventory,corpse); + 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"); @@ -288,8 +296,8 @@ private static void staleAccess() { } 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)); - NBTTagCompound before=data(corpse);corpse.baseTick(); - RuntimeChecks.require(before.getTag("Equipment").equals(data(corpse).getTag("Equipment")),"Full inventory overwritten by overflow"); + CompoundNBT 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"); EntityLootableBody large=body();large.addItem(item(19,100)); Map expected=new TreeMap<>();add(expected,item(19,100)); @@ -300,10 +308,10 @@ private static void overflow() { 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).hasKey("Aux"),"Overflow remainder changed count or custom NBT"); + RuntimeChecks.require(RuntimeChecks.sameStack(queued.getStackInSlot(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));EntityPlayerMP a=player(),b=player(); + 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"); @@ -311,56 +319,56 @@ private static void twoViewers() { 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();EntityPlayerMP player=player();CorpseContainer container=new CorpseContainer(player.inventory,corpse); + 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.getChunk(corpse.getPosition()); + net.minecraft.world.chunk.Chunk chunk=world.getChunkAt(corpse.getPosition()); chunk.setLastSaveTime(world.getGameTime());chunk.setModified(false); - RuntimeChecks.require(!chunk.needsSaving(false),"Dirty-chunk test precondition"); + RuntimeChecks.require(!chunk.isModified(),"Dirty-chunk test precondition"); return chunk; } private static void dirtyInventory() { EntityLootableBody corpse=body();corpse.setInventorySlotContents(6,item(19,23)); - RuntimeChecks.require(world.spawnEntity(corpse),"Dirty-chunk corpse spawn"); + RuntimeChecks.require(world.addEntity(corpse),"Dirty-chunk corpse spawn"); net.minecraft.world.chunk.Chunk chunk=cleanChunk(corpse);corpse.markDirty(); - RuntimeChecks.require(chunk.needsSaving(false),"Inventory markDirty does not schedule a chunk save"); + RuntimeChecks.require(chunk.isModified(),"Inventory markDirty does not schedule a chunk save"); } private static void dirtyMutations() { - EntityLootableBody corpse=body();RuntimeChecks.require(world.spawnEntity(corpse),"Mutation corpse spawn"); + 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.needsSaving(false),"Slot insertion not scheduled for save"); - cleanChunk(corpse);corpse.decrStackSize(6,1);RuntimeChecks.require(chunk.needsSaving(false),"Extraction not scheduled for save"); - cleanChunk(corpse);corpse.removeStackFromSlot(6);RuntimeChecks.require(chunk.needsSaving(false),"Removal not scheduled for save"); - cleanChunk(corpse);corpse.initializeItems(new ItemStack[]{item(20,11)});RuntimeChecks.require(chunk.needsSaving(false),"Initial inventory not scheduled for save"); + 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.needsSaving(false),"Overflow insertion not scheduled for save"); + cleanChunk(corpse);corpse.addItem(item(70,7));RuntimeChecks.require(chunk.isModified(),"Overflow insertion not scheduled for save"); } - static void seedBlockedRelease(WorldServer target,File folder) throws Exception { + static void seedBlockedRelease(ServerWorld 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"); RuntimeChecks.awaitChunk(target,3,0); for(int i=0;i<48;i++)corpse.setInventorySlotContents(i,RuntimeChecks.stack(i)); corpse.addItem(RuntimeChecks.stack(70));corpse.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(corpse); - RuntimeChecks.require(target.spawnEntity(corpse),"Blocked-release fixture spawn"); + RuntimeChecks.require(target.addEntity(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();} finally {MinecraftForge.EVENT_BUS.unregister(reject);} - RuntimeChecks.require(corpse.isAlive() && target.loadedEntityList.contains(corpse) && expected.equals(inventory(corpse)),"Blocked-release fixture lost inventory/owner"); + 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 WorldServer delegate; int attempts; - RejectingWorld(WorldServer target) { - super(target.getSaveHandler(),target.getMapStorage(),target.getWorldInfo(),target.dimension.getType().create(),new Profiler(),false); + private final ServerWorld delegate; int attempts; + RejectingWorld(ServerWorld target) { + super(target.getWorldInfo(),target.dimension.getType(),(world,dimension)->target.getChunkProvider(),net.minecraft.profiler.EmptyProfiler.INSTANCE,false); delegate=target; } - @Override protected IChunkProvider createChunkProvider(){return delegate.getChunkProvider();} - @Override public boolean isChunkLoaded(int x,int z,boolean empty){return true;} + @Override public AbstractChunkProvider getChunkProvider(){return delegate.getChunkProvider();} + @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();} @@ -368,6 +376,17 @@ private static final class RejectingWorld extends World { @Override public net.minecraft.tags.NetworkTagManager getTags(){return delegate.getTags();} @Override public net.minecraft.server.MinecraftServer getServer(){return delegate.getServer();} @Override public long getGameTime(){return delegate==null?0:delegate.getGameTime();} - @Override public boolean spawnEntity(Entity entity){attempts++;return false;} + + @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 func_217406_a(String id){return delegate.func_217406_a(id);} + @Override public void func_217399_a(net.minecraft.world.storage.MapData data){delegate.func_217399_a(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;} } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java index 8d55dab..b837ba8 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java @@ -1,4 +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 cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; @@ -7,8 +13,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.UUID; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.block.Blocks; import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.server.MinecraftServer; @@ -16,7 +22,7 @@ import net.minecraft.world.*; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.network.NetworkRegistry; import net.minecraftforge.fml.network.simple.*; import net.minecraftforge.api.distmarker.Dist; @@ -39,20 +45,20 @@ 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 EntityPlayerMP player(String role){return server.getPlayerList().getPlayerByUsername("Container"+role);} + private ServerPlayerEntity player(String role){return server.getPlayerList().getPlayerByUsername("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"); - EntityPlayerMP a=player("A"),b=player("B"); + ServerPlayerEntity a=player("A"),b=player("B"); if(phase==0 && a!=null && b!=null){ - WorldServer w=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)); + ServerWorld w=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(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().setOrCreateGameRule("doMobSpawning","false",null); + 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.spawnEntity(body),"Shared corpse spawn"); + 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); setPhase(1); @@ -74,13 +80,13 @@ else if(phase==4 && ack("B",4)){ }else if(phase==7 && ack("A",7) && ack("B",7)){ File folder=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)).getSaveHandler().getWorldDirectory(); RuntimeChecks.saveExpected(folder,"multiplayer-body",body);savePlayer(folder,a,"A");savePlayer(folder,b,"B"); - server.getPlayerList().saveAllPlayerData();server.saveAllWorlds(false);mark("two_tcp_clients_final_gui_and_saved_nbt"); + server.getPlayerList().saveAllPlayerData();server.save(false,true,false);mark("two_tcp_clients_final_gui_and_saved_nbt"); setPhase(8); }else if(phase==8 && server.getPlayerList().getCurrentPlayerCount()==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(); + System.out.println("LOOTABLE_BODIES_MULTIPLAYER_PASS");finishing=true;server.initiateShutdown(false); } - }catch(Throwable error){error.printStackTrace();finishing=true;server.initiateShutdown();} + }catch(Throwable error){error.printStackTrace();finishing=true;server.initiateShutdown(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)); @@ -90,23 +96,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(EntityPlayerMP p,int index){int n=0;ItemStack expected=RuntimeChecks.stack(index);for(int i=0;i results=new ArrayList<>(); private static int failures; - static void run(MinecraftServer server,WorldServer target,File folder) throws Exception { + static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { world=target;results.clear();failures=0; - INBTBase[] wrong={new NBTTagString("not a timer"),new NBTTagFloat(1.5F),new NBTTagDouble(Double.NaN),new NBTTagDouble(Double.POSITIVE_INFINITY),new NBTTagCompound(),new NBTTagList(),new NBTTagByteArray(new byte[]{1}),new NBTTagIntArray(new int[]{1})}; + INBT[] wrong={new StringNBT("not a timer"),new FloatNBT(1.5F),new DoubleNBT(Double.NaN),new DoubleNBT(Double.POSITIVE_INFINITY),new CompoundNBT(),new ListNBT(),new ByteArrayNBT(new byte[]{1}),new IntArrayNBT(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,new NBTTagLong(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,new LongNBT(value))); } - for(int i=0;ibadName("Name",value));} - check("Name_over_packet_string_limit",()->badName("Name",new NBTTagString(repeat('n',32768)))); - check("Name_utf8_bytes_over_packet_limit",()->badName("Name",new NBTTagString(repeat('\u20ac',10923)))); - check("CustomName_utf8_bytes_over_packet_limit",()->badName("CustomName",new NBTTagString(repeat('\u20ac',10923)))); + for(int i=0;ibadName("Name",value));} + check("Name_over_packet_string_limit",()->badName("Name",new StringNBT(repeat('n',32768)))); + check("Name_utf8_bytes_over_packet_limit",()->badName("Name",new StringNBT(repeat('\u20ac',10923)))); + check("CustomName_utf8_bytes_over_packet_limit",()->badName("CustomName",new StringNBT(repeat('\u20ac',10923)))); check("name_exact_packet_byte_limit_preserved",NameLifetimeChecks::packetBoundary); - check("old_maximum_name_remains_recoverable_when_native_json_cannot_fit",()->{NBTTagCompound n=base();String original=repeat('a',32767);n.setString("Name",original);EntityLootableBody b=read(n);RuntimeChecks.require(b.getCustomNameTag().equals("Saved Owner"),"Unencodable native name entered metadata");retained(b,"Name",new NBTTagString(original));stable(b);}); - check("CustomName_wrong_type_retained",()->badName("CustomName",new NBTTagInt(27))); - check("CustomName_over_packet_string_limit",()->badName("CustomName",new NBTTagString(repeat('c',32768)))); + 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",new StringNBT(original));stable(b);}); + check("CustomName_wrong_type_retained",()->badName("CustomName",new IntNBT(27))); + check("CustomName_over_packet_string_limit",()->badName("CustomName",new StringNBT(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); @@ -68,51 +80,51 @@ static void run(MinecraftServer server,WorldServer target,File folder) throws Ex 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<>(world.loadedEntityList); - try{world.getWorldInfo().setWorldTotalTime(10000);LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1;test.run();results.add("PASS "+name);System.out.println("NAME_LIFETIME_PASS "+name);} + Set before=new HashSet<>(FixtureApi.entities(world)); + try{world.getWorldInfo().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{world.getWorldInfo().setWorldTotalTime(clock);LootableBodies.ticksPerItemDecay=rate;LootableBodies.corpseDecayTime=life;LootableBodies.allowCorpseDecay=decay;LootableBodies.decayOnlyWhenEmpty=empty;for(Entity e:new ArrayList(world.loadedEntityList))if(!before.contains(e))world.removeEntityDangerously(e);} + finally{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);} } - static NBTTagCompound 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));NBTTagCompound n=new NBTTagCompound();b.writeUnlessPassenger(n);n.setString("Name","Saved Owner");n.setByte("Vac",(byte)20);return n;} - static EntityLootableBody read(NBTTagCompound n){NBTTagCompound copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.read(n);RuntimeChecks.require(same(n,copy),"Input NBT mutated");return b;} - static NBTTagCompound saved(EntityLootableBody b){NBTTagCompound n=new NBTTagCompound();b.writeUnlessPassenger(n);return n;} - static void retained(EntityLootableBody b,String key,INBTBase raw){NBTTagList 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){world.getWorldInfo().setWorldTotalTime(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 missingName(){EntityLootableBody b=read(base());b.baseTick();CompoundNBT n=base();n.remove("Name");n.remove("CustomName");b.read(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();CompoundNBT n=saved(b);n.putByte("Vac",(byte)3);b.setUserName("");b.writeAdditional(n);RuntimeChecks.require(!n.contains("Name") && !n.contains("Vac"),"Writer resurrected old scalar fields");} + private static void clearedCustomName(){EntityLootableBody b=read(base());b.baseTick();CompoundNBT n=saved(b);b.setUserName("");b.writeUnlessPassenger(n);RuntimeChecks.require(!n.contains("CustomName") && read(n).getCustomNameTag().isEmpty(),"Reused vanilla CustomName restored cleared attribution");} + private static void blankName(){CompoundNBT 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());CompoundNBT n=base();n.remove("DeathTime");n.remove("DecayTime");n.remove("Vac");world.getWorldInfo().setGameTime(11000);b.read(n);CompoundNBT 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(){CompoundNBT 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){INBT[] values={new ByteNBT((byte)10),new ShortNBT((short)10),new IntNBT(10),new LongNBT(10)};CompoundNBT n=base();for(String k:new String[]{"Vac","DeathTime","DecayTime"})n.put(k,values[type].copy());EntityLootableBody b=read(n);CompoundNBT 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(){CompoundNBT n=base();for(String k:new String[]{"Vac","DeathTime","DecayTime"})n.putLong(k,0);EntityLootableBody b=read(n);CompoundNBT 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(){CompoundNBT n=base();n.putLong("Vac",20);RuntimeChecks.require(!saved(read(n)).contains(RECOVERY),"Valid limit rejected");invalid("Vac",new IntNBT(21));} + private static EntityLootableBody timed(long timestamp){CompoundNBT n=base();n.putLong("DeathTime",timestamp);n.putLong("DecayTime",timestamp);return read(n);} + private static void future(){CompoundNBT n=base();n.putLong("DeathTime",Long.MAX_VALUE);n.putLong("DecayTime",Long.MAX_VALUE);EntityLootableBody b=read(n);LootableBodies.ticksPerItemDecay=10;world.getWorldInfo().setGameTime(10011);b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==10010,"Future timestamp stalled normal progression");} + private static void rollback(){EntityLootableBody b=timed(9999);world.getWorldInfo().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);world.getWorldInfo().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){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 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");world.getWorldInfo().setWorldTotalTime(11);b.baseTick();RuntimeChecks.require(b.getStackInSlot(6).getDamage()==18 && termination(b)>=0,"Elapsed threshold ignored");} + 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");world.getWorldInfo().setGameTime(11);b.baseTick();RuntimeChecks.require(b.getStackInSlot(6).getDamage()==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 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(){NBTTagCompound n=base();n.setTag("Equipment",new NBTTagList());n.setString("DeathTime","opaque");EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Scalar evidence counted as inventory");LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=true;LootableBodies.corpseDecayTime=40;world.getWorldInfo().setWorldTotalTime(10041);b.baseTick();RuntimeChecks.require(termination(b)>=0,"Scalar evidence made empty corpse immortal");} - private static void inventory(){NBTTagCompound n=base();NBTTagCompound missing=NbtRecoveryChecks.entry(121,11,7);missing.setString("id","absentfixture:missing");n.getList("Equipment",10).add(missing);n.setString("DeathTime","bad");n.setTag("Name",new NBTTagCompound());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(){NBTTagCompound n=base(),record=new NBTTagCompound();record.setString("Future","opaque");n.setTag(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(){NBTTagCompound n=base();NBTTagString raw=new NBTTagString("opaque");n.setTag(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).hasKey(RECOVERY),"Healthy save gained invalid-field data");} - // NBTTagDouble.equals uses ==, so NaN copies are unequal without any mutation. - static boolean same(INBTBase a,INBTBase b){if(a==null || b==null)return a==b;return Arrays.equals(encoded(a),encoded(b));} - private static byte[] encoded(INBTBase value){try{NBTTagCompound n=new NBTTagCompound();n.setTag("Value",value);ByteArrayOutputStream out=new ByteArrayOutputStream();CompressedStreamTools.write(n,new DataOutputStream(out));return out.toByteArray();}catch(IOException 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;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=new StringNBT("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){if(a==null || b==null)return a==b;return Arrays.equals(encoded(a),encoded(b));} + private static byte[] encoded(INBT value){try{CompoundNBT n=new CompoundNBT();n.put("Value",value);ByteArrayOutputStream out=new ByteArrayOutputStream();CompressedStreamTools.write(n,new DataOutputStream(out));return out.toByteArray();}catch(IOException e){throw new AssertionError(e);}} 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 66b70c1..c67fc33 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java @@ -1,4 +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 cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; @@ -8,16 +11,16 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiMainMenu; +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.init.Blocks; -import net.minecraft.init.Items; +import net.minecraft.block.Blocks; +import net.minecraft.item.Items; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.EnumHand; +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; @@ -37,7 +40,7 @@ final class NamelessCorpseClientChecks { private boolean originalUnicode, originalBidi; private boolean renderedBody; - @SubscribeEvent public void rendered(RenderLivingEvent.Post event) { + @SubscribeEvent public void rendered(RenderLivingEvent.Post event) { if(event.getEntity().getUniqueID().equals(BODY)) renderedBody=true; } @@ -62,52 +65,52 @@ private static void assertLocale(Minecraft mc, String code) throws Exception { RuntimeChecks.disableVacuum(body); for (int slot=0;slot<48;slot++) body.setInventorySlotContents(slot,RuntimeChecks.stack(slot)); body.addItem(RuntimeChecks.stack(70)); - NBTTagCompound data = NameLifetimeChecks.saved(body); - NBTTagCompound retained = new NBTTagCompound(); - retained.setString("Source","FutureFixture"); retained.setString("Data","retain verbatim"); - data.setTag("LootableBodiesUnresolved",NbtRecoveryChecks.list(retained)); - body = (EntityLootableBody)EntityType.create(data,mc.world); + CompoundNBT data = NameLifetimeChecks.saved(body); + CompoundNBT retained = new CompoundNBT(); + retained.putString("Source","FutureFixture"); retained.putString("Data","retain verbatim"); + data.put("LootableBodiesUnresolved",NbtRecoveryChecks.list(retained)); + body = (EntityLootableBody)FixtureApi.loadEntity(data,mc.world); RuntimeChecks.require(body != null && new ResourceLocation("lootablebodies:corpse").equals(body.getType().getRegistryName()), "Registry/display identity changed"); - NBTTagCompound before = NameLifetimeChecks.saved(body); + CompoundNBT 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.hasKey("Name") && !before.hasKey("CustomName"), "Localized fallback became an owner name"); + 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)EntityType.create(before.copy(),mc.world); + EntityLootableBody loaded = (EntityLootableBody)FixtureApi.loadEntity(before.copy(),mc.world); 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); NBTTagCompound named = NameLifetimeChecks.saved(body); + body.setUserName(owner); CompoundNBT 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(""); NBTTagCompound cleared = NameLifetimeChecks.saved(body); + body.setUserName(""); CompoundNBT 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); - NBTTagCompound eggBefore = egg.write(new NBTTagCompound()); + 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 NBTTagCompound())), "Egg name lookup mutated its native ID/NBT"); + RuntimeChecks.require(NameLifetimeChecks.same(eggBefore,egg.write(new CompoundNBT())), "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 GuiMainMenu) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.limitFramerate=30;mc.gameSettings.renderDistanceChunks=4; + 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(); MinecraftForge.EVENT_BUS.register(this); mc.launchIntegratedServer("name-fixture","Name fixture",new WorldSettings(4267321,GameType.CREATIVE,false,false,WorldType.FLAT));stage=1; } else if(stage==1 && mc.world!=null && mc.player!=null && mc.getIntegratedServer()!=null) { - pending=mc.getIntegratedServer().addScheduledTask(() -> { - WorldServer world=mc.getIntegratedServer().getWorld(net.minecraft.world.dimension.DimensionType.getById(0)); - world.getGameRules().setOrCreateGameRule("doMobSpawning","false",null); + pending=mc.getIntegratedServer().runAsync(() -> { + ServerWorld world=mc.getIntegratedServer().getWorld(net.minecraft.world.dimension.DimensionType.getById(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.spawnEntity(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.spawnEntity(named); - net.minecraft.entity.player.EntityPlayerMP player=mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID()); + 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); });stage=2;frames=0; } else if(stage==2 && pending.isDone() && ++frames>30) { @@ -138,9 +141,9 @@ void tick(Minecraft mc) throws Exception { mc.fontRenderer.setBidiFlag(originalBidi);stage=4; }} } else if(stage==4) { - for(Entity entity:mc.world.loadedEntityList)if(entity.getUniqueID().equals(BODY)) { + for(Entity entity:FixtureApi.entities(mc.world))if(entity.getUniqueID().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,EnumHand.MAIN_HAND);stage=5;frames=0;break; + mc.playerController.interactWithEntity(mc.player,entity,Hand.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); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java index 307bb9f..cf5b76e 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java @@ -1,4 +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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -6,14 +11,14 @@ import java.util.*; import java.util.concurrent.Future; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.screen.MainMenuScreen; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; -import net.minecraft.inventory.ClickType; +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.EnumHand; +import net.minecraft.util.Hand; import net.minecraft.util.ScreenShotHelper; import net.minecraft.util.math.BlockPos; import net.minecraft.world.GameType; @@ -25,14 +30,14 @@ private interface Action {void run() throws Exception;} private UUID playerId; private int stage,frames; private Future pending; - private NBTTagCompound visible; + private CompoundNBT visible; private Map before; - private NBTTagList expectedPlayer; + private ListNBT expectedPlayer; void start(Minecraft mc){playerId=mc.player.getUniqueID();} boolean tick(Minecraft mc) throws Exception { frames++;RuntimeChecks.require(frames<300,"NBT graphical stage timeout "+stage); if(stage==0){schedule(mc,()->{ - EntityPlayerMP p=player(mc);p.inventory.clear();p.inventory.setItemStack(ItemStack.EMPTY);p.setGameType(GameType.SURVIVAL); + 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"); });next(1);} @@ -47,20 +52,20 @@ else if(stage==30 && done() && frames>20){schedule(mc,()->{ 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==6 && frames>25){schedule(mc,()->{ Map 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==90 && done() && frames>20){schedule(mc,()->{ body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"nbt-client-after",body(mc)); - RuntimeChecks.require(Objects.equals(visible.getTag(NbtRecoveryChecks.RECOVERY),NbtRecoveryChecks.saved(body(mc)).getTag(NbtRecoveryChecks.RECOVERY)),"Unresolved raw data changed on graphical disk reload"); - NbtWorldChecks.verifyMetadata(body(mc));RuntimeChecks.require(NameLifetimeChecks.same(visible.getTag(NameLifetimeChecks.RECOVERY),full(body(mc)).getTag(NameLifetimeChecks.RECOVERY)),"Raw scalar evidence changed on graphical reload"); - RuntimeChecks.require(expectedPlayer.equals(player(mc).inventory.write(new NBTTagList())),"Recovered player NBT changed on graphical disk reload");checkPlayer(player(mc).inventory);mark(mc,"malformed_corpse_and_player_disk_reload"); + 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"); });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;} @@ -68,24 +73,24 @@ else if(stage==90 && done() && frames>20){schedule(mc,()->{ 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:mc.world.loadedEntityList)if(entity instanceof EntityLootableBody)System.out.println("NBT_GUI_CLIENT_BODY uuid="+entity.getUniqueID()+" position="+entity.getPosition()+" id="+entity.getEntityId()); + 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())); } } return false; } - private void verifyGui(Minecraft mc){ItemStack[] expected=new ItemStack[48];NBTTagList 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;NBTTagCompound n=s.write(new NBTTagCompound());n.removeTag("Count");String key=n.toString();map.put(key,map.getOrDefault(key,0)+s.getCount());} + 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 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 NBTTagCompound full(EntityLootableBody b){NBTTagCompound n=new NBTTagCompound();b.writeUnlessPassenger(n);return n;} - private EntityPlayerMP player(Minecraft mc){return mc.getIntegratedServer().getPlayerList().getPlayerByUUID(playerId);} + 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(net.minecraft.world.dimension.DimensionType.getById(0)),bodyId);} - private File folder(Minecraft mc){return player(mc).world.getSaveHandler().getWorldDirectory();} + private File folder(Minecraft mc){return player(mc).getServerWorld().getSaveHandler().getWorldDirectory();} 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:mc.world.loadedEntityList)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,EnumHand.MAIN_HAND);return true;}return false;} - private void schedule(Minecraft mc,Action action){pending=mc.getIntegratedServer().addScheduledTask(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} + 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 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.mainWindow.getFramebufferWidth(),mc.mainWindow.getFramebufferHeight(),mc.getFramebuffer(),message->System.out.println(message.getString()));} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java index bede164..b72c85f 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java @@ -1,51 +1,61 @@ 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 cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; import net.minecraft.entity.Entity; -import net.minecraft.entity.item.EntityItem; +import net.minecraft.entity.item.ItemEntity; import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; /** 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 WorldServer world; + private static ServerWorld world; private static int failures; private static final List results=new ArrayList<>(); - static void run(MinecraftServer server,WorldServer target,File folder) throws Exception { + static void run(MinecraftServer server,ServerWorld 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(new NBTTagInt(s)));} + for(int slot:new int[]{-1,48,127,256}) {final int s=slot;check("invalid_slot_"+s,()->invalidSlot(new IntNBT(s)));} check("missing_slot_does_not_default_to_helmet",()->invalidSlot(null)); - check("string_slot_is_not_coerced_to_zero",()->invalidSlot(new NBTTagString("7"))); - check("fractional_slot_is_not_truncated",()->invalidSlot(new NBTTagFloat(6.5F))); - check("long_slot_is_not_narrowed",()->invalidSlot(new NBTTagLong(4294967302L))); + check("string_slot_is_not_coerced_to_zero",()->invalidSlot(new StringNBT("7"))); + check("fractional_slot_is_not_truncated",()->invalidSlot(new FloatNBT(6.5F))); + check("long_slot_is_not_narrowed",()->invalidSlot(new LongNBT(4294967302L))); check("valid_wide_slot_is_read_without_byte_wrap",NbtRecoveryChecks::validWideSlot); - check("missing_equipment_item_raw_retained",()->unreadable("Equipment","id",new NBTTagString("absentfixture:missing"))); - check("missing_aux_item_raw_retained",()->unreadable("Aux","id",new NBTTagString("absentfixture:missing"))); - check("air_item_is_not_a_replacement",()->unreadable("Equipment","id",new NBTTagString("minecraft:air"))); - check("numeric_item_id_not_guessed",()->unreadable("Equipment","id",new NBTTagShort((short)264))); + check("missing_equipment_item_raw_retained",()->unreadable("Equipment","id",new StringNBT("absentfixture:missing"))); + check("missing_aux_item_raw_retained",()->unreadable("Aux","id",new StringNBT("absentfixture:missing"))); + check("air_item_is_not_a_replacement",()->unreadable("Equipment","id",new StringNBT("minecraft:air"))); + check("numeric_item_id_not_guessed",()->unreadable("Equipment","id",new ShortNBT((short)264))); check("missing_count_retained_raw",()->unreadable("Equipment","Count",null)); - check("zero_count_retained_raw",()->unreadable("Equipment","Count",new NBTTagByte((byte)0))); - check("negative_count_retained_raw",()->unreadable("Equipment","Count",new NBTTagByte((byte)-1))); - check("wide_count_does_not_wrap_to_zero",()->unreadable("Equipment","Count",new NBTTagInt(256))); - check("string_count_retained_raw",()->unreadable("Equipment","Count",new NBTTagString("17"))); - check("fractional_count_retained_raw",()->unreadable("Equipment","Count",new NBTTagFloat(17.5F))); + check("zero_count_retained_raw",()->unreadable("Equipment","Count",new ByteNBT((byte)0))); + check("negative_count_retained_raw",()->unreadable("Equipment","Count",new ByteNBT((byte)-1))); + check("wide_count_does_not_wrap_to_zero",()->unreadable("Equipment","Count",new IntNBT(256))); + check("string_count_retained_raw",()->unreadable("Equipment","Count",new StringNBT("17"))); + check("fractional_count_retained_raw",()->unreadable("Equipment","Count",new FloatNBT(17.5F))); check("valid_integer_count_preserved",NbtRecoveryChecks::integerCount); - check("wrong_damage_type_retained_raw",()->unreadable("Equipment","Damage",new NBTTagString("17"))); - check("negative_damage_not_reset",()->unreadable("Equipment","Damage",new NBTTagShort((short)-1))); - check("wide_damage_not_narrowed",()->unreadable("Equipment","Damage",new NBTTagInt(32768))); - check("wrong_custom_tag_type_retained_raw",()->unreadable("Equipment","tag",new NBTTagString("opaque"))); + check("wrong_damage_type_retained_raw",()->unreadable("Equipment","Damage",new StringNBT("17"))); + check("negative_damage_not_reset",()->unreadable("Equipment","Damage",new ShortNBT((short)-1))); + check("wide_damage_not_narrowed",()->unreadable("Equipment","Damage",new IntNBT(32768))); + check("wrong_custom_tag_type_retained_raw",()->unreadable("Equipment","tag",new StringNBT("opaque"))); check("unknown_capability_payload_retained_raw",NbtRecoveryChecks::unknownCaps); - check("unknown_stack_root_data_retained_raw",()->unreadable("Equipment","UnknownPayload",new NBTTagString("retain verbatim"))); + check("unknown_stack_root_data_retained_raw",()->unreadable("Equipment","UnknownPayload",new StringNBT("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); @@ -72,49 +82,49 @@ static void run(MinecraftServer server,WorldServer target,File folder) throws Ex RuntimeChecks.require(failures==0,"NBT recovery regressions: "+failures+" / "+results.size());System.out.println("LOOTABLE_BODIES_NBT_PASS checks="+results.size()); } private static void check(String name,Check test){ - Set before=new HashSet<>(world.loadedEntityList); + Set before=new HashSet<>(FixtureApi.entities(world)); try{test.run();results.add("PASS "+name);System.out.println("NBT_CHECK_PASS "+name);} catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("NBT_CHECK_FAIL "+name+": "+e);e.printStackTrace();} - finally{for(Entity e:new ArrayList(world.loadedEntityList))if(!before.contains(e))world.removeEntityDangerously(e);} + 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 NBTTagCompound entry(int index,int count,int slot){NBTTagCompound n=item(index,count).write(new NBTTagCompound());n.setByte("Slot",(byte)slot);return n;} - static NBTTagList list(NBTTagCompound... entries){NBTTagList l=new NBTTagList();for(NBTTagCompound n:entries)l.add(n.copy());return l;} - static NBTTagCompound root(NBTTagCompound... entries){NBTTagCompound n=new NBTTagCompound();n.setTag("Equipment",list(entries));n.setByte("Vac",(byte)20);return n;} - static NBTTagCompound saved(EntityLootableBody b){NBTTagCompound n=new NBTTagCompound();b.writeAdditional(n);return n;} - static EntityLootableBody read(NBTTagCompound n){NBTTagCompound copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.readAdditional(n);RuntimeChecks.require(n.equals(copy),"Reader modified source NBT");return b;} + 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;} 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;NBTTagCompound n=s.write(new NBTTagCompound());n.removeTag("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));NBTTagList 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).getTag(RECOVERY));} - static void retained(EntityLootableBody b,INBTBase raw){NBTTagList records=saved(b).getList(RECOVERY,10);boolean found=false;for(int i=0;i 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=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.spawnEntity(b),"Raw fixture spawn"); - NBTTagCompound n=new NBTTagCompound();RuntimeChecks.require(b.writeUnlessPassenger(n),"Raw fixture serialization"); + 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);} } private static ItemStack known(int i){return i==6?NbtRecoveryChecks.item(120,17):NbtRecoveryChecks.item(i,i<6?1:64);} - private static NBTTagCompound rawEquipment(){ - NBTTagCompound n=new NBTTagCompound();NBTTagList equipment=new NBTTagList(); + private static CompoundNBT rawEquipment(){ + CompoundNBT n=new CompoundNBT();ListNBT equipment=new ListNBT(); for(int i=0;i<48;i++) { - NBTTagCompound entry=known(i).write(new NBTTagCompound());entry.setByte("Slot",(byte)i); - if(i==7){entry=NbtRecoveryChecks.entry(123,9,7);entry.setString("id",RETURNING);} + CompoundNBT entry=known(i).write(new CompoundNBT());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.setByte("Count",(byte)0);} - if(i==10){entry=NbtRecoveryChecks.entry(129,6,10);NBTTagCompound caps=new NBTTagCompound();caps.setString("absentfixture:inventory","opaque capabilities");entry.setTag("ForgeCaps",caps);} + 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);} equipment.add(entry); } - equipment.add(NbtRecoveryChecks.entry(121,11,6));NBTTagCompound bad=NbtRecoveryChecks.entry(122,13,6);bad.setInt("Slot",999);equipment.add(bad);n.setTag("Equipment",equipment); - n.setTag("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.setInt("Vac",256); - n.setLong("DeathTime",Long.MIN_VALUE); - n.setDouble("DecayTime",Double.NaN); - NBTTagCompound name=new NBTTagCompound();name.setString("Opaque","saved malformed attribution");n.setTag("Name",name); - NBTTagCompound health=HealthAttributeChecks.malformedFields();for(String key:health.keySet())n.setTag(key,health.getTag(key).copy()); - NBTTagCompound effects=EffectHurtChecks.malformedFields();for(String key:effects.keySet())n.setTag(key,effects.getTag(key).copy()); - NBTTagCompound rotation=RotationFallChecks.malformedFields();for(String key:rotation.keySet())n.setTag(key,rotation.getTag(key).copy()); + 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); + 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()); return n; } - private static NBTTagCompound missing(int index,int count,int slot){NBTTagCompound n=NbtRecoveryChecks.entry(index,count,slot);n.setString("id","absentfixture:missing");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 final class RawBody extends EntityLootableBody { RawBody(World w){super(w);} - @Override public boolean writeUnlessPassenger(NBTTagCompound n){if(removed)return false;n.setString("id","lootablebodies:corpse");writeWithoutTypeId(n);return true;} - @Override public void writeAdditional(NBTTagCompound n){super.writeAdditional(n);NBTTagCompound raw=rawEquipment();for(String key:raw.keySet())n.setTag(key,raw.getTag(key).copy());n.setTag("Pos",PositionMotionChecks.vector(Double.NaN,80,posZ));n.setTag("Motion",PositionMotionChecks.vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY));} + @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,posZ));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.posX==coordinate+8.5 && b.posY==80 && b.posZ==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){NBTTagCompound n=s.write(new NBTTagCompound());n.removeTag("Count");String key=n.toString();totals.put(key,totals.getOrDefault(key,0)+s.getCount());} + 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());} 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){NBTTagCompound raw=NbtRecoveryChecks.entry(123,9,7);raw.setString("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){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);} RuntimeChecks.require(expected.equals(NbtRecoveryChecks.items(b)),"Disk recovery item/count/damage/custom NBT lost or duplicated"); - NBTTagCompound source=rawEquipment();int rawCount=0; - for(String list:new String[]{"Equipment","Aux"}){NBTTagList 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"); - for(String key:new String[]{"Name","Vac","DeathTime","DecayTime"})NameLifetimeChecks.retained(b,key,raw.getTag(key)); + RuntimeChecks.require(!saved.contains("Vac") && saved.getLong("DeathTime")>=0 && saved.getLong("DeathTime")<=b.world.getGameTime() && saved.getLong("DecayTime")>=0 && saved.getLong("DecayTime")<=b.world.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);NBTTagCompound expected;try(FileInputStream in=new FileInputStream(new File(folder,"nbt-"+name+".nbt"))){expected=CompressedStreamTools.readCompressed(in);} - RuntimeChecks.require(Objects.equals(expected.getTag(NbtRecoveryChecks.RECOVERY),NbtRecoveryChecks.saved(b).getTag(NbtRecoveryChecks.RECOVERY)),"Raw recovery NBT changed on actual disk reload"); - RuntimeChecks.require(NameLifetimeChecks.same(expected.getTag(NameLifetimeChecks.RECOVERY),NbtRecoveryChecks.saved(b).getTag(NameLifetimeChecks.RECOVERY)),"Raw scalar data changed on actual disk reload"); + 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);} + 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 1ccc1d1..3204ccb 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java @@ -1,16 +1,22 @@ 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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.events.PlayerDeathEventHandler; import java.util.*; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Items; +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.server.MinecraftServer; import net.minecraft.util.DamageSource; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.WorldType; +import net.minecraft.world.GameType; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayerFactory; import com.mojang.authlib.GameProfile; @@ -24,16 +30,32 @@ private interface Check { void run() throws Exception; } private static int failures; private static void check(String name, Check action) { try { action.run(); System.out.println("PORT_BASELINE_PASS " + name); } - catch (Throwable error) { failures++; System.out.println("PORT_BASELINE_FAIL " + name + " " + error); } + catch (Throwable error) { failures++; System.out.println("PORT_BASELINE_FAIL " + name + " " + error); error.printStackTrace(); } } private static void require(boolean ok, String message) { if (!ok) throw new AssertionError(message); } public static void run(MinecraftServer server) throws Exception { failures=0; - WorldServer world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)); + ServerWorld world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)); + check("native_received_packet_observer_is_non_destructive", () -> { + net.minecraft.network.PacketBuffer buffer=new net.minecraft.network.PacketBuffer(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); + 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(net.minecraft.world.dimension.DimensionType.OVERWORLD,WorldType.DEFAULT,GameType.SURVIVAL); + outgoing.writePacketData(buffer); + net.minecraft.network.play.server.SRespawnPacket incoming=new net.minecraft.network.play.server.SRespawnPacket();incoming.readPacketData(buffer); + require(RespawnPacketTrace.describe(outgoing).equals(RespawnPacketTrace.describe(incoming)),"Respawn observer differs before/after native decoding"); + } finally {buffer.release();} + }); check("rejected_spawn_keeps_death_inventory", () -> { - EntityPlayerMP player=FakePlayerFactory.get(world,new GameProfile(UUID.randomUUID(),"PortFixture")); - ArrayList drops=new ArrayList<>(); - drops.add(new EntityItem(world,8,80,8,new ItemStack(Items.DIAMOND,7))); + ServerPlayerEntity 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) { if(e.getEntity() instanceof EntityLootableBody) e.setCanceled(true); }}; @@ -44,36 +66,36 @@ public static void run(MinecraftServer server) throws Exception { }); check("duplicate_slots_preserve_both_records", () -> { EntityLootableBody body=new EntityLootableBody(world); - NBTTagCompound nbt=new NBTTagCompound(); body.writeAdditional(nbt); - NBTTagList equipment=new NBTTagList(); - for(int i=0;i<2;i++) { NBTTagCompound item=new ItemStack(Items.DIAMOND,i+2).write(new NBTTagCompound()); item.setByte("Slot",(byte)10); equipment.add(item); } - nbt.setTag("Equipment",equipment); body.readAdditional(nbt); - NBTTagCompound saved=new NBTTagCompound();body.writeAdditional(saved); + 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); int count=0; for(String key:Arrays.asList("Equipment","Aux")) { - NBTTagList list=saved.getList(key,10); + ListNBT list=saved.getList(key,10); for(int i=0;i { - EntityLootableBody body=new EntityLootableBody(world); NBTTagCompound nbt=new NBTTagCompound();body.writeAdditional(nbt); - NBTTagCompound item=new ItemStack(Items.DIAMOND,7).write(new NBTTagCompound());item.setString("id","missingfixture:keepsake");item.setByte("Slot",(byte)10); - NBTTagList list=new NBTTagList();list.add(item);nbt.setTag("Equipment",list);body.readAdditional(nbt); - NBTTagCompound saved=new NBTTagCompound();body.writeAdditional(saved); + 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); 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); NBTTagCompound nbt=new NBTTagCompound();body.writeAdditional(nbt); - NBTTagCompound item=new ItemStack(Items.EMERALD,9).write(new NBTTagCompound());item.setInt("Slot",12345); - NBTTagList list=new NBTTagList();list.add(item);nbt.setTag("Equipment",list);body.readAdditional(nbt); - NBTTagCompound saved=new NBTTagCompound();body.writeAdditional(saved); + 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); require(!body.isEmpty(),"invalid slot discarded inventory"); }); check("malformed_name_and_timer_are_retained", () -> { - EntityLootableBody body=new EntityLootableBody(world); NBTTagCompound nbt=new NBTTagCompound();body.writeAdditional(nbt); - nbt.setTag("Name",new NBTTagInt(17));nbt.setDouble("DeathTime",Double.NaN);body.readAdditional(nbt); - NBTTagCompound saved=new NBTTagCompound();body.writeAdditional(saved); + EntityLootableBody body=new EntityLootableBody(world); CompoundNBT nbt=new CompoundNBT();body.writeAdditional(nbt); + nbt.put("Name",new IntNBT(17));nbt.putDouble("DeathTime",Double.NaN);body.readAdditional(nbt); + CompoundNBT saved=new CompoundNBT();body.writeAdditional(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 308d505..cefc090 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java @@ -1,4 +1,17 @@ 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 cyano.lootable.entities.EntityLootableBody; import java.io.*; @@ -8,22 +21,22 @@ import net.minecraft.nbt.*; import net.minecraft.server.MinecraftServer; import net.minecraft.util.math.AxisAlignedBB; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import net.minecraft.world.chunk.storage.ChunkLoader; /** Validate before vanilla builds geometry, then exercise actual chunk admission. */ final class PositionMotionChecks { private interface Check {void run() throws Exception;} - private static WorldServer world; + private static ServerWorld world; private static int failures; private static final List results=new ArrayList<>(); - static void run(MinecraftServer server,WorldServer target,File folder) throws Exception { + static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { world=target;failures=0;results.clear(); - INBTBase[] wrong={new NBTTagString("opaque"),new NBTTagCompound(),new NBTTagByteArray(new byte[]{1,2,3}),new NBTTagIntArray(new int[]{1,2,3}),new NBTTagDouble(Double.NaN)}; + INBT[] wrong={new StringNBT("opaque"),new CompoundNBT(),new ByteArrayNBT(new byte[]{1,2,3}),new IntArrayNBT(new int[]{1,2,3}),new DoubleNBT(Double.NaN)}; for(String key:new String[]{"Pos","Motion"}) { - for(int i=0;ibadVector(key,raw));} - for(INBTBase raw:new INBTBase[]{new NBTTagString("opaque"),new NBTTagCompound(),new NBTTagByteArray(new byte[]{1})})check(key+"_wrong_element_type_"+raw.getId(),()->{NBTTagList list=new NBTTagList();for(int i=0;i<3;i++)list.add(raw.copy());badVector(key,list);}); + for(int i=0;ibadVector(key,raw));} + for(INBT raw:new INBT[]{new StringNBT("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 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));} @@ -50,33 +63,33 @@ static void run(MinecraftServer server,WorldServer 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 NBTTagList vector(double... values){NBTTagList l=new NBTTagList();for(double v:values)l.add(new NBTTagDouble(v));return l;} - private static NBTTagCompound saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} - private static NBTTagCompound 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.motionX=.25;b.motionY=-.5;b.motionZ=.75;return saved(b);} - private static EntityLootableBody read(NBTTagCompound n){NBTTagCompound before=n.copy();Entity e=EntityType.create(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.posX,b.posY,b.posZ,b.motionX,b.motionY,b.motionZ,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.posX-b.width/2))<1e-6 && a.minY==b.posY && Math.abs(a.maxZ-(b.posZ+b.width/2))<1e-6,"Bounding box differs from sanitized position");} - private static double[] values(EntityLootableBody b,String key){return key.equals("Pos")?new double[]{b.posX,b.posY,b.posZ}:new double[]{b.motionX,b.motionY,b.motionZ};} + static ListNBT vector(double... values){ListNBT l=new ListNBT();for(double v:values)l.add(new DoubleNBT(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.posX,b.posY,b.posZ,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.posX-b.getWidth()/2))<1e-6 && a.minY==b.posY && Math.abs(a.maxZ-(b.posZ+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.posX,b.posY,b.posZ}:new double[]{b.getMotion().x,b.getMotion().y,b.getMotion().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){NBTTagCompound n=base(),a=saved(b);for(String k:new String[]{"Equipment","Aux"})require(NameLifetimeChecks.same(n.getTag(k),a.getTag(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);NBTTagCompound a=saved(b),z=saved(read(a));for(String k:new String[]{"Pos","Motion","Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY})require(NameLifetimeChecks.same(a.getTag(k),z.getTag(k)),"Round trip changed "+k);} - private static void badVector(String key,INBTBase raw){NBTTagCompound n=base();n.setTag(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;NBTTagCompound n=base();NBTTagList raw=vector(v);n.setTag(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)); + 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()); EntityLootableBody body=body(world,coordinate,-1); LootableBodies.voidPlatform=false;body.baseTick(); @@ -30,24 +30,24 @@ static void run(MinecraftServer server) throws Exception { 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"); conserved(body);checks++; } - WorldServer world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0));world.getChunk(30,30); + ServerWorld world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(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()); EntityLootableBody body=body(world,488,80); RuntimeChecks.require(body.handleWaterMovement(),"Water fixture was not submerged"); - LootableBodies.corpseBuoyancy=false;body.motionY=0;body.baseTick(); - RuntimeChecks.require(body.motionY==0,"Disabled buoyancy accelerated the corpse"); - LootableBodies.corpseBuoyancy=true;body.motionY=0;body.baseTick(); - RuntimeChecks.require(body.motionY>0,"Enabled buoyancy did not lift the corpse");conserved(body);checks++; + 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++; System.out.println("LOOTABLE_BODIES_112_RECOVERY_PASS checks="+checks); } finally {LootableBodies.corpseBuoyancy=buoyancy;LootableBodies.voidPlatform=platform;LootableBodies.dimensionSpecificPlatform=specific;} } - private static EntityLootableBody body(WorldServer world,int coordinate,double y) { + 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 void conserved(EntityLootableBody body) { RuntimeChecks.require(RuntimeChecks.sameStack(body.getStackInSlot(6),RuntimeChecks.stack(420)),"Recovery option changed complete item NBT"); - NBTTagCompound saved=new NBTTagCompound();body.writeUnlessPassenger(saved);EntityLootableBody reloaded=new EntityLootableBody(body.world);reloaded.read(saved); + 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"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java index 050a634..06e8027 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java @@ -1,4 +1,15 @@ 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 com.mojang.authlib.GameProfile; import cyano.lootable.entities.EntityLootableBody; @@ -6,10 +17,10 @@ import java.io.*; import java.util.*; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; -import net.minecraft.init.Items; -import net.minecraft.inventory.ClickType; +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.*; @@ -27,11 +38,11 @@ final class ReportedIssueChecks { private interface Check { void run() throws Exception; } private static final List results = new ArrayList<>(); private static MinecraftServer server; - private static WorldServer world; + private static ServerWorld world; private static int failures, sequence, transaction; private static File folder; - static void run(MinecraftServer instance, WorldServer target, File directory) throws Exception { + static void run(MinecraftServer instance, ServerWorld 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); @@ -50,100 +61,100 @@ static void run(MinecraftServer instance, WorldServer target, File directory) th System.out.println("LOOTABLE_BODIES_REPORTED_ISSUES_PASS checks="+results.size()); } private static void check(String name, Check action) { - Set before=new HashSet<>(world.loadedEntityList); + 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();} finally { - for(Entity entity:new ArrayList(world.loadedEntityList)) if(!before.contains(entity)) { - if(entity instanceof EntityPlayerMP) server.getPlayerList().playerLoggedOut((EntityPlayerMP)entity); - else world.removeEntityDangerously(entity); + for(Entity entity:new ArrayList(FixtureApi.entities(world))) if(!before.contains(entity)) { + if(entity instanceof ServerPlayerEntity) server.getPlayerList().playerLoggedOut((ServerPlayerEntity)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.TextComponentString("Issue 5 original shovel")); return stack; + stack.setDisplayName(new net.minecraft.util.text.StringTextComponent("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)); - NBTTagCompound n=new NBTTagCompound();body.writeAdditional(n); - NBTTagList aux=n.getList("Aux",10); + 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; - NBTTagCompound n=stack.write(new NBTTagCompound());n.removeTag("Count"); + CompoundNBT n=stack.write(new CompoundNBT());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(EntityPlayerMP player) { + private static Map tagged(ServerPlayerEntity player) { Map n=new TreeMap<>(); for(int i=0;i before=new HashSet<>(world.loadedEntityList);player.onKillCommand(); - for(Entity e:world.loadedEntityList)if(e instanceof EntityLootableBody && !before.contains(e))return (EntityLootableBody)e; + private static EntityLootableBody death(ServerPlayerEntity player) { + Set before=new HashSet<>(FixtureApi.entities(world));player.onKillCommand(); + 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 EntityPlayerMP respawn(EntityPlayerMP player) { - NetHandlerPlayServer connection=player.connection; - connection.processClientStatus(new CPacketClientStatus(CPacketClientStatus.State.PERFORM_RESPAWN)); - EntityPlayerMP replacement=connection.player; + private static ServerPlayerEntity respawn(ServerPlayerEntity player) { + ServerPlayNetHandler connection=player.connection; + connection.processClientStatus(new CClientStatusPacket(CClientStatusPacket.State.PERFORM_RESPAWN)); + ServerPlayerEntity replacement=connection.player; RuntimeChecks.require(replacement!=player && replacement.isAlive(),"Actual player recreation did not occur"); replacement.setPosition(32.5,80,0.5);return replacement; } - private static void click(EntityPlayerMP p,int slot,int button,ClickType type) throws IOException { + 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()); 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); - CPacketClickWindow packet=new CPacketClickWindow();packet.readPacketData(wire);p.connection.processClickWindow(packet); + CClickWindowPacket packet=new CClickWindowPacket();packet.readPacketData(wire);p.connection.processClickWindow(packet); RuntimeChecks.require(p.openContainer.getCanCraft(p),"Packet prediction rejected the click transaction"); } finally {wire.release();} } private static void deathSequence() throws Exception { - world.getGameRules().setOrCreateGameRule("keepInventory","false",null);RuntimeChecks.awaitChunk(world,2,0); - EntityPlayerMP p=new EntityPlayerMP(server,world,new GameProfile(UUID.randomUUID(),"IssueFive"),new PlayerInteractionManager(world)); + 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); // 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.SPacketPlayerListItem( - net.minecraft.network.play.server.SPacketPlayerListItem.Action.ADD_PLAYER,p)); - RuntimeChecks.require(world.spawnEntity(p),"Fixture player spawn");server.getPlayerList().getPlayers().add(p); + 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)); 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(p.inventory,first);gui.windowId=17;p.openContainer=gui;gui.addListener(p); + p=respawn(p);CorpseContainer gui=new CorpseContainer(17,p.inventory,first);p.openContainer=gui;gui.addListener(p); 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);CPacketCloseWindow packet=new CPacketCloseWindow();packet.readPacketData(close);p.connection.processCloseWindow(packet);} + try {close.writeByte(gui.windowId);CCloseWindowPacket packet=new CCloseWindowPacket();packet.readPacketData(close);p.connection.processCloseWindow(packet);} 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().hasKey("fixture"))active=slot; + 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; RuntimeChecks.require(active>0,"Shift-click did not leave a different occupied hotbar slot"); PacketBuffer held=new PacketBuffer(Unpooled.buffer()); - try {held.writeShort(active);CPacketHeldItemChange packet=new CPacketHeldItemChange();packet.readPacketData(held);p.connection.processHeldItemChange(packet);} + try {held.writeShort(active);CHeldItemChangePacket packet=new CHeldItemChangePacket();packet.readPacketData(held);p.connection.processHeldItemChange(packet);} finally {held.release();} - // Deliberately do not tick/sync inventoryContainer or reopen inventory here. + // 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"); System.out.println("ISSUE5_REAL_RESPAWN_AND_HELD_ITEM_PACKET_PASS"); p=respawn(p);server.getPlayerList().playerLoggedOut(p); - NBTTagCompound n=new NBTTagCompound();second.writeUnlessPassenger(n); + CompoundNBT n=new CompoundNBT();second.writeUnlessPassenger(n); EntityLootableBody loaded=new EntityLootableBody(world);loaded.read(n); RuntimeChecks.require(expected.equals(tagged(loaded)),"Second corpse NBT reload lost recovery"); } @@ -154,25 +165,26 @@ private static EntityLootableBody fullBody(int cx,int cz,double offset) { for(int i=0;i<48;i++)body.setInventorySlotContents(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.spawnEntity(body),"Physics fixture spawn");return body; + RuntimeChecks.require(world.addEntity(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()); EntityLootableBody b=fullBody(c,c,8.5);Map expected=inventory(b); - for(int i=0;i<40;i++)world.tickEntity(b); + FixtureApi.awaitTicking(b); + for(int i=0;i<40;i++)world.updateEntity(b); RuntimeChecks.require(b.onGround && b.posY==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.tickEntity(b); + for(int i=0;i<40;i++)world.updateEntity(b); RuntimeChecks.require(b.ticksExisted==ticks && b.posY==80 && !world.getChunkProvider().chunkExists(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); - unloadChunk(128,128);world.tickEntities(); + unloadChunk(128,128);world.tick(()->true); RuntimeChecks.require(!world.getChunkProvider().chunkExists(128,128),"Fixture chunk did not actually unload"); - RuntimeChecks.require(!world.loadedEntityList.contains(b),"Unloaded corpse still receives normal world ticks"); + 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"); } @@ -180,10 +192,10 @@ private static final class Support implements AutoCloseable { EntityLootableBody body; int cx,cz,nx,nz; BlockPos platform; boolean forced; Map expected; - void unload(){unloadChunk(nx,nz);RuntimeChecks.require(!world.getChunkProvider().chunkExists(nx,nz),"Support chunk did not unload");} - void frozen(){for(int i=0;i<100;i++)world.tickEntity(body);System.out.println("ISSUE17_UNLOADED_SUPPORT y="+body.posY+" alive="+body.isAlive()+" inventory="+inventory(body).size()+" supportLoaded="+world.getChunkProvider().chunkExists(nx,nz));RuntimeChecks.require(body.isAlive() && body.posY==80 && expected.equals(inventory(body)),"Corpse fell through unloaded neighbour support and lost recoverable inventory");} - void resume(){RuntimeChecks.awaitChunk(world,nx,nz);for(int i=0;i<10;i++)world.tickEntity(body);RuntimeChecks.require(body.onGround && body.posY==80 && body.addedToChunk && expected.equals(inventory(body)),"Corpse failed to resume safely after support terrain loaded");} - @Override public void close(){if(forced)world.func_212414_b(cx,cz,false);} + void unload(){world.forceChunk(cx,cz,false);net.minecraft.util.math.ChunkPos pos=new net.minecraft.util.math.ChunkPos(cx,cz);world.getChunkProvider().func_217228_a(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.posY+" alive="+body.isAlive()+" inventory="+inventory(body).size()+" supportLoaded="+world.getChunkProvider().chunkExists(nx,nz));RuntimeChecks.require(body.isAlive() && body.posY==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.onGround && body.posY==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().func_217222_b(net.minecraft.world.server.TicketType.FORCED,pos,0,pos);} } private static Support support(int axis,int direction) { Support f=new Support();f.cx=144+(sequence++)*8;f.cz=f.cx; @@ -195,50 +207,41 @@ private static Support support(int axis,int direction) { 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));world.removeBlock(new BlockPos(f.platform.getX(),y,f.platform.getZ()));} + 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.func_212414_b(f.cx,f.cz,true); - RuntimeChecks.require(f.forced && world.func_212416_f(f.cx,f.cz),"Native forced chunk unavailable"); - for(int i=0;i<10;i++)world.tickEntity(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"); + FixtureApi.awaitTicking(f.body); + for(int i=0;i<10;i++)world.updateEntity(f.body); RuntimeChecks.require(f.body.onGround && f.body.posY==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)) { - NBTTagCompound before=new NBTTagCompound();f.body.writeUnlessPassenger(before); + CompoundNBT before=new CompoundNBT();f.body.writeUnlessPassenger(before); f.unload();f.frozen(); - NBTTagCompound after=new NBTTagCompound();f.body.writeUnlessPassenger(after); + CompoundNBT after=new CompoundNBT();f.body.writeUnlessPassenger(after); RuntimeChecks.require(before.equals(after),"Waiting for terrain changed healthy saved NBT"); f.resume(); } } private static void realVoid() { try(Support f=support(0,1)) { - world.removeBlock(f.platform); + 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.tickEntity(f.body); + for(int i=0;i<250 && f.body.isAlive();i++)world.updateEntity(f.body); for(int i=0;i<15;i++)f.body.baseTick(); - System.out.println("VOID_CONTROL y="+f.body.posY+" alive="+f.body.isAlive()+" items="+inventory(f.body).size()+" motion="+f.body.motionY); + System.out.println("VOID_CONTROL y="+f.body.posY+" alive="+f.body.isAlive()+" items="+inventory(f.body).size()+" motion="+f.body.getMotion().y); RuntimeChecks.require(!f.body.isAlive() && inventory(f.body).isEmpty(),"Loaded real void behavior changed into a rescue feature"); } } private static void unloadChunk(int x,int z) { - net.minecraft.world.gen.ChunkProviderServer provider=world.getChunkProvider(); - provider.queueUnload(RuntimeChecks.awaitChunk(world,x,z)); - // Each 1.10 provider tick saves/unloads at most 100 chunks. An integrated - // client can leave a larger queue when the test teleports its players. - int attempts=provider.getLoadedChunks().size()/100+2; - for(int i=0;itrue); - flushChunks(); - RuntimeChecks.require(!provider.chunkExists(x,z),"Fixture chunk remained loaded after draining the unload queue"); - } - private static void flushChunks() { - // Reload only after the engine's queued region write has completed. - try {net.minecraft.world.storage.ThreadedFileIOBase.getThreadedIOInstance().waitForFinish();} - catch(InterruptedException error){Thread.currentThread().interrupt();throw new AssertionError(error);} + FixtureApi.unload(world,x,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")); @@ -246,7 +249,7 @@ static void seedPaused() throws Exception { 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,WorldServer target,File directory) throws Exception { + static void coldReload(MinecraftServer instance,ServerWorld 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]); @@ -254,17 +257,19 @@ static void coldReload(MinecraftServer instance,WorldServer target,File director RuntimeChecks.require(!world.getChunkProvider().chunkExists(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.tickEntity(b); + for(int i=0;i<100;i++)world.updateEntity(b); RuntimeChecks.require(b.isAlive() && b.posY==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); - for(int i=0;i<20;i++)world.tickEntity(b); + FixtureApi.awaitTicking(b); + for(int i=0;i<20;i++)world.updateEntity(b); RuntimeChecks.require(b.onGround && b.posY==80 && b.isAlive(),"Cold-loaded corpse failed to resume on restored platform"); RuntimeChecks.verifyExpected(folder,"issue17-paused",b); + world.forceChunk(cx,cz,false); System.out.println("ISSUE17_COLD_RELOAD_DEFERRED_SUPPORT_AND_COMPLETE_NBT_PASS"); } - private static final class QuietConnection extends NetHandlerPlayServer { - QuietConnection(EntityPlayerMP p){super(server,new NetworkManager(EnumPacketDirection.SERVERBOUND),p);} - @Override public void sendPacket(Packet packet) { } + private static final class QuietConnection extends ServerPlayNetHandler { + QuietConnection(ServerPlayerEntity p){super(server,new NetworkManager(PacketDirection.SERVERBOUND),p);} + @Override public void sendPacket(IPacket packet) { } } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java index ee983ff..abefd65 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java @@ -1,4 +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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -7,13 +14,17 @@ 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.EntityPlayerMP; -import net.minecraft.init.*; -import net.minecraft.inventory.ClickType; +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.CPacketHeldItemChange; +import net.minecraft.network.play.client.CHeldItemChangePacket; import net.minecraft.util.*; import net.minecraft.util.math.BlockPos; import net.minecraft.world.*; @@ -22,34 +33,35 @@ final class ReportedIssueClientChecks { private interface Action {void run() throws Exception;} private int stage,frames,active; + private boolean distantViewerObserved,approachPending; private Future pending; private UUID firstId,secondId; private File report; - private NBTTagCompound savedBody,savedPlayer; + private CompoundNBT 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.TextComponentString("Issue 5 original shovel"));return s; + s.setDisplayName(new net.minecraft.util.text.StringTextComponent("Issue 5 original shovel"));return s; } void tick(Minecraft mc) throws Exception { frames++; - if(stage==0 && mc.currentScreen instanceof GuiMainMenu) { + 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.limitFramerate=30;mc.gameSettings.renderDistanceChunks=4; + mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; mc.launchIntegratedServer("issue-fixture","Issue fixture",new WorldSettings(4267321,GameType.SURVIVAL,false,false,WorldType.FLAT).enableCommands());next(1); } else if(stage==1 && ready(mc)) { schedule(mc,()->{ - WorldServer w=player(mc).getServerWorld();w.getGameRules().setOrCreateGameRule("keepInventory","false",null); - w.getGameRules().setOrCreateGameRule("doMobSpawning","false",null); + 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()); - EntityPlayerMP p=player(mc);p.setGameType(GameType.SURVIVAL);p.inventory.clear();p.inventory.currentItem=4; + 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.inventoryContainer.detectAndSendChanges(); + p.connection.setPlayerLocation(32.5,80,.5,0,0);p.container.detectAndSendChanges(); firstId=die(p,null).getUniqueID(); });next(2); - } else if(stage==2 && done() && mc.currentScreen instanceof GuiGameOver && frames>10) { + } 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 GuiGameOver)) { + } 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==4 && done() && open(mc,firstId)) {next(5); } else if(stage==5 && gui(mc) && frames>15) { @@ -66,23 +78,37 @@ void tick(Minecraft mc) throws Exception { 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 CPacketHeldItemChange(active)); + mc.getConnection().sendPacket(new CHeldItemChangePacket(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 GuiGameOver && frames>10) { + } 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 GuiGameOver)) { + } else if(stage==9 && ready(mc) && mc.player.isAlive() && !(mc.currentScreen instanceof DeathScreen)) { schedule(mc,()->{ - EntityPlayerMP p=player(mc);EntityLootableBody second=null; - for(Entity e:p.world.loadedEntityList)if(e instanceof EntityLootableBody && !e.getUniqueID().equals(firstId) && p.getGameProfile().getName().equals(((EntityLootableBody)e).getCustomNameTag()))second=(EntityLootableBody)e; + 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 NBTTagList()).size()==0,"Second death duplicated player inventory"); - p.connection.setPlayerLocation(32.5,80,.5,0,0); + RuntimeChecks.require(p.inventory.write(new ListNBT()).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); mark("second_death_without_inventory_resync_preserves_original_shovel_and_all_tags"); });next(10); - } else if(stage==10 && done() && open(mc,secondId)) {next(11); + } 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; + if(present) { + RuntimeChecks.require(frames<200,"Distant viewer did not leave corpse tracking range"); + return; + } + distantViewerObserved=true; + System.out.println("ISSUE5_DISTANT_VIEWER_CONFIRMED corpse="+secondId); + } + 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); @@ -101,7 +127,7 @@ void tick(Minecraft mc) throws Exception { mc.player.closeScreen();schedule(mc,()->{ RuntimeChecks.require(body(mc).isEmpty(),"Final recovery left duplicate corpse items"); Map expected=originals(),actual=new TreeMap<>(); - EntityPlayerMP p=player(mc); + ServerPlayerEntity p=player(mc); for(int i=0;i before=new HashSet<>(p.world.loadedEntityList);p.onKillCommand(); - for(Entity e:p.world.loadedEntityList)if(e instanceof EntityLootableBody && !before.contains(e))return (EntityLootableBody)e; + 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; throw new AssertionError("Actual connected death absent"); } private static void addTagged(Map result,ItemStack stack) { - if((stack == null || stack.isEmpty()) || !stack.hasTag() || !stack.getTag().hasKey("fixture"))return; - NBTTagCompound n=stack.write(new NBTTagCompound());n.removeTag("Count");result.put(n.toString(),result.getOrDefault(n.toString(),0)+stack.getCount()); + 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()); } private static Map originals(){Map m=new TreeMap<>();addTagged(m,original());addTagged(m,RuntimeChecks.stack(301));return m;} private static void verifyOriginals(EntityLootableBody b) { @@ -132,24 +158,59 @@ private static void verifyOriginals(EntityLootableBody b) { } 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 void save(Minecraft mc) throws Exception { - savedBody=new NBTTagCompound();body(mc).writeUnlessPassenger(savedBody);savedPlayer=new NBTTagCompound();savedPlayer.setTag("Inventory",player(mc).inventory.write(new NBTTagList())); + savedBody=new CompoundNBT();body(mc).writeUnlessPassenger(savedBody);savedPlayer=new CompoundNBT();savedPlayer.put("Inventory",player(mc).inventory.write(new ListNBT())); File folder=player(mc).getServerWorld().getSaveHandler().getWorldDirectory(); 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().saveAllWorlds(false); + mc.getIntegratedServer().getPlayerList().saveAllPlayerData();mc.getIntegratedServer().save(false,true,false); } private void verifyDisk(Minecraft mc) { - player(mc).getServerWorld().getChunk(2,0);NBTTagCompound actual=new NBTTagCompound();body(mc).writeUnlessPassenger(actual); - for(String key:Arrays.asList("id","UUIDMost","UUIDLeast","Equipment","Aux","Name"))RuntimeChecks.require(Objects.equals(savedBody.getTag(key),actual.getTag(key)),"Disk corpse differs: "+key); - RuntimeChecks.require(savedPlayer.getList("Inventory",10).equals(player(mc).inventory.write(new NBTTagList())),"Complete player inventory differs after disk reload"); + 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"); } - private EntityPlayerMP player(Minecraft mc){return mc.getIntegratedServer().getPlayerList().getPlayerByUUID(mc.player.getUniqueID());} + 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 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 open(Minecraft mc,UUID id){for(Entity e:mc.world.loadedEntityList)if(e instanceof EntityLootableBody && e.getUniqueID().equals(id)){mc.playerController.interactWithEntity(mc.player,e,EnumHand.MAIN_HAND);return true;}return false;} - private void schedule(Minecraft mc,Action a){pending=mc.getIntegratedServer().addScheduledTask(()->{try{a.run();}catch(Exception e){throw new RuntimeException(e);}});} + 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;} + if(frames%10!=0)return false; + if(!approachPending) { + // A distant client legitimately has no entity to approach yet. + // 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.posX,z=corpse.posZ-2; + int y=net.minecraft.util.math.MathHelper.floor(corpse.posY); + // 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); + System.out.println("ISSUE5_VIEWER_APPROACH corpse="+id); + } + }); + approachPending=true; + return false; + } + 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); + 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 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);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java index 0f8ec8d..3f77529 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java @@ -1,4 +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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.LootableBodies; @@ -7,22 +10,22 @@ import java.nio.file.*; import java.util.*; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Blocks; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.block.Blocks; import net.minecraft.item.ItemStack; import net.minecraft.server.MinecraftServer; import net.minecraft.util.math.BlockPos; import net.minecraft.world.*; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.*; +import net.minecraftforge.event.TickEvent; /** Death/respawn timing matrix, with an independently connected observer. */ public final class RespawnPacketChecks { private final MinecraftServer server; private int index,phase,ticks,phaseTicks,startupTicks; - private EntityPlayerMP dead; - private EntityPlayerMP observerDead; + private ServerPlayerEntity dead; + private ServerPlayerEntity observerDead; private EntityLootableBody body; private EntityLootableBody observerBody; private final boolean simultaneous=Boolean.getBoolean("lootablebodies.respawn.simultaneous"); @@ -56,48 +59,48 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { } } static File file(String name){return new File(System.getProperty("lootablebodies.multiplayer.sync"),name);} - private EntityPlayerMP player(String role){return server.getPlayerList().getPlayerByUsername("Container"+role);} + private ServerPlayerEntity player(String role){return server.getPlayerList().getPlayerByUsername("Container"+role);} private boolean ack(String role,int p){return file(role+".case-"+index+"-"+p).isFile();} - private void trace(EntityPlayerMP 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(ServerPlayerEntity p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.netManager,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"); - EntityPlayerMP a=player("A"),b=player("B");trace(a);trace(b); + ServerPlayerEntity 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:body.world.getServer().getWorld(body.dimension).getEntityTracker().getTrackingPlayers(body))); + 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))); 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.world.dimension.DimensionType dim=kind==7?net.minecraft.world.dimension.DimensionType.NETHER:net.minecraft.world.dimension.DimensionType.OVERWORLD;WorldServer w=server.getWorld(dim); + int kind=index%8;net.minecraft.world.dimension.DimensionType dim=kind==7?net.minecraft.world.dimension.DimensionType.THE_NETHER:net.minecraft.world.dimension.DimensionType.OVERWORLD;ServerWorld w=server.getWorld(dim); if(a.dimension!=dim)transfer(a,dim); if(b.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()); - for(int y=80;y<=84;y++)w.removeBlock(new BlockPos(x+dx,y,z+dz)); + for(int y=80;y<=84;y++)w.removeBlock(new BlockPos(x+dx,y,z+dz),false); } - w.getGameRules().setOrCreateGameRule("doMobSpawning","false",null);w.getGameRules().setOrCreateGameRule("keepInventory","false",null); + 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(); a.setSpawnPoint(new BlockPos(kind==5?160:(int)Math.floor(x),80,(int)Math.floor(z)-3),true); b.setSpawnPoint(new BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),true); if(kind==7)a.setSpawnPoint(new BlockPos(96,80,93),true); 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.inventoryContainer.detectAndSendChanges(); - if(simultaneous){b.inventory.mainInventory.set(9,RuntimeChecks.stack(620+index));b.inventoryContainer.detectAndSendChanges();} + 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); state(1); } else if(phase==1&&ack("A",1)&&ack("B",1)&&phaseTicks>10) { - dead=a;Set before=new HashSet<>(a.world.loadedEntityList);a.onKillCommand();body=null; - for(Entity ent:a.world.loadedEntityList)if(ent instanceof EntityLootableBody&&!before.contains(ent))body=(EntityLootableBody)ent; + 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; RuntimeChecks.require(body!=null,"No server death corpse");state(2); if(simultaneous) { - observerDead=b;before=new HashSet<>(b.world.loadedEntityList);b.onKillCommand();observerBody=null; - for(Entity ent:b.world.loadedEntityList)if(ent instanceof EntityLootableBody&&!before.contains(ent))observerBody=(EntityLootableBody)ent; + 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; 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()))) { @@ -110,6 +113,10 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { // death and return. Saved UUIDs survive; runtime entity IDs do not. body=live(body);if(simultaneous)observerBody=live(observerBody);state(3); } else if(phase==3) { + // 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.posX,body.posY,body.posZ-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"); @@ -119,9 +126,9 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { a.closeScreen();b.closeScreen();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(); + Files.write(file("respawn.pass").toPath(),new byte[]{1});System.out.println("LOOTABLE_BODIES_RESPAWN_MATRIX_PASS");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.initiateShutdown();} + } 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);} } 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()); @@ -131,19 +138,19 @@ private void state(int value) throws Exception { catch(java.nio.file.AccessDeniedException busy){if(retry==50)throw busy;Thread.sleep(1);} System.out.println("RESPAWN_MATRIX_PHASE "+line); } - private void transfer(EntityPlayerMP player,net.minecraft.world.dimension.DimensionType dimension) { - WorldServer target=server.getWorld(dimension); - player.changeDimension(dimension,new Teleporter(target){ - @Override public void placeInPortal(Entity entity,float yaw){entity.setLocationAndAngles(96.5,80,96.5,yaw,0);} - }); + private void transfer(ServerPlayerEntity player,net.minecraft.world.dimension.DimensionType dimension) { + ServerWorld target=server.getWorld(dimension); + player.changeDimension(dimension); + player.connection.setPlayerLocation(96.5,80,96.5,player.rotationYaw,0); + } - private void writeTracked(EntityPlayerMP player,String role) throws Exception { - List expected=new ArrayList<>();WorldServer w=player.getServerWorld(); - for(Entity e:w.loadedEntityList)if(e instanceof EntityLootableBody&&e.isAlive()&&w.getEntityTracker().getTrackingPlayers(e).contains(player))expected.add(e.getEntityId()+" "+e.getUniqueID()); + 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()); Collections.sort(expected);Files.write(file("expected-"+index+"-"+role+".txt").toPath(),expected,StandardCharsets.UTF_8); } private EntityLootableBody live(EntityLootableBody previous) { - WorldServer w=server.getWorld(previous.dimension); + ServerWorld w=server.getWorld(previous.dimension); w.getChunk((int)Math.floor(previous.posX/16),(int)Math.floor(previous.posZ/16)); return RuntimeChecks.find(w,previous.getUniqueID()); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java index 080e3cb..9d311e5 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java @@ -1,4 +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 cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -10,16 +15,19 @@ 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.TileEntitySkull; +import net.minecraft.tileentity.SkullTileEntity; import net.minecraft.world.World; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; /** Respawns on the death notification, without waiting for a corpse spawn. */ final class RespawnPacketClientChecks { @@ -36,13 +44,21 @@ final class RespawnPacketClientChecks { @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 GuiGameOver) { + if(mc.currentScreen 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) { if(phase!=2)return; - try {net.minecraftforge.fml.common.ObfuscationReflectionHelper.setPrivateValue(Entity.class,null,entityId,"field_70152_a");} - catch(Exception error){throw new RuntimeException(error);} + try { + java.lang.reflect.Field counter=null; + for(java.lang.reflect.Field field:Entity.class.getDeclaredFields()) + if(java.lang.reflect.Modifier.isStatic(field.getModifiers())&&field.getType()==java.util.concurrent.atomic.AtomicInteger.class) { + RuntimeChecks.require(counter==null,"Ambiguous native entity counter");counter=field; + } + RuntimeChecks.require(counter!=null,"Missing native entity counter");counter.setAccessible(true); + // Native 1.14 increments before assigning the new entity ID. + ((java.util.concurrent.atomic.AtomicInteger)counter.get(null)).set(entityId-1); + } 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); @@ -54,9 +70,9 @@ private void respawn(Minecraft mc) { if(e.phase==TickEvent.Phase.START) { if(mc.world!=null&&mc.world!=profileWorld) { profileWorld=mc.world; - // Packet tests use cached synthetic profiles, never real accounts or + // IPacket tests use cached synthetic profiles, never real accounts or // live profile-service availability. The local_texture default stays false. - TileEntitySkull.setProfileCache(new PlayerProfileCache(null,new File(mc.gameDir,"fixture-usercache.json")) { + SkullTileEntity.setProfileCache(new PlayerProfileCache(null,new File(mc.gameDir,"fixture-usercache.json")) { @Override public GameProfile getGameProfileForUsername(String name) { RuntimeChecks.require(name.matches("Container[AB]"),"Unexpected packet fixture profile"); UUID id=UUID.nameUUIDFromBytes(("OfflinePlayer:"+name).getBytes(StandardCharsets.UTF_8)); @@ -77,16 +93,16 @@ private void respawn(Minecraft mc) { 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 GuiMainMenu) { - connecting=true;mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.limitFramerate=30;mc.gameSettings.renderDistanceChunks=4; + if(!connecting&&FixtureConnections.ready(role)&&mc.currentScreen instanceof MainMenuScreen) { + connecting=true;mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; FixtureConnections.connect(mc); } if(mc.world==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.addScheduledTask(()->{observe(mc,"AFTER "+packet);respawn(mc);})); + RespawnPacketTrace.install(mc.getConnection().getNetworkManager(),new File(mc.gameDir,"client-packets.txt"),packet->mc.runAsync(()->{observe(mc,"AFTER "+packet);respawn(mc);})); } - if(mc.currentScreen instanceof GuiGameOver)deathTicks++; + if(mc.currentScreen 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); @@ -99,12 +115,12 @@ else if(phase==1&&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||!mc.world.loadedEntityList.contains(tracked))return; + if(!(tracked instanceof EntityLootableBody)||!tracked.getUniqueID().equals(UUID.fromString(pair[1]))||tracked.removed||!FixtureApi.entities(mc.world).contains(tracked))return; } Entity entity=mc.world.getEntityByID(entityId); - if(!(entity instanceof EntityLootableBody)||!entity.getUniqueID().equals(uuid)||!mc.world.loadedEntityList.contains(entity)||entity.removed)return; + if(!(entity instanceof EntityLootableBody)||!entity.getUniqueID().equals(uuid)||!FixtureApi.entities(mc.world).contains(entity)||entity.removed)return; if(role.equals("A")) { - if(!(mc.currentScreen instanceof CorpseGUIContainer)){mc.playerController.interactWithEntity(mc.player,entity,EnumHand.MAIN_HAND);return;} + 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(!found||++guiFrames<10)return; } @@ -117,7 +133,7 @@ else if(phase==1&&frames>20){ // Close the transport before clearing the world. The next main // loop drains already queued destroy packets against that world; // clearing it here races vanilla 1.11's unguarded packet handler. - mc.getConnection().getNetworkManager().closeChannel(new net.minecraft.util.text.TextComponentString("Disposable check complete")); + mc.getConnection().getNetworkManager().closeChannel(new net.minecraft.util.text.StringTextComponent("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();} @@ -125,7 +141,7 @@ else if(phase==1&&frames>20){ 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:mc.world.loadedEntityList)if(ent instanceof EntityLootableBody)bodies.append(ent.getEntityId()).append(':').append(ent.getUniqueID()).append(':').append(ent.removed).append(','); + 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":mc.world.dimension.getType().getId())+" player="+(mc.player==null?"none":mc.player.getEntityId())+" mapped="+(mapped==null?null:mapped.getUniqueID())+" 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 3e05bc0..f234bd1 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java @@ -1,4 +1,11 @@ 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 io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -7,10 +14,10 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.network.NetworkManager; -import net.minecraft.network.Packet; +import net.minecraft.network.IPacket; import net.minecraft.network.PacketBuffer; import net.minecraft.network.play.server.*; -import net.minecraft.network.play.client.CPacketClientStatus; +import net.minecraft.network.play.client.CClientStatusPacket; import net.minecraftforge.fml.common.network.ByteBufUtils; import net.minecraftforge.fml.network.ICustomPacket; @@ -29,7 +36,7 @@ static void install(NetworkManager manager,File log,After after) { String description=describe(packet);if(description!=null)record("IN "+description); if(Boolean.getBoolean("lootablebodies.respawn.dropSpawn")&&log.getName().equals("client-packets.txt") &&description!=null&&description.startsWith("MOD_SPAWN ")&&description.contains("mod=lootablebodies ")) { - record("FAULT_DROPPED "+description);((ICustomPacket)packet).getData().release();return; + record("FAULT_DROPPED "+description);if(packet instanceof ICustomPacket)((ICustomPacket)packet).getInternalData().release();return; } super.channelRead(context,packet); if(description!=null&&after!=null)after.received(description); @@ -42,25 +49,52 @@ 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.getData().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.IRegistry.field_212629_r.get(type); + 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); 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 SPacketRespawn||packet instanceof SPacketDestroyEntities||packet instanceof SPacketUnloadChunk - ||packet instanceof SPacketChunkData||packet instanceof SPacketCombatEvent||packet instanceof SPacketEntityMetadata||packet instanceof CPacketClientStatus))return null; + 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) { + // MobEntity uses vanilla spawn packets in 1.14. Received packets + // have entries rather than a live dataManager, so cannot be written + // again. Read the two mapped scalar fields and unique UUID only. + 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) { + // Native 1.14 reads dimensionInt and leaves dimensionID null until + // the client handles the packet. Re-serializing it here disconnects + // the transport. Observe both representations without calling the + // client-only accessor or mutating the packet. + try { + net.minecraft.world.dimension.DimensionType dimension=null;int id=0; + for(java.lang.reflect.Field field:SRespawnPacket.class.getDeclaredFields()) { + if(field.getType()==net.minecraft.world.dimension.DimensionType.class) { + field.setAccessible(true);dimension=(net.minecraft.world.dimension.DimensionType)field.get(packet); + } else if(field.getType()==int.class) {field.setAccessible(true);id=field.getInt(packet);} + } + return "RESPAWN dimension="+(dimension==null?id:dimension.getId()); + } catch(ReflectiveOperationException invalid) {throw new AssertionError("Fixture respawn observation",invalid);} + } // 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()); try { - ((Packet)packet).writePacketData(b); - if(packet instanceof SPacketRespawn)return "RESPAWN dimension="+b.readInt(); - if(packet instanceof SPacketDestroyEntities){int[] ids=new int[b.readVarInt()];for(int i=0;i results=new ArrayList<>(); - static void run(MinecraftServer server,WorldServer target,File folder) throws Exception { + static void run(MinecraftServer server,ServerWorld target,File folder) throws Exception { world=target;failures=0;results.clear(); - INBTBase[] wrong={new NBTTagString("opaque"),new NBTTagCompound(),new NBTTagByteArray(new byte[]{1,2}),new NBTTagIntArray(new int[]{1,2}),new NBTTagList()}; - for(int i=0;ibadFall(raw));} - for(int i=0;ibadRotation(raw));} - for(INBTBase raw:new INBTBase[]{new NBTTagString("opaque"),new NBTTagCompound(),new NBTTagByteArray(new byte[]{1})})check("Rotation_wrong_element_type_"+raw.getId(),()->{NBTTagList l=new NBTTagList();l.add(raw.copy());l.add(raw.copy());badRotation(l);}); + INBT[] wrong={new StringNBT("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[]{new StringNBT("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);}); 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(INBTBase raw:new INBTBase[]{new NBTTagFloat(Float.NaN),new NBTTagFloat(Float.NEGATIVE_INFINITY),new NBTTagFloat(Float.POSITIVE_INFINITY),new NBTTagDouble(Double.NaN),new NBTTagDouble(Double.NEGATIVE_INFINITY),new NBTTagDouble(Double.POSITIVE_INFINITY),new NBTTagInt(-1),new NBTTagFloat(-.25F),new NBTTagDouble(-Double.MAX_VALUE),new NBTTagDouble(Double.MAX_VALUE),new NBTTagDouble(1e-300)})check("FallDistance_invalid_"+raw,()->badFall(raw)); + for(INBT raw:new INBT[]{new FloatNBT(Float.NaN),new FloatNBT(Float.NEGATIVE_INFINITY),new FloatNBT(Float.POSITIVE_INFINITY),new DoubleNBT(Double.NaN),new DoubleNBT(Double.NEGATIVE_INFINITY),new DoubleNBT(Double.POSITIVE_INFINITY),new IntNBT(-1),new FloatNBT(-.25F),new DoubleNBT(-Double.MAX_VALUE),new DoubleNBT(Double.MAX_VALUE),new DoubleNBT(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(new NBTTagFloat(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(new FloatNBT(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); @@ -48,33 +61,33 @@ static void run(MinecraftServer server,WorldServer 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 NBTTagCompound saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} - private static NBTTagCompound 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(NBTTagCompound n){NBTTagCompound before=n.copy();Entity e=EntityType.create(n,world);require(e instanceof EntityLootableBody,"Factory discarded corpse");require(NameLifetimeChecks.same(before,n),"Source NBT mutated");return (EntityLootableBody)e;} - static NBTTagList rotation(float... angles){NBTTagList l=new NBTTagList();for(float a:angles)l.add(new NBTTagFloat(a));return l;} - static NBTTagList wideRotation(double... angles){NBTTagList l=new NBTTagList();for(double a:angles)l.add(new NBTTagDouble(a));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("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(new FloatNBT(a));return l;} + static ListNBT wideRotation(double... angles){ListNBT l=new ListNBT();for(double a:angles)l.add(new DoubleNBT(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 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){NBTTagCompound n=base(),a=saved(b);for(String k:new String[]{"Equipment","Aux"})require(NameLifetimeChecks.same(n.getTag(k),a.getTag(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);NBTTagCompound 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.getTag(k),z.getTag(k)),"Round trip changed "+k);} - private static void badFall(INBTBase raw){NBTTagCompound n=base();n.setTag("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(INBTBase raw){NBTTagCompound n=base();n.setTag("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){NBTTagCompound n=base();NBTTagList r=axis==0?wideRotation(raw,17):wideRotation(37.25,raw);n.setTag("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 NBTTagCompound malformedFields(){NBTTagCompound n=new NBTTagCompound();n.setTag("Rotation",wideRotation(37,Double.NaN));n.setDouble("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");NBTTagCompound raw=malformedFields();NameLifetimeChecks.retained(b,"Rotation",raw.getTag("Rotation"));NameLifetimeChecks.retained(b,"FallDistance",raw.getTag("FallDistance"));} + 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"));} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java index f827c37..fcb8a20 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java @@ -1,4 +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 com.mojang.authlib.GameProfile; import cyano.lootable.LootableBodies; @@ -8,15 +15,15 @@ import java.io.*; import java.util.*; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.init.Items; -import net.minecraft.inventory.EntityEquipmentSlot; +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.server.MinecraftServer; import net.minecraft.util.DamageSource; import net.minecraft.util.math.BlockPos; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.common.util.FakePlayerFactory; import net.minecraftforge.fml.common.*; @@ -31,6 +38,7 @@ 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 boolean rejectedFirstCorpseOpen; public RuntimeChecks() { MultiplayerChecks.registerChannel(); net.minecraftforge.eventbus.api.IEventBus bus=net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus(); @@ -40,10 +48,18 @@ public RuntimeChecks() { new ClientChecks().register();new MultiplayerClientChecks().register();new VisibilityClientChecks().register();new RespawnPacketClientChecks().register(); }); } + @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) { + 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) { EntityLootableBody body=(EntityLootableBody)event.getEntity(); - require(event.getWorld().getChunk(body.getPosition()).isLoaded(),"Manual corpse fixture spawned before chunk admission: "+body.getPosition()+" name="+body.getCustomNameTag()); + require(event.getWorld().getChunkProvider().chunkExists(net.minecraft.util.math.MathHelper.floor(body.posX)>>4,net.minecraft.util.math.MathHelper.floor(body.posZ)>>4),"Manual corpse fixture spawned before chunk admission: "+body.getPosition()+" name="+body.getCustomNameTag()); } } @SubscribeEvent public void started(FMLServerStartedEvent event) { @@ -63,7 +79,7 @@ public RuntimeChecks() { } catch (Throwable error) { failure = error; error.printStackTrace(); - } finally { server.initiateShutdown(); } + } finally { server.initiateShutdown(false); } } static void require(boolean condition, String message) { if (!condition) throw new AssertionError(message); @@ -72,11 +88,11 @@ static boolean sameStack(ItemStack first,ItemStack second) { return ItemStack.areItemStacksEqual(first==null?ItemStack.EMPTY:first,second==null?ItemStack.EMPTY:second); } public static void run(MinecraftServer server, String phase) throws Exception { - WorldServer world = server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)); + ServerWorld world = server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)); File folder = world.getSaveHandler().getWorldDirectory(); - require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.4.113021"), "identity/version"); + require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.5.114041"), "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.4.113021") : "2.4.4.113021"), "loaded release version"); + System.getProperty("lootablebodies.expectedIssueVersion","2.4.5.114041") : "2.4.5.114041"), "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 && @@ -174,41 +190,33 @@ public static void run(MinecraftServer server, String phase) throws Exception { require(!world.getChunkProvider().chunkExists(256, 256), "deferred chunk opened early"); } } else throw new AssertionError("Unknown phase " + phase); - server.saveAllWorlds(false); + server.save(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(WorldServer world,int x,int z) { - net.minecraft.world.biome.Biome[] biomes=new net.minecraft.world.biome.Biome[256];java.util.Arrays.fill(biomes,net.minecraft.init.Biomes.PLAINS);return new net.minecraft.world.chunk.Chunk(world,x,z,biomes,net.minecraft.world.chunk.UpgradeData.EMPTY,world.getPendingBlockTicks(),world.getPendingFluidTicks(),0L); + static net.minecraft.world.chunk.Chunk emptyChunk(ServerWorld world,int x,int z) { + net.minecraft.world.biome.Biome[] biomes=new net.minecraft.world.biome.Biome[256];java.util.Arrays.fill(biomes,net.minecraft.world.biome.Biomes.PLAINS);return new net.minecraft.world.chunk.Chunk(world,new net.minecraft.util.math.ChunkPos(x,z),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); - NBTTagCompound custom = new NBTTagCompound(); - custom.setString("fixture", "slot-" + index); - custom.setTag("typed_longs",new NBTTagLongArray(new long[]{Long.MIN_VALUE,index,Long.MAX_VALUE})); - NBTTagCompound nested = new NBTTagCompound(); nested.setInt("unchanged", index * 17); - NBTTagList contents = new NBTTagList(); - NBTTagCompound item = new NBTTagCompound(); new ItemStack(Items.DIAMOND,3).write(item); contents.add(item); - nested.setTag("contents", contents); custom.setTag("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.TextComponentString("Fixture " + index)); - if (index == 4) stack.addEnchantment(net.minecraft.init.Enchantments.UNBREAKING, 2); + CompoundNBT custom = new CompoundNBT(); + 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); + 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); return stack; } - static net.minecraft.world.chunk.Chunk awaitChunk(WorldServer world,int x,int z) { - net.minecraft.world.chunk.Chunk admitted=world.getChunk(x,z); - // Generation completes on a worker, then queues Chunk.onLoad on the - // server. Admit this disposable chunk before manually populating it. - for(int tasks=0;!admitted.isLoaded();tasks++) { - require(tasks<10000,"Native fixture chunk admission did not complete"); - java.util.concurrent.FutureTask pending=world.getServer().futureTaskQueue.poll(); - require(pending!=null,"Missing native chunk admission task");pending.run(); - try{pending.get();}catch(Exception failed){throw new AssertionError("Native fixture admission failed",failed);} - } - require(admitted.isLoaded(),"Fixture must await native chunk admission before spawning a corpse"); + 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"); return admitted; } - static EntityLootableBody fixture(WorldServer world, UUID uuid, int coordinate, boolean full) { + static EntityLootableBody fixture(ServerWorld 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); @@ -217,36 +225,36 @@ static EntityLootableBody fixture(WorldServer world, UUID uuid, int coordinate, for (int i=0; i<48; i++) if (full || i % 7 == 0 || i == 47) body.setInventorySlotContents(i,stack(i)); if (full) { body.addItem(stack(70)); body.addItem(stack(71)); } disableVacuum(body); - require(world.spawnEntity(body), "fixture spawn"); + require(world.addEntity(body), "fixture spawn"); return body; } - static void verifyRoundTrip(WorldServer world, EntityLootableBody body) { - NBTTagCompound before = new NBTTagCompound(); body.writeUnlessPassenger(before); + static void verifyRoundTrip(ServerWorld world, EntityLootableBody body) { + CompoundNBT before = new CompoundNBT(); body.writeUnlessPassenger(before); require(before.getString("id").equals("lootablebodies:corpse"), "persistent entity name: " + before.getString("id")); - Entity copy = net.minecraft.entity.EntityType.create(before.copy(), world); + Entity copy = FixtureApi.loadEntity(before.copy(), world); require(copy instanceof EntityLootableBody, "NBT factory registration"); - NBTTagCompound after = new NBTTagCompound(); copy.writeUnlessPassenger(after); + CompoundNBT after = new CompoundNBT(); copy.writeUnlessPassenger(after); compare(before,after); if (body.getUniqueID().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, WorldServer world, File folder) throws Exception { - EntityPlayerMP player = new EntityPlayerMP(server, world, + 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.NetHandlerPlayServer(server, - new net.minecraft.network.NetworkManager(net.minecraft.network.EnumPacketDirection.SERVERBOUND), player); - player.setPosition(32.5,80,0.5); require(world.getChunk(2,0).isLoaded(),"Death fixture must await native chunk admission before spawning its corpse"); - world.getGameRules().setOrCreateGameRule("keepInventory","false",null); + 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(EntityEquipmentSlot.HEAD,stack(0)); - player.setItemStackToSlot(EntityEquipmentSlot.OFFHAND,stack(5)); + player.setItemStackToSlot(EquipmentSlotType.HEAD,stack(0)); + player.setItemStackToSlot(EquipmentSlotType.OFFHAND,stack(5)); player.onDeath(DamageSource.OUT_OF_WORLD); EntityLootableBody corpse = null; - for (Entity entity : new ArrayList(world.loadedEntityList)) + 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"); @@ -261,12 +269,12 @@ static void death(MinecraftServer server, WorldServer world, File folder) throws 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.motionX=0; corpse.motionY=0; corpse.motionZ=0; + 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); disableVacuum(corpse); saveExpected(folder,"death",corpse); } - static EntityLootableBody find(WorldServer world, UUID uuid) { - for(Entity entity : world.loadedEntityList) if(entity.getUniqueID().equals(uuid)) return (EntityLootableBody)entity; + static EntityLootableBody find(ServerWorld world, UUID uuid) { + for(Entity entity : FixtureApi.entities(world)) if(entity.getUniqueID().equals(uuid)) return (EntityLootableBody)entity; throw new AssertionError("Saved corpse missing " + uuid); } static void disableVacuum(EntityLootableBody body) { @@ -276,16 +284,16 @@ static void disableVacuum(EntityLootableBody body) { } catch (Exception error) { throw new AssertionError(error); } } static void saveExpected(File folder,String name,EntityLootableBody body) throws Exception { - NBTTagCompound data = new NBTTagCompound(); body.writeUnlessPassenger(data); + CompoundNBT data = new CompoundNBT(); body.writeUnlessPassenger(data); CompressedStreamTools.writeCompressed(data,new FileOutputStream(new File(folder,name+".nbt"))); } static void verifyExpected(File folder,String name,EntityLootableBody body) throws Exception { - NBTTagCompound expected = CompressedStreamTools.readCompressed(new FileInputStream(new File(folder,name+".nbt"))); - NBTTagCompound actual = new NBTTagCompound(); body.writeUnlessPassenger(actual); compare(expected,actual); + CompoundNBT expected = CompressedStreamTools.readCompressed(new FileInputStream(new File(folder,name+".nbt"))); + CompoundNBT actual = new CompoundNBT(); body.writeUnlessPassenger(actual); compare(expected,actual); } - static void compare(NBTTagCompound expected,NBTTagCompound actual) { + static void compare(CompoundNBT expected,CompoundNBT actual) { for(String key : Arrays.asList("id","UUIDMost","UUIDLeast","Pos","Rotation","CustomName","Equipment","Aux","DeathTime","DecayTime")) - require(Objects.equals(expected.getTag(key),actual.getTag(key)),"Saved field differs " + key + ": " + expected.getTag(key) + " != " + actual.getTag(key)); - if(expected.hasKey("Name")) require(expected.getString("Name").equals(actual.getString("Name")),"profile name"); + 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 e7a0904..ffc8022 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.Potion; +import net.minecraft.potion.Effect; 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/TypedNbt.java b/src/integrationTest/java/cyano/lootable/buildtest/TypedNbt.java index 6e17230..5b87ae4 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/TypedNbt.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/TypedNbt.java @@ -1,25 +1,32 @@ package cyano.lootable.buildtest; +import net.minecraft.nbt.INBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.IntArrayNBT; +import net.minecraft.nbt.FloatNBT; +import net.minecraft.nbt.DoubleNBT; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ByteArrayNBT; import java.util.TreeSet; import net.minecraft.nbt.*; /** Compound order is immaterial; NBT types, list order and every array byte matter. */ final class TypedNbt { - static String canonical(INBTBase tag) { + static String canonical(INBT tag) { if(tag==null)return "null"; StringBuilder text=new StringBuilder().append(tag.getId()).append(':'); - if(tag instanceof NBTTagCompound) { - NBTTagCompound compound=(NBTTagCompound)tag; - for(String key:new TreeSet<>(compound.keySet()))text.append(key.length()).append(':').append(key).append('=').append(canonical(compound.getTag(key))).append(';'); - } else if(tag instanceof NBTTagList) { - NBTTagList list=(NBTTagList)tag;text.append(list.getTagType()).append('/').append(list.size()).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('['); for(int i=0;icom.mojang.datafixers.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.datafixers.Dynamic input,int source,int target) { com.mojang.datafixers.Dynamic result=nativeFixer.update(type,input,source,target); - if(type.typeName().equals("item_stack") && sourceconversion("lootablebodies.Corpse",new NBTTagInt(512))); - check("native_111_identity_survives",()->conversion("lootablebodies:corpse",new NBTTagInt(922))); + check("legacy_id_before_entity_construction",()->conversion("lootablebodies.Corpse",new IntNBT(512))); + check("native_111_identity_survives",()->conversion("lootablebodies:corpse",new IntNBT(922))); check("missing_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",null)); - check("wrong_type_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",new NBTTagString("damaged"))); - check("nonfinite_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",new NBTTagDouble(Double.NaN))); - check("negative_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",new NBTTagInt(-1))); - check("incorrect_new_stamp_does_not_skip_legacy_items",()->conversion("lootablebodies.Corpse",new NBTTagInt(922))); + check("wrong_type_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",new StringNBT("damaged"))); + check("nonfinite_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",new DoubleNBT(Double.NaN))); + check("negative_chunk_version_preserves_48_slots",()->conversion("lootablebodies.Corpse",new IntNBT(-1))); + check("incorrect_new_stamp_does_not_skip_legacy_items",()->conversion("lootablebodies.Corpse",new IntNBT(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); @@ -60,10 +68,16 @@ static void run(MinecraftServer server) throws Exception { check("maintained111_still_receives112_item_fixers",()->bed(922,1,14)); check("current112_marker_never_reapplies_older_item_fixers",()->bed(1343,2,0)); check("legacy_xp_sound_remaps_without_ignoring_unrelated_entries",UpgradeChecks::soundMapping); + 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.getTrackingRange()*16==32,"Native tracking range changed from32blocks"); + RuntimeChecks.require(cyano.lootable.LootableBodies.CORPSE.getUpdateFrequency()==1 && cyano.lootable.LootableBodies.CORPSE.shouldSendVelocityUpdates(),"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)); check("bed_with_no_source_evidence_stays_raw_and_retriable",UpgradeChecks::ambiguousBed); - System.out.println("LOOTABLE_BODIES_UPGRADE_CONTRACT_RESULT checks=23 failures="+failures); + System.out.println("LOOTABLE_BODIES_UPGRADE_CONTRACT_RESULT checks=26 failures="+failures); RuntimeChecks.require(failures==0,"Upgrade contracts: "+failures); } private static void check(String name,Check check) { @@ -71,92 +85,115 @@ 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 { - NBTTagCompound entity=body(legacy?"lootablebodies.Corpse":"lootablebodies:corpse"); - if(!legacy)entity.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").setString("id","minecraft:pig"); - NBTTagCompound input=chunk(entity,legacy?new NBTTagInt(1343):null); - NBTTagCompound chest=new NBTTagCompound();chest.setString("id","minecraft:chest");chest.setInt("x",2);chest.setInt("y",80);chest.setInt("z",2); - NBTTagCompound bed=new NBTTagCompound();bed.setString("id","minecraft:bed");bed.setByte("Count",(byte)1);bed.setShort("Damage",(short)0);bed.setByte("Slot",(byte)0); - NBTTagList items=new NBTTagList();items.add(bed);chest.setTag("Items",items);NBTTagList tiles=new NBTTagList();tiles.add(chest);input.getCompound("Level").setTag("TileEntities",tiles); - NBTTagCompound fixed=processChunk(input,"1.12.2"); - NBTTagCompound actual=fixed.getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); - NBTTagCompound expected=bed.copy();expected.setString("id","minecraft:white_bed");expected.removeTag("Damage"); + CompoundNBT 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?new IntNBT(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"); RuntimeChecks.require(NameLifetimeChecks.same(expected,actual),"Native white bed changed under chunk conversion: "+actual); - NBTTagCompound converted=saved(fixed.getCompound("Level").getList("Entities",10).getCompound(0)); + CompoundNBT 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 { - NBTTagCompound input=body("lootablebodies:corpse"),item=new NBTTagCompound();item.setString("id","minecraft:bed");item.setByte("Count",(byte)1);item.setShort("Damage",(short)0);item.setByte("Slot",(byte)6); - NBTTagList items=new NBTTagList();items.add(item);input.setTag("Equipment",items); - NBTTagCompound 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.getTag("Data")) && record.getCompound("SourceVersions").getBoolean("Ambiguous"),"Ambiguous bed was guessed or lost"); + 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); + 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); RuntimeChecks.require(!again.isEmpty(),"Raw recoverable bed allowed empty-corpse decay"); - NBTTagCompound twice=new NBTTagCompound();again.writeAdditional(twice); - RuntimeChecks.require(NameLifetimeChecks.same(once.getTag("LootableBodiesUnresolved"),twice.getTag("LootableBodiesUnresolved")),"Ambiguous bed duplicated or changed during reload"); + CompoundNBT twice=new CompoundNBT();again.writeAdditional(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)); - Class.forName("cyano.lootable.upgrade.LegacyRegistryMappings").getMethod("sounds",net.minecraftforge.event.RegistryEvent.MissingMappings.class).invoke(null,event); + 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(other.getAction()==net.minecraftforge.event.RegistryEvent.MissingMappings.Action.DEFAULT,"Unrelated missing mappings must retain Forge's normal handling"); } + private static void nativeRenames() { + rename(net.minecraftforge.registries.ForgeRegistries.ITEMS,"sign","oak_sign"); + rename(net.minecraftforge.registries.ForgeRegistries.ITEMS,"rose_red","red_dye"); + rename(net.minecraftforge.registries.ForgeRegistries.ITEMS,"cactus_green","green_dye"); + rename(net.minecraftforge.registries.ForgeRegistries.ITEMS,"dandelion_yellow","yellow_dye"); + rename(net.minecraftforge.registries.ForgeRegistries.BLOCKS,"sign","oak_sign"); + rename(net.minecraftforge.registries.ForgeRegistries.BLOCKS,"wall_sign","oak_wall_sign"); + } + private static > void rename(net.minecraftforge.registries.IForgeRegistry registry,String oldName,String current) { + net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping old=new net.minecraftforge.event.RegistryEvent.MissingMappings.Mapping<>(registry,registry,new net.minecraft.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 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(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.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(!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 { - NBTTagCompound input=body("lootablebodies:corpse"); - input.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").setString("id","minecraft:pig"); - NBTTagCompound item=new NBTTagCompound();item.setString("id","minecraft:bed");item.setByte("Count",(byte)2);item.setShort("Damage",(short)0);item.setByte("Slot",(byte)41);input.getList("Equipment",10).set(41,item); - if(marker>0)input.setInt("LootableBodiesDataVersion",marker); - NBTTagCompound result=saved(process(chunk(input,new NBTTagInt(source)))); - NBTTagList equipment=result.getList("Equipment",10);NBTTagCompound found=null; + CompoundNBT 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); + if(marker>0)input.putInt("LootableBodiesDataVersion",marker); + CompoundNBT result=saved(process(chunk(input,new IntNBT(source)))); + ListNBT equipment=result.getList("Equipment",10);CompoundNBT found=null; for(int i=0;i preservation=null;Object previous=null;Field active=null; @@ -165,97 +202,97 @@ private static NBTTagCompound processChunk(NBTTagCompound source,String minecraf // 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) { } - NBTTagCompound input=source.copy(); - if(preservation!=null)try{input=(NBTTagCompound)preservation.getMethod("chunkContext",NBTTagCompound.class).invoke(null,input);}catch(NoSuchMethodException beforeBoundaryFix){} - NBTTagCompound result=nativeChunk(input); - if(preservation!=null)try{preservation.getMethod("clearChunkContext",NBTTagCompound.class).invoke(null,result);}catch(NoSuchMethodException beforeBoundaryFix){} + 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){} return result; } finally {if(active!=null)active.set(null,previous);} } - private static NBTTagCompound saved(NBTTagCompound fixed) { + private static CompoundNBT saved(CompoundNBT fixed) { RuntimeChecks.require(fixed.getString("id").equals("lootablebodies:corpse"),"legacy identity was not converted before construction"); - Entity entity=net.minecraft.entity.EntityType.create(fixed.copy(),world); + Entity entity=FixtureApi.loadEntity(fixed.copy(),world); RuntimeChecks.require(entity instanceof EntityLootableBody,"corpse factory returned no body"); - NBTTagCompound saved=new NBTTagCompound();((EntityLootableBody)entity).writeAdditional(saved);return saved; + CompoundNBT saved=new CompoundNBT();((EntityLootableBody)entity).writeAdditional(saved);return saved; } - private static Map inventory(NBTTagCompound body) { + private static Map inventory(CompoundNBT body) { Map counts=new TreeMap<>();for(String key:Arrays.asList("Equipment","Aux")) { - NBTTagList list=body.getList(key,10);for(int i=0;i0,"malformed original fields were lost in vanilla fixing"); } private static void missingItem() throws Exception { - NBTTagCompound body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20);raw.setString("id","absentfixture:keepsake"); - NBTTagCompound result=saved(process(chunk(body,new NBTTagInt(512))));NBTTagList recovery=result.getList("LootableBodiesUnresolved",10); - RuntimeChecks.require(recovery.size()==1,"unavailable item not retained exactly once");NBTTagCompound record=recovery.getCompound(0); - RuntimeChecks.require(NameLifetimeChecks.same(raw,record.getTag("Data")) && record.getCompound("SourceVersions").getInt("DataVersion")==512,"unavailable item lost its original payload or source context"); + CompoundNBT body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20);raw.putString("id","absentfixture:keepsake"); + CompoundNBT result=saved(process(chunk(body,new IntNBT(512))));ListNBT recovery=result.getList("LootableBodiesUnresolved",10); + RuntimeChecks.require(recovery.size()==1,"unavailable item not retained exactly once");CompoundNBT 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 { - NBTTagCompound raw=body("lootablebodies.Corpse"),fixed=process(chunk(raw,new NBTTagInt(512))),once=saved(fixed); - NBTTagCompound again=nativeChunk(chunk(fixed.copy(),new NBTTagInt(512))).getCompound("Level").getList("Entities",10).getCompound(0); - NBTTagCompound twice=saved(again);RuntimeChecks.require(inventory(once).equals(inventory(twice)),"repeated data fixing duplicated or lost items"); + CompoundNBT raw=body("lootablebodies.Corpse"),fixed=process(chunk(raw,new IntNBT(512))),once=saved(fixed); + CompoundNBT again=nativeChunk(chunk(fixed.copy(),new IntNBT(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"); } private static void recovery() throws Exception { - NBTTagCompound body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20).copy();raw.setString("id","absentfixture:keepsake"); - NBTTagCompound record=new NBTTagCompound();record.setString("Source","Aux");record.setTag("Data",raw.copy());NBTTagList records=new NBTTagList();records.add(record);body.setTag("LootableBodiesUnresolved",records); - NBTTagCompound output=saved(process(chunk(body,new NBTTagInt(512))));NBTTagCompound kept=output.getList("LootableBodiesUnresolved",10).getCompound(0); - RuntimeChecks.require(kept.getString("Source").equals("Aux") && NameLifetimeChecks.same(raw,kept.getTag("Data")),"existing Source/Data payload changed"); + 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,new IntNBT(512))));CompoundNBT 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 NBTTagCompound fullSave(NBTTagCompound fixed) { - Entity entity=net.minecraft.entity.EntityType.create(fixed.copy(),world);RuntimeChecks.require(entity instanceof EntityLootableBody,"Missing body for full save"); - NBTTagCompound result=new NBTTagCompound();entity.writeUnlessPassenger(result);return result; + private static CompoundNBT fullSave(CompoundNBT 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; } private static void savedCounter() throws Exception { - NBTTagCompound source=body("lootablebodies.Corpse");source.getList("Equipment",10).getCompound(20).getCompound("tag").setInt("upgrade_counter",0); - NBTTagCompound once=fullSave(process(chunk(source,new NBTTagInt(512)))); - NBTTagCompound counted=null; - for(INBTBase entry:once.getList("Equipment",10))if(((NBTTagCompound)entry).getByte("Slot")==20)counted=(NBTTagCompound)entry; + CompoundNBT source=body("lootablebodies.Corpse");source.getList("Equipment",10).getCompound(20).getCompound("tag").putInt("upgrade_counter",0); + CompoundNBT once=fullSave(process(chunk(source,new IntNBT(512)))); + CompoundNBT counted=null; + for(INBT entry:once.getList("Equipment",10))if(((CompoundNBT)entry).getByte("Slot")==20)counted=(CompoundNBT)entry; RuntimeChecks.require(counted!=null && counted.getCompound("tag").getInt("upgrade_counter")==1,"Item fixer did not run exactly once on first load: "+counted); - NBTTagCompound twice=fullSave(process(chunk(once,new NBTTagInt(922))));RuntimeChecks.require(inventory(once).equals(inventory(twice)),"Old item version was applied again after saving migrated items"); + CompoundNBT twice=fullSave(process(chunk(once,new IntNBT(922))));RuntimeChecks.require(inventory(once).equals(inventory(twice)),"Old item version was applied again after saving migrated items"); } - private static NBTTagCompound currentStamps(NBTTagCompound chunk) {chunk.setInt("DataVersion",1631);chunk.setTag("ForgeDataVersion",new NBTTagCompound());chunk.getCompound("ForgeDataVersion").setInt("minecraft",-1);return chunk;} + 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 void migratedDamagedStamp() throws Exception { - NBTTagCompound source=body("lootablebodies:corpse");source.setInt("LootableBodiesDataVersion",1);source.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").setString("id","minecraft:pig"); - NBTTagCompound result=saved(process(currentStamps(chunk(source,null))));RuntimeChecks.require(expectedInventory(source,922).equals(inventory(result)),"Damaged version stamp truncated an already migrated Equipment list"); + 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"); } private static void forgedModStamp() throws Exception { - NBTTagCompound source=body("lootablebodies.Corpse"),expected=source.copy(); - NBTTagCompound input=currentStamps(chunk(source,new NBTTagInt(512)));input.getCompound("ForgeDataVersion").setInt("minecraft",512); + CompoundNBT source=body("lootablebodies.Corpse"),expected=source.copy(); + CompoundNBT input=currentStamps(chunk(source,new IntNBT(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 { - NBTTagCompound fixed=process(chunk(body("lootablebodies.Corpse"),new NBTTagInt(512))); - EntityLootableBody entity=(EntityLootableBody)net.minecraft.entity.EntityType.create(fixed.copy(),world);entity.clear();entity.writeAdditional(fixed); - EntityLootableBody reloaded=(EntityLootableBody)net.minecraft.entity.EntityType.create(fixed.copy(),world); + CompoundNBT fixed=process(chunk(body("lootablebodies.Corpse"),new IntNBT(512))); + EntityLootableBody entity=(EntityLootableBody)FixtureApi.loadEntity(fixed.copy(),world);entity.clear();entity.writeAdditional(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 { - NBTTagCompound input=chunk(body("lootablebodies.Corpse"),new NBTTagInt(922)),versions=new NBTTagCompound(); - versions.setInt("minecraft",922);versions.setInt("lootablebodies",1);versions.setInt("lootablebodiesbuildtest",1);input.setTag("ForgeDataVersion",versions); + CompoundNBT input=chunk(body("lootablebodies.Corpse"),new IntNBT(922)),versions=new CompoundNBT(); + 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 { - NBTTagCompound input=chunk(body("lootablebodies.Corpse"),new NBTTagInt(922)),versions=new NBTTagCompound(); - versions.setInt("minecraft",922);versions.setInt("lootablebodies",1);versions.setInt("lootablebodiesbuildtest",1);input.setTag("ForgeDataVersion",versions); - NBTTagCompound chest=new NBTTagCompound();chest.setString("id","Chest");chest.setInt("x",2);chest.setInt("y",80);chest.setInt("z",2); - NBTTagCompound banner=new NBTTagCompound();banner.setString("id","minecraft:banner");banner.setByte("Count",(byte)2);banner.setShort("Damage",(short)0);banner.setByte("Slot",(byte)0); - NBTTagCompound tag=new NBTTagCompound(),block=new NBTTagCompound();block.setInt("Base",5);tag.setTag("BlockEntityTag",block);banner.setTag("tag",tag);NBTTagList items=new NBTTagList();items.add(banner);chest.setTag("Items",items);NBTTagList tiles=new NBTTagList();tiles.add(chest);input.getCompound("Level").setTag("TileEntities",tiles); - NBTTagCompound 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.hasKey("Damage") && fixed.getByte("Count")==2,"False version stamps skipped the vanilla banner conversion: "+fixed); + CompoundNBT input=chunk(body("lootablebodies.Corpse"),new IntNBT(922)),versions=new CompoundNBT(); + 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); + 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(NBTTagCompound source,int version) { - NBTTagCompound expected=source.copy(); + private static Map expectedInventory(CompoundNBT source,int version) { + CompoundNBT expected=source.copy(); for(String key:Arrays.asList("Equipment","Aux")) { - NBTTagList items=expected.getList(key,10); + ListNBT items=expected.getList(key,10); for(int i=0;i(world().loadedEntityList);a.onKillCommand();first=newCorpse();saveId("first",first);phase(2); + // A real death offset exposes assumptions about the corpse's coordinates. + if(Boolean.getBoolean("lootablebodies.visibility.offsetFirstDeath")) + a.connection.setPlayerLocation(96.5,PLAYER_Y,98.5,0,0); + before=new HashSet<>(FixtureApi.entities(world()));a.onKillCommand();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(96.5,PLAYER_Y,93.5,0,25);phase(4); + a.connection.setPlayerLocation(first.posX,first.posY,first.posZ-2.5,0,25); + b.connection.setPlayerLocation(first.posX+1.5,first.posY,first.posZ-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"); 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); } else if(phase==6 && both(6)) { - RuntimeChecks.require(world().getEntityTracker().getTrackingPlayers(first).contains(b) && !world().getEntityTracker().getTrackingPlayers(first).contains(a),"Range assertions differ from tracker"); + 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); } else if(phase==7 && both(7)) {mark("range_return_respawns_same_uuid");phase(8); } else if(phase==8 && a==null) {phase(9); @@ -98,10 +111,10 @@ public VisibilityChecks(MinecraftServer server) throws Exception { } 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.inventoryContainer.detectAndSendChanges();phase(13); + a.inventory.mainInventory.set(9,RuntimeChecks.stack(411));a.container.detectAndSendChanges();phase(13); } else if(phase==13 && both(13)) { - before=new HashSet<>(world().loadedEntityList);EntityTNTPrimed tnt=new EntityTNTPrimed(world(),a.posX,a.posY,a.posZ,null);tnt.setFuse(0); - RuntimeChecks.require(world().spawnEntity(tnt),"TNT fixture spawn");phase(14); + before=new HashSet<>(FixtureApi.entities(world()));TNTEntity tnt=new TNTEntity(world(),a.posX,a.posY,a.posZ,null);tnt.setFuse(0); + RuntimeChecks.require(world().addEntity(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);} @@ -115,50 +128,60 @@ public VisibilityChecks(MinecraftServer server) throws Exception { RuntimeChecks.require(has(first,410),"Earlier corpse inventory changed during TNT/reconnect checks"); a.closeScreen();b.closeScreen();File folder=world().getSaveHandler().getWorldDirectory(); RuntimeChecks.saveExpected(folder,"visibility-first",first);RuntimeChecks.saveExpected(folder,"visibility-second",second); - NBTTagCompound state=new NBTTagCompound();state.setUniqueId("First",first.getUniqueID());state.setUniqueId("Second",second.getUniqueID());state.setUniqueId("Player",a.getUniqueID());state.setTag("Inventory",a.inventory.write(new NBTTagList())); + 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.saveAllWorlds(false);mark("tagged_inventory_recovered_and_complete_nbt_saved");phase(18); + 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(); + Files.write(file("visibility.pass").toPath(),new byte[]{1});System.out.println("LOOTABLE_BODIES_VISIBILITY_PASS");finished=true;server.initiateShutdown(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("both_clients_leave_and_corpse_chunk_really_unloads"); - net.minecraft.world.storage.ThreadedFileIOBase.getThreadedIOInstance().waitForFinish(); + FixtureApi.flush(server); a.connection.setPlayerLocation(first.posX,first.posY,first.posZ-2.5,0,25);b.connection.setPlayerLocation(first.posX+1.5,first.posY,first.posZ-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.saveAllWorlds(false);phase(18); + mark("chunk_reentry_both_clients_render_same_uuids_and_inventory");server.getPlayerList().saveAllPlayerData();server.save(false,true,false);phase(18); } - } catch(Throwable failure) {failure.printStackTrace();finished=true;server.initiateShutdown();} + } catch(Throwable failure) {failure.printStackTrace();finished=true;server.initiateShutdown(false);} } - private EntityLootableBody newCorpse(){for(Entity e:world().loadedEntityList)if(e instanceof EntityLootableBody&&!before.contains(e))return (EntityLootableBody)e;throw new AssertionError("No server corpse for real death");} + 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 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));Files.move(tmp.toPath(),file("phase").toPath(),StandardCopyOption.REPLACE_EXISTING,StandardCopyOption.ATOMIC_MOVE);System.out.println("VISIBILITY_PHASE "+n);} + 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)); + // Windows readers can briefly hold this verifier IPC file without delete sharing. + // Keep atomic publication; every other I/O failure and a persistent lock still fail. + for(int attempt=0;;attempt++) { + try {Files.move(tmp.toPath(),file("phase").toPath(),StandardCopyOption.REPLACE_EXISTING,StandardCopyOption.ATOMIC_MOVE);break;} + catch(AccessDeniedException busy) {if(attempt>=20)throw busy;Thread.sleep(5L);} + } + 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(EntityPlayerMP p,int i){int count=0;for(int s=0;s=expected.getLong(key)&&actual.getLong(key)<=w.getGameTime(),"Reloaded live timer out of range: "+key); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java index 97b3eb7..cd46a6f 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java @@ -1,4 +1,8 @@ package cyano.lootable.buildtest; +import net.minecraft.client.gui.screen.MainMenuScreen; +import net.minecraft.client.gui.screen.DeathScreen; +import net.minecraft.entity.boss.dragon.phase.Phase; +import net.minecraft.util.text.StringTextComponent; import cyano.lootable.entities.EntityLootableBody; import cyano.lootable.graphics.CorpseGUIContainer; @@ -8,15 +12,18 @@ 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.inventory.ClickType; +import net.minecraft.inventory.container.ClickType; import net.minecraft.item.ItemStack; import net.minecraft.util.*; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; /** Receives real Forge spawn/destroy/respawn packets; never takes native input focus. */ final class VisibilityClientChecks { @@ -28,7 +35,7 @@ final class VisibilityClientChecks { void register(){if(Boolean.getBoolean("lootablebodies.visibility")&&role!=null){net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);MinecraftForge.EVENT_BUS.register(this);}} @SubscribeEvent public void mouse(net.minecraftforge.client.event.GuiScreenEvent.MouseClickedEvent.Pre e){e.setCanceled(true);} @SubscribeEvent public void key(net.minecraftforge.client.event.GuiScreenEvent.KeyboardKeyPressedEvent.Pre e){e.setCanceled(true);} - @SubscribeEvent public void rendered(RenderLivingEvent.Post 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().getUniqueID());} @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(); @@ -39,8 +46,8 @@ final class VisibilityClientChecks { 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 GuiMainMenu && (phase==0 || phase==9 || phase==11)) { - connecting=true;mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.limitFramerate=30;mc.gameSettings.renderDistanceChunks=4; + 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; FixtureConnections.connect(mc); } if(mc.world==null || mc.player==null)return; @@ -52,11 +59,11 @@ final class VisibilityClientChecks { 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 GuiGameOver))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(); } else if(phase==3 || phase==15) { if(role.equals("A")) { - if(mc.currentScreen instanceof GuiGameOver){if(!respawnSent){mc.player.respawnPlayer();respawnSent=true;}return;} + if(mc.currentScreen instanceof DeathScreen){if(!respawnSent){mc.player.respawnPlayer();respawnSent=true;}return;} if(!mc.player.isAlive())return; } if(body(mc,phase==3?"first":"second")!=null)ack(); @@ -64,7 +71,7 @@ final class VisibilityClientChecks { EntityLootableBody b=body(mc,phase==16?"second":"first");if(b==null)return; if(!(mc.currentScreen instanceof CorpseGUIContainer)) { if(!rendered.contains(b.getUniqueID()))return; - mc.playerController.interactWithEntity(mc.player,b,EnumHand.MAIN_HAND);return; + if(frames%10==0)mc.playerController.interactWithEntity(mc.player,b,Hand.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; @@ -75,7 +82,7 @@ final class VisibilityClientChecks { } 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();} } else if(phase==8 || phase==10) { - if(role.equals(phase==8?"A":"B")){mc.world.sendQuittingDisconnectingPacket();mc.loadWorld(null);mc.displayGuiScreen(new GuiMainMenu());connecting=false;} + if(role.equals(phase==8?"A":"B")){mc.world.sendQuittingDisconnectingPacket();mc.func_213254_o();mc.displayGuiScreen(new MainMenuScreen());connecting=false;} ack(); } else if(phase==12 || phase==120) { EntityLootableBody b=body(mc,"first");if(b!=null&&!b.removed&&rendered.contains(b.getUniqueID()))ack(); @@ -86,7 +93,7 @@ final class VisibilityClientChecks { } 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.TextComponentString("Disposable check complete")); + mc.getConnection().getNetworkManager().closeChannel(new net.minecraft.util.text.StringTextComponent("Disposable check complete")); finished=true; } else if(phase==20) { if(body(mc,"first")==null&&body(mc,"second")==null)ack(); @@ -99,7 +106,7 @@ final class VisibilityClientChecks { } 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:mc.world.loadedEntityList)if(e instanceof EntityLootableBody&&e.getUniqueID().equals(id))return (EntityLootableBody)e;return null; + for(Entity e:FixtureApi.entities(mc.world))if(e instanceof EntityLootableBody&&e.getUniqueID().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.mainWindow.getFramebufferWidth(),mc.mainWindow.getFramebufferHeight(),mc.getFramebuffer(),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 fad1ca8..c84edee 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java @@ -14,7 +14,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.resources.*; import net.minecraft.server.management.PlayerProfileCache; -import net.minecraft.tileentity.TileEntitySkull; +import net.minecraft.tileentity.SkullTileEntity; import net.minecraft.util.ResourceLocation; @@ -39,16 +39,16 @@ 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(TileEntitySkull.class,PlayerProfileCache.class); - Field service=field(TileEntitySkull.class,MinecraftSessionService.class); + Field cache=field(SkullTileEntity.class,PlayerProfileCache.class); + Field service=field(SkullTileEntity.class,MinecraftSessionService.class); Object oldCache=cache.get(null),oldService=service.get(null); AtomicInteger requests=new AtomicInteger(); CountDownLatch failedRequest=new CountDownLatch(1); try { - TileEntitySkull.setProfileCache(new PlayerProfileCache(null,new File(mc.gameDir,"fault-usercache.json")) { + SkullTileEntity.setProfileCache(new PlayerProfileCache(null,new File(mc.gameDir,"fault-usercache.json")) { @Override public GameProfile getGameProfileForUsername(String name) {return new GameProfile(profile.getId(),name);} }); - TileEntitySkull.setSessionService((MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ + SkullTileEntity.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()); })); @@ -57,7 +57,7 @@ static void run(Minecraft mc) throws Exception { 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. - TileEntitySkull.setSessionService((MinecraftSessionService)Proxy.newProxyInstance(MinecraftSessionService.class.getClassLoader(),new Class[]{MinecraftSessionService.class},(proxy,method,args)->{ + SkullTileEntity.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()); })); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java index 2c18016..6b6de39 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java @@ -1,4 +1,5 @@ package cyano.lootable.buildtest; +import net.minecraft.client.gui.screen.MainMenuScreen; import java.io.File; import java.lang.reflect.Field; @@ -9,7 +10,10 @@ import cyano.lootable.entities.EntityLootableBody; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; -import net.minecraft.client.gui.inventory.GuiInventory; +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.resources.SkinManager; import net.minecraft.util.ScreenShotHelper; import net.minecraft.world.*; @@ -19,8 +23,8 @@ final class VisibilitySkinClientChecks { private Field skinField; private SkinManager original; void tick(Minecraft mc) throws Exception { - if(stage==0 && mc.currentScreen instanceof GuiMainMenu) { - mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.limitFramerate=30;mc.gameSettings.renderDistanceChunks=4; + if(stage==0 && mc.currentScreen instanceof MainMenuScreen) { + mc.gameSettings.pauseOnLostFocus=false;mc.gameSettings.framerateLimit=30;mc.gameSettings.renderDistanceChunks=4; mc.launchIntegratedServer("skin-fixture","Skin fixture",new WorldSettings(4267321,GameType.SURVIVAL,false,false,WorldType.FLAT).enableCommands());stage=1; } else if(stage==1 && mc.world!=null && mc.player!=null && ++frames>60) { VisibilitySkinChecks.run(mc); @@ -36,18 +40,18 @@ void tick(Minecraft mc) throws Exception { System.out.println("LOOTABLE_BODIES_VISIBILITY_SKIN_PASS");mc.shutdown();stage=3; } } - private static final class Panel extends GuiScreen { + private static final class Panel extends Screen { private final EntityLootableBody[] bodies=new EntityLootableBody[3]; - Panel(Minecraft mc){for(int i=0;i<3;i++){bodies[i]=new EntityLootableBody(mc.world);bodies[i].setGameProfile(new GameProfile(new UUID(0,512+i),"VisibilityModel"));}} + 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(int mouseX,int mouseY,float partial) { - drawRect(0,0,width,height,0xff304050); + fill(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(fontRenderer,names[i],x,35,0xffffff); - GuiInventory.drawEntityOnScreen(x,height*2/3,65,20,-20,bodies[i]); + drawCenteredString(font,names[i],x,35,0xffffff); + InventoryScreen.drawEntityOnScreen(x,height*2/3,65,20,-20,bodies[i]); }} finally {LootableBodies.useLocalSkin=local;} } - @Override public boolean doesGuiPauseGame(){return false;} + @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 d9883e7..8b492c0 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java @@ -1,29 +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 cyano.lootable.entities.EntityLootableBody; import java.io.*; import java.util.*; -import net.minecraft.block.BlockSnow; +import net.minecraft.block.SnowBlock; import net.minecraft.entity.EntityType; -import net.minecraft.init.Blocks; +import net.minecraft.block.Blocks; import net.minecraft.nbt.*; import net.minecraft.server.MinecraftServer; import net.minecraft.util.DamageSource; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Direction; import net.minecraft.util.math.BlockPos; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; /** Real block collision and chunk-provider regressions; never shipped with the mod. */ final class WallEscapeChecks { private interface Check { void run() throws Exception; } - private static WorldServer world; + private static ServerWorld world; private static int sequence, failures; private static final List results = new ArrayList<>(); - static void run(MinecraftServer server, WorldServer target, File folder) throws Exception { + static void run(MinecraftServer server, ServerWorld target, File folder) throws Exception { world = target; sequence = 0; failures = 0; results.clear(); check("centred_body_never_uses_NaN_as_destination", WallEscapeChecks::centred); - for (EnumFacing direction : EnumFacing.values()) + for (Direction direction : Direction.values()) check("only_clear_exit_" + direction, () -> oneExit(direction)); check("off_centre_destination_checks_entire_body", WallEscapeChecks::overlap); check("snow_layer_is_not_clear_air", WallEscapeChecks::snow); @@ -32,7 +36,7 @@ static void run(MinecraftServer server, WorldServer target, File folder) throws check("already_clear_body_does_not_teleport", WallEscapeChecks::alreadyClear); check("negative_coordinates_keep_finite_safe_exit", WallEscapeChecks::negative); check("suffocation_damage_uses_escape_and_keeps_default_health", WallEscapeChecks::damage); - for (EnumFacing direction : EnumFacing.Plane.HORIZONTAL) + for (Direction direction : Direction.Plane.HORIZONTAL) check("unloaded_exit_is_not_generated_" + direction, () -> unloaded(direction)); check("ticking_sealed_body_waits_then_escapes_opened_wall", WallEscapeChecks::ticking); check("escaped_and_sealed_bodies_saved_for_cold_reload", () -> seed(folder)); @@ -69,19 +73,19 @@ private static EntityLootableBody body(BlockPos centre) { for (int slot = 0; slot < 48; slot++) body.setInventorySlotContents(slot, RuntimeChecks.stack(slot)); body.addItem(RuntimeChecks.stack(70)); body.addItem(RuntimeChecks.stack(71)); RuntimeChecks.disableVacuum(body); - NBTTagCompound saved = NameLifetimeChecks.saved(body); - NBTTagCompound unresolved = new NBTTagCompound(); - unresolved.setString("Source", "FutureFixture"); unresolved.setString("Data", "retain verbatim"); - saved.setTag("LootableBodiesUnresolved", NbtRecoveryChecks.list(unresolved)); - NBTTagCompound invalid = new NBTTagCompound(); - invalid.setString("Source", "FutureMetadata"); invalid.setTag("Data", new NBTTagByteArray(new byte[]{4,3,2,1})); - saved.setTag("LootableBodiesInvalidFields", NbtRecoveryChecks.list(invalid)); - return (EntityLootableBody) EntityType.create(saved, world); - } - - private static void unchanged(NBTTagCompound before, EntityLootableBody body) { - NBTTagCompound after = NameLifetimeChecks.saved(body); - before = before.copy(); before.removeTag("Pos"); after.removeTag("Pos"); + CompoundNBT saved = NameLifetimeChecks.saved(body); + CompoundNBT unresolved = new CompoundNBT(); + 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})); + 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); + before = before.copy(); before.remove("Pos"); after.remove("Pos"); RuntimeChecks.require(NameLifetimeChecks.same(before, after), "Escape changed saved fields besides position"); PositionMotionChecks.finite(body); } @@ -94,18 +98,18 @@ private static void clear(EntityLootableBody body) { private static void centred() { BlockPos centre = site(false); terrain(centre, false); world.setBlockState(centre, Blocks.STONE.getDefaultState(), 2); // A zero-vector lookup previously sampled (0,0,0). Test it as air, too. - net.minecraft.block.state.IBlockState origin = world.getBlockState(BlockPos.ORIGIN); + net.minecraft.block.BlockState origin = world.getBlockState(BlockPos.ZERO); try { - world.removeBlock(BlockPos.ORIGIN); - EntityLootableBody body = body(centre); NBTTagCompound before = NameLifetimeChecks.saved(body); + world.removeBlock(BlockPos.ZERO,false); + EntityLootableBody body = body(centre); CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); RuntimeChecks.require(body.posY == centre.getY()+1.015625, "Centred body missed the existing upward exit priority"); - } finally { world.setBlockState(BlockPos.ORIGIN, origin, 2); } + } finally { world.setBlockState(BlockPos.ZERO, origin, 2); } } - private static void oneExit(EnumFacing direction) { - BlockPos centre = site(false); terrain(centre, true); BlockPos exit = centre.offset(direction); world.removeBlock(exit); - EntityLootableBody body = body(centre); NBTTagCompound before = NameLifetimeChecks.saved(body); + 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); body.jumpOutOfWall(); clear(body); unchanged(before, body); RuntimeChecks.require(body.posX == exit.getX()+.5 && body.posY == exit.getY()+.015625 && body.posZ == exit.getZ()+.5, "Missed sole adjacent exit"); } @@ -113,18 +117,18 @@ private static void oneExit(EnumFacing direction) { 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); - NBTTagCompound before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); + CompoundNBT 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()); - world.setBlockState(centre.up(), Blocks.SNOW.getDefaultState().with(net.minecraft.block.BlockSnowLayer.LAYERS, 8), 2); + 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); EntityLootableBody body = body(centre); body.jumpOutOfWall(); clear(body); RuntimeChecks.require(body.getPosition().equals(centre.north()), "Snow collision was ignored"); } private static void fence() { - BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.north()); world.removeBlock(centre.east()); + 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); EntityLootableBody body = body(centre); body.jumpOutOfWall(); clear(body); RuntimeChecks.require(body.getPosition().equals(centre.east()), "Tall collision below destination was ignored"); @@ -132,7 +136,7 @@ private static void fence() { private static void sealed() { BlockPos centre = site(false); terrain(centre, true); EntityLootableBody body = body(centre); - NBTTagCompound before = NameLifetimeChecks.saved(body); + CompoundNBT before = NameLifetimeChecks.saved(body); for (int i = 0; i < 32; i++) body.jumpOutOfWall(); RuntimeChecks.require(NameLifetimeChecks.same(before, NameLifetimeChecks.saved(body)), "Sealed corpse drifted or changed saved data"); PositionMotionChecks.finite(body); @@ -140,37 +144,34 @@ private static void sealed() { private static void alreadyClear() { BlockPos centre = site(false); terrain(centre, false); EntityLootableBody body = body(centre); - NBTTagCompound before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); + CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); RuntimeChecks.require(NameLifetimeChecks.same(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()); - EntityLootableBody body = body(centre); NBTTagCompound before = NameLifetimeChecks.saved(body); + BlockPos centre = site(true); terrain(centre, true); world.removeBlock(centre.west(),false); + EntityLootableBody body = body(centre); CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); clear(body); unchanged(before, body); RuntimeChecks.require(body.getPosition().equals(centre.west()), "Negative coordinate exit was rounded incorrectly"); } private static void damage() { - BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.up()); + BlockPos centre = site(false); terrain(centre, true); world.removeBlock(centre.up(),false); EntityLootableBody body = body(centre); float health = body.getHealth(); body.attackEntityFrom(DamageSource.IN_WALL, 1); clear(body); RuntimeChecks.require(body.isAlive() && body.getHealth() == health, "Default suffocation health rules changed"); } - private static void unloaded(EnumFacing direction) throws Exception { + private static void unloaded(Direction direction) throws Exception { BlockPos centre = site(false); int x = centre.getX() >> 4, z = centre.getZ() >> 4; - centre = new BlockPos(x*16+(direction == EnumFacing.WEST ? 0 : direction == EnumFacing.EAST ? 15 : 8), 80, - z*16+(direction == EnumFacing.NORTH ? 0 : direction == EnumFacing.SOUTH ? 15 : 8)); - terrain(centre, true); world.removeBlock(centre.offset(direction)); + 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(); - world.getChunkProvider().queueUnload(world.getChunk(nx,nz)); - int attempts = world.getChunkProvider().getLoadedChunks().size()/100+2; - for (int i = 0; i < attempts && world.getChunkProvider().chunkExists(nx,nz); i++) world.getChunkProvider().tick(()->true); - net.minecraft.world.storage.ThreadedFileIOBase.getThreadedIOInstance().waitForFinish(); + FixtureApi.unload(world,nx,nz); RuntimeChecks.require(!world.getChunkProvider().chunkExists(nx,nz), "Fixture neighbour did not unload"); - EntityLootableBody body = body(centre); NBTTagCompound before = NameLifetimeChecks.saved(body); + EntityLootableBody body = body(centre); CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); RuntimeChecks.require(!world.getChunkProvider().chunkExists(nx,nz), "Escape forced an unloaded neighbour to load"); RuntimeChecks.require(NameLifetimeChecks.same(before, NameLifetimeChecks.saved(body)), "Escape moved without loaded terrain"); @@ -179,12 +180,12 @@ private static void unloaded(EnumFacing direction) throws Exception { private static void ticking() { BlockPos centre = site(false); terrain(centre, true); EntityLootableBody body = body(centre); - NBTTagCompound before = NameLifetimeChecks.saved(body); + CompoundNBT before = NameLifetimeChecks.saved(body); for (int i = 0; i < 100; i++) body.tick(); RuntimeChecks.require(body.posX == centre.getX()+.5 && body.posY == centre.getY()+.015625 && body.posZ == 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.getTag(key),NameLifetimeChecks.saved(body).getTag(key)), "Ticking escape changed " + key); - world.removeBlock(centre.up()); + RuntimeChecks.require(NameLifetimeChecks.same(before.get(key),NameLifetimeChecks.saved(body).get(key)), "Ticking escape changed " + key); + world.removeBlock(centre.up(),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"); } @@ -192,24 +193,24 @@ private static void ticking() { 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()); + if (!trapped) world.removeBlock(centre.up(),false); EntityLootableBody body = body(centre); body.setUniqueId(UUID.nameUUIDFromBytes(("wall-escape-"+trapped).getBytes(java.nio.charset.StandardCharsets.UTF_8))); - NBTTagCompound before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); unchanged(before,body); + CompoundNBT before = NameLifetimeChecks.saved(body); body.jumpOutOfWall(); unchanged(before,body); if (!trapped) clear(body); else RuntimeChecks.require(NameLifetimeChecks.same(before,NameLifetimeChecks.saved(body)), "Sealed disk fixture moved"); RuntimeChecks.awaitChunk(world,body.getPosition().getX()>>4,body.getPosition().getZ()>>4); - RuntimeChecks.require(world.spawnEntity(body), "Could not save wall fixture"); + RuntimeChecks.require(world.addEntity(body), "Could not save wall fixture"); CompressedStreamTools.writeCompressed(NameLifetimeChecks.saved(body), new FileOutputStream(new File(folder,"wall-escape-"+trapped+".nbt"))); } } - static void reload(WorldServer target, File folder) throws Exception { + static void reload(ServerWorld target, File folder) throws Exception { world = target; for (boolean trapped : new boolean[]{false,true}) { - NBTTagCompound expected; + CompoundNBT expected; try (InputStream in = new FileInputStream(new File(folder,"wall-escape-"+trapped+".nbt"))) { expected = CompressedStreamTools.readCompressed(in); } - NBTTagList pos = expected.getList("Pos",6); + 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")); RuntimeChecks.require(NameLifetimeChecks.same(expected,NameLifetimeChecks.saved(body)), "Cold reload changed wall fixture NBT"); @@ -225,12 +226,12 @@ static void historical(MinecraftServer server, File folder, boolean reload) thro world = server.getWorld(net.minecraft.world.dimension.DimensionType.getById(-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 : world.loadedEntityList) + for (net.minecraft.entity.Entity entity : FixtureApi.entities(world)) if (entity instanceof EntityLootableBody && Math.abs(entity.posX-128.5)<.001 && Math.abs(entity.posZ-128.5)<.001) body = (EntityLootableBody) entity; RuntimeChecks.require(body != null, "Original Nether corpse not found at its saved position"); RuntimeChecks.require(!world.isCollisionBoxesEmpty(body,body.getBoundingBox()), "Historical buried-body terrain was cleared"); - NBTTagCompound expected = NameLifetimeChecks.saved(body); + CompoundNBT expected = NameLifetimeChecks.saved(body); File file = new File(folder,"wall-historical.nbt"); if (reload) { try (InputStream in = new FileInputStream(file)) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java index c392117..d6a1e7d 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java @@ -1,4 +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 com.mojang.authlib.GameProfile; import cyano.lootable.LootableBodies; @@ -7,7 +16,7 @@ import java.io.*; import java.util.*; import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.entity.player.ServerPlayerEntity; import net.minecraft.inventory.*; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; @@ -17,44 +26,45 @@ import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; -import net.minecraft.world.WorldServer; +import net.minecraft.world.server.ServerWorld; import net.minecraftforge.common.util.FakePlayerFactory; /** Acceptance against worlds written by the actual historical production jars. */ final class WorldUpgradeChecks { - private static NBTTagCompound manifest,ledger; + private static CompoundNBT manifest,ledger; private static File folder; - private static boolean old,native112; + private static boolean old,native112,native113; + private static int sourceVersion; private static int checks; static void run(MinecraftServer server,String phase) throws Exception { folder=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)).getSaveHandler().getWorldDirectory(); - manifest=read(new File(folder,"upgrade-fixtures.nbt"));old=manifest.getString("Minecraft").equals("1.10.2");native112=manifest.getString("Minecraft").equals("1.12.2"); - File stored=new File(folder,"upgrade-recovered.nbt");ledger=stored.isFile()?read(stored):new NBTTagCompound();checks=0; - if(!ledger.contains("Recoveries",10))ledger.setTag("Recoveries",new NBTTagCompound()); + manifest=read(new File(folder,"upgrade-fixtures.nbt"));old=manifest.getString("Minecraft").equals("1.10.2");native112=manifest.getString("Minecraft").equals("1.12.2");native113=manifest.getString("Minecraft").equals("1.13.2");sourceVersion=old?512:native112?1343:native113?1631:922; + 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()); 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"); - NBTTagList bodies=manifest.getList("Bodies",10); + ListNBT bodies=manifest.getList("Bodies",10); for(int i=0;i=0 && record.getInt("Wave")!=wave)continue; - WorldServer world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(record.getInt("Dimension")));NBTTagCompound source=record.getCompound("Body"); + CompoundNBT record=bodies.getCompound(i);if(wave>=0 && record.getInt("Wave")!=wave)continue; + ServerWorld world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(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); UUID uuid=source.getUniqueId("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) && body.dimension.getId()==record.getInt("Dimension"),"UUID/dimension changed"); - require(body.getCustomNameTag().equals(source.getString("CustomName")),"Owner attribution changed"); + require(body.getCustomNameTag().equals(source.contains("Name",8)?source.getString("Name"):source.getString("CustomName")),"Owner attribution changed"); require(body.posX==source.getList("Pos",6).getDouble(0) && body.posY==source.getList("Pos",6).getDouble(1) && body.posZ==source.getList("Pos",6).getDouble(2),"Position changed in dimension "+world.dimension.getType().getId()+": "+body.getPositionVector()); - NBTTagCompound actual=save(body);require(actual.getInt("LootableBodiesDataVersion")==3,"Missing corpse migration marker"); - require(NameLifetimeChecks.same(source.getTag("LootableBodiesInvalidFields"),actual.getTag("LootableBodiesInvalidFields")),"Original invalid-field evidence changed"); + CompoundNBT 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); - NBTTagCompound recovery=ledger.getCompound("Recoveries").getCompound(id); - if(!recovery.hasKey("Storage"))verifySlots(source,actual); + CompoundNBT 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").setTag(id,new NBTTagCompound());recovery=ledger.getCompound("Recoveries").getCompound(id);} + if(!ledger.getCompound("Recoveries").contains(id,10)) {ledger.getCompound("Recoveries").put(id,new CompoundNBT());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"); @@ -66,108 +76,108 @@ 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<>(); - NBTTagList entries=source.getList("Equipment",10); + private static void verifySlots(CompoundNBT source,CompoundNBT actual) { + Map slots=new TreeMap<>(); + ListNBT entries=source.getList("Equipment",10); for(int i=0;i=48 || slots.containsKey(slot))continue; - item.removeTag("Slot");slots.put(slot,item); + item.remove("Slot");slots.put(slot,item); } - Map saved=new TreeMap<>();NBTTagList entriesNow=actual.getList("Equipment",10); - for(int i=0;i entry:slots.entrySet()) + Map saved=new TreeMap<>();ListNBT 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 NBTTagCompound canonicalItem(NBTTagCompound source) { + private static CompoundNBT canonicalItem(CompoundNBT source) { if(source==null)return null; - NBTTagCompound item=source.copy();String id=item.getString("id"); + CompoundNBT 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,NBTTagCompound source,boolean indexed) { + private static void add(Map map,CompoundNBT source,boolean indexed) { if(!source.contains("id",8) || source.getByte("Count")<=0)return; - NBTTagCompound item=canonicalItem(source);int count=item.getByte("Count")&255;item.removeTag("Count");if(!indexed)item.removeTag("Slot"); + CompoundNBT 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(NBTTagCompound body){Map map=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}){NBTTagList items=body.getList(key,10);for(int i=0;i expectedInventory(NBTTagCompound source) { - Map expected=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}) {NBTTagList list=source.getList(key,10);for(int i=0;i 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 expected=new ArrayList<>(); - for(String key:new String[]{"Equipment","Aux"}) {NBTTagList 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 contents(IInventory inventory,boolean indexed) {Map map=new TreeMap<>();for(int slot=0;slot indexed(NBTTagList list) {Map map=new TreeMap<>();for(int i=0;i storage(WorldServer world,NBTTagCompound recovery) {Map map=new TreeMap<>();NBTTagList list=storageList(recovery);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;i0) {destination=(IInventory)world.getTileEntity(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(ServerWorld world,int wave) { + ListNBT containers=manifest.getList("Containers",10); + 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 verifyPlayer(MinecraftServer server) throws Exception { - WorldServer world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0));UUID id=manifest.getUniqueId("Player"); - NBTTagCompound expected=read(new File(folder,"upgrade-player-source.nbt")); - EntityPlayerMP player=new EntityPlayerMP(server,world,new GameProfile(id,"UpgradeFixture"),new PlayerInteractionManager(world));world.getSaveHandler().getPlayerNBTManager().readPlayerData(player); - // InventoryPlayer serializes armour as 100..103 and offhand as 150. - NBTTagList actual=player.inventory.write(new NBTTagList());require(indexedPlayer(expected,"Inventory").equals(indexedCurrent(actual)),"Player inventory slots, counts or custom data changed"); + ServerWorld world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0));UUID id=manifest.getUniqueId("Player"); + CompoundNBT expected=read(new File(folder,"upgrade-player-source.nbt")); + ServerPlayerEntity player=new ServerPlayerEntity(server,world,new GameProfile(id,"UpgradeFixture"),new PlayerInteractionManager(world));world.getSaveHandler().readPlayerData(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");world.getSaveHandler().getPlayerNBTManager().writePlayerData(player); + require(player.getUniqueID().equals(id),"Player UUID changed");world.getSaveHandler().writePlayerData(player); } - private static Map indexedPlayer(NBTTagCompound player,String key) { - int version=player.contains("DataVersion",3)?player.getInt("DataVersion"):(old?512:native112?1343:922); + private static Map indexedPlayer(CompoundNBT player,String key) { + int version=player.contains("DataVersion",3)?player.getInt("DataVersion"):(sourceVersion); Map map=new TreeMap<>(); - for(INBTBase entry:player.getList(key,10))add(map,expectedItem((NBTTagCompound)entry,version),true); + for(INBT entry:player.getList(key,10))add(map,expectedItem((CompoundNBT)entry,version),true); return map; } - private static Map indexedCurrent(NBTTagList list){Map map=new TreeMap<>();for(int i=0;i indexedCurrent(ListNBT list){Map map=new TreeMap<>();for(int i=0;i>4,((int)pos.getDouble(2))>>4);Entity found=null;for(Entity e:world.loadedEntityList)if(e.getUniqueID().equals(source.getUniqueId("UUID")))found=e;require(found!=null,"Vanilla entity disappeared");NBTTagCompound actual=new NBTTagCompound();found.writeUnlessPassenger(actual);require(actual.getString("id").equals(record.getString("ExpectedType")),"Vanilla entity-name conversion failed");require(found.dimension.getId()==record.getInt("Dimension"),"Vanilla entity dimension changed");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");} + ListNBT list=manifest.getList("Vanilla",10); + for(int i=0;i>4,((int)pos.getDouble(2))>>4);Entity found=null;for(Entity e:FixtureApi.entities(world))if(e.getUniqueID().equals(source.getUniqueId("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(found.dimension.getId()==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");} } - private static Map indexedNativeEquipment(NBTTagList list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(NBTTagList list){Map map=new TreeMap<>();for(int i=0;i indexedNativeEquipment(ListNBT list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(ListNBT list){Map map=new TreeMap<>();for(int i=0;imessage->{ - Minecraft mc=Minecraft.getInstance(); - if(mc.world==null)return null; - Entity entity=mc.world.getEntityByID(message.getAdditionalData().readVarInt()); - if(!(entity instanceof EntityLootableBody))return null; - CorpseGUIContainer gui=new CorpseGUIContainer(mc.player.inventory,(EntityLootableBody)entity); - gui.inventorySlots.windowId=message.getWindowId();return gui; - }); FMLJavaModLoadingContext.get().getModEventBus().addListener(ClientProxy::setup); } - private static void enableConfigButton(net.minecraftforge.client.event.GuiScreenEvent.DrawScreenEvent.Pre event) { - if(!(event.getGui() instanceof net.minecraftforge.fml.client.gui.GuiModList))return; - // Forge 25's ModInfo.hasConfigUI() returns false unconditionally. The - // ordinary button already calls CONFIGGUIFACTORY when enabled. - try { - Class type=net.minecraftforge.fml.client.gui.GuiModList.class; - java.lang.reflect.Field selected=type.getDeclaredField("selectedMod"),button=type.getDeclaredField("configButton"); - selected.setAccessible(true);button.setAccessible(true);Object mod=selected.get(event.getGui()); - if(mod instanceof net.minecraftforge.forgespi.language.IModInfo - && ((net.minecraftforge.forgespi.language.IModInfo)mod).getModId().equals(LootableBodies.MODID) - && net.minecraftforge.fml.ModList.get().getModContainerById(LootableBodies.MODID).get().getCustomExtension(ExtensionPoint.CONFIGGUIFACTORY).isPresent()) - ((net.minecraft.client.gui.GuiButton)button.get(event.getGui())).enabled=true; - }catch(ReflectiveOperationException incompatible){throw new IllegalStateException("Cannot connect the Forge Mods-list Config button",incompatible);} - } private static void setup(net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent event) { RenderingRegistry.registerEntityRenderingHandler(EntityLootableBody.class,CorpseRenderer::new); + net.minecraft.client.gui.ScreenManager.registerFactory(LootableBodies.CORPSE_CONTAINER,CorpseGUIContainer::new); } } diff --git a/src/main/java/cyano/lootable/LootableBodies.java b/src/main/java/cyano/lootable/LootableBodies.java index bfdff26..6f1f019 100644 --- a/src/main/java/cyano/lootable/LootableBodies.java +++ b/src/main/java/cyano/lootable/LootableBodies.java @@ -9,7 +9,7 @@ import net.minecraft.entity.EntityType; import net.minecraft.item.Item; import net.minecraft.item.ItemGroup; -import net.minecraft.item.ItemSpawnEgg; +import net.minecraft.item.SpawnEggItem; import net.minecraft.util.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.common.MinecraftForge; @@ -26,10 +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.4.113021"; + public static final String VERSION = "2.4.5.114041"; 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; private static LootableBodies INSTANCE; private static LegacyConfiguration configuration; @@ -53,25 +54,40 @@ public LootableBodies() { INSTANCE=this; loadConfiguration(FMLPaths.CONFIGDIR.get().resolve("lootablebodies.cfg").toFile()); net.minecraftforge.eventbus.api.IEventBus bus=FMLJavaModLoadingContext.get().getModEventBus(); + bus.addListener(cyano.lootable.upgrade.LegacyProfessionRegistry::create); bus.addGenericListener(EntityType.class,this::registerEntities); bus.addGenericListener(Item.class,this::registerItems); + bus.addGenericListener(net.minecraft.inventory.container.ContainerType.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)CORPSE=new EntityType<>(EntityLootableBody.class,EntityLootableBody::new, - true,true,null,false,null,true,32,1,true); + if(CORPSE==null) { + EntityType.Builder type=EntityType.Builder.create((entityType,world)->new EntityLootableBody(world),net.minecraft.entity.EntityClassification.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(); + CORPSE=type.build(CORPSE_ID.toString()); + } return CORPSE; } private void registerEntities(RegistryEvent.Register> event) { event.getRegistry().register(corpseType().setRegistryName(CORPSE_ID)); } private void registerItems(RegistryEvent.Register event) { - CORPSE_EGG=new ItemSpawnEgg(corpseType(),1,1,new Item.Properties().group(ItemGroup.MISC)) + CORPSE_EGG=new SpawnEggItem(corpseType(),1,1,new Item.Properties().group(ItemGroup.MISC)) .setRegistryName(MODID,"corpse_spawn_egg"); event.getRegistry().register(CORPSE_EGG); } + 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()); + if(!(body instanceof EntityLootableBody))throw new IllegalStateException("Corpse container has no matching entity"); + return new cyano.lootable.graphics.CorpseContainer(window,inventory,(EntityLootableBody)body); + }); + event.getRegistry().register(CORPSE_CONTAINER.setRegistryName(CORPSE_ID)); + } public static LootableBodies getInstance(){return INSTANCE;} @SubscribeEvent public void beforeWorldLoading(FMLServerAboutToStartEvent event) { File world=event.getServer().getActiveAnvilConverter().getFile(event.getServer().getFolderName(),""); @@ -86,11 +102,11 @@ private void registerItems(RegistryEvent.Register event) { } @SubscribeEvent(priority=net.minecraftforge.eventbus.api.EventPriority.HIGHEST) public void worldLoaded(net.minecraftforge.event.world.WorldEvent.Load event) { - if(event.getWorld() instanceof net.minecraft.world.WorldServer)ChunkVersionContext.installWorld((net.minecraft.world.WorldServer)event.getWorld()); + if(event.getWorld() instanceof net.minecraft.world.server.ServerWorld)ChunkVersionContext.installWorld((net.minecraft.world.server.ServerWorld)event.getWorld()); } @SubscribeEvent(priority=net.minecraftforge.eventbus.api.EventPriority.HIGHEST) public void beforeSpawn(net.minecraftforge.event.world.WorldEvent.CreateSpawnPosition event) { - if(event.getWorld() instanceof net.minecraft.world.WorldServer)ChunkVersionContext.installWorld((net.minecraft.world.WorldServer)event.getWorld()); + if(event.getWorld() instanceof net.minecraft.world.server.ServerWorld)ChunkVersionContext.installWorld((net.minecraft.world.server.ServerWorld)event.getWorld()); } @SubscribeEvent public void worldStopped(FMLServerStoppedEvent event){UpgradePreservation.close();} private static boolean or(boolean... values){for(boolean value:values)if(value)return true;return false;} diff --git a/src/main/java/cyano/lootable/entities/EntityLootableBody.java b/src/main/java/cyano/lootable/entities/EntityLootableBody.java index 2a32c21..50c63b8 100644 --- a/src/main/java/cyano/lootable/entities/EntityLootableBody.java +++ b/src/main/java/cyano/lootable/entities/EntityLootableBody.java @@ -7,30 +7,30 @@ import com.mojang.authlib.GameProfile; import cyano.lootable.LootableBodies; import net.minecraft.block.material.Material; -import net.minecraft.block.state.IBlockState; -import net.minecraft.init.Blocks; -import net.minecraft.entity.EntityLiving; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.entity.MobEntity; import net.minecraft.entity.CreatureAttribute; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.attributes.AttributeModifier; import net.minecraft.entity.ai.attributes.IAttributeInstance; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.inventory.EntityEquipmentSlot; +import net.minecraft.entity.item.ItemEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.EquipmentSlotType; import net.minecraft.inventory.IInventory; import net.minecraft.item.Item; -import net.minecraft.item.ItemSpade; +import net.minecraft.item.ShovelItem; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.INBTBase; -import net.minecraft.nbt.NBTPrimitive; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.potion.Potion; -import net.minecraft.tileentity.TileEntitySkull; +import net.minecraft.nbt.INBT; +import net.minecraft.nbt.NumberNBT; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.potion.Effect; +import net.minecraft.tileentity.SkullTileEntity; import net.minecraft.util.DamageSource; import net.minecraft.util.EntityDamageSource; -import net.minecraft.util.EnumHand; -import net.minecraft.util.EnumFacing; +import net.minecraft.util.Hand; +import net.minecraft.util.Direction; import net.minecraft.util.NonNullList; import net.minecraft.util.SoundEvent; import net.minecraft.util.ResourceLocation; @@ -50,10 +50,10 @@ import java.util.concurrent.RejectedExecutionException; -public class EntityLootableBody extends EntityLiving implements IInventory, net.minecraft.world.IInteractionObject{ +public class EntityLootableBody extends MobEntity implements IInventory, net.minecraft.inventory.container.INamedContainerProvider{ - public static final EntityEquipmentSlot[] EQUIPMENT_SLOTS = new EntityEquipmentSlot[] {EntityEquipmentSlot.HEAD, EntityEquipmentSlot.CHEST, EntityEquipmentSlot.LEGS, EntityEquipmentSlot.FEET, EntityEquipmentSlot.MAINHAND, EntityEquipmentSlot.OFFHAND}; + public static final EquipmentSlotType[] EQUIPMENT_SLOTS = new EquipmentSlotType[] {EquipmentSlotType.HEAD, EquipmentSlotType.CHEST, EquipmentSlotType.LEGS, EquipmentSlotType.FEET, EquipmentSlotType.MAINHAND, EquipmentSlotType.OFFHAND}; final static byte VACUUM_TIMELIMIT = 20; final static int VACUUM_RADIUS = 3; @@ -61,18 +61,18 @@ public class EntityLootableBody extends EntityLiving implements IInventory, net. private final NonNullList 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 NBTTagCompound sourceVersions = new NBTTagCompound(); - private NBTTagCompound inventoryVersions = new NBTTagCompound(); + private final List unresolvedInventory = new ArrayList<>(); + private CompoundNBT sourceVersions = new CompoundNBT(); + private CompoundNBT inventoryVersions = new CompoundNBT(); 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 INBTBase loadedPosition; + private INBT loadedPosition; private boolean missingPositionX, missingPositionZ; private long deathTimestamp; private long decayTimestamp; @@ -92,9 +92,9 @@ public class EntityLootableBody extends EntityLiving implements IInventory, net. public EntityLootableBody(World w) { super(LootableBodies.CORPSE,w); - // Keep EntityLiving's actual defaults (including follow range 16 and + // Keep MobEntity's actual defaults (including follow range 16 and // configured corpse health), rather than the generic attribute defaults. - for(IAttributeInstance attribute : getAttributeMap().getAllAttributes()) { + for(IAttributeInstance attribute : getAttributes().getAllAttributes()) { String name = attribute.getAttribute().getName(); defaultAttributeBases.put(name,attribute.getBaseValue()); defaultAttributeModifiers.put(name,new ArrayList<>(attribute.getModifiers())); @@ -104,19 +104,17 @@ public EntityLootableBody(World w) { // Local display preferences must not become world/server entity state. // Existing saved visibility flags are still read unchanged by vanilla. this.setCustomNameVisible(true); - this.setSize(0.85F, 0.75F); + // The registered EntityType supplies the unchanged corpse dimensions. + - this.isImmuneToFire = LootableBodies.completelyInvulnerable || (!LootableBodies.hurtByEnvironment); } - public EntityLootableBody(EntityPlayer player){ + public EntityLootableBody(PlayerEntity player){ this(player.getEntityWorld()); this.setPosition(player.posX, player.posY+0.5, player.posZ); - this.motionX = player.motionX; - this.motionY = player.motionY+0.0784000015258789; - this.motionZ = player.motionZ; + this.setMotion(player.getMotion().add(0D,0.0784000015258789D,0D)); //this.newPosX = posX; // no newPosX? //this.newPosY = posY; // no newPosY? @@ -141,10 +139,10 @@ public void tick() { // A Forge ticket can tick our centre chunk while neighbouring support // terrain is absent. Never treat those missing collision boxes as air. AxisAlignedBB bounds = getBoundingBox(); - BlockPos from = new BlockPos(bounds.minX + Math.min(0D,motionX), 0, - bounds.minZ + Math.min(0D,motionZ)); - BlockPos to = new BlockPos(bounds.maxX + Math.max(0D,motionX), 0, - bounds.maxZ + Math.max(0D,motionZ)); + 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; } super.tick(); @@ -168,7 +166,7 @@ public void baseTick(){ if(terminate > 0) terminate--; if(terminate == 0){ // needs to be manually deleted for some reason - getEntityWorld().removeEntity(this); + this.remove(); } return; } @@ -234,20 +232,19 @@ public void baseTick(){ // handle item vacuuming if(isServer && vacuumTime < VACUUM_TIMELIMIT){ vacuumTime++; - List items = getEntityWorld().getEntitiesWithinAABB(EntityItem.class, new AxisAlignedBB( + List items = getEntityWorld().getEntitiesWithinAABB(ItemEntity.class, new AxisAlignedBB( posX - VACUUM_RADIUS, posY - VACUUM_RADIUS, posZ - VACUUM_RADIUS, posX + VACUUM_RADIUS, posY + VACUUM_RADIUS, posZ + VACUUM_RADIUS)); - for(EntityItem item : items){ + for(ItemEntity item : items){ if(item.removed) continue; ItemStack stack = item.getItem(); this.addItem(stack); - getEntityWorld().removeEntity(item); + item.remove(); } } if(isServer && LootableBodies.corpseBuoyancy && vacuumTime >= VACUUM_TIMELIMIT && (this.inWater || this.isInLava())) { - this.motionY += 0.025D; - this.motionY *= 0.9; + this.setMotion(getMotion().x,(getMotion().y+0.025D)*0.9D,getMotion().z); } if(isServer && LootableBodies.voidPlatform && this.posY < 0) { @@ -274,7 +271,7 @@ private void generateVoidPlatform() { World world = this.world; BlockPos testPos = new BlockPos((int)this.posX, 0, (int)this.posZ); - IBlockState platformBlockstate = getPlatformBlock(this.dimension.getId()); + BlockState platformBlockstate = getPlatformBlock(this.dimension.getId()); for(int j = -1; j <= 1; j++) { for(int k = -1; k <= 1; k++) { @@ -284,7 +281,7 @@ private void generateVoidPlatform() { } } - private IBlockState getPlatformBlock(int i) { + private BlockState getPlatformBlock(int i) { switch(LootableBodies.dimensionSpecificPlatform ? i : 0) { case -1: return Blocks.NETHERRACK.getDefaultState(); @@ -311,7 +308,7 @@ private ItemStack decayItem(ItemStack item, int amount) { && item.getItem().isDamageable()){ int durabilityRemaining = item.getMaxDamage() - item.getDamage() - 1; if(durabilityRemaining > 0){ - item.damageItem(Math.min(durabilityRemaining,amount),this); + item.damageItem(Math.min(durabilityRemaining,amount),this,broken->{}); } } return item; @@ -388,11 +385,11 @@ 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 EntityPlayer){ - EntityPlayer player = (EntityPlayer)src.getTrueSource(); - ItemStack item = player.getHeldItem(EnumHand.MAIN_HAND); + if(src instanceof EntityDamageSource && src.getTrueSource() instanceof PlayerEntity){ + PlayerEntity player = (PlayerEntity)src.getTrueSource(); + ItemStack item = player.getHeldItem(Hand.MAIN_HAND); if(!emptyStack(item)) { - if (item.getItem() instanceof ItemSpade || item.getItem().getToolTypes(item).contains(net.minecraftforge.common.ToolType.SHOVEL)) { + if (item.getItem() instanceof ShovelItem || item.getItem().getToolTypes(item).contains(net.minecraftforge.common.ToolType.SHOVEL)) { this.onKillCommand(); } } @@ -436,7 +433,7 @@ public void setGameProfile(GameProfile gp){ } /** Refreshes an existing client body after enabling player skins in the GUI. */ public String getCustomNameTag(){return getCustomName()==null?"":getCustomName().getString();} - public void setCustomNameTag(String name){setCustomName(name==null || name.isEmpty()?null:new net.minecraft.util.text.TextComponentString(name));} + public void setCustomNameTag(String name){setCustomName(name==null || name.isEmpty()?null:new net.minecraft.util.text.StringTextComponent(name));} public void refreshClientSkin() { GameProfile profile = gpSwap.get(); @@ -447,7 +444,7 @@ private void resolveProfile(GameProfile initial) { try { PROFILE_LOOKUPS.execute(() -> { try { - GameProfile resolved = TileEntitySkull.updateGameProfile(initial); + 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) { @@ -473,7 +470,7 @@ private void setDeathTimestamp(long timestamp) { @Override protected void registerAttributes() { - // called during constructor of EntityLivingBase + // called during constructor of LivingEntity super.registerAttributes(); this.getAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(LootableBodies.corpseHP); } @@ -488,7 +485,7 @@ public void setRotation(float newRot){ @Override - public boolean canDespawn() { + public boolean canDespawn(double distanceToClosestPlayer) { return false; } @@ -506,8 +503,8 @@ public void jumpOutOfWall(){ if(length > 0 && tryWallExit(posX+x/length,posY,posZ+z/length)) return; // Retain the original adjacent-exit priority, but check the whole body. - for(EnumFacing direction : new EnumFacing[]{EnumFacing.UP,EnumFacing.NORTH, - EnumFacing.EAST,EnumFacing.SOUTH,EnumFacing.WEST,EnumFacing.DOWN}) { + for(Direction direction : new Direction[]{Direction.UP,Direction.NORTH, + Direction.EAST,Direction.SOUTH,Direction.WEST,Direction.DOWN}) { BlockPos exit = currentCoord.offset(direction); if(tryWallExit(exit.getX()+0.5,exit.getY()+0.015625,exit.getZ()+0.5)) return; } @@ -550,7 +547,7 @@ public void playAmbientSound() { @Override - protected int getExperiencePoints(final EntityPlayer p_getExperiencePoints_1_) { + protected int getExperiencePoints(final PlayerEntity p_getExperiencePoints_1_) { return 0; } @@ -561,12 +558,12 @@ protected SoundEvent getAmbientSound() { } @Override - protected Item getDropItem() { - return null; + protected ResourceLocation getLootTable() { + return net.minecraft.world.storage.loot.LootTables.EMPTY; } @Override - protected void dropEquipment(boolean wasRecentlyHit, int lootingModifier) { + protected void dropSpecialItems(DamageSource source, int lootingModifier, boolean wasRecentlyHit) { // Corpse equipment belongs to the inventory release path, not mob loot. } @@ -577,7 +574,7 @@ public boolean canBeSteered() { @Override - public boolean canBeLeashedTo(EntityPlayer player) { + public boolean canBeLeashedTo(PlayerEntity player) { return false; // leashing not allowed } @@ -692,14 +689,14 @@ public void markDirty() { * @param player */ @Override - public boolean isUsableByPlayer(EntityPlayer player) { + public boolean isUsableByPlayer(PlayerEntity player) { return player != null && player.isAlive() && this.isAlive() && terminate < 0 && player.getEntityWorld() == getEntityWorld() && player.getPosition().distanceSq(this.getPosition()) < 25; } @Override - public boolean processInteract(EntityPlayer player, EnumHand hand){ + public boolean processInteract(PlayerEntity player, Hand hand){ if(isUsableByPlayer(player)) { GUIHandler.open(player,this); return true; @@ -707,15 +704,15 @@ public boolean processInteract(EntityPlayer player, EnumHand hand){ return false; } - @Override public net.minecraft.inventory.Container createContainer(net.minecraft.entity.player.InventoryPlayer inventory, EntityPlayer player){return new cyano.lootable.graphics.CorpseContainer(inventory,this);} - @Override public String getGuiID(){return "lootablebodies:corpse";} + @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);} + public String getGuiID(){return "lootablebodies:corpse";} @Override - public void openInventory(EntityPlayer player) { + public void openInventory(PlayerEntity player) { } @Override - public void closeInventory(EntityPlayer player) { + public void closeInventory(PlayerEntity player) { } /** @@ -734,17 +731,14 @@ public boolean isItemValidForSlot(int index, ItemStack stack) { } } - @Override public int getField(int id) { return 0; } - @Override public void setField(int id, int value) { // do nothing } - @Override public int getFieldCount() { return 0; } @@ -772,16 +766,16 @@ public boolean isEmpty(){ private boolean dropAllItems(){ for(int slot = 0; slot < getSizeInventory(); slot++){ ItemStack stack = getStackInSlot(slot); - if(!emptyStack(stack) && getEntityWorld().spawnEntity( - new EntityItem(getEntityWorld(),posX,posY,posZ,stack.copy()))) { + if(!emptyStack(stack) && getEntityWorld().addEntity( + new ItemEntity(getEntityWorld(),posX,posY,posZ,stack.copy()))) { setInventorySlotContents(slot,null); } } Iterator iterator = auxInventory.iterator(); while(iterator.hasNext()) { ItemStack stack = iterator.next(); - if(emptyStack(stack) || getEntityWorld().spawnEntity( - new EntityItem(getEntityWorld(),posX,posY,posZ,stack.copy()))) iterator.remove(); + if(emptyStack(stack) || getEntityWorld().addEntity( + new ItemEntity(getEntityWorld(),posX,posY,posZ,stack.copy()))) iterator.remove(); } markDirty(); return isEmpty(); @@ -802,78 +796,78 @@ public CreatureAttribute getCreatureAttribute() @Override - public void writeAdditional(final NBTTagCompound root) { + public void writeAdditional(final CompoundNBT root) { UpgradePreservation.clearTransient(root); - root.setInt(UpgradePreservation.VERSION,UpgradePreservation.DATA_VERSION); - if(sourceVersions.isEmpty()) root.removeTag(UpgradePreservation.SOURCES); - else root.setTag(UpgradePreservation.SOURCES,sourceVersions.copy()); - root.removeTag("ActiveEffects"); + 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 NBTTagList equipmentListTag = new NBTTagList(); + final ListNBT equipmentListTag = new ListNBT(); for (int i = 0; i < this.getSizeInventory(); i++) { ItemStack item = this.getStackInSlot(i); if (!emptyStack(item)) { - NBTTagCompound slotTag = new NBTTagCompound(); - slotTag.setByte("Slot", (byte)i); + CompoundNBT slotTag = new CompoundNBT(); + slotTag.putByte("Slot", (byte)i); item.write(slotTag); equipmentListTag.add(slotTag); } } - root.setTag("Equipment", equipmentListTag); + root.put("Equipment", equipmentListTag); - root.removeTag("Aux"); + root.remove("Aux"); if(!this.auxInventory.isEmpty()){ - final NBTTagList nbtauxtaglist = new NBTTagList(); + final ListNBT nbtauxtaglist = new ListNBT(); Iterator iter = this.auxInventory.iterator(); while (iter.hasNext()) { ItemStack i = iter.next(); if(emptyStack(i)) continue; - final NBTTagCompound nbttagcompound1 = new NBTTagCompound(); + final CompoundNBT nbttagcompound1 = new CompoundNBT(); i.write(nbttagcompound1); nbtauxtaglist.add(nbttagcompound1); } - root.setTag("Aux", nbtauxtaglist); + root.put("Aux", nbtauxtaglist); } - root.removeTag(UNRESOLVED_ITEMS_TAG); + root.remove(UNRESOLVED_ITEMS_TAG); if(!unresolvedInventory.isEmpty()) { - NBTTagList retained = new NBTTagList(); - for(NBTTagCompound record : unresolvedInventory) retained.add(record.copy()); - root.setTag(UNRESOLVED_ITEMS_TAG,retained); + ListNBT retained = new ListNBT(); + for(CompoundNBT record : unresolvedInventory) retained.add(record.copy()); + root.put(UNRESOLVED_ITEMS_TAG,retained); } - root.removeTag(INVALID_FIELDS_TAG); + root.remove(INVALID_FIELDS_TAG); if(!invalidFields.isEmpty()) { - NBTTagList retained = new NBTTagList(); - for(NBTTagCompound record : invalidFields) retained.add(record.copy()); - root.setTag(INVALID_FIELDS_TAG,retained); + ListNBT retained = new ListNBT(); + for(CompoundNBT record : invalidFields) retained.add(record.copy()); + root.put(INVALID_FIELDS_TAG,retained); } - root.removeTag("Vac"); - if(vacuumTime < VACUUM_TIMELIMIT)root.setByte("Vac", vacuumTime); - root.removeTag("Name"); + root.remove("Vac"); + if(vacuumTime < VACUUM_TIMELIMIT)root.putByte("Vac", vacuumTime); + root.remove("Name"); String name = getCustomNameTag(); - if(name != null && !name.trim().isEmpty()) root.setString("Name",name); - else root.removeTag("CustomName"); - root.setLong("DeathTime", this.getDeathTimestamp()); - root.setLong("DecayTime",this.decayTimestamp); + if(name != null && !name.trim().isEmpty()) root.putString("Name",name); + else root.remove("CustomName"); + root.putLong("DeathTime", this.getDeathTimestamp()); + root.putLong("DecayTime",this.decayTimestamp); } @Override - public void read(final NBTTagCompound supplied) { - final NBTTagCompound root=UpgradePreservation.restore(supplied); + public void read(final CompoundNBT supplied) { + final CompoundNBT root=UpgradePreservation.restore(supplied); // Entity reads geometry before our inventory helper. Sanitize a copy // before it builds bounding boxes or derives previous-tick positions. - NBTTagCompound clean = root.copy(); + CompoundNBT clean = root.copy(); boolean[] badPosition = new boolean[4], badMotion = new boolean[4]; boolean[] badRotation = new boolean[3]; - clean.setTag("Pos",readVector(root.getTag("Pos"),false,badPosition)); - clean.setTag("Motion",readVector(root.getTag("Motion"),true,badMotion)); - clean.setTag("Rotation",readRotation(root.getTag("Rotation"),badRotation)); - Float distance = finiteFloat(root.getTag("FallDistance")); - boolean badFall = root.hasKey("FallDistance") && (distance == null || distance < 0F); - clean.setFloat("FallDistance",distance != null && distance >= 0F ? distance : 0F); + clean.put("Pos",readVector(root.get("Pos"),false,badPosition)); + clean.put("Motion",readVector(root.get("Motion"),true,badMotion)); + clean.put("Rotation",readRotation(root.get("Rotation"),badRotation)); + Float distance = finiteFloat(root.get("FallDistance")); + boolean badFall = root.contains("FallDistance") && (distance == null || distance < 0F); + clean.putFloat("FallDistance",distance != null && distance >= 0F ? distance : 0F); // Vanilla expects JSON here. Our attribution reader owns compatibility // and retains malformed input without sending it to vanilla's parser. - clean.removeTag("CustomName"); + clean.remove("CustomName"); super.read(clean); String fallback=readName(root,"CustomName"),owner=readName(root,"Name"); setUserName(owner!=null?owner:fallback!=null?fallback:""); @@ -883,31 +877,31 @@ public void read(final NBTTagCompound supplied) { setRenderYawOffset(rotationYaw); prevRenderYawOffset = rotationYaw; positionNeedsChunk = true; malformedPosition = hasInvalidComponent(badPosition); - loadedPosition = root.hasKey("Pos") ? root.getTag("Pos").copy() : null; + loadedPosition = root.contains("Pos") ? root.get("Pos").copy() : null; missingPositionX = badPosition[0]; missingPositionZ = badPosition[2]; - if(malformedPosition && root.hasKey("Pos")) retainInvalid("Pos",root.getTag("Pos")); - if(hasInvalidComponent(badMotion) && root.hasKey("Motion")) retainInvalid("Motion",root.getTag("Motion")); - if(hasInvalidComponent(badRotation) && root.hasKey("Rotation")) retainInvalid("Rotation",root.getTag("Rotation")); - if(badFall) retainInvalid("FallDistance",root.getTag("FallDistance")); + if(malformedPosition && 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(INBTBase raw) { + private static Float finiteFloat(INBT 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 NBTTagList readRotation(INBTBase raw, boolean[] invalid) { - NBTTagList list = raw instanceof NBTTagList ? (NBTTagList)raw : 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; - NBTTagList clean = new NBTTagList(); + ListNBT clean = new ListNBT(); 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(new net.minecraft.nbt.NBTTagFloat(axis == 0 ? angle % 360F : 0F)); + clean.add(new net.minecraft.nbt.FloatNBT(axis == 0 ? angle % 360F : 0F)); } return clean; } @@ -917,10 +911,10 @@ private static boolean hasInvalidComponent(boolean[] invalid) { return false; } - private static NBTTagList readVector(INBTBase raw, boolean motion, boolean[] invalid) { - NBTTagList list = raw instanceof NBTTagList ? (NBTTagList)raw : null; + 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; - NBTTagList clean = new NBTTagList(); + ListNBT clean = new ListNBT(); 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)) { @@ -928,7 +922,7 @@ private static NBTTagList readVector(INBTBase raw, boolean motion, boolean[] inv // A lost height must not put recoverable inventory in the void. value = !motion && axis == 1 ? 64D : 0D; } - clean.add(new net.minecraft.nbt.NBTTagDouble(value)); + clean.add(new net.minecraft.nbt.DoubleNBT(value)); } return clean; } @@ -953,12 +947,12 @@ public void recoverPositionInChunk(int chunkX, int chunkZ) { } @Override - public void readAdditional(final NBTTagCompound supplied) { - final NBTTagCompound root=UpgradePreservation.restore(supplied); - sourceVersions=root.contains(UpgradePreservation.SOURCES,10) ? root.getCompound(UpgradePreservation.SOURCES).copy() : new NBTTagCompound(); + 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(); // 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 NBTTagCompound(); + inventoryVersions=root.getBoolean(UpgradePreservation.ITEMS_PENDING) ? sourceVersions : new CompoundNBT(); // Replace, rather than append to, an existing inventory on a repeated load. this.clear(); // These flags are not saved. A reused entity must behave like a fresh @@ -967,48 +961,48 @@ public void readAdditional(final NBTTagCompound supplied) { dead = false; removed = false; // Item readers may update their input tags; the caller's saved data is immutable. - NBTTagCompound vanilla = root.copy(); + CompoundNBT vanilla = root.copy(); // Our historical DeathTime is a world timestamp, not vanilla's animation. - vanilla.setShort("DeathTime",(short)0); + vanilla.putShort("DeathTime",(short)0); invalidFields.clear(); - INBTBase metadata = root.getTag(INVALID_FIELDS_TAG); + INBT metadata = root.get(INVALID_FIELDS_TAG); if(isInventoryList(metadata)) { - NBTTagList records = (NBTTagList)metadata; + ListNBT records = (ListNBT)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(IAttributeInstance attribute : getAttributeMap().getAllAttributes()) restoreAttribute(attribute); - vanilla.setTag("Attributes",readAttributes(root.getTag("Attributes"))); - vanilla.setTag("ActiveEffects",readEffects(root.getTag("ActiveEffects"))); - vanilla.setShort("HurtTime",(short)readTimer(root,"HurtTime",Short.MAX_VALUE,0)); + for(IAttributeInstance attribute : getAttributes().getAllAttributes()) 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.removeTag("Health"); - vanilla.setFloat("AbsorptionAmount",readLivingFloat(root,"AbsorptionAmount",0F,Float.MAX_VALUE)); + vanilla.remove("Health"); + vanilla.putFloat("AbsorptionAmount",readLivingFloat(root,"AbsorptionAmount",0F,Float.MAX_VALUE)); super.readAdditional(vanilla); updatePotionMetadata(); - for(IAttributeInstance attribute : getAttributeMap().getAllAttributes()) { + for(IAttributeInstance attribute : getAttributes().getAllAttributes()) { if(!Double.isFinite(attribute.getValue())) { // Also protect against engine ordering/parent-attribute arithmetic. - if(root.hasKey("Attributes")) retainInvalid("Attributes",root.getTag("Attributes")); + if(root.contains("Attributes")) retainInvalid("Attributes",root.get("Attributes")); restoreAttribute(attribute); } } setHealth(readLivingFloat(root,"Health",getMaxHealth(),getMaxHealth())); - INBTBase equipment = root.getTag("Equipment"); + INBT 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); } readInventoryTag("Equipment",equipment); - readInventoryTag("Aux",root.getTag("Aux")); - INBTBase recovery = root.getTag(UNRESOLVED_ITEMS_TAG); + readInventoryTag("Aux",root.get("Aux")); + INBT recovery = root.get(UNRESOLVED_ITEMS_TAG); if(isInventoryList(recovery)) { - NBTTagList records = (NBTTagList)recovery; + ListNBT records = (ListNBT)recovery; for(int i = 0; i < records.size(); i++) { - NBTTagCompound record = records.getCompound(i); + CompoundNBT record = records.getCompound(i); String source = record.getString("Source"); if((record.keySet().size() == 2 || (record.keySet().size() == 3 && record.contains("SourceVersions",10))) && record.contains("Source",8) && record.contains("Data",10) @@ -1032,10 +1026,10 @@ public void readAdditional(final NBTTagCompound supplied) { this.setRotation(this.rotationYaw, 0); } - private void retainInvalid(String source, INBTBase data) { - NBTTagCompound record = new NBTTagCompound(); - record.setString("Source",source); - record.setTag("Data",data.copy()); + private void retainInvalid(String source, INBT data) { + CompoundNBT record = new CompoundNBT(); + record.putString("Source",source); + record.put("Data",data.copy()); if(!invalidFields.contains(record)) invalidFields.add(record); } @@ -1047,50 +1041,50 @@ private void restoreAttribute(IAttributeInstance attribute) { if(defaults != null) for(AttributeModifier modifier : defaults) attribute.applyModifier(modifier); } - private NBTTagList readEffects(INBTBase raw) { - NBTTagList clean = new NBTTagList(); + private ListNBT readEffects(INBT raw) { + ListNBT clean = new ListNBT(); if(raw == null) return clean; if(!isInventoryList(raw)) { retainInvalid("ActiveEffects",raw); return clean; } - Set seen = new HashSet<>(); - NBTTagList list = (NBTTagList)raw; + Set seen = new HashSet<>(); + ListNBT list = (ListNBT)raw; for(int i = 0; i < list.size(); i++) { - NBTTagCompound record = list.getCompound(i); - INBTBase rawId = record.getTag("Id"), rawAmplifier = record.getTag("Amplifier"); + CompoundNBT record = list.getCompound(i); + INBT 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.hasKey("Duration") ? integral(record.getTag("Duration")) : Long.valueOf(0); - Potion potion = id != null && id >= 0 && id <= 255 ? Potion.getPotionById(id.intValue()) : null; + 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; 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; } - NBTTagCompound accepted = new NBTTagCompound(); - accepted.setByte("Id",id.byteValue()); accepted.setInt("Duration",duration.intValue()); - accepted.setByte("Amplifier",amplifier.byteValue()); + CompoundNBT accepted = new CompoundNBT(); + 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()); // Forge 1.11 persists curative stacks. Preserve their complete custom // data; an unreadable list uses Forge's defaults and stays recoverable. - if(record.hasKey("CurativeItems")) { - INBTBase rawCuratives = record.getTag("CurativeItems"); + if(record.contains("CurativeItems")) { + INBT rawCuratives = record.get("CurativeItems"); boolean valid = isInventoryList(rawCuratives); if(valid) { - NBTTagList items = (NBTTagList)rawCuratives; + ListNBT items = (ListNBT)rawCuratives; for(int c=0; c= 0 && flag <= 1) accepted.setBoolean(key,flag == 1); - else { accepted.setBoolean(key,fallback); changed = true; } + Long flag = integral(record.get(key)); + if(!record.contains(key)) accepted.putBoolean(key,fallback); + else if(flag != null && flag >= 0 && flag <= 1) accepted.putBoolean(key,flag == 1); + else { accepted.putBoolean(key,fallback); changed = true; } } if(changed) retainInvalid("ActiveEffect",record); clean.add(accepted); @@ -1098,14 +1092,14 @@ private NBTTagList readEffects(INBTBase raw) { return clean; } - private static Double finiteNumber(INBTBase raw) { - if(!(raw instanceof NBTPrimitive) || raw.getId() < 1 || raw.getId() > 6) return null; - double value = ((NBTPrimitive)raw).getDouble(); + private static Double finiteNumber(INBT raw) { + if(!(raw instanceof NumberNBT) || raw.getId() < 1 || raw.getId() > 6) return null; + double value = ((NumberNBT)raw).getDouble(); return Double.isFinite(value) ? value : null; } - private float readLivingFloat(NBTTagCompound root, String key, float fallback, float maximum) { - INBTBase raw = root.getTag(key); + private float readLivingFloat(CompoundNBT root, String key, float fallback, float maximum) { + INBT raw = root.get(key); if(raw == null) return fallback; Double value = finiteNumber(raw); if(value != null && value >= 0 && value <= maximum) { @@ -1116,17 +1110,17 @@ private float readLivingFloat(NBTTagCompound root, String key, float fallback, f return fallback; } - private NBTTagList readAttributes(INBTBase raw) { - NBTTagList clean = new NBTTagList(); + private ListNBT readAttributes(INBT raw) { + ListNBT clean = new ListNBT(); if(raw == null) return clean; if(!isInventoryList(raw)) { retainInvalid("Attributes",raw); return clean; } Set seen = new HashSet<>(); - NBTTagList list = (NBTTagList)raw; + ListNBT list = (ListNBT)raw; for(int i = 0; i < list.size(); i++) { - NBTTagCompound record = list.getCompound(i); + CompoundNBT record = list.getCompound(i); IAttributeInstance attribute = record.contains("Name",8) - ? getAttributeMap().getAttributeInstanceByName(record.getString("Name")) : null; - Double base = finiteNumber(record.getTag("Base")); + ? getAttributes().getAttributeInstanceByName(record.getString("Name")) : null; + Double base = finiteNumber(record.get("Base")); if(attribute == null || base == null) { retainInvalid("Attribute",record); continue; } String name = attribute.getAttribute().getName(); // corpse_HP has always allowed 1..Short.MAX_VALUE. Preserve a base @@ -1138,36 +1132,36 @@ private NBTTagList readAttributes(INBTBase raw) { retainInvalid("Attribute",record); continue; } boolean changed = !Arrays.asList("Name","Base","Modifiers").containsAll(record.keySet()); - NBTTagCompound accepted = new NBTTagCompound(); - accepted.setString("Name",name); accepted.setDouble("Base",base); - INBTBase rawModifiers = record.getTag("Modifiers"); + CompoundNBT accepted = new CompoundNBT(); + accepted.putString("Name",name); accepted.putDouble("Base",base); + INBT rawModifiers = record.get("Modifiers"); if(rawModifiers != null) { - NBTTagList modifiers = new NBTTagList(); + ListNBT modifiers = new ListNBT(); if(!isInventoryList(rawModifiers)) changed = true; else { Set ids = new HashSet<>(); - NBTTagList entries = (NBTTagList)rawModifiers; + ListNBT entries = (ListNBT)rawModifiers; for(int m = 0; m < entries.size(); m++) { - NBTTagCompound entry = entries.getCompound(m); - Long operation = integral(entry.getTag("Operation")); - Long most = integral(entry.getTag("UUIDMost")), least = integral(entry.getTag("UUIDLeast")); - Double amount = finiteNumber(entry.getTag("Amount")); + CompoundNBT entry = entries.getCompound(m); + Long operation = integral(entry.get("Operation")); + Long most = integral(entry.get("UUIDMost")), least = integral(entry.get("UUIDLeast")); + Double amount = finiteNumber(entry.get("Amount")); if(!entry.contains("Name",8) || entry.getString("Name").isEmpty() || operation == null || operation < 0 || operation > 2 || most == null || least == null || amount == null) { changed = true; continue; } UUID id = new UUID(most,least); if(ids.contains(id)) { changed = true; continue; } - NBTTagCompound modifier = new NBTTagCompound(); - modifier.setString("Name",entry.getString("Name")); modifier.setDouble("Amount",amount); - modifier.setInt("Operation",operation.intValue()); modifier.setUniqueId("UUID",id); - NBTTagList candidate = modifiers.copy(); candidate.add(modifier); + CompoundNBT modifier = new CompoundNBT(); + 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); if(!finiteAttributeValue(base,candidate)) { changed = true; continue; } ids.add(id); modifiers.add(modifier); if(!Arrays.asList("Name","Amount","Operation","UUIDMost","UUIDLeast").containsAll(entry.keySet())) changed = true; } } - accepted.setTag("Modifiers",modifiers); + accepted.put("Modifiers",modifiers); } if(changed) retainInvalid("Attribute",record); clean.add(accepted); @@ -1175,28 +1169,28 @@ private NBTTagList readAttributes(INBTBase raw) { return clean; } - private static boolean finiteAttributeValue(double base, NBTTagList modifiers) { + private static boolean finiteAttributeValue(double base, ListNBT modifiers) { double value = base; for(int i = 0; i < modifiers.size(); i++) { - NBTTagCompound m = modifiers.getCompound(i); + CompoundNBT 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++) { - NBTTagCompound m = modifiers.getCompound(i); + CompoundNBT 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++) { - NBTTagCompound m = modifiers.getCompound(i); + CompoundNBT m = modifiers.getCompound(i); if(m.getInt("Operation") == 2) value *= 1D + m.getDouble("Amount"); } return Double.isFinite(value); } - private long readTimer(NBTTagCompound root, String key, long maximum, long fallback) { - INBTBase raw = root.getTag(key); + private long readTimer(CompoundNBT root, String key, long maximum, long fallback) { + INBT raw = root.get(key); if(raw == null) return fallback; Long value = integral(raw); if(value != null && value >= 0 && value <= maximum) return value; @@ -1206,13 +1200,13 @@ private long readTimer(NBTTagCompound root, String key, long maximum, long fallb private long checkedTimestamp(String key, long timestamp, long now) { if(timestamp >= 0 && timestamp <= now) return timestamp; - retainInvalid(key,new net.minecraft.nbt.NBTTagLong(timestamp)); + retainInvalid(key,new net.minecraft.nbt.LongNBT(timestamp)); markDirty(); return now; } - private String readName(NBTTagCompound root,String key) { - INBTBase raw=root.getTag(key);if(raw==null)return null; + private String readName(CompoundNBT root,String key) { + INBT 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) { @@ -1221,7 +1215,7 @@ private String readName(NBTTagCompound root,String key) { if(text==null){retainInvalid(key,raw);return null;}name=text.getString(); }catch(RuntimeException malformed){retainInvalid(key,raw);return null;} } - String packet=net.minecraft.util.text.ITextComponent.Serializer.toJson(new net.minecraft.util.text.TextComponentString(name)); + String packet=net.minecraft.util.text.ITextComponent.Serializer.toJson(new net.minecraft.util.text.StringTextComponent(name)); if(name.length()<=32767 && name.getBytes(StandardCharsets.UTF_8).length<=32767 && packet.getBytes(StandardCharsets.UTF_8).length<=32767)return name.trim().isEmpty()?"":name; } @@ -1230,76 +1224,76 @@ private String readName(NBTTagCompound root,String key) { - private static boolean isInventoryList(INBTBase data) { - return data instanceof NBTTagList && (((NBTTagList)data).size() == 0 || ((NBTTagList)data).getTagType() == 10); + private static boolean isInventoryList(INBT data) { + return data instanceof ListNBT && (((ListNBT)data).size() == 0 || ((ListNBT)data).getTagType() == 10); } - private void retainRaw(String source, INBTBase data) { - NBTTagCompound record = new NBTTagCompound(); - record.setString("Source",source); - record.setTag("Data",data.copy()); - if(!inventoryVersions.isEmpty()) record.setTag("SourceVersions",inventoryVersions.copy()); + private void retainRaw(String source, INBT data) { + CompoundNBT record = new CompoundNBT(); + 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, INBTBase data) { + private void readInventoryTag(String source, INBT data) { if(data == null) return; if(!isInventoryList(data)) { retainRaw(source + "List",data); return; } - NBTTagList list = (NBTTagList)data; + ListNBT list = (ListNBT)data; for(int i = 0; i < list.size(); i++) readInventoryEntry(source,list.getCompound(i),null); } - private static Long integral(INBTBase data) { - return data instanceof NBTPrimitive && data.getId() >= 1 && data.getId() <= 4 ? ((NBTPrimitive)data).getLong() : null; + private static Long integral(INBT data) { + return data instanceof NumberNBT && data.getId() >= 1 && data.getId() <= 4 ? ((NumberNBT)data).getLong() : null; } - private ItemStack decodeInventoryItem(NBTTagCompound raw) { + private ItemStack decodeInventoryItem(CompoundNBT 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; if(!raw.contains("id",8)) return null; - Long count = integral(raw.getTag("Count")); - Long legacyDamage = raw.hasKey("Damage") ? integral(raw.getTag("Damage")) : Long.valueOf(0L); - Long damage = raw.getCompound("tag").hasKey("Damage") ? integral(raw.getCompound("tag").getTag("Damage")) : legacyDamage; + Long count = integral(raw.get("Count")); + Long legacyDamage = raw.contains("Damage") ? integral(raw.get("Damage")) : Long.valueOf(0L); + Long damage = raw.getCompound("tag").contains("Damage") ? integral(raw.getCompound("tag").get("Damage")) : legacyDamage; if(legacyDamage == null || legacyDamage < 0 || legacyDamage > 32767 - || (raw.hasKey("Damage") && (damage==null || legacyDamage.longValue()!=damage.longValue())))return null; + || (raw.contains("Damage") && (damage==null || legacyDamage.longValue()!=damage.longValue())))return null; if(count == null || count <= 0 || count > 127 || damage == null || damage < 0 || damage > 32767) return null; - if((raw.hasKey("tag") && !raw.contains("tag",10)) || (raw.hasKey("ForgeCaps") && !raw.contains("ForgeCaps",10))) return null; + if((raw.contains("tag") && !raw.contains("tag",10)) || (raw.contains("ForgeCaps") && !raw.contains("ForgeCaps",10))) return null; try { ResourceLocation id = new ResourceLocation(raw.getString("id")); if(!net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(id)) return null; Item registered = net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(id); if(registered == null || Item.getIdFromItem(registered) == 0) return null; - NBTTagCompound input = raw.copy(); - input.setString("id",id.toString()); - input.setByte("Count",count.byteValue()); - if(raw.hasKey("Damage"))input.setShort("Damage",legacyDamage.shortValue()); + CompoundNBT 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); if(emptyStack(stack) || stack.getItem() != registered || stack.getCount() != count.intValue() || stack.getDamage() != damage.intValue() || stack.getMaxStackSize() <= 0) return null; - NBTTagCompound encoded = stack.write(new NBTTagCompound()); - if(!Objects.equals(raw.getTag("tag"),encoded.getTag("tag")) - || !Objects.equals(raw.getTag("ForgeCaps"),encoded.getTag("ForgeCaps"))) return null; + CompoundNBT encoded = stack.write(new CompoundNBT()); + if(!Objects.equals(raw.get("tag"),encoded.get("tag")) + || !Objects.equals(raw.get("ForgeCaps"),encoded.get("ForgeCaps"))) return null; return stack; } catch(RuntimeException unreadable) { return null; } } - private void readInventoryEntry(String source, NBTTagCompound raw, NBTTagCompound retained) { - NBTTagCompound context=retained!=null && retained.contains("SourceVersions",10) ? retained.getCompound("SourceVersions") : inventoryVersions; - NBTTagCompound converted=ItemMigration.convert(raw,context); + 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); ItemStack stack = converted==null ? null : decodeInventoryItem(converted); if(emptyStack(stack)) { if(retained == null) retainRaw(source,raw); else { - NBTTagCompound copy=retained.copy(); - if(!copy.hasKey("SourceVersions") && !context.isEmpty()) copy.setTag("SourceVersions",context.copy()); + CompoundNBT 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()); if(source.equals("Equipment")) { - Long number = integral(raw.getTag("Slot")); + Long number = integral(raw.get("Slot")); if(number != null && number >= 0 && number < getSizeInventory()) { int slot = number.intValue(); if(emptyStack(getStackInSlot(slot)) && isItemValidForSlot(slot,stack)) { diff --git a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java index 97d7aca..d336573 100644 --- a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java +++ b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java @@ -5,12 +5,12 @@ import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.init.Items; -import net.minecraft.inventory.EntityEquipmentSlot; +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.EnumHandSide; +import net.minecraft.util.HandSide; import net.minecraft.world.World; import net.minecraftforge.event.entity.living.LivingDeathEvent; import net.minecraftforge.event.entity.EntityEvent; @@ -31,7 +31,7 @@ public class PlayerDeathEventHandler { - private static final Map> equipmentCache = new WeakHashMap<>(); + private static final Map> equipmentCache = new WeakHashMap<>(); @SubscribeEvent(priority= EventPriority.HIGHEST) public void corpseEnteringChunk(EntityEvent.EnteringChunk e) { @@ -41,22 +41,22 @@ public void corpseEnteringChunk(EntityEvent.EnteringChunk e) { @SubscribeEvent(priority= EventPriority.LOW, receiveCanceled = true) public void entityDeathEvent(LivingDeathEvent e){ - if(e.getEntity() instanceof EntityPlayer + if(e.getEntity() instanceof PlayerEntity && !e.getEntity().getEntityWorld().isRemote) { - final EntityPlayer player = (EntityPlayer)e.getEntity(); + final PlayerEntity player = (PlayerEntity)e.getEntity(); equipmentCache.remove(player); if(e.isCanceled() || e.getResult() == Event.Result.DENY || player.isSpectator() - || player.getEntityWorld().getGameRules().getBoolean("keepInventory")) return; - Map cache = new IdentityHashMap<>(); - for(EntityEquipmentSlot slot : EntityLootableBody.EQUIPMENT_SLOTS){ + || 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); if(!emptyStack(stack)) cache.put(stack,slot); } - if(player.getPrimaryHand() == EnumHandSide.LEFT){ + if(player.getPrimaryHand() == HandSide.LEFT){ // swap main and off hand items (easier than messing with the rendering code) - cache.put(player.getItemStackFromSlot(EntityEquipmentSlot.MAINHAND),EntityEquipmentSlot.OFFHAND); - cache.put(player.getItemStackFromSlot(EntityEquipmentSlot.OFFHAND),EntityEquipmentSlot.MAINHAND); + cache.put(player.getItemStackFromSlot(EquipmentSlotType.MAINHAND),EquipmentSlotType.OFFHAND); + cache.put(player.getItemStackFromSlot(EquipmentSlotType.OFFHAND),EquipmentSlotType.MAINHAND); } equipmentCache.put(player,cache); } @@ -64,12 +64,12 @@ public void entityDeathEvent(LivingDeathEvent e){ @SubscribeEvent(priority= EventPriority.LOWEST, receiveCanceled = true) public void entityDropEvent(LivingDropsEvent e){ - if(e.getEntity() instanceof EntityPlayer + if(e.getEntity() instanceof PlayerEntity && !e.getEntity().getEntityWorld().isRemote) { - final EntityPlayer player = (EntityPlayer)e.getEntity(); - Map cache = equipmentCache.remove(player); + final PlayerEntity player = (PlayerEntity)e.getEntity(); + Map cache = equipmentCache.remove(player); if(e.isCanceled() || e.getResult() == Event.Result.DENY || player.isSpectator() - || player.getEntityWorld().getGameRules().getBoolean("keepInventory")) return; + || player.getEntityWorld().getGameRules().getBoolean(net.minecraft.world.GameRules.KEEP_INVENTORY)) return; if(cache == null) cache = Collections.emptyMap(); final World w = player.getEntityWorld(); @@ -78,11 +78,11 @@ public void entityDropEvent(LivingDropsEvent e){ corpse.setRotation(player.rotationYaw); List items = new ArrayList<>(); - Set assignedSlots = EnumSet.noneOf(EntityEquipmentSlot.class); - for (EntityItem itemEntity : e.getDrops()) { + Set assignedSlots = EnumSet.noneOf(EquipmentSlotType.class); + for (ItemEntity itemEntity : e.getDrops()) { ItemStack item = itemEntity.getItem(); if(emptyStack(item) || item.getCount() <= 0) continue; - EntityEquipmentSlot slot = cache.get(item); + EquipmentSlotType 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()); @@ -97,7 +97,7 @@ public void entityDropEvent(LivingDropsEvent e){ corpse.addItem(new ItemStack(Items.ROTTEN_FLESH,1+w.rand.nextInt(3))); } - if(w.spawnEntity(corpse)) { + if(w.addEntity(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 0b80dfd..e60ba4c 100644 --- a/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java +++ b/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java @@ -11,6 +11,6 @@ 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.loadedEntityList)if(entity instanceof EntityLootableBody)((EntityLootableBody)entity).refreshClientSkin(); + for(Entity entity:mc.world.getAllEntities())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 e21f043..4105348 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java @@ -4,52 +4,58 @@ 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.minecraftforge.api.distmarker.*; /** The ordinary Mods-list settings screen, retaining the existing CFG choices. */ @OnlyIn(Dist.CLIENT) -public final class CorpseConfigGui extends GuiScreen { +public final class CorpseConfigGui extends Screen { private static final String[] APPEARANCE={"local_texture","display_nametag"}; private static final String[] RULES={"corpse_HP","add_bones_to_corpse","item_decay_rate","hurt_by_environment","hurt_by_weapons","hurt_by_misc","enable_corpse_decay","empty_only_decay","corpse_decay_time","enable_corpse_buoyancy","prevent_void_loss","dim_specific_platform"}; - private final GuiScreen parent; + 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(GuiScreen parent){this(parent,null,new LinkedHashMap<>(),0);for(String key:keys())pending.put(key,property(key).getString());} - private CorpseConfigGui(GuiScreen parent,String group,Map pending,int page){this.parent=parent;this.group=group;this.pending=new LinkedHashMap<>(pending);this.page=page;} + 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 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 GuiButton button(int id,int x,int y,int width,String label,Runnable click){return addButton(new GuiButton(id,x,y,width,20,label){@Override public void onClick(double x,double y){super.onClick(x,y);if(enabled)click.run();}});} - @Override protected void initGui(){ + 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));} + 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,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"),()->mc.displayGuiScreen(new CorpseConfigGui(this,"appearance",pending,0))); - button(2,left,95,310,I18n.format("lootablebodies.config.rules"),()->mc.displayGuiScreen(new CorpseConfigGui(this,"rules",pending,0))).enabled=mc.world==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; }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(mc,width,height);}); - else {GuiTextField field=new GuiTextField(i,fontRenderer,width/2+60,y+1,95,18);field.setMaxStringLength(64);field.setText(pending.get(key));fields.put(key,field);children.add(field);} + if(value.getType()=='B')button(20+i,width/2+70,y,85,booleanLabel(key),()->{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,property(key).getLanguageKey());field.setMaxStringLength(64);field.setText(pending.get(key));fields.put(key,field);children.add(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(mc,width,height);}); + 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(200,width/2-155,height-28,150,I18n.format("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);mc.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.displayGuiScreen(parent); }); - button(201,width/2+5,height-28,150,I18n.format("gui.cancel"),()->mc.displayGuiScreen(parent)); + button(201,width/2+5,height-28,150,I18n.format("gui.cancel"),()->minecraft.displayGuiScreen(parent)); } - private void init(net.minecraft.client.Minecraft minecraft,int width,int height){setWorldAndResolution(minecraft,width,height);} - private void capture(){for(Map.Entry entry:fields.entrySet())pending.put(entry.getKey(),entry.getValue().getText());} - private void changePage(int change){capture();page+=change;init(mc,width,height);} + private void capture(){for(Map.Entry entry:fields.entrySet())pending.put(entry.getKey(),entry.getValue().getText());} + 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 boolean validPending(){ for(String key:keys()){ @@ -58,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(GuiTextField field:fields.values())field.tick();} + @Override public void tick(){for(TextFieldWidget field:fields.values())field.tick();} @Override public void render(int mouseX,int mouseY,float partialTicks){ - drawDefaultBackground();drawCenteredString(fontRenderer,LootableBodies.NAME,width/2,12,0xffffff); - drawCenteredString(fontRenderer,I18n.format(group==null?"lootablebodies.config.local_file":"lootablebodies.config."+group),width/2,28,0xcccccc); + renderBackground();drawCenteredString(font,LootableBodies.NAME,width/2,12,0xffffff); + drawCenteredString(font,I18n.format(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(fontRenderer,fontRenderer.trimStringToWidth(text,205),width/2-155,row.getValue()+6,0xffffff); + String text=I18n.format(property(row.getKey()).getLanguageKey());drawString(font,font.trimStringToWidth(text,205),width/2-155,row.getValue()+6,0xffffff); } - super.render(mouseX,mouseY,partialTicks);for(GuiTextField field:fields.values())field.drawTextField(mouseX,mouseY,partialTicks); + super.render(mouseX,mouseY,partialTicks);for(TextFieldWidget field:fields.values())field.render(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) - drawHoveringText(fontRenderer.listFormattedStringToWidth(I18n.format("lootablebodies.config.rules.tooltip"),300),mouseX,mouseY); - if(invalid)drawCenteredString(fontRenderer,I18n.format("lootablebodies.config.invalid"),width/2,height-72,0xff5555); + renderTooltip(font.listFormattedStringToWidth(I18n.format("lootablebodies.config.rules.tooltip"),300),mouseX,mouseY); + if(invalid)drawCenteredString(font,I18n.format("lootablebodies.config.invalid"),width/2,height-72,0xff5555); } - @Override public void close(){capture();mc.displayGuiScreen(parent);} + @Override public void onClose(){capture();minecraft.displayGuiScreen(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 66653cf..ecf6cc5 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java @@ -1,10 +1,10 @@ package cyano.lootable.graphics; import java.util.function.BiFunction; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.screen.Screen; import net.minecraftforge.api.distmarker.*; /** The regular Forge Mods-list Config button entry point. */ @OnlyIn(Dist.CLIENT) -public final class CorpseConfigGuiFactory implements BiFunction { - @Override public GuiScreen apply(Minecraft minecraft,GuiScreen parent){return new CorpseConfigGui(parent);} +public final class CorpseConfigGuiFactory implements BiFunction { + @Override public Screen apply(Minecraft minecraft,Screen parent){return new CorpseConfigGui(parent);} } diff --git a/src/main/java/cyano/lootable/graphics/CorpseContainer.java b/src/main/java/cyano/lootable/graphics/CorpseContainer.java index 613caef..407a186 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseContainer.java @@ -4,15 +4,15 @@ import cyano.lootable.entities.EntityLootableBody; import net.minecraft.entity.Entity; -import net.minecraft.entity.item.EntityItem; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.entity.player.InventoryPlayer; -import net.minecraft.inventory.EntityEquipmentSlot; +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.Slot; -import net.minecraft.inventory.ClickType; -import net.minecraft.item.ItemArmor; +import net.minecraft.inventory.container.Slot; +import net.minecraft.inventory.container.ClickType; +import net.minecraft.item.ArmorItem; import net.minecraft.item.ItemStack; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -22,19 +22,21 @@ /** * Created by Chris on 4/12/2016. */ -public class CorpseContainer extends net.minecraft.inventory.Container { +public class CorpseContainer extends net.minecraft.inventory.container.Container { - private static final EntityEquipmentSlot[] EQUIPMENT_SLOTS = EntityLootableBody.EQUIPMENT_SLOTS; + private static final EquipmentSlotType[] EQUIPMENT_SLOTS = EntityLootableBody.EQUIPMENT_SLOTS; private final IInventory targetInventory; private boolean dragging; - public CorpseContainer(InventoryPlayer playerItems, IInventory entity){ + public CorpseContainer(PlayerInventory playerItems, IInventory entity){this(0,playerItems,entity);} + public CorpseContainer(int windowId, PlayerInventory playerItems, IInventory 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.Slot(targetInventory, i, 8, 8 + 18*i) { + this.addSlot(new net.minecraft.inventory.container.Slot(targetInventory, i, 8, 8 + 18*i) { @Override public int getSlotStackLimit() { return 1; @@ -55,7 +57,7 @@ public String getSlotTexture() while(index < EQUIPMENT_SLOTS.length){ // held items final int i = index; - this.addSlot(new net.minecraft.inventory.Slot(targetInventory, i, 8 + 18 * (i - 4), 98 ) { + this.addSlot(new net.minecraft.inventory.container.Slot(targetInventory, i, 8 + 18 * (i - 4), 98 ) { @OnlyIn(Dist.CLIENT) public String getSlotTexture() { @@ -81,12 +83,12 @@ public String getSlotTexture() @Override - public boolean canInteractWith(EntityPlayer entityplayer) { + public boolean canInteractWith(PlayerEntity entityplayer) { return targetInventory.isUsableByPlayer(entityplayer); } @Override - public ItemStack slotClick(int slotId, int button, ClickType type, EntityPlayer player) { + public ItemStack slotClick(int slotId, int button, ClickType type, PlayerEntity player) { // A click can arrive before the next player tick closes a stale window. if(!canInteractWith(player)) { resetDrag(); @@ -145,7 +147,7 @@ public ItemStack slotClick(int slotId, int button, ClickType type, EntityPlayer } @Override - public void onContainerClosed(EntityPlayer player) { + public void onContainerClosed(PlayerEntity player) { ItemStack cursor = player.inventory.getItemStack(); if(!emptyStack(cursor)) { releaseOrRetain(player,cursor); @@ -159,24 +161,24 @@ protected void resetDrag() { super.resetDrag(); } - private void resync(EntityPlayer player) { + private void resync(PlayerEntity player) { // Accepted transaction replies assume client prediction succeeded. A // rejected toss needs an explicit source/cursor correction as well. - if(player instanceof EntityPlayerMP) ((EntityPlayerMP)player).sendContainerToPlayer(this); + if(player instanceof ServerPlayerEntity) ((ServerPlayerEntity)player).sendContainerToPlayer(this); } - private void releaseOrRetain(EntityPlayer player, ItemStack stack) { + private void releaseOrRetain(PlayerEntity player, ItemStack stack) { if(emptyStack(stack) || stack.getCount() <= 0 || tryRelease(player,stack,false)) return; ItemStack remainder = stack.copy(); - // InventoryPlayer discards overflow in creative mode. Retain real + // PlayerInventory discards overflow in creative mode. Retain real // corpse/cursor items even when every player slot is full. storeInPlayerInventory(player.inventory,remainder); if(remainder.getCount() > 0) ((EntityLootableBody)targetInventory).retainItem(remainder); } - private void storeInPlayerInventory(InventoryPlayer inventory, ItemStack remainder) { + private void storeInPlayerInventory(PlayerInventory inventory, ItemStack remainder) { // Keep vanilla's selected-slot, off-hand, main-slot and first-empty - // priority, without InventoryPlayer's creative overflow discard. + // priority, without PlayerInventory's creative overflow discard. while(remainder.getCount() > 0) { int destination = -1; if(!remainder.isDamaged()) { @@ -204,14 +206,14 @@ private void storeInPlayerInventory(InventoryPlayer inventory, ItemStack remaind } } - private boolean canStore(InventoryPlayer inventory, int slot, ItemStack incoming) { + private boolean canStore(PlayerInventory inventory, int slot, ItemStack incoming) { ItemStack current = inventory.getStackInSlot(slot); return !emptyStack(current) && current.isStackable() && current.getItem() == incoming.getItem() && ItemStack.areItemStackTagsEqual(current,incoming) && current.getCount() < Math.min(current.getMaxStackSize(),inventory.getInventoryStackLimit()); } - private boolean tryRelease(EntityPlayer player, ItemStack stack, boolean includeName) { + private boolean tryRelease(PlayerEntity player, ItemStack stack, boolean includeName) { if(emptyStack(stack) || stack.getCount() <= 0) return false; if(player.world.isRemote) { player.dropItem(stack.copy(),includeName); @@ -219,8 +221,8 @@ private boolean tryRelease(EntityPlayer player, ItemStack stack, boolean include } // Forge's legacy toss helper clears the caller's captured drops and // ignores spawn rejection. Preserve both ownership and the toss event. - java.util.Collection capturing = player.captureDrops(new java.util.ArrayList<>()); - EntityItem dropped; + java.util.Collection capturing = player.captureDrops(new java.util.ArrayList<>()); + ItemEntity dropped; try { dropped = player.dropItem(stack.copy(),false,includeName); if(dropped != null) player.captureDrops().remove(dropped); @@ -234,7 +236,7 @@ private boolean tryRelease(EntityPlayer player, ItemStack stack, boolean include capturing.add(event.getEntityItem()); return true; } - return player.world.spawnEntity(event.getEntityItem()); + return player.world.addEntity(event.getEntityItem()); } @Override @@ -267,7 +269,7 @@ protected boolean mergeItemStack(ItemStack stack, int start, int end, boolean re @Override - public ItemStack transferStackInSlot(EntityPlayer player, int slot) { + public ItemStack transferStackInSlot(PlayerEntity player, int slot) { int hostSize = targetInventory.getSizeInventory(); ItemStack stack = ItemStack.EMPTY; Slot slotObject = (Slot) inventorySlots.get(slot); @@ -301,7 +303,7 @@ else if (!this.mergeItemStack(stackInSlot, 0, hostSize, false)) { return stack; } - protected void bindPlayerInventory(InventoryPlayer playerInv, int yOffset) { + protected void bindPlayerInventory(PlayerInventory 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 a042995..936ba82 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java @@ -1,23 +1,28 @@ package cyano.lootable.graphics; import cyano.lootable.LootableBodies; -import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.entity.player.PlayerInventory; import net.minecraft.inventory.IInventory; import net.minecraft.util.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import org.lwjgl.opengl.GL11; +import com.mojang.blaze3d.platform.GlStateManager; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.util.text.StringTextComponent; /** * Created by Chris on 4/12/2016. */ @OnlyIn(Dist.CLIENT) -public class CorpseGUIContainer extends net.minecraft.client.gui.inventory.GuiContainer{ +public class CorpseGUIContainer extends net.minecraft.client.gui.screen.inventory.ContainerScreen{ private final ResourceLocation image = new ResourceLocation(LootableBodies.MODID+":textures/gui/corpse.png"); - public CorpseGUIContainer(InventoryPlayer playerItems, IInventory entity) { - super(new CorpseContainer(playerItems,entity)); + 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(CorpseContainer container, PlayerInventory playerItems, ITextComponent title){ + super(container,playerItems,title); this.xSize = 176; this.ySize = 222; } @@ -32,10 +37,10 @@ public CorpseGUIContainer(InventoryPlayer playerItems, IInventory entity) { */ @Override protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { - GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F); int x = (width - xSize) / 2; int y = (height - ySize) / 2; - this.mc.getTextureManager().bindTexture(image); - this.drawTexturedModalRect(x, y, 0, 0, this.xSize, this.ySize); // x, y, textureOffsetX, textureOffsetY, width, height) + this.minecraft.getTextureManager().bindTexture(image); + this.blit(x, y, 0, 0, this.xSize, this.ySize); // x, y, textureOffsetX, textureOffsetY, width, height) } } diff --git a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java index ae3c29b..d5ae411 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java @@ -5,17 +5,18 @@ import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.entity.model.ModelPlayer; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.OpenGlHelper; -import net.minecraft.client.renderer.entity.RenderLivingBase; -import net.minecraft.client.renderer.entity.RenderManager; -import net.minecraft.client.renderer.entity.RenderPigZombie; -import net.minecraft.client.renderer.entity.RenderPlayer; -import net.minecraft.client.renderer.entity.layers.LayerArrow; -import net.minecraft.client.renderer.entity.layers.LayerBipedArmor; -import net.minecraft.client.renderer.entity.layers.LayerCustomHead; -import net.minecraft.client.renderer.entity.layers.LayerHeldItem; +import net.minecraft.client.renderer.entity.model.PlayerModel; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.GLX; +import net.minecraft.client.renderer.entity.model.BipedModel; +import net.minecraft.client.renderer.entity.LivingRenderer; +import net.minecraft.client.renderer.entity.EntityRendererManager; +import net.minecraft.client.renderer.entity.PigZombieRenderer; +import net.minecraft.client.renderer.entity.PlayerRenderer; +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.resources.DefaultPlayerSkin; import net.minecraft.util.ResourceLocation; import cyano.lootable.ModLog; @@ -31,40 +32,40 @@ @OnlyIn(Dist.CLIENT) -public class CorpseRenderer extends RenderLivingBase { +public class CorpseRenderer extends LivingRenderer> { private static final ResourceLocation localSkin = new ResourceLocation(LootableBodies.MODID+":textures/entity/corpse/corpse.png"); - protected final ModelPlayer thickArmsModel; - protected final ModelPlayer thinArmsModel; + protected final PlayerModel thickArmsModel; + protected final PlayerModel thinArmsModel; - public CorpseRenderer(RenderManager renderManagerIn) { - super(renderManagerIn, new ModelPlayer(0.0F, true), 0.5F); - thinArmsModel = (ModelPlayer)this.mainModel; - thickArmsModel = new ModelPlayer(0.0F, false); + public CorpseRenderer(EntityRendererManager renderManagerIn) { + super(renderManagerIn, new PlayerModel(0.0F, true), 0.5F); + thinArmsModel = (PlayerModel)this.entityModel; + thickArmsModel = new PlayerModel(0.0F, false); // - this.addLayer(new LayerBipedArmor(this)); - this.addLayer(new LayerHeldItem(this)); - this.addLayer(new LayerArrow(this)); - this.addLayer(new LayerCustomHead(this.getMainModel().bipedHead)); - RenderPlayer k; - RenderPigZombie j; + this.addLayer(new BipedArmorLayer<>(this,new BipedModel<>(0.5F),new BipedModel<>(1.0F))); + this.addLayer(new HeldItemLayer(this)); + this.addLayer(new ArrowLayer(this)); + this.addLayer(new HeadLayer<>(this)); + PlayerRenderer k; + PigZombieRenderer j; } - public ModelPlayer getMainModel() + public PlayerModel getMainModel() { - return (ModelPlayer)super.getMainModel(); + return (PlayerModel)super.getEntityModel(); } public void setModel(boolean thinArms){ if(thinArms){ - this.mainModel = this.thinArmsModel; + this.entityModel = this.thinArmsModel; } else { - this.mainModel = this.thickArmsModel; + this.entityModel = this.thickArmsModel; } } /** - * Returns the location of an entity's texture. Doesn't seem to be called unless you call Render.bindEntityTexture. + * Returns the location of an entity's texture. Doesn't seem to be called unless you call EntityRenderer.bindEntityTexture. * * @param entity The entity to be rendered */ @@ -102,7 +103,7 @@ public static ResourceLocation getSkin(GameProfile profile) { @Override public void doRender(EntityLootableBody entity, double x, double y, double z, float yaw, float partialTick) { - if (net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.RenderLivingEvent.Pre(entity, this, partialTick, x, y, z))) + if (net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.RenderLivingEvent.Pre<>(entity, this, partialTick, x, y, z))) return; this.setModel(entity.useThinArms()); @@ -110,10 +111,10 @@ public void doRender(EntityLootableBody entity, double x, double y, double z, fl // render the model GlStateManager.pushMatrix(); GlStateManager.disableCull(); - this.mainModel.swingProgress = 0; + this.entityModel.swingProgress = 0; boolean shouldSit = entity.isPassenger() && (entity.getRidingEntity() != null && entity.getRidingEntity().shouldRiderSit()); - this.mainModel.isRiding = shouldSit; - this.mainModel.isChild = entity.isChild(); + this.entityModel.isSitting = shouldSit; + this.entityModel.isChild = entity.isChild(); try { float rotationInterpolation = 0F; @@ -130,14 +131,14 @@ public void doRender(EntityLootableBody entity, double x, double y, double z, fl GlStateManager.enableAlphaTest(); - this.mainModel.setLivingAnimations(entity, armSwing, armSwingAmount, 0); - this.mainModel.setRotationAngles(armSwing, armSwingAmount, age, headYaw, headPitch, scale, entity); + this.entityModel.setLivingAnimations(entity, armSwing, armSwingAmount, 0); + this.entityModel.setRotationAngles(entity, armSwing, armSwingAmount, age, headYaw, headPitch, scale); // sigh, unable to pose the model if (this.renderOutlines) { boolean flag1 = this.setScoreTeamColor(entity); GlStateManager.enableColorMaterial(); - GlStateManager.enableOutlineMode(this.getTeamColor(entity)); + GlStateManager.setupSolidRenderingTextureCombine(this.getTeamColor(entity)); if (!this.renderMarker) { this.renderModel(entity, armSwing, armSwingAmount, age, headYaw, headPitch, scale); @@ -147,7 +148,7 @@ public void doRender(EntityLootableBody entity, double x, double y, double z, fl this.renderLayers(entity, armSwing, armSwingAmount, partialTick, age, headYaw, headPitch, scale); - GlStateManager.disableOutlineMode(); + GlStateManager.tearDownSolidRenderingTextureCombine(); GlStateManager.disableColorMaterial(); if (flag1) { @@ -173,16 +174,16 @@ public void doRender(EntityLootableBody entity, double x, double y, double z, fl ModLog.log(Level.ERROR, ex, "Couldn\'t render entity"); } - GlStateManager.activeTexture(OpenGlHelper.GL_TEXTURE1); - GlStateManager.enableTexture2D(); - GlStateManager.activeTexture(OpenGlHelper.GL_TEXTURE0); + GlStateManager.activeTexture(GLX.GL_TEXTURE1); + GlStateManager.enableTexture(); + GlStateManager.activeTexture(GLX.GL_TEXTURE0); GlStateManager.enableCull(); GlStateManager.popMatrix(); if (!this.renderOutlines && LootableBodies.displayNameTag) { this.renderName(entity, x, y, z); } - net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.RenderLivingEvent.Post(entity, this, partialTick, x, y, z)); + net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.RenderLivingEvent.Post<>(entity, this, partialTick, x, y, z)); this.setModel(true); } diff --git a/src/main/java/cyano/lootable/graphics/GUIHandler.java b/src/main/java/cyano/lootable/graphics/GUIHandler.java index d573b77..6e8a13f 100644 --- a/src/main/java/cyano/lootable/graphics/GUIHandler.java +++ b/src/main/java/cyano/lootable/graphics/GUIHandler.java @@ -10,7 +10,7 @@ public final class GUIHandler { private static final GUIHandler INSTANCE=new GUIHandler(); private GUIHandler() { } public static GUIHandler getInstance(){return INSTANCE;} - public static void open(EntityPlayer player,EntityLootableBody corpse){if(player instanceof EntityPlayerMP)NetworkHooks.openGui((EntityPlayerMP)player,corpse,buf->buf.writeVarInt(corpse.getEntityId()));} - public Object getServerGuiElement(int id,EntityPlayer 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,EntityPlayer 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(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;} } diff --git a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java index 7fba0ca..d72a76a 100644 --- a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java +++ b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java @@ -3,14 +3,14 @@ import io.netty.channel.Channel; import java.util.concurrent.RejectedExecutionException; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiConnecting; -import net.minecraft.network.EnumConnectionState; +import net.minecraft.client.gui.screen.ConnectingScreen; +import net.minecraft.network.ProtocolType; import net.minecraft.network.NetworkManager; 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.common.gameevent.TickEvent; +import net.minecraftforge.event.TickEvent; /** Repairs the pinned Netty's stale read-clear task during client login. */ @OnlyIn(Dist.CLIENT) @@ -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 GuiConnecting))return; + if(!(minecraft.currentScreen instanceof ConnectingScreen))return; NetworkManager manager=ObfuscationReflectionHelper.getPrivateValue( - GuiConnecting.class,(GuiConnecting)minecraft.currentScreen,"field_146371_g"); + ConnectingScreen.class,(ConnectingScreen)minecraft.currentScreen,"field_146371_g"); 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()==EnumConnectionState.LOGIN) + && channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).get()==ProtocolType.LOGIN) channel.read(); }); }catch(RejectedExecutionException closed) { diff --git a/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java b/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java index 624b864..597a62c 100644 --- a/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java +++ b/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java @@ -3,10 +3,10 @@ import java.lang.reflect.*; import java.util.*; import java.util.function.UnaryOperator; -import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.server.MinecraftServer; -import net.minecraft.world.WorldServer; -import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import net.minecraft.world.server.ServerWorld; +import net.minecraft.world.chunk.storage.ChunkLoader; import net.minecraft.world.gen.feature.structure.LegacyStructureDataUtil; /** Installs preservation before native save handlers and chunk readers run. */ public final class ChunkVersionContext { @@ -15,13 +15,13 @@ public static void installServer(MinecraftServer server) { installFixer(server,null); installFixer(server.getActiveAnvilConverter(),null); } - public static void installWorld(WorldServer world) { - Object loader=world.getChunkProvider().chunkLoader; - if(!(loader instanceof AnvilChunkLoader))throw new IllegalStateException("Unsupported chunk reader; refusing unprotected world loading"); + 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(world.getSaveHandler(),null); - installFixer(loader,chunk->LegacyStructureDataUtil.func_212183_a(world.getDimension().getType(),world.getMapStorage()).func_212181_a(chunk)); + installFixer(loader,chunk->LegacyStructureDataUtil.func_215130_a(world.getDimension().getType(),world.getServer().getWorld(net.minecraft.world.dimension.DimensionType.OVERWORLD).getSavedData()).func_212181_a(chunk)); } - 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/ItemMigration.java b/src/main/java/cyano/lootable/upgrade/ItemMigration.java index 75777b9..84c7010 100644 --- a/src/main/java/cyano/lootable/upgrade/ItemMigration.java +++ b/src/main/java/cyano/lootable/upgrade/ItemMigration.java @@ -10,19 +10,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 NBTTagCompound convert(NBTTagCompound raw,NBTTagCompound versions) { + public static CompoundNBT convert(CompoundNBT raw,CompoundNBT versions) { if(versions==null || versions.isEmpty())return raw.copy(); - NBTTagCompound fixed=convertNbt(raw,versions); + CompoundNBT fixed=convertNbt(raw,versions); return fixed!=null && knownNestedItems(fixed)?fixed:null; } /** NBT conversion is independent of live registry resolution. */ - static NBTTagCompound convertNbt(NBTTagCompound raw,NBTTagCompound versions) { + static CompoundNBT convertNbt(CompoundNBT raw,CompoundNBT versions) { if(versions==null || versions.isEmpty()) return raw.copy(); if(versions.getBoolean("Ambiguous")) { - NBTTagCompound agreed=null; + CompoundNBT agreed=null; for(int candidate:versions.getIntArray("CandidateDataVersions")) { - NBTTagCompound context=versions.copy();context.removeTag("Ambiguous");context.removeTag("CandidateDataVersions");context.setInt("DataVersion",candidate); - NBTTagCompound converted=convertNbt(raw,context); + CompoundNBT context=versions.copy();context.remove("Ambiguous");context.remove("CandidateDataVersions");context.putInt("DataVersion",candidate); + CompoundNBT converted=convertNbt(raw,context); if(converted==null || (agreed!=null && !agreed.equals(converted)))return null; agreed=converted; } @@ -30,91 +30,91 @@ static NBTTagCompound convertNbt(NBTTagCompound raw,NBTTagCompound versions) { } if(pipeline==null || !versions.contains("DataVersion",3))return null; int source=versions.getInt("DataVersion"); - if(source<512 || source>1631 || integral(raw.getTag("Count"))==null)return null; + if(source<512 || source>UpgradePreservation.MINECRAFT_VERSION || integral(raw.get("Count"))==null)return null; try { - NBTTagCompound item=NBTUtil.update(pipeline,net.minecraft.util.datafix.TypeReferences.ITEM_STACK,raw.copy(),source,1631); - if(!Objects.equals(integral(raw.getTag("Count")),integral(item.getTag("Count"))) || !Objects.equals(raw.getTag("Slot"),item.getTag("Slot")) || !Objects.equals(raw.getTag("ForgeCaps"),item.getTag("ForgeCaps")))return null; + CompoundNBT item=cyano.lootable.upgrade.NativeNbt.update(pipeline,net.minecraft.util.datafix.TypeReferences.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.getTag(key),item.getTag(key)))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; return nestedInventoriesSurvive(raw,item)?item:null; }catch(RuntimeException invalid){return null;} } - private static boolean flatteningSurvives(INBTBase before,INBTBase after,int source) { - if(before instanceof NBTTagCompound) { - if(!(after instanceof NBTTagCompound))return false; - NBTTagCompound a=(NBTTagCompound)before,b=(NBTTagCompound)after; - if(a.hasKey("id") && a.hasKey("Count") && source<1451) { - Long damage=a.hasKey("Damage")?integral(a.getTag("Damage")):0L; + 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; + 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); - boolean durability=Objects.equals(damage,integral(b.getCompound("tag").getTag("Damage"))); + 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()) { - INBTBase old=a.getTag(key),next=b.getTag(key); + INBT old=a.get(key),next=b.get(key); if(key.equals("ench") || key.equals("StoredEnchantments")) { - if(!enchantmentsSurvive(old,b.getTag(key.equals("ench")?"Enchantments":key)))return false; + 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 NBTTagList && after instanceof NBTTagList) { - NBTTagList a=(NBTTagList)before,b=(NBTTagList)after; + } else if(before instanceof ListNBT && after instanceof ListNBT) { + ListNBT a=(ListNBT)before,b=(ListNBT)after; if(a.size()!=b.size())return false; for(int i=0;i=1 && nbt.getId()<=4 ? ((NBTPrimitive)nbt).getLong() : null; + private static Long integral(INBT nbt) { + return nbt instanceof NumberNBT && nbt.getId()>=1 && nbt.getId()<=4 ? ((NumberNBT)nbt).getLong() : null; } - private static boolean knownNestedItems(INBTBase tag) { - if(tag instanceof NBTTagCompound) { - NBTTagCompound compound=(NBTTagCompound)tag; - if(compound.hasKey("id") && compound.hasKey("Count")) { + private static boolean knownNestedItems(INBT tag) { + if(tag instanceof CompoundNBT) { + CompoundNBT compound=(CompoundNBT)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; } - for(String key:compound.keySet()) if(!key.equals("ForgeCaps") && !knownNestedItems(compound.getTag(key))) return false; - } else if(tag instanceof NBTTagList) { - NBTTagList list=(NBTTagList)tag;for(int i=0;i registry = new RegistryBuilder() + .setName(new ResourceLocation("minecraft:villagerprofessions")) + .setType(Entry.class).setIDRange(0, 32767) + .disableSaving().disableSync().create(); + // These are the six actual 1.10-1.13 vanilla entries, in original ID order. + // Unknown mod professions remain missing and retain Forge's normal gate. + for (String name : new String[]{"farmer", "librarian", "priest", "smith", "butcher", "nitwit"}) + registry.register(new Entry().setRegistryName(new ResourceLocation("minecraft", name))); + } + + public static final class Entry extends ForgeRegistryEntry { } +} diff --git a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java index 0de72a1..6536bd1 100644 --- a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java +++ b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java @@ -8,12 +8,43 @@ import net.minecraftforge.eventbus.api.SubscribeEvent; /** The vanilla XP pickup sound was renamed after Minecraft 1.10. */ -@Mod.EventBusSubscriber(modid=LootableBodies.MODID,bus=Mod.EventBusSubscriber.Bus.MOD) +@Mod.EventBusSubscriber(modid=LootableBodies.MODID,bus=Mod.EventBusSubscriber.Bus.FORGE) public final class LegacyRegistryMappings { + public static ResourceLocation itemReplacement(ResourceLocation old) { + if (!old.getNamespace().equals("minecraft")) return null; + switch (old.getPath()) { + case "sign": return new ResourceLocation("minecraft:oak_sign"); + case "rose_red": return new ResourceLocation("minecraft:red_dye"); + case "cactus_green": return new ResourceLocation("minecraft:green_dye"); + case "dandelion_yellow": return new ResourceLocation("minecraft:yellow_dye"); + default: return null; + } + } + public static ResourceLocation blockReplacement(ResourceLocation old) { + if (!old.getNamespace().equals("minecraft")) return null; + switch (old.getPath()) { + case "sign": return new ResourceLocation("minecraft:oak_sign"); + case "wall_sign": return new ResourceLocation("minecraft:oak_wall_sign"); + default: return null; + } + } private static final ResourceLocation OLD_XP_SOUND=new ResourceLocation("minecraft:entity.experience_orb.touch"); private static final ResourceLocation XP_SOUND=new ResourceLocation("minecraft:entity.experience_orb.pickup"); private LegacyRegistryMappings() { } + @SubscribeEvent public static void items(RegistryEvent.MissingMappings event) { + for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { + ResourceLocation replacement = itemReplacement(mapping.key); + if (replacement != null) mapping.remap(event.getRegistry().getValue(replacement)); + } + } + @SubscribeEvent public static void blocks(RegistryEvent.MissingMappings event) { + for (RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()) { + ResourceLocation replacement = blockReplacement(mapping.key); + if (replacement != null) mapping.remap(event.getRegistry().getValue(replacement)); + } + } + @SubscribeEvent public static void sounds(RegistryEvent.MissingMappings event) { for(RegistryEvent.MissingMappings.Mapping mapping:event.getAllMappings()) if(mapping.key.equals(OLD_XP_SOUND)) mapping.remap(event.getRegistry().getValue(XP_SOUND)); diff --git a/src/main/java/cyano/lootable/upgrade/NativeNbt.java b/src/main/java/cyano/lootable/upgrade/NativeNbt.java new file mode 100644 index 0000000..65d5dd6 --- /dev/null +++ b/src/main/java/cyano/lootable/upgrade/NativeNbt.java @@ -0,0 +1,16 @@ +package cyano.lootable.upgrade; + +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.Dynamic; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.NBTDynamicOps; + +/** 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(); + } +} diff --git a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java index f86ed84..549245e 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java +++ b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java @@ -12,15 +12,15 @@ /** 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; + NativeSafeFixer(DataFixer delegate,UnaryOperator structures){this.delegate=delegate;this.structures=structures;} @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 NBTTagCompound) || input.getOps()!=NBTDynamicOps.INSTANCE)return delegate.update(type,input,version,target); - NBTTagCompound raw=(NBTTagCompound)input.getValue();String kind=type.typeName(); + if(!(input.getValue() instanceof CompoundNBT) || input.getOps()!=NBTDynamicOps.INSTANCE)return delegate.update(type,input,version,target); + CompoundNBT raw=(CompoundNBT)input.getValue();String kind=type.typeName(); try { int source=kind.equals("chunk")?chunkVersion(raw,version):version; - NBTTagCompound result; + CompoundNBT 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); @@ -28,7 +28,7 @@ final class NativeSafeFixer implements DataFixer { } else result=convert(type,raw,source,target); return (Dynamic)new Dynamic<>(NBTDynamicOps.INSTANCE,result); }catch(RuntimeException failure) { - // ChunkProviderServer catches Exception and regenerates a missing + // ServerChunkProvider catches Exception and regenerates a missing // chunk. Preservation failure must escape that fallback and stop. if(kind.equals("chunk"))throw new UnsafeChunkUpgrade(failure); throw failure; @@ -38,48 +38,48 @@ 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 NBTTagCompound convert(DSL.TypeReference type,NBTTagCompound input,int source,int target) { - NBTTagCompound copy=input.copy();Tokens tokens=new Tokens();String kind=type.typeName(); + private CompoundNBT convert(DSL.TypeReference type,CompoundNBT input,int source,int target) { + CompoundNBT copy=input.copy();Tokens tokens=new Tokens();String kind=type.typeName(); if(kind.equals("chunk")) { - NBTTagCompound level=copy.getCompound("Level"); + CompoundNBT level=copy.getCompound("Level"); // Vanilla discards typed Sections/TileEntities from incomplete old // chunks. Keep stored terrain as well as entities and containers. if(source<1493 && (nonempty(level,"Sections") || nonempty(level,"Entities") || nonempty(level,"TileEntities"))) { - level.setBoolean("TerrainPopulated",true);level.setBoolean("LightPopulated",true); + 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") || kind.equals("entity_tree"))copy=shieldEntity(copy,source,tokens,0,0,false); else if(kind.equals("player")) { - NBTTagCompound vehicle=copy.getCompound("RootVehicle"); - if(vehicle.contains("Entity",10))vehicle.setTag("Entity",shieldEntity(vehicle.getCompound("Entity"),source,tokens,0,0,false)); - for(String shoulder:new String[]{"ShoulderEntityLeft","ShoulderEntityRight"})if(copy.contains(shoulder,10))copy.setTag(shoulder,shieldEntity(copy.getCompound(shoulder),source,tokens,0,0,false)); + CompoundNBT 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); - NBTTagCompound result; + CompoundNBT 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 // copies before the later item and flattening fixes run. - result=NBTUtil.update(delegate,type,copy,source,703); + result=cyano.lootable.upgrade.NativeNbt.update(delegate,type,copy,source,703); bridgeIdentifiers(result,kind); - result=NBTUtil.update(delegate,type,result,705,target); - } else result=source3)throw new IllegalStateException("This corpse was saved by a newer Lootable Bodies format"); - NBTTagCompound safe=raw.copy();boolean current=raw.getString("id").equals("lootablebodies:corpse") && raw.contains(UpgradePreservation.VERSION,3) && raw.getInt(UpgradePreservation.VERSION)==3; - safe.setString("id","lootablebodies:corpse");safe.setInt(UpgradePreservation.VERSION,3); + static CompoundNBT stageCorpse(CompoundNBT raw,CompoundNBT context) { + if(raw.contains(UpgradePreservation.VERSION,3) && raw.getInt(UpgradePreservation.VERSION)>UpgradePreservation.DATA_VERSION)throw new IllegalStateException("This corpse was saved by a newer Lootable Bodies format"); + CompoundNBT safe=raw.copy();boolean current=raw.getString("id").equals("lootablebodies:corpse") && raw.contains(UpgradePreservation.VERSION,3) && raw.getInt(UpgradePreservation.VERSION)==UpgradePreservation.DATA_VERSION; + safe.putString("id","lootablebodies:corpse");safe.putInt(UpgradePreservation.VERSION,UpgradePreservation.DATA_VERSION); if(current)return safe; - safe.setTag(UpgradePreservation.SOURCES,context.copy()); - NBTTagList records=new NBTTagList();boolean custom=list(raw.getTag("Equipment")); - retainInventory(records,"Equipment",raw.getTag("Equipment"),context); - retainInventory(records,"Aux",raw.getTag("Aux"),context); + safe.put(UpgradePreservation.SOURCES,context.copy()); + ListNBT records=new ListNBT();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); } - INBTBase recovery=raw.getTag("LootableBodiesUnresolved"); - if(list(recovery))for(INBTBase value:(NBTTagList)recovery) { - NBTTagCompound record=((NBTTagCompound)value).copy(); - if(record.contains("Source",8) && record.contains("Data",10) && !record.hasKey("SourceVersions") && (record.getString("Source").equals("Equipment") || record.getString("Source").equals("Aux")))record.setTag("SourceVersions",context.copy()); + INBT recovery=raw.get("LootableBodiesUnresolved"); + if(list(recovery))for(INBT value:(ListNBT)recovery) { + CompoundNBT record=((CompoundNBT)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.setTag("Equipment",new NBTTagList());safe.removeTag("Aux");safe.removeTag("ArmorItems");safe.removeTag("HandItems"); - if(!records.isEmpty())safe.setTag("LootableBodiesUnresolved",records);else safe.removeTag("LootableBodiesUnresolved"); + safe.put("Equipment",new ListNBT());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.removeTag("LootableBodiesUpgradePayload");safe.removeTag("LootableBodiesInventorySource"); + safe.remove("LootableBodiesUpgradePayload");safe.remove("LootableBodiesInventorySource"); if(safe.contains("CustomName",8) && context.getInt("DataVersion")<1514) - safe.setString("CustomName",ITextComponent.Serializer.toJson(new TextComponentString(safe.getString("CustomName")))); + safe.putString("CustomName",ITextComponent.Serializer.toJson(new StringTextComponent(safe.getString("CustomName")))); return safe; } - private static boolean list(INBTBase nbt){return nbt instanceof NBTTagList && (((NBTTagList)nbt).isEmpty() || ((NBTTagList)nbt).getTagType()==10);} - private static void retainInventory(NBTTagList records,String source,INBTBase data,NBTTagCompound context) { + 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) { if(data==null)return; - if(list(data))for(INBTBase entry:(NBTTagList)data)retain(records,source,entry,context); + if(list(data))for(INBT entry:(ListNBT)data)retain(records,source,entry,context); else retain(records,source+"List",data,context); } - private static void retainMirror(NBTTagList records,NBTTagList items,int[] slots,NBTTagCompound context) { + private static void retainMirror(ListNBT records,ListNBT items,int[] slots,CompoundNBT context) { for(int i=0;i(item.keySet()))protectEggs(item.getTag(key),source,tokens); - }else if(value instanceof NBTTagList)for(INBTBase entry:(NBTTagList)value)protectEggs(entry,source,tokens); + for(String key:new ArrayList<>(item.keySet()))protectEggs(item.get(key),source,tokens); + }else if(value instanceof ListNBT)for(INBT entry:(ListNBT)value)protectEggs(entry,source,tokens); } - private INBTBase restore(INBTBase value,Tokens tokens) { - if(value instanceof NBTTagCompound) { - NBTTagCompound nbt=(NBTTagCompound)value; + private INBT restore(INBT value,Tokens tokens) { + if(value instanceof CompoundNBT) { + CompoundNBT nbt=(CompoundNBT)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.removeTag(tokens.key);NBTTagCompound tag=nbt.getCompound("tag");tag.setTag("EntityTag",original.data.copy());nbt.setTag("tag",tag); + nbt.remove(tokens.key);CompoundNBT tag=nbt.getCompound("tag");tag.put("EntityTag",original.data.copy());nbt.put("tag",tag); } - for(String key:new ArrayList<>(nbt.keySet()))nbt.setTag(key,restore(nbt.getTag(key),tokens));return nbt; + for(String key:new ArrayList<>(nbt.keySet()))nbt.put(key,restore(nbt.get(key),tokens));return nbt; } - if(value instanceof NBTTagList){NBTTagList list=(NBTTagList)value;for(int i=0;i values=new HashMap<>();final Map restored=new HashMap<>(); - String put(NBTTagCompound data,boolean egg){String id=Integer.toString(values.size());values.put(id,new Protected(data,egg));return id;} + String put(CompoundNBT data,boolean egg){String id=Integer.toString(values.size());values.put(id,new Protected(data,egg));return id;} } - private static boolean sections(NBTTagCompound root,String key,int type) { - NBTTagList sections=root.getCompound("Level").getList("Sections",10);for(int i=0;i=512 && version<=1631 && (!legacy || version<1451) && (!palette || version>=1451); + return version>=512 && version<=UpgradePreservation.MINECRAFT_VERSION && (!legacy || version<1451) && (!palette || version>=1451); } - private static int chunkVersion(NBTTagCompound chunk,int requested) { + private static int chunkVersion(CompoundNBT 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();NBTTagCompound level=chunk.getCompound("Level"); + UpgradeJournal journal=UpgradePreservation.journal();CompoundNBT level=chunk.getCompound("Level"); int preserved=-1; for(UpgradeJournal.LocatedCorpse located:UpgradeJournal.corpses(level.getList("Entities",10))) { try { - NBTTagCompound original=journal==null || !located.entity.hasUniqueId("UUID")?null:journal.originalFor(located.entity.getUniqueId("UUID"),level.getInt("xPos"),level.getInt("zPos")); + CompoundNBT original=journal==null || !located.entity.hasUniqueId("UUID")?null:journal.originalFor(located.entity.getUniqueId("UUID"),level.getInt("xPos"),level.getInt("zPos")); if(original!=null) { - NBTTagCompound context=original.getCompound("SourceVersions");int candidate=UpgradePreservation.chunkSourceVersion(context); + CompoundNBT 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; @@ -250,7 +250,7 @@ private static int chunkVersion(NBTTagCompound 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) { - NBTTagCompound world=journal.worldVersions();int candidate=UpgradePreservation.chunkSourceVersion(world); + CompoundNBT 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) || candidate active=new HashMap<>(); - private NBTTagCompound worldContext=new NBTTagCompound(); + private CompoundNBT worldContext=new CompoundNBT(); private UpgradeJournal(Path world) throws IOException { this.world=world.toRealPath(); @@ -33,7 +33,7 @@ public static UpgradeJournal scan(Path world) throws IOException { UpgradeJournal journal=new UpgradeJournal(world); journal.inspect(); for(Path original:journal.active.values()) { - NBTTagCompound corpse=journal.readSnapshot(original).getCompound("Entity"); + CompoundNBT 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 +41,39 @@ public static UpgradeJournal scan(Path world) throws IOException { } public int size() { return active.size(); } - NBTTagCompound worldVersions(){return worldContext.copy();} + CompoundNBT worldVersions(){return worldContext.copy();} /** Only an entity already present in this chunk may consume its original. */ - public NBTTagCompound originalFor(UUID id,int chunkX,int chunkZ) throws IOException { + public CompoundNBT originalFor(UUID id,int chunkX,int chunkZ) throws IOException { Path path=active.get(id); if(path==null) return null; - NBTTagCompound original=readSnapshot(path); + CompoundNBT original=readSnapshot(path); return original.getInt("ChunkX")==chunkX && original.getInt("ChunkZ")==chunkZ ? original : null; } - NBTTagCompound originalFor(UUID id) throws IOException { + CompoundNBT originalFor(UUID id) throws IOException { Path path=active.get(id); return path==null ? null : readSnapshot(path); } - static boolean corpse(NBTTagCompound entity) { + static boolean corpse(CompoundNBT entity) { String id=entity.getString("id"); return id.equals("lootablebodies:corpse") || id.equals("lootablebodies.Corpse"); } static final class LocatedCorpse { - final NBTTagCompound entity;final int root;final int[] passengers; - LocatedCorpse(NBTTagCompound entity,int root,int[] passengers){this.entity=entity;this.root=root;this.passengers=passengers;} + 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;} } - static List corpses(NBTTagList roots) { + static List corpses(ListNBT roots) { List result=new ArrayList<>(); for(int root=0;root result) { + private static void collect(CompoundNBT 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)); - NBTTagList passengers=entity.getList("Passengers",10); + ListNBT passengers=entity.getList("Passengers",10); for(int i=0;i files=Files.newDirectoryStream(snapshots,"*.nbt")) { for(Path path:files) readSnapshot(path); } - Map previous=readIndex(); - NBTTagCompound worldVersions=readWorldVersions();worldContext=worldVersions.copy(); - NBTTagList updated=new NBTTagList(); + Map previous=readIndex(); + CompoundNBT worldVersions=readWorldVersions();worldContext=worldVersions.copy(); + ListNBT updated=new ListNBT(); for(Path region:regions) { String relative=world.relativize(region).toString().replace('\\','/'); String hash=fileDigest(region); - NBTTagCompound record=previous.get(relative); + CompoundNBT record=previous.get(relative); if(record==null || !hash.equals(record.getString("Digest"))) record=scanRegion(region,relative,hash,worldVersions,record); else record=record.copy(); - NBTTagList originals=record.getList("Snapshots",8); + ListNBT 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; - NBTTagCompound envelope=readCompressed(index); + CompoundNBT 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"); - NBTTagCompound nbt; + CompoundNBT nbt; try(DataInputStream input=new DataInputStream(new GZIPInputStream(new ByteArrayInputStream(payload)))) { nbt=CompressedStreamTools.read(input,new NBTSizeTracker(MAX_NBT)); } - if(!nbt.contains("Format",3) || nbt.getInt("Format")!=FORMAT || !compoundList(nbt.getTag("Regions"))) + if(!nbt.contains("Format",3) || nbt.getInt("Format")!=FORMAT || !compoundList(nbt.get("Regions"))) throw new IOException("Invalid Lootable Bodies upgrade index"); - NBTTagList regions=nbt.getList("Regions",10); + ListNBT regions=nbt.getList("Regions",10); for(int i=0;i0 && list.getTagType()!=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;j readIndex() throws IOException { return records; } - private NBTTagCompound scanRegion(Path file,String relative,String hash,NBTTagCompound worldVersions,NBTTagCompound previous) throws IOException { + private CompoundNBT scanRegion(Path file,String relative,String hash,CompoundNBT worldVersions,CompoundNBT previous) throws IOException { Matcher name=REGION.matcher(file.getFileName().toString()); if(!name.matches()) throw new IOException("Invalid region name"); long rx=Long.parseLong(name.group(1)),rz=Long.parseLong(name.group(2)); if(rxInteger.MAX_VALUE/32 || rzInteger.MAX_VALUE/32) throw new IOException("Region coordinates out of range"); - NBTTagList preserved=new NBTTagList(); - Map prior=new HashMap<>(); - Map priorLevels=new HashMap<>(); + ListNBT preserved=new ListNBT(); + Map prior=new HashMap<>(); + Map priorLevels=new HashMap<>(); Map matchingChunks=new HashMap<>(); if(previous!=null) { - NBTTagList originals=previous.getList("Snapshots",8); + ListNBT originals=previous.getList("Snapshots",8); for(int i=0;iUpgradePreservation.chunkSourceVersion(selected.getCompound("SourceVersions")))prior.put(key,original); } } @@ -195,32 +195,32 @@ private NBTTagCompound scanRegion(Path file,String relative,String hash,NBTTagCo 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); - NBTTagCompound chunk; + CompoundNBT chunk; try(DataInputStream nbt=new DataInputStream(stream)) { chunk=CompressedStreamTools.read(nbt,new NBTSizeTracker(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); - NBTTagCompound level=chunk.getCompound("Level");int x=(int)rx*32+(i%32),z=(int)rz*32+(i/32); + 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); - if(!level.hasKey("Entities")) continue; - if(!compoundList(level.getTag("Entities"))) throw new IOException("Unreadable entity list in "+relative); - NBTTagList entities=level.getList("Entities",10); + if(!level.contains("Entities")) continue; + if(!compoundList(level.get("Entities"))) throw new IOException("Unreadable entity list in "+relative); + ListNBT entities=level.getList("Entities",10); String originalChunk=digest(payload); for(LocatedCorpse located:corpses(entities)) { - NBTTagCompound entity=located.entity;int e=located.root; + CompoundNBT entity=located.entity;int e=located.root; uuid(entity); // An ambiguous identity cannot safely select a backup. - NBTTagCompound snapshot=new NBTTagCompound();snapshot.setInt("Format",FORMAT);snapshot.setString("Region",relative);snapshot.setString("RegionDigest",hash); + CompoundNBT snapshot=new CompoundNBT();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"); - snapshot.setString("OriginalChunk",originalChunk);snapshot.setByte("OriginalCompression",(byte)compression);snapshot.setInt("EntityIndex",e); - if(located.passengers.length>0)snapshot.setIntArray("PassengerPath",located.passengers); - snapshot.setString("Dimension",world.relativize(file.getParent().getParent()).toString().replace('\\','/')); - snapshot.setInt("ChunkX",x);snapshot.setInt("ChunkZ",z);snapshot.setTag("Entity",entity.copy()); - NBTTagCompound versions=sourceVersions(chunk,worldVersions,entity),priorSnapshot=prior.get(chunkKey(x,z)); + snapshot.putString("OriginalChunk",originalChunk);snapshot.putByte("OriginalCompression",(byte)compression);snapshot.putInt("EntityIndex",e); + 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)); if(priorSnapshot!=null) { String savedChunk=priorSnapshot.getString("OriginalChunk"); - NBTTagCompound oldLevel=priorLevels.get(savedChunk); + CompoundNBT 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)); @@ -231,40 +231,40 @@ private NBTTagCompound scanRegion(Path file,String relative,String hash,NBTTagCo // journal replace changed terrain, inventory or live entities. long key=chunkKey(x,z);Boolean same=matchingChunks.get(key); if(same==null){same=sameNbt(oldLevel,level);matchingChunks.put(key,same);} - if(same)versions.setInt("VerifiedChunkDataVersion",UpgradePreservation.chunkSourceVersion(priorSnapshot.getCompound("SourceVersions"))); + if(same)versions.putInt("VerifiedChunkDataVersion",UpgradePreservation.chunkSourceVersion(priorSnapshot.getCompound("SourceVersions"))); } - snapshot.setTag("SourceVersions",versions); + snapshot.put("SourceVersions",versions); byte[] original=compressed(snapshot);String filename=digest(original)+".nbt";Path destination=snapshotPath(filename); if(Files.exists(destination)) {if(!Arrays.equals(original,Files.readAllBytes(destination))) throw new IOException("Recovery snapshot collision");} else atomic(destination,original,false); - if(entity.hasKey("LootableBodiesUpgradePayload") || entity.hasKey("LootableBodiesInventorySource")) + if(entity.contains("LootableBodiesUpgradePayload") || entity.contains("LootableBodiesInventorySource")) throw new IOException("Unexpected intermediate corpse migration data on disk; original chunk has been preserved"); - preserved.add(new NBTTagString(filename)); + preserved.add(new StringNBT(filename)); } } } catch(RuntimeException damaged) { throw new IOException("Cannot safely preserve "+relative,damaged); } - NBTTagCompound record=new NBTTagCompound();record.setString("Path",relative);record.setString("Digest",hash);record.setTag("Snapshots",preserved);return record; + CompoundNBT record=new CompoundNBT();record.putString("Path",relative);record.putString("Digest",hash);record.put("Snapshots",preserved);return record; } - private NBTTagCompound readWorldVersions() throws IOException { - NBTTagCompound context=new NBTTagCompound(); + private CompoundNBT readWorldVersions() throws IOException { + CompoundNBT context=new CompoundNBT(); Path level=world.resolve("level.dat"); if(Files.isRegularFile(level)) { - NBTTagCompound root=readCompressed(level),data=root.getCompound("Data"); - if(data.hasKey("DataVersion")) context.setTag("WorldDataVersion",data.getTag("DataVersion").copy()); - if(data.hasKey("Version"))context.setTag("WorldVersion",data.getTag("Version").copy()); - if(root.hasKey("FML")) context.setTag("WorldFML",root.getTag("FML").copy()); - else if(data.hasKey("FML")) context.setTag("WorldFML",data.getTag("FML").copy()); + CompoundNBT 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()); + else if(data.contains("FML")) context.put("WorldFML",data.get("FML").copy()); } return context; } - private static NBTTagCompound sourceVersions(NBTTagCompound chunk,NBTTagCompound world,NBTTagCompound entity) { - NBTTagCompound context=world.copy(); - if(chunk.hasKey("DataVersion")) context.setTag("ChunkDataVersion",chunk.getTag("DataVersion").copy()); - if(chunk.hasKey("ForgeDataVersion")) context.setTag("ForgeDataVersion",chunk.getTag("ForgeDataVersion").copy()); + private static CompoundNBT sourceVersions(CompoundNBT chunk,CompoundNBT world,CompoundNBT entity) { + CompoundNBT 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; - if(version<512 || version>1631) version=world.contains("WorldDataVersion",3) ? world.getInt("WorldDataVersion") : -1; + if(version<512 || version>UpgradePreservation.MINECRAFT_VERSION) version=world.contains("WorldDataVersion",3) ? world.getInt("WorldDataVersion") : -1; // Dotted IDs and our own per-corpse markers identify known source families. // A lowercase ID alone cannot distinguish original 1.11 and 1.12 saves. boolean legacy=entity.getString("id").equals("lootablebodies.Corpse"); @@ -272,46 +272,48 @@ private static NBTTagCompound sourceVersions(NBTTagCompound chunk,NBTTagCompound if(marker==1) version=922; else if(marker==2)version=1343; else if(marker==3)version=1631; - if(legacy) {if(version!=512)context.setBoolean("Inferred",true);version=512;} + else if(marker==4)version=1976; + if(legacy) {if(version!=512)context.putBoolean("Inferred",true);version=512;} int family=-1; - NBTTagList mods=world.getCompound("WorldFML").getList("ModList",10); + ListNBT mods=world.getCompound("WorldFML").getList("ModList",10); for(int i=0;i1631) { - version=family;context.setBoolean("Inferred",true); + if(version<512 || version>UpgradePreservation.MINECRAFT_VERSION) { + version=family;context.putBoolean("Inferred",true); } else if(marker==0 && !legacy && family>=512 && ((family<1125 && version>family) || (family<1451 && version>=1451))) { // An original older writer cannot have emitted a newer item format. // Its retained FML metadata disambiguates a falsely current stamp. - version=family;context.setBoolean("Inferred",true); + version=family;context.putBoolean("Inferred",true); } - if(version<512 || version>1631) { + if(version<512 || version>UpgradePreservation.MINECRAFT_VERSION) { // Keep ambiguous records retriable without guessing a bed colour or // discarding known items whose conversion agrees in both families. - context.setBoolean("Ambiguous",true);context.setIntArray("CandidateDataVersions",new int[]{922,1343,1631});version=922; + context.putBoolean("Ambiguous",true);context.putIntArray("CandidateDataVersions",new int[]{922,1343,1631,1976});version=922; } - context.setInt("DataVersion",version);return context; + context.putInt("DataVersion",version);return context; } private static long chunkKey(int x,int z){return ((long)x<<32)|(z&0xffffffffL);} - private static boolean sameNbt(INBTBase a,INBTBase b) { + private static boolean sameNbt(INBT a,INBT b) { if(a==null || b==null)return a==b; if(a.getId()!=b.getId())return false; - if(a instanceof NBTTagDouble)return Double.doubleToRawLongBits(((NBTTagDouble)a).getDouble())==Double.doubleToRawLongBits(((NBTTagDouble)b).getDouble()); - if(a instanceof NBTTagFloat)return Float.floatToRawIntBits(((NBTTagFloat)a).getFloat())==Float.floatToRawIntBits(((NBTTagFloat)b).getFloat()); - if(a instanceof NBTTagCompound) { - NBTTagCompound x=(NBTTagCompound)a,y=(NBTTagCompound)b; + if(a instanceof DoubleNBT)return Double.doubleToRawLongBits(((DoubleNBT)a).getDouble())==Double.doubleToRawLongBits(((DoubleNBT)b).getDouble()); + if(a instanceof FloatNBT)return Float.floatToRawIntBits(((FloatNBT)a).getFloat())==Float.floatToRawIntBits(((FloatNBT)b).getFloat()); + if(a instanceof CompoundNBT) { + CompoundNBT x=(CompoundNBT)a,y=(CompoundNBT)b; if(!x.keySet().equals(y.keySet()))return false; - for(String key:x.keySet())if(!sameNbt(x.getTag(key),y.getTag(key)))return false; + for(String key:x.keySet())if(!sameNbt(x.get(key),y.get(key)))return false; return true; } - if(a instanceof NBTTagList) { - NBTTagList x=(NBTTagList)a,y=(NBTTagList)b; + if(a instanceof ListNBT) { + ListNBT x=(ListNBT)a,y=(ListNBT)b; if(x.getTagType()!=y.getTagType() || x.size()!=y.size())return false; for(int i=0;i0) { int stamped=versions.contains("ChunkDataVersion",3)?versions.getInt("ChunkDataVersion"):-1; - version=stamped>=512 && stamped<=1631?Math.min(stamped,family):family; + version=stamped>=512 && stamped<=MINECRAFT_VERSION?Math.min(stamped,family):family; } return version; } diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 020afc2..bc28d54 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[25,)" +loaderVersion="[28,)" license="${mod_license}" [[mods]] modId="lootablebodies" @@ -11,12 +11,12 @@ description="Saves your items on a lootable corpse when you die." [[dependencies.lootablebodies]] modId="forge" mandatory=true -versionRange="[25.0.223,)" +versionRange="[28.2.26,)" ordering="NONE" side="BOTH" [[dependencies.lootablebodies]] modId="minecraft" mandatory=true -versionRange="[1.13.2]" +versionRange="[1.14.4]" ordering="NONE" side="BOTH" diff --git a/src/nativeSeed113/java/cyano/lootable/buildtest/FixtureItems.java b/src/nativeSeed113/java/cyano/lootable/buildtest/FixtureItems.java new file mode 100644 index 0000000..77724d4 --- /dev/null +++ b/src/nativeSeed113/java/cyano/lootable/buildtest/FixtureItems.java @@ -0,0 +1,34 @@ +package cyano.lootable.buildtest; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.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; + +/** 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,NBTTagCompound nbt){return new Stored();} + }.setRegistryName("lootablebodiesbuildtest:keepsake"); + bus.register(new RegistryItems()); + } + 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 NBTTagCompound data=new NBTTagCompound(); + Stored(){data.setString("owner","Disposable fixture");data.setLong("number",9876543210123L);data.setByteArray("opaque",new byte[]{0,1,-1,17});} + public boolean hasCapability(Capability capability,EnumFacing side){return false;} + public net.minecraftforge.common.util.LazyOptional getCapability(Capability capability,EnumFacing side){return net.minecraftforge.common.util.LazyOptional.empty();} + public NBTTagCompound serializeNBT(){return data.copy();} + public void deserializeNBT(NBTTagCompound nbt){data=nbt.copy();} + } +} diff --git a/src/nativeSeed113/java/cyano/lootable/fixture/NativeWorldSeeder113.java b/src/nativeSeed113/java/cyano/lootable/fixture/NativeWorldSeeder113.java new file mode 100644 index 0000000..04ac5f0 --- /dev/null +++ b/src/nativeSeed113/java/cyano/lootable/fixture/NativeWorldSeeder113.java @@ -0,0 +1,140 @@ +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.EntityArmorStand; +import net.minecraft.entity.item.EntityMinecartChest; +import net.minecraft.entity.passive.EntityPig; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.init.*; +import net.minecraft.inventory.EntityEquipmentSlot; +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.TileEntityChest; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +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 NativeWorldSeeder113 { + private static final UUID PLAYER=UUID.fromString("00000000-2222-4222-8222-000000000001"); + private static boolean oldMinecraft,native112; + public NativeWorldSeeder113(){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();} + } + 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.13.2") || !version.equals("2.4.4.113021")) 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; + NBTTagCompound manifest=new NBTTagCompound();manifest.setString("Minecraft",minecraft);manifest.setString("ModVersion",version); + NBTTagList bodies=new NBTTagList(),containers=new NBTTagList(),vanilla=new NBTTagList(); + for(int dimension:new int[]{0,-1,1}) { + WorldServer world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(dimension)); + for(int wave=0;wave<3;wave++) { + int coordinate=wave==0?128:wave==1?4096:8192; + awaitChunk(world,coordinate>>4,coordinate>>4); + 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); + 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.4.113021")) throw new AssertionError("Recovery fixtures require a checksum-pinned maintained writer"); + NBTTagCompound damaged=new NBTTagCompound();body.writeUnlessPassenger(damaged); + NBTTagList equipment=damaged.getList("Equipment",10); + equipment.getCompound(20).setString("id","lootablebodiesbuildtest:returning_item"); + NBTTagCompound duplicate=equipment.getCompound(21).copy();duplicate.setByte("Count",(byte)9);duplicate.getCompound("tag").setString("extension","duplicate slot");equipment.add(duplicate); + NBTTagCompound invalid=equipment.getCompound(22).copy();invalid.setInt("Slot",999);invalid.setByte("Count",(byte)7);invalid.getCompound("tag").setString("extension","invalid slot");equipment.add(invalid); + damaged.setInt("Name",17);damaged.setDouble("DeathTime",Double.NaN);damaged.setString("Health","opaque health"); + damaged.setString("ActiveEffects","opaque effects");damaged.setString("HurtTime","opaque hurt timer");damaged.setDouble("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.spawnEntity(body))throw new AssertionError("Historical fixture corpse spawn"); + NBTTagCompound record=new NBTTagCompound(),payload=new NBTTagCompound(); + if(!body.writeUnlessPassenger(payload))throw new AssertionError("Historical corpse writer"); + record.setInt("Dimension",dimension);record.setInt("Wave",wave);record.setTag("Body",payload);bodies.add(record); + BlockPos chestPos=new BlockPos(coordinate+2,80,coordinate+2);world.setBlockState(chestPos,Blocks.CHEST.getDefaultState()); + TileEntityChest chest=(TileEntityChest)world.getTileEntity(chestPos);for(int slot=0;slot<16;slot++)chest.setInventorySlotContents(slot,item(slot+6)); + NBTTagCompound chestRecord=new NBTTagCompound(),chestNbt=new NBTTagCompound(); + chest.write(chestNbt); + chestRecord.setInt("Dimension",dimension);chestRecord.setInt("Wave",wave);chestRecord.setTag("Tile",chestNbt);containers.add(chestRecord); + if(wave==0) { + for(int profession=0;profession<6;profession++) { + net.minecraft.entity.passive.EntityVillager villager=new net.minecraft.entity.passive.EntityVillager(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.setProfession(profession); + NBTTagCompound data=new NBTTagCompound();villager.writeUnlessPassenger(data);data.setInt("Career",1);data.setInt("CareerLevel",3); + NBTTagList inventory=new NBTTagList();NBTTagCompound carried=item(7).write(new NBTTagCompound());inventory.add(carried);data.setTag("Inventory",inventory);villager.read(data); + if(!world.spawnEntity(villager))throw new AssertionError("Historical villager spawn"); + NBTTagCompound recordVillager=new NBTTagCompound(),savedVillager=new NBTTagCompound();villager.writeUnlessPassenger(savedVillager); + recordVillager.setInt("Dimension",dimension);recordVillager.setString("ExpectedType","minecraft:villager");recordVillager.setTag("Entity",savedVillager);vanilla.add(recordVillager); + } + EntityPig pig=new EntityPig(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.TextComponentString("Saved pig"));world.spawnEntity(pig); + NBTTagCompound p=new NBTTagCompound();pig.writeUnlessPassenger(p);NBTTagCompound v=new NBTTagCompound();v.setInt("Dimension",dimension);v.setString("ExpectedType","minecraft:pig");v.setTag("Entity",p);vanilla.add(v); + EntityArmorStand stand=new EntityArmorStand(world);stand.setUniqueId(UUID.nameUUIDFromBytes((minecraft+"/stand/"+dimension).getBytes("UTF-8")));stand.setPosition(coordinate+5,82,coordinate+5);stand.setNoGravity(true);stand.setItemStackToSlot(EntityEquipmentSlot.HEAD,item(0));world.spawnEntity(stand); + p=new NBTTagCompound();stand.writeUnlessPassenger(p);v=new NBTTagCompound();v.setInt("Dimension",dimension);v.setString("ExpectedType","minecraft:armor_stand");v.setTag("Entity",p);vanilla.add(v); + EntityMinecartChest cart=new EntityMinecartChest(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.spawnEntity(cart); + p=new NBTTagCompound();cart.writeUnlessPassenger(p);v=new NBTTagCompound();v.setInt("Dimension",dimension);v.setString("ExpectedType","minecraft:chest_minecart");v.setTag("Entity",p);vanilla.add(v); + } + } + } + WorldServer world=server.getWorld(net.minecraft.world.dimension.DimensionType.getById(0)); + EntityPlayerMP player=new EntityPlayerMP(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 EntityEquipmentSlot[]{EntityEquipmentSlot.HEAD,EntityEquipmentSlot.CHEST,EntityEquipmentSlot.LEGS,EntityEquipmentSlot.FEET}[slot],item(slot)); + player.setItemStackToSlot(EntityEquipmentSlot.OFFHAND,item(5));for(int slot=0;slot<27;slot++)player.getInventoryEnderChest().setInventorySlotContents(slot,item(slot+6)); + world.getSaveHandler().getPlayerNBTManager().writePlayerData(player); + manifest.setUniqueId("Player",PLAYER);manifest.setTag("Bodies",bodies);manifest.setTag("Containers",containers);manifest.setTag("Vanilla",vanilla); + server.saveAllWorlds(false);net.minecraft.world.storage.ThreadedFileIOBase.getThreadedIOInstance().waitForFinish(); + File folder=world.getSaveHandler().getWorldDirectory();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 boolean minecraftLongArrays(){return true;} + private static void awaitChunk(WorldServer world,int x,int z)throws Exception { + net.minecraft.world.chunk.Chunk chunk=world.getChunk(x,z); + for(int attempt=0;!chunk.isLoaded();attempt++) { + if(attempt>10000)throw new AssertionError("Historical native chunk admission timed out"); + java.util.concurrent.FutureTask task=world.getServer().futureTaskQueue.poll(); + if(task==null)throw new AssertionError("Missing historical admission task");task.run();task.get(); + } + } + 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.SIGN,11); + else stack=new ItemStack(Items.DIAMOND,slot%32+1); + NBTTagCompound tag=new NBTTagCompound();tag.setString("fixture_slot","original-"+slot);tag.setLong("typed_long",1234567890123L);tag.setByteArray("typed_bytes",new byte[]{0,-1,17}); + if(slot==6){NBTTagCompound pig=new NBTTagCompound();pig.setString("id",oldMinecraft?"Pig":"minecraft:pig");pig.setString("CustomName","Saved pig egg");tag.setTag("EntityTag",pig);} + if(slot==7){NBTTagCompound tile=new NBTTagCompound();NBTTagList items=new NBTTagList();NBTTagCompound nested=new ItemStack(Items.EMERALD,17).write(new NBTTagCompound());nested.setByte("Slot",(byte)3);items.add(nested);tile.setTag("Items",items);tag.setTag("BlockEntityTag",tile);} + if(slot==8 && oldMinecraft){NBTTagCompound banner=new NBTTagCompound();banner.setInt("Base",5);tag.setTag("BlockEntityTag",banner);} + if(minecraftLongArrays()) { + tag.setTag("typed_longs",new NBTTagLongArray(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.TextComponentString("Original keepsake "+slot));if(slot==4)stack.addEnchantment(Enchantments.UNBREAKING,2);return stack; + } +} diff --git a/src/nativeSeed113/resources/META-INF/mods.toml b/src/nativeSeed113/resources/META-INF/mods.toml new file mode 100644 index 0000000..90e771e --- /dev/null +++ b/src/nativeSeed113/resources/META-INF/mods.toml @@ -0,0 +1,13 @@ +modLoader="javafml" +loaderVersion="[25,)" +license="LGPL-2.1-only" +[[mods]] +modId="lootablebodiesbuildtest" +version="1" +displayName="Disposable historical world writer" +[[dependencies.lootablebodiesbuildtest]] +modId="lootablebodies" +mandatory=true +versionRange="[2.4.4.113021]" +ordering="AFTER" +side="BOTH" diff --git a/src/nativeSeed113/resources/pack.mcmeta b/src/nativeSeed113/resources/pack.mcmeta new file mode 100644 index 0000000..b2e875c --- /dev/null +++ b/src/nativeSeed113/resources/pack.mcmeta @@ -0,0 +1 @@ +{"pack":{"pack_format":4,"description":"Disposable historical world writer"}} diff --git a/src/test/java/cyano/lootable/contracts/BuildContractTest.java b/src/test/java/cyano/lootable/contracts/BuildContractTest.java index c35cc0f..923e24e 100644 --- a/src/test/java/cyano/lootable/contracts/BuildContractTest.java +++ b/src/test/java/cyano/lootable/contracts/BuildContractTest.java @@ -50,7 +50,7 @@ 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.13.2]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); + assertTrue(info.contains("versionRange=\"[1.14.4]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); assertFalse(Files.exists(Paths.get("build/resources/main/mcmod.info"))); assertEquals(4,new com.google.gson.JsonParser().parse(text("build/resources/main/pack.mcmeta")).getAsJsonObject().getAsJsonObject("pack").get("pack_format").getAsInt()); } @@ -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("EntityEquipmentSlot.HEAD, EntityEquipmentSlot.CHEST, EntityEquipmentSlot.LEGS, EntityEquipmentSlot.FEET, EntityEquipmentSlot.MAINHAND, EntityEquipmentSlot.OFFHAND")); + assertTrue(source.contains("EquipmentSlotType.HEAD, EquipmentSlotType.CHEST, EquipmentSlotType.LEGS, EquipmentSlotType.FEET, EquipmentSlotType.MAINHAND, EquipmentSlotType.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 bab3795..e68b2c8 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.GuiScreen.class,String.class,Map.class,int.class);ctor.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); @SuppressWarnings("unchecked") Map original=(Map)pending.get(root); Object child=ctor.newInstance(root,"rules",original,0); @SuppressWarnings("unchecked") Map edits=(Map)pending.get(child); diff --git a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java index 99dd72e..72c2a50 100644 --- a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java +++ b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java @@ -19,98 +19,98 @@ class NativeUpgradeBoundaryTest { private static final UUID ID=UUID.fromString("00000000-3333-4333-8333-000000000001"); private static final DataFixer VANILLA=DataFixesManager.getDataFixer(); @AfterEach void close(){UpgradePreservation.close();} - private NBTTagCompound item(String id,int count,int damage,int slot) { - NBTTagCompound item=new NBTTagCompound();item.setString("id",id);item.setByte("Count",(byte)count);item.setShort("Damage",(short)damage);if(slot>=0)item.setByte("Slot",(byte)slot); - NBTTagCompound tag=new NBTTagCompound();tag.setString("keepsake","slot-"+slot);item.setTag("tag",tag);return item; - } - private NBTTagCompound corpse() { - NBTTagCompound body=new NBTTagCompound();body.setString("id","lootablebodies.Corpse");body.setUniqueId("UUID",ID);body.setString("Name","Saved owner");body.setString("CustomName","Saved owner");body.setLong("DeathTime",700); - NBTTagList equipment=new NBTTagList();for(int i=0;i<48;i++)equipment.add(item("minecraft:stone",i+1,1,i));body.setTag("Equipment",equipment); - NBTTagList aux=new NBTTagList();aux.add(item("minecraft:gold_ingot",17,0,-1));body.setTag("Aux",aux);body.setDouble("FallDistance",Double.NaN);return body; - } - private NBTTagCompound chunk(NBTTagCompound entity,boolean populated) { - NBTTagCompound root=new NBTTagCompound(),level=new NBTTagCompound();root.setInt("DataVersion",512);root.setTag("Level",level);level.setInt("xPos",0);level.setInt("zPos",0);level.setBoolean("TerrainPopulated",populated);level.setBoolean("LightPopulated",populated);level.setByteArray("Biomes",new byte[256]);level.setIntArray("HeightMap",new int[256]); - NBTTagList entities=new NBTTagList();if(entity!=null)entities.add(entity.copy());level.setTag("Entities",entities); - NBTTagCompound section=new NBTTagCompound();section.setByte("Y",(byte)0);byte[] blocks=new byte[4096];blocks[0]=54;section.setByteArray("Blocks",blocks);section.setByteArray("Data",new byte[2048]);section.setByteArray("BlockLight",new byte[2048]);section.setByteArray("SkyLight",new byte[2048]);NBTTagList sections=new NBTTagList();sections.add(section);level.setTag("Sections",sections); - NBTTagCompound chest=new NBTTagCompound();chest.setString("id","Chest");chest.setInt("x",0);chest.setInt("y",0);chest.setInt("z",0);NBTTagList contents=new NBTTagList();contents.add(item("minecraft:stone",23,1,0));chest.setTag("Items",contents);NBTTagList tiles=new NBTTagList();tiles.add(chest);level.setTag("TileEntities",tiles);return root; - } - private byte[] encoded(NBTTagCompound nbt)throws Exception{ByteArrayOutputStream bytes=new ByteArrayOutputStream();CompressedStreamTools.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} - private void preserve(NBTTagCompound nbt)throws Exception { + 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);} 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 NBTTagCompound update(NBTTagCompound chunk)throws Exception { + private CompoundNBT update(CompoundNBT chunk)throws Exception { preserve(chunk);DataFixer safe=UpgradePreservation.wrapper(VANILLA); - NBTTagCompound first=NBTUtil.update(safe,TypeReferences.CHUNK,chunk.copy(),chunk.getInt("DataVersion"),1493); - return NBTUtil.update(safe,TypeReferences.CHUNK,first,1493,1631); + 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); } - private NBTTagCompound findCorpse(NBTTagCompound root) { - NBTTagCompound found=find(root);assertNotNull(found,"The original corpse must remain present");return found; + private CompoundNBT findCorpse(CompoundNBT root) { + CompoundNBT found=find(root);assertNotNull(found,"The original corpse must remain present");return found; } - private NBTTagCompound find(INBTBase value) { - if(value instanceof NBTTagCompound){NBTTagCompound nbt=(NBTTagCompound)value;if(nbt.getString("id").equals("lootablebodies:corpse") || nbt.getString("id").equals("lootablebodies.Corpse"))return nbt;for(String key:nbt.keySet()){NBTTagCompound found=find(nbt.getTag(key));if(found!=null)return found;}} - if(value instanceof NBTTagList){for(INBTBase entry:(NBTTagList)value){NBTTagCompound found=find(entry);if(found!=null)return found;}}return null; + 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; } @Test void legacyIdentityAndEveryRawInventoryEntrySurviveBothAnvilPasses()throws Exception { - NBTTagCompound body=corpse(),source=chunk(body,true);byte[] before=encoded(source);NBTTagCompound result=update(source),saved=findCorpse(result); - assertEquals("lootablebodies:corpse",saved.getString("id"));assertEquals(3,saved.getInt("LootableBodiesDataVersion"));assertEquals(ID,saved.getUniqueId("UUID")); - NBTTagList records=saved.getList("LootableBodiesUnresolved",10);assertEquals(49,records.size()); - for(int i=0;i<48;i++){NBTTagCompound 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.hasKey("LootableBodiesUpgradePayload"));assertFalse(saved.hasKey("LootableBodiesInventorySource")); + 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(4,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"));} + assertEquals("Aux",records.getCompound(48).getString("Source"));assertArrayEquals(before,encoded(source));assertFalse(saved.contains("LootableBodiesUpgradePayload"));assertFalse(saved.contains("LootableBodiesInventorySource")); } @Test void occupiedPartlyGeneratedChunksKeepTerrainAndVanillaContainers()throws Exception { - NBTTagCompound result=update(chunk(corpse(),false)),level=result.getCompound("Level"); + CompoundNBT 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)); - NBTTagList tiles=level.getList("TileEntities",10);assertEquals(1,tiles.size());NBTTagCompound 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")); + 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")); } @Test void vanillaContainersAreProtectedEvenWithoutACorpseInTheirChunk()throws Exception { - NBTTagCompound result=update(chunk(null,false));assertEquals(1,result.getCompound("Level").getList("Sections",10).size());assertEquals(1,result.getCompound("Level").getList("TileEntities",10).size()); + CompoundNBT 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 { - NBTTagCompound source=chunk(null,false);source.getCompound("Level").setTag("TileEntities",new NBTTagList()); - NBTTagCompound level=update(source).getCompound("Level"); + CompoundNBT source=chunk(null,false);source.getCompound("Level").put("TileEntities",new ListNBT()); + CompoundNBT 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 { - NBTTagCompound result=update(chunk(corpse(),true));DataFixer safe=UpgradePreservation.wrapper(VANILLA);NBTTagCompound again=NBTUtil.update(safe,TypeReferences.CHUNK,result.copy(),1631,1631); + 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); assertEquals(49,findCorpse(again).getList("LootableBodiesUnresolved",10).size()); - again.getCompound("Level").setTag("Entities",new NBTTagList());assertNull(find(NBTUtil.update(safe,TypeReferences.CHUNK,again,1631,1631))); + again.getCompound("Level").put("Entities",new ListNBT());assertNull(find(cyano.lootable.upgrade.NativeNbt.update(safe,TypeReferences.CHUNK,again,1976,1976))); } @Test void vanillaMinecartParentsAndNestedCorpsesBothUpgrade()throws Exception { - NBTTagCompound cart=new NBTTagCompound();cart.setString("id","MinecartRideable");cart.setUniqueId("UUID",UUID.fromString("00000000-3333-4333-8333-000000000099"));NBTTagList riders=new NBTTagList();riders.add(corpse());cart.setTag("Passengers",riders); - NBTTagCompound 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()); + 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()); } @Test void corpseEggsKeepTheirTypeCountAndEntityPayload()throws Exception { for(String id:new String[]{"lootablebodies.Corpse","lootablebodies:corpse"}){ - NBTTagCompound egg=item("minecraft:spawn_egg",7,0,4),entity=new NBTTagCompound();entity.setString("id",id);entity.setString("Name","Egg owner");egg.getCompound("tag").setTag("EntityTag",entity);byte[] before=encoded(egg); - NBTTagCompound fixed=NBTUtil.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.ITEM_STACK,egg.copy(),512,1631); + 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); 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 { - NBTTagCompound egg=item("minecraft:spawn_egg",11,0,0),entity=new NBTTagCompound();entity.setString("id","lootablebodies.Corpse");egg.getCompound("tag").setTag("EntityTag",entity);NBTTagList items=new NBTTagList();items.add(egg);NBTTagCompound player=new NBTTagCompound();player.setTag("Inventory",items); - NBTTagCompound result=NBTUtil.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.PLAYER,player.copy(),512,1631);NBTTagCompound fixed=result.getList("Inventory",10).getCompound(0);assertEquals("lootablebodies:corpse_spawn_egg",fixed.getString("id"));assertEquals(11,fixed.getByte("Count")); + 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")); } @Test void aFalseCurrentMarkerCannotBypassLegacyIdentityConversion()throws Exception { - NBTTagCompound body=corpse();body.setInt("LootableBodiesDataVersion",3); - NBTTagCompound result=update(chunk(body,true)); + CompoundNBT body=corpse();body.putInt("LootableBodiesDataVersion",3); + CompoundNBT result=update(chunk(body,true)); assertEquals(49,findCorpse(result).getList("LootableBodiesUnresolved",10).size()); assertEquals(512,findCorpse(result).getCompound("LootableBodiesSourceVersions").getInt("DataVersion")); } @Test void futureCorpseFormatsAreNeverOverwritten() { - NBTTagCompound body=corpse();body.setString("id","lootablebodies:corpse");body.setInt("LootableBodiesDataVersion",99); - assertThrows(IllegalStateException.class,()->NBTUtil.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.ENTITY_TYPE,body,1343,1631)); + 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)); } @Test void chunkConversionFailuresCannotBeMistakenForAbsentChunks() { UpgradePreservation.close(); - assertThrows(Error.class,()->NBTUtil.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.CHUNK,chunk(corpse(),true),512,1631)); + assertThrows(Error.class,()->cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),TypeReferences.CHUNK,chunk(corpse(),true),512,1976)); } @Test void aPlausibleFalseStampCannotSkipVanillaBannerConversion()throws Exception { - NBTTagCompound source=chunk(corpse(),true);source.setInt("DataVersion",922); - NBTTagCompound banner=item("minecraft:banner",2,0,0),block=new NBTTagCompound();block.setInt("Base",5);banner.getCompound("tag").setTag("BlockEntityTag",block); - NBTTagList items=new NBTTagList();items.add(banner);source.getCompound("Level").getList("TileEntities",10).getCompound(0).setTag("Items",items); - NBTTagCompound converted=update(source).getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); + 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); 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 c9c64bc..caf4669 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.4.113021", version); + assertEquals("2.4.5.114041", 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.13.2]\"")); + assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.14.4]\"")); } @Test void maintainedReleaseHasLgplAndRetainsTheOriginalNotice() throws Exception { diff --git a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java index d67a678..b79519a 100644 --- a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java +++ b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java @@ -19,103 +19,103 @@ 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 NBTTagCompound original(Object journal,UUID id,int x,int z) throws Exception { - return (NBTTagCompound)journal.getClass().getMethod("originalFor",UUID.class,int.class,int.class).invoke(journal,id,x,z); - } - private NBTTagCompound body(UUID id) { - NBTTagCompound body=new NBTTagCompound();body.setString("id","lootablebodies.Corpse");body.setUniqueId("UUID",id); - body.setDouble("FallDistance",Double.NaN);body.setString("Name","Saved owner");body.setLong("DeathTime",700); - NBTTagList equipment=new NBTTagList(); - for(int i=0;i<48;i++) {NBTTagCompound item=new NBTTagCompound();item.setString("id","minecraft:diamond");item.setByte("Count",(byte)(i+1));item.setShort("Damage",(short)i);item.setByte("Slot",(byte)i);NBTTagCompound custom=new NBTTagCompound();custom.setString("keepsake","slot-"+i);item.setTag("tag",custom);equipment.add(item);} - body.setTag("Equipment",equipment); - NBTTagCompound record=new NBTTagCompound();record.setString("Source","Equipment");record.setTag("Data",equipment.getCompound(47).copy()); - NBTTagList recovery=new NBTTagList();recovery.add(record);body.setTag("LootableBodiesUnresolved",recovery); + 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 CompoundNBT body(UUID id) { + CompoundNBT body=new CompoundNBT();body.putString("id","lootablebodies.Corpse");body.putUniqueId("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);} + 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); return body; } - private Path region(String dimension,NBTTagCompound... bodies) throws Exception { + private Path region(String dimension,CompoundNBT... bodies) throws Exception { return regionVersion(dimension,512,bodies); } - private Path regionVersion(String dimension,int version,NBTTagCompound... bodies) throws Exception { - NBTTagCompound chunk=new NBTTagCompound(),level=new NBTTagCompound();if(version>=0)chunk.setInt("DataVersion",version);chunk.setTag("Level",level);level.setInt("xPos",0);level.setInt("zPos",0); - NBTTagList entities=new NBTTagList();for(NBTTagCompound body:bodies)entities.add(body.copy());level.setTag("Entities",entities); + 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); ByteArrayOutputStream bytes=new ByteArrayOutputStream(); try(DataOutputStream compressed=new DataOutputStream(new DeflaterOutputStream(bytes))){CompressedStreamTools.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(NBTTagCompound nbt) throws Exception {ByteArrayOutputStream bytes=new ByteArrayOutputStream();CompressedStreamTools.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} + private byte[] encoded(CompoundNBT nbt) throws Exception {ByteArrayOutputStream bytes=new ByteArrayOutputStream();CompressedStreamTools.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} @Test void native112VersionIsNotDowngradedTo111() throws Exception { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");regionVersion("",1343,b); + CompoundNBT b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1343,b); assertEquals(1343,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); } @Test void native113MarkerAndStampSurvivePreservation() throws Exception { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");b.setInt("LootableBodiesDataVersion",3);regionVersion("",1631,b); + CompoundNBT 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 { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");b.setInt("LootableBodiesDataVersion",2);regionVersion("",1631,b); + CompoundNBT 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 { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");b.setInt("LootableBodiesDataVersion",3);regionVersion("",-1,b); - NBTTagCompound versions=original(scan(),ID,0,0).getCompound("SourceVersions"); + 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"); assertEquals(1631,versions.getInt("DataVersion"));assertFalse(versions.getBoolean("Ambiguous")); } @Test void actual112WriterMetadataRejectsAFalselyFlattenedStamp() throws Exception { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");regionVersion("",1631,b); - NBTTagCompound root=new NBTTagCompound(),fml=new NBTTagCompound(),mod=new NBTTagCompound();mod.setString("ModId","lootablebodies");mod.setString("ModVersion","2.4.0");NBTTagList mods=new NBTTagList();mods.add(mod);fml.setTag("ModList",mods);root.setTag("FML",fml);root.setTag("Data",new NBTTagCompound()); + 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);} int source=original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion");assertTrue(source>=1139 && source<=1343); } @Test void corpsesSavedAsVanillaVehiclePassengersArePreserved() throws Exception { - NBTTagCompound cart=new NBTTagCompound();cart.setString("id","minecraft:minecart");cart.setUniqueId("UUID",UUID.fromString("00000000-1111-4111-8111-000000000099")); - NBTTagList riders=new NBTTagList();riders.add(body(ID));cart.setTag("Passengers",riders);region("",cart); - NBTTagCompound saved=original(scan(),ID,0,0);assertNotNull(saved);assertArrayEquals(encoded(body(ID)),encoded(saved.getCompound("Entity"))); + 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"))); region("",cart.copy());assertNotNull(original(scan(),ID,0,0)); - cart.removeTag("Passengers");region("",cart);assertNull(original(scan(),ID,0,0)); + cart.remove("Passengers");region("",cart);assertNull(original(scan(),ID,0,0)); } @Test void current112MarkerCanBePreservedAndReloaded() throws Exception { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");b.setInt("LootableBodiesDataVersion",2);regionVersion("",1343,b); + CompoundNBT 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 { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");regionVersion("",-1,b); + CompoundNBT 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 { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");b.setInt("LootableBodiesDataVersion",1);regionVersion("",1343,b); + CompoundNBT 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 { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");regionVersion("",-1,b); - NBTTagCompound root=new NBTTagCompound(),fml=new NBTTagCompound(),mod=new NBTTagCompound();mod.setString("ModId","lootablebodies");mod.setString("ModVersion","2.4.0");NBTTagList mods=new NBTTagList();mods.add(mod);fml.setTag("ModList",mods);root.setTag("FML",fml);root.setTag("Data",new NBTTagCompound()); + 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);} - NBTTagCompound context=original(scan(),ID,0,0).getCompound("SourceVersions");assertTrue(context.getInt("DataVersion")>=1139);assertFalse(context.getBoolean("Ambiguous"));assertEquals(fml,context.getCompound("WorldFML")); + CompoundNBT 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 { - NBTTagCompound b=body(ID);b.setString("id","lootablebodies:corpse");regionVersion("",1343,b); - NBTTagCompound root=new NBTTagCompound(),fml=new NBTTagCompound(),mod=new NBTTagCompound();mod.setString("ModId","lootablebodies");mod.setString("ModVersion","2.3.0");NBTTagList mods=new NBTTagList();mods.add(mod);fml.setTag("ModList",mods);root.setTag("FML",fml);root.setTag("Data",new NBTTagCompound()); + 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);} assertEquals(922,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); } @Test void stampOnlyChangesRetainPreviouslyVerifiedChunkVersion() throws Exception { - NBTTagCompound b=body(ID);regionVersion("",512,b);scan(); + CompoundNBT b=body(ID);regionVersion("",512,b);scan(); regionVersion("",-1,b); - NBTTagCompound versions=original(scan(),ID,0,0).getCompound("SourceVersions"); + CompoundNBT versions=original(scan(),ID,0,0).getCompound("SourceVersions"); assertEquals(512,versions.getInt("VerifiedChunkDataVersion")); } @Test void changedChunkPayloadCannotReusePreviousVersionEvidence() throws Exception { - NBTTagCompound b=body(ID);regionVersion("",512,b);scan(); - NBTTagCompound added=new NBTTagCompound();added.setString("id","minecraft:pig"); + CompoundNBT b=body(ID);regionVersion("",512,b);scan(); + CompoundNBT added=new CompoundNBT();added.putString("id","minecraft:pig"); regionVersion("",-1,b,added); - assertFalse(original(scan(),ID,0,0).getCompound("SourceVersions").hasKey("VerifiedChunkDataVersion")); + 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 { - NBTTagCompound body=body(ID);Path region=region("",body);byte[] before=Files.readAllBytes(region);java.nio.file.attribute.FileTime stamp=Files.getLastModifiedTime(region); - Object journal=scan();NBTTagCompound saved=original(journal,ID,0,0); + 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); 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)); @@ -126,8 +126,8 @@ private Path regionVersion(String dimension,int version,NBTTagCompound... bodies 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 { - NBTTagCompound 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).setByte("Count",(byte)99);region("",body);scan();Files.write(index,stale); + CompoundNBT 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"))); } @Test void interruptedTemporaryWriteIsNeverTrusted() throws Exception { @@ -138,9 +138,9 @@ private Path regionVersion(String dimension,int version,NBTTagCompound... bodies @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"); - NBTTagCompound index=CompressedStreamTools.readCompressed(Files.newInputStream(path)); - if(index.contains("Payload",7)) {byte[] payload=index.getByteArray("Payload");payload[payload.length/2]^=1;index.setByteArray("Payload",payload);} - else index.getList("Regions",10).getCompound(0).setTag("Snapshots",new NBTTagList()); + CompoundNBT index=CompressedStreamTools.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);} assertThrows(IOException.class,()->scan(),"A readable damaged index must not be accepted as a complete scan"); } @@ -150,19 +150,19 @@ private Path regionVersion(String dimension,int version,NBTTagCompound... bodies @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);NBTTagCompound snapshot=original(scan(),ID,0,0); + byte[] originalChunk=Arrays.copyOfRange(source,8197,8192+4+size);CompoundNBT 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 { - NBTTagCompound body=body(ID),fake=body(ID);fake.setInt("LootableBodiesDataVersion",1);fake.setTag("Equipment",new NBTTagList());body.setTag("LootableBodiesUpgradePayload",fake); + CompoundNBT body=body(ID),fake=body(ID);fake.putInt("LootableBodiesDataVersion",1);fake.put("Equipment",new ListNBT());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 { - NBTTagCompound corpse=body(ID);corpse.setString("id","lootablebodies:corpse");corpse.setInt("LootableBodiesDataVersion",99);Path file=region("",corpse);byte[] before=Files.readAllBytes(file); + CompoundNBT 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 c708673..d624a0d 100644 --- a/src/test/java/cyano/lootable/network/LoginReadGuardTest.java +++ b/src/test/java/cyano/lootable/network/LoginReadGuardTest.java @@ -8,7 +8,7 @@ import java.io.*; import java.net.*; import java.util.concurrent.*; -import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.ProtocolType; import net.minecraft.network.NetworkManager; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -44,7 +44,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(EnumConnectionState.LOGIN); + channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).set(ProtocolType.LOGIN); drain(); } void drain()throws Exception {channel.eventLoop().submit(()->{}).get(5,TimeUnit.SECONDS);} @@ -85,7 +85,7 @@ public void close()throws Exception { } } @Test void otherProtocolStatesAreNotChanged()throws Exception { - for(EnumConnectionState state:new EnumConnectionState[]{EnumConnectionState.HANDSHAKING,EnumConnectionState.STATUS,EnumConnectionState.PLAY}) { + for(ProtocolType state:new ProtocolType[]{ProtocolType.HANDSHAKING,ProtocolType.STATUS,ProtocolType.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()); @@ -117,9 +117,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(EnumConnectionState.PLAY);} + try {assertTrue(entered.await(5,TimeUnit.SECONDS));guard(c.channel);c.channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).set(ProtocolType.PLAY);} finally {resume.countDown();} - c.drain();assertEquals(0,c.channel.readInterest());assertEquals(EnumConnectionState.PLAY,c.channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).get()); + c.drain();assertEquals(0,c.channel.readInterest());assertEquals(ProtocolType.PLAY,c.channel.attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).get()); } } } diff --git a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java index 0ebbf35..ed45ce1 100644 --- a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java +++ b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java @@ -9,58 +9,58 @@ /** Actual native item-fixer contracts; registry resolution is tested in Forge. */ class ItemNbtConversionTest { @BeforeEach void pipeline(){ItemMigration.register(UpgradePreservation.wrapper(DataFixesManager.getDataFixer()));} - private NBTTagCompound context(int version){NBTTagCompound nbt=new NBTTagCompound();nbt.setInt("DataVersion",version);return nbt;} - private NBTTagCompound item(String id,int damage){NBTTagCompound nbt=new NBTTagCompound();nbt.setString("id",id);nbt.setByte("Count",(byte)7);nbt.setShort("Damage",(short)damage);nbt.setByte("Slot",(byte)47);NBTTagCompound custom=new NBTTagCompound();custom.setString("keepsake","saved");nbt.setTag("tag",custom);return nbt;} - private byte[] bytes(NBTTagCompound nbt)throws Exception{ByteArrayOutputStream bytes=new ByteArrayOutputStream();CompressedStreamTools.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} + 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();} @Test void blockVariantsFlattenWithoutLosingTheirCountSlotOrCustomTag()throws Exception { - NBTTagCompound raw=item("minecraft:stone",1);byte[] before=bytes(raw);NBTTagCompound 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)); + 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)); } @Test void durabilityAndEnchantmentsUseTheirNativeFields() { - NBTTagCompound raw=item("minecraft:diamond_sword",123);NBTTagCompound enchantment=new NBTTagCompound();enchantment.setShort("id",(short)16);enchantment.setShort("lvl",(short)3);NBTTagList enchantments=new NBTTagList();enchantments.add(enchantment);raw.getCompound("tag").setTag("ench",enchantments); - NBTTagCompound result=ItemMigration.convertNbt(raw,context(1343));assertNotNull(result);assertEquals(123,result.getCompound("tag").getInt("Damage"));NBTTagCompound sharpness=result.getCompound("tag").getList("Enchantments",10).getCompound(0);assertEquals("minecraft:sharpness",sharpness.getString("id"));assertEquals(3,sharpness.getShort("lvl")); + 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")); } @Test void nestedInventoriesAndCapabilitiesRemainComplete()throws Exception { - NBTTagCompound raw=item("minecraft:blue_shulker_box",0),nested=item("minecraft:stone",1);nested.setByte("Count",(byte)29);nested.setByte("Slot",(byte)3);NBTTagList contents=new NBTTagList();contents.add(nested);NBTTagCompound block=new NBTTagCompound();block.setString("id","minecraft:shulker_box");block.setTag("Items",contents);raw.getCompound("tag").setTag("BlockEntityTag",block);NBTTagCompound caps=new NBTTagCompound();caps.setByteArray("retained",new byte[]{1,4,9});raw.setTag("ForgeCaps",caps); - NBTTagCompound result=ItemMigration.convertNbt(raw,context(922));assertNotNull(result);assertArrayEquals(bytes(caps),bytes(result.getCompound("ForgeCaps")));NBTTagCompound 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")); + 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")); } @Test void currentNativeItemsAreNotFlattenedAgain()throws Exception { - NBTTagCompound raw=item("minecraft:granite",0);raw.removeTag("Damage");assertArrayEquals(bytes(raw),bytes(ItemMigration.convertNbt(raw,context(1631)))); + CompoundNBT raw=item("minecraft:granite",0);raw.remove("Damage");assertArrayEquals(bytes(raw),bytes(ItemMigration.convertNbt(raw,context(UpgradePreservation.MINECRAFT_VERSION)))); } @Test void bedColoursStillUseTheCorrectHistoricalContext() { - NBTTagCompound 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")); + 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")); } @Test void ambiguousConversionsRemainRawAndRetriable() { - NBTTagCompound versions=context(922);versions.setBoolean("Ambiguous",true);versions.setIntArray("CandidateDataVersions",new int[]{922,1343});assertNull(ItemMigration.convertNbt(item("minecraft:bed",0),versions));assertNotNull(ItemMigration.convertNbt(item("minecraft:stone",1),versions)); + 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)); } @Test void malformedCountsAndUnknownFutureVersionsAreNotGuessed() { - NBTTagCompound raw=item("minecraft:stone",1);raw.setString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(2000))); + CompoundNBT raw=item("minecraft:stone",1);raw.putString("Count","bad");assertNull(ItemMigration.convertNbt(raw,context(512)));assertNull(ItemMigration.convertNbt(item("minecraft:stone",1),context(2000))); } @Test void unrecognisedMetadataAndEnchantmentsRemainRecoverable() { assertNull(ItemMigration.convertNbt(item("minecraft:stone",99),context(1343))); assertNull(ItemMigration.convertNbt(item("missing:variant",4),context(1343))); - NBTTagCompound raw=item("minecraft:diamond_sword",0),enchantment=new NBTTagCompound(); - enchantment.setShort("id",(short)200);enchantment.setShort("lvl",(short)5); - NBTTagList list=new NBTTagList();list.add(enchantment);raw.getCompound("tag").setTag("ench",list); + CompoundNBT raw=item("minecraft:diamond_sword",0),enchantment=new CompoundNBT(); + enchantment.putShort("id",(short)200);enchantment.putShort("lvl",(short)5); + ListNBT list=new ListNBT();list.add(enchantment);raw.getCompound("tag").put("ench",list); assertNull(ItemMigration.convertNbt(raw,context(1343))); } - private NBTTagCompound original110(int slot)throws Exception { + private CompoundNBT original110(int slot)throws Exception { try(InputStream in=getClass().getResourceAsStream("/legacy-items/original-110.nbt")) { - NBTTagList list=CompressedStreamTools.readCompressed(in).getList("Items",10); - for(INBTBase value:list){NBTTagCompound item=(NBTTagCompound)value;if(item.getByte("Slot")==slot)return item;} + ListNBT list=CompressedStreamTools.readCompressed(in).getList("Items",10); + for(INBT value:list){CompoundNBT item=(CompoundNBT)value;if(item.getByte("Slot")==slot)return item;} }throw new AssertionError("Missing sealed historical item"); } @Test void actualOriginalPigEggKeepsItsTypeAndSavedEntityName()throws Exception { - NBTTagCompound raw=original110(6);byte[] before=bytes(raw);NBTTagCompound fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); + CompoundNBT raw=original110(6);byte[] before=bytes(raw);CompoundNBT 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.fromJson(fixed.getCompound("tag").getCompound("EntityTag").getString("CustomName")).getString());assertArrayEquals(before,bytes(raw)); } @Test void actualOriginalChestKeepsItsNestedInventoryInNativeFormat()throws Exception { - NBTTagCompound raw=original110(7),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); - NBTTagCompound 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.hasKey("Damage")); + 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.fromJson(fixed.getCompound("tag").getCompound("display").getString("Name")).getString()); } @Test void actualOriginalBannerKeepsItsBaseColour()throws Exception { - NBTTagCompound 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. + 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. 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 new file mode 100644 index 0000000..a3a155f --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java @@ -0,0 +1,30 @@ +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 org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class Native114RegistryRenameTest { + @Test void itemRegistryBridgesAgreeWithVanillasActualItemConversion() { + for (String[] names : new String[][]{{"sign","oak_sign"},{"rose_red","red_dye"}, + {"cactus_green","green_dye"},{"dandelion_yellow","yellow_dye"}}) { + ResourceLocation old = new ResourceLocation("minecraft", names[0]); + ResourceLocation current = new ResourceLocation("minecraft", names[1]); + 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); + assertEquals(current.toString(), converted.getString("id")); assertEquals(11, converted.getByte("Count")); + } + } + @Test void onlyTheTwoRetiredVanillaSignBlocksAreBridged() { + assertEquals(new ResourceLocation("minecraft:oak_sign"), LegacyRegistryMappings.blockReplacement(new ResourceLocation("minecraft:sign"))); + assertEquals(new ResourceLocation("minecraft:oak_wall_sign"), LegacyRegistryMappings.blockReplacement(new ResourceLocation("minecraft:wall_sign"))); + for (String name : new String[]{"othermod:sign", "minecraft:unknown", "minecraft:oak_sign"}) { + assertNull(LegacyRegistryMappings.itemReplacement(new ResourceLocation(name))); + assertNull(LegacyRegistryMappings.blockReplacement(new ResourceLocation(name))); + } + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java new file mode 100644 index 0000000..147308d --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java @@ -0,0 +1,71 @@ +package cyano.lootable.upgrade; + +import java.util.UUID; +import net.minecraft.nbt.*; +import net.minecraft.util.datafix.DataFixesManager; +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 CompoundNBT item(int slot) { + CompoundNBT item = new CompoundNBT(); 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}); + 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")); + 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); + return body; + } + @Test void currentFormatNamesThe114VanillaSchema() { + assertEquals(1976, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(4, UpgradePreservation.DATA_VERSION); + } + @Test void released113EquipmentAndOverflowAreRetainedBeforeNativeConversion() { + CompoundNBT original = body(3), before = original.copy(); + CompoundNBT staged = NativeSafeFixer.stageCorpse(original, context(1631)); + assertEquals(4, staged.getInt(UpgradePreservation.VERSION)); + assertEquals(original.getUniqueId("UUID"), staged.getUniqueId("UUID")); + assertTrue(staged.getList("Equipment", 10).isEmpty()); + ListNBT records = staged.getList("LootableBodiesUnresolved", 10); assertEquals(49, records.size()); + for (int i=0; i<48; i++) { + CompoundNBT 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")); + } + assertEquals("Aux", records.getCompound(48).getString("Source")); assertEquals(before, original); + } + @Test void olderRecoveryPayloadsKeepTheirOriginalSourceAndData() { + CompoundNBT original = body(3), record = new CompoundNBT(); + record.putString("Source", "Equipment"); record.put("Data", item(47)); + record.put("SourceVersions", context(922)); ListNBT recovery = new ListNBT(); recovery.add(record.copy()); + original.put("LootableBodiesUnresolved", recovery); + CompoundNBT staged = NativeSafeFixer.stageCorpse(original, context(1631)); + assertEquals(4, staged.getInt(UpgradePreservation.VERSION)); + assertEquals(record, staged.getList("LootableBodiesUnresolved", 10).getCompound(49)); + assertEquals(record, original.getList("LootableBodiesUnresolved", 10).getCompound(0)); + } + @Test void current114RecordsAreNotWrappedOrDuplicatedAgain() { + CompoundNBT original = body(4); + 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)); + 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/tools/audit_runtime_logs.py b/tools/audit_runtime_logs.py index d4ef020..9604740 100644 --- a/tools/audit_runtime_logs.py +++ b/tools/audit_runtime_logs.py @@ -42,15 +42,72 @@ def stack(a,b): def native_control_line(line): - """Exact Forge 25 diagnostics reproduced by the independent platform control.""" + """Exact Forge 28 diagnostics reproduced by the independent platform control.""" if line == 'AL lib: (EE) alc_cleanup: 1 device not closed': return True + if re.fullmatch(r'\[(?:\d{2}[A-Za-z]{3}\d{4} )?\d{2}:\d{2}:\d{2}(?:\.\d{3})?\] \[(?:Client|Server) thread/ERROR\] ' + r'\[(?:net\.minecraftforge\.fml\.network\.simple\.IndexedMessageCodec|ne\.mi\.fm\.ne\.si\.IndexedMessageCodec)(?:/SIMPLENET|/)?\]: Received empty payload on channel fml:handshake',line): + return True + clock=r'\[(?:\d{2}[A-Za-z]{3}\d{4} )?\d{2}:\d{2}:\d{2}(?:\.\d{3})?\] ' + arguments=r'\[(?:minecraft/ArgumentTypes|net\.minecraft\.command\.arguments\.ArgumentTypes)/?\]: ' + if re.fullmatch(clock+r'\[Netty(?: Epoll)? Server IO #[0-9]+/ERROR\] '+arguments+ + r'Could not serialize net\.minecraftforge\.server\.command\.(?PModIdArgument|EnumArgument)@[0-9a-f]{1,8} ' + r'\(class net\.minecraftforge\.server\.command\.(?P=argument)\) - will not be sent to client!',line): + return True + if re.fullmatch(clock+r'\[Netty(?: Epoll)? Client IO #[0-9]+/ERROR\] '+arguments+r'Could not deserialize minecraft:',line): + return True header=r'\[(?:\d{2}[A-Za-z]{3}\d{4} )?\d{2}:\d{2}:\d{2}(?:\.\d{3})?\] \[[^]\r\n]+/FATAL\] ' return bool(re.fullmatch(header+r'(?:' r'\[(?:net\.minecraftforge\.fml\.packs\.ModFileResourcePack|ne\.mi\.fm\.pa\.ModFileResourcePack)/?\]: Failed to clean up tempdir [^\r\n]+[\\/]modpacktmp\d+|' r'\[(?:net\.minecraftforge\.common\.ForgeConfig|ne\.mi\.co\.ForgeConfig)(?:/CORE|/)?\]: Forge config just got changed on the file system!)',line)) +def dead_player_movement_blocks(lines): + """Full Forge 28 stack reproduced with native packets and this mod absent.""" + prefix=[ + 'net.minecraft.world.server.TicketManager.func_219367_b(SourceFile:208)', + 'net.minecraft.world.server.ChunkManager.func_219183_a(ChunkManager.java:847)', + 'net.minecraft.world.server.ServerChunkProvider.func_217221_a(SourceFile:475)', + 'net.minecraft.network.play.ServerPlayNetHandler.func_147347_a(ServerPlayNetHandler.java:759)', + 'net.minecraft.network.play.client.CPlayerPacket.func_148833_a(SourceFile:126)', + 'net.minecraft.network.play.client.CPlayerPacket$PositionPacket.func_148833_a(SourceFile:57)', + 'net.minecraft.network.PacketThreadUtil.func_225383_a(SourceFile:21)', + 'net.minecraft.util.concurrent.TickDelayedTask.run(SourceFile:18)', + 'net.minecraft.util.concurrent.ThreadTaskExecutor.func_213166_h(SourceFile:144)', + 'net.minecraft.util.concurrent.RecursiveEventLoop.func_213166_h(SourceFile:23)', + 'net.minecraft.util.concurrent.ThreadTaskExecutor.func_213168_p(SourceFile:118)', + 'net.minecraft.server.MinecraftServer.func_213205_aW(MinecraftServer.java:701)', + 'net.minecraft.server.MinecraftServer.func_213168_p(MinecraftServer.java:695)'] + # The same pinned server drains tasks through its two native loop branches. + tails=[['net.minecraft.util.concurrent.ThreadTaskExecutor.func_213161_c(SourceFile:127)', + 'net.minecraft.server.MinecraftServer.func_213202_o(MinecraftServer.java:681)'], + ['net.minecraft.util.concurrent.ThreadTaskExecutor.func_213160_bf(SourceFile:103)', + 'net.minecraft.server.MinecraftServer.func_213202_o(MinecraftServer.java:680)']] + ending=['net.minecraft.server.MinecraftServer.run(MinecraftServer.java:628)', + 'java.lang.Thread.run(Thread.java:750)'] + # The independent control also reproduces this with native combined + # position/rotation packets. Keep both exact packet frames and loop tails. + stacks=[] + for packet in [prefix[5],'net.minecraft.network.play.client.CPlayerPacket$PositionRotationPacket.func_148833_a(SourceFile:18)']: + shaped=prefix[:5]+[packet]+prefix[6:] + stacks.extend(shaped+tail+ending for tail in tails) + header=re.compile(r'^\[(?:\d{2}[A-Za-z]{3}\d{4} )?\d{2}:\d{2}:\d{2}(?:\.\d{3})?\] ' + r'\[Server thread/FATAL\] \[(?:minecraft/ThreadTaskExecutor|net\.minecraft\.util\.concurrent\.ThreadTaskExecutor)/?\]: Error executing task on Server$') + frame=re.compile(r'\tat ([^()\r\n]+\([^()\r\n]*\))(?: ~?\[[^\]\r\n]+\](?: \{[^{}\r\n]*\})?)?') + boundaries=[i for i,line in enumerate(lines) if line.startswith('[')]+[len(lines)] + accepted=set();count=0 + for a,b in zip(boundaries,boundaries[1:]): + if not header.fullmatch(lines[a]):continue + body=[line for line in lines[a+1:b] if line.strip()] + if not body or body[0]!='java.lang.NullPointerException: null':continue + matches=[frame.fullmatch(line) for line in body[1:]] + if not all(matches):continue + frames=[match.group(1) for match in matches] + if frames not in stacks:continue + accepted.update(range(a,b));count+=1 + return accepted,count + + def audit(path, development=False, upgrade=False, fixture_logins=False, native=False): if path.name.endswith('.log.gz'): with gzip.open(path,'rt',encoding='utf-8',errors='replace') as stream: text=stream.read() @@ -62,11 +119,14 @@ def audit(path, development=False, upgrade=False, fixture_logins=False, native=F platform=['FML appears to be missing any signature data. This is not a good thing'] if development: platform.append('The binary patch set is missing. Either you are in a development environment, or things are not going to work!') - lines=text.splitlines() + # Forge's native Windows console colours its messages. Parse the same text + # as the complete file logs while keeping the original evidence untouched. + lines=re.sub(r'\x1b\[[0-9;]*m','',text).splitlines() proven=development_control_lines(lines) if development else set() + native_movement,movement_count=dead_player_movement_blocks(lines) if native else (set(),0) version_check_block=False for number,line in enumerate(lines): - if number in login_lines: continue + if number in login_lines or number in native_movement: continue if line.startswith('['): version_check_block=('Forge Version Check/DEBUG]' in line and ('[ForgeVersionCheck/forge]: Failed to process update information' in line or @@ -90,13 +150,14 @@ def audit(path, development=False, upgrade=False, fixture_logins=False, native=F failures.append(line) if not text.strip(): failures.append('Empty runtime log') if failures: raise RuntimeError(str(path)+'\n'+'\n'.join(failures)) - return {'path':str(path),'lines':len(text.splitlines()),'recovered_forge_pre_login_failures':len(login_lines),'expected_platform_errors':sum( + return {'path':str(path),'lines':len(text.splitlines()),'recovered_forge_pre_login_failures':len(login_lines), + 'known_forge28_dead_player_movement_failures':movement_count,'expected_platform_errors':sum( 1 for line in text.splitlines() if re.search(r'/ERROR\]|/FATAL\]',line))} if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__) - parser.add_argument('--native',action='store_true',help='Allow only independently reproduced Forge 25 platform diagnostics') + parser.add_argument('--native',action='store_true',help='Allow only independently reproduced Forge 28 platform diagnostics') parser.add_argument('--development',action='store_true') parser.add_argument('--upgrade',action='store_true') parser.add_argument('--recursive',action='store_true') diff --git a/tools/command_logs.py b/tools/command_logs.py new file mode 100644 index 0000000..e3578d4 --- /dev/null +++ b/tools/command_logs.py @@ -0,0 +1,15 @@ +"""Keep complete tool logs and expose the failure details in CI output.""" +import subprocess +import sys + + +def run_logged(command, path, **options): + try: + with path.open('w', encoding='utf-8') as log: + return subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, + check=True, **options) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + print('Command failed; complete log: ' + str(path), file=sys.stderr) + print('\n'.join(path.read_text(encoding='utf-8', errors='replace').splitlines()[-80:]), + file=sys.stderr) + raise diff --git a/tools/fixture_login.py b/tools/fixture_login.py index 7700baf..ca5d020 100644 --- a/tools/fixture_login.py +++ b/tools/fixture_login.py @@ -8,6 +8,10 @@ JOIN = re.compile(r'^\[(?:\d{2}[A-Za-z]{3}\d{4} )?\d{2}:\d{2}:\d{2}(?:\.\d{3})?\] \[Server thread/INFO\] \[(?:minecraft/DedicatedServer|minecraft/MinecraftServer|net\.minecraft\.server\.dedicated\.DedicatedServer|net\.minecraft\.server\.MinecraftServer|ne\.mi\.se\.MinecraftServer)/?\]: Container([AB]) (joined|left) the game$') LIMIT = 3 +def plain(text): + """Parse native console colours without changing saved evidence or line numbers.""" + return re.sub(r"\x1b\[[0-9;]*m", "", text) + RESET = re.compile(r"^\[\d{2}:\d{2}:\d{2}\] \[Server thread/INFO\] \[(?:minecraft/NetHandlerPlayServer|net\.minecraft\.network\.NetHandlerPlayServer)\]: Container([AB]) lost connection: Internal Exception: io\.netty\.channel\.unix\.Errors\$NativeIoException: syscall:read\(\.\.\) failed: Connection reset by peer$") @@ -15,7 +19,7 @@ def completed_client(home, role): """A terminal phase, pass file and exact client log marker must all agree.""" log=home/('client'+role+'.log') if not log.is_file():return False - lines=log.read_text(encoding='utf-8',errors='replace').splitlines() + lines=plain(log.read_text(encoding='utf-8',errors='replace')).splitlines() for mode,phase,marker in [('respawn','9','LOOTABLE_BODIES_RESPAWN_CLIENT_PASS '+role), ('visibility','18','LOOTABLE_BODIES_VISIBILITY_CLIENT_PASS role='+role), ('multiplayer','8','LOOTABLE_BODIES_MULTIPLAYER_CLIENT_PASS role='+role)]: @@ -34,14 +38,14 @@ def planned_disconnect(home, role, phase): if not log.is_file():return False marker='VISIBILITY_CLIENT_PHASE '+role+' '+phase return any(line==marker or line.endswith(': '+marker) - for line in log.read_text(encoding='utf-8',errors='replace').splitlines()) + 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 poll(self): text=(self.home/'server.log').read_text(encoding='utf-8',errors='replace') - lines=text[:text.rfind('\n')+1].splitlines() + lines=plain(text[:text.rfind('\n')+1]).splitlines() for line in lines[self.seen:]: phase=re.search(r'(?:^|: )VISIBILITY_PHASE ([0-9]+)$',line) if phase:self.visibility_phase=phase[1] @@ -70,7 +74,7 @@ def receipt_lines(path, text): data=json.loads(receipt.read_text(encoding='utf-8')) if data.get('schema')!=1 or data.get('sha256')!=hashlib.sha256(path.read_bytes()).hexdigest(): raise RuntimeError('Invalid or stale Forge login receipt: '+str(path)) - lines=text.splitlines();allowed=set(data['lines']);counts={'A':0,'B':0};online=set();pending=set();actual=set() + lines=plain(text).splitlines();allowed=set(data['lines']);counts={'A':0,'B':0};online=set();pending=set();actual=set() for i,line in enumerate(lines): join=JOIN.fullmatch(line) if join: @@ -88,7 +92,7 @@ def write_receipts(home): home=Path(home) for path in [home/'server.log',home/'server/logs/latest.log',home/'server/logs/debug.log']: if not path.is_file():continue - text=path.read_text(encoding='utf-8',errors='replace');lines=text.splitlines() + text=path.read_text(encoding='utf-8',errors='replace');lines=plain(text).splitlines() allowed=[i for i,line in enumerate(lines) if FAILURE.fullmatch(line)] if not allowed:continue counts={'A':0,'B':0} diff --git a/tools/fixtures/forge28-dead-player-movement.txt b/tools/fixtures/forge28-dead-player-movement.txt new file mode 100644 index 0000000..db9d371 --- /dev/null +++ b/tools/fixtures/forge28-dead-player-movement.txt @@ -0,0 +1,19 @@ +[12:00:00] [Server thread/FATAL] [minecraft/ThreadTaskExecutor]: Error executing task on Server +java.lang.NullPointerException: null + at net.minecraft.world.server.TicketManager.func_219367_b(SourceFile:208) + at net.minecraft.world.server.ChunkManager.func_219183_a(ChunkManager.java:847) + at net.minecraft.world.server.ServerChunkProvider.func_217221_a(SourceFile:475) + at net.minecraft.network.play.ServerPlayNetHandler.func_147347_a(ServerPlayNetHandler.java:759) + at net.minecraft.network.play.client.CPlayerPacket.func_148833_a(SourceFile:126) + at net.minecraft.network.play.client.CPlayerPacket$PositionPacket.func_148833_a(SourceFile:57) + at net.minecraft.network.PacketThreadUtil.func_225383_a(SourceFile:21) + at net.minecraft.util.concurrent.TickDelayedTask.run(SourceFile:18) + at net.minecraft.util.concurrent.ThreadTaskExecutor.func_213166_h(SourceFile:144) + at net.minecraft.util.concurrent.RecursiveEventLoop.func_213166_h(SourceFile:23) + at net.minecraft.util.concurrent.ThreadTaskExecutor.func_213168_p(SourceFile:118) + at net.minecraft.server.MinecraftServer.func_213205_aW(MinecraftServer.java:701) + at net.minecraft.server.MinecraftServer.func_213168_p(MinecraftServer.java:695) + at net.minecraft.util.concurrent.ThreadTaskExecutor.func_213161_c(SourceFile:127) + at net.minecraft.server.MinecraftServer.func_213202_o(MinecraftServer.java:681) + at net.minecraft.server.MinecraftServer.run(MinecraftServer.java:628) + at java.lang.Thread.run(Thread.java:750) diff --git a/tools/fixtures/forge28-server-default.toml b/tools/fixtures/forge28-server-default.toml new file mode 100644 index 0000000..c77d09d --- /dev/null +++ b/tools/fixtures/forge28-server-default.toml @@ -0,0 +1,30 @@ + +#Server configuration settings +[server] + #Set this to true to remove any Entity 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. + removeErroringEntities = false + #Set this to true to remove any TileEntity 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. + removeErroringTileEntities = false + #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 + #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 + #Log cascading chunk generation issues during terrain population. + logCascadingWorldGeneration = true + #Fix vanilla issues that cause worldgen cascading. This DOES change vanilla worldgen so DO NOT report bugs related to world differences if this flag is on. + fixVanillaCascading = false + #The time in ticks the server will wait when a dimension was queued to unload. This can be useful when rapidly loading and unloading dimensions, like e.g. throwing items through a nether portal a few time per second. + #Range: > 0 + dimensionUnloadQueueDelay = 0 + #Controls the number threshold at which Packet51 is preferred over Packet52, default and minimum 64, maximum 1024 + #Range: 64 ~ 1024 + clumpingThreshold = 64 + #Vanilla will treat crafting recipess 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 + #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 + diff --git a/tools/native_runtime.py b/tools/native_runtime.py index f511047..5b5ff98 100644 --- a/tools/native_runtime.py +++ b/tools/native_runtime.py @@ -1,4 +1,4 @@ -"""Disposable official Forge 25 launches, separate from the development bootstrap.""" +"""Disposable official Forge 28 launches, separate from the development bootstrap.""" import hashlib import json import os @@ -9,15 +9,11 @@ import urllib.error import urllib.request import zipfile +from command_logs import run_logged -FORGE = 'forge-1.13.2-25.0.223' -INSTALLER_SHA256 = 'D46CB326E7DC32695084090C8169BB57F32F1C23B801DDB35A336F252E381191' -SERVER_INPUTS = { - FORGE + '.jar': '0EE9DDDA49381A67707909502AC9212C89F9D7A8772160E566A27CEFDA4173C5', - 'libraries/net/minecraftforge/forge/1.13.2-25.0.223/' + FORGE + '-server.jar': - 'C67C31EFD51348222D65CD3BAE975125BCF078EC9AAD5499B09E08DCF9EBC93F', - 'minecraft_server.1.13.2.jar': 'FFD3AA2C25C5BA68A706B59F2ABDC69AC1748E115CA9D3B47941E197736F088E', -} +FORGE = 'forge-1.14.4-28.2.26' +INSTALLER_SHA256 = 'E68A0E2D589B2954C5D109A04B80747AC8C061ACABE074DB1607F9C2FD7D2CE1' +SERVER_INPUTS = {'forge-1.14.4-28.2.26.jar': '3917C2F46E0AE4D93C007156A850B0CA2B6586649C6B4D3D8AC8D58D74801E01', 'libraries/net/minecraftforge/forge/1.14.4-28.2.26/forge-1.14.4-28.2.26-server.jar': 'C7736D4541B97166FCEF9FE2A1B42AD96D2FF8CFEB63C53F83B18658A6B596CE', 'minecraft_server.1.14.4.jar': '5ECDEDAB3A6E129321A444490D0A467C25EA702A24A99CEBE3B6AED41F8F5729'} def project_assets(project): @@ -40,11 +36,11 @@ def prepare_server(home, distribution, candidate=None, probe=None): launcher = distribution / (FORGE + '.jar') libraries = distribution / 'libraries' if not launcher.is_file() or not libraries.is_dir(): - raise FileNotFoundError('Incomplete official Forge 25 server installation') + raise FileNotFoundError('Incomplete official Forge 28 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 25 server input: ' + name) + raise RuntimeError('Incorrect official Forge 28 server input: ' + name) home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() shutil.copy2(launcher, home / launcher.name) @@ -110,7 +106,7 @@ def _download(artifact, destination): destination.parent.mkdir(parents=True, exist_ok=True) for address in (artifact['url'], artifact['url'] + '?source=lootablebodies-runtime'): try: - request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesRuntimeTests/2.4.4.113021'}) + request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesRuntimeTests/2.4.5.114041'}) with urllib.request.urlopen(request, timeout=60) as response: content = response.read() if hashlib.sha1(content).hexdigest() != expected: @@ -148,11 +144,11 @@ def validate_client_paths(paths): def client_inputs(runtime): runtime = runtime.resolve() - base_path = runtime / 'versions/1.13.2/1.13.2.json' - if not base_path.is_file() or hashlib.sha1(base_path.read_bytes()).hexdigest() != 'fa3ddc22146c46bfeb0e9d322c6f83b937e25005': - raise RuntimeError('Missing or incorrect official Minecraft 1.13.2 descriptor') + base_path = runtime / 'versions/1.14.4/1.14.4.json' + if not base_path.is_file() or hashlib.sha1(base_path.read_bytes()).hexdigest() != 'be146d5f66a3627ed0a87c234c4d8dde8ab35098': + raise RuntimeError('Missing or incorrect official Minecraft 1.14.4 descriptor') base = json.loads(base_path.read_text(encoding='utf-8')) - forge = json.loads((runtime / 'versions/1.13.2-forge-25.0.223/1.13.2-forge-25.0.223.json').read_text(encoding='utf-8')) + forge = json.loads((runtime / 'versions/1.14.4-forge-28.2.26/1.14.4-forge-28.2.26.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) @@ -189,7 +185,7 @@ def client_inputs(runtime): raise RuntimeError('Conflicting native runtime input: ' + target.name) else: target.write_bytes(content) - base_jar = runtime / 'versions/1.13.2/1.13.2.jar' + base_jar = runtime / 'versions/1.14.4/1.14.4.jar' _download(base['downloads']['client'], base_jar) paths = list(modules.values()) + [base_jar] validate_client_paths(paths) @@ -200,9 +196,9 @@ 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.13.2/1.13.2.json').read_text(encoding='utf-8')) + base = json.loads((runtime / 'versions/1.14.4/1.14.4.json').read_text(encoding='utf-8')) assets = runtime / 'assets' - index = assets / 'indexes/1.13.1.json' + index = assets / 'indexes/1.14.json' _download(base['assetIndex'], index) objects = json.loads(index.read_text(encoding='utf-8'))['objects'] unique = {item['hash'] for item in objects.values()} @@ -233,20 +229,21 @@ def prepare_client(home, candidate=None, probe=None): if not artifact.is_file(): raise FileNotFoundError(artifact) shutil.copy2(artifact, home / 'mods' / artifact.name) - (home / 'options.txt').write_text('realmsNotifications:false\npauseOnLostFocus:false\nrenderDistance:4\nmaxFps:30\n', encoding='utf-8') + # These owned clients exercise graphics and inventory, not audio playback. + (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.13.1.json' + index = assets / 'indexes/1.14.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.13.2-forge-25.0.223', '--gameDir', str(home.resolve()), - '--assetsDir', str(assets.resolve()), '--assetIndex', '1.13.1', + '--version', '1.14.4-forge-28.2.26', '--gameDir', str(home.resolve()), + '--assetsDir', str(assets.resolve()), '--assetIndex', '1.14', '--uuid', '00000000-0000-4000-8000-000000000001', '--accessToken', 'disposable-test', '--userType', 'legacy', '--versionType', 'release', '--width', '854', '--height', '480'] @@ -256,16 +253,15 @@ def install(java,home,client=False): 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.13.2-25.0.223/'+installer.name,installer,INSTALLER_SHA256) + fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.14.4-28.2.26/'+installer.name,installer,INSTALLER_SHA256) if client: - base=home/'versions/1.13.2/1.13.2.json' - _download({'sha1':'fa3ddc22146c46bfeb0e9d322c6f83b937e25005','url':'https://piston-meta.mojang.com/v1/packages/fa3ddc22146c46bfeb0e9d322c6f83b937e25005/1.13.2.json'},base) + base=home/'versions/1.14.4/1.14.4.json' + _download({'sha1':'be146d5f66a3627ed0a87c234c4d8dde8ab35098','url':'https://piston-meta.mojang.com/v1/packages/be146d5f66a3627ed0a87c234c4d8dde8ab35098/1.14.4.json'},base) metadata=json.loads(base.read_text());_download(metadata['downloads']['client'],base.with_suffix('.jar')) (home/'launcher_profiles.json').write_text('{"profiles":{}}\n') - with (home/'install.log').open('w',encoding='utf-8') as log: - subprocess.run([str(java),'-Djava.awt.headless=true','-jar',installer.name, + run_logged([str(java),'-Djava.awt.headless=true','-jar',installer.name, '--installClient' if client else '--installServer',str(home)], - cwd=home,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=600) + 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} diff --git a/tools/prepare_legacy_tools.py b/tools/prepare_legacy_tools.py index 41d2a35..2a6b36b 100644 --- a/tools/prepare_legacy_tools.py +++ b/tools/prepare_legacy_tools.py @@ -6,6 +6,7 @@ from pathlib import Path, PurePosixPath import subprocess import tarfile +from command_logs import run_logged SOURCE = 'ee8ee733fa091e961f27812ad780d56c35813f30' REPOSITORY = 'https://github.com/MinecraftModDevelopmentMods/LootableBodies.git' @@ -21,23 +22,23 @@ def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest().upper() -def prepare(project, output): +def prepare(project, output, commit=SOURCE): source = output / 'source' manifest = output / 'source.json' if source.exists(): recorded = json.loads(manifest.read_text(encoding='utf-8')) - if recorded['commit'] != SOURCE or any( + if recorded['commit'] != commit or any( not (source / name).is_file() or digest(source / name) != expected for name, expected in recorded['files'].items()): raise RuntimeError('Historical source changed; preserve it and use a new output folder') return source output.mkdir(parents=True, exist_ok=False) - if subprocess.run(['git', 'cat-file', '-e', SOURCE], cwd=project, + if subprocess.run(['git', 'cat-file', '-e', commit], cwd=project, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode: - subprocess.run(['git', 'fetch', '--no-tags', REPOSITORY, SOURCE], cwd=project, check=True) + subprocess.run(['git', 'fetch', '--no-tags', REPOSITORY, commit], cwd=project, check=True) archive = output / 'released-source.tar' with archive.open('wb') as stream: - subprocess.run(['git', 'archive', '--format=tar', SOURCE], cwd=project, + subprocess.run(['git', 'archive', '--format=tar', commit], cwd=project, stdout=stream, check=True) source.mkdir() files = {} @@ -53,7 +54,7 @@ def prepare(project, output): destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(tar.extractfile(entry).read()) files[name.as_posix()] = digest(destination) - manifest.write_text(json.dumps({'commit': SOURCE, 'files': files}, indent=2) + '\n', encoding='utf-8') + manifest.write_text(json.dumps({'commit': commit, 'files': files}, indent=2) + '\n', encoding='utf-8') return source @@ -73,11 +74,10 @@ def main(): wrapper = source / ('gradlew.bat' if os.name == 'nt' else 'gradlew') if os.name != 'nt': wrapper.chmod(0o755) - with (output / 'build.log').open('w', encoding='utf-8') as log: - subprocess.run([str(wrapper), 'prepareLegacyWorldFixtures', 'prepareOriginal112Checks', 'preparePackagedIntegrationProbe', + run_logged([str(wrapper), 'prepareLegacyWorldFixtures', 'prepareOriginal112Checks', 'preparePackagedIntegrationProbe', '--init-script='+str(project/'gradle/historical-writers.init.gradle'), '-PhistoricalWriterSource='+str(project),'--no-daemon', '--console=plain'], cwd=source, env=environment, - stdout=log, stderr=subprocess.STDOUT, check=True, timeout=1800) + path=output / 'build.log', timeout=1800) hashes = {} for name in OUTPUTS: artifact = source / 'build' / name diff --git a/tools/prepare_migration_inputs.py b/tools/prepare_migration_inputs.py index ac0ea0c..281522d 100644 --- a/tools/prepare_migration_inputs.py +++ b/tools/prepare_migration_inputs.py @@ -18,6 +18,8 @@ 'C82CC329BA53A218CADB681D646F64A9C457341577BC4F9AA07E2F9926434CCF'), } MODS.update({ + 'released-113.jar': ('https://github.com/MinecraftModDevelopmentMods/LootableBodies/releases/download/2.4.4.113021/LootableBodies-2.4.4.113021.jar', + '464CC1CDDA6C04CD72F5F1A2A504EB8A665E6E48AC255918437262DB2A22A4C1'), 'released-112.jar': ('https://github.com/MinecraftModDevelopmentMods/LootableBodies/releases/download/2.4.3.112021/LootableBodies-2.4.3.112021.jar', 'A9F4B115828834F60C5905526A8A99794B1DFE4802AB4D48858F2D597F3FCBCA'), 'released-111.jar': ('https://github.com/MinecraftModDevelopmentMods/LootableBodies/releases/download/2.4.2.111021/LootableBodies-2.4.2.111021.jar', @@ -50,7 +52,7 @@ def fetch(url, path, expected): for address in [url, url + '?source=lootablebodies-migration', url + '?source=lootablebodies111']: try: print('MIGRATION_INPUT_FETCH',path.name,flush=True) - request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesMigrationTests/2.4.4.113021'}) + request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesMigrationTests/2.4.5.114041'}) with urllib.request.urlopen(request, timeout=30) as response: chunks=[]; started=time.monotonic() while True: @@ -59,6 +61,12 @@ def fetch(url, path, expected): chunks.append(part) if time.monotonic()-started>120: raise TimeoutError('Artifact transfer exceeded two minutes') payload=b''.join(chunks) + actual=hashlib.sha256(payload).hexdigest().upper() + if actual!=expected: + print('MIGRATION_INPUT_CHECKSUM_REJECTED',path.name,'bytes='+str(len(payload)), + 'sha256='+actual,'expected='+expected,flush=True) + payload=None + continue break except urllib.error.HTTPError as error: if error.code not in (403, 429, 500, 502, 503, 504): @@ -100,6 +108,10 @@ def main(): if sha(universal) != universal_sha or hashlib.sha1(server.read_bytes()).hexdigest() != server_sha: raise RuntimeError('Installed Forge/Minecraft artifact checksum mismatch') report[name] = {'installer_sha256': installer_sha, 'universal_sha256': universal_sha, 'server_sha1': server_sha} + from prepare_native113_tools import historical_runtime + runtime = historical_runtime(Path(__file__).resolve().parents[1]) + runtime.install(args.java.resolve(), root/'forge113') + report['forge113'] = json.loads((root/'forge113/runtime-inputs.json').read_text()) (root / 'inputs.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8') print('MIGRATION_INPUTS_VERIFIED', flush=True) diff --git a/tools/prepare_native113_tools.py b/tools/prepare_native113_tools.py new file mode 100644 index 0000000..b601924 --- /dev/null +++ b/tools/prepare_native113_tools.py @@ -0,0 +1,68 @@ +"""Build the 1.13 fixture writer against the exact released 1.13 source.""" +import argparse +import json +import os +from pathlib import Path +import subprocess +from prepare_legacy_tools import prepare, digest +from command_logs import run_logged + +SOURCE = '8219190bab22600475542d20ed5df74fea15c27b' +OUTPUT = 'native113-seed/lootablebodies-native113-world-fixtures-reobf.jar' +PROBE = 'integration-test-mod/lootablebodies-build-tests-reobf.jar' + + +def historical_runtime(project): + """Read the frozen release's installer/launch code, never the candidate's.""" + import importlib.util + root = project / '.gradle/legacy-tools/released-113' + 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.13 runtime tools; run prepare_native113_tools.py') + spec = importlib.util.spec_from_file_location('released113_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-113').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), 'prepareNative113WorldFixtures', 'preparePackagedIntegrationProbe', + '--init-script='+str(project/'gradle/native113-writer.init.gradle'), + '-PhistoricalWriterSource='+str(project), '--no-daemon', '--console=plain'], + cwd=source, env=environment, path=output / 'build.log', timeout=1800) + artifact = source / 'build' / OUTPUT + if not artifact.is_file() or not artifact.stat().st_size: + raise FileNotFoundError(artifact) + files = [p for p in (project/'src/nativeSeed113').rglob('*') if p.is_file()] + probe = source / 'build' / PROBE + if not probe.is_file() or not probe.stat().st_size: + raise FileNotFoundError(probe) + record = {'source': SOURCE, 'artifact': OUTPUT, 'sha256': digest(artifact), + 'probe': PROBE, 'probe_sha256': digest(probe), + '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) + import shutil + shutil.copy2(artifact, target) + print('Historical 1.13 writer built from released source '+SOURCE) + + +if __name__ == '__main__': + main() diff --git a/tools/test_audit_runtime_logs.py b/tools/test_audit_runtime_logs.py index 16d90e5..e35bfd9 100644 --- a/tools/test_audit_runtime_logs.py +++ b/tools/test_audit_runtime_logs.py @@ -6,6 +6,56 @@ class RuntimeLogAuditTests(unittest.TestCase): + def dead_player_movement(self): + return (Path(__file__).parent/'fixtures/forge28-dead-player-movement.txt').read_text() + + def test_native_dead_player_movement_matches_the_complete_control_stack(self): + known=self.dead_player_movement() + for stack in (known,known.replace('func_213161_c(SourceFile:127)','func_213160_bf(SourceFile:103)').replace('MinecraftServer.java:681','MinecraftServer.java:680')): + with self.subTest(stack=stack): + self.assertEqual(1,self.check_log(stack,native=True)['known_forge28_dead_player_movement_failures']) + self.assertEqual(2,self.check_log(known+known,native=True)['known_forge28_dead_player_movement_failures']) + + def test_native_dead_player_movement_requires_native_mode(self): + for options in ({},{'development':True},{'upgrade':True}): + with self.subTest(options=options),self.assertRaises(RuntimeError): + self.check_log(self.dead_player_movement(),**options) + + def test_native_dead_player_position_rotation_matches_independent_control(self): + known=self.dead_player_movement().replace('CPlayerPacket$PositionPacket.func_148833_a(SourceFile:57)', + 'CPlayerPacket$PositionRotationPacket.func_148833_a(SourceFile:18)') + for stack in (known,known.replace('func_213161_c(SourceFile:127)','func_213160_bf(SourceFile:103)').replace('MinecraftServer.java:681','MinecraftServer.java:680')): + with self.subTest(stack=stack): + self.assertEqual(1,self.check_log(stack,native=True)['known_forge28_dead_player_movement_failures']) + + def test_position_rotation_control_still_requires_exact_complete_native_stack(self): + known=self.dead_player_movement().replace('CPlayerPacket$PositionPacket.func_148833_a(SourceFile:57)', + 'CPlayerPacket$PositionRotationPacket.func_148833_a(SourceFile:18)') + for options in ({},{'development':True},{'upgrade':True}): + with self.subTest(options=options),self.assertRaises(RuntimeError):self.check_log(known,**options) + for changed in (known.replace('PositionRotationPacket.func_148833_a(SourceFile:18)', + 'PositionRotationPacket.func_148833_a(SourceFile:19)'), + known.replace('SourceFile:208','SourceFile:209'), + known.replace('PositionRotationPacket','RotationPacket'), + known+'\tat cyano.lootable.Other.hook(Other.java:12)\n', + known+'Caused by: java.lang.IllegalStateException: other failure\n'): + with self.subTest(changed=changed),self.assertRaises(RuntimeError):self.check_log(changed,native=True) + + def test_native_dead_player_movement_rejects_changed_or_additional_failures(self): + known=self.dead_player_movement() + variants=[known.replace('SourceFile:208','SourceFile:209'), + known.replace('CPlayerPacket$PositionPacket','CPlayerPacket$RotationPacket'), + known.replace('java.lang.NullPointerException: null','java.lang.IllegalStateException: null'), + known.replace('/FATAL]','/ERROR]'),known.replace('Server thread','Netty Server IO #1'), + known.replace('minecraft/ThreadTaskExecutor','minecraft/OtherExecutor'), + known+'\tat cyano.lootable.Other.hook(Other.java:12)\n', + known+'Caused by: java.lang.IllegalStateException: new failure\n', + known+'Suppressed: java.lang.IllegalStateException: new failure\n', + known.rsplit('\n\tat java.lang.Thread.run',1)[0]+'\n', + known+'[12:00:01] [Server thread/INFO] Next task\njava.lang.NullPointerException: null\n'] + for changed in variants: + with self.subTest(changed=changed),self.assertRaises(RuntimeError):self.check_log(changed,native=True) + def check_log(self, text, **options): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / 'runtime.log' @@ -91,6 +141,40 @@ def test_native_openal_shutdown_diagnostic_is_exact(self): with self.subTest(text=text), self.assertRaises(RuntimeError): self.check_log(text,native=text!=known) + def test_native_terminal_colours_do_not_change_log_classification(self): + known='[15:16:49] [Thread-1/FATAL] [ne.mi.co.ForgeConfig/CORE]: Forge config just got changed on the file system!' + self.check_log('\x1b[m\x1b[1;31m'+known+'\x1b[m\n',native=True) + for text in [known+' extra failure',known.replace('ForgeConfig','OtherConfig'), + '[15:16:49] [Server thread/ERROR] Unexpected error']: + with self.subTest(text=text),self.assertRaises(RuntimeError): + self.check_log('\x1b[1;31m'+text+'\x1b[m\n',native=True) + + def test_native_forge28_empty_handshake_control_is_narrow(self): + known='[15:49:16] [Client thread/ERROR] [net.minecraftforge.fml.network.simple.IndexedMessageCodec]: Received empty payload on channel fml:handshake' + self.check_log(known+'\n',native=True) + for text in [known, known+' extra failure', known.replace('fml:handshake','other:channel'), + known.replace('IndexedMessageCodec','OtherCodec')]: + with self.subTest(text=text),self.assertRaises(RuntimeError): + self.check_log(text+'\n',native=text!=known) + + def test_native_forge28_tcp_command_tree_diagnostics_are_exact(self): + lines=[ + '[16:19:28] [Netty Server IO #1/ERROR] [minecraft/ArgumentTypes]: Could not serialize net.minecraftforge.server.command.ModIdArgument@62e9cbe6 (class net.minecraftforge.server.command.ModIdArgument) - will not be sent to client!', + '[16:19:28.111] [Netty Server IO #2/ERROR] [net.minecraft.command.arguments.ArgumentTypes/]: Could not serialize net.minecraftforge.server.command.EnumArgument@d8352c6 (class net.minecraftforge.server.command.EnumArgument) - will not be sent to client!', + '[16:19:28] [Netty Client IO #1/ERROR] [net.minecraft.command.arguments.ArgumentTypes]: Could not deserialize minecraft:', + '[16:19:30] [Server thread/ERROR] [ne.mi.fm.ne.si.IndexedMessageCodec/SIMPLENET]: Received empty payload on channel fml:handshake'] + lines += [line.replace('Netty Server', 'Netty Epoll Server').replace('Netty Client', 'Netty Epoll Client') + for line in lines if 'Netty' in line] + for line in lines: + with self.subTest(line=line): + self.check_log(line+'\n',native=True) + with self.assertRaises(RuntimeError):self.check_log(line+'\n') + for changed in [line+' extra failure',line.replace('ModIdArgument','OtherArgument').replace('EnumArgument','OtherArgument').replace('deserialize minecraft:','deserialize other:').replace('fml:handshake','other:channel')]: + with self.assertRaises(RuntimeError):self.check_log(changed+'\n',native=True) + if 'Netty Epoll' in line: + with self.assertRaises(RuntimeError): + self.check_log(line.replace('Netty Epoll', 'Netty Unknown')+'\n',native=True) + def forge14_control(self): return (Path(__file__).parent / 'fixtures/forge14-bootstrap-diagnostics.txt').read_text(encoding='utf-8') diff --git a/tools/test_fixture_login.py b/tools/test_fixture_login.py index 9981515..a823f64 100644 --- a/tools/test_fixture_login.py +++ b/tools/test_fixture_login.py @@ -80,6 +80,19 @@ def test_clients_boot_together_but_only_a_join_opens_b_connection_gate(self): self.log.write_text(JOIN);self.monitor.poll() self.assertTrue((self.root/'sync/client-A-joined').is_file()) + def test_native_coloured_join_opens_the_second_client_gate(self): + self.log.write_text('\x1b[m\x1b[32m'+JOIN+'\x1b[m') + self.monitor.poll() + self.assertTrue((self.root/'sync/client-A-joined').is_file()) + + def test_coloured_retry_keeps_original_bytes_in_the_receipt_hash(self): + original='\x1b[32m'+FAILURE+'\x1b[m'+JOIN + self.log.write_text(original);self.monitor.poll() + self.assertEqual('1',(self.root/'sync/login-retry-A').read_text()) + (self.root/'sync/login-retried-A').write_text('1') + write_receipts(self.root);audit(self.log,fixture_logins=True) + self.assertEqual(original,self.log.read_text()) + def test_live_log_receipts_survive_the_cold_reload(self): self.log.write_text(FAILURE+JOIN);self.monitor.poll() (self.root/'sync/login-retried-A').write_text('1') diff --git a/tools/test_historical_markers.py b/tools/test_historical_markers.py new file mode 100644 index 0000000..73ceef6 --- /dev/null +++ b/tools/test_historical_markers.py @@ -0,0 +1,30 @@ +import tempfile +import unittest +from pathlib import Path +from verify_world_upgrades import preserve_historical_markers + + +class HistoricalMarkerTests(unittest.TestCase): + def test_earlier_runner_markers_are_preserved_outside_the_disposable_world(self): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory);world=home/'world';world.mkdir() + (world/'upgrade-near.pass').write_bytes(b'previous release passed\n') + (world/'level.dat').write_bytes(b'immutable world data') + (world/'manual.pass').write_bytes(b'unrelated file') + preserve_historical_markers(home) + self.assertFalse((world/'upgrade-near.pass').exists()) + self.assertEqual(b'previous release passed\n',(home/'historical-run-markers/upgrade-near.pass').read_bytes()) + self.assertEqual(b'immutable world data',(world/'level.dat').read_bytes()) + self.assertEqual(b'unrelated file',(world/'manual.pass').read_bytes()) + + def test_existing_evidence_cannot_be_replaced(self): + with tempfile.TemporaryDirectory() as directory: + home=Path(directory);world=home/'world';world.mkdir() + (world/'upgrade-near.pass').write_bytes(b'new') + prior=home/'historical-run-markers';prior.mkdir();(prior/'upgrade-near.pass').write_bytes(b'old') + with self.assertRaises(RuntimeError):preserve_historical_markers(home) + self.assertEqual(b'new',(world/'upgrade-near.pass').read_bytes()) + self.assertEqual(b'old',(prior/'upgrade-near.pass').read_bytes()) + + +if __name__=='__main__':unittest.main() diff --git a/tools/test_tool_diagnostics.py b/tools/test_tool_diagnostics.py new file mode 100644 index 0000000..89731c3 --- /dev/null +++ b/tools/test_tool_diagnostics.py @@ -0,0 +1,83 @@ +import contextlib +import io +import hashlib +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +import native_runtime +import prepare_legacy_tools +import prepare_native113_tools +import prepare_migration_inputs + + +class ToolDiagnosticsTests(unittest.TestCase): + payload = 'original beginning\n' + 'build progress\n' * 100 + 'fixture failure detail\n' + + def test_rejected_download_tries_next_address_before_writing(self): + valid=b'checksum-locked fixture' + expected=hashlib.sha256(valid).hexdigest().upper() + with tempfile.TemporaryDirectory() as directory: + target=Path(directory)/'fixture.jar' + with patch('urllib.request.urlopen',side_effect=[io.BytesIO(b'incomplete'),io.BytesIO(valid)]) as fetch, contextlib.redirect_stdout(io.StringIO()): + prepare_migration_inputs.fetch('https://example.invalid/fixture.jar',target,expected) + self.assertEqual(2,fetch.call_count) + self.assertEqual(valid,target.read_bytes()) + + def test_every_rejected_download_is_checked_and_existing_file_preserved(self): + with tempfile.TemporaryDirectory() as directory: + target=Path(directory)/'fixture.jar';target.write_bytes(b'previous verified fixture') + with patch('urllib.request.urlopen',side_effect=[io.BytesIO(b'bad') for _ in range(3)]) as fetch, contextlib.redirect_stdout(io.StringIO()): + with self.assertRaisesRegex(RuntimeError,'checksum mismatch'): + prepare_migration_inputs.fetch('https://example.invalid/fixture.jar',target,'0'*64) + self.assertEqual(3,fetch.call_count) + self.assertEqual(b'previous verified fixture',target.read_bytes()) + + def failed_command(self, command, **options): + options['stdout'].write(self.payload) + options['stdout'].flush() + raise subprocess.CalledProcessError(7, command) + + def check_writer(self, module): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / 'source' + source.mkdir() + (source / ('gradlew.bat' if sys.platform == 'win32' else 'gradlew')).write_text('fixture') + arguments = ['writer', '--output', str(root), '--java17-home', str(root), + '--java8-home', str(root), '--java25-home', str(root)] + stderr = io.StringIO() + with patch.object(sys, 'argv', arguments), patch.object(module, 'prepare', return_value=source), \ + patch('subprocess.run', side_effect=self.failed_command), contextlib.redirect_stderr(stderr): + with self.assertRaises(subprocess.CalledProcessError) as failure: + module.main() + self.assertEqual(7, failure.exception.returncode) + self.assertEqual(self.payload, (root / 'build.log').read_text()) + self.assertIn('fixture failure detail', stderr.getvalue()) + self.assertIn(str(root / 'build.log'), stderr.getvalue()) + + def test_legacy_writer_failure_keeps_full_log_and_explains_it_in_ci(self): + self.check_writer(prepare_legacy_tools) + + def test_native113_writer_failure_keeps_full_log_and_explains_it_in_ci(self): + self.check_writer(prepare_native113_tools) + + def test_installer_failure_keeps_full_log_and_explains_it_in_ci(self): + with tempfile.TemporaryDirectory() as directory: + home = Path(directory) / 'native' + stderr = io.StringIO() + with patch('prepare_migration_inputs.fetch'), patch('subprocess.run', side_effect=self.failed_command), \ + contextlib.redirect_stderr(stderr): + with self.assertRaises(subprocess.CalledProcessError) as failure: + native_runtime.install(Path(sys.executable), home) + self.assertEqual(7, failure.exception.returncode) + self.assertEqual(self.payload, (home / 'install.log').read_text()) + self.assertIn('fixture failure detail', stderr.getvalue()) + self.assertIn(str(home / 'install.log'), stderr.getvalue()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test_upgrade_abort.py b/tools/test_upgrade_abort.py index 5037bb3..af99975 100644 --- a/tools/test_upgrade_abort.py +++ b/tools/test_upgrade_abort.py @@ -37,13 +37,66 @@ def check_fixture(self, case, existing): def test_only_the_stock_new_forge_config_is_exempt_from_world_data_comparison(self): with tempfile.TemporaryDirectory() as directory: world=Path(directory);config=world/'serverconfig/forge-server.toml';config.parent.mkdir() - stock=(Path(__file__).parent/'fixtures/forge25-server-default.toml').read_bytes();config.write_bytes(stock) + stock=(Path(__file__).parent/'fixtures/forge28-server-default.toml').read_bytes();config.write_bytes(stock) self.assertEqual(['serverconfig/forge-server.toml'],allowed_platform_additions({}, {'serverconfig/forge-server.toml':'new'},world)) config.write_bytes(stock+b'\nchanged=true\n') self.assertEqual([],allowed_platform_additions({}, {'serverconfig/forge-server.toml':'changed'},world)) config.write_bytes(stock) self.assertEqual([],allowed_platform_additions({'serverconfig/forge-server.toml':'old'}, {'serverconfig/forge-server.toml':'new'},world)) + def test_target_forge28_default_is_recognised(self): + with tempfile.TemporaryDirectory() as directory: + world=Path(directory);config=world/'serverconfig/forge-server.toml';config.parent.mkdir() + stock=(Path(__file__).parent/'fixtures/forge28-server-default.toml').read_bytes();config.write_bytes(stock) + self.assertEqual(['serverconfig/forge-server.toml'],allowed_platform_additions({}, {'serverconfig/forge-server.toml':'new'},world)) + + def test_new_stock_config_accepts_platform_order_and_format_only(self): + import tomllib + with tempfile.TemporaryDirectory() as directory: + world = Path(directory) + config = world / 'serverconfig/forge-server.toml' + config.parent.mkdir() + stock = tomllib.loads((Path(__file__).parent / 'fixtures/forge28-server-default.toml').read_text())['server'] + lines = ['# Linux may write these entries in a different order', '[server]'] + for name, value in reversed(list(stock.items())): + lines.append(name + ' = ' + (str(value).lower() if isinstance(value, bool) else str(value))) + config.write_text('\n'.join(lines) + '\n') + self.assertEqual(['serverconfig/forge-server.toml'], allowed_platform_additions( + {}, {'serverconfig/forge-server.toml': 'new'}, world)) + + def test_config_values_and_types_must_match_exactly(self): + import hashlib + with tempfile.TemporaryDirectory() as directory: + world = Path(directory) + config = world / 'serverconfig/forge-server.toml' + config.parent.mkdir() + stock = (Path(__file__).parent / 'fixtures/forge28-server-default.toml').read_bytes() + before = {'serverconfig/forge-server.toml': hashlib.sha256(stock).hexdigest().upper()} + after = {'serverconfig/forge-server.toml': 'changed'} + for old, new in [(b'clumpingThreshold = 64', b'clumpingThreshold = 65'), + (b'clumpingThreshold = 64', b'clumpingThreshold = 64.0'), + (b'fixAdvancementLoading = true', b'fixAdvancementLoading = 1'), + (b'dimensionUnloadQueueDelay = 0', b'dimensionUnloadQueueDelay = false')]: + with self.subTest(replacement=new): + config.write_bytes(stock.replace(old, new)) + self.assertEqual([], allowed_platform_additions({}, after, world)) + self.assertEqual([], allowed_platform_additions(before, after, world, stock)) + + def test_only_proven_native_defaults_can_be_added_to_an_existing_config(self): + import hashlib + with tempfile.TemporaryDirectory() as directory: + world=Path(directory);config=world/'serverconfig/forge-server.toml';config.parent.mkdir() + fixtures=Path(__file__).parent/'fixtures' + original=(fixtures/'forge25-server-default.toml').read_bytes() + target=(fixtures/'forge28-server-default.toml').read_bytes();config.write_bytes(target) + before={'serverconfig/forge-server.toml':hashlib.sha256(original).hexdigest().upper()} + after={'serverconfig/forge-server.toml':'changed'} + self.assertEqual(['serverconfig/forge-server.toml'],allowed_platform_additions(before,after,world,original)) + for changed in [target.replace(b'clumpingThreshold = 64',b'clumpingThreshold = 65'), + target+b'\nunknownOption = true\n']: + config.write_bytes(changed) + self.assertEqual([],allowed_platform_additions(before,after,world,original)) + def test_existing_journal_damaged_index(self): self.check_fixture('damaged-index', True) diff --git a/tools/verify_multiplayer.py b/tools/verify_multiplayer.py index 104512d..e454e6e 100644 --- a/tools/verify_multiplayer.py +++ b/tools/verify_multiplayer.py @@ -14,8 +14,8 @@ from release_identity import MAIN_JAR as MAIN -def client_command(project,java,distribution,root,cache,shared,port,role,hidden_agent=None): - prepare_client(root,project/'build/libs'/MAIN,project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar') +def client_command(project,java,distribution,root,cache,shared,port,role,hidden_agent=None,probe=None): + prepare_client(root,project/'build/libs'/MAIN,probe or project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar') (root/'config').mkdir();(root/'config/lootablebodies.cfg').write_text('options {\n B:local_texture=true\n}\n') extra=['-Dlootablebodies.multiplayer.role='+role,'-Dlootablebodies.multiplayer.sync='+str(shared), '-Dlootablebodies.multiplayer.port='+str(port), diff --git a/tools/verify_native_respawn_control.py b/tools/verify_native_respawn_control.py new file mode 100644 index 0000000..f17b14f --- /dev/null +++ b/tools/verify_native_respawn_control.py @@ -0,0 +1,72 @@ +"""Reproduce the pinned Forge 28 delayed-movement error without Lootable Bodies.""" +import argparse,json,socket,subprocess,time +from pathlib import Path +from audit_runtime_logs import audit +from native_runtime import prepare_server,prepare_client,client_command,project_assets,FORGE,digest + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + 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('--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) + prepare_client(root/'client',probe=probe) + for home in (root/'server',root/'client'): + if list((home/'mods').glob('*.jar'))!=[home/'mods'/probe.name]: + raise RuntimeError('Independent control must contain only its own verifier') + with socket.socket() as reservation: + reservation.bind(('127.0.0.1',0));port=reservation.getsockname()[1] + props=root/'server/server.properties' + props.write_text(props.read_text().replace('server-port=0','server-port='+str(port))+'view-distance=4\n') + extra=['-Dlootablebodies.control.deaths=true','-Dlootablebodies.control.port='+str(port), + '-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'], + 'client':client_command(args.java.resolve(),root/'client',args.forge_client_home.resolve(), + project_assets(project),username='ControlOnly',extra=extra)} + 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) + try: + start('server');deadline=time.monotonic()+120 + while 'FORGE_PLATFORM_CONTROL_READY' not in (root/'server.log').read_text(errors='replace'): + if processes['server'].poll() is not None or time.monotonic()>deadline:raise RuntimeError('Native control server did not start') + time.sleep(.2) + start('client');processes['client'].wait(timeout=240) + client=(root/'client.log').read_text(errors='replace') + if processes['client'].returncode or 'FORGE_PLATFORM_CONTROL_PASS' not in client or client.count('NATIVE_RESPAWN ')!=8: + raise RuntimeError('Native control did not complete eight delayed respawns') + processes['server'].stdin.write('stop\n');processes['server'].stdin.flush();processes['server'].wait(timeout=60) + if processes['server'].returncode:raise RuntimeError('Native control server exit failure') + finally: + for process in processes.values(): + if process.poll() is None:process.terminate() + for process in processes.values(): + try:process.wait(timeout=10) + except subprocess.TimeoutExpired:process.kill();process.wait() + for log in logs:log.close() + reports=[] + for name in ('server','client'): + if list((root/name).rglob('crash-reports/*.txt')):raise RuntimeError('Unexpected native control crash') + for path in (root/(name+'.log'),root/name/'logs/latest.log',root/name/'logs/debug.log'): + if not path.is_file():raise FileNotFoundError(path) + reports.append(audit(path,native=True)) + count=reports[0]['known_forge28_dead_player_movement_failures'] + if count<1 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, + '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) + + +if __name__=='__main__':main() diff --git a/tools/verify_packaged.py b/tools/verify_packaged.py index abfa7d7..0b6d1cd 100644 --- a/tools/verify_packaged.py +++ b/tools/verify_packaged.py @@ -1,4 +1,4 @@ -"""Verify the exact production jar in disposable native Forge 25 servers.""" +"""Verify the exact production jar in disposable native Forge 28 servers.""" import argparse import json from pathlib import Path diff --git a/tools/verify_packaged_client.py b/tools/verify_packaged_client.py index 296dc57..4ee6586 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 25.""" +"""Run the exact production jar and isolated verifier in official Forge 28.""" import argparse import hashlib import json diff --git a/tools/verify_release_setup.py b/tools/verify_release_setup.py index 7a3140f..60651e0 100644 --- a/tools/verify_release_setup.py +++ b/tools/verify_release_setup.py @@ -49,8 +49,8 @@ def run(name, tasks, expected=None, prepared=False): run("valid-prepared", ["verifyPreparedReleaseArtifacts", f"-PpreparedReleaseDir={bundle}"], prepared=True) for name, override, expected in [ - ("three-component-version", "mod_version=2.4.1", "Major.Minor.Bug.113021"), - ("wrong-target", "mod_version=2.4.4.112021", "Major.Minor.Bug.113021"), + ("three-component-version", "mod_version=2.4.1", "Major.Minor.Bug.114041"), + ("wrong-target", "mod_version=2.4.4.112021", "Major.Minor.Bug.114041"), ("wrong-group", "mod_group=zone.moddev.mc.orespawn", "Release identity mismatch: mod_group"), ("wrong-artifact", "mod_artifact=OreSpawn", "Release identity mismatch: mod_artifact"), ("wrong-mod-id", "mod_id=orespawn", "Release identity mismatch: mod_id"), @@ -58,7 +58,7 @@ def run(name, tasks, expected=None, prepared=False): ("wrong-loader", "loader_name=neoforge", "Release identity mismatch: loader_name"), ("wrong-license", "mod_license=CC0-1.0", "Release identity mismatch: mod_license"), ("wrong-java", "java_version=17", "Release identity mismatch: java_version"), - ("stale-java-version", "mod_version=2.4.5.113021", "Java release version or persistent identity differs"), + ("stale-java-version", "mod_version=2.4.6.114041", "Java release version or persistent identity differs"), ]: run(name, ["verifyReleaseConfiguration", "-P" + override], expected) diff --git a/tools/verify_upgrade_abort.py b/tools/verify_upgrade_abort.py index beeed62..4d0a159 100644 --- a/tools/verify_upgrade_abort.py +++ b/tools/verify_upgrade_abort.py @@ -4,6 +4,7 @@ from pathlib import Path import shutil import subprocess +import tomllib from native_runtime import prepare_server as prepare, fingerprint, digest, FORGE from release_identity import MAIN_JAR @@ -13,15 +14,45 @@ def source_files(world): if not name.startswith('data/lootablebodies-upgrade')} -def allowed_platform_additions(before, after, world): +def same_typed_values(expected, actual): + if type(expected) is not type(actual): + return False + if isinstance(expected, dict): + return expected.keys() == actual.keys() and all( + same_typed_values(value, actual[name]) for name, value in expected.items()) + if isinstance(expected, list): + return len(expected) == len(actual) and all( + same_typed_values(left, right) for left, right in zip(expected, actual)) + return expected == actual + + +def allowed_platform_additions(before, after, world, original_config=None): # Forge writes its stock server config before the about-to-start event. - # Only an exact, newly created default is allowed; existing config is data. + # Require every default value and its type; Forge's entry order varies by JVM. name = 'serverconfig/forge-server.toml' - if name in before or name not in after: + if name not in after: return [] - expected = (Path(__file__).parent / 'fixtures/forge25-server-default.toml').read_bytes() + expected = (Path(__file__).parent / 'fixtures/forge28-server-default.toml').read_bytes() actual = (world / name).read_bytes() - return [name] if actual.replace(b'\r\n', b'\n') == expected.replace(b'\r\n', b'\n') else [] + if name in before: + if original_config is None or before[name] != __import__('hashlib').sha256(original_config).hexdigest().upper(): + return [] + # The pinned Forge itself upgrades its own config before our preflight. + # Require proof that every existing value survives and only its two new + # target defaults are added. Original bytes remain in the sealed source. + try: + previous = tomllib.loads(original_config.decode('utf-8')) + upgraded = tomllib.loads(actual.decode('utf-8')) + previous['server'].setdefault('treatEmptyTagsAsAir', False) + previous['server'].setdefault('fixAdvancementLoading', True) + return [name] if same_typed_values(previous, upgraded) else [] + except (ValueError, KeyError, UnicodeError): + return [] + try: + return [name] if same_typed_values(tomllib.loads(expected.decode('utf-8')), + tomllib.loads(actual.decode('utf-8'))) else [] + except (ValueError, UnicodeError): + return [] def inject_unsafe_journal(world, case): @@ -68,6 +99,8 @@ def main(): world = home / 'world' inject_unsafe_journal(world, case) before = source_files(world) + config = world/'serverconfig/forge-server.toml' + original_config = config.read_bytes() if config.is_file() else None log = home / 'abort.log' with log.open('w', encoding='utf-8') as output: result = subprocess.run([str(args.java.resolve()), '-Xmx1G', '-jar', @@ -80,14 +113,29 @@ def main(): 'LOOTABLE_BODIES_RUNTIME_PASS', 'Done (']): raise RuntimeError('World loading continued after unsafe preservation') after = source_files(world) - platform_additions = allowed_platform_additions(before, after, world) + raw_after = dict(after) + platform_additions = allowed_platform_additions(before, after, world, original_config) for name in platform_additions: - del after[name] - if before != after or fingerprint(source) != sealed: - raise RuntimeError('A failed preflight changed source world data') + if name in before: after[name] = before[name] + else: del after[name] + source_after = fingerprint(source) + comparison = { + 'before': before, 'after': raw_after, + 'allowed_stock_config_files': platform_additions, + 'world_added': sorted(set(after) - set(before)), + 'world_removed': sorted(set(before) - set(after)), + 'world_changed': sorted(name for name in set(before) & set(after) if before[name] != after[name]), + 'sealed_source_unchanged': source_after == sealed, + 'original_forge_config': original_config.decode('utf-8', errors='replace') if original_config else None, + 'resulting_forge_config': config.read_text(encoding='utf-8') if config.is_file() else None, + } + (home / 'comparison.json').write_text(json.dumps(comparison, indent=2) + '\n', encoding='utf-8') + if before != after or source_after != sealed: + raise RuntimeError('A failed preflight changed source world data: ' + str(home / 'comparison.json')) report['cases'][case] = {'exit': result.returncode, 'source_unchanged': True, 'world_data_unchanged': True, 'world_loading_started': False, - 'stock_forge_config_created': platform_additions} + 'stock_forge_config_created': [name for name in platform_additions if name not in before], + 'stock_forge_config_updated': [name for name in platform_additions if name in before]} print('UPGRADE_ABORT_PASS', case, flush=True) (root / 'result.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8') diff --git a/tools/verify_visibility.py b/tools/verify_visibility.py index acbfa25..f7fb941 100644 --- a/tools/verify_visibility.py +++ b/tools/verify_visibility.py @@ -21,6 +21,7 @@ def main(): parser.add_argument('--output',type=Path,required=True) parser.add_argument('--mode',choices=['visibility','respawn'],required=True) parser.add_argument('--source-server',type=Path) + parser.add_argument('--probe-jar',type=Path,help='Isolated verifier jar; production jar is unchanged') parser.add_argument('--cases',type=int,default=16) parser.add_argument('--simultaneous',action='store_true') parser.add_argument('--drop-spawn',action='store_true',help='Destructive negative control: discards every owner corpse spawn; expected to fail') @@ -30,7 +31,7 @@ def main(): java=args.java.resolve(); distribution=args.forge_home.resolve(); home=args.output.resolve() home.mkdir(parents=True,exist_ok=False); shared=home/'sync'; shared.mkdir() jar=project/'build/libs'/MAIN; before=digest(jar) - probe=project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar' + probe=(args.probe_jar or project/'build/integration-test-mod/lootablebodies-build-tests-reobf.jar').resolve() prepare(home/'server',distribution,jar,probe) source_before=None if args.source_server: @@ -47,7 +48,7 @@ def main(): 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) + command=client_command(project,java,args.forge_client_home.resolve(),home/('client'+role),None,shared,port,role,args.hidden_agent,probe) 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') diff --git a/tools/verify_world_upgrades.py b/tools/verify_world_upgrades.py index e33a81b..7e82c79 100644 --- a/tools/verify_world_upgrades.py +++ b/tools/verify_world_upgrades.py @@ -18,6 +18,11 @@ 'recovery-110': ('1.10.2', '2.4.1.110021', '7F9F0EE7BF09F943ED85398982EB7C2207F3DD55B75E9E92A7531C393BE1045B'), } ORIGINS.update({ + 'via113-110': ('1.10.2', '2.2.0', '688AFBBF4485D1F3746C68903C9E0885D57F42CCFBFA0CB4A0FC42982C0B3F2B'), + 'via113-111': ('1.11.2', '2.4.2.111021', '59ACF5652505F08A7F011307B3CD68F3754FCFC14C4C330BD952475351AEC932'), + 'via113-112': ('1.12.2', '2.4.3.112021', 'A9F4B115828834F60C5905526A8A99794B1DFE4802AB4D48858F2D597F3FCBCA'), + 'released-113': ('1.13.2', '2.4.4.113021', '464CC1CDDA6C04CD72F5F1A2A504EB8A665E6E48AC255918437262DB2A22A4C1'), + 'recovery-113': ('1.13.2', '2.4.4.113021', '464CC1CDDA6C04CD72F5F1A2A504EB8A665E6E48AC255918437262DB2A22A4C1'), 'via112-110': ('1.10.2','2.2.0','688AFBBF4485D1F3746C68903C9E0885D57F42CCFBFA0CB4A0FC42982C0B3F2B'), 'via112-111': ('1.11.2','2.4.2.111021','59ACF5652505F08A7F011307B3CD68F3754FCFC14C4C330BD952475351AEC932'), 'released-112': ('1.12.2','2.4.3.112021','A9F4B115828834F60C5905526A8A99794B1DFE4802AB4D48858F2D597F3FCBCA'), @@ -74,6 +79,25 @@ def seed(java, home, minecraft, version, recovery=False): return fingerprint(home / 'world') +def seed_native113(java, home, recovery=False): + from prepare_native113_tools import historical_runtime + runtime = historical_runtime(Path(__file__).resolve().parents[1]) + with (home/'seed.log').open('w', encoding='utf-8') as log: + result = subprocess.run([str(java), '-Xmx2G', + '-Dlootablebodies.fixture.version=2.4.4.113021', + '-Dlootablebodies.fixture.recovery='+str(recovery).lower(), + '-jar', runtime.FORGE+'.jar', '--nogui'], cwd=home, + stdout=log, stderr=subprocess.STDOUT, timeout=240) + expected = 'LOOTABLE_BODIES_LEGACY_SEED_PASS minecraft=1.13.2 mod=2.4.4.113021 corpses=9 dimensions=3' + if result.returncode or expected not in (home/'seed.log').read_text(errors='replace') or not (home/'world/legacy-seed.pass').is_file(): + raise RuntimeError('Released 1.13 writer failed: '+str(home/'seed.log')) + if list((home/'crash-reports').glob('*')): + raise RuntimeError('Released 1.13 writer crashed') + for path in [home/'seed.log', home/'logs/latest.log', home/'logs/debug.log']: + audit(path, native=True) + return fingerprint(home/'world') + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--java', type=Path, required=True) @@ -81,6 +105,7 @@ def main(): parser.add_argument('--forge111-home', type=Path, required=True) parser.add_argument('--forge112-home', type=Path, required=True) parser.add_argument('--forge113-home', type=Path) + parser.add_argument('--forge114-home', type=Path) for origin in ORIGINS: if origin.startswith('recovery-') or origin.startswith('via'): continue @@ -103,6 +128,22 @@ def main(): for origin, (minecraft, version, expected) in ORIGINS.items(): if origin not in args.origins: continue + if origin.startswith('via113-'): + from prepare_native113_tools import historical_runtime + original = args.original_110 if origin.endswith('110') else args.released_111 if origin.endswith('111') else args.released_112 + if digest(original.resolve()) != expected: + raise RuntimeError('Incorrect historical source jar for '+origin) + source_home = root/(origin+'-source') + distribution = {'1.10.2':args.forge110_home,'1.11.2':args.forge111_home,'1.12.2':args.forge112_home}[minecraft] + prepare(source_home, distribution.resolve(), minecraft, original.resolve(), seeder) + seed(args.java.resolve(), source_home, minecraft, version) + details = transition113(args.java.resolve(), project, source_home/'world', + fingerprint(source_home/'world'), root/origin, args.forge113_home, + args.released_113.resolve(), origin, minecraft, version, expected) + report['origins'][origin] = details + (root/'seed-report.json').write_text(json.dumps(report, indent=2)+'\n', encoding='utf-8') + print('LEGACY_WORLD_SEALED', origin, len(details['world']), 'files', flush=True) + continue if origin.startswith('via112-'): details=seed_transition112(args,project,root,seeder,origin) report['origins'][origin]=details @@ -118,6 +159,21 @@ def main(): source = getattr(args, origin.replace('recovery-', 'released-').replace('-', '_')).resolve() if digest(source) != expected: raise RuntimeError('Historical jar checksum mismatch: ' + origin) + if minecraft == '1.13.2': + from prepare_native113_tools import historical_runtime + runtime = historical_runtime(project) + if not args.forge113_home: + raise ValueError('--forge113-home is required for the released 1.13 writer') + home = root/origin + native_seeder = project/'build/native113-seed/lootablebodies-native113-world-fixtures-reobf.jar' + runtime.prepare_server(home, args.forge113_home.resolve(), source, native_seeder) + hashes = seed_native113(args.java.resolve(), home, origin.startswith('recovery-')) + report['origins'][origin] = {'minecraft': minecraft, 'mod_version': version, + 'jar_sha256': expected, 'seeder_sha256': digest(native_seeder), + 'writer_source_commit': '8219190bab22600475542d20ed5df74fea15c27b', 'world': hashes} + (root/'seed-report.json').write_text(json.dumps(report, indent=2)+'\n', encoding='utf-8') + print('LEGACY_WORLD_SEALED', origin, len(hashes), 'files', flush=True) + continue distribution = {'1.10.2':args.forge110_home,'1.11.2':args.forge111_home,'1.12.2':args.forge112_home}[minecraft] home = root / origin prepare(home, distribution.resolve(), minecraft, source, seeder) @@ -154,6 +210,33 @@ def seed_transition(args,project,root,seeder): 'source_world_unchanged':True,'distant_regions_unchanged':distant,'world':fingerprint(target/'world')} +def transition113(java, project, source, sealed, target, distribution, released, origin, + minecraft, version, expected): + """Use the released 1.13 writer on a copy, leaving six distant regions old.""" + from prepare_native113_tools import historical_runtime, SOURCE + runtime = historical_runtime(project) + record = json.loads((project/'.gradle/legacy-tools/released-113/tools.json').read_text()) + probe = project/'.gradle/legacy-tools/released-113/source/build'/record['probe'] + if record['source'] != SOURCE or digest(probe) != record['probe_sha256'] or digest(released) != ORIGINS['released-113'][2]: + raise RuntimeError('Released 1.13 transition tool or artifact changed') + if fingerprint(source) != sealed or not distribution: + raise RuntimeError('Missing or changed historical transition source') + runtime.prepare_server(target, distribution.resolve(), released, probe) + shutil.copytree(source, target/'world') + world = target/'world' + shutil.copy2(next((world/'playerdata').glob('*.dat')), world/'upgrade-player-source.nbt') + distant = {str(p.relative_to(world)):digest(p) for p in world.rglob('region/r.*.mca') + if p.name in ('r.8.8.mca','r.16.16.mca')} + runtime.run_server(java, target, 'upgrade-near', + message='LOOTABLE_BODIES_WORLD_UPGRADE_PASS phase=upgrade-near') + if fingerprint(source) != sealed or len(distant) != 6 or any(digest(world/p) != sha for p,sha in distant.items()): + raise RuntimeError('Released 1.13 transition changed its source or unopened regions') + return {'minecraft':minecraft+' via 1.13.2','mod_version':version+' via 2.4.4.113021', + 'jar_sha256':expected,'intermediate_jar_sha256':digest(released), + 'transition_probe_sha256':digest(probe),'source_world_unchanged':True, + 'distant_regions_unchanged':distant,'world':fingerprint(world)} + + def seed_transition112(args,project,root,seeder,origin): original=args.original_110.resolve() if origin=='via112-110' else args.released_111.resolve() minecraft,version,expected=ORIGINS[origin];released=args.released_112.resolve() @@ -236,11 +319,25 @@ def patch_stamps(world, disposable): return changes +def preserve_historical_markers(home): + home=home.resolve();world=home/'world' + markers=[world/(phase+'.pass') for phase in PHASES if (world/(phase+'.pass')).exists()] + if not markers:return [] + archive=home/'historical-run-markers' + if archive.exists():raise RuntimeError('Historical runner evidence already exists') + for marker in markers: + if marker.is_symlink() or not marker.is_file() or not marker.resolve().is_relative_to(world): + raise RuntimeError('Historical marker is outside the disposable world') + archive.mkdir() + for marker in markers:marker.rename(archive/marker.name) + return [marker.name for marker in markers] + + def verify(args, project): if not args.seeds: raise ValueError('--seeds is required for the upgrade matrix') - if not args.forge113_home: - raise ValueError('--forge113-home is required for the native upgrade matrix') + if not args.forge114_home: + raise ValueError('--forge114-home is required for the native upgrade matrix') root = args.output.resolve() root.mkdir(parents=True, exist_ok=False) candidate = project / 'build/libs' / MAIN_JAR @@ -269,9 +366,10 @@ def verify(args, project): name = origin + '-' + variant home = root / name from native_runtime import prepare_server - prepare_server(home,args.forge113_home.resolve(),candidate,probe) + prepare_server(home,args.forge114_home.resolve(),candidate,probe) shutil.copytree(source, home / 'world') world = home / 'world' + historical_markers=preserve_historical_markers(home) player_files = list((world / 'playerdata').glob('*.dat')) if len(player_files) != 1: raise RuntimeError('Expected exactly one sealed fixture player') @@ -294,7 +392,7 @@ def verify(args, project): 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, 3): + if entity.value['id'].value != 'lootablebodies:corpse' or entity.value.get('LootableBodiesDataVersion') != Tag(3, 4): raise RuntimeError('A loaded corpse was not actually saved in the new format') persisted += 1 if persisted < (3 if wave >= 0 else 8): @@ -308,7 +406,8 @@ def verify(args, project): if fingerprint(source) != details['world'] or digest(candidate) != candidate_hash or digest(probe) != probe_hash: raise RuntimeError('Sealed source/candidate changed') report['cases'][name] = {'phases': phases, 'distant_regions': distant, 'stamp_mutations': changes, - 'source_unchanged': True, 'origin_jar_sha256': details['jar_sha256']} + 'source_unchanged': True, 'origin_jar_sha256': details['jar_sha256'], + 'preserved_historical_markers':historical_markers} (root / 'result.json').write_text(json.dumps(report, indent=2) + '\n', encoding='utf-8') print('WORLD_UPGRADE_PASS', name, phase, flush=True) print('WORLD_UPGRADE_MATRIX_PASS cases=' + str(len(report['cases'])), flush=True)