From 36718c4f0294efe481b0e60bc1d6e80f47316cc0 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 22:41:44 +0000 Subject: [PATCH 1/2] Update Lootable Bodies for Forge 26.1.2 --- .github/release120.json | 18 +-- .github/scripts/test_release_workflow.py | 25 +++- .github/workflows/artifact-audit-26.1.yml | 100 ++++++++++++++++ .github/workflows/codeql-analysis.yml | 14 +-- .github/workflows/deploy-release.yml | 56 +++++---- .github/workflows/native120.yml | 14 +-- .github/workflows/release-workflow-checks.yml | 6 +- CHANGELOG.txt | 4 +- README.md | 4 +- RELEASING.md | 2 +- TESTING.md | 11 +- build.gradle | 57 +++------ gradle.properties | 14 +-- gradle/assets.gradle | 10 +- gradle/audit.gradle | 13 +- gradle/ide.gradle | 12 +- gradle/release.gradle | 18 ++- gradle/runtime.gradle | 14 ++- settings.gradle | 2 +- .../java/buildcontrol/ForgeControl.java | 7 +- .../java/buildcontrol/ResourceControl.java | 12 +- src/controlTest/resources/META-INF/mods.toml | 2 +- src/controlTest/resources/pack.mcmeta | 6 +- .../lootable/buildtest/ClientChecks.java | 4 +- .../buildtest/ConfigClientChecks.java | 5 +- .../buildtest/ConnectedClientChecks.java | 10 +- .../buildtest/ContainerInteractionChecks.java | 74 ++++++------ .../lootable/buildtest/DropClientChecks.java | 29 +++-- .../cyano/lootable/buildtest/FixtureApi.java | 24 ++-- .../lootable/buildtest/FixtureStackData.java | 3 +- .../lootable/buildtest/MultiplayerChecks.java | 22 +++- .../buildtest/MultiplayerClientChecks.java | 14 +-- .../buildtest/NamelessCorpseClientChecks.java | 2 +- .../lootable/buildtest/NbtClientChecks.java | 6 +- .../buildtest/PositionMotionChecks.java | 2 +- .../buildtest/ReportedIssueChecks.java | 12 +- .../buildtest/ReportedIssueClientChecks.java | 10 +- .../buildtest/RespawnPacketClientChecks.java | 2 +- .../buildtest/RespawnPacketTrace.java | 2 +- .../buildtest/RotationFallChecks.java | 2 +- .../lootable/buildtest/RuntimeChecks.java | 14 +-- .../lootable/buildtest/VisibilityChecks.java | 6 +- .../buildtest/VisibilityClientChecks.java | 6 +- .../buildtest/VisibilitySkinClientChecks.java | 61 ++++++++-- .../buildtest/mixin/ProbeFrameMixin.java | 2 +- .../resources/META-INF/mods.toml | 4 +- src/integrationTest/resources/pack.mcmeta | 6 +- src/main/java/cyano/lootable/ClientProxy.java | 2 - .../java/cyano/lootable/LootableBodies.java | 8 +- .../lootable/entities/EntityLootableBody.java | 8 +- .../events/PlayerDeathEventHandler.java | 4 +- .../graphics/ClientConfigChanges.java | 2 - .../lootable/graphics/CorpseConfigGui.java | 16 ++- .../graphics/CorpseConfigGuiFactory.java | 2 - .../lootable/graphics/CorpseContainer.java | 30 ++--- .../lootable/graphics/CorpseGUIContainer.java | 14 +-- .../lootable/graphics/CorpseRenderer.java | 23 ++-- .../cyano/lootable/graphics/GUIHandler.java | 2 - .../lootable/mixin/CapabilityDataMixin.java | 9 +- .../lootable/mixin/DedicatedWorldMixin.java | 23 +++- .../lootable/mixin/DimensionStorageMixin.java | 31 +++++ .../lootable/mixin/FileFixLevelDataMixin.java | 27 +++++ .../cyano/lootable/mixin/FileFixerMixin.java | 63 ++++++++++ .../lootable/mixin/LegacyStructuresMixin.java | 12 -- .../lootable/mixin/RegionStorageMixin.java | 20 +--- .../cyano/lootable/mixin/SaveAccessMixin.java | 16 ++- .../network/ClientLoginReadGuard.java | 3 - .../upgrade/EarlyWorldPreservation.java | 100 ++++++++++++++-- .../cyano/lootable/upgrade/ItemMigration.java | 20 +++- .../upgrade/LegacyRegistryMappings.java | 5 +- .../lootable/upgrade/NativeSafeFixer.java | 4 +- .../lootable/upgrade/UpgradeJournal.java | 46 ++++++- .../lootable/upgrade/UpgradePreservation.java | 19 ++- src/main/resources/META-INF/mods.toml | 6 +- src/main/resources/lootablebodies.mixins.json | 2 +- src/main/resources/pack.mcmeta | 6 +- .../buildtest/Native117EntityInboxTest.java | 31 ++--- .../buildtest/Native117MenuProtocolTest.java | 6 +- .../Native117ProbeIsolationTest.java | 2 +- .../buildtest/Native117TestEnvironment.java | 30 +++-- .../lootable/contracts/BuildContractTest.java | 6 +- .../lootable/contracts/ConfigScreenTest.java | 2 +- .../contracts/NativeUpgradeBoundaryTest.java | 2 +- .../contracts/ReleaseContractTest.java | 4 +- .../upgrade/Native114UpgradeTest.java | 8 +- .../upgrade/Native115UpgradeTest.java | 6 +- .../upgrade/Native116DamagedStampTest.java | 4 +- .../upgrade/Native116UpgradeTest.java | 2 +- .../upgrade/Native117EntityChunkTest.java | 2 +- .../upgrade/Native118EarlyHookTest.java | 9 +- .../upgrade/Native118EntityBoundaryTest.java | 8 +- .../upgrade/Native118TerrainBoundaryTest.java | 6 +- .../upgrade/Native119CoremodEngineTest.java | 24 ++-- .../upgrade/Native1206ComponentsTest.java | 8 +- .../lootable/upgrade/Native120TargetTest.java | 22 ++-- .../upgrade/Native2612ItemBridgeTest.java | 37 ++++++ tools/fixtures/native2612-flat-level.dat | Bin 0 -> 42656 bytes tools/fixtures/native2612-flat-worldgen.dat | Bin 0 -> 333 bytes .../native2612-forge-only-flat-level.dat | Bin 0 -> 42558 bytes .../native2612-forge-only-flat-worldgen.dat | Bin 0 -> 333 bytes tools/hosted_policy.py | 14 +-- tools/native120_build.py | 2 +- tools/native120_client.py | 16 +-- tools/native120_client_logs.py | 6 +- tools/native120_logs.py | 4 +- tools/native120_persistence.py | 12 +- tools/native2612_flat_inputs.json | 12 ++ tools/native2612_server_inputs.json | 102 ++++++++++++++++ tools/native2612_worker_budget.json | 37 ++++++ tools/native_evidence.py | 12 +- tools/native_runtime.py | 112 +++++++++++------- tools/qualification-policy.json | 14 +-- tools/test_hosted_policy.py | 10 +- tools/test_native120_build.py | 2 +- tools/test_native120_client_contracts.py | 10 +- tools/test_native120_client_logs.py | 10 +- tools/test_native120_flat_header.py | 38 ++++-- tools/test_native120_infrastructure.py | 18 +-- tools/test_native120_launch.py | 22 ++-- tools/test_native120_persistence.py | 10 +- tools/test_native_evidence.py | 4 +- tools/test_native_runtime.py | 4 +- tools/test_native_server_preparation.py | 2 +- tools/test_ordinary_server_setup.py | 4 +- tools/test_owned_process.py | 19 ++- tools/test_runtime_diagnostics.py | 34 +++--- tools/verify_native_respawn_control.py | 8 +- tools/verify_normal_server.py | 4 +- tools/verify_packaged.py | 4 +- tools/verify_upgrade_abort.py | 12 +- tools/verify_world_upgrades.py | 27 ++++- 131 files changed, 1353 insertions(+), 717 deletions(-) create mode 100644 .github/workflows/artifact-audit-26.1.yml create mode 100644 src/main/java/cyano/lootable/mixin/DimensionStorageMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/FileFixLevelDataMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/FileFixerMixin.java delete mode 100644 src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java create mode 100644 src/test/java/cyano/lootable/upgrade/Native2612ItemBridgeTest.java create mode 100644 tools/fixtures/native2612-flat-level.dat create mode 100644 tools/fixtures/native2612-flat-worldgen.dat create mode 100644 tools/fixtures/native2612-forge-only-flat-level.dat create mode 100644 tools/fixtures/native2612-forge-only-flat-worldgen.dat create mode 100644 tools/native2612_flat_inputs.json create mode 100644 tools/native2612_server_inputs.json create mode 100644 tools/native2612_worker_budget.json diff --git a/.github/release120.json b/.github/release120.json index 8b62032..df47089 100644 --- a/.github/release120.json +++ b/.github/release120.json @@ -1,16 +1,16 @@ { - "version": "2.4.14.121111", - "minecraft": "1.21.11", - "forge": "61.2.4", + "version": "2.4.15.2601021", + "minecraft": "26.1.2", + "forge": "64.1.5", "local_qualification_complete": true, "qualification_integrity_status": "verified", "manual_accepted": true, "files": { - "CHANGELOG.txt": "2FE5E10AF26A39BB3684983EEFEF739F3C25E847BC543F08D9D46B6758EA391D", - "LootableBodies-2.4.14.121111-javadoc.jar": "B9D9C52A62928073028DD695549AE39B8CACAF256E7428FBDA773EE3E7309462", - "LootableBodies-2.4.14.121111-sources.jar": "C23B401E5AA952F59AB55B34E4A23B9F2A19289D961BFCE6E104E6A505C91902", - "LootableBodies-2.4.14.121111.jar": "273FF5DA71743CFA883B036DD82B9C6D5F85CB8302791D71BD5384674A22CC71", - "LootableBodies-2.4.14.121111.pom": "D94861900E3523B689DDA074350DE75DF634BAA2D101426EE03CF19EA2570B5D", - "SHA256SUMS": "2240EB872764D99467D4217E4F49B3D720137BF68032784B43FFB2AD8593BEFF" + "CHANGELOG.txt": "D0183E5D681EC7B6F6A2C0724574356A0E29C8F7026B0DA336DFD56391288608", + "LootableBodies-2.4.15.2601021-javadoc.jar": "1DDB03654C943D5AABFE2798E2DA4A97A7232871FD3CA48047733F42058AD7D8", + "LootableBodies-2.4.15.2601021-sources.jar": "02EF4CB3DAFABEB55707A2079586AD3B33D00B164BD8DACFA6E5F01F6291D594", + "LootableBodies-2.4.15.2601021.jar": "2836DBCF55E11043C3CFAF871CFAB3A8F1B66E715DBAF9C83215687F97551CA8", + "LootableBodies-2.4.15.2601021.pom": "1B7EBF95AEEBA457347029D957AD47D8C2A478557310E67BC77C366C4F15AA1D", + "SHA256SUMS": "E00D677544174570C2BC8163F09BD1A22794166EEB004EF5322B758E719D4DE9" } } diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 08ee8a3..a79d9f2 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -74,13 +74,14 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.20.1': ('120011', 'master-1.20.1', '47.4.28'), '1.20.6': ('120061', 'master-1.20.6', '50.2.14'), '1.21.1': ('121011', 'master-1.21.1', '52.1.20'), - '1.21.11': ('121111', 'master-1.21.11', '61.2.4')}[minecraft] + '1.21.11': ('121111', 'master-1.21.11', '61.2.4'), + '26.1.2': ('2601021', 'master-26.1.2', '64.1.5')}[minecraft] version = '2.4.2.' + suffix files={name:'C'*64 for name in ['main.jar','sources.jar','javadoc.jar','release.pom','CHANGELOG.txt','SHA256SUMS']} receipt={'schema':1,'source':SHA,'version':version,'files':files,'evidence_sha256':'F'*64,'qualified_locally':True,'manual_accepted':True} if local_receipt is not None:receipt=local_receipt values = dict(minecraft_version=minecraft, forge_version=forge, loader_name='forge', loader_code='1', - java_version='21' if minecraft in ('1.20.6','1.21.1','1.21.11') else '17' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='21.0.7+6' if minecraft in ('1.20.6','1.21.1','1.21.11') else '17.0.20.1+1' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='21' if minecraft in ('1.20.6','1.21.1','1.21.11') else '17', mod_id='lootablebodies', + java_version='25' if minecraft=='26.1.2' else '21' if minecraft in ('1.20.6','1.21.1','1.21.11') else '17' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='25.0.3+9' if minecraft=='26.1.2' else '21.0.7+6' if minecraft in ('1.20.6','1.21.1','1.21.11') else '17.0.20.1+1' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='25' if minecraft=='26.1.2' else '21' if minecraft in ('1.20.6','1.21.1','1.21.11') else '17', mod_id='lootablebodies', mod_artifact='LootableBodies', mod_group='zone.moddev.mc.lootablebodies', mod_license='LGPL-2.1-only', curseforge_project_id='240968', mod_version=version) if mutation: @@ -90,13 +91,13 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.14.4': MATRIX114 + ['Validate release workflow'], '1.15.2': MATRIX115, '1.16.5': MATRIX116}.get(minecraft, []) if names is None else names if minecraft=='1.17.1' and names is None:checks=CORE117+MATRIX117 - if minecraft in ('1.18.2','1.19.4','1.20.1','1.20.6','1.21.1','1.21.11') and names is None:checks=CORE118 + if minecraft in ('1.18.2','1.19.4','1.20.1','1.20.6','1.21.1','1.21.11','26.1.2') and names is None:checks=CORE118 def command(args, **options): if args[0] == 'git': return SHA + '\n' if any('/actions/workflows/native'+code+'.yml/' in args[-1] for code in ['117','118','119','120']): - code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120','1.21.1':'120','1.21.11':'120'}[minecraft] + code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120','1.21.1':'120','1.21.11':'120','26.1.2':'120'}[minecraft] row=dict(id=int(code),head_sha=SHA,head_branch=branch,event='push',path='.github/workflows/native'+code+'.yml',conclusion='success') if qualification:row.update(qualification) return json.dumps({'workflow_runs':[row]}) @@ -104,7 +105,7 @@ def command(args, **options): audit = {'id': 1, 'display_title': 'Audit ' + minecraft + ' artifacts from ' + SHA, 'conclusion': 'success' if audit_success else 'failure', 'event': 'workflow_run', 'head_sha': 'c' * 40, 'head_branch': 'master-1.12', - 'path': '.github/workflows/artifact-audit-1.21.yml'} + 'path': '.github/workflows/artifact-audit-26.1.yml' if minecraft=='26.1.2' else '.github/workflows/artifact-audit-1.21.yml'} if audit_run is not None: audit.update(audit_run) return json.dumps({'workflow_runs': [audit]}) @@ -209,6 +210,20 @@ def test_12111_requires_the_exact_archive_and_dispatcher(self): self.validate('1.21.11', **options) self.assertNotEqual(0, self.select('2.4.15.261021')[0]) + def test_2612_uses_java25_exact_archives_and_current_dispatcher(self): + self.assertEqual((0, {'minecraft_version':'26.1.2','source_branch':'master-26.1.2'}), self.select('2.4.15.2601021')) + self.validate('26.1.2', audit_run={'event':'workflow_dispatch'}) + for options in ({'mutation':{'java_version':'21'}}, {'mutation':{'java_toolchain_version':'25.0.2+10'}}, + {'audit_success':False}, {'qualification':{'head_branch':'master-1.21.11'}}, + {'local_receipt':{'manual_accepted':False}}, {'audit_run':{'head_sha':'d'*40}}, + {'audit_run':{'path':'.github/workflows/artifact-audit-1.21.yml'}}): + with self.subTest(options=options), self.assertRaises(SystemExit):self.validate('26.1.2', **options) + workflow=WORKFLOW.read_text() + self.assertIn("needs.preflight.outputs.minecraft_version == '26.1.2' && 'Java 25'",workflow) + for line in workflow.splitlines(): + if 'if: needs.preflight.outputs.minecraft_version !=' in line: + self.assertIn("!= '26.1.2'",line) + def test_routes_all_supported_versions(self): for suffix, minecraft, branch in [('110021', '1.10.2', '1.10.2'), ('111021', '1.11.2', '1.11.2-dev'), ('112021', '1.12.2', 'master-1.12'), ('113021', '1.13.2', 'master-1.13.2'), ('114041', '1.14.4', 'master-1.14.4')]: with self.subTest(minecraft=minecraft): diff --git a/.github/workflows/artifact-audit-26.1.yml b/.github/workflows/artifact-audit-26.1.yml new file mode 100644 index 0000000..4be2b91 --- /dev/null +++ b/.github/workflows/artifact-audit-26.1.yml @@ -0,0 +1,100 @@ +name: Audit LootableBodies 26.1 artifacts +run-name: Audit 26.1.2 artifacts from ${{ github.event.workflow_run.head_sha || inputs.source_sha }} +on: + workflow_run: + workflows: [LootableBodies 1.20 CI, LootableBodies 26.1.2 CI] + types: [completed] + workflow_dispatch: + inputs: + minecraft_version: + description: Minecraft version of the merged archive build + type: choice + default: '26.1.2' + options: ['26.1.2'] + source_run: + description: Successful archive build run ID + required: true + type: string + source_sha: + description: Exact merged source SHA + required: true + type: string +permissions: + contents: read + actions: read +jobs: + audit: + if: >- + github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && + github.ref == 'refs/heads/master-1.12' && + (github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && + (github.event.workflow_run.head_branch == 'master-26.1.2' && github.event.workflow_run.name == 'LootableBodies 26.1.2 CI'))) + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Verify the exact merged archive build + id: source + env: + GH_TOKEN: ${{ github.token }} + TARGET_VERSION: ${{ inputs.minecraft_version || '26.1.2' }} + SOURCE_RUN: ${{ github.event.workflow_run.id || inputs.source_run }} + SOURCE_SHA: ${{ github.event.workflow_run.head_sha || inputs.source_sha }} + run: | + python3 - <<'PY' + import json, os, re, subprocess + + repo = 'MinecraftModDevelopmentMods/LootableBodies' + run_id = os.environ['SOURCE_RUN'] + source = os.environ['SOURCE_SHA'] + if not re.fullmatch(r'[1-9][0-9]*', run_id) or not re.fullmatch(r'[a-f0-9]{40}', source): + raise SystemExit('An exact build run ID and source SHA are required') + + def api(path): + return json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/{path}'])) + + run = api(f'actions/runs/{run_id}') + target = os.environ.get('TARGET_VERSION', '26.1.2') + if target not in ('26.1.2',): + raise SystemExit('Unsupported archive audit target') + source_branch = 'master-' + target + branch = api('git/ref/heads/' + source_branch) + if (str(run['id']) != run_id or run['head_repository']['full_name'] != repo or + run['event'] != 'push' or run['head_branch'] != source_branch or run['head_sha'] != source or + run['path'] != '.github/workflows/native120.yml' or run['status'] != 'completed' or + run['conclusion'] != 'success' or branch['object']['sha'] != source): + raise SystemExit('The selected run is not the successful build of the current merged source') + with open(os.environ['GITHUB_OUTPUT'], 'a') as out: + out.write(f'run={run_id}\nsha={source}\nversion={target}\n') + PY + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ steps.source.outputs.sha }} + persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: LootableBodies-${{ steps.source.outputs.version }}-${{ steps.source.outputs.sha }} + run-id: ${{ steps.source.outputs.run }} + github-token: ${{ github.token }} + path: audited + - name: Independently verify every locally qualified byte + env: + SOURCE_SHA: ${{ steps.source.outputs.sha }} + run: | + python3 -B tools/native120_build.py --bundle audited + python3 - <<'PY' + import json,os + from pathlib import Path + accepted=json.loads(Path('.github/release120.json').read_text()) + Path('independent-audit.json').write_text(json.dumps({'source':os.environ['SOURCE_SHA'],'files':accepted['files'],'all_six_exact':True,'identical_to_locally_audited_bundle':True},indent=2)+'\n') + PY + # Exact hashes reuse the local classfile/licence/identity/mapping audit. + # This independent receipt is a byte match, not a fresh gameplay run or + # another Gradle/Forge preparation. The build job audits new archives. + - name: Retain independent audit evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: native120-independent-audit-${{ steps.source.outputs.sha }} + if-no-files-found: error + path: independent-audit.json diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0fa98ea..d4df42a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,11 +3,11 @@ name: CodeQL on: push: branches: - - master-1.21.11 + - master-26.1.2 - 'feature/**' pull_request: branches: - - master-1.21.11 + - master-26.1.2 schedule: - cron: '43 7 * * 4' @@ -31,21 +31,15 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + - name: Install pinned Java 25 Gradle and compilation runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - name: Install pinned Java 21 Gradle and compilation runtime - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '21.0.7+6.0.LTS' - - name: Pin production JDK run: | - echo "LOOTABLEBODIES_JAVA21_HOME=$JAVA_HOME_21_X64" >> "$GITHUB_ENV" + echo "LOOTABLEBODIES_JAVA25_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 509dc98..8f57740 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: release_version: - description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011, 118021, 119041, 120011, 120061, 121011 or 121111. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011, 118021, 119041, 120011, 120061, 121011, 121111 or 2601021. required: true type: string curseforge_release_level: @@ -20,7 +20,7 @@ on: default: false type: boolean local_qualification: - description: Hash-bound local qualification receipt, required for the 1.17.1, 1.18.2, 1.19.4, 1.20.1, 1.20.6, 1.21.1 and 1.21.11 releases. + description: Hash-bound local qualification receipt, required for the 1.17.1, 1.18.2, 1.19.4, 1.20.1, 1.20.6, 1.21.1, 1.21.11 and 26.1.2 releases. required: false default: '' type: string @@ -54,7 +54,7 @@ jobs: set -euo pipefail [[ "$GITHUB_REPOSITORY" == 'MinecraftModDevelopmentMods/LootableBodies' ]] [[ "$GITHUB_REF" == 'refs/heads/master-1.12' ]] - [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(110021|111021|112021|113021|114041|115021|116051|117011|118021|119041|120011|120061|121011|121111)$ ]] + [[ "$RELEASE_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(110021|111021|112021|113021|114041|115021|116051|117011|118021|119041|120011|120061|121011|121111|2601021)$ ]] 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 ;; @@ -70,6 +70,7 @@ jobs: *.120061) minecraft_version=1.20.6; source_branch=master-1.20.6 ;; *.121011) minecraft_version=1.21.1; source_branch=master-1.21.1 ;; *.121111) minecraft_version=1.21.11; source_branch=master-1.21.11 ;; + *.2601021) minecraft_version=26.1.2; source_branch=master-26.1.2 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -108,16 +109,17 @@ jobs: '1.20.1': ('master-1.20.1', '47.4.28', '120011'), '1.20.6': ('master-1.20.6', '50.2.14', '120061'), '1.21.1': ('master-1.21.1', '52.1.20', '121011'), - '1.21.11': ('master-1.21.11', '61.2.4', '121111')} + '1.21.11': ('master-1.21.11', '61.2.4', '121111'), + '26.1.2': ('master-26.1.2', '64.1.5', '2601021')} branch, forge, suffix = targets[minecraft] if os.environ['SOURCE_BRANCH'] != branch: raise SystemExit('The selected branch does not match its Minecraft version') native17 = minecraft == '1.17.1' - archive_only = minecraft in ('1.18.2', '1.19.4', '1.20.1', '1.20.6', '1.21.1', '1.21.11') + archive_only = minecraft in ('1.18.2', '1.19.4', '1.20.1', '1.20.6', '1.21.1', '1.21.11', '26.1.2') locally_qualified = native17 or archive_only expected = {'minecraft_version': minecraft, 'forge_version': forge, - 'loader_name': 'forge', 'loader_code': '1', 'java_version': '21' if minecraft in ('1.20.6', '1.21.1', '1.21.11') else '17' if archive_only else '16' if native17 else '8', - 'java_toolchain_version': '21.0.7+6' if minecraft in ('1.20.6', '1.21.1', '1.21.11') else '17.0.20.1+1' if archive_only else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '21' if minecraft in ('1.20.6', '1.21.1', '1.21.11') else '17', + 'loader_name': 'forge', 'loader_code': '1', 'java_version': '25' if minecraft == '26.1.2' else '21' if minecraft in ('1.20.6', '1.21.1', '1.21.11', '26.1.2') else '17' if archive_only else '16' if native17 else '8', + 'java_toolchain_version': '25.0.3+9' if minecraft == '26.1.2' else '21.0.7+6' if minecraft in ('1.20.6', '1.21.1', '1.21.11', '26.1.2') else '17.0.20.1+1' if archive_only else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '25' if minecraft == '26.1.2' else '21' if minecraft in ('1.20.6', '1.21.1', '1.21.11', '26.1.2') else '17', 'mod_id': 'lootablebodies', 'mod_artifact': 'LootableBodies', 'mod_group': 'zone.moddev.mc.lootablebodies', 'mod_license': 'LGPL-2.1-only', 'curseforge_project_id': '240968'} @@ -157,7 +159,7 @@ jobs: except (ValueError,TypeError): raise SystemExit('A local qualification receipt is required') keys={'schema','source','version','files','evidence_sha256','qualified_locally','manual_accepted'} - target_code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120','1.21.1':'120','1.21.11':'120'}[minecraft] + target_code={'1.17.1':'117','1.18.2':'118','1.19.4':'119','1.20.1':'120','1.20.6':'120','1.21.1':'120','1.21.11':'120','26.1.2':'120'}[minecraft] metadata=json.loads(Path(f'.github/release{target_code}.json').read_text()) accepted=metadata['files'] if archive_only and (metadata.get('local_qualification_complete') is not True or metadata.get('qualification_integrity_status')!='verified'): @@ -181,15 +183,15 @@ jobs: matching = [check for check in checks if check['name'] == name] if not matching or max(matching, key=lambda c: c['id'])['conclusion'] != 'success': raise SystemExit(f'The selected commit has no successful {name} check') - if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19.4', '1.20.1', '1.20.6', '1.21.1', '1.21.11'): + if minecraft in ('1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19.4', '1.20.1', '1.20.6', '1.21.1', '1.21.11', '26.1.2'): target = minecraft.rsplit('.', 1)[0] - event_query = '' if minecraft in ('1.21.1', '1.21.11') else '&event=workflow_run' + event_query = '' if minecraft in ('1.21.1', '1.21.11', '26.1.2') else '&event=workflow_run' audits = json.loads(subprocess.check_output(['gh', 'api', f'repos/{repo}/actions/workflows/artifact-audit-{target}.yml/runs?per_page=100{event_query}'], text=True))['workflow_runs'] matching = [run for run in audits if run['display_title'] == f'Audit {minecraft} artifacts from {sha}' - and (minecraft not in ('1.21.1', '1.21.11') or (run['event'] in ('workflow_run', 'workflow_dispatch') + and (minecraft not in ('1.21.1', '1.21.11', '26.1.2') or (run['event'] in ('workflow_run', 'workflow_dispatch') and run['head_branch'] == 'master-1.12' and run['head_sha'] == os.environ['GITHUB_SHA'] - and run['path'] == '.github/workflows/artifact-audit-1.21.yml'))] + and run['path'] == ('.github/workflows/artifact-audit-26.1.yml' if minecraft == '26.1.2' else '.github/workflows/artifact-audit-1.21.yml')))] if not matching or max(matching, key=lambda run: run['id'])['conclusion'] != 'success': raise SystemExit('The selected commit has no successful independent artifact audit') release = subprocess.run(['gh', 'api', f'repos/{repo}/releases/tags/{version}'], capture_output=True, text=True) @@ -219,25 +221,25 @@ jobs: with: ref: ${{ needs.preflight.outputs.release_sha }} - name: Install Mavenizer Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' && needs.preflight.outputs.minecraft_version != '26.1.2' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '25.0.3+9.0.LTS' - name: Install production Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' && needs.preflight.outputs.minecraft_version != '26.1.2' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '26.1.2' && '25.0.3+9.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' && needs.preflight.outputs.minecraft_version != '26.1.2' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '26.1.2' && '25.0.3+9.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' && needs.preflight.outputs.minecraft_version != '26.1.2' run: | if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.1' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" @@ -248,7 +250,7 @@ jobs: fi echo "LOOTABLEBODIES_MAVENIZER_JAVA_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" - name: Set up Gradle - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' + if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' && needs.preflight.outputs.minecraft_version != '26.1.2' uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Download the qualified native117 six-file bundle if: needs.preflight.outputs.minecraft_version == '1.17.1' @@ -293,7 +295,7 @@ jobs: if: needs.preflight.outputs.minecraft_version == '1.19.4' run: python3 -B tools/native119_build.py --bundle build/release/bundle - name: Download the qualified native120 six-file bundle - if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11' + if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11' || needs.preflight.outputs.minecraft_version == '26.1.2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: LootableBodies-${{ needs.preflight.outputs.minecraft_version }}-${{ needs.preflight.outputs.release_sha }} @@ -301,10 +303,10 @@ jobs: github-token: ${{ github.token }} path: build/release/bundle - name: Verify all six native120 bytes before publication - if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11' + if: needs.preflight.outputs.minecraft_version == '1.20.1' || needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11' || needs.preflight.outputs.minecraft_version == '26.1.2' run: python3 -B tools/native120_build.py --bundle build/release/bundle - name: Build, test and audit the release - if: needs.preflight.outputs.minecraft_version != '1.17.1' && needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' + if: needs.preflight.outputs.minecraft_version != '1.17.1' && needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.11' && needs.preflight.outputs.minecraft_version != '26.1.2' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ @@ -409,15 +411,17 @@ jobs: uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11') && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '26.1.2' && '25.0.3+9.0.LTS' || (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11') && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin - java-version: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11') && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} + java-version: ${{ needs.preflight.outputs.minecraft_version == '26.1.2' && '25.0.3+9.0.LTS' || (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11') && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain run: | - if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.6' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.21.1' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.21.11' ]]; then + if [[ '${{ needs.preflight.outputs.minecraft_version }}' == '26.1.2' ]]; then + echo "LOOTABLEBODIES_JAVA25_HOME=$JAVA_HOME_25_X64" >> "$GITHUB_ENV" + elif [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.6' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.21.1' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.21.11' ]]; then echo "LOOTABLEBODIES_JAVA21_HOME=$JAVA_HOME_21_X64" >> "$GITHUB_ENV" elif [[ '${{ needs.preflight.outputs.minecraft_version }}' == '1.18.2' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.19.4' || '${{ needs.preflight.outputs.minecraft_version }}' == '1.20.1' ]]; then echo "LOOTABLEBODIES_JAVA17_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV" @@ -477,7 +481,7 @@ jobs: game-versions: ${{ needs.preflight.outputs.minecraft_version }} game-version-filter: none environment: client | server - java: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11') && 'Java 21' || needs.preflight.outputs.minecraft_version == '1.19.4' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.20.1' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} + java: ${{ needs.preflight.outputs.minecraft_version == '26.1.2' && 'Java 25' || (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11') && 'Java 21' || needs.preflight.outputs.minecraft_version == '1.19.4' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.20.1' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} fail-mode: fail files: | ${{ runner.temp }}/release-input/LootableBodies-${{ needs.preflight.outputs.release_version }}.jar diff --git a/.github/workflows/native120.yml b/.github/workflows/native120.yml index c47d825..96241dd 100644 --- a/.github/workflows/native120.yml +++ b/.github/workflows/native120.yml @@ -1,10 +1,10 @@ -name: LootableBodies 1.21.11 CI +name: LootableBodies 26.1.2 CI on: push: - branches: [master-1.21.11] + branches: [master-26.1.2] pull_request: - branches: [master-1.21.11] + branches: [master-26.1.2] permissions: contents: read @@ -45,15 +45,11 @@ jobs: with: distribution: temurin java-version: '25.0.3+9.0.LTS' - - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 - with: - distribution: temurin - java-version: '21.0.7+6.0.LTS' # Full Java/Python suites, games, IDE consumers and SDK installs stay local. # Fork pushes run policy checks only. MMD builds independently check bytes. - name: Compile and audit release archives env: - LOOTABLEBODIES_JAVA21_HOME: ${{ env.JAVA_HOME_21_X64 }} + LOOTABLEBODIES_JAVA25_HOME: ${{ env.JAVA_HOME_25_X64 }} LOOTABLEBODIES_MAVENIZER_JAVA_HOME: ${{ env.JAVA_HOME_25_X64 }} run: | set -euo pipefail @@ -63,7 +59,7 @@ jobs: - name: Retain the exact six release files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.21.11-${{ github.sha }} + name: LootableBodies-26.1.2-${{ github.sha }} if-no-files-found: error path: build/release/bundle/* - name: Retain build evidence diff --git a/.github/workflows/release-workflow-checks.yml b/.github/workflows/release-workflow-checks.yml index 5acb79f..d1bccd9 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.21.11'] + branches: ['feature/**', 'master-26.1.2'] pull_request: - branches: ['master-1.21.11'] + branches: ['master-26.1.2'] permissions: contents: read jobs: @@ -25,7 +25,7 @@ jobs: run: >- "$RUNNER_TEMP/actionlint/actionlint" -shellcheck= -pyflakes= .github/workflows/deploy-release.yml .github/workflows/release-workflow-checks.yml .github/workflows/artifact-audit-1.13.yml .github/workflows/artifact-audit-1.14.yml .github/workflows/artifact-audit-1.15.yml .github/workflows/artifact-audit-1.16.yml .github/workflows/artifact-audit-1.17.yml - .github/workflows/native120.yml .github/workflows/artifact-audit-1.20.yml .github/workflows/artifact-audit-1.21.yml .github/workflows/codeql-analysis.yml + .github/workflows/native120.yml .github/workflows/artifact-audit-1.20.yml .github/workflows/artifact-audit-1.21.yml .github/workflows/artifact-audit-26.1.yml .github/workflows/codeql-analysis.yml - name: Test version selection and publication checks run: python3 .github/scripts/test_release_workflow.py -v diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a403299..9f914fe 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,6 @@ -2.4.14.121111 - Minecraft 1.21.11, Forge +2.4.15.2601021 - Minecraft 26.1.2, Forge -Updated for Minecraft 1.21.11. +Updated for Minecraft 26.1.2. Preserves bodies and their contents when upgrading older worlds. Back up the whole world before upgrading. diff --git a/README.md b/README.md index 8f136c2..f349006 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lootable Bodies for Minecraft 1.21.11 +# Lootable Bodies for Minecraft 26.1.2 Your items stay on a corpse when you die. Right-click the body to recover them. -Use Forge 61.2.4 and Java 21. Install `LootableBodies-2.4.14.121111.jar` +Use Forge 64.1.5 and Java 25. Install `LootableBodies-2.4.15.2601021.jar` in the client and server mods folders. Back up your world before upgrading. Created by Cyanobacterium (DrCyano), maintained by SkyBlade1978. diff --git a/RELEASING.md b/RELEASING.md index cd03b61..8477f37 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -17,7 +17,7 @@ archive checks, security scans and small workflow/wrapper contracts. It does not and security checks pass. Recheck the exact merged source and all six file hashes. The independent artifact audit checks the uploaded files without rebuilding them. 4. Rehash the retained local evidence and supply its receipt to the guarded - release workflow. For 1.17.1, 1.18.2, 1.19.4, 1.20.1, 1.20.6, 1.21.1 and 1.21.11 the receipt binds the exact source commit, version, + release workflow. For 1.17.1, 1.18.2, 1.19.4, 1.20.1, 1.20.6, 1.21.1, 1.21.11 and 26.1.2 the receipt binds the exact source commit, version, six file hashes, evidence hash and manual acceptance. Publish the files already checked by CI, then independently download and compare the public GitHub files and CurseForge jar. An accepted upload alone does not complete the release. diff --git a/TESTING.md b/TESTING.md index fa21e2c..ae1fc93 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,7 +1,6 @@ # Testing -Run qualification for Forge 1.21.11 locally with Forge 61.2.4 and Java 21. -Qualification runs locally on this branch. +Run the Forge 26.1.2 checks locally with Forge 64.1.5 and Java 25. During development, reproduce the changed behaviour first, then batch the cheap Java and Python checks. Reserve the full runtime suite for a frozen candidate. @@ -20,11 +19,11 @@ For a frozen candidate: 3. Prepare and seal genuine historical writers/worlds once. Run the known recovery/terrain/entity-region failure origins first and count these runs in the full matrix. Stop later work after a genuine failure; do not hunt green. -4. Complete the current 68-origin, 136-case, ten-phase history contract, all +4. Complete the current 68-origin, 136-case, ten-phase history contract and the direct upgrade from the public 1.21.11 jar. Run all refusals, packaged/development launches, five graphical modes, configuration restart, rejected viewer, actual two-client inventory packets, 13 visibility checks and 64 respawns. Keep both 6,200 ticks and 310 seconds and the connected - restart. Keep the real native controls, Eclipse and Java 21 Tooling checks. + restart. Keep the real native controls, Eclipse and Java 25 Tooling checks. 5. Independently check complete logs, crashes, images, diagnostics, receipts, original-world seals and every published byte. Missing, changed or unreadable evidence blocks reuse and publication. Reused proof keeps its original run @@ -32,8 +31,8 @@ For a frozen candidate: Unchanged mod-free resource/protocol controls can be reused only with the same control, SDK, JDK, flags, resources and auditor contract. Changes to those inputs -require new control qualification. Use `tools/fixtures/native12111-forge-only-flat-level.dat` -for the Forge-only respawn control so it starts with Forge's own saved registry. +require new control qualification. Use `tools/fixtures/native2612-forge-only-flat-level.dat` +and its sealed world generator file for the Forge-only respawn control. Changes confined to a test fixture require the affected proof and tool contracts, not an automatic replay of unrelated passed groups. A changed production migration path still requires its full diff --git a/build.gradle b/build.gradle index 9ac7b74..00ee573 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,6 @@ plugins { id 'eclipse' id 'idea' id 'maven-publish' - id 'net.minecraftforge.renamer' version '1.1.5' id 'net.minecraftforge.gradle' version '7.0.34' } @@ -13,69 +12,52 @@ group = project.mod_group version = project.mod_version base.archivesName = project.mod_artifact -// Gradle uses Java 21; every compiler, test and game process uses this JDK 21. -// Eclipse does not inherit the command-line helper's environment. Keep its -// machine-specific toolchain selection in an ignored local properties file. +// Gradle, Mavenizer and the game use the pinned Java 25 installation. Properties localJavaSetup = new Properties() File localJavaSetupFile = file('gradle-local.properties') if (localJavaSetupFile.isFile()) localJavaSetupFile.withInputStream { localJavaSetup.load(it) } -def pinnedJava21 = providers.environmentVariable('LOOTABLEBODIES_JAVA21_HOME') - .orElse(providers.gradleProperty('lootablebodies.java21.home')) - .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.java21.home') }) -def exactJava21Spec = pinnedJava21.isPresent() ? - org.gradle.jvm.toolchain.internal.SpecificInstallationToolchainSpec.fromJavaHome( - project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava21.get())) : null -def pinnedJava25 = providers.environmentVariable('LOOTABLEBODIES_MAVENIZER_JAVA_HOME') - .orElse(providers.gradleProperty('lootablebodies.mavenizer.java.home')) - .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.mavenizer.java.home') }) +def pinnedJava25 = providers.environmentVariable('LOOTABLEBODIES_JAVA25_HOME') + .orElse(providers.gradleProperty('lootablebodies.java25.home')) + .orElse(providers.provider { localJavaSetup.getProperty('lootablebodies.java25.home') }) def exactJava25Spec = pinnedJava25.isPresent() ? org.gradle.jvm.toolchain.internal.SpecificInstallationToolchainSpec.fromJavaHome( project.services.get(org.gradle.api.internal.provider.PropertyFactory), file(pinnedJava25.get())) : null -ext.mavenizerLauncher = exactJava25Spec != null ? javaToolchains.launcherFor(exactJava25Spec) : - javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25); vendor = JvmVendorSpec.ADOPTIUM } -fgtools.configure('mavenizer') { javaLauncher.set(mavenizerLauncher) } java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) vendor = JvmVendorSpec.ADOPTIUM } withSourcesJar() withJavadocJar() } -ext.productionLauncher = exactJava21Spec != null ? javaToolchains.launcherFor(exactJava21Spec) : +ext.productionLauncher = exactJava25Spec != null ? javaToolchains.launcherFor(exactJava25Spec) : javaToolchains.launcherFor(java.toolchain) +ext.mavenizerLauncher = productionLauncher +fgtools.configure('mavenizer') { javaLauncher.set(mavenizerLauncher) } fgtools.configure('slimelauncher') { javaLauncher.set(productionLauncher) } -def productionCompiler = exactJava21Spec != null ? javaToolchains.compilerFor(exactJava21Spec) : +def productionCompiler = exactJava25Spec != null ? javaToolchains.compilerFor(exactJava25Spec) : javaToolchains.compilerFor(java.toolchain) -def productionJavadoc = exactJava21Spec != null ? javaToolchains.javadocToolFor(exactJava21Spec) : +def productionJavadoc = exactJava25Spec != null ? javaToolchains.javadocToolFor(exactJava25Spec) : javaToolchains.javadocToolFor(java.toolchain) tasks.register('verifyJavaSetup') { group = 'verification' doLast { - if (JavaVersion.current() != JavaVersion.VERSION_21) throw new GradleException('Run Gradle with Java 21') + if (JavaVersion.current() != JavaVersion.VERSION_25) throw new GradleException('Run Gradle with Java 25') def metadata = productionLauncher.get().metadata Properties release = new Properties() metadata.installationPath.file('release').asFile.withInputStream { release.load(it) } - if (release.getProperty('JAVA_RUNTIME_VERSION') != '"21.0.7+6-LTS"' || - release.getProperty('IMPLEMENTOR') != '"Eclipse Adoptium"') { - throw new GradleException("Production requires Temurin 21.0.7+6, found ${metadata.javaRuntimeVersion} ${metadata.vendor}") - } + if (release.getProperty('JAVA_RUNTIME_VERSION') != '"25.0.3+9-LTS"' || + release.getProperty('IMPLEMENTOR') != '"Eclipse Adoptium"') + throw new GradleException("Production requires Temurin 25.0.3+9, found ${metadata.javaRuntimeVersion} ${metadata.vendor}") logger.lifecycle("Gradle Java: ${System.getProperty('java.runtime.version')}; production: ${metadata}") - def worker = mavenizerLauncher.get().metadata - Properties workerRelease = new Properties() - worker.installationPath.file('release').asFile.withInputStream { workerRelease.load(it) } - if (workerRelease.getProperty('JAVA_RUNTIME_VERSION') != '"25.0.3+9-LTS"' || - workerRelease.getProperty('IMPLEMENTOR') != '"Eclipse Adoptium"') - throw new GradleException('The separate Mavenizer worker requires Temurin 25.0.3+9-LTS') - logger.lifecycle("Mavenizer worker: ${worker}; game/compilation remain on Java 21") } } tasks.withType(JavaCompile).configureEach { dependsOn 'verifyJavaSetup' javaCompiler = productionCompiler - sourceCompatibility = '21' - targetCompatibility = '21' + sourceCompatibility = '25' + targetCompatibility = '25' options.encoding = 'UTF-8' options.compilerArgs.addAll(['-Xmaxerrs','1000']) } @@ -144,7 +126,6 @@ ext.normalizeArchiveText = { details -> apply from: 'gradle/assets.gradle' minecraft { - mappings channel: project.mapping_channel, version: project.mapping_version runs { configureEach { workingDir.convention layout.projectDirectory.dir( @@ -233,10 +214,10 @@ tasks.named('jar', Jar) { 'Implementation-Version': project.version, 'Implementation-Vendor': project.mod_vendor, 'Maven-Artifact': "${project.group}:${base.archivesName.get()}:${project.version}", 'Implementation-License': project.mod_license, - 'Built-On-Java': '21', 'Built-On': "${minecraft_version}-${forge_version}", 'MixinConfigs': 'lootablebodies.mixins.json') + 'Built-On-Java': '25', 'Built-On': "${minecraft_version}-${forge_version}", 'MixinConfigs': 'lootablebodies.mixins.json') filesMatching(archiveTextPatterns, normalizeArchiveText) } -// Forge 52.1.20 ships its runtime with official names. +// Minecraft 26.1.2 ships with named classes. ext.releaseJar = tasks.named('jar', Jar) ['sourcesJar', 'javadocJar'].each { name -> tasks.named(name, Jar) { @@ -249,7 +230,7 @@ ext.releaseJar = tasks.named('jar', Jar) filesMatching(archiveTextPatterns, normalizeArchiveText) } } -// Java 21 writes the index and its bundled SVG icons with the host line +// Javadoc writes the index and its bundled SVG icons with the host line // separator. Keep the documentation bytes identical on Windows and Linux. tasks.named('javadocJar', Jar) { inputs.property('javadocLineEndings', 'crlf-index-and-icons') diff --git a/gradle.properties b/gradle.properties index 05e8d75..0514eea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,17 +5,17 @@ org.gradle.caching=true org.gradle.parallel=false org.gradle.workers.max=2 net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.21.11 -forge_version=61.2.4 +minecraft_version=26.1.2 +forge_version=64.1.5 mapping_channel=official -mapping_version=1.21.11 +mapping_version=26.1.2 # Release identity and dispatcher target metadata. loader_name=forge loader_code=1 -java_version=21 -java_toolchain_version=21.0.7+6 -gradle_java_version=21 +java_version=25 +java_toolchain_version=25.0.3+9 +gradle_java_version=25 curseforge_project_id=240968 mod_id=lootablebodies @@ -25,7 +25,7 @@ mod_vendor=Cyanobacterium mod_license=LGPL-2.1-only mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=2.4.14.121111 +mod_version=2.4.15.2601021 mod_group=zone.moddev.mc.lootablebodies mod_authors=Cyanobacterium,SkyBlade1978 mod_description=Saves your items on a lootable corpse when you die. diff --git a/gradle/assets.gradle b/gradle/assets.gradle index 1ea547f..1b49c21 100644 --- a/gradle/assets.gradle +++ b/gradle/assets.gradle @@ -1,6 +1,6 @@ // Keep development launches independent of any installed Minecraft instance. ext.minecraftGameHome = layout.projectDirectory.dir('.gradle/minecraft-home') -ext.minecraftAssetIndex = '29' +ext.minecraftAssetIndex = '30' // Buildship refresh has no command-line -P arguments. Retain a prepared local // client location rather than requiring its assets/natives to be copied. Properties localMinecraftSetup = new Properties() @@ -10,7 +10,7 @@ String assetFolder=org.gradle.internal.os.OperatingSystem.current().isMacOsX()?' ext.minecraftAssets = layout.projectDirectory.dir(providers.gradleProperty('minecraftAssetsDirectory') .getOrElse(localMinecraftSetup.getProperty('minecraftAssetsDirectory',minecraftGameHome.dir(assetFolder).asFile.absolutePath))) ext.minecraftNatives = layout.projectDirectory.dir(providers.gradleProperty('minecraftNativesDirectory') - .getOrElse(localMinecraftSetup.getProperty('minecraftNativesDirectory','.gradle/native12111-client-inputs/natives'))) + .getOrElse(localMinecraftSetup.getProperty('minecraftNativesDirectory','.gradle/native2612-client-inputs/natives'))) tasks.register('prepareMinecraftAssets') { group = 'ide' doLast { @@ -28,10 +28,10 @@ tasks.register('prepareMinecraftAssets') { java.nio.file.Files.move(pending.toPath(),output.toPath(),java.nio.file.StandardCopyOption.REPLACE_EXISTING) } } - File descriptor=new File(root,'1.21.11.json') - download('https://piston-meta.mojang.com/v1/packages/35f8a9bc4874e2c95e51b3ad21e043a33776f7c2/1.21.11.json','35f8a9bc4874e2c95e51b3ad21e043a33776f7c2',descriptor) + File descriptor=new File(root,'26.1.2.json') + download('https://piston-meta.mojang.com/v1/packages/4e49d699db7dba839226d0ed1fcf36c465c2edbe/26.1.2.json','4e49d699db7dba839226d0ed1fcf36c465c2edbe',descriptor) def version=new groovy.json.JsonSlurper().parse(descriptor) - if(version.id!='1.21.11' || version.assetIndex.id!=minecraftAssetIndex) + if(version.id!='26.1.2' || version.assetIndex.id!=minecraftAssetIndex) throw new GradleException('Prepared Minecraft client metadata has the wrong target identity') File index=new File(root,"indexes/${minecraftAssetIndex}.json") download(version.assetIndex.url,version.assetIndex.sha1,index) diff --git a/gradle/audit.gradle b/gradle/audit.gradle index 602cf3a..db8dd2f 100644 --- a/gradle/audit.gradle +++ b/gradle/audit.gradle @@ -33,7 +33,7 @@ ext.auditReleaseArchives = { File directory -> if (!path.startsWith('cyano/lootable/')) throw new GradleException("Foreign production class ${path}") byte[] bytes = zip.getInputStream(entry).bytes int major = ((bytes[6] & 255) << 8) | (bytes[7] & 255) - if (major != 65) throw new GradleException("${path} is class major ${major}") + if (major != 69) throw new GradleException("${path} is class major ${major}") } if (path.endsWith('.lang')) { java.nio.charset.StandardCharsets.UTF_8.newDecoder().decode( @@ -57,7 +57,8 @@ ext.auditReleaseArchives = { File directory -> ['META-INF/mods.toml', 'pack.mcmeta', 'cyano/lootable/LootableBodies.class', 'lootablebodies.mixins.json', 'cyano/lootable/mixin/DataFixersMixin.class', 'cyano/lootable/mixin/SaveAccessMixin.class', 'cyano/lootable/mixin/DedicatedWorldMixin.class', - 'cyano/lootable/mixin/RegionStorageMixin.class', 'cyano/lootable/mixin/LegacyStructuresMixin.class', + 'cyano/lootable/mixin/RegionStorageMixin.class', 'cyano/lootable/mixin/FileFixerMixin.class', + 'cyano/lootable/mixin/FileFixLevelDataMixin.class', 'cyano/lootable/mixin/DimensionStorageMixin.class', 'cyano/lootable/mixin/CapabilityDataMixin.class', 'cyano/lootable/mixin/CorpseRenderDispatcherMixin.class', 'cyano/lootable/upgrade/EarlyWorldPreservation.class', 'assets/lootablebodies/textures/gui/corpse.png', @@ -65,17 +66,17 @@ ext.auditReleaseArchives = { File directory -> if (!names.contains(it)) throw new GradleException("Missing production entry ${it}") } String info=zip.getInputStream(zip.getEntry('META-INF/mods.toml')).getText('UTF-8') - ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.21.11]"'].each { + ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[26.1.2]"'].each { if(!info.contains(it))throw new GradleException("Incorrect production metadata: ${it}") } def pack=new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('pack.mcmeta')).getText('UTF-8')).pack - if(pack.min_format!=[94,1] || pack.max_format!=[94,1] || pack.containsKey('pack_format') || pack.containsKey('supported_formats') || pack.containsKey('forge:server_data_pack_format') || + if(pack.min_format!=[84,0] || pack.max_format!=[101,1] || pack.containsKey('pack_format') || pack.containsKey('supported_formats') || pack.containsKey('forge:server_data_pack_format') || pack.containsKey('forge:resource_pack_format') || pack.containsKey('forge:data_pack_format')) - throw new GradleException('Forge mod pack metadata must use data format 94.1') + throw new GradleException('Forge mod pack metadata must declare resource format 84.0 and data format 101.1') def mixins = new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('lootablebodies.mixins.json')).getText('UTF-8')) if (mixins.required != true || mixins.compatibilityLevel != 'JAVA_21' || mixins.package != 'cyano.lootable.mixin' || mixins.injectors != [defaultRequire: 1] || - mixins.mixins != ['DataFixersMixin', 'SaveAccessMixin', 'RegionStorageMixin', 'LegacyStructuresMixin', 'CapabilityDataMixin'] || mixins.server != ['DedicatedWorldMixin'] || + mixins.mixins != ['DataFixersMixin', 'SaveAccessMixin', 'RegionStorageMixin', 'FileFixerMixin', 'FileFixLevelDataMixin', 'DimensionStorageMixin', 'CapabilityDataMixin'] || mixins.server != ['DedicatedWorldMixin'] || mixins.client != ['CorpseRenderDispatcherMixin'] || manifest.getValue('MixinConfigs') != 'lootablebodies.mixins.json' || manifest.getValue('FMLAT') != null || names.contains('META-INF/coremods.json') || names.any { it.startsWith('coremods/') && !it.endsWith('/') }) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 23e5bd8..ad17886 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -1,5 +1,5 @@ eclipse { - jdt.javaRuntimeName = 'JavaSE-21' + jdt.javaRuntimeName = 'JavaSE-25' classpath { downloadSources = true downloadJavadoc = true @@ -36,7 +36,7 @@ tasks.register('configureEclipseBuildship') { File localJavaSetup = file('gradle-local.properties') Properties toolchains = new Properties() if (localJavaSetup.isFile()) localJavaSetup.withInputStream { toolchains.load(it) } - toolchains.setProperty('lootablebodies.java21.home', + toolchains.setProperty('lootablebodies.java25.home', productionLauncher.get().metadata.installationPath.asFile.canonicalPath) toolchains.setProperty('lootablebodies.mavenizer.java.home', mavenizerLauncher.get().metadata.installationPath.asFile.canonicalPath) @@ -71,11 +71,11 @@ tasks.register('isolateEclipseProductionRuns') { if (!launch.isFile()) throw new GradleException("Generate ${name} with genEclipseRuns first") String contents = launch.getText('UTF-8') File nativeRoot=minecraftNatives.asFile - if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native 1.21.11 client inputs before generating Eclipse launches') + if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native 26.1.2 client inputs before generating Eclipse launches') contents=contents.replace('{asset_index}',minecraftAssetIndex) .replace('{assets_root}',minecraftAssets.asFile.absolutePath) .replace('{natives}',nativeRoot.absolutePath) - contents = contents.replace('/JavaSE-8', '/JavaSE-21') + contents = contents.replace('/JavaSE-8', '/JavaSE-25') contents = contents.replaceAll(//, '') contents = contents.replace('', ' \n') @@ -161,9 +161,9 @@ tasks.register('verifyEclipseProductionClasspath') { it.@key.text() == 'org.eclipse.jdt.launching.CLASSPATH' }.listEntry.collect { new groovy.xml.XmlSlurper().parseText(it.@value.text()) } if (entries.count { it.@projectName.text() == rootProject.name } != 1 || - !entries.any { it.@containerPath.text().endsWith('/JavaSE-21') } || + !entries.any { it.@containerPath.text().endsWith('/JavaSE-25') } || launchModel.mapAttribute.mapEntry.any { it.@key.text() == 'MOD_CLASSES' }) - throw new GradleException("${name} must launch the production Eclipse project on Java 21") + throw new GradleException("${name} must launch the production Eclipse project on Java 25") def libraries = entries.findAll { it.@externalArchive.text() }.collect { file(it.@externalArchive.text()).canonicalFile } if (entries.size() != libraries.size() + 2 || libraries != eclipseRunLibraries(name - '.launch').collect { it.canonicalFile } || diff --git a/gradle/release.gradle b/gradle/release.gradle index f957a46..d259e8e 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,10 +5,10 @@ def expectedIdentity = [ mod_id: 'lootablebodies', mod_artifact: 'LootableBodies', mod_group: 'zone.moddev.mc.lootablebodies', mod_vendor: 'Cyanobacterium', mod_name: "DrCyano's Lootable Bodies", mod_authors: 'Cyanobacterium,SkyBlade1978', - minecraft_version: '1.21.11', forge_version: '61.2.4', - mapping_channel: 'official', mapping_version: '1.21.11', + minecraft_version: '26.1.2', forge_version: '64.1.5', + mapping_channel: 'official', mapping_version: '26.1.2', loader_name: 'forge', loader_code: '1', curseforge_project_id: '240968', - java_version: '21', java_toolchain_version: '21.0.7+6', gradle_java_version: '21', + java_version: '25', java_toolchain_version: '25.0.3+9', gradle_java_version: '25', mod_license: 'LGPL-2.1-only', mod_license_name: 'GNU Lesser General Public License, Version 2.1', mod_license_url: 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt', mod_url: 'https://github.com/MinecraftModDevelopmentMods/LootableBodies', @@ -152,17 +152,15 @@ tasks.register('verifyPreparedReleaseArtifacts') { } } -// Only the confirmed MMD release workflow may upload an audited prepared bundle. +// Publish only the already audited bundle from the confirmed MMD dispatcher. gradle.taskGraph.whenReady { graph -> if (graph.allTasks.any { it instanceof AbstractPublishToMaven || it.name in ['publish', 'publishToMavenLocal'] }) { - if (graph.allTasks.any { it.name == 'publishToMavenLocal' || it.class.name.contains('PublishToMavenLocal') } || - System.getenv('GITHUB_ACTIONS') != 'true' || + if (System.getenv('GITHUB_ACTIONS') != 'true' || System.getenv('GITHUB_REPOSITORY') != 'MinecraftModDevelopmentMods/LootableBodies' || - System.getenv('GITHUB_WORKFLOW') != 'Release Lootable Bodies' || - System.getenv('GITHUB_REF') != 'refs/heads/master-1.12' || + !System.getenv('LOOTABLEBODIES_RELEASE_SHA')?.matches('[a-f0-9]{40}') || System.getenv('LOOTABLEBODIES_RELEASE_VERSION') != project.version.toString() || - !(System.getenv('GITHUB_RUN_ID') ==~ /[0-9]+/) || !preparedReleaseDir.isPresent()) - throw new GradleException('Publication is disabled outside the confirmed MMD release workflow') + !preparedReleaseDir.isPresent() || graph.allTasks.any { it.name == 'publishToMavenLocal' }) + throw new GradleException('Publication is disabled outside the confirmed MMD release') } } diff --git a/gradle/runtime.gradle b/gradle/runtime.gradle index 7437cbe..3df8a06 100644 --- a/gradle/runtime.gradle +++ b/gradle/runtime.gradle @@ -71,15 +71,23 @@ def prepareOwnedLoaderConfig = { File root -> } } -// Use an actual Forge 61 codec header in each newly owned flat fixture world. +// Use the actual Forge 64 header and generator data in each newly owned flat fixture world. def prepareOwnedFlatWorld = { File root -> - File header = file('tools/fixtures/native12111-flat-level.dat') - if (!header.isFile() || releaseDigest(header) != '33DBE405659FB4EF57D23A07485EBC9AFA57694D7B54357C5B534A3E8963F86F') + File header = file('tools/fixtures/native2612-flat-level.dat') + def fixtures = new groovy.json.JsonSlurper().parse(file('tools/native2612_flat_inputs.json')) + def fixture = header.isFile() ? fixtures[releaseDigest(header)] : null + if (fixture == null || fixture.header != header.name) throw new GradleException('Missing or changed native flat fixture header') + File settings = file('tools/fixtures/' + fixture.worldgen) + if (!settings.isFile() || releaseDigest(settings) != fixture.worldgen_sha256) + throw new GradleException('Missing or changed native flat fixture world generator') File world = new File(root,'world') if (world.exists()) throw new GradleException('Refusing to replace an existing fixture world') world.mkdirs() new File(world,'level.dat').bytes = header.bytes + File worldgen = new File(world,'data/minecraft/world_gen_settings.dat') + worldgen.parentFile.mkdirs() + worldgen.bytes = settings.bytes } def runDirectory = layout.dir(providers.provider { file(providers.gradleProperty('integrationDirectory').getOrElse('build/integration-run')) }) diff --git a/settings.gradle b/settings.gradle index 7d0fb6c..f478699 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ plugins { } rootProject.name = 'LootableBodies' -// The MMD repository builds and audits release archives. +// Local qualification and manual testing come before hosted checks. if (System.getenv('GITHUB_ACTIONS') == 'true' && System.getenv('GITHUB_REPOSITORY') != 'MinecraftModDevelopmentMods/LootableBodies') { throw new GradleException('Hosted builds are limited to the MMD repository') diff --git a/src/controlTest/java/buildcontrol/ForgeControl.java b/src/controlTest/java/buildcontrol/ForgeControl.java index 083772c..884c566 100644 --- a/src/controlTest/java/buildcontrol/ForgeControl.java +++ b/src/controlTest/java/buildcontrol/ForgeControl.java @@ -7,7 +7,6 @@ import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.EventLoopGroupHolder; -import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.level.storage.LevelData; import net.minecraftforge.event.server.ServerStartedEvent; import net.minecraftforge.event.TickEvent; @@ -25,7 +24,7 @@ private void loadComplete(net.minecraftforge.fml.event.lifecycle.FMLLoadComplete if(rounds>0)event.enqueueWork(()->ResourceControl.run(rounds)); } public void started(ServerStartedEvent event) { - if(net.minecraftforge.fml.ModList.get().getModContainerById("lootablebodies").isPresent())throw new AssertionError("Independent Forge control contains Lootable Bodies"); + if(net.minecraftforge.fml.ModList.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().halt(false);} @@ -151,8 +150,8 @@ private void finish(net.minecraft.client.Minecraft mc) { } private void createWorld(net.minecraft.client.Minecraft client) { client.createWorldOpenFlows().createFreshLevel("platform-fixture", - new net.minecraft.world.level.LevelSettings("Platform fixture",net.minecraft.world.level.GameType.CREATIVE,false, - net.minecraft.world.Difficulty.NORMAL,false,new GameRules(net.minecraft.world.level.WorldDataConfiguration.DEFAULT.enabledFeatures()),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), + new net.minecraft.world.level.LevelSettings("Platform fixture",net.minecraft.world.level.GameType.CREATIVE, + net.minecraft.world.level.LevelSettings.DifficultySettings.DEFAULT,false,net.minecraft.world.level.WorldDataConfiguration.DEFAULT), new net.minecraft.world.level.levelgen.WorldOptions(4267321L,false,false), access->access.lookupOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) .getOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions(),new net.minecraft.client.gui.screens.TitleScreen()); diff --git a/src/controlTest/java/buildcontrol/ResourceControl.java b/src/controlTest/java/buildcontrol/ResourceControl.java index fdc858c..148bc7f 100644 --- a/src/controlTest/java/buildcontrol/ResourceControl.java +++ b/src/controlTest/java/buildcontrol/ResourceControl.java @@ -24,16 +24,16 @@ private ResourceControl() { } static void run(int rounds) { try { if(rounds!=50)throw new AssertionError("Each bounded resource qualification shard must contain50reloads"); - if(net.minecraftforge.fml.ModList.get().getModContainerById("lootablebodies").isPresent()) + if(net.minecraftforge.fml.ModList.getModContainerById("lootablebodies").isPresent()) throw new AssertionError("Independent resource control contains Lootable Bodies"); if(net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer()!=null) throw new AssertionError("Resource control has already constructed a server"); TracingExecutor prepare = Util.backgroundExecutor(); int cpus=Runtime.getRuntime().availableProcessors(); - if (!(prepare.service() instanceof ForkJoinPool pool)) throw new AssertionError("Forge 61 preparation executor changed"); + if (!(prepare.service() instanceof ForkJoinPool pool)) throw new AssertionError("Forge 64 preparation executor changed"); int workers = pool.getParallelism(), limit = Integer.getInteger("max.bg.threads", 255); - if(limit<1 || limit>255 || workers!=Math.max(1,Math.min(cpus-1,limit)))throw new AssertionError("Forge 61 worker configuration differs from its actual Util executor"); - System.out.println("NATIVE61_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); + if(limit<1 || limit>255 || workers!=Math.max(1,Math.min(cpus-1,limit)))throw new AssertionError("Forge 64 worker configuration differs from its actual Util executor"); + System.out.println("NATIVE64_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); PackRepository packs=new PackRepository(new ServerPacksSource(new net.minecraft.world.level.validation.DirectoryValidator(path->false))); WorldLoader.InitConfig config=new WorldLoader.InitConfig( new WorldLoader.PackConfig(packs,WorldDataConfiguration.DEFAULT,false,false),Commands.CommandSelection.DEDICATED, LevelBasedPermissionSet.GAMEMASTER); @@ -48,13 +48,13 @@ static void run(int rounds) { .get(12,TimeUnit.SECONDS)) { if(resources.reloaded().getRecipeManager().getRecipes().isEmpty())throw new AssertionError("No native recipes loaded"); } - if(round%10==0)System.out.println("NATIVE61_RESOURCE_RELOAD_COMPLETE="+round); + if(round%10==0)System.out.println("NATIVE64_RESOURCE_RELOAD_COMPLETE="+round); } } if(net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer()!=null) throw new AssertionError("Resource control unexpectedly constructed a server"); Files.writeString(Path.of("resource-control.pass"),"rounds="+rounds+" cpus="+cpus+" preparationWorkers="+workers+" noWorld=true noLootableBodies=true\n",StandardOpenOption.CREATE_NEW); - System.out.println("NATIVE61_RESOURCE_CONTROL_PASS rounds="+rounds); + System.out.println("NATIVE64_RESOURCE_CONTROL_PASS rounds="+rounds); System.out.flush();System.exit(0); } catch(Throwable failure) { failure.printStackTrace();System.out.flush();System.exit(2); diff --git a/src/controlTest/resources/META-INF/mods.toml b/src/controlTest/resources/META-INF/mods.toml index 04325ca..25ec30f 100644 --- a/src/controlTest/resources/META-INF/mods.toml +++ b/src/controlTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[61,)" +loaderVersion="[64,)" license="LGPL-2.1-only" [[mods]] modId="forgeplatformcontrol" diff --git a/src/controlTest/resources/pack.mcmeta b/src/controlTest/resources/pack.mcmeta index 289fad6..04b0a70 100644 --- a/src/controlTest/resources/pack.mcmeta +++ b/src/controlTest/resources/pack.mcmeta @@ -2,11 +2,11 @@ "pack": { "description": "Disposable Forge control", "min_format": [ - 94, - 1 + 84, + 0 ], "max_format": [ - 94, + 101, 1 ] } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java index dc8af53..a7d505a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java @@ -99,7 +99,7 @@ private void tick(TickEvent.ClientTickEvent.Post event) { pending.get(); NamelessCorpseClientChecks.englishContract(mc); for(Entity entity : FixtureApi.entities(mc.level)) if(entity instanceof EntityLootableBody && entity.getUUID().equals(RuntimeChecks.NEAR)) { - mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND); + mc.gameMode.interact(mc.player, entity, new net.minecraft.world.phys.EntityHitResult(entity), InteractionHand.MAIN_HAND); stage=3; break; } } else if(stage==3 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { @@ -119,7 +119,7 @@ private void tick(TickEvent.ClientTickEvent.Post event) { } else if(stage==6 && pending.isDone()) { pending.get(); for(Entity entity:FixtureApi.entities(mc.level)) if(entity instanceof EntityLootableBody && entity.getUUID().equals(RuntimeChecks.NEAR)) { - mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND); + mc.gameMode.interact(mc.player, entity, new net.minecraft.world.phys.EntityHitResult(entity), InteractionHand.MAIN_HAND); stage=7; frames=0; break; } } else if(stage==7 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java index 94b1d33..219d647 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java @@ -34,6 +34,7 @@ void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Pre event) { tooltipsChecked++; } void tick(Minecraft mc)throws Exception { + if(mc.getOverlay()!=null)return; if(++frames<20)return;frames=0; if(report==null)report=new File(mc.gameDirectory,"config-checks.txt"); if(stage==0 && mc.screen instanceof TitleScreen) { @@ -112,8 +113,8 @@ private void openMods(Minecraft mc)throws Exception { try { for(int y:positions)for(int x:new int[]{screen.width/2-150,screen.width/2,screen.width/2+150}) { int before=observer.count; - var graphics = new GuiGraphics(mc, new net.minecraft.client.gui.render.state.GuiRenderState(), x, y+5); - screen.renderWithTooltipAndSubtitles(graphics,x,y+5,0); + var graphics = new GuiGraphicsExtractor(mc, new net.minecraft.client.renderer.state.gui.GuiRenderState(), x, y+5); + screen.extractRenderStateWithTooltipAndSubtitles(graphics,x,y+5,0); RuntimeChecks.require(observer.count==before+1, "Native config background rendered "+(observer.count-before)+" times"); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java index 43fc707..72302e0 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java @@ -18,7 +18,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -64,7 +64,7 @@ boolean tick(Minecraft mc) throws Exception { } else if(stage==4 && gui(mc) && frames>20) { verifyGui(mc); screenshot(mc,"connected-corpse-before-recovery.png"); RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(6).getItem(),RuntimeChecks.stack(19)),"Client recovery slot NBT differs"); - mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,6,0,ClickType.QUICK_MOVE,mc.player); + mc.gameMode.handleContainerInput(mc.player.containerMenu.containerId,6,0,ContainerInput.QUICK_MOVE,mc.player); next(5); } else if(stage==5 && frames>30) { schedule(mc,() -> { @@ -91,8 +91,8 @@ boolean tick(Minecraft mc) throws Exception { screenshot(mc,"connected-corpse-first-reload.png"); RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(7).getItem(),RuntimeChecks.stack(20)),"Client normal-click slot NBT differs"); int window=mc.player.containerMenu.containerId; - mc.gameMode.handleInventoryMouseClick(window,7,0,ClickType.PICKUP,mc.player); - mc.gameMode.handleInventoryMouseClick(window,48,0,ClickType.PICKUP,mc.player); + mc.gameMode.handleContainerInput(window,7,0,ContainerInput.PICKUP,mc.player); + mc.gameMode.handleContainerInput(window,48,0,ContainerInput.PICKUP,mc.player); next(11); } else if(stage==11 && frames>30) { schedule(mc,() -> { @@ -201,7 +201,7 @@ private static CompoundTag read(File folder,String name) throws Exception { private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} private boolean open(Minecraft mc){ for(Entity entity:FixtureApi.entities(mc.level))if(entity instanceof EntityLootableBody && entity.getUUID().equals(corpseId)){ - mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND);return true; + mc.gameMode.interact(mc.player, entity, new net.minecraft.world.phys.EntityHitResult(entity), InteractionHand.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 e55bda8..b367a68 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java @@ -1,5 +1,5 @@ package cyano.lootable.buildtest; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.minecraft.server.level.ServerLevel; @@ -124,13 +124,13 @@ static void runDrops(MinecraftServer instance,ServerLevel target,File folder) th private static void toolThrow(int rejection) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b);b.setItem(4,item(4,1));Map before=total(b,p); RejectedDrops reject=new RejectedDrops();reject.toss=rejection==1;reject.join=rejection==2;reject.install(); - try{packet(p,4,0,ClickType.THROW,null);}finally{reject.close();} + try{packet(p,4,0,ContainerInput.THROW,null);}finally{reject.close();} conserved(before,b,p);equal(b.getItem(4),rejection==0?null:item(4,1),"Equipment throw changed damage/enchantment/custom data"); } private static void interruptDrag(boolean outside) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b);b.setItem(6,item(120,17));p.containerMenu.setCarried(item(121,10));Map before=total(b,p); - packet(p,-999,0,ClickType.QUICK_CRAFT,null);packet(p,7,1,ClickType.QUICK_CRAFT,null); - packet(p,outside?-999:6,0,outside?ClickType.PICKUP:ClickType.THROW,null); + packet(p,-999,0,ContainerInput.QUICK_CRAFT,null);packet(p,7,1,ContainerInput.QUICK_CRAFT,null); + packet(p,outside?-999:6,0,outside?ContainerInput.PICKUP:ContainerInput.THROW,null); equal(p.containerMenu.getCarried(),item(121,10),"Interrupted drag dropped cursor");equal(b.getItem(6),item(120,17),"Interrupted drag threw source");conserved(before,b,p); } private static void throwing(boolean creative,int source,int button,int rejection,boolean capture) throws Exception { @@ -142,7 +142,7 @@ private static void throwing(boolean creative,int source,int button,int rejectio 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; reject.install(); - try{packet(p,source==2?-999:source==0?6:48,button,source==2?ClickType.PICKUP:ClickType.THROW,null);} + try{packet(p,source==2?-999:source==0?6:48,button,source==2?ContainerInput.PICKUP:ContainerInput.THROW,null);} finally{reject.close();} conserved(before,b,p);RuntimeChecks.require((p.captureDrops()!=null)==capture,"Toss changed capture state"); int amount=source==2?(button==0?17:1):(button==0?1:17); @@ -160,7 +160,7 @@ private static void creativeFallback(boolean toss,boolean swap) throws Exception if(swap){b.setItem(0,item(0,1));p.getInventory().getNonEquipmentItems().set(0,pumpkins(64));} else p.containerMenu.setCarried(item(120,17)); Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=toss;reject.join=!toss;reject.install(); - try{if(swap)packet(p,0,0,ClickType.SWAP,null);else close(p);}finally{reject.close();} + try{if(swap)packet(p,0,0,ContainerInput.SWAP,null);else close(p);}finally{reject.close();} conserved(before,b,p);RuntimeChecks.require(b.isAlive(),"Creative fallback owner no longer alive"); } private static void collect(int mode) throws Exception { @@ -169,9 +169,9 @@ private static void collect(int mode) throws Exception { ItemStack differentDamage=item(120,12);differentDamage.setDamageValue((differentDamage.getDamageValue()+1)%16);b.setItem(9,differentDamage); p.getInventory().getNonEquipmentItems().set(9,item(120,10));p.containerMenu.setCarried(mode==3?ItemStack.EMPTY:item(120,mode==2?64:5)); if(mode==4)p.setGameMode(GameType.CREATIVE); - if(mode==5){q.containerMenu.setCarried(item(120,4));packet(q,10,0,ClickType.PICKUP_ALL,null);} + if(mode==5){q.containerMenu.setCarried(item(120,4));packet(q,10,0,ContainerInput.PICKUP_ALL,null);} Map before=total(b,p,q);int wanted=mode==3?0:mode==2?64:mode==5?15:64; - packet(p,10,mode==1?1:0,ClickType.PICKUP_ALL,null); + packet(p,10,mode==1?1:0,ContainerInput.PICKUP_ALL,null); RuntimeChecks.require(((p.containerMenu.getCarried()).isEmpty()?0:p.containerMenu.getCarried().getCount())==wanted,"Double click count/capacity differs"); equal(b.getItem(8),item(121,14),"Double click collected different custom NBT");equal(b.getItem(9),differentDamage,"Double click collected different damage"); if(mode==0)equal(p.getInventory().getNonEquipmentItems().get(9),item(120,1),"Forward collection order differs"); @@ -188,7 +188,7 @@ private static void fallbackOrder(boolean offhand) throws Exception { private static void cloneStack(boolean creative,boolean occupied) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setItem(6,item(120,17)); if(occupied)p.containerMenu.setCarried(item(121,4));Map expected=total(b,p); - if(creative && !occupied)add(expected,item(120,64));packet(p,6,2,ClickType.CLONE,null); + if(creative && !occupied)add(expected,item(120,64));packet(p,6,2,ContainerInput.CLONE,null); equal(b.getItem(6),item(120,17),"Creative clone removed source");equal(p.containerMenu.getCarried(),occupied?item(121,4):creative?item(120,64):null,"Clone cursor/NBT differs");conserved(expected,b,p); } private static void creativeDrag(boolean creative) throws Exception { @@ -233,10 +233,10 @@ private static CorpseContainer open(ServerPlayer p,EntityLootableBody b){ private static ItemStack pumpkins(int count){ItemStack s=new ItemStack(Blocks.CARVED_PUMPKIN,count);FixtureStackData.setCustom(s,FixtureStackData.custom(item(120,1)));return s;} private static void fill(ServerPlayer p){for(int i=0;i<36;i++)p.getInventory().getNonEquipmentItems().set(i,item(100+i,64));} private static void fill(EntityLootableBody b){for(int i=0;i<48;i++)b.setItem(i,item(i,i<6?1:64));b.addItem(item(70,31));} - private static void packet(ServerPlayer p,int slot,int button,ClickType type,ItemStack predicted) throws Exception { + private static void packet(ServerPlayer p,int slot,int button,ContainerInput type,ItemStack predicted) throws Exception { packetWithState(p,slot,button,type,predicted,p.containerMenu.getStateId()); } - private static void packetWithState(ServerPlayer p,int slot,int button,ClickType type,ItemStack predicted,int stateId) throws Exception { + private static void packetWithState(ServerPlayer p,int slot,int button,ContainerInput type,ItemStack predicted,int stateId) throws Exception { // These server-side fixtures deliberately report pre-action predictions; // native broadcasts must repair them. Real clients cover normal prediction. ServerboundContainerClickPacket outgoing=FixtureApi.clickPacket(p,stateId,slot,button,type,predicted); @@ -247,9 +247,9 @@ private static void packetWithState(ServerPlayer p,int slot,int button,ClickType p.connection.handleContainerClick(decoded); } finally {wire.release();} } - private static void click(ServerPlayer p,int slot,int button,ClickType type) throws Exception { + private static void click(ServerPlayer p,int slot,int button,ContainerInput type) throws Exception { ItemStack predicted=null; - if((type==ClickType.PICKUP || type==ClickType.QUICK_MOVE) && slot>=0){ + if((type==ContainerInput.PICKUP || type==ContainerInput.QUICK_MOVE) && slot>=0){ ItemStack s=p.containerMenu.getSlot(slot).getItem();predicted=s.copy(); } packet(p,slot,button,type,predicted); @@ -263,9 +263,9 @@ private static void close(ServerPlayer p) throws Exception { } finally {wire.release();} } private static void drag(ServerPlayer p,int mode,int... slots) throws Exception { - packet(p,-999,mode<<2,ClickType.QUICK_CRAFT,null); - for(int slot:slots)packet(p,slot,1|(mode<<2),ClickType.QUICK_CRAFT,null); - packet(p,-999,2|(mode<<2),ClickType.QUICK_CRAFT,null); + packet(p,-999,mode<<2,ContainerInput.QUICK_CRAFT,null); + for(int slot:slots)packet(p,slot,1|(mode<<2),ContainerInput.QUICK_CRAFT,null); + packet(p,-999,2|(mode<<2),ContainerInput.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 CompoundTag data(EntityLootableBody b){CompoundTag n=new CompoundTag();FixtureApi.writeAdditional(b,n);return n;} @@ -297,22 +297,22 @@ private static void hotbar() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b); for(int key=0;key<9;key++){ p.getInventory().getNonEquipmentItems().set(key,item(30+key,13));Map before=total(b,p); - click(p,6,key,ClickType.SWAP);equal(b.getItem(6),item(30+key,13),"Empty hotbar swap failed");equal(p.getInventory().getNonEquipmentItems().get(key),null,"Hotbar source remained"); - click(p,6,key,ClickType.SWAP);equal(b.getItem(6),null,"Hotbar return left source"); + click(p,6,key,ContainerInput.SWAP);equal(b.getItem(6),item(30+key,13),"Empty hotbar swap failed");equal(p.getInventory().getNonEquipmentItems().get(key),null,"Hotbar source remained"); + click(p,6,key,ContainerInput.SWAP);equal(b.getItem(6),null,"Hotbar return left source"); b.setItem(6,item(19,23));before=total(b,p); - click(p,6,key,ClickType.SWAP);equal(b.getItem(6),item(30+key,13),"Occupied hotbar incoming differs");equal(p.getInventory().getNonEquipmentItems().get(key),item(19,23),"Occupied hotbar outgoing differs"); - click(p,6,key,ClickType.SWAP);conserved(before,b,p);b.setItem(6,null);p.getInventory().getNonEquipmentItems().set(key,ItemStack.EMPTY); + click(p,6,key,ContainerInput.SWAP);equal(b.getItem(6),item(30+key,13),"Occupied hotbar incoming differs");equal(p.getInventory().getNonEquipmentItems().get(key),item(19,23),"Occupied hotbar outgoing differs"); + click(p,6,key,ContainerInput.SWAP);conserved(before,b,p);b.setItem(6,null);p.getInventory().getNonEquipmentItems().set(key,ItemStack.EMPTY); } } private static void armorSwap() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.getInventory().getNonEquipmentItems().set(0,item(4,1)); - click(p,0,0,ClickType.SWAP);equal(b.getItem(0),null,"Sword inserted into armor slot"); - p.getInventory().getNonEquipmentItems().set(0,pumpkins(64));Map before=total(b,p);click(p,0,0,ClickType.SWAP); + click(p,0,0,ContainerInput.SWAP);equal(b.getItem(0),null,"Sword inserted into armor slot"); + p.getInventory().getNonEquipmentItems().set(0,pumpkins(64));Map before=total(b,p);click(p,0,0,ContainerInput.SWAP); equal(b.getItem(0),pumpkins(1),"Hotbar armor limit ignored");equal(p.getInventory().getNonEquipmentItems().get(0),pumpkins(63),"Hotbar armor remainder lost");conserved(before,b,p); } private static void wearableShift() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();p.getInventory().getNonEquipmentItems().set(9,pumpkins(64));open(p,b); - Map before=total(b,p);click(p,48,0,ClickType.QUICK_MOVE); + Map before=total(b,p);click(p,48,0,ContainerInput.QUICK_MOVE); RuntimeChecks.require(!(b.getItem(0)).isEmpty() && b.getItem(0).getCount()==1,"Shift-click put a stack into the one-item head slot"); conserved(before,b,p); } @@ -334,21 +334,21 @@ private static void dragArmor() throws Exception { } private static void dragCancel() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(19,10));Map before=total(b,p); - packet(p,-999,2,ClickType.QUICK_CRAFT,null);packet(p,-999,0,ClickType.QUICK_CRAFT,null);packet(p,6,1,ClickType.QUICK_CRAFT,null); - packet(p,48,0,ClickType.PICKUP,null);packet(p,-999,2,ClickType.QUICK_CRAFT,null); + packet(p,-999,2,ContainerInput.QUICK_CRAFT,null);packet(p,-999,0,ContainerInput.QUICK_CRAFT,null);packet(p,6,1,ContainerInput.QUICK_CRAFT,null); + packet(p,48,0,ContainerInput.PICKUP,null);packet(p,-999,2,ContainerInput.QUICK_CRAFT,null); equal(b.getItem(6),null,"Cancelled drag wrote selected slot");equal(p.containerMenu.getCarried(),item(19,10),"Cancelled drag consumed cursor");conserved(before,b,p); } private static void fullPlayer() throws Exception { EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer p=player();fill(p);open(p,b); - Map before=total(b,p);packet(p,6,0,ClickType.QUICK_MOVE,null);equal(b.getItem(6),item(19,23),"Full player inventory consumed corpse stack");conserved(before,b,p); + Map before=total(b,p);packet(p,6,0,ContainerInput.QUICK_MOVE,null);equal(b.getItem(6),item(19,23),"Full player inventory consumed corpse stack");conserved(before,b,p); } private static void partialPlayer() throws Exception { EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer p=player();fill(p);p.getInventory().getNonEquipmentItems().set(35,item(19,63));open(p,b); - Map before=total(b,p);click(p,6,0,ClickType.QUICK_MOVE);equal(p.getInventory().getNonEquipmentItems().get(35),item(19,64),"Partial merge did not fill one space");equal(b.getItem(6),item(19,22),"Partial merge discarded remainder");conserved(before,b,p); + Map before=total(b,p);click(p,6,0,ContainerInput.QUICK_MOVE);equal(p.getInventory().getNonEquipmentItems().get(35),item(19,64),"Partial merge did not fill one space");equal(b.getItem(6),item(19,22),"Partial merge discarded remainder");conserved(before,b,p); } private static void fullCorpse() throws Exception { EntityLootableBody b=body();fill(b);ServerPlayer p=player();p.getInventory().getNonEquipmentItems().set(9,item(110,23));open(p,b); - CompoundTag unchanged=data(b);Map before=total(b,p);packet(p,48,0,ClickType.QUICK_MOVE,null);b.baseTick(); + CompoundTag unchanged=data(b);Map before=total(b,p);packet(p,48,0,ContainerInput.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 { @@ -368,7 +368,7 @@ void close() { private static void displacement(boolean rejectJoin,boolean rejectToss) throws Exception { EntityLootableBody b=body();b.setItem(0,item(0,1));ServerPlayer p=player();fill(p);p.getInventory().getNonEquipmentItems().set(0,pumpkins(64));open(p,b);Map before=total(b,p); RejectedDrops reject=new RejectedDrops();reject.join=rejectJoin;reject.toss=rejectToss;reject.install(); - try{click(p,0,0,ClickType.SWAP);}finally{reject.close();} + try{click(p,0,0,ContainerInput.SWAP);}finally{reject.close();} equal(b.getItem(0),pumpkins(1),"Displacement changed normal incoming swap");equal(p.getInventory().getNonEquipmentItems().get(0),pumpkins(63),"Displacement changed hotbar remainder");conserved(before,b,p); if(!rejectJoin && !rejectToss)RuntimeChecks.require(FixtureApi.entities(world,6,6).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Successful displacement no longer drops normally"); } @@ -398,9 +398,9 @@ private static void twoPlayerShift() throws Exception { EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer a=player(),c=player();open(a,b);open(c,b); ServerPlayer first=order==0?a:c,second=order==0?c:a;Map before=total(b,a,c); int staleState=second.containerMenu.getStateId(); - click(first,6,0,ClickType.QUICK_MOVE);second.containerMenu.broadcastChanges(); + click(first,6,0,ContainerInput.QUICK_MOVE);second.containerMenu.broadcastChanges(); RecordingConnection staleConnection=(RecordingConnection)second.connection;int beforeResync=staleConnection.resyncs; - packetWithState(second,6,0,ClickType.QUICK_MOVE,item(19,23),staleState); + packetWithState(second,6,0,ContainerInput.QUICK_MOVE,item(19,23),staleState); a.containerMenu.broadcastChanges();c.containerMenu.broadcastChanges(); RuntimeChecks.require(staleConnection.resyncs>beforeResync && staleConnection.lastContentStateId==second.containerMenu.getStateId(), "Stale menu state did not receive an authoritative native state correction"); @@ -413,25 +413,25 @@ private static void twoPlayerShift() throws Exception { private static void interleavedDrag(boolean swap) throws Exception { EntityLootableBody b=body();ServerPlayer a=player(),c=player();open(a,b);open(c,b);a.containerMenu.setCarried(item(19,10)); if(swap)c.getInventory().getNonEquipmentItems().set(0,item(20,7));else c.containerMenu.setCarried(item(20,7));Map before=total(b,a,c); - packet(a,-999,0,ClickType.QUICK_CRAFT,null);packet(a,6,1,ClickType.QUICK_CRAFT,null);packet(a,7,1,ClickType.QUICK_CRAFT,null); - click(c,6,0,swap?ClickType.SWAP:ClickType.PICKUP);packet(a,-999,2,ClickType.QUICK_CRAFT,null); + packet(a,-999,0,ContainerInput.QUICK_CRAFT,null);packet(a,6,1,ContainerInput.QUICK_CRAFT,null);packet(a,7,1,ContainerInput.QUICK_CRAFT,null); + click(c,6,0,swap?ContainerInput.SWAP:ContainerInput.PICKUP);packet(a,-999,2,ContainerInput.QUICK_CRAFT,null); equal(b.getItem(6),item(20,7),"Other player's stack was overwritten by drag");equal(b.getItem(7),item(19,5),"Surviving drag slot differs");equal(a.containerMenu.getCarried(),item(19,5),"Interrupted drag lost remainder");conserved(before,b,a,c); } private static void twoPlayerClose() throws Exception { EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer a=player(),c=player();fill(a);open(a,b);open(c,b);Map before=total(b,a,c); - click(a,6,0,ClickType.PICKUP);packet(c,6,0,ClickType.QUICK_MOVE,item(19,23));RejectedDrops reject=new RejectedDrops();reject.join=true;reject.install(); + click(a,6,0,ContainerInput.PICKUP);packet(c,6,0,ContainerInput.QUICK_MOVE,item(19,23));RejectedDrops reject=new RejectedDrops();reject.join=true;reject.install(); try{close(a);}finally{reject.close();} c.containerMenu.broadcastChanges();conserved(before,b,a,c); } private static void stale(boolean removed) throws Exception { EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer p=player();open(p,b);Map before=total(b,p); if(removed)b.discard();else p.setPos(200,80,200); - packet(p,6,0,ClickType.QUICK_MOVE,null);equal(b.getItem(6),item(19,23),"Stale packet extracted before tick closed GUI");conserved(before,b,p); + packet(p,6,0,ContainerInput.QUICK_MOVE,null);equal(b.getItem(6),item(19,23),"Stale packet extracted before tick closed GUI");conserved(before,b,p); } private static void staleDrag() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.containerMenu.setCarried(item(19,10));Map before=total(b,p); - packet(p,-999,0,ClickType.QUICK_CRAFT,null);packet(p,6,1,ClickType.QUICK_CRAFT,null);p.setPos(200,80,200); - packet(p,-999,2,ClickType.QUICK_CRAFT,null);equal(b.getItem(6),null,"Distant drag still distributed items"); + packet(p,-999,0,ContainerInput.QUICK_CRAFT,null);packet(p,6,1,ContainerInput.QUICK_CRAFT,null);p.setPos(200,80,200); + packet(p,-999,2,ContainerInput.QUICK_CRAFT,null);equal(b.getItem(6),null,"Distant drag still distributed items"); p.setPos(96.5,80,96.5);drag(p,0,7);equal(b.getItem(7),item(19,10),"New drag reused invalid selection");conserved(before,b,p); } private static final class RecordingConnection extends ServerGamePacketListenerImpl { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java index a660af9..33f6053 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java @@ -17,7 +17,7 @@ import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -46,34 +46,34 @@ boolean tick(Minecraft mc) throws Exception { if(stage==0){schedule(mc,()->prepare(mc));next(1);} else if(stage==1 && done()){if(open(mc))next(2);} else if(stage==2 && gui(mc) && frames>20){ - equal(slot(mc,6),item(120,17),"Initial client source");click(mc,6,0,ClickType.THROW);next(3); + equal(slot(mc,6),item(120,17),"Initial client source");click(mc,6,0,ContainerInput.THROW);next(3); } else if(stage==3 && frames>20){ equal(slot(mc,6),item(120,16),"Accepted client single throw"); schedule(mc,()->{equal(body(mc).getItem(6),item(120,16),"Accepted server single throw");drop(mc,item(120,1));mark(mc,"network_accepted_throw");reject.toss=true;reject.install();});next(4); - } else if(stage==4 && done()) {click(mc,6,1,ClickType.THROW);next(5);} + } else if(stage==4 && done()) {click(mc,6,1,ContainerInput.THROW);next(5);} else if(stage==5 && frames>20){ equal(slot(mc,6),item(120,16),"Cancelled throw did not correct client source");screenshot(mc,"drop-cancelled-source-resync.png"); schedule(mc,()->{equal(body(mc).getItem(6),item(120,16),"Cancelled server throw");mark(mc,"network_cancelled_throw_source_resync");reject.toss=false;reject.join=true;});next(6); - } else if(stage==6 && done()){click(mc,6,0,ClickType.PICKUP);next(7);} - else if(stage==7 && frames>20){equal(mc.player.containerMenu.getCarried(),item(120,16),"Pickup cursor");click(mc,-999,1,ClickType.PICKUP);next(8);} + } else if(stage==6 && done()){click(mc,6,0,ContainerInput.PICKUP);next(7);} + else if(stage==7 && frames>20){equal(mc.player.containerMenu.getCarried(),item(120,16),"Pickup cursor");click(mc,-999,1,ContainerInput.PICKUP);next(8);} else if(stage==8 && frames>20){ equal(mc.player.containerMenu.getCarried(),item(120,16),"Rejected outside drop did not correct client cursor");equal(slot(mc,6),null,"Outside source reappeared");screenshot(mc,"drop-rejected-cursor-resync.png"); schedule(mc,()->{equal(player(mc).containerMenu.getCarried(),item(120,16),"Rejected server outside cursor");mark(mc,"network_rejected_outside_cursor_resync");reject.join=false;});next(9); - } else if(stage==9 && done()){click(mc,-999,0,ClickType.PICKUP);next(10);} + } else if(stage==9 && done()){click(mc,-999,0,ContainerInput.PICKUP);next(10);} else if(stage==10 && frames>20){ equal(mc.player.containerMenu.getCarried(),null,"Accepted outside client cursor"); schedule(mc,()->{drop(mc,item(120,16));equal(player(mc).containerMenu.getCarried(),null,"Accepted outside server cursor");mark(mc,"network_accepted_outside_drop"); body(mc).setItem(6,item(120,17));player(mc).getInventory().getNonEquipmentItems().set(9,item(120,5));refresh(mc);});next(11); - } else if(stage==11 && done() && frames>20){click(mc,48,0,ClickType.PICKUP);click(mc,10,0,ClickType.PICKUP_ALL);next(12);} + } else if(stage==11 && done() && frames>20){click(mc,48,0,ContainerInput.PICKUP);click(mc,10,0,ContainerInput.PICKUP_ALL);next(12);} else if(stage==12 && frames>20){ equal(mc.player.containerMenu.getCarried(),item(120,42),"Double-click client cursor");equal(slot(mc,8),item(121,10),"Double-click changed unlike NBT"); schedule(mc,()->{equal(player(mc).containerMenu.getCarried(),item(120,42),"Double-click server cursor");equal(body(mc).getItem(6),null,"Double-click source 6");equal(body(mc).getItem(7),null,"Double-click source 7");mark(mc,"network_double_click_exact_nbt");});next(13); - } else if(stage==13 && done()){click(mc,48,0,ClickType.PICKUP);next(14);} + } else if(stage==13 && done()){click(mc,48,0,ContainerInput.PICKUP);next(14);} else if(stage==14 && frames>20){schedule(mc,()->{player(mc).setGameMode(GameType.CREATIVE);body(mc).setItem(6,item(120,17));refresh(mc);});next(15);} - else if(stage==15 && done() && frames>20){RuntimeChecks.require(mc.player.getAbilities().instabuild,"Client not creative");click(mc,6,2,ClickType.CLONE);next(16);} + else if(stage==15 && done() && frames>20){RuntimeChecks.require(mc.player.getAbilities().instabuild,"Client not creative");click(mc,6,2,ContainerInput.CLONE);next(16);} else if(stage==16 && frames>20){ equal(mc.player.containerMenu.getCarried(),item(120,64),"Creative clone client NBT");equal(slot(mc,6),item(120,17),"Creative clone changed source"); - click(mc,-999,8,ClickType.QUICK_CRAFT);click(mc,7,9,ClickType.QUICK_CRAFT);click(mc,9,9,ClickType.QUICK_CRAFT);click(mc,-999,10,ClickType.QUICK_CRAFT);next(17); + click(mc,-999,8,ContainerInput.QUICK_CRAFT);click(mc,7,9,ContainerInput.QUICK_CRAFT);click(mc,9,9,ContainerInput.QUICK_CRAFT);click(mc,-999,10,ContainerInput.QUICK_CRAFT);next(17); } else if(stage==17 && frames>20){ equal(slot(mc,7),item(120,64),"Creative client fill first");equal(slot(mc,9),item(120,64),"Creative client fill second");equal(mc.player.containerMenu.getCarried(),null,"Creative fill vanilla cursor");screenshot(mc,"drop-creative-filled.png"); schedule(mc,()->{equal(body(mc).getItem(7),item(120,64),"Creative server fill first");equal(body(mc).getItem(9),item(120,64),"Creative server fill second");equal(player(mc).getInventory().getNonEquipmentItems().get(9),item(120,42),"Creative operation changed recovered inventory");mark(mc,"network_creative_clone_and_fill"); @@ -114,7 +114,7 @@ private void prepare(Minecraft mc){ private static void equal(ItemStack a,ItemStack b,String message){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),message+" expected="+b+" actual="+a);} private void drop(Minecraft mc,ItemStack expected){int count=0;for(Entity e:new ArrayList(FixtureApi.entities(player(mc).level())))if(e instanceof ItemEntity && !e.isRemoved() && !preexistingDrops.contains(e.getUUID())){equal(((ItemEntity)e).getItem(),expected,"Accepted new world drop NBT/count");count++;e.discard();}RuntimeChecks.require(count==1,"Expected exactly one new accepted drop, actual="+count);} private static ItemStack slot(Minecraft mc,int index){return mc.player.containerMenu.getSlot(index).getItem();} - private static void click(Minecraft mc,int slot,int button,ClickType type){mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,button,type,mc.player);} + private static void click(Minecraft mc,int slot,int button,ContainerInput type){mc.gameMode.handleContainerInput(mc.player.containerMenu.containerId,slot,button,type,mc.player);} private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} private static boolean ready(Minecraft mc){return mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null;} private boolean open(Minecraft mc) throws Exception { @@ -133,9 +133,8 @@ private boolean open(Minecraft mc) throws Exception { if (player == null || corpse == null || !player.connection.hasClientLoaded() || !corpse.stillValid(player)) { return false; } - var packet = net.minecraft.network.protocol.game.ServerboundInteractPacket - .createInteractionPacket(corpse, false, InteractionHand.MAIN_HAND); - return packet.isWithinRange(player, corpse.getBoundingBox(), 3.0); + return player.level().getWorldBorder().isWithinBounds(corpse.blockPosition()) + && player.isWithinEntityInteractionRange(corpse.getBoundingBox(), 3.0); }); return false; } @@ -143,7 +142,7 @@ private boolean open(Minecraft mc) throws Exception { boolean ready = openReady.get(); openReady = null; if (!ready) return false; - var result = mc.gameMode.interact(mc.player, target, InteractionHand.MAIN_HAND); + var result = mc.gameMode.interact(mc.player, target, new net.minecraft.world.phys.EntityHitResult(target), InteractionHand.MAIN_HAND); System.out.println("DROP_NATIVE_INTERACTION_SENT target=" + target.getUUID() + " clientResult=" + result); return true; } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java index e6f4418..32f5980 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureApi.java @@ -44,8 +44,8 @@ static net.minecraft.world.level.storage.PlayerDataStorage playerData(MinecraftS static final class ClientWorlds { static void create(net.minecraft.client.Minecraft client,String folder,String title,net.minecraft.world.level.GameType mode,boolean commands) { client.createWorldOpenFlows().createFreshLevel(folder, - new net.minecraft.world.level.LevelSettings(title,mode,false,net.minecraft.world.Difficulty.NORMAL,commands, - new net.minecraft.world.level.gamerules.GameRules(net.minecraft.world.flag.FeatureFlags.DEFAULT_FLAGS),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), + new net.minecraft.world.level.LevelSettings(title,mode,net.minecraft.world.level.LevelSettings.DifficultySettings.DEFAULT,commands, + net.minecraft.world.level.WorldDataConfiguration.DEFAULT), new net.minecraft.world.level.levelgen.WorldOptions(4267321L,false,false), access->access.lookupOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) .getOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions(),new net.minecraft.client.gui.screens.TitleScreen()); @@ -102,7 +102,7 @@ static net.minecraft.nbt.ListTag saveEnderInventory(ServerPlayer player) { static net.minecraft.network.protocol.game.ServerboundContainerClickPacket clickPacket( ServerPlayer player, int stateId, int slot, int button, - net.minecraft.world.inventory.ClickType type, net.minecraft.world.item.ItemStack predicted) { + net.minecraft.world.inventory.ContainerInput type, net.minecraft.world.item.ItemStack predicted) { var ops = player.registryAccess().createSerializationContext(net.minecraft.util.HashOps.CRC32C_INSTANCE); net.minecraft.network.HashedPatchMap.HashGenerator hash = component -> component.encodeValue(ops).getOrThrow().asInt(); var changed = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap(); @@ -128,12 +128,12 @@ static void writeAdditional(cyano.lootable.entities.EntityLootableBody body, Com static Entity readChunkEntity(CompoundTag data, ServerLevel world, net.minecraft.world.level.chunk.LevelChunk chunk) { // Invoke the actual native chunk serializer; do not replace its admission callback. CompoundTag root=new CompoundTag();net.minecraft.nbt.ListTag list=new net.minecraft.nbt.ListTag();list.add(data.copy());root.put("entities",list); - root.putInt("xPos",chunk.getPos().x);root.putInt("zPos",chunk.getPos().z);root.put("sections",new net.minecraft.nbt.ListTag());root.putInt("DataVersion",4671); + root.putInt("xPos",chunk.getPos().x());root.putInt("zPos",chunk.getPos().z());root.put("sections",new net.minecraft.nbt.ListTag());root.putInt("DataVersion",4790); // Current fixture NBT follows the real native conversion boundary too. // The post-load admission callback remains Minecraft's own method. CompoundTag fixed=cyano.lootable.upgrade.NativeNbt.update( cyano.lootable.upgrade.UpgradePreservation.wrapper(net.minecraft.util.datafix.DataFixers.getDataFixer()), - net.minecraft.util.datafix.fixes.References.CHUNK,root,4671,4671); + net.minecraft.util.datafix.fixes.References.CHUNK,root,4790,4790); try { java.lang.reflect.Method nativeRead=null; for(java.lang.reflect.Method method:net.minecraft.world.level.chunk.storage.SerializableChunkData.class.getDeclaredMethods()) @@ -146,7 +146,7 @@ static Entity readChunkEntity(CompoundTag data, ServerLevel world, net.minecraft if(processor==null)throw new IllegalStateException("Native entity admission returned no processor"); processor.run(chunk); java.util.UUID id=readUuid(TestNativeNbt.typedList(fixed, "entities", 10).getCompoundOrEmpty(0), "UUID"); - return sectionEntities(world,chunk.getPos().toLong()).filter(entity->entity.getUUID().equals(id)).findFirst().orElse(null); + return sectionEntities(world,chunk.getPos().pack()).filter(entity->entity.getUUID().equals(id)).findFirst().orElse(null); }catch(ReflectiveOperationException failure){throw new IllegalStateException("Native chunk admission failed",failure);} } private static List compounds(CompoundTag data, String key) { @@ -157,7 +157,7 @@ static List entities(ServerLevel world,int chunkX,int chunkZ) { // Forge 50 may have admitted a drop to the native section before its // chunk is tracked. Count both stores, once per actual entity. var all=new java.util.LinkedHashSet<>(entities(world)); - sectionEntities(world,net.minecraft.world.level.ChunkPos.asLong(chunkX,chunkZ)).forEach(all::add); + sectionEntities(world,net.minecraft.world.level.ChunkPos.pack(chunkX,chunkZ)).forEach(all::add); return new ArrayList<>(all); } private static java.util.stream.Stream sectionEntities(ServerLevel world,long chunk) { @@ -199,15 +199,9 @@ static void awaitEntityInbox(ServerLevel world,int x,int z) { net.minecraft.world.level.entity.PersistentEntitySectionManager manager= (net.minecraft.world.level.entity.PersistentEntitySectionManager)singleField( ServerLevel.class,net.minecraft.world.level.entity.PersistentEntitySectionManager.class).get(world); - long chunk=net.minecraft.world.level.ChunkPos.asLong(x,z); + long chunk=net.minecraft.world.level.ChunkPos.pack(x,z); try { - NativeEntityInbox.await(manager,chunk,()->{ - // The real server loop services both executors. Fixture - // callbacks execute before that loop, so pump both queues. - boolean serverTask=world.getServer().pollTask(); - boolean chunkTask=world.getChunkSource().pollTask(); - return serverTask || chunkTask; - },System::nanoTime,System.nanoTime()+java.util.concurrent.TimeUnit.SECONDS.toNanos(30),world.getServer()::managedBlock); + NativeEntityInbox.await(manager,chunk,world.getChunkSource()::pollTask,System::nanoTime,System.nanoTime()+java.util.concurrent.TimeUnit.SECONDS.toNanos(30),world.getServer()::managedBlock); }catch(IllegalStateException failure){ net.minecraft.world.level.chunk.LevelChunk cached=world.getChunkSource().getChunkNow(x,z); String state="full="+(cached==null?"absent":cached.getFullStatus())+" manager="+manager.gatherStats(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java index f271473..8822e28 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java @@ -12,8 +12,7 @@ private FixtureStackData() { } static net.minecraft.core.HolderLookup.Provider registries() { var server=net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(); if(server!=null && server.isSameThread())return server.registryAccess(); - var client=net.minecraftforge.fml.DistExecutor.unsafeCallWhenOn( - net.minecraftforge.api.distmarker.Dist.CLIENT,()->ClientChecks::fixtureRegistries); + var client=net.minecraftforge.fml.loading.FMLEnvironment.dist.isClient() ? ClientChecks.fixtureRegistries() : null; if(client!=null)return client; if(server!=null)return server.registryAccess(); return net.minecraft.data.registries.VanillaRegistries.createLookup(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java index ab3e001..5c63f0c 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java @@ -101,8 +101,12 @@ public void tick(TickEvent.ServerTickEvent.Post e){ RuntimeChecks.require(++ticks<2400,"Two-client timeout phase="+phase); if(new File(sync,"abort").exists())throw new AssertionError("Runner aborted multiplayer fixture"); ServerPlayer a=player("A"),b=player("B"); + ServerLevel w=server.getLevel(FixtureApi.dimensionKey(0)); + if(phase>=1 && phase<=7 && w.getEntity(BODY) instanceof EntityLootableBody active && active!=body){ + System.out.println("TCP_CORPSE_REINDEXED old="+body.getId()+" reason="+body.getRemovalReason()+" active="+active.getId()); + body=active; + } if(phase==0 && a!=null && b!=null){ - ServerLevel w=server.getLevel(FixtureApi.dimensionKey(0)); for(int x=93;x<=100;x++)for(int z=93;z<=100;z++)w.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); w.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.SPAWN_MOBS,false,null); body=new EntityLootableBody(w);body.setUUID(BODY);body.setPos(96.5,80.5,96.5);body.setNoGravity(true);body.setUserName("SharedFixture");RuntimeChecks.disableVacuum(body); @@ -111,9 +115,17 @@ public void tick(TickEvent.ServerTickEvent.Post e){ a.connection.teleport(96.5,80,94.5,0,0);b.connection.teleport(98.5,80,96.5,0,0); setPhase(1); }else if(phase==1 && ack("A",1) && ack("B",1))setPhase(2); - else if(phase==2 && ack("A",2) && ack("B",2)){ - RuntimeChecks.require((body.getItem(6)).isEmpty() && count(a,19)+count(b,19)==23,"Two TCP clients duplicated/lost contested stack"); - RuntimeChecks.require(count(a,19)==0 || count(b,19)==0,"Both clients recovered the same stack"); + else if(phase==2){ + RuntimeChecks.require(w.getEntity(BODY)==body && a.containerMenu.getSlot(6).container==body + && b.containerMenu.getSlot(6).container==body,"Two TCP viewers must reference the active corpse"); + ItemStack remaining=body.getItem(6); + int inBody=remaining.getCount(), inA=count(a,19), inB=count(b,19); + RuntimeChecks.require((remaining.isEmpty() || RuntimeChecks.sameStack(remaining,stack(19,23))) + && inBody+inA+inB==23,"Two TCP clients duplicated/lost contested stack: "+inBody+"/"+inA+"/"+inB); + RuntimeChecks.require(inA==0 || inB==0,"Both clients recovered the same stack"); + // A received test acknowledgement does not by itself prove + // that Minecraft has applied a queued container click. + if(inBody!=0 || !ack("A",2) || !ack("B",2))return; mark("two_tcp_clients_contested_shift_click"); a.getInventory().clearContent();b.getInventory().clearContent();body.clearContent(); a.containerMenu.setCarried(stack(19,10));b.getInventory().getNonEquipmentItems().set(0,stack(20,7));refresh(a);refresh(b);setPhase(3); @@ -155,7 +167,7 @@ static void verifyReload(MinecraftServer server) throws Exception { File folder=world.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile();RuntimeChecks.verifyExpected(folder,"multiplayer-body",body); for(String role:new String[]{"A","B"}){ CompoundTag expected;try(FileInputStream in=new FileInputStream(new File(folder,"multiplayer-player-"+role+".nbt"))){expected=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} - CompoundTag actual;try(FileInputStream in=new FileInputStream(new File(folder,"playerdata/"+expected.getStringOr("UUID", "")+".dat"))){actual=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} + CompoundTag actual;try(FileInputStream in=new FileInputStream(new File(folder,net.minecraft.world.level.storage.LevelResource.PLAYER_DATA_DIR.id()+"/"+expected.getStringOr("UUID", "")+".dat"))){actual=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} RuntimeChecks.require(expected.get("Inventory").equals(actual.get("Inventory")),"Real player "+role+" inventory changed on disk"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java index f9efd09..d323eca 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerClientChecks.java @@ -10,7 +10,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.TitleScreen; import net.minecraft.world.entity.Entity; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.InteractionHand; import net.minecraft.client.Screenshot; import net.minecraftforge.common.MinecraftForge; @@ -51,24 +51,24 @@ private void tick(TickEvent.ClientTickEvent.Post event){ } if(frames%100==0)System.out.println("TCP_CLIENT_STATE role="+role+" phase="+phase+" screen="+(mc.screen==null?"world":mc.screen.getClass().getSimpleName())+" container="+mc.player.containerMenu.getClass().getSimpleName()+" window="+mc.player.containerMenu.containerId+" position="+mc.player.blockPosition()+" completed="+completed); if(!(mc.screen instanceof CorpseGUIContainer)){ - if(frames%10==0)for(Entity entity:FixtureApi.entities(mc.level))if(entity instanceof EntityLootableBody && entity.getUUID().equals(MultiplayerChecks.BODY))mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND); + if(frames%10==0)for(Entity entity:FixtureApi.entities(mc.level))if(entity instanceof EntityLootableBody && entity.getUUID().equals(MultiplayerChecks.BODY))mc.gameMode.interact(mc.player, entity, new net.minecraft.world.phys.EntityHitResult(entity), InteractionHand.MAIN_HAND); return; } if(completed.contains(phase) || frames<15)return;int window=mc.player.containerMenu.containerId; if(phase==1){ if(!RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(6).getItem(),MultiplayerChecks.stack(19,23)))return; shot(mc,"shared-ready.png");ack(1); - }else if(phase==2){mc.gameMode.handleInventoryMouseClick(window,6,0,ClickType.QUICK_MOVE,mc.player);ack(2);} + }else if(phase==2){mc.gameMode.handleContainerInput(window,6,0,ContainerInput.QUICK_MOVE,mc.player);ack(2);} else if(phase==3 && role.equals("A")){ if(!RuntimeChecks.sameStack(mc.player.containerMenu.getCarried(),MultiplayerChecks.stack(19,10)))return; - mc.gameMode.handleInventoryMouseClick(window,-999,0,ClickType.QUICK_CRAFT,mc.player);mc.gameMode.handleInventoryMouseClick(window,6,1,ClickType.QUICK_CRAFT,mc.player);mc.gameMode.handleInventoryMouseClick(window,7,1,ClickType.QUICK_CRAFT,mc.player);ack(3); + mc.gameMode.handleContainerInput(window,-999,0,ContainerInput.QUICK_CRAFT,mc.player);mc.gameMode.handleContainerInput(window,6,1,ContainerInput.QUICK_CRAFT,mc.player);mc.gameMode.handleContainerInput(window,7,1,ContainerInput.QUICK_CRAFT,mc.player);ack(3); }else if(phase==4 && role.equals("B")){ if(!RuntimeChecks.sameStack(mc.player.getInventory().getNonEquipmentItems().get(0),MultiplayerChecks.stack(20,7)))return; - mc.gameMode.handleInventoryMouseClick(window,6,0,ClickType.SWAP,mc.player);ack(4); - }else if(phase==5 && role.equals("A")){mc.gameMode.handleInventoryMouseClick(window,-999,2,ClickType.QUICK_CRAFT,mc.player);ack(5);} + mc.gameMode.handleContainerInput(window,6,0,ContainerInput.SWAP,mc.player);ack(4); + }else if(phase==5 && role.equals("A")){mc.gameMode.handleContainerInput(window,-999,2,ContainerInput.QUICK_CRAFT,mc.player);ack(5);} else if(phase==6 && role.equals("A")){ MultiplayerChecks.equal(mc.player.containerMenu.getCarried(),MultiplayerChecks.stack(19,5),"TCP client cursor prediction differs"); - mc.gameMode.handleInventoryMouseClick(window,48,0,ClickType.PICKUP,mc.player);ack(6); + mc.gameMode.handleContainerInput(window,48,0,ContainerInput.PICKUP,mc.player);ack(6); }else if(phase==7){ MultiplayerChecks.equal(mc.player.containerMenu.getSlot(6).getItem(),MultiplayerChecks.stack(20,7),"Shared GUI slot 6 differs");MultiplayerChecks.equal(mc.player.containerMenu.getSlot(7).getItem(),MultiplayerChecks.stack(19,5),"Shared GUI slot 7 differs"); MultiplayerChecks.equal(mc.player.containerMenu.getCarried(),null,"Shared GUI cursor differs");shot(mc,"shared-final.png");ack(7); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java index c944129..690df21 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java @@ -151,7 +151,7 @@ void tick(Minecraft mc) throws Exception { } else if(stage==4) { for(Entity entity:FixtureApi.entities(mc.level))if(entity.getUUID().equals(BODY)) { RuntimeChecks.require("Corpse".equals(entity.getName().getString()) && !entity.hasCustomName(),"Network corpse has wrong English fallback/custom name"); - mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND);stage=5;frames=0;break; + mc.gameMode.interact(mc.player, entity, new net.minecraft.world.phys.EntityHitResult(entity), InteractionHand.MAIN_HAND);stage=5;frames=0;break; } } else if(stage==5 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { for(int slot=0;slot<48;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(slot)),"Name fix changed network inventory slot "+slot); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java index bbc3cad..bf35f26 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java @@ -17,7 +17,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -62,7 +62,7 @@ else if(stage==30 && done() && frames>20 && trackedOnClient(mc)){schedule(mc,()- before=NbtRecoveryChecks.items(b);visible=full(b);RuntimeChecks.saveExpected(folder(mc),"nbt-client-before",b); });next(4);} else if(stage==4 && done() && frames>20){if(open(mc))next(5);} - else if(stage==5 && gui(mc) && frames>20){verifyGui(mc);equal(mc.player.containerMenu.getSlot(6).getItem(),NbtRecoveryChecks.item(120,17),"Malformed GUI recovery source differs");screenshot(mc,"nbt-recovered-corpse.png");mark(mc,"malformed_disk_load_gui_exact_inventory");mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,6,0,ClickType.QUICK_MOVE,mc.player);next(6);} + else if(stage==5 && gui(mc) && frames>20){verifyGui(mc);equal(mc.player.containerMenu.getSlot(6).getItem(),NbtRecoveryChecks.item(120,17),"Malformed GUI recovery source differs");screenshot(mc,"nbt-recovered-corpse.png");mark(mc,"malformed_disk_load_gui_exact_inventory");mc.gameMode.handleContainerInput(mc.player.containerMenu.containerId,6,0,ContainerInput.QUICK_MOVE,mc.player);next(6);} else if(stage==6 && frames>25){schedule(mc,()->{ Map after=NbtRecoveryChecks.items(body(mc));for(int i=0;i entity.getUUID().equals(bodyId)); } - private boolean open(Minecraft mc){for(Entity e:FixtureApi.entities(mc.level))if(e.getUUID().equals(bodyId)){RuntimeChecks.require(((EntityLootableBody)e).getCustomNameTag().equals("NbtFixture"),"Malformed name reached tracked client entity");RuntimeChecks.require(e instanceof EntityLootableBody && Float.isFinite(((EntityLootableBody)e).getHealth()) && ((EntityLootableBody)e).getHealth()>0,"Malformed health reached tracked client entity");RuntimeChecks.require(((EntityLootableBody)e).getActiveEffects().isEmpty() && ((EntityLootableBody)e).hurtTime==0,"Malformed effect/hurt state reached tracked client entity");PositionMotionChecks.finite((EntityLootableBody)e);RotationFallChecks.finite((EntityLootableBody)e);mc.gameMode.interact(mc.player,e,InteractionHand.MAIN_HAND);return true;}return false;} + private boolean open(Minecraft mc){for(Entity e:FixtureApi.entities(mc.level))if(e.getUUID().equals(bodyId)){RuntimeChecks.require(((EntityLootableBody)e).getCustomNameTag().equals("NbtFixture"),"Malformed name reached tracked client entity");RuntimeChecks.require(e instanceof EntityLootableBody && Float.isFinite(((EntityLootableBody)e).getHealth()) && ((EntityLootableBody)e).getHealth()>0,"Malformed health reached tracked client entity");RuntimeChecks.require(((EntityLootableBody)e).getActiveEffects().isEmpty() && ((EntityLootableBody)e).hurtTime==0,"Malformed effect/hurt state reached tracked client entity");PositionMotionChecks.finite((EntityLootableBody)e);RotationFallChecks.finite((EntityLootableBody)e);mc.gameMode.interact(mc.player, e, new net.minecraft.world.phys.EntityHitResult(e), InteractionHand.MAIN_HAND);return true;}return false;} private void schedule(Minecraft mc,Action action){pending=mc.getSingleplayerServer().submitAsync(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} private boolean done() throws Exception{if(!pending.isDone())return false;pending.get();return true;} private void next(int value){stage=value;frames=0;System.out.println("NBT_CLIENT_STAGE "+stage);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java index f651b14..4f67060 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java @@ -95,7 +95,7 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex private static void boundaries(){CompoundTag n=base();n.put("Pos",vector(-3e7,2e7,3e7));n.put("Motion",vector(-10,0,10));EntityLootableBody b=read(n);sameValue(b.getX(),-3e7,"Boundary X");sameValue(b.getY(),2e7,"Boundary Y");sameValue(b.getZ(),3e7,"Boundary Z");sameValue(b.getDeltaMovement().x,-10,"Boundary motion");sameValue(b.getDeltaMovement().z,10,"Boundary motion");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Boundary quarantined");stable(b);} private static void voidPosition(){CompoundTag n=base();n.put("Pos",vector(160.5,-65,160.5));EntityLootableBody b=read(n);sameValue(b.getY(),-65,"Valid void position moved");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid negative Y quarantined");stable(b);} private static void repeated(){EntityLootableBody b=read(base());b.setPos(42,100,43);b.setDeltaMovement(4,b.getDeltaMovement().y,b.getDeltaMovement().z);b.setDeltaMovement(b.getDeltaMovement().x,5,b.getDeltaMovement().z);b.setDeltaMovement(b.getDeltaMovement().x,b.getDeltaMovement().y,6);CompoundTag n=base();n.putString("Pos","opaque");n.remove("Motion");FixtureApi.loadEntityData(b,n);sameValue(b.getX(),0,"Reused old X");sameValue(b.getY(),64,"Reused old Y");sameValue(b.getZ(),0,"Reused old Z");for(double v:values(b,"Motion"))sameValue(v,0,"Reused old motion");stable(b);} - private static EntityLootableBody chunk(CompoundTag n,int x,int z){CompoundTag before=n.copy();Entity e=FixtureApi.readChunkEntity(n,world,RuntimeChecks.emptyChunk(world,x,z));require(e instanceof EntityLootableBody && !e.isRemoved(),"Chunk discarded or killed malformed corpse");require(NameLifetimeChecks.same(before,n),"Chunk loader mutated source");EntityLootableBody b=(EntityLootableBody)e;require(FixtureApi.sectionContains(b,x,z),"Admission coordinates wrong");System.out.println("POSITION_NATIVE_SECTION_INDEX chunk="+x+","+z+" forgeAdded="+b.isAddedToWorld());finite(b);return b;} + private static EntityLootableBody chunk(CompoundTag n,int x,int z){CompoundTag before=n.copy();Entity e=FixtureApi.readChunkEntity(n,world,RuntimeChecks.emptyChunk(world,x,z));require(e instanceof EntityLootableBody && !e.isRemoved(),"Chunk discarded or killed malformed corpse");require(NameLifetimeChecks.same(before,n),"Chunk loader mutated source");EntityLootableBody b=(EntityLootableBody)e;require(FixtureApi.sectionContains(b,x,z),"Admission coordinates wrong");System.out.println("POSITION_NATIVE_SECTION_INDEX chunk="+x+","+z+" forgeAdded="+b.isAddedToLevel());finite(b);return b;} private static void chunkAdmission(){CompoundTag n=base();n.put("Pos",vector(Double.NaN,80,160.5));EntityLootableBody b=chunk(n,10,10);sameValue(b.getX(),168.5,"Containing chunk anchor missing");sameValue(b.getZ(),160.5,"Valid Z was moved");NameLifetimeChecks.retained(b,"Pos",n.get("Pos"));stable(b);} private static void allInvalidChunk(){CompoundTag n=base();n.putString("Pos","opaque");EntityLootableBody b=chunk(n,-11,10);sameValue(b.getX(),-167.5,"Negative containing chunk center");sameValue(b.getY(),64,"Invalid Y default");sameValue(b.getZ(),168.5,"Containing Z chunk center");stable(b);} private static void finiteMismatch(boolean x){CompoundTag n=base();EntityLootableBody b=chunk(n,x?-11:10,x?10:11);sameValue(b.getX(),x?-167.5:160.5,"Finite mismatched X");sameValue(b.getZ(),x?160.5:184.5,"Finite mismatched Z");NameLifetimeChecks.retained(b,"Pos",n.get("Pos"));stable(b);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java index 9f2ce8c..15fedfe 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java @@ -22,7 +22,7 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.item.Items; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -142,7 +142,7 @@ private static ServerPlayer respawn(ServerPlayer player) { acknowledgeNativeLoad(replacement); replacement.setPos(32.5,80,0.5);return replacement; } - private static void click(ServerPlayer p,int slot,int button,ClickType type) throws IOException { + private static void click(ServerPlayer p,int slot,int button,ContainerInput type) throws IOException { QuietConnection observed=(QuietConnection)p.connection;int before=observed.menuEvents; // Server-side conservation fixture: omit predicted slots and require // native authoritative corrections. Real clients test normal prediction. @@ -184,9 +184,9 @@ private static void deathSequence() throws Exception { Map expected=tagged(p); EntityLootableBody first=death(p);RuntimeChecks.require(expected.equals(tagged(first)),"First death lost inventory"); p=respawn(p);CorpseContainer gui=new CorpseContainer(17,p.getInventory(),first);p.containerMenu=gui;p.initMenu(gui); - click(p,4,0,ClickType.SWAP); + click(p,4,0,ContainerInput.SWAP); RuntimeChecks.require(RuntimeChecks.sameStack(p.getInventory().getNonEquipmentItems().get(0),shovel()),"Key1 did not recover original main hand"); - for(int slot=0;slot<48;slot++)if(gui.getSlot(slot).hasItem())click(p,slot,0,ClickType.QUICK_MOVE); + for(int slot=0;slot<48;slot++)if(gui.getSlot(slot).hasItem())click(p,slot,0,ContainerInput.QUICK_MOVE); RuntimeChecks.require(first.isEmpty() && expected.equals(tagged(p)),"First recovery lost or duplicated tagged inventory: remaining="+inventory(first)+" expected="+expected+" player="+tagged(p)+" state="+gui.getStateId()); FriendlyByteBuf close=new FriendlyByteBuf(Unpooled.buffer()); try {ServerboundContainerClosePacket.STREAM_CODEC.encode(close,new ServerboundContainerClosePacket(gui.containerId));p.connection.handleContainerClose(ServerboundContainerClosePacket.STREAM_CODEC.decode(close));RuntimeChecks.require(!close.isReadable(),"Native issue close left undecoded fields");} @@ -245,10 +245,10 @@ private static final class Support implements AutoCloseable { EntityLootableBody body; int cx,cz,nx,nz; BlockPos platform; boolean forced; Map expected; - void unload(){world.setChunkForced(cx,cz,false);net.minecraft.world.level.ChunkPos pos=new net.minecraft.world.level.ChunkPos(cx,cz);world.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.FORCED,pos,0,true);unloadChunk(nx,nz);RuntimeChecks.require(!world.getChunkSource().hasChunk(nx,nz),"Support chunk did not unload");} + void unload(){world.setChunkForced(cx,cz,false);net.minecraft.world.level.ChunkPos pos=new net.minecraft.world.level.ChunkPos(cx,cz);world.getChunkSource().addTicketWithRadius(net.minecraft.server.level.TicketType.FORCED,pos,0);unloadChunk(nx,nz);RuntimeChecks.require(!world.getChunkSource().hasChunk(nx,nz),"Support chunk did not unload");} void frozen(){for(int i=0;i<100;i++)body.tick();System.out.println("ISSUE17_UNLOADED_SUPPORT y="+body.getY()+" alive="+body.isAlive()+" inventory="+inventory(body).size()+" supportLoaded="+world.getChunkSource().hasChunk(nx,nz));RuntimeChecks.require(body.isAlive() && body.getY()==80 && expected.equals(inventory(body)),"Corpse fell through unloaded neighbour support and lost recoverable inventory");} void resume(){RuntimeChecks.awaitChunk(world,nx,nz);FixtureApi.awaitTicking(body);for(int i=0;i<10;i++)world.tickNonPassenger(body);RuntimeChecks.require(body.onGround() && body.getY()==80 && FixtureApi.sectionContains(body,cx,cz) && expected.equals(inventory(body)),"Corpse failed to resume safely after support terrain loaded");} - @Override public void close(){if(forced)world.setChunkForced(cx,cz,false);net.minecraft.world.level.ChunkPos pos=new net.minecraft.world.level.ChunkPos(cx,cz);world.getChunkSource().removeRegionTicket(net.minecraft.server.level.TicketType.FORCED,pos,0,true);} + @Override public void close(){if(forced)world.setChunkForced(cx,cz,false);net.minecraft.world.level.ChunkPos pos=new net.minecraft.world.level.ChunkPos(cx,cz);world.getChunkSource().removeTicketWithRadius(net.minecraft.server.level.TicketType.FORCED,pos,0);} } private static Support support(int axis,int direction) {return support(axis,direction,null);} private static Support support(int axis,int direction,UUID savedId) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java index 04134dd..10e46f2 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java @@ -23,7 +23,7 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.Items; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -78,11 +78,11 @@ void tick(Minecraft mc) throws Exception { } else if(stage==4 && done() && open(mc,firstId)) {next(5); } else if(stage==5 && gui(mc) && frames>15) { equal(mc.player.containerMenu.getSlot(4).getItem(),original(),"First corpse main hand");shot(mc,"issue5-first-main-hand.png"); - mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,4,0,ClickType.SWAP,mc.player);next(6); + mc.gameMode.handleContainerInput(mc.player.containerMenu.containerId,4,0,ContainerInput.SWAP,mc.player);next(6); } else if(stage==6 && frames>15) { equal(mc.player.getInventory().getNonEquipmentItems().get(0),original(),"Key1 original shovel recovery"); for(int slot=0;slot<48;slot++)if(mc.player.containerMenu.getSlot(slot).hasItem()) - mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,0,ClickType.QUICK_MOVE,mc.player); + mc.gameMode.handleContainerInput(mc.player.containerMenu.containerId,slot,0,ContainerInput.QUICK_MOVE,mc.player); next(7); } else if(stage==7 && frames>20) { equal(mc.player.getInventory().getNonEquipmentItems().get(0),original(),"Original shovel changed before switch"); @@ -138,7 +138,7 @@ void tick(Minecraft mc) throws Exception { } else if(stage==16 && gui(mc) && frames>20) { verifyClientOriginals(mc);shot(mc,"issue5-reloaded-corpse.png"); for(int slot=0;slot<48;slot++)if(mc.player.containerMenu.getSlot(slot).hasItem()) - mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,0,ClickType.QUICK_MOVE,mc.player); + mc.gameMode.handleContainerInput(mc.player.containerMenu.containerId,slot,0,ContainerInput.QUICK_MOVE,mc.player); next(17); } else if(stage==17 && frames>20) { mc.player.closeContainer();schedule(mc,()->{ @@ -242,7 +242,7 @@ private boolean open(Minecraft mc,UUID id) throws Exception { approachPending=false; for(Entity e:FixtureApi.entities(mc.level))if(e instanceof EntityLootableBody && e.getUUID().equals(id) && mc.player.distanceToSqr(e)<=9){ System.out.println("ISSUE5_GUI_OPEN_REQUEST corpse="+id+" distance_sq="+mc.player.distanceToSqr(e)); - mc.gameMode.interact(mc.player,e,InteractionHand.MAIN_HAND); + mc.gameMode.interact(mc.player, e, new net.minecraft.world.phys.EntityHitResult(e), InteractionHand.MAIN_HAND); break; } return false; diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java index 3c1f2b2..c66eff9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java @@ -119,7 +119,7 @@ else if(phase==1&&frames>20){ Entity entity=mc.level.getEntity(entityId); if(!(entity instanceof EntityLootableBody)||!entity.getUUID().equals(uuid)||!FixtureApi.entities(mc.level).contains(entity)||entity.isRemoved())return; if(role.equals("A")) { - if(!(mc.screen instanceof CorpseGUIContainer)){mc.gameMode.interact(mc.player,entity,InteractionHand.MAIN_HAND);return;} + if(!(mc.screen instanceof CorpseGUIContainer)){mc.gameMode.interact(mc.player, entity, new net.minecraft.world.phys.EntityHitResult(entity), InteractionHand.MAIN_HAND);return;} boolean found=false;for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(520+index)))found=true; if(!found||++guiFrames<10)return; } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java index 57c9f6b..b3a8bcd 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketTrace.java @@ -47,7 +47,7 @@ static String describe(Object packet) { if(packet instanceof ClientboundRemoveEntitiesPacket destroy) return "DESTROY ids="+Arrays.toString(destroy.getEntityIds().toIntArray()); if(packet instanceof ClientboundForgetLevelChunkPacket unload) - return "UNLOAD chunk="+unload.pos().x+","+unload.pos().z; + return "UNLOAD chunk="+unload.pos().x()+","+unload.pos().z(); if(packet instanceof ClientboundLevelChunkWithLightPacket chunk) return "CHUNK chunk="+chunk.getX()+","+chunk.getZ()+" full=true"; if(packet instanceof ClientboundPlayerCombatEnterPacket)return "COMBAT event=ENTER_COMBAT"; diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java index 0a4c8ff..a8d475b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java @@ -102,7 +102,7 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex private static void validYaw(float yaw){CompoundTag n=base();n.put("Rotation",rotation(yaw,17));EntityLootableBody b=read(n);angles(b,yaw%360F);require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid yaw quarantined");stable(b);} private static void validPitch(float pitch){CompoundTag n=base();n.put("Rotation",rotation(37.25F,pitch));EntityLootableBody b=read(n);angles(b,37.25F);require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Finite pitch quarantined despite existing flattening");stable(b);} private static void missing(){CompoundTag n=base();n.remove("Rotation");n.remove("FallDistance");EntityLootableBody b=read(n);angles(b,0);value(b.fallDistance,0,"Missing distance not zero");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Missing fields invented raw data");stable(b);} - private static void repeated(){EntityLootableBody b=read(base());net.minecraftforge.fml.util.ObfuscationReflectionHelper.setPrivateValue(Entity.class,b,Float.NaN,"f_19857_");net.minecraftforge.fml.util.ObfuscationReflectionHelper.setPrivateValue(Entity.class,b,Float.POSITIVE_INFINITY,"f_19858_");b.yRotO=Float.NEGATIVE_INFINITY;b.xRotO=Float.NaN;b.yHeadRot=Float.NaN;b.yHeadRotO=Float.NaN;b.yBodyRot=Float.POSITIVE_INFINITY;b.yBodyRotO=Float.NEGATIVE_INFINITY;b.fallDistance=Float.NaN;CompoundTag n=base();n.remove("Rotation");n.remove("FallDistance");FixtureApi.loadEntityData(b,n);angles(b,0);value(b.fallDistance,0,"Repeated read kept stale fall distance");stable(b);} + private static void repeated(){EntityLootableBody b=read(base());net.minecraftforge.fml.util.ObfuscationReflectionHelper.setPrivateValue(Entity.class,b,Float.NaN,"yRot");net.minecraftforge.fml.util.ObfuscationReflectionHelper.setPrivateValue(Entity.class,b,Float.POSITIVE_INFINITY,"xRot");b.yRotO=Float.NEGATIVE_INFINITY;b.xRotO=Float.NaN;b.yHeadRot=Float.NaN;b.yHeadRotO=Float.NaN;b.yBodyRot=Float.POSITIVE_INFINITY;b.yBodyRotO=Float.NEGATIVE_INFINITY;b.fallDistance=Float.NaN;CompoundTag n=base();n.remove("Rotation");n.remove("FallDistance");FixtureApi.loadEntityData(b,n);angles(b,0);value(b.fallDistance,0,"Repeated read kept stale fall distance");stable(b);} private static void healthy(){EntityLootableBody b=read(base());kept(b);require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy save gained recovery records");require(LootableBodies.corpseHP==20 && !LootableBodies.hurtByEnvironment,"Defaults changed");stable(b);} private static void chunkAdmission(){CompoundTag n=base();n.put("Pos",PositionMotionChecks.vector(Double.NaN,80,160.5));n.put("Rotation",wideRotation(37.25,Double.NaN));n.putDouble("FallDistance",Double.NaN);CompoundTag before=n.copy();Entity e=FixtureApi.readChunkEntity(n,world,RuntimeChecks.emptyChunk(world,10,10));require(e instanceof EntityLootableBody && !e.isRemoved(),"Chunk lost malformed corpse");EntityLootableBody b=(EntityLootableBody)e;require(b.getX()==168.5 && b.getY()==80 && b.getZ()==160.5,"Position recovery changed");angles(b,37.25F);value(b.fallDistance,0,"Bad distance at admission");require(NameLifetimeChecks.same(before,n),"Chunk mutated raw input");stable(b);} private static void zeroHealth(){CompoundTag n=base();n.put("Rotation",rotation(Float.NaN,Float.NaN));n.putDouble("FallDistance",Double.NaN);n.putFloat("Health",0);EntityLootableBody b=read(n);require(b.getHealth()==0 && !b.isAlive(),"Rotation/fall repair healed zero health");angles(b,0);kept(b);} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java index f761508..d6fe506 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java @@ -54,7 +54,7 @@ public RuntimeChecks(FMLJavaModLoadingContext context) { RegisterEvent.getBus(bus).addListener(registries::items); RegisterEvent.getBus(bus).addListener(registries::potions); FixtureItems.register(bus); - net.minecraftforge.event.entity.player.PlayerInteractEvent.EntityInteract.BUS.addListener(this::rejectFirstCorpseOpen); + net.minecraftforge.event.entity.player.PlayerInteractEvent.EntityInteractSpecific.BUS.addListener(this::rejectFirstCorpseOpen); net.minecraftforge.event.entity.EntityJoinLevelEvent.BUS.addListener(Priority.LOWEST,this::fixtureAdmission); ServerStartedEvent.BUS.addListener(this::started); if (FMLEnvironment.dist == net.minecraftforge.api.distmarker.Dist.CLIENT) { @@ -64,7 +64,7 @@ public RuntimeChecks(FMLJavaModLoadingContext context) { new RespawnPacketClientChecks().register(); } } - private boolean rejectFirstCorpseOpen(net.minecraftforge.event.entity.player.PlayerInteractEvent.EntityInteract event) { + private boolean rejectFirstCorpseOpen(net.minecraftforge.event.entity.player.PlayerInteractEvent.EntityInteractSpecific event) { if(Boolean.getBoolean("lootablebodies.rejectFirstCorpseOpen") && !rejectedFirstCorpseOpen && !event.getLevel().isClientSide() && event.getTarget() instanceof EntityLootableBody) { rejectedFirstCorpseOpen=true; @@ -77,7 +77,7 @@ private void fixtureAdmission(net.minecraftforge.event.entity.EntityJoinLevelEve NbtWorldChecks.verifyGraphicalAdmission(event.getEntity()); if(pendingDeathId!=null && event.getEntity() instanceof EntityLootableBody && !event.getLevel().isClientSide() && "BuildDeath".equals(((EntityLootableBody)event.getEntity()).getCustomNameTag()))event.getEntity().setUUID(pendingDeathId); - if(Boolean.getBoolean("lootablebodies.assertFixtureAdmission") && event.getEntity() instanceof EntityLootableBody && !event.getLevel().isClientSide() && !event.getEntity().isAddedToWorld()) { + if(Boolean.getBoolean("lootablebodies.assertFixtureAdmission") && event.getEntity() instanceof EntityLootableBody && !event.getLevel().isClientSide() && !event.getEntity().isAddedToLevel()) { EntityLootableBody body=(EntityLootableBody)event.getEntity(); require(event.getLevel().getChunkSource().hasChunk(net.minecraft.util.Mth.floor(body.getX())>>4,net.minecraft.util.Mth.floor(body.getZ())>>4),"Manual corpse fixture spawned before chunk admission: "+body.blockPosition()+" name="+body.getCustomNameTag()); } @@ -110,9 +110,9 @@ static boolean sameStack(ItemStack first,ItemStack second) { public static void run(MinecraftServer server, String phase) throws Exception { ServerLevel world = server.getLevel(FixtureApi.dimensionKey(0)); File folder = world.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); - require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.14.121111"), "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.14.121111") : "2.4.14.121111"), "loaded release version"); + require(LootableBodies.MODID.equals("lootablebodies") && LootableBodies.VERSION.equals("2.4.15.2601021"), "identity/version"); + require(net.minecraftforge.fml.ModList.getModContainerById("lootablebodies").get().getModInfo().getVersion().toString().equals(phase.equals("seed") ? "2.4.0" : phase.equals("issues") ? + System.getProperty("lootablebodies.expectedIssueVersion","2.4.15.2601021") : "2.4.15.2601021"), "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 && @@ -250,7 +250,7 @@ static EntityLootableBody fixture(ServerLevel world, UUID uuid, int coordinate, if (full) { body.addItem(stack(70)); body.addItem(stack(71)); } disableVacuum(body); require(world.addFreshEntity(body), "fixture spawn"); - require(body.isAddedToWorld(),"Fresh fixture missed Forge's admission callback"); + require(body.isAddedToLevel(),"Fresh fixture missed Forge's admission callback"); return body; } static void verifyRoundTrip(ServerLevel world, EntityLootableBody body) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java index fcdbf3d..792dfa9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityChecks.java @@ -81,7 +81,7 @@ public void tick(TickEvent.ServerTickEvent.Post e) { // Keep this disposable respawn within the intended viewing area. w.setRespawnData(net.minecraft.world.level.storage.LevelData.RespawnData.of(w.dimension(),new BlockPos(96,PLAYER_Y,93),0F,0F)); w.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.RESPAWN_RADIUS,0,server); - w.setDayTime(6000); + w.clockManager().setTotalTicks(w.dimensionType().defaultClock().orElseThrow(),6000); for(int x=88;x<=170;x++)for(int z=89;z<=103;z++)w.setBlockAndUpdate(new BlockPos(x,GROUND_Y,z),Blocks.OBSIDIAN.defaultBlockState()); for(ServerPlayer p:new ServerPlayer[]{a,b}) {p.setGameMode(GameType.SURVIVAL);p.getInventory().clearContent();FixtureApi.spawn(p,new BlockPos(96,PLAYER_Y,93),true,false,p.level().dimension());} a.connection.teleport(96.5,PLAYER_Y,96.5,0,65);b.connection.teleport(98.5,PLAYER_Y,93.5,0,25); @@ -145,7 +145,7 @@ public void tick(TickEvent.ServerTickEvent.Post e) { mark("cold_server_restart_both_clients_render_and_open_saved_corpse");a.closeContainer();b.closeContainer(); for(int x=397;x<=404;x++)for(int z=397;z<=404;z++)world().setBlockAndUpdate(new BlockPos(x,GROUND_Y,z),Blocks.OBSIDIAN.defaultBlockState()); a.connection.teleport(400.5,PLAYER_Y,400.5,0,25);b.connection.teleport(402.5,PLAYER_Y,400.5,0,25);phase(20); - } else if(phase==20 && both(20) && phaseTicks>240 && !world().getChunkSource().hasChunk(first.chunkPosition().x,first.chunkPosition().z) && !world().getChunkSource().hasChunk(second.chunkPosition().x,second.chunkPosition().z)) { + } else if(phase==20 && both(20) && phaseTicks>240 && !world().getChunkSource().hasChunk(first.chunkPosition().x(),first.chunkPosition().z()) && !world().getChunkSource().hasChunk(second.chunkPosition().x(),second.chunkPosition().z())) { mark("both_clients_leave_and_corpse_chunk_really_unloads"); FixtureApi.flush(server); a.connection.teleport(first.getX(),first.getY(),first.getZ()-2.5,0,25);b.connection.teleport(first.getX()+1.5,first.getY(),first.getZ()-2.5,0,25);phase(21); @@ -178,7 +178,7 @@ static void reload(MinecraftServer server) throws Exception { ServerLevel w=server.getLevel(FixtureApi.dimensionKey(0));File folder=w.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile();CompoundTag n; try(FileInputStream in=new FileInputStream(new File(folder,"visibility-state.nbt"))){n=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} for(String key:new String[]{"First","Second"}) {EntityLootableBody b=loadExpected(w,key.toLowerCase(Locale.ROOT));RuntimeChecks.require(b.getUUID().equals(readUuid(n, key)),"Saved identity differs");verifySnapshot(w,key.toLowerCase(Locale.ROOT),b);} - CompoundTag p;try(FileInputStream in=new FileInputStream(new File(folder,"playerdata/"+readUuid(n, "Player")+".dat"))){p=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} + CompoundTag p;try(FileInputStream in=new FileInputStream(new File(folder,net.minecraft.world.level.storage.LevelResource.PLAYER_DATA_DIR.id()+"/"+readUuid(n, "Player")+".dat"))){p=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} RuntimeChecks.require(n.get("Inventory").equals(p.get("Inventory")),"Recovered inventory changed on cold restart"); } private static EntityLootableBody loadExpected(ServerLevel w,String name) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java index 70148e9..f982d34 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java @@ -15,7 +15,7 @@ import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.world.entity.Entity; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; import net.minecraft.client.Screenshot; import net.minecraft.world.InteractionHand; @@ -148,7 +148,7 @@ private void tick(boolean beforeClientTick) { EntityLootableBody b=body(mc,phase==16?"second":"first");if(b==null)return; if(!(mc.screen instanceof CorpseGUIContainer)) { if(!rendered.contains(b.getUUID()))return; - if(frames%10==0)mc.gameMode.interact(mc.player,b,InteractionHand.MAIN_HAND);return; + if(frames%10==0)mc.gameMode.interact(mc.player, b, new net.minecraft.world.phys.EntityHitResult(b), InteractionHand.MAIN_HAND);return; } int index=phase==16?411:410;boolean found=false; for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(index)))found=true; @@ -164,7 +164,7 @@ private void tick(boolean beforeClientTick) { } else if(phase==12 || phase==120) { EntityLootableBody b=body(mc,"first");if(b!=null&&!b.isRemoved()&&rendered.contains(b.getUUID()))ack(); } else if(phase==17) { - if(role.equals("A"))for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(411)))mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,slot,0,ClickType.QUICK_MOVE,mc.player); + if(role.equals("A"))for(int slot=0;slot<48;slot++)if(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(411)))mc.gameMode.handleContainerInput(mc.player.containerMenu.containerId,slot,0,ContainerInput.QUICK_MOVE,mc.player); // The following acknowledgement is not inventory proof; the server checks the resulting packet state. ack(); } else if(phase==18) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java index 799f263..6465882 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java @@ -33,24 +33,67 @@ void tick(Minecraft mc) throws Exception { new net.minecraft.client.renderer.texture.SkinTextureDownloader(mc.getProxy(),mc.getTextureManager(),mc),mc) { @Override public java.util.function.Supplier createLookup(GameProfile p, boolean secure){throw new IllegalStateException("injected render skin cache failure");} }); - mc.setScreen(new Panel(mc));stage=2;frames=0; + Panel panel=new Panel(mc); + panel.verifyExtractedTextures(mc); + mc.setScreen(panel);stage=2;frames=0; } else if(stage==2 && ++frames>30) { - Screenshot.grab(mc.gameDirectory,"skin-fallback-models.png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); + Panel panel=(Panel)mc.screen; + String name=new String[]{"skin-fallback-wide.png","skin-fallback-slim.png","skin-local.png"}[panel.preview]; + Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); + if(panel.preview+1new GameProfile(new UUID(0,n),"VisibilityModel")) + .filter(candidate->net.minecraft.client.resources.DefaultPlayerSkin.get(candidate).model()==model) + .findFirst().orElseThrow(); + bodies[i].setGameProfile(profile); + } + bodies[2].setGameProfile(bodies[0].getGameProfile()); + } + + void verifyExtractedTextures(Minecraft mc) throws java.io.IOException { + var renderer=(cyano.lootable.graphics.CorpseRenderer)mc.getEntityRenderDispatcher().getRenderer(bodies[0]); + var wide=renderer.createRenderState(); + var slim=renderer.createRenderState(); + var local=renderer.createRenderState(); + boolean configured=LootableBodies.useLocalSkin; + try { + LootableBodies.useLocalSkin=false; + renderer.extractRenderState(bodies[0],wide,0); + renderer.extractRenderState(bodies[1],slim,0); + LootableBodies.useLocalSkin=true; + renderer.extractRenderState(bodies[2],local,0); + } finally {LootableBodies.useLocalSkin=configured;} + RuntimeChecks.require(wide.skin.model()==net.minecraft.world.entity.player.PlayerModelType.WIDE && + slim.skin.model()==net.minecraft.world.entity.player.PlayerModelType.SLIM,"Failed skins did not keep both native arm models"); + RuntimeChecks.require(!renderer.getTextureLocation(wide).equals(renderer.getTextureLocation(slim)) && + renderer.getTextureLocation(local).equals(net.minecraft.resources.Identifier.parse("lootablebodies:textures/entity/corpse/corpse.png")), + "Queued corpse renders lost their distinct textures"); + java.nio.file.Files.writeString(mc.gameDirectory.toPath().resolve("skin-checks.txt"), + "PASS extracted_state_preserves_distinct_wide_slim_and_local_textures\n", + java.nio.file.StandardOpenOption.APPEND); + } + @Override public void extractRenderState(net.minecraft.client.gui.GuiGraphicsExtractor graphics,int mouseX,int mouseY,float partial) { graphics.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; - graphics.drawCenteredString(font,names[i],x,35,0xffffff); - InventoryScreen.renderEntityInInventoryFollowsMouse(graphics,x-75,65,x+75,height-40,65,0.0625F,x+20,height*2/3-20,bodies[i]); - }} finally {LootableBodies.useLocalSkin=local;} + // The native picture-in-picture renderer reuses one texture per + // render-state type. Capture one body per frame to inspect each skin. + try { + LootableBodies.useLocalSkin=preview==2; + int x=width/2; + graphics.centeredText(font,names[preview],x,35,0xffffffff); + InventoryScreen.extractEntityInInventoryFollowsMouse(graphics,x-75,65,x+75,height-40,65,0.0625F,x+20,height*2/3-20,bodies[preview]); + } finally {LootableBodies.useLocalSkin=local;} } @Override public boolean isPauseScreen(){return false;} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/mixin/ProbeFrameMixin.java b/src/integrationTest/java/cyano/lootable/buildtest/mixin/ProbeFrameMixin.java index b98fa56..42dde7a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/mixin/ProbeFrameMixin.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/mixin/ProbeFrameMixin.java @@ -5,7 +5,7 @@ import net.minecraft.client.DeltaTracker; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.culling.Frustum; -import net.minecraft.client.renderer.state.LevelRenderState; +import net.minecraft.client.renderer.state.level.LevelRenderState; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/integrationTest/resources/META-INF/mods.toml b/src/integrationTest/resources/META-INF/mods.toml index 94bc715..ff4b7ed 100644 --- a/src/integrationTest/resources/META-INF/mods.toml +++ b/src/integrationTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[61,)" +loaderVersion="[64,)" license="LGPL-2.1-only" [[mods]] modId="lootablebodiesbuildtest" @@ -8,6 +8,6 @@ displayName="Disposable build checks" [[dependencies.lootablebodiesbuildtest]] modId="lootablebodies" mandatory=true -versionRange="[2.4.14.121111]" +versionRange="[2.4.15.2601021]" ordering="AFTER" side="BOTH" diff --git a/src/integrationTest/resources/pack.mcmeta b/src/integrationTest/resources/pack.mcmeta index 5b6207a..bb1a6ae 100644 --- a/src/integrationTest/resources/pack.mcmeta +++ b/src/integrationTest/resources/pack.mcmeta @@ -2,11 +2,11 @@ "pack": { "description": "Disposable build checks", "min_format": [ - 94, - 1 + 84, + 0 ], "max_format": [ - 94, + 101, 1 ] } diff --git a/src/main/java/cyano/lootable/ClientProxy.java b/src/main/java/cyano/lootable/ClientProxy.java index a1c0c28..e7f6ac0 100644 --- a/src/main/java/cyano/lootable/ClientProxy.java +++ b/src/main/java/cyano/lootable/ClientProxy.java @@ -3,13 +3,11 @@ import cyano.lootable.graphics.*; import net.minecraft.client.Minecraft; import net.minecraft.world.entity.Entity; -import net.minecraftforge.api.distmarker.*; import net.minecraftforge.client.ConfigScreenHandler; import net.minecraftforge.client.event.EntityRenderersEvent; import net.minecraftforge.event.TickEvent; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -@OnlyIn(Dist.CLIENT) public final class ClientProxy { private ClientProxy() { } public static void register(FMLJavaModLoadingContext context) { diff --git a/src/main/java/cyano/lootable/LootableBodies.java b/src/main/java/cyano/lootable/LootableBodies.java index 3ffb952..72cdd53 100644 --- a/src/main/java/cyano/lootable/LootableBodies.java +++ b/src/main/java/cyano/lootable/LootableBodies.java @@ -32,7 +32,7 @@ public final class LootableBodies { public static final String MODID = "lootablebodies"; public static final String NAME = "DrCyano's Lootable Bodies"; - public static final String VERSION = "2.4.14.121111"; + public static final String VERSION = "2.4.15.2601021"; public static final Identifier CORPSE_ID = Identifier.fromNamespaceAndPath(MODID,"corpse"); public static EntityType CORPSE; public static Item CORPSE_EGG; @@ -63,14 +63,14 @@ public LootableBodies(FMLJavaModLoadingContext context) { var modBus = context.getModBusGroup(); FMLCommonSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(cyano.lootable.upgrade.EarlyWorldPreservation::install)); - NewRegistryEvent.getBus(modBus).addListener(cyano.lootable.upgrade.LegacyProfessionRegistry::create); - EntityAttributeCreationEvent.getBus(modBus).addListener(event -> + NewRegistryEvent.BUS.addListener(cyano.lootable.upgrade.LegacyProfessionRegistry::create); + EntityAttributeCreationEvent.BUS.addListener(event -> event.put(corpseType(), EntityLootableBody.corpseAttributeDefaults().build())); RegisterEvent.getBus(modBus).addListener(this::registerEntities); RegisterEvent.getBus(modBus).addListener(this::registerItems); RegisterEvent.getBus(modBus).addListener(cyano.lootable.upgrade.LegacyRegistryMappings::retiredMemories); RegisterEvent.getBus(modBus).addListener(this::registerContainers); - BuildCreativeModeTabContentsEvent.getBus(modBus).addListener(event -> { + BuildCreativeModeTabContentsEvent.BUS.addListener(event -> { if (CreativeModeTabs.SPAWN_EGGS.equals(event.getTabKey())) event.accept(() -> CORPSE_EGG); }); new PlayerDeathEventHandler().register(); diff --git a/src/main/java/cyano/lootable/entities/EntityLootableBody.java b/src/main/java/cyano/lootable/entities/EntityLootableBody.java index f26393a..6681cb7 100644 --- a/src/main/java/cyano/lootable/entities/EntityLootableBody.java +++ b/src/main/java/cyano/lootable/entities/EntityLootableBody.java @@ -257,9 +257,7 @@ public void baseTick(){ } public boolean handleWaterMovement() { - // Native 1.17 updates both water and lava in this entry point. - // Keep its fluid heights, fire clearing and current pushing together. - return super.updateInWaterStateAndDoFluidPushing(); + return super.updateFluidInteraction(); } private void generateVoidPlatform() { @@ -666,7 +664,7 @@ public int getMaxStackSize() { @Override public void setChanged() { Level level = level(); - if(level != null && !level.isClientSide() && isAddedToWorld() && level.hasChunkAt(blockPosition())) { + if(level != null && !level.isClientSide() && isAddedToLevel() && level.hasChunkAt(blockPosition())) { level.getChunkAt(blockPosition()).markUnsaved(); } } @@ -1260,7 +1258,7 @@ private String readName(CompoundTag root, String key) { var nbt = registryAccess().createSerializationContext(NbtOps.INSTANCE); String saved = raw.asString().orElse(""); int marker = root.getIntOr(UpgradePreservation.VERSION, 0); - boolean legacyJson = marker >= 3 && marker < UpgradePreservation.DATA_VERSION; + boolean legacyJson = marker >= 3 && marker < 13; // Staged copies can already carry the current marker while // retaining a historical JSON component until the first save. boolean stagedJson = !root.getCompoundOrEmpty(UpgradePreservation.SOURCES).isEmpty() diff --git a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java index d2ef52d..f6a1d7d 100644 --- a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java +++ b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java @@ -92,8 +92,8 @@ public void entityDropEvent(LivingDropsEvent e){ corpse.initializeItems(items.toArray(new ItemStack[0])); if(LootableBodies.addBonesToCorpse){ - corpse.addItem(new ItemStack(Items.BONE,1+world.random.nextInt(3))); - corpse.addItem(new ItemStack(Items.ROTTEN_FLESH,1+world.random.nextInt(3))); + corpse.addItem(new ItemStack(Items.BONE,1+world.getRandom().nextInt(3))); + corpse.addItem(new ItemStack(Items.ROTTEN_FLESH,1+world.getRandom().nextInt(3))); } if(world.addFreshEntity(corpse)) { diff --git a/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java b/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java index c22f05f..7d78c80 100644 --- a/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java +++ b/src/main/java/cyano/lootable/graphics/ClientConfigChanges.java @@ -3,8 +3,6 @@ import cyano.lootable.entities.EntityLootableBody; import net.minecraft.client.Minecraft; import net.minecraft.world.entity.Entity; -import net.minecraftforge.api.distmarker.*; -@OnlyIn(Dist.CLIENT) public final class ClientConfigChanges { private ClientConfigChanges() { } public static void apply() { diff --git a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java index 0b5b455..600c44f 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java @@ -8,10 +8,8 @@ import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.resources.language.I18n; -import net.minecraftforge.api.distmarker.*; /** The ordinary Mods-list settings screen, retaining the existing CFG choices. */ -@OnlyIn(Dist.CLIENT) 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"}; @@ -64,19 +62,19 @@ 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 render(GuiGraphics graphics,int mouseX,int mouseY,float partialTicks){ - super.render(graphics,mouseX,mouseY,partialTicks); - graphics.drawCenteredString(font,LootableBodies.NAME,width/2,12,0xffffffff); - graphics.drawCenteredString(font,I18n.get(group==null?"lootablebodies.config.local_file":"lootablebodies.config."+group),width/2,28,0xffcccccc); + @Override public void extractRenderState(GuiGraphicsExtractor graphics,int mouseX,int mouseY,float partialTicks){ + super.extractRenderState(graphics,mouseX,mouseY,partialTicks); + graphics.centeredText(font,LootableBodies.NAME,width/2,12,0xffffffff); + graphics.centeredText(font,I18n.get(group==null?"lootablebodies.config.local_file":"lootablebodies.config."+group),width/2,28,0xffcccccc); for(Map.Entry row:rows.entrySet()){ - String text=I18n.get(property(row.getKey()).getLanguageKey());graphics.drawString(font,font.plainSubstrByWidth(text,205),width/2-155,row.getValue()+6,0xffffffff); + String text=I18n.get(property(row.getKey()).getLanguageKey());graphics.text(font,font.plainSubstrByWidth(text,205),width/2-155,row.getValue()+6,0xffffffff); } - for(EditBox field:fields.values())field.render(graphics,mouseX,mouseY,partialTicks); + for(EditBox field:fields.values())field.extractRenderState(graphics,mouseX,mouseY,partialTicks); for(Map.Entry row:rows.entrySet())if(mouseX>=width/2-155 && mouseX=row.getValue() && mouseY=width/2-155 && mouseX=95 && mouseY<115) graphics.renderComponentTooltip(font,java.util.Collections.singletonList(net.minecraft.network.chat.Component.translatable("lootablebodies.config.rules.tooltip")),mouseX,mouseY,net.minecraft.world.item.ItemStack.EMPTY); - if(invalid)graphics.drawCenteredString(font,I18n.get("lootablebodies.config.invalid"),width/2,height-72,0xffff5555); + if(invalid)graphics.centeredText(font,I18n.get("lootablebodies.config.invalid"),width/2,height-72,0xffff5555); } @Override public void onClose(){capture();minecraft.setScreen(parent);} public static boolean isValidDecayTime(String value) { diff --git a/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java b/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java index 9802060..c0b489a 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGuiFactory.java @@ -2,9 +2,7 @@ import java.util.function.BiFunction; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.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 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 821b664..aba0c68 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseContainer.java @@ -11,10 +11,8 @@ import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.Container; import net.minecraft.world.inventory.Slot; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.ItemStack; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.event.entity.item.ItemTossEvent; /** @@ -44,24 +42,12 @@ public int getMaxStackSize() { public boolean mayPlace(ItemStack item) { return !emptyStack(item) && item.getCount() > 0 && super.mayPlace(item) && item.canEquip(EQUIPMENT_SLOTS[i], (Entity) targetInventory); } - @OnlyIn(Dist.CLIENT) - public String getSlotTexture() - { - return new String[]{"item/empty_armor_slot_helmet","item/empty_armor_slot_chestplate","item/empty_armor_slot_leggings","item/empty_armor_slot_boots"}[i]; - } }); index++; } while(index < EQUIPMENT_SLOTS.length){ // held items - final int i = index; - this.addSlot(new net.minecraft.world.inventory.Slot(targetInventory, i, 8 + 18 * (i - 4), 98 ) { - @OnlyIn(Dist.CLIENT) - public String getSlotTexture() - { - return "item/empty_armor_slot_shield"; - } - }); + this.addSlot(new Slot(targetInventory, index, 8 + 18 * (index - 4), 98)); index++; } @@ -90,19 +76,19 @@ public boolean stillValid(Player entityplayer) { } @Override - public void clicked(int slotId, int button, ClickType type, Player player) { + public void clicked(int slotId, int button, ContainerInput type, Player player) { // A click can arrive before the next player tick closes a stale window. if(!stillValid(player)) { resetQuickCraft(); return; } - if(type != ClickType.QUICK_CRAFT && dragging) { + if(type != ContainerInput.QUICK_CRAFT && dragging) { resetQuickCraft(); return; } - if(type == ClickType.QUICK_CRAFT && getQuickcraftHeader(button) == 0 + if(type == ContainerInput.QUICK_CRAFT && getQuickcraftHeader(button) == 0 && !emptyStack(getCarried()) && isValidQuickcraftType(getQuickcraftType(button),player)) dragging = true; - if(type == ClickType.THROW && emptyStack(getCarried()) + if(type == ContainerInput.THROW && emptyStack(getCarried()) && slotId >= 0 && slotId < slots.size()) { Slot slot = getSlot(slotId); if(slot.hasItem() && slot.mayPickup(player)) { @@ -116,7 +102,7 @@ public void clicked(int slotId, int button, ClickType type, Player player) { } return; } - if((type == ClickType.PICKUP || type == ClickType.QUICK_MOVE) && slotId == -999 && (button == 0 || button == 1)) { + if((type == ContainerInput.PICKUP || type == ContainerInput.QUICK_MOVE) && slotId == -999 && (button == 0 || button == 1)) { ItemStack cursor = getCarried(); if(!emptyStack(cursor)) { ItemStack original = cursor.copy(); @@ -129,7 +115,7 @@ public void clicked(int slotId, int button, ClickType type, Player player) { } return; } - if(type == ClickType.SWAP && button >= 0 && button < 9 + if(type == ContainerInput.SWAP && button >= 0 && button < 9 && slotId >= 0 && slotId < targetInventory.getContainerSize()) { Slot slot = getSlot(slotId); ItemStack incoming = player.getInventory().getItem(button); diff --git a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java index 26c5521..9e64557 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java @@ -4,16 +4,13 @@ import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.Container; import net.minecraft.resources.Identifier; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.Component; /** * Created by Chris on 4/12/2016. */ -@OnlyIn(Dist.CLIENT) public class CorpseGUIContainer extends net.minecraft.client.gui.screens.inventory.AbstractContainerScreen{ private final Identifier image = Identifier.parse(LootableBodies.MODID+":textures/gui/corpse.png"); @@ -22,9 +19,7 @@ public CorpseGUIContainer(Inventory playerItems, Container entity) { this(new CorpseContainer(playerItems,entity),playerItems,entity instanceof net.minecraft.world.entity.Entity?((net.minecraft.world.entity.Entity)entity).getDisplayName():Component.literal(LootableBodies.NAME)); } public CorpseGUIContainer(CorpseContainer container, Inventory playerItems, Component title){ - super(container,playerItems,title); - this.imageWidth = 176; - this.imageHeight = 222; + super(container, playerItems, title, 176, 222); } /** @@ -36,14 +31,15 @@ public CorpseGUIContainer(CorpseContainer container, Inventory playerItems, Comp * @param mouseY Mouse y coordinate */ @Override - protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) { + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTicks) { + super.extractBackground(graphics, mouseX, mouseY, partialTicks); int x = (width - imageWidth) / 2; int y = (height - imageHeight) / 2; graphics.blit(RenderPipelines.GUI_TEXTURED, image, x, y, 0, 0, imageWidth, imageHeight, 256, 256); } @Override - protected void renderLabels(GuiGraphics graphics,int mouseX,int mouseY) { + protected void extractLabels(GuiGraphicsExtractor graphics,int mouseX,int mouseY) { // This seven-row layout has no label strip. Keep the original screen's // foreground; the new vanilla labels would overlap corpse item slots. } diff --git a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java index 07b9f57..b68b9fd 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java @@ -19,20 +19,21 @@ import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; import net.minecraft.client.renderer.entity.layers.ItemInHandLayer; import net.minecraft.client.renderer.entity.state.AvatarRenderState; -import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; import net.minecraft.client.resources.DefaultPlayerSkin; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.player.PlayerModelType; import net.minecraft.world.entity.player.PlayerSkin; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; /** Corpse renderer, originally created by Chris on 4/10/2016. */ -@OnlyIn(Dist.CLIENT) public class CorpseRenderer extends LivingEntityRenderer { private static final Identifier LOCAL_SKIN = Identifier.fromNamespaceAndPath(LootableBodies.MODID, "textures/entity/corpse/corpse.png"); + private static final class CorpseRenderState extends AvatarRenderState { + private boolean localSkin; + } + private final PlayerModel thickArmsModel; private final PlayerModel thinArmsModel; @@ -49,14 +50,16 @@ public CorpseRenderer(EntityRendererProvider.Context context) { @Override public AvatarRenderState createRenderState() { - return new AvatarRenderState(); + return new CorpseRenderState(); } @Override public void extractRenderState(EntityLootableBody body, AvatarRenderState state, float partialTicks) { super.extractRenderState(body, state, partialTicks); HumanoidMobRenderer.extractHumanoidRenderState(body, state, partialTicks, itemModelResolver); - state.skin = LootableBodies.useLocalSkin ? DefaultPlayerSkin.getDefaultSkin() : playerSkin(body.getGameProfile()); + CorpseRenderState corpseState = (CorpseRenderState) state; + corpseState.localSkin = LootableBodies.useLocalSkin; + state.skin = corpseState.localSkin ? DefaultPlayerSkin.getDefaultSkin() : playerSkin(body.getGameProfile()); state.id = body.getId(); state.arrowCount = body.getArrowCount(); state.bodyRot = body.getRotation(); @@ -69,7 +72,7 @@ public void extractRenderState(EntityLootableBody body, AvatarRenderState state, @Override public Identifier getTextureLocation(AvatarRenderState state) { - return LootableBodies.useLocalSkin ? LOCAL_SKIN : state.skin.body().texturePath(); + return ((CorpseRenderState) state).localSkin ? LOCAL_SKIN : state.skin.body().texturePath(); } public static Identifier getSkin(GameProfile profile) { @@ -87,7 +90,7 @@ private static PlayerSkin playerSkin(GameProfile profile) { @Override public void submit(AvatarRenderState state, PoseStack matrices, SubmitNodeCollector collector, CameraRenderState camera) { - model = !LootableBodies.useLocalSkin && state.skin.model() == PlayerModelType.SLIM ? thinArmsModel : thickArmsModel; + model = !((CorpseRenderState) state).localSkin && state.skin.model() == PlayerModelType.SLIM ? thinArmsModel : thickArmsModel; try { super.submit(state, matrices, collector, camera); } finally { @@ -109,8 +112,8 @@ protected boolean shouldShowName(EntityLootableBody body, double distanceSquared } @Override - protected void submitNameTag(AvatarRenderState state, PoseStack matrices, + protected void submitNameDisplay(AvatarRenderState state, PoseStack matrices, SubmitNodeCollector collector, CameraRenderState camera) { - if (LootableBodies.displayNameTag) super.submitNameTag(state, matrices, collector, camera); + if (LootableBodies.displayNameTag) super.submitNameDisplay(state, matrices, collector, camera); } } diff --git a/src/main/java/cyano/lootable/graphics/GUIHandler.java b/src/main/java/cyano/lootable/graphics/GUIHandler.java index b90e8f2..9c646ab 100644 --- a/src/main/java/cyano/lootable/graphics/GUIHandler.java +++ b/src/main/java/cyano/lootable/graphics/GUIHandler.java @@ -4,7 +4,6 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; -import net.minecraftforge.api.distmarker.*; /** Native Forge container opening, with the historical local helper kept internal. */ public final class GUIHandler { private static final GUIHandler INSTANCE=new GUIHandler(); @@ -12,5 +11,4 @@ private GUIHandler() { } public static GUIHandler getInstance(){return INSTANCE;} public static void open(Player player,EntityLootableBody corpse){if(player instanceof ServerPlayer server)server.openMenu(corpse,buf->buf.writeVarInt(corpse.getId()));} public Object getServerGuiElement(int id,Player player,Level world,int x,int y,int z){Entity entity=world.getEntity(x);return id==0 && entity instanceof EntityLootableBody?new CorpseContainer(player.getInventory(),(EntityLootableBody)entity):null;} - @OnlyIn(Dist.CLIENT) public Object getClientGuiElement(int id,Player player,Level world,int x,int y,int z){Entity entity=world.getEntity(x);return id==0 && entity instanceof EntityLootableBody?new CorpseGUIContainer(player.getInventory(),(EntityLootableBody)entity):null;} } diff --git a/src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java b/src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java index f56cf42..cdc9ee6 100644 --- a/src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java +++ b/src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java @@ -1,20 +1,21 @@ package cyano.lootable.mixin; import cyano.lootable.upgrade.UpgradePreservation; +import java.nio.file.Path; import net.minecraft.nbt.CompoundTag; import net.minecraft.util.datafix.DataFixTypes; -import net.minecraft.world.level.storage.DimensionDataStorage; +import net.minecraft.world.level.storage.SavedDataStorage; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -@Mixin(value = DimensionDataStorage.class, remap = false) +@Mixin(value = SavedDataStorage.class, remap = false) abstract class CapabilityDataMixin { @Inject(method = "readTagFromDisk", at = @At("RETURN"), cancellable = true) - private void preserveOldCapabilities(String id, DataFixTypes fixer, int version, + private void preserveOldCapabilities(Path path, DataFixTypes fixer, int version, CallbackInfoReturnable callback) { - if (fixer == null && id.equals("capabilities")) + if (fixer == null && path.endsWith(Path.of("forge", "capabilities.dat"))) callback.setReturnValue(UpgradePreservation.upgradeLevelCapabilities(callback.getReturnValue())); } } diff --git a/src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java b/src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java index 3a548ba..3a2b86c 100644 --- a/src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java +++ b/src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java @@ -6,19 +6,32 @@ import cyano.lootable.upgrade.EarlyWorldPreservation; import java.io.IOException; import net.minecraft.server.Main; +import net.minecraft.util.filefix.AbortedFileFixException; +import net.minecraft.util.filefix.FileFixException; +import net.minecraft.util.filefix.FileFixerUpper; +import net.minecraft.util.worldupdate.UpgradeProgress; import net.minecraft.world.level.storage.LevelStorageSource; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @Mixin(value = Main.class, remap = false) abstract class DedicatedWorldMixin { - @WrapOperation(method = "main", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;getDataTag()Lcom/mojang/serialization/Dynamic;")) - private static Dynamic readWorld(LevelStorageSource.LevelStorageAccess access, Operation> original) throws IOException { + @WrapOperation(method = "main", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;getUnfixedDataTagWithFallback()Lcom/mojang/serialization/Dynamic;")) + private static Dynamic readWorld(LevelStorageSource.LevelStorageAccess access, Operation> original) + throws IOException { return EarlyWorldPreservation.readDedicatedWorld(access, () -> original.call(access)); } - @WrapOperation(method = "main", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;getDataTagFallback()Lcom/mojang/serialization/Dynamic;")) - private static Dynamic readFallback(LevelStorageSource.LevelStorageAccess access, Operation> original) throws IOException { - return EarlyWorldPreservation.readDedicatedWorld(access, () -> original.call(access)); + @WrapOperation(method = "main", at = @At(value = "INVOKE", + target = "Lnet/minecraft/util/filefix/FileFixerUpper;fix(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;Lcom/mojang/serialization/Dynamic;Lnet/minecraft/util/worldupdate/UpgradeProgress;)Lcom/mojang/serialization/Dynamic;")) + private static Dynamic fixWorldFiles(FileFixerUpper fixer, LevelStorageSource.LevelStorageAccess access, + Dynamic header, UpgradeProgress progress, Operation> original) throws FileFixException { + try { + return original.call(fixer, access, header, progress); + } catch (AbortedFileFixException failure) { + EarlyWorldPreservation.handleDedicatedFileRefusal(access, failure); + throw failure; + } } } diff --git a/src/main/java/cyano/lootable/mixin/DimensionStorageMixin.java b/src/main/java/cyano/lootable/mixin/DimensionStorageMixin.java new file mode 100644 index 0000000..e4ede6f --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/DimensionStorageMixin.java @@ -0,0 +1,31 @@ +package cyano.lootable.mixin; + +import com.mojang.datafixers.schemas.Schema; +import java.util.List; +import java.util.Map; +import net.minecraft.util.filefix.FileFix; +import net.minecraft.util.filefix.access.FileRelation; +import net.minecraft.util.filefix.fixes.DimensionStorageFileFix; +import net.minecraft.util.filefix.operations.FileFixOperations; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = DimensionStorageFileFix.class, remap = false) +abstract class DimensionStorageMixin extends FileFix { + protected DimensionStorageMixin(Schema schema) { + super(schema); + } + + @Inject(method = "makeFixer", at = @At("HEAD")) + private void moveForgeCapabilities(CallbackInfo callback) { + addFileFixOperation(FileFixOperations.applyInFolders(FileRelation.DIMENSIONS_DATA, + List.of(FileFixOperations.move("capabilities.dat", "forge/capabilities.dat")))); + addFileFixOperation(FileFixOperations.groupMove(Map.of( + "data", "dimensions/minecraft/overworld/data", + "DIM-1/data", "dimensions/minecraft/the_nether/data", + "DIM1/data", "dimensions/minecraft/the_end/data"), + List.of(FileFixOperations.move("capabilities.dat", "forge/capabilities.dat")))); + } +} diff --git a/src/main/java/cyano/lootable/mixin/FileFixLevelDataMixin.java b/src/main/java/cyano/lootable/mixin/FileFixLevelDataMixin.java new file mode 100644 index 0000000..ff63875 --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/FileFixLevelDataMixin.java @@ -0,0 +1,27 @@ +package cyano.lootable.mixin; + +import com.mojang.serialization.Dynamic; +import java.io.IOException; +import java.nio.file.Path; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.util.filefix.access.CompressedNbt; +import net.minecraft.util.filefix.access.LevelDat; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(value = LevelDat.class, remap = false) +abstract class FileFixLevelDataMixin extends CompressedNbt { + protected FileFixLevelDataMixin(Path path, MissingSeverity severity) { + super(path, severity); + } + + @ModifyArg(method = "write", at = @At(value = "INVOKE", + target = "Lnet/minecraft/util/filefix/access/LevelDat;writeFile(Lcom/mojang/serialization/Dynamic;)V")) + private Dynamic retainRootFields(Dynamic updated) throws IOException { + Dynamic converted = updated.convert(NbtOps.INSTANCE); + Dynamic root = readFile().orElseThrow(() -> new IOException("Missing level data during native file upgrade")); + return root.set("Data", converted.get("Data").result().orElseThrow()); + } +} diff --git a/src/main/java/cyano/lootable/mixin/FileFixerMixin.java b/src/main/java/cyano/lootable/mixin/FileFixerMixin.java new file mode 100644 index 0000000..0ed611a --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/FileFixerMixin.java @@ -0,0 +1,63 @@ +package cyano.lootable.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.datafixers.DataFixer; +import com.mojang.serialization.Dynamic; +import cyano.lootable.upgrade.EarlyWorldPreservation; +import cyano.lootable.upgrade.UpgradePreservation; +import java.io.IOException; +import java.nio.file.Path; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.util.filefix.AbortedFileFixException; +import net.minecraft.util.filefix.FileFixerUpper; +import net.minecraft.util.worldupdate.UpgradeProgress; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = FileFixerUpper.class, remap = false) +abstract class FileFixerMixin implements EarlyWorldPreservation.LayoutAttached { + @Unique private static final String FIX = + "fix(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;" + + "Lcom/mojang/serialization/Dynamic;Lnet/minecraft/util/worldupdate/UpgradeProgress;I)" + + "Lcom/mojang/serialization/Dynamic;"; + + @Shadow public abstract boolean requiresFileFixing(int version); + + @Inject(method = FIX, at = @At("HEAD")) + private void beforeFileChanges(LevelStorageSource.LevelStorageAccess access, Dynamic header, + UpgradeProgress progress, int target, CallbackInfoReturnable> callback) + throws AbortedFileFixException { + EarlyWorldPreservation.beforeFileFix(access, requiresFileFixing(NbtUtils.getDataVersion(header))); + } + + @Inject(method = FIX, at = @At("RETURN")) + private void afterFileChanges(LevelStorageSource.LevelStorageAccess access, Dynamic header, + UpgradeProgress progress, int target, CallbackInfoReturnable> callback) + throws AbortedFileFixException { + EarlyWorldPreservation.afterFileFix(access); + } + + @ModifyExpressionValue(method = {FIX, "writeUpdatedLevelData"}, at = @At(value = "INVOKE", + target = "Lcom/mojang/datafixers/DataFixerBuilder$Result;fixer()Lcom/mojang/datafixers/DataFixer;")) + private DataFixer protectNativeLevelConversion(DataFixer fixer) { + return UpgradePreservation.wrapper(fixer); + } + + @WrapOperation(method = "writeUpdatedLevelData", at = @At(value = "INVOKE", + target = "Lnet/minecraft/nbt/NbtIo;writeCompressed(Lnet/minecraft/nbt/CompoundTag;Ljava/nio/file/Path;)V")) + private void retainLevelMetadata(CompoundTag updated, Path path, Operation original) throws IOException { + CompoundTag root = NbtIo.readCompressed(path, NbtAccounter.defaultQuota()); + root.put("Data", updated.getCompoundOrEmpty("Data")); + original.call(root, path); + } +} diff --git a/src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java b/src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java deleted file mode 100644 index daee56a..0000000 --- a/src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java +++ /dev/null @@ -1,12 +0,0 @@ -package cyano.lootable.mixin; - -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(value = LegacyStructureDataHandler.class, remap = false) -public interface LegacyStructuresMixin { - @Invoker("updateFromLegacy") - CompoundTag lootablebodies$updateFromLegacy(CompoundTag chunk); -} diff --git a/src/main/java/cyano/lootable/mixin/RegionStorageMixin.java b/src/main/java/cyano/lootable/mixin/RegionStorageMixin.java index 733f11a..57be62c 100644 --- a/src/main/java/cyano/lootable/mixin/RegionStorageMixin.java +++ b/src/main/java/cyano/lootable/mixin/RegionStorageMixin.java @@ -2,10 +2,8 @@ import com.mojang.datafixers.DataFixer; import cyano.lootable.upgrade.UpgradePreservation; -import java.util.function.Supplier; import net.minecraft.nbt.CompoundTag; import net.minecraft.util.datafix.DataFixTypes; -import net.minecraft.world.level.chunk.storage.LegacyTagFixer; import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -18,23 +16,15 @@ abstract class RegionStorageMixin implements UpgradePreservation.RegionAttached { @Shadow @Final private DataFixer fixerUpper; @Shadow @Final private DataFixTypes dataFixType; - @Shadow @Final private Supplier legacyFixer; - @Inject(method = "upgradeChunkTag(Lnet/minecraft/nbt/CompoundTag;ILnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;", + @Inject(method = "upgradeChunkTag(Lnet/minecraft/nbt/CompoundTag;ILnet/minecraft/nbt/CompoundTag;I)Lnet/minecraft/nbt/CompoundTag;", at = @At("HEAD"), cancellable = true) - private void preserveRegion(CompoundTag input, int fallback, CompoundTag context, + private void preserveRegion(CompoundTag input, int fallback, CompoundTag context, int target, CallbackInfoReturnable callback) { if (dataFixType != DataFixTypes.CHUNK && dataFixType != DataFixTypes.ENTITY_CHUNK) return; + // Native 26.1.2 migrates legacy structures in the file fixer. It may + // request an intermediate target before it applies its own changes. callback.setReturnValue(UpgradePreservation.upgradeRegion(fixerUpper, dataFixType, input, - fallback, context, this::lootablebodies$applyLegacyStructures)); - } - - private CompoundTag lootablebodies$applyLegacyStructures(CompoundTag converted) { - LegacyTagFixer legacy = legacyFixer.get(); - // At version 1493 the native entry point initializes its structure - // index without running its unprotected pre-1493 conversion again. - CompoundTag initialized = legacy.applyFix(converted); - return legacy instanceof LegacyStructuresMixin structures - ? structures.lootablebodies$updateFromLegacy(initialized) : initialized; + fallback, context, target, null)); } } diff --git a/src/main/java/cyano/lootable/mixin/SaveAccessMixin.java b/src/main/java/cyano/lootable/mixin/SaveAccessMixin.java index 5da5ee5..3ead73c 100644 --- a/src/main/java/cyano/lootable/mixin/SaveAccessMixin.java +++ b/src/main/java/cyano/lootable/mixin/SaveAccessMixin.java @@ -7,21 +7,19 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(value = LevelStorageSource.LevelStorageAccess.class, remap = false) abstract class SaveAccessMixin implements EarlyWorldPreservation.Attached { - @Inject(method = "getDataTag(Z)Lcom/mojang/serialization/Dynamic;", at = @At("HEAD")) - private void preserveBeforeRead(boolean fallback, CallbackInfoReturnable> callback) { - EarlyWorldPreservation.beforeRead((LevelStorageSource.LevelStorageAccess) (Object) this, fallback); + @Inject(method = "getUnfixedDataTagWithFallback", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;restoreLevelDataFromOld()Z")) + private void preserveBeforeFallbackRestoration(CallbackInfoReturnable> callback) { + EarlyWorldPreservation.beforeRead((LevelStorageSource.LevelStorageAccess) (Object) this, true); } - @ModifyArg(method = "saveDataTag(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/nbt/CompoundTag;)V", - at = @At(value = "INVOKE", target = "Lnet/minecraftforge/common/ForgeHooks;writeAdditionalLevelSaveData(Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/nbt/CompoundTag;)V"), - index = 1) - private CompoundTag preserveBeforeWrite(CompoundTag root) { + @Inject(method = "saveLevelData(Lnet/minecraft/nbt/CompoundTag;)V", at = @At("HEAD")) + private void preserveBeforeWrite(CompoundTag root, CallbackInfo callback) { EarlyWorldPreservation.beforeWrite((LevelStorageSource.LevelStorageAccess) (Object) this, root); - return root; } } diff --git a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java index a47cb61..4d72541 100644 --- a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java +++ b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java @@ -7,13 +7,10 @@ import net.minecraft.client.gui.screens.ConnectScreen; import net.minecraft.network.ConnectionProtocol; import net.minecraft.network.Connection; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; import net.minecraftforge.event.TickEvent; /** Repairs the pinned Netty's stale read-clear task during client login. */ -@OnlyIn(Dist.CLIENT) public final class ClientLoginReadGuard { public void tick(TickEvent.ClientTickEvent.Post event) { Minecraft minecraft=Minecraft.getInstance(); diff --git a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java index 976a8ba..089decd 100644 --- a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java +++ b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java @@ -5,6 +5,7 @@ import java.io.DataInputStream; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.Collections; import java.util.Map; @@ -14,6 +15,8 @@ import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.Tag; +import net.minecraft.util.filefix.AbortedFileFixException; +import net.minecraft.util.filefix.FileFixerUpper; import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelStorageSource; import org.apache.logging.log4j.Level; @@ -23,18 +26,23 @@ public final class EarlyWorldPreservation { private static volatile boolean installed; private static final Map metadata=Collections.synchronizedMap(new WeakHashMap<>()); + private static final Map relocating= + Collections.synchronizedMap(new WeakHashMap<>()); private EarlyWorldPreservation(){ } public static synchronized void install() { if(installed)return; verifyAttachment(LevelStorageSource.LevelStorageAccess.class); if (!UpgradePreservation.RegionAttached.class.isAssignableFrom(net.minecraft.world.level.chunk.storage.SimpleRegionStorage.class)) throw new IllegalStateException("Native region preservation could not be attached. World loading has been stopped."); + if (!LayoutAttached.class.isAssignableFrom(FileFixerUpper.class)) + throw new IllegalStateException("Native file relocation preservation could not be attached."); if(!(net.minecraft.util.datafix.DataFixers.getDataFixer() instanceof NativeSafeFixer)) throw new IllegalStateException("Native data fixer preservation could not be attached. World loading has been stopped."); installed=true; } /** Implemented only by the transformed native save access. */ public interface Attached { } + public interface LayoutAttached { } private static void verifyAttachment(Class reader) { if (reader != LevelStorageSource.LevelStorageAccess.class || !Attached.class.isAssignableFrom(reader)) @@ -43,13 +51,17 @@ private static void verifyAttachment(Class reader) { /** Called before Forge reads extra data or native level conversion. */ public static void beforeRead(LevelStorageSource.LevelStorageAccess save,boolean fallback) { + prepareRead(save, fallback); + } + + private static UpgradeJournal prepareRead(LevelStorageSource.LevelStorageAccess save, boolean fallback) { try { if(!installed)throw new IllegalStateException("Early preservation was not installed before world loading"); Path world=save.getLevelPath(LevelResource.ROOT); Path header=world.resolve(fallback?"level.dat_old":"level.dat"); if(!Files.isRegularFile(header)) { metadata.remove(save); - return; // The native reader selects or rejects the fallback. + return null; // The native reader selects or rejects the fallback. } CompoundTag root; try(DataInputStream input=new DataInputStream(new GZIPInputStream(Files.newInputStream(header)))) { @@ -57,9 +69,69 @@ public static void beforeRead(LevelStorageSource.LevelStorageAccess save,boolean } Tag original=root.get("lootablebodies"); if(original!=null)metadata.put(save,original.copy());else metadata.remove(save); - UpgradePreservation.prepareEarly(world,header); + return UpgradePreservation.prepareEarly(world,header); } catch(IOException | RuntimeException unsafe){throw new UnsafeWorldUpgrade(unsafe);} } + public static void beforeFileFix(LevelStorageSource.LevelStorageAccess save, boolean changesLayout) + throws AbortedFileFixException { + try { + Path world=save.getLevelPath(LevelResource.ROOT); + if(changesLayout) verifyLayoutCanMove(world); + UpgradeJournal preserved=prepareRead(save,false); + if(changesLayout && preserved!=null) relocating.put(save,preserved); + } catch(IOException unsafe) { + throw new AbortedFileFixException(new UnsafeFileLayout(new UnsafeWorldUpgrade(unsafe))); + } catch(UnsafeWorldUpgrade unsafe) { + throw new AbortedFileFixException(new UnsafeFileLayout(unsafe)); + } + } + + private static void verifyLayoutCanMove(Path world) throws IOException { + if(Files.exists(world.resolve("filefix"), LinkOption.NOFOLLOW_LINKS)) + throw new IOException("An earlier native file upgrade is incomplete"); + for(String dimension : new String[]{"overworld", "the_nether", "the_end"}) { + Path old = switch(dimension) { + case "the_nether" -> world.resolve("DIM-1"); + case "the_end" -> world.resolve("DIM1"); + default -> world; + }; + Path moved=world.resolve("dimensions/minecraft").resolve(dimension); + for(String store : new String[]{"region", "entities", "poi"}) { + requireMoveDestinationFree(old.resolve(store), moved.resolve(store)); + } + Path oldCapabilities=old.resolve("data/capabilities.dat"); + Path newCapabilities=moved.resolve("data/capabilities.dat"); + requireMoveDestinationFree(oldCapabilities,newCapabilities); + requireMoveDestinationFree(oldCapabilities,moved.resolve("data/forge/capabilities.dat")); + } + } + + private static void requireMoveDestinationFree(Path from, Path to) throws IOException { + if(Files.exists(from, LinkOption.NOFOLLOW_LINKS) && Files.exists(to, LinkOption.NOFOLLOW_LINKS)) + throw new IOException("Both old and new world paths exist: "+from+" and "+to); + } + + public static void afterFileFix(LevelStorageSource.LevelStorageAccess save) throws AbortedFileFixException { + UpgradeJournal preserved=relocating.remove(save); + if(preserved==null)return; + Path world=save.getLevelPath(LevelResource.ROOT); + try { + UpgradePreservation.prepareAfterLayoutMove(world,world.resolve("level.dat"),preserved); + } catch(IOException | RuntimeException unsafe) { + throw new AbortedFileFixException(new UnsafeFileLayout(new UnsafeWorldUpgrade(unsafe))); + } + } + + private static final class UnsafeFileLayout extends IOException { + private static final long serialVersionUID=1L; + UnsafeFileLayout(UnsafeWorldUpgrade cause){super(cause.getMessage(),cause);} + } + + public static void handleDedicatedFileRefusal(LevelStorageSource.LevelStorageAccess save, AbortedFileFixException failure) { + if(failure.getCause() instanceof UnsafeFileLayout rejected && + rejected.getCause() instanceof UnsafeWorldUpgrade unsafe) stopDedicated(save,unsafe); + } + /** The dedicated startup entry point has no shutdown hook before world construction. */ @FunctionalInterface public interface WorldReader { @@ -70,18 +142,24 @@ public static Dynamic readDedicatedWorld(LevelStorageSource.LevelStorageAcces try { return reader.read(); } catch(UnsafeWorldUpgrade unsafe) { - metadata.remove(save); - try { - save.close(); - } catch(IOException closeFailure) { - unsafe.addSuppressed(closeFailure); - } - ModLog.log(Level.ERROR,unsafe,unsafe.getMessage()); - LogManager.shutdown(); - System.exit(1); + stopDedicated(save,unsafe); throw unsafe; } } + + private static void stopDedicated(LevelStorageSource.LevelStorageAccess save, UnsafeWorldUpgrade unsafe) { + metadata.remove(save); + relocating.remove(save); + UpgradePreservation.close(); + try { + save.close(); + } catch(IOException closeFailure) { + unsafe.addSuppressed(closeFailure); + } + ModLog.log(Level.ERROR,unsafe,unsafe.getMessage()); + LogManager.shutdown(); + System.exit(1); + } /** Restore only this mod's original field on the same native save access. */ public static void beforeWrite(LevelStorageSource.LevelStorageAccess save,CompoundTag root) { Tag original=metadata.get(save);if(original!=null)root.put("lootablebodies",original.copy()); diff --git a/src/main/java/cyano/lootable/upgrade/ItemMigration.java b/src/main/java/cyano/lootable/upgrade/ItemMigration.java index 07952f3..bc2a0e8 100644 --- a/src/main/java/cyano/lootable/upgrade/ItemMigration.java +++ b/src/main/java/cyano/lootable/upgrade/ItemMigration.java @@ -22,6 +22,7 @@ private ItemMigration() { } private static final int LAST_LEGACY_ITEM_VERSION=3817; private static final int RELEASED_COMPONENT_VERSION=3839; private static final int RELEASED_121_VERSION=3955; + private static final int RELEASED_12111_VERSION=4671; /** Null means the caller retains the original raw record for recovery. */ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { @@ -42,9 +43,9 @@ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { if(source==UpgradePreservation.MINECRAFT_VERSION)return raw.copy(); if(source==RELEASED_COMPONENT_VERSION) { CompoundTag intermediate = convertReleasedComponents(raw); - return intermediate == null ? null : convert121Components(intermediate); + return intermediate == null ? null : convertModernComponents(intermediate,RELEASED_121_VERSION); } - if(source==RELEASED_121_VERSION)return convert121Components(raw); + if(source==RELEASED_121_VERSION || source==RELEASED_12111_VERSION)return convertModernComponents(raw,source); if(source<512 || source>LAST_LEGACY_ITEM_VERSION || pipeline==null)return null; CompoundTag legacy=convertNbt(raw,versions); if(legacy==null || !supportedLegacyStack(legacy))return null; @@ -53,7 +54,7 @@ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { input.remove("Damage"); CompoundTag fixed=NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK, input,LAST_LEGACY_ITEM_VERSION,RELEASED_121_VERSION); - return componentsSurvive(legacy,fixed,true)?convert121Components(fixed):null; + return componentsSurvive(legacy,fixed,true)?convertModernComponents(fixed,RELEASED_121_VERSION):null; } catch(RuntimeException invalid) { return null; } @@ -74,16 +75,23 @@ private static CompoundTag convertReleasedComponents(CompoundTag raw) { } } - private static CompoundTag convert121Components(CompoundTag raw) { + private static CompoundTag convertModernComponents(CompoundTag raw, int source) { if (pipeline == null || !Set.of("id", "count", "components", "Slot").containsAll(raw.keySet()) || !hasType(raw, "id", 8) || Identifier.tryParse(raw.getStringOr("id", "")) == null || (raw.contains("components") && !hasType(raw, "components", 10))) return null; Long count = raw.contains("count") ? integral(raw.get("count")) : Long.valueOf(1); if (count == null || count <= 0 || count > 127) return null; try { + CompoundTag released = raw; + if (source == RELEASED_121_VERSION) { + released = NativeNbt.update(pipeline, net.minecraft.util.datafix.fixes.References.ITEM_STACK, + raw.copy(), source, RELEASED_12111_VERSION); + if (!components121Survive(raw, released)) return null; + } CompoundTag fixed = NativeNbt.update(pipeline, net.minecraft.util.datafix.fixes.References.ITEM_STACK, - raw.copy(), RELEASED_121_VERSION, UpgradePreservation.MINECRAFT_VERSION); - return components121Survive(raw, fixed) ? fixed : null; + released.copy(), RELEASED_12111_VERSION, UpgradePreservation.MINECRAFT_VERSION); + // The 26.1.2 native fixer has no item changes after 1.21.11. + return released.equals(fixed) ? fixed : null; } catch (RuntimeException invalid) { return null; } diff --git a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java index 932ca5f..e840f47 100644 --- a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java +++ b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java @@ -131,10 +131,13 @@ public static Identifier soundReplacement(Identifier old) { if(old.getPath().equals("entity.zombie_pigman."+suffix))return Identifier.parse("minecraft:entity.zombified_piglin."+suffix); return null; } - /** This old Long memory had no serializer or saved values in vanilla1.15. */ + /** Retain the registry slots of retired memories that never serialized values. */ public static void retiredMemories(RegisterEvent event) { event.register(Registries.MEMORY_MODULE_TYPE,Identifier.parse("minecraft:golem_last_seen_time"), ()->new net.minecraft.world.entity.ai.memory.MemoryModuleType(java.util.Optional.empty())); + event.register(Registries.MEMORY_MODULE_TYPE,Identifier.parse("minecraft:interactable_doors"), + ()->new net.minecraft.world.entity.ai.memory.MemoryModuleType>( + java.util.Optional.empty())); } public static void items(MissingMappingsEvent event) { diff --git a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java index 41efade..3881658 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java +++ b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java @@ -103,7 +103,7 @@ else if(kind.equals("player")) { if(!hasType(result, "Level", 10))throw new IllegalStateException("Protected legacy biomes require staged native height conversion"); result.getCompoundOrEmpty("Level").put("Biomes",currentBiomes); } - if(currentSettings!=null)result.put("WorldGenSettings",updateCurrentWorldSettings((CompoundTag)currentSettings,source,target)); + if(currentSettings!=null)result.put(target >= 4771 ? "world_gen_settings" : "WorldGenSettings",updateCurrentWorldSettings((CompoundTag)currentSettings,source,target)); for(String token:tokens.values.keySet())if(!Integer.valueOf(1).equals(tokens.restored.get(token)))throw new IllegalStateException("Native conversion lost or duplicated protected Lootable Bodies data"); if(kind.equals("chunk")) { // Native height conversion flattens Level but leaves DataVersion @@ -325,7 +325,7 @@ private CompoundTag sourceContext(CompoundTag entity,int version,int x,int z,boo }catch(IOException damaged){throw new IllegalStateException("Corpse preservation could not be verified",damaged);} } CompoundTag context=new CompoundTag();int marker=entity.getIntOr(UpgradePreservation.VERSION, 0); - context.putInt("DataVersion",entity.getStringOr("id", "").equals("lootablebodies.Corpse")?512:marker==1?922:marker==2?1343:marker==3?1631:marker==4?1976:marker==5?2230:marker==6?2586:marker==7?2730:marker==8?2975:marker==9?3337:marker==10?3465:marker==11?3839:marker==12?3955:version);return context; + context.putInt("DataVersion",entity.getStringOr("id", "").equals("lootablebodies.Corpse")?512:marker==1?922:marker==2?1343:marker==3?1631:marker==4?1976:marker==5?2230:marker==6?2586:marker==7?2730:marker==8?2975:marker==9?3337:marker==10?3465:marker==11?3839:marker==12?3955:marker==13?4671:version);return context; } static CompoundTag stageCorpse(CompoundTag raw,CompoundTag context) { if(hasType(raw, UpgradePreservation.VERSION, 3) && raw.getIntOr(UpgradePreservation.VERSION, 0)>UpgradePreservation.DATA_VERSION)throw new IllegalStateException("This corpse was saved by a newer Lootable Bodies format"); diff --git a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java index 7193479..5f1808b 100644 --- a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java +++ b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java @@ -48,9 +48,19 @@ public static UpgradeJournal scan(Path world) throws IOException { } static UpgradeJournal scan(Path world,Path levelHeader)throws IOException { + return scan(world, levelHeader, null); + } + + static UpgradeJournal scanAfterLayoutMove(Path world, Path levelHeader, UpgradeJournal before) throws IOException { + if (!world.toRealPath().equals(before.world)) + throw new IOException("Native relocation must preserve the same world identity"); + return scan(world, levelHeader, before.worldVersions()); + } + + private static UpgradeJournal scan(Path world, Path levelHeader, CompoundTag preMoveContext) throws IOException { Files.createDirectories(world); UpgradeJournal journal=new UpgradeJournal(world,levelHeader); - journal.inspect(); + journal.inspect(preMoveContext); for(Path original:journal.active.values()) { CompoundTag corpse=journal.readSnapshot(original).getCompoundOrEmpty("Entity"); if(hasType(corpse, UpgradePreservation.VERSION, 3) && corpse.getIntOr(UpgradePreservation.VERSION, 0)>UpgradePreservation.DATA_VERSION) @@ -125,7 +135,7 @@ private static void collect(CompoundTag entity,int root,int[] path,List regions=new ArrayList<>(); @@ -149,13 +159,23 @@ private void inspect() throws IOException { for(Path path:files) readSnapshot(path); } Map previous=readIndex(); - CompoundTag worldVersions=readWorldVersions();worldContext=worldVersions.copy(); + CompoundTag worldVersions=preMoveContext != null ? preMoveContext : readWorldVersions(); + worldContext=worldVersions.copy(); ListTag updated=new ListTag(); for(Path region:regions) { String relative=world.relativize(region).toString().replace('\\','/'); String hash=fileDigest(region); CompoundTag record=previous.get(relative); - if(record==null || !hash.equals(record.getStringOr("Digest", ""))) + if(record==null && preMoveContext!=null) { + String oldPath=legacyRegionPath(relative); + if(oldPath!=null) { + if(Files.exists(world.resolve(oldPath), LinkOption.NOFOLLOW_LINKS)) + throw new IOException("Both old and relocated region paths exist: "+relative); + record=previous.get(oldPath); + } + } + if(record==null || !relative.equals(record.getStringOr("Path", "")) + || !hash.equals(record.getStringOr("Digest", ""))) record=scanRegion(region,relative,hash,worldVersions,record); else record=record.copy(); ListTag originals=typedList(record, "Snapshots", 8); @@ -179,6 +199,18 @@ private void inspect() throws IOException { atomic(directory.resolve("index.nbt"),compressed(envelope),true); } + static String legacyRegionPath(String path) { + for(String store : new String[]{"region", "entities"}) { + if(path.startsWith("dimensions/minecraft/overworld/"+store+"/")) + return path.substring("dimensions/minecraft/overworld/".length()); + if(path.startsWith("dimensions/minecraft/the_nether/"+store+"/")) + return "DIM-1/"+path.substring("dimensions/minecraft/the_nether/".length()); + if(path.startsWith("dimensions/minecraft/the_end/"+store+"/")) + return "DIM1/"+path.substring("dimensions/minecraft/the_end/".length()); + } + return null; + } + private Map readIndex() throws IOException { Map records=new HashMap<>(); Path index=directory.resolve("index.nbt"); @@ -355,7 +387,8 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co else if(marker==10)version=3465; else if(marker==11)version=3839; else if(marker==12)version=3955; - else if(marker==13)version=UpgradePreservation.MINECRAFT_VERSION; + else if(marker==13)version=4671; + else if(marker==14)version=UpgradePreservation.MINECRAFT_VERSION; if(legacy) {if(version!=512)context.putBoolean("Inferred",true);version=512;} if(legacy || (marker>=1 && marker<=UpgradePreservation.DATA_VERSION)) context.putInt("CorpseWriterDataVersion",version); @@ -364,7 +397,8 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co for(int i=0;i structures) { + return upgradeRegion(fixer, kind, input, fallback, context, MINECRAFT_VERSION, structures); + } + + public static CompoundTag upgradeRegion(DataFixer fixer, DataFixTypes kind, CompoundTag input, + int fallback, CompoundTag context, int target, UnaryOperator structures) { CompoundTag copy = input.copy(); SimpleRegionStorage.injectDatafixingContext(copy, context); DataFixer delegate = fixer instanceof NativeSafeFixer safe ? safe.delegate : fixer; boolean terrain = kind == DataFixTypes.CHUNK; CompoundTag converted = NativeNbt.update(new NativeSafeFixer(delegate, terrain ? structures : null), terrain ? References.CHUNK : References.ENTITY_CHUNK, copy, - NbtUtils.getDataVersion(copy, fallback), MINECRAFT_VERSION); + NbtUtils.getDataVersion(copy, fallback), target); converted.remove("__context"); return converted; } @@ -90,7 +99,7 @@ public static CompoundTag upgradeLevelCapabilities(CompoundTag input) { if (input.contains("DataVersion")) { if (!hasType(input, "DataVersion", 3)) return input; int source = input.getIntOr("DataVersion", -1); - if (source < 512 || source >= MINECRAFT_VERSION) return input; + if (source < 512 || source >= 4671) return input; } else if (!input.keySet().equals(java.util.Set.of("data")) || !input.getCompoundOrEmpty("data").isEmpty()) { // The original Forge writer emitted an unstamped empty capability // record. Leave other unversioned shapes to the native codec. @@ -119,7 +128,7 @@ static int chunkSourceVersion(CompoundTag versions) { for(int m=0;m(POS, Collections.singletonList(entity))); - assertFalse(f.manager.areEntitiesLoaded(POS.toLong())); - NativeEntityInbox.await(f.manager, POS.toLong(), () -> false, () -> 0, 10); - assertTrue(f.manager.areEntitiesLoaded(POS.toLong())); + assertFalse(f.manager.areEntitiesLoaded(POS.pack())); + NativeEntityInbox.await(f.manager, POS.pack(), () -> false, () -> 0, 10); + assertTrue(f.manager.areEntitiesLoaded(POS.pack())); assertEquals(1, f.tracked.get()); assertEquals(1, f.storage.loads); assertEquals(0, f.storage.stores); } @Test void delayedReadProcessesNativeInboxAfterPollingTasks() { Fixture f = new Fixture(); AtomicInteger polls = new AtomicInteger(); AtomicLong clock = new AtomicLong(); - NativeEntityInbox.await(f.manager, POS.toLong(), () -> { + NativeEntityInbox.await(f.manager, POS.pack(), () -> { polls.incrementAndGet(); f.storage.read.complete(new ChunkEntities<>(POS, Collections.singletonList(new Saved()))); return true; }, clock::getAndIncrement, 10); - assertEquals(1, polls.get()); assertEquals(1, f.tracked.get()); assertTrue(f.manager.areEntitiesLoaded(POS.toLong())); + assertEquals(1, polls.get()); assertEquals(1, f.tracked.get()); assertTrue(f.manager.areEntitiesLoaded(POS.pack())); } @Test void pendingReadRefusesAtDeadlineWithoutInventingEmptySuccessOrSaving() { Fixture f = new Fixture(); AtomicLong clock = new AtomicLong(); IllegalStateException failure = assertThrows(IllegalStateException.class, () -> - NativeEntityInbox.await(f.manager, POS.toLong(), () -> false, clock::getAndIncrement, 3)); + NativeEntityInbox.await(f.manager, POS.pack(), () -> false, clock::getAndIncrement, 3)); assertTrue(failure.getMessage().contains("deadline")); - assertFalse(f.manager.areEntitiesLoaded(POS.toLong())); assertFalse(f.storage.read.isDone()); + assertFalse(f.manager.areEntitiesLoaded(POS.pack())); assertFalse(f.storage.read.isDone()); assertEquals(1, f.storage.loads); assertEquals(0, f.storage.stores); assertEquals(0, f.tracked.get()); } @Test void genuineEmptyStorageStillPassesThroughNativeInbox() { Fixture f = new Fixture(); f.storage.read.complete(new ChunkEntities<>(POS, Collections.emptyList())); - assertFalse(f.manager.areEntitiesLoaded(POS.toLong())); - NativeEntityInbox.await(f.manager, POS.toLong(), () -> false, () -> 0, 10); - assertTrue(f.manager.areEntitiesLoaded(POS.toLong())); assertEquals(0, f.tracked.get()); assertEquals(0, f.storage.stores); + assertFalse(f.manager.areEntitiesLoaded(POS.pack())); + NativeEntityInbox.await(f.manager, POS.pack(), () -> false, () -> 0, 10); + assertTrue(f.manager.areEntitiesLoaded(POS.pack())); assertEquals(0, f.tracked.get()); assertEquals(0, f.storage.stores); } private static final class BeforeGameLoop extends net.minecraft.util.thread.BlockableEventLoop { private final Thread owner=Thread.currentThread(); - BeforeGameLoop(){super("NativeFixtureBeforeGameLoop");} + BeforeGameLoop(){super("NativeFixtureBeforeGameLoop",false);} + @Override public boolean pollTask(){return super.pollTask();} public Runnable wrapRunnable(Runnable task){return task;} protected boolean shouldRun(Runnable task){return false;} protected Thread getRunningThread(){return owner;} @@ -94,14 +95,14 @@ private static final class BeforeGameLoop extends net.minecraft.util.thread.Bloc Fixture f=new Fixture();BeforeGameLoop loop=new BeforeGameLoop();AtomicLong clock=new AtomicLong(); loop.schedule(()->f.storage.read.complete(new ChunkEntities<>(POS,Collections.singletonList(new Saved())))); assertFalse(loop.pollTask(),"Native event-loop gate must reproduce the pre-loop condition"); - NativeEntityInbox.await(f.manager,POS.toLong(),loop::pollTask,clock::getAndIncrement,10,loop::managedBlock); - assertTrue(f.manager.areEntitiesLoaded(POS.toLong()));assertEquals(1,f.tracked.get());assertEquals(0,loop.getPendingTasksCount()); + NativeEntityInbox.await(f.manager,POS.pack(),loop::pollTask,clock::getAndIncrement,10,loop::managedBlock); + assertTrue(f.manager.areEntitiesLoaded(POS.pack()));assertEquals(1,f.tracked.get());assertEquals(0,loop.getPendingTasksCount()); loop.schedule(()->{});assertFalse(loop.pollTask(),"Managed blocking must not leave the native task gate bypassed"); } @Test void nativeManagedBlockRestoresItsGateAfterAPendingDeadline() { Fixture f=new Fixture();BeforeGameLoop loop=new BeforeGameLoop();AtomicLong clock=new AtomicLong(); - assertThrows(IllegalStateException.class,()->NativeEntityInbox.await(f.manager,POS.toLong(),loop::pollTask, + assertThrows(IllegalStateException.class,()->NativeEntityInbox.await(f.manager,POS.pack(),loop::pollTask, clock::getAndIncrement,3,loop::managedBlock)); - loop.schedule(()->{});assertFalse(loop.pollTask());assertFalse(f.manager.areEntitiesLoaded(POS.toLong()));assertEquals(0,f.storage.stores); + loop.schedule(()->{});assertFalse(loop.pollTask());assertFalse(f.manager.areEntitiesLoaded(POS.pack()));assertEquals(0,f.storage.stores); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java index 5cc3a1b..803bde5 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java @@ -11,7 +11,7 @@ import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.*; import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; import net.minecraft.world.item.*; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @@ -55,13 +55,13 @@ private RegistryAccess registries() { changed.put(6, HashedStack.create(stack(13), hash)); changed.put(47, HashedStack.EMPTY); ServerboundContainerClickPacket outgoing = new ServerboundContainerClickPacket( - 7, 32766, (short)6, (byte)1, ClickType.PICKUP, changed, HashedStack.create(stack(9), hash)); + 7, 32766, (short)6, (byte)1, ContainerInput.PICKUP, changed, HashedStack.create(stack(9), hash)); RegistryFriendlyByteBuf buffer=wire(); try { ServerboundContainerClickPacket.STREAM_CODEC.encode(buffer,outgoing);byte[] before=new byte[buffer.readableBytes()];buffer.getBytes(0,before); ServerboundContainerClickPacket decoded=ServerboundContainerClickPacket.STREAM_CODEC.decode(buffer); assertEquals(7,decoded.containerId());assertEquals(32766,decoded.stateId());assertEquals(6,decoded.slotNum());assertEquals(1,decoded.buttonNum()); - assertEquals(ClickType.PICKUP,decoded.clickType());assertTrue(decoded.carriedItem().matches(stack(9), hash)); + assertEquals(ContainerInput.PICKUP,decoded.containerInput());assertTrue(decoded.carriedItem().matches(stack(9), hash)); assertEquals(changed.keySet(),decoded.changedSlots().keySet());assertTrue(decoded.changedSlots().get(6).matches(stack(13), hash));assertTrue(decoded.changedSlots().get(47).matches(ItemStack.EMPTY, hash)); assertEquals(before.length,buffer.readerIndex());byte[] after=new byte[before.length];buffer.getBytes(0,after);assertArrayEquals(before,after); }finally{buffer.release();} diff --git a/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java index 04dcf33..6298b67 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117ProbeIsolationTest.java @@ -16,7 +16,7 @@ private Set packages(String archive) throws Exception { java.util.zip.ZipEntry entry=entries.nextElement();String path=entry.getName(); if(!path.endsWith(".class"))continue; byte[] code=zip.getInputStream(entry).readAllBytes(); - assertEquals(65,((code[6]&255)<<8)|(code[7]&255),path); + assertEquals(69,((code[6]&255)<<8)|(code[7]&255),path); result.add(path.substring(0,path.lastIndexOf('/'))); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java b/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java index 20b8778..a786387 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java +++ b/src/test/java/cyano/lootable/buildtest/Native117TestEnvironment.java @@ -4,40 +4,48 @@ import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.ModLoader; import net.minecraftforge.fml.loading.FMLLoader; -import net.minecraftforge.fml.loading.LoadingModList; import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; /** Boots native codecs in plain JUnit without discovering or loading any mods. */ public final class Native117TestEnvironment implements LauncherSessionListener { + private static boolean initialized; + @Override public void launcherSessionOpened(LauncherSession session) { initialize(); } public static synchronized void initialize() { net.minecraft.SharedConstants.tryDetectVersion(); - if(FMLLoader.getLoadingModList()==null) { + if(!initialized) { try { - LoadingModList loading=LoadingModList.of(List.of(),List.of(),null); - loading.setBrokenFiles(List.of()); - var loadingField=FMLLoader.class.getDeclaredField("loadingModList"); - loadingField.setAccessible(true); - loadingField.set(null,loading); + var stateType=Class.forName("net.minecraftforge.fml.loading.ModSorter$State"); + var stateConstructor=stateType.getDeclaredConstructor(List.class,List.class); + stateConstructor.setAccessible(true); + var loadingType=Class.forName("net.minecraftforge.fml.loading.LoadingModListImpl"); + var init=loadingType.getDeclaredMethod("init",stateType, + net.minecraftforge.fml.loading.EarlyLoadingException.class); + init.setAccessible(true); + init.invoke(null,stateConstructor.newInstance(List.of(),List.of()),null); var layerField=FMLLoader.class.getDeclaredField("moduleLayerManager"); layerField.setAccessible(true); layerField.set(null,(cpw.mods.modlauncher.api.IModuleLayerManager) layer->java.util.Optional.of(ModuleLayer.boot())); - ModList mods=ModList.of(List.of(),List.of()); + ModList.init(); var setLoaded=ModList.class.getDeclaredMethod("setLoadedMods",List.class); setLoaded.setAccessible(true); - setLoaded.invoke(mods,List.of()); + setLoaded.invoke(null,List.of()); var valid=ModLoader.class.getDeclaredField("loadingStateValid"); valid.setAccessible(true); - valid.setBoolean(ModLoader.get(),true); + valid.setBoolean(null,true); + net.minecraft.server.Bootstrap.bootStrap(); + net.minecraft.core.registries.BuiltInRegistries.DATA_COMPONENT_INITIALIZERS + .build(net.minecraft.data.registries.VanillaRegistries.createLookup()) + .forEach(net.minecraft.core.component.DataComponentInitializers.PendingComponents::apply); + initialized=true; } catch(ReflectiveOperationException failure) { throw new IllegalStateException("Cannot initialize the empty Forge unit-test context",failure); } } - net.minecraft.server.Bootstrap.bootStrap(); } } diff --git a/src/test/java/cyano/lootable/contracts/BuildContractTest.java b/src/test/java/cyano/lootable/contracts/BuildContractTest.java index 5304acc..4fb5f49 100644 --- a/src/test/java/cyano/lootable/contracts/BuildContractTest.java +++ b/src/test/java/cyano/lootable/contracts/BuildContractTest.java @@ -49,11 +49,11 @@ private String text(String path) throws Exception { assertTrue(info.contains("modId=\"lootablebodies\"")); Properties release=new Properties();try(java.io.InputStream input=Files.newInputStream(Paths.get("gradle.properties"))){release.load(input);} assertTrue(info.contains("version=\""+release.getProperty("mod_version")+"\"")); - assertTrue(info.contains("versionRange=\"[1.21.11]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); + assertTrue(info.contains("versionRange=\"[26.1.2]\""));assertTrue(info.contains("Cyanobacterium"));assertFalse(info.contains("${")); assertFalse(Files.exists(Paths.get("build/resources/main/mcmod.info"))); com.google.gson.JsonObject pack=new com.google.gson.JsonParser().parse(text("build/resources/main/pack.mcmeta")).getAsJsonObject().getAsJsonObject("pack"); - assertEquals("[94,1]",pack.getAsJsonArray("min_format").toString()); - assertEquals("[94,1]",pack.getAsJsonArray("max_format").toString()); + assertEquals("[84,0]",pack.getAsJsonArray("min_format").toString()); + assertEquals("[101,1]",pack.getAsJsonArray("max_format").toString()); assertFalse(pack.has("pack_format")); assertFalse(pack.has("supported_formats")); assertFalse(pack.has("forge:server_data_pack_format")); diff --git a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java index 50132d0..1a47945 100644 --- a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java +++ b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java @@ -149,7 +149,7 @@ private void save() throws Exception { cyano.lootable.graphics.CorpseGUIContainer screen=new cyano.lootable.graphics.CorpseGUIContainer( (cyano.lootable.graphics.CorpseContainer)null,new net.minecraft.world.entity.player.Inventory(null,new net.minecraft.world.entity.EntityEquipment()),net.minecraft.network.chat.Component.literal("Saved owner")); java.lang.reflect.Method foreground=cyano.lootable.graphics.CorpseGUIContainer.class.getDeclaredMethod( - "renderLabels",net.minecraft.client.gui.GuiGraphics.class,int.class,int.class);foreground.setAccessible(true); + "extractLabels",net.minecraft.client.gui.GuiGraphicsExtractor.class,int.class,int.class);foreground.setAccessible(true); // The original layout uses all seven rows. Native labels would draw // over those slots; its original foreground needs no font or item state. foreground.invoke(screen,null,0,0); diff --git a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java index 2c87042..c33a232 100644 --- a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java +++ b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java @@ -62,7 +62,7 @@ private CompoundTag find(Tag value) { } @Test void legacyIdentityAndEveryRawInventoryEntrySurviveBothAnvilPasses()throws Exception { CompoundTag body=corpse(),source=chunk(body,true);byte[] before=encoded(source);CompoundTag result=update(source),saved=findCorpse(result); - assertEquals("lootablebodies:corpse",saved.getStringOr("id", ""));assertEquals(13,saved.getIntOr("LootableBodiesDataVersion", 0));assertEquals(ID,readUuid(saved, "UUID")); + assertEquals("lootablebodies:corpse",saved.getStringOr("id", ""));assertEquals(14,saved.getIntOr("LootableBodiesDataVersion", 0));assertEquals(ID,readUuid(saved, "UUID")); ListTag records=typedList(saved, "LootableBodiesUnresolved", 10);assertEquals(49,records.size()); for(int i=0;i<48;i++){CompoundTag record=records.getCompoundOrEmpty(i);assertEquals("Equipment",record.getStringOr("Source", ""));assertArrayEquals(encoded(typedList(body, "Equipment", 10).getCompoundOrEmpty(i)),encoded(record.getCompoundOrEmpty("Data")));assertEquals(512,record.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0));} assertEquals("Aux",records.getCompoundOrEmpty(48).getStringOr("Source", ""));assertArrayEquals(before,encoded(source));assertFalse(saved.contains("LootableBodiesUpgradePayload"));assertFalse(saved.contains("LootableBodiesInventorySource")); diff --git a/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java b/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java index 5ed9697..b81b2d4 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.14.121111", version); + assertEquals("2.4.15.2601021", 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("Identifier.fromNamespaceAndPath(MODID,\"corpse\")")); - assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.21.11]\"")); + assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[26.1.2]\"")); } @Test void maintainedReleaseHasLgplAndRetainsTheOriginalNotice() throws Exception { diff --git a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java index 3e9ecfc..98c501d 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java @@ -33,13 +33,13 @@ private CompoundTag body(int marker) { return body; } @Test void currentFormatNamesThe119VanillaSchema() { - assertEquals(4671, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(13,UpgradePreservation.DATA_VERSION); + assertEquals(4790, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(14,UpgradePreservation.DATA_VERSION); } @Test void released113EquipmentAndOverflowAreRetainedBeforeNativeConversion() { CompoundTag original = body(3), before = original.copy(); CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(readUuid(original, "UUID"), readUuid(staged, "UUID")); assertTrue(typedList(staged, "Equipment", 10).isEmpty()); ListTag records = typedList(staged, "LootableBodiesUnresolved", 10); assertEquals(49, records.size()); @@ -57,7 +57,7 @@ private CompoundTag body(int marker) { record.put("SourceVersions", context(922)); ListTag recovery = new ListTag(); recovery.add(record.copy()); original.put("LootableBodiesUnresolved", recovery); CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(record, typedList(staged, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(49)); assertEquals(record, typedList(original, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(0)); } diff --git a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java index 28dd6bd..d679e03 100644 --- a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java @@ -35,8 +35,8 @@ private int[] currentBiomes() { return values; } @Test void currentFormatUsesThe119SchemaAndItsOwnMarker() { - assertEquals(4671, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(13,UpgradePreservation.DATA_VERSION); + assertEquals(4790, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(14,UpgradePreservation.DATA_VERSION); } @Test void currentBiomeValuesSurvivePlausibleOlderStampsAndRepeatedConversion() { int[] values = currentBiomes(); CompoundTag original = chunk(values), before = original.copy(); @@ -80,7 +80,7 @@ private int[] currentBiomes() { ListTag items = new ListTag(); items.add(item); raw.put("Equipment", items); context.putInt("DataVersion", 1976); CompoundTag before = raw.copy(); CompoundTag staged = NativeSafeFixer.stageCorpse(raw, context); - assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); CompoundTag record = typedList(staged, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(0); assertEquals(item, record.get("Data")); assertEquals("Equipment", record.getStringOr("Source", "")); assertEquals(1976, record.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); diff --git a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java index dedaf70..2b7d51b 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java @@ -55,7 +55,7 @@ private void preserve(CompoundTag chunk)throws Exception { context.putString("dimension", Level.OVERWORLD.identifier().toString()); CompoundTag nativeInput = source.copy(); SimpleRegionStorage.injectDatafixingContext(nativeInput, context); - CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeInput,2230,4671); + CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeInput,2230,4790); CompoundTag damaged=source.copy();damaged.remove("DataVersion");preserve(damaged); byte[] originalRegion=Files.readAllBytes(world.resolve("region/r.0.0.mca")); try(SimpleRegionStorage loader = new SimpleRegionStorage( @@ -63,7 +63,7 @@ private void preserve(CompoundTag chunk)throws Exception { world.resolve("reader"), NativeVanillaDataFixer.get(), false, DataFixTypes.CHUNK)) { CompoundTag actual = UpgradePreservation.upgradeRegion(NativeVanillaDataFixer.get(), DataFixTypes.CHUNK, damaged, 512, context, input -> { throw new AssertionError("Current source must not replay legacy structure conversion"); }); - assertEquals(expected.get("block_entities"),loader.upgradeChunkTag(source.copy(), 2230, context).get("block_entities")); + assertEquals(expected.get("block_entities"),loader.upgradeChunkTag(source.copy(), 2230, context, net.minecraft.SharedConstants.getCurrentVersion().dataVersion().version()).get("block_entities")); assertNotNull(expected.get("block_entities"));assertFalse(actual.contains("Level")); assertEquals(expected.get("block_entities"),actual.get("block_entities")); assertArrayEquals(originalRegion,Files.readAllBytes(world.resolve("region/r.0.0.mca"))); diff --git a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java index 568c2cc..df6db75 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java @@ -40,7 +40,7 @@ private CompoundTag legacy() { body.put("Equipment",inventory);return body; } @Test void currentFormatUsesThe119SchemaAndMarker() { - assertEquals(4671,UpgradePreservation.MINECRAFT_VERSION);assertEquals(13,UpgradePreservation.DATA_VERSION); + assertEquals(4790,UpgradePreservation.MINECRAFT_VERSION);assertEquals(14,UpgradePreservation.DATA_VERSION); } @Test void legacyCorpseUuidSurvivesShieldingWithoutTruncatingCustomInventory() { CompoundTag body=legacy(),before=body.copy(),context=new CompoundTag();context.putInt("DataVersion",2230); diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java index 6d4cd75..64763b5 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java @@ -31,7 +31,7 @@ private CompoundTag convert(CompoundTag root,int source) { CompoundTag raw=chunk(2586,cart),before=raw.copy();Path region=write(world,raw);byte[] bytes=Files.readAllBytes(region); UpgradePreservation.prepare(world); CompoundTag fixed=convert(raw,2586).getListOrEmpty("Entities").getCompoundOrEmpty(0).getListOrEmpty("Passengers").getCompoundOrEmpty(0); - assertEquals(13,fixed.getIntOr(UpgradePreservation.VERSION, 0));assertEquals(2586,fixed.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); + assertEquals(14,fixed.getIntOr(UpgradePreservation.VERSION, 0));assertEquals(2586,fixed.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); ListTag records=typedList(fixed, "LootableBodiesUnresolved", 10);assertEquals(50,records.size()); for(int slot=0;slot<48;slot++) {assertEquals(typedList(corpse, "Equipment", 10).getCompoundOrEmpty(slot),records.getCompoundOrEmpty(slot).getCompoundOrEmpty("Data"));assertEquals("Equipment",records.getCompoundOrEmpty(slot).getStringOr("Source", ""));} assertEquals(typedList(corpse, "Aux", 10).getCompoundOrEmpty(0),records.getCompoundOrEmpty(48).getCompoundOrEmpty("Data")); diff --git a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java index bf9bde1..614bda4 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java @@ -13,18 +13,19 @@ class Native118EarlyHookTest { var nativeClass = Native119CoremodEngineTest.classFile(SAVE); var mixin = Native119CoremodEngineTest.mixin("SaveAccessMixin", SAVE); assertTrue(mixin.interfaces.contains(HOOK+"$Attached")); - Native119CoremodEngineTest.method(nativeClass,"getDataTag","(Z)Lcom/mojang/serialization/Dynamic;"); + var fallback = Native119CoremodEngineTest.method(nativeClass,"getUnfixedDataTagWithFallback","()Lcom/mojang/serialization/Dynamic;"); + Native119CoremodEngineTest.requiredCall(fallback,SAVE,"restoreLevelDataFromOld"); var write = Native119CoremodEngineTest.method(nativeClass, "saveDataTag", - "(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/nbt/CompoundTag;)V"); + "(Lnet/minecraft/world/level/storage/WorldData;Ljava/util/UUID;)V"); assertEquals(1, Native119CoremodEngineTest.calls(write, "net/minecraftforge/common/ForgeHooks", "writeAdditionalLevelSaveData")); - assertEquals(1, Native119CoremodEngineTest.calls(Native119CoremodEngineTest.named(mixin,"preserveBeforeRead"), HOOK, "beforeRead")); + assertEquals(1, Native119CoremodEngineTest.calls(Native119CoremodEngineTest.named(mixin,"preserveBeforeFallbackRestoration"), HOOK, "beforeRead")); assertEquals(1, Native119CoremodEngineTest.calls(Native119CoremodEngineTest.named(mixin,"preserveBeforeWrite"), HOOK, "beforeWrite")); } @Test void missingNativeWriteSeamIsRejectedByTheLayoutContract() throws Exception { var nativeClass = Native119CoremodEngineTest.classFile(SAVE); var write = Native119CoremodEngineTest.method(nativeClass, "saveDataTag", - "(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/nbt/CompoundTag;)V"); + "(Lnet/minecraft/world/level/storage/WorldData;Ljava/util/UUID;)V"); for (var instruction : write.instructions.toArray()) { if (instruction instanceof MethodInsnNode call && call.owner.equals("net/minecraftforge/common/ForgeHooks") diff --git a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java index 7b1efe0..0f17753 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java @@ -26,14 +26,14 @@ class Native118EntityBoundaryTest { assertEquals(1,journal.size()); CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3955); CompoundTag actual=typedList(converted, "Entities", 10).getCompoundOrEmpty(0); - assertEquals(13,actual.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,actual.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(2730,actual.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); assertEquals(50,typedList(actual, "LootableBodiesUnresolved", 10).size()); for(int slot=0;slot<48;slot++)assertEquals(typedList(corpse, "Equipment", 10).getCompoundOrEmpty(slot),typedList(actual, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(slot).getCompoundOrEmpty("Data")); assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); } @Test void futureFlattenedTerrainIsRefusedDuringPreflightEvenWithoutACorpse() throws Exception { - CompoundTag root=new CompoundTag();root.putInt("DataVersion",4672);root.putInt("xPos",0);root.putInt("zPos",0); + CompoundTag root=new CompoundTag();root.putInt("DataVersion",4791);root.putInt("xPos",0);root.putInt("zPos",0); root.put("sections",new ListTag());root.put("entities",new ListTag()); Path folder=world.resolve("region");Files.createDirectories(folder);Path file=folder.resolve("r.0.0.mca"); try(RegionFile region=new RegionFile(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"region"),file,folder,false);DataOutputStream output=region.getChunkDataOutputStream(new ChunkPos(0,0))){NbtIo.write(root,output);} @@ -42,7 +42,7 @@ class Native118EntityBoundaryTest { assertNull(UpgradePreservation.journal());assertArrayEquals(original,Files.readAllBytes(file)); } @Test void futureSeparateEntityVersionRefusesEvenWhenItsCorpseHasAnOlderKnownMarker() throws Exception { - CompoundTag raw=chunk(4672,body(new UUID(118,172),7)),before=raw.copy(); + CompoundTag raw=chunk(4791,body(new UUID(118,172),7)),before=raw.copy(); Path file=write(world,raw);byte[] original=Files.readAllBytes(file); assertThrows(IOException.class,()->UpgradePreservation.prepareEarly(world,world.resolve("level.dat"))); assertNull(UpgradePreservation.journal());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); @@ -54,7 +54,7 @@ class Native118EntityBoundaryTest { CompoundTag raw=chunk(2730,body(new UUID(118,173+i),7));raw.put("DataVersion",stamps[i]);CompoundTag before=raw.copy(); Path file=write(folder,raw);byte[] original=Files.readAllBytes(file);UpgradePreservation.prepareEarly(folder,folder.resolve("level.dat")); CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3955).getListOrEmpty("Entities").getCompoundOrEmpty(0); - assertEquals(13,converted.getIntOr(UpgradePreservation.VERSION, 0));assertEquals(2730,converted.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); + assertEquals(14,converted.getIntOr(UpgradePreservation.VERSION, 0));assertEquals(2730,converted.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); assertEquals(50,typedList(converted, "LootableBodiesUnresolved", 10).size());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java index 475f3a4..bcbb921 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java @@ -125,7 +125,7 @@ private CompoundTag legacy() { NbtIo.writeCompressed(expected,world.resolve("native-reference.nbt")); CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,3955); assertFalse(fixed.contains("Level"));assertEquals(expected.get("sections"),fixed.get("sections"));assertTrue(typedList(fixed, "sections", 10).size()>=16); - CompoundTag staged=typedList(fixed, "entities", 10).getCompoundOrEmpty(0);assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + CompoundTag staged=typedList(fixed, "entities", 10).getCompoundOrEmpty(0);assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(50,typedList(staged, "LootableBodiesUnresolved", 10).size());assertEquals(2730,staged.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); old.remove("__context");assertEquals(before,old);assertArrayEquals(original,Files.readAllBytes(file)); } @@ -147,10 +147,10 @@ private CompoundTag legacy() { assertEquals(before,old);assertArrayEquals(original,Files.readAllBytes(file)); } @Test void genuinelyFutureFlattenedTerrainStillRefusesBeforeConversion()throws Exception { - CompoundTag future=root();future.putInt("DataVersion",4672);CompoundTag before=future.copy(); + CompoundTag future=root();future.putInt("DataVersion",4791);CompoundTag before=future.copy(); Path file=write(future);byte[] original=Files.readAllBytes(file); assertThrows(IOException.class,()->UpgradeJournal.scan(world)); - assertThrows(Error.class,()->NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,future,4672,4671)); + assertThrows(Error.class,()->NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,future,4791,4790)); assertEquals(before,future);assertArrayEquals(original,Files.readAllBytes(file)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java index 06e9e6c..50c8ef3 100644 --- a/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java @@ -12,7 +12,7 @@ import org.objectweb.asm.tree.MethodNode; import static org.junit.jupiter.api.Assertions.*; -/** Forge 61 uses required Mixins. Packaged runs separately exercise their actual application. */ +/** Forge uses required Mixins. Packaged runs separately exercise their actual application. */ class Native119CoremodEngineTest { static ClassNode classFile(String name) throws Exception { try (var input = Native119CoremodEngineTest.class.getClassLoader().getResourceAsStream(name+".class")) { @@ -62,7 +62,7 @@ static void requiredCall(MethodNode method, String owner, String name) { var config = JsonParser.parseReader(new InputStreamReader(input, StandardCharsets.UTF_8)).getAsJsonObject(); assertTrue(config.get("required").getAsBoolean()); assertEquals(1, config.getAsJsonObject("injectors").get("defaultRequire").getAsInt()); - assertEquals(java.util.Set.of("DataFixersMixin","SaveAccessMixin","RegionStorageMixin","LegacyStructuresMixin","CapabilityDataMixin"), + assertEquals(java.util.Set.of("DataFixersMixin","SaveAccessMixin","RegionStorageMixin","FileFixerMixin","FileFixLevelDataMixin","DimensionStorageMixin","CapabilityDataMixin"), config.getAsJsonArray("mixins").asList().stream().map(v -> v.getAsString()).collect(java.util.stream.Collectors.toSet())); assertEquals("DedicatedWorldMixin", config.getAsJsonArray("server").get(0).getAsString()); assertEquals(1, config.getAsJsonArray("client").size()); @@ -81,25 +81,29 @@ static void requiredCall(MethodNode method, String owner, String name) { String target = "net/minecraft/server/Main"; String save = "net/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess"; var entry = method(classFile(target),"main","([Ljava/lang/String;)V"); - requiredCall(entry,save,"getDataTag"); - requiredCall(entry,save,"getDataTagFallback"); + String fileFixer = "net/minecraft/util/filefix/FileFixerUpper"; + requiredCall(entry,save,"getUnfixedDataTagWithFallback"); + requiredCall(entry,fileFixer,"fix"); var mixin = mixin("DedicatedWorldMixin",target); - for (String handler : java.util.List.of("readWorld","readFallback")) { - requiredCall(named(mixin,handler),"cyano/lootable/upgrade/EarlyWorldPreservation","readDedicatedWorld"); - } + requiredCall(named(mixin,"readWorld"),"cyano/lootable/upgrade/EarlyWorldPreservation","readDedicatedWorld"); + requiredCall(named(mixin,"fixWorldFiles"),"cyano/lootable/upgrade/EarlyWorldPreservation","handleDedicatedFileRefusal"); + var layout = mixin("FileFixerMixin",fileFixer); + assertTrue(layout.interfaces.contains("cyano/lootable/upgrade/EarlyWorldPreservation$LayoutAttached")); + requiredCall(named(layout,"beforeFileChanges"),"cyano/lootable/upgrade/EarlyWorldPreservation","beforeFileFix"); + requiredCall(named(layout,"afterFileChanges"),"cyano/lootable/upgrade/EarlyWorldPreservation","afterFileFix"); } @Test void regionHookProtectsTheActualVersionShortcut() throws Exception { String target = "net/minecraft/world/level/chunk/storage/SimpleRegionStorage"; - method(classFile(target),"upgradeChunkTag","(Lnet/minecraft/nbt/CompoundTag;ILnet/minecraft/nbt/CompoundTag;)Lnet/minecraft/nbt/CompoundTag;"); + method(classFile(target),"upgradeChunkTag","(Lnet/minecraft/nbt/CompoundTag;ILnet/minecraft/nbt/CompoundTag;I)Lnet/minecraft/nbt/CompoundTag;"); var mixin = mixin("RegionStorageMixin",target); assertTrue(mixin.interfaces.contains("cyano/lootable/upgrade/UpgradePreservation$RegionAttached")); requiredCall(named(mixin,"preserveRegion"),"cyano/lootable/upgrade/UpgradePreservation","upgradeRegion"); } @Test void capabilitiesHookUsesTheActualDimensionDataReader() throws Exception { - String target = "net/minecraft/world/level/storage/DimensionDataStorage"; - method(classFile(target),"readTagFromDisk","(Ljava/lang/String;Lnet/minecraft/util/datafix/DataFixTypes;I)Lnet/minecraft/nbt/CompoundTag;"); + String target = "net/minecraft/world/level/storage/SavedDataStorage"; + method(classFile(target),"readTagFromDisk","(Ljava/nio/file/Path;Lnet/minecraft/util/datafix/DataFixTypes;I)Lnet/minecraft/nbt/CompoundTag;"); requiredCall(named(mixin("CapabilityDataMixin",target),"preserveOldCapabilities"), "cyano/lootable/upgrade/UpgradePreservation","upgradeLevelCapabilities"); } diff --git a/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java index d0bd478..dfb9c02 100644 --- a/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java @@ -39,10 +39,10 @@ private static CompoundTag legacy(String id,int count,int slot) { return stack; } - @Test void targetUsesTheActual4671NativeSchema() { - assertEquals(4671,SharedConstants.getCurrentVersion().dataVersion().version()); - assertEquals(4671,UpgradePreservation.MINECRAFT_VERSION); - assertEquals(13,UpgradePreservation.DATA_VERSION); + @Test void targetUsesTheActual4790NativeSchema() { + assertEquals(4790,SharedConstants.getCurrentVersion().dataVersion().version()); + assertEquals(4790,UpgradePreservation.MINECRAFT_VERSION); + assertEquals(14,UpgradePreservation.DATA_VERSION); } @Test void released120ItemShapePreservesDamageEnchantmentsAndTypedCustomData() { diff --git a/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java b/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java index b851287..804b2df 100644 --- a/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java @@ -22,14 +22,14 @@ private static int targetDataVersion() { } @Test void targetMatchesTheActualNativeWorldVersion() { net.minecraft.SharedConstants.tryDetectVersion(); - assertEquals("1.21.11",net.minecraft.SharedConstants.getCurrentVersion().name()); - assertEquals(net.minecraft.server.packs.metadata.pack.PackFormat.of(75,0), + assertEquals("26.1.2",net.minecraft.SharedConstants.getCurrentVersion().name()); + assertEquals(net.minecraft.server.packs.metadata.pack.PackFormat.of(84,0), net.minecraft.SharedConstants.getCurrentVersion().packVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); - assertEquals(net.minecraft.server.packs.metadata.pack.PackFormat.of(94,1), + assertEquals(net.minecraft.server.packs.metadata.pack.PackFormat.of(101,1), net.minecraft.SharedConstants.getCurrentVersion().packVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); - assertEquals(4671,targetDataVersion()); + assertEquals(4790,targetDataVersion()); assertEquals(targetDataVersion(),UpgradePreservation.MINECRAFT_VERSION); - assertEquals(13,UpgradePreservation.DATA_VERSION); + assertEquals(14,UpgradePreservation.DATA_VERSION); } @Test void unstampedEmptyLegacyCapabilityDataGetsTheCurrentEnvelope() { CompoundTag legacy = new CompoundTag(); @@ -53,7 +53,7 @@ private static int targetDataVersion() { body.put("Equipment",items);CompoundTag before=body.copy(); CompoundTag source=new CompoundTag();source.putInt("DataVersion",2586); CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); - assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(48,typedList(staged, "LootableBodiesUnresolved", 10).size()); for(int slot=0;slot<48;slot++) { CompoundTag record=typedList(staged, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(slot); @@ -63,9 +63,9 @@ private static int targetDataVersion() { } assertEquals(before,body); } - @Test void productionClassesUseJava21() throws Exception { + @Test void productionClassesUseJava25() throws Exception { try(DataInputStream bytes=new DataInputStream(getClass().getResourceAsStream("/cyano/lootable/entities/EntityLootableBody.class"))) { - assertEquals(0xCAFEBABE,bytes.readInt());bytes.readUnsignedShort();assertEquals(65,bytes.readUnsignedShort()); + assertEquals(0xCAFEBABE,bytes.readInt());bytes.readUnsignedShort();assertEquals(69,bytes.readUnsignedShort()); } } @Test void marker6OverridesADamagedOuterEntityStamp() { @@ -84,7 +84,7 @@ private static int targetDataVersion() { CompoundTag body=Native117RegionFixtures.body(java.util.UUID.randomUUID(),7),before=body.copy(); CompoundTag source=new CompoundTag();source.putInt("DataVersion",2730); CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); - assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(2730,staged.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); ListTag records=typedList(staged, "LootableBodiesUnresolved", 10);assertEquals(50,records.size()); for(int slot=0;slot<48;slot++) { @@ -108,7 +108,7 @@ private static int targetDataVersion() { CompoundTag body=Native117RegionFixtures.body(java.util.UUID.randomUUID(),8),before=body.copy(); CompoundTag source=new CompoundTag();source.putInt("DataVersion",2975); CompoundTag staged=NativeSafeFixer.stageCorpse(body,source); - assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(2975,staged.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); ListTag records=typedList(staged, "LootableBodiesUnresolved", 10);assertEquals(50,records.size()); for(int slot=0;slot<48;slot++) { @@ -138,7 +138,7 @@ private static int targetDataVersion() { CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), net.minecraft.util.datafix.fixes.References.ENTITY_CHUNK,root,512,targetDataVersion()); CompoundTag staged=typedList(fixed, "Entities", 10).getCompoundOrEmpty(0); - assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(14,staged.getIntOr(UpgradePreservation.VERSION, 0)); assertEquals(3337,staged.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); CompoundTag detached=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); diff --git a/src/test/java/cyano/lootable/upgrade/Native2612ItemBridgeTest.java b/src/test/java/cyano/lootable/upgrade/Native2612ItemBridgeTest.java new file mode 100644 index 0000000..7e16187 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native2612ItemBridgeTest.java @@ -0,0 +1,37 @@ +package cyano.lootable.upgrade; + +import static org.junit.jupiter.api.Assertions.*; + +import cyano.lootable.buildtest.NativeVanillaDataFixer; +import net.minecraft.nbt.CompoundTag; +import org.junit.jupiter.api.Test; + +class Native2612ItemBridgeTest { + @Test void released12111StacksSurviveTheExactNativeItemBoundary() { + CompoundTag source = new CompoundTag(); + source.putInt("DataVersion",4671); + CompoundTag plain = new CompoundTag(); + plain.putString("id","minecraft:diamond"); + plain.putInt("count",3); + plain.putByte("Slot",(byte)47); + CompoundTag sword = new CompoundTag(); + sword.putString("id","minecraft:diamond_sword"); + sword.putInt("count",1); + sword.putByte("Slot",(byte)4); + CompoundTag components = new CompoundTag(); + components.putInt("minecraft:damage",7); + components.putString("minecraft:custom_name","BridgeBlade"); + CompoundTag enchantments = new CompoundTag(); + enchantments.putInt("minecraft:sharpness",2); + components.put("minecraft:enchantments",enchantments); + sword.put("components",components); + UpgradePreservation.register(NativeVanillaDataFixer.get()); + for (CompoundTag stack : java.util.List.of(plain,sword)) { + CompoundTag original = stack.copy(); + assertEquals(original,ItemMigration.convert(stack,source)); + assertEquals(original,stack); + } + sword.putString("UnrecognizedData","preserve for recovery"); + assertNull(ItemMigration.convert(sword,source)); + } +} diff --git a/tools/fixtures/native2612-flat-level.dat b/tools/fixtures/native2612-flat-level.dat new file mode 100644 index 0000000000000000000000000000000000000000..93383cab6f81d0c3b2429ec6d8cf1c2eba734d68 GIT binary patch literal 42656 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

UI}C-Npu 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 literal 0 HcmV?d00001 diff --git a/tools/fixtures/native2612-flat-worldgen.dat b/tools/fixtures/native2612-flat-worldgen.dat new file mode 100644 index 0000000000000000000000000000000000000000..92953b3fd8d9c2a8b4755fa4439332d74ae5d636 GIT binary patch literal 333 zcmV-T0kZxdiwFP!00000|AmrGPQx$|g79L zofgXYIc8ey$%Wp)q~o+Lw^&AQP0|Hvf>}+fsyT282Jkw9QaK(|7m<31RON*CMK2$> zoMSc2U39}aC4+VJms2ga-A{&;_d6OtyU~IN{aijF|NAI&O@*Owx^Zq<%x#>}0le(f zE4*!8Nf~Cl5GwI0d9X2>w`n!2!dT^LaCtMTJGwaQGx%Q~>ty6#=C2~zQ@@;lKXcmd fe`{xI!opzj8aDea>`&NF;O-M&fC6q*@B#n;eO{s+ literal 0 HcmV?d00001 diff --git a/tools/fixtures/native2612-forge-only-flat-level.dat b/tools/fixtures/native2612-forge-only-flat-level.dat new file mode 100644 index 0000000000000000000000000000000000000000..c51ac4d85f4b1c1ae8ed33c82a22ddf8d5898bb9 GIT binary patch literal 42558 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 literal 0 HcmV?d00001 diff --git a/tools/fixtures/native2612-forge-only-flat-worldgen.dat b/tools/fixtures/native2612-forge-only-flat-worldgen.dat new file mode 100644 index 0000000000000000000000000000000000000000..92953b3fd8d9c2a8b4755fa4439332d74ae5d636 GIT binary patch literal 333 zcmV-T0kZxdiwFP!00000|AmrGPQx$|g79L zofgXYIc8ey$%Wp)q~o+Lw^&AQP0|Hvf>}+fsyT282Jkw9QaK(|7m<31RON*CMK2$> zoMSc2U39}aC4+VJms2ga-A{&;_d6OtyU~IN{aijF|NAI&O@*Owx^Zq<%x#>}0le(f zE4*!8Nf~Cl5GwI0d9X2>w`n!2!dT^LaCtMTJGwaQGx%Q~>ty6#=C2~zQ@@;lKXcmd fe`{xI!opzj8aDea>`&NF;O-M&fC6q*@B#n;eO{s+ literal 0 HcmV?d00001 diff --git a/tools/hosted_policy.py b/tools/hosted_policy.py index 3d253e1..e2731a1 100644 --- a/tools/hosted_policy.py +++ b/tools/hosted_policy.py @@ -19,7 +19,7 @@ def gradle_roots(command): return roots def validate_workflow(text): - if "java-version: '21.0.7+6.0.LTS'" not in text: + if "java-version: '25.0.3+9.0.LTS'" not in text: raise ValueError('Use the setup-java SemVer identifier for the exact qualified JDK') commands='\n'.join(line.strip() for line in text.splitlines() if line.startswith(' ') and not line.strip().startswith('#')) for forbidden in ('unittest discover','-m unittest','native_runtime.py','native117_ci.py','verify_world_upgrades.py','verify_packaged','prepare_native','xvfb-run','--seed-only'): @@ -64,7 +64,7 @@ def require_qualified_manifest(record): if record.get('local_qualification_complete') is not True or record.get('qualification_integrity_status')!='verified': raise ValueError('Local qualification evidence is incomplete or on an integrity hold') version=record.get('version','') - if not re.fullmatch(r'\d+\.\d+\.\d+\.121111',version) or record.get('minecraft')!='1.21.11' or record.get('forge')!='61.2.4':raise ValueError('Wrong target qualification') + if not re.fullmatch(r'\d+\.\d+\.\d+\.2601021',version) or record.get('minecraft')!='26.1.2' or record.get('forge')!='64.1.5':raise ValueError('Wrong target qualification') base='LootableBodies-'+version names={'CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom'} if set(record.get('files',{}))!=names or any(not re.fullmatch('[A-F0-9]{64}',value or '') for value in record['files'].values()):raise ValueError('Incomplete six-file declaration') @@ -74,19 +74,19 @@ def require_qualified_manifest(record): def check_source(root): validate_workflow((root/'.github/workflows/native120.yml').read_text(encoding='utf-8')) for workflow in ('codeql-analysis.yml','deploy-release.yml'): - if "'21.0.7+6.0.LTS'" not in (root/'.github/workflows'/workflow).read_text(encoding='utf-8'): - raise ValueError('Missing qualified Java 21 version in '+workflow) + if "'25.0.3+9.0.LTS'" not in (root/'.github/workflows'/workflow).read_text(encoding='utf-8'): + raise ValueError('Missing qualified Java 25 version in '+workflow) if 'feature/**' in(root/'.github/workflows/build.yml').read_text():raise ValueError('Copied legacy CI still catches modern feature branches') if 'workflow_dispatch:' in(root/'.github/workflows/migration-matrix.yml').read_text():raise ValueError('Copied legacy migration workflow can still dispatch on a modern ref') guard=(root/'settings.gradle').read_text() policy=json.loads((root/'tools/qualification-policy.json').read_text()) - if policy['target']!='1.21.11' or any(rule not in guard for rule in ( + if policy['target']!='26.1.2' or policy['hosted'].get('local_only') is not False or any(rule not in guard for rule in ( "System.getenv('GITHUB_ACTIONS') == 'true'", "System.getenv('GITHUB_REPOSITORY') != 'MinecraftModDevelopmentMods/LootableBodies'", - 'Hosted builds are limited to the MMD repository' )): + 'Hosted builds are limited to the MMD repository')): raise ValueError('Missing MMD-only hosted build guard') for name in ('native120.yml', 'codeql-analysis.yml', 'release-workflow-checks.yml'): - if 'master-1.21.11' not in (root/'.github/workflows'/name).read_text(encoding='utf-8'): + if 'master-26.1.2' not in (root/'.github/workflows'/name).read_text(encoding='utf-8'): raise ValueError('Missing current release check trigger in ' + name) runtime=(root/'gradle/runtime.gradle').read_text() if "tasks.named('check') { dependsOn 'integrationTest' }" in runtime or "tasks.register('localRuntimeVerification')" not in runtime:raise ValueError('Game checks must remain explicit local gates') diff --git a/tools/native120_build.py b/tools/native120_build.py index 7887b69..e1b8d23 100644 --- a/tools/native120_build.py +++ b/tools/native120_build.py @@ -7,7 +7,7 @@ def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest().upper() def check_bundle(manifest,bundle): record=json.loads(Path(manifest).read_text());bundle=Path(bundle) - if (record.get('version'),record.get('minecraft'),record.get('forge'))!=(MOD_VERSION,'1.21.11','61.2.4'): + if (record.get('version'),record.get('minecraft'),record.get('forge'))!=(MOD_VERSION,'26.1.2','64.1.5'): raise ValueError('Wrong target release identity') if record.get('local_qualification_complete') is not True:raise ValueError('Local gameplay qualification is incomplete') if record.get('qualification_integrity_status')!='verified':raise ValueError('Local qualification evidence is on an integrity hold or unverified') diff --git a/tools/native120_client.py b/tools/native120_client.py index 4bc1f33..a45912e 100644 --- a/tools/native120_client.py +++ b/tools/native120_client.py @@ -1,4 +1,4 @@ -"""Pinned Forge 61 client preparation and descriptor-driven Java 21 launch.""" +"""Pinned Forge 64 client preparation and descriptor-driven Java 25 launch.""" import hashlib import json import os @@ -8,10 +8,10 @@ import struct import zipfile -BASE_VERSION = '1.21.11' -VERSION = '1.21.11-forge-61.2.4' -BASE_SHA1 = '35f8a9bc4874e2c95e51b3ad21e043a33776f7c2' -FORGE_SHA256 = '999897CC245CB4ADAD06B2178DBD7A298E24579D521F739BF17D884FB6660EEB' +BASE_VERSION = '26.1.2' +VERSION = '26.1.2-forge-64.1.5' +BASE_SHA1 = '4e49d699db7dba839226d0ed1fcf36c465c2edbe' +FORGE_SHA256 = '7ABAEB12D68D59B4A95965B38E1EFCD90B24C22A5825944FC1C646FF8FEE3E31' def native_for_platform(library,system,architecture): @@ -43,9 +43,9 @@ def descriptors(runtime): base = runtime/'versions'/BASE_VERSION/(BASE_VERSION+'.json') forge = runtime/'versions'/VERSION/(VERSION+'.json') if not base.is_file() or hashlib.sha1(base.read_bytes()).hexdigest() != BASE_SHA1: - raise RuntimeError('Incorrect official native 1.21.11 client descriptor') + raise RuntimeError('Incorrect official native 26.1.2 client descriptor') if not forge.is_file() or hashlib.sha256(forge.read_bytes()).hexdigest().upper() != FORGE_SHA256: - raise RuntimeError('Incorrect installed Forge 61 client descriptor') + raise RuntimeError('Incorrect installed Forge 64 client descriptor') return json.loads(base.read_text()), json.loads(forge.read_text()) @@ -175,7 +175,7 @@ def command(java, home, runtime, assets, prepared, *, username, extra=()): base, forge, record = load(runtime,prepared) index = assets/'indexes'/(base['assetIndex']['id']+'.json') if not index.is_file() or hashlib.sha1(index.read_bytes()).hexdigest() != base['assetIndex']['sha1']: - raise RuntimeError('Incorrect official native 1.21.11 asset index') + raise RuntimeError('Incorrect official native 26.1.2 asset index') values = {'version_name':VERSION,'library_directory':record['library_directory'], 'classpath_separator':os.pathsep,'classpath':os.pathsep.join(record['classpath']), 'natives_directory':record['natives'],'launcher_name':'LootableBodiesTests','launcher_version':'1', diff --git a/tools/native120_client_logs.py b/tools/native120_client_logs.py index 0ee865f..5814d44 100644 --- a/tools/native120_client_logs.py +++ b/tools/native120_client_logs.py @@ -1,4 +1,4 @@ -"""Explicit Forge 61 client diagnostics, separate from strict server/error audits.""" +"""Strict current client diagnostics and the retained Forge 61 audit contract.""" from pathlib import Path import gzip,hashlib,json,re,tempfile from native120_logs import audit as strict_audit @@ -26,7 +26,7 @@ def validated_baseline(policy_path=None,proof_path=None): if policy['events']!=['minecraft:item.goat_horn.play','minecraft:entity.goat.screaming.horn_break'] or any(not name.startswith('minecraft:') or name.removeprefix('minecraft:') in definitions for name in policy['events']):raise RuntimeError('Native diagnostic is no longer an unbound vanilla event') return policy -def audit_client(path,**options): +def audit_client(path,*,minecraft="26.1.2",**options): path=Path(path);raw=path.read_bytes() text=(gzip.decompress(raw) if path.name.endswith('.gz') else raw).decode('utf-8') lines=text.splitlines(keepends=True) @@ -37,6 +37,8 @@ def audit_client(path,**options): result=strict_audit(path,**options) result.update(classified_native_sound_warnings=0,raw_strict_audit_passed=True,residual_strict_audit_passed=True) return result + if minecraft!='1.21.11': + raise RuntimeError(str(path)+'\nUnqualified native client sound diagnostic for '+minecraft) policy=validated_baseline();events=policy['events'];observed=[] pattern=r'^\[\d{2}:\d{2}:\d{2}(?:\.\d{3})?\] \[Render thread/WARN\] \[net\.minecraft\.client\.sounds\.SoundEngine\]: Missing sound for event: (minecraft:[a-z0-9_./]+)$' for index in selected: diff --git a/tools/native120_logs.py b/tools/native120_logs.py index 191c609..616f0df 100644 --- a/tools/native120_logs.py +++ b/tools/native120_logs.py @@ -4,7 +4,7 @@ def audit(path,development=False,upgrade=False,fixture_logins=False,native=True): if fixture_logins: - raise RuntimeError('Legacy login-retry classification is not Forge 61 qualification') + raise RuntimeError('Legacy login-retry classification is not Forge 64 qualification') if path.name.endswith('.log.gz'): with gzip.open(path,'rt',encoding='utf-8') as stream:text=stream.read() else:text=path.read_text(encoding='utf-8') @@ -15,6 +15,6 @@ def audit(path,development=False,upgrade=False,fixture_logins=False,native=True) # Retain all generic exception/assertion/missing-resource checks, without # importing older platform/development/upgrade waivers into this target. result=_legacy_audit(path) - result['target_minecraft']='1.21.11' + result['target_minecraft']='26.1.2' result['legacy_native_controls_used']=False return result diff --git a/tools/native120_persistence.py b/tools/native120_persistence.py index 2b9144f..fb376b4 100644 --- a/tools/native120_persistence.py +++ b/tools/native120_persistence.py @@ -1,4 +1,4 @@ -"""Read-only native 1.21.11 saved entity inspection, distinct from old-writer proof.""" +"""Read-only native 26.1.2 saved entity inspection, distinct from old-writer proof.""" import re,struct from nbt_regions import Tag,read_region @@ -26,15 +26,15 @@ def selected(region):return wave<0 or region.name==['r.0.0.mca','r.8.8.mca','r.1 rx,rz=map(int,match.groups()) for index,chunk in read_region(region).items(): root=chunk['root'].value;position=root.get('Position');entities=root.get('Entities') - if root.get('DataVersion')!=Tag(3,4671):raise ValueError('Wrong native 1.21.11 entity-chunk data version') + if root.get('DataVersion')!=Tag(3,4790):raise ValueError('Wrong native 26.1.2 entity-chunk data version') if position is None or position.kind!=11 or len(position.value)!=8 or struct.unpack('>ii',position.value)!=(rx*32+(index&31),rz*32+(index>>5)): - raise ValueError('Wrong native 1.21.11 entity-chunk Position/index') - if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native 1.21.11 root Entities list') + raise ValueError('Wrong native 26.1.2 entity-chunk Position/index') + if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native 26.1.2 root Entities list') for entity in entities.value[1]: if entity.value.get('id',Tag(8,'')).value not in names:continue - if entity.value['id']!=Tag(8,'lootablebodies:corpse') or entity.value.get('LootableBodiesDataVersion')!=Tag(3,13):raise ValueError('Wrong native 1.21.11 corpse identity/marker') + if entity.value['id']!=Tag(8,'lootablebodies:corpse') or entity.value.get('LootableBodiesDataVersion')!=Tag(3,14):raise ValueError('Wrong native 26.1.2 corpse identity/marker') identity=entity.value.get('UUID') - if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native 1.21.11 corpse UUID type') + if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native 26.1.2 corpse UUID type') if identity.value in seen:raise ValueError('Duplicate persisted corpse UUID') seen.add(identity.value);count+=1 return count diff --git a/tools/native2612_flat_inputs.json b/tools/native2612_flat_inputs.json new file mode 100644 index 0000000..5484d34 --- /dev/null +++ b/tools/native2612_flat_inputs.json @@ -0,0 +1,12 @@ +{ + "149C39C40EEEB0241C0B15A9113BF72055FA7CB41FC55C2126BE79EF90330543": { + "header": "native2612-flat-level.dat", + "worldgen": "native2612-flat-worldgen.dat", + "worldgen_sha256": "E42863502AC6509AF62AB9440952883845EFAC4161D0FFE06ADF3E98B2ECADA7" + }, + "E64804965621AD9F107A12A55F29EA2CC97C2218BFC92E8DE73AFB7C80F23EA3": { + "header": "native2612-forge-only-flat-level.dat", + "worldgen": "native2612-forge-only-flat-worldgen.dat", + "worldgen_sha256": "E42863502AC6509AF62AB9440952883845EFAC4161D0FFE06ADF3E98B2ECADA7" + } +} diff --git a/tools/native2612_server_inputs.json b/tools/native2612_server_inputs.json new file mode 100644 index 0000000..059e5c4 --- /dev/null +++ b/tools/native2612_server_inputs.json @@ -0,0 +1,102 @@ +{ + "forge-26.1.2-64.1.5-shim.jar": "0C74CB2A8E7A71AEB7055396C1E25864584A2D837D7DD42136A06D0B41ADD545", + "libraries/at/yawk/lz4/lz4-java/1.10.1/lz4-java-1.10.1.jar": "A58A84C4271E50DF4C96ED916CCB7E48A869F8ED9CDCDA1AD5D3D4C33B0214A3", + "libraries/com/azure/azure-json/1.4.0/azure-json-1.4.0.jar": "C50BC998CD1A6C689F8644B51C206217BF2DA09D5B949E777490A60290CC3A0D", + "libraries/com/electronwill/night-config/core/3.7.4/core-3.7.4.jar": "CA45C88482C0F909810889DF14A05350BF8AC7B82C57EBBBAF32AACE8DDB6B52", + "libraries/com/electronwill/night-config/toml/3.7.4/toml-3.7.4.jar": "835783CB048326B02DD701E04A7EDAF141068DA1E4554A1F1D37466BB4D7810B", + "libraries/com/github/jponge/lzma-java/1.3/lzma-java-1.3.jar": "75CCC0E24D37ED5B8916E1BCFFAD1E067C2B1CFA21B635287F968DB5C3C1963A", + "libraries/com/github/oshi/oshi-core/6.9.0/oshi-core-6.9.0.jar": "B29BA9BCE041E6EA822203D5B5A692660C87551241510EBC76EEDFFE684844C7", + "libraries/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar": "4241C14A7727C34FEEA6507EC801318A3D4A90F070E4525681079FB94EE4C593", + "libraries/com/google/code/gson/gson/2.13.2/gson-2.13.2.jar": "DD0CE1B55A3ED2080CB70F9C655850CDA86C206862310009DCB5E5C95265A5E0", + "libraries/com/google/guava/failureaccess/1.0.3/failureaccess-1.0.3.jar": "CBFC3906B19B8F55DD7CFD6DFE0AA4532E834250D7F080BD8D211A3E246B59CB", + "libraries/com/google/guava/guava/33.5.0-jre/guava-33.5.0-jre.jar": "1E301F0C52AC248B0B14FDC3D12283C77252D4D6F48521D572E7D8C4C2CC4AC7", + "libraries/com/microsoft/azure/msal4j/1.23.1/msal4j-1.23.1.jar": "414D4FE3C66C7BB1A741014F352E5F0A8329172703E7FBB0020D232A8C8385B1", + "libraries/com/mojang/authlib/7.0.63/authlib-7.0.63.jar": "44DE93D9DF077FA1D4829ADA260EFE9D126476B9B7ABDFBA30CFBB5DB145CB2F", + "libraries/com/mojang/brigadier/1.3.10/brigadier-1.3.10.jar": "C8EE4136E474AC7723CA2B432EC8D1A2BC88EF7D1EC57C314BA9E33CDC83DD75", + "libraries/com/mojang/datafixerupper/9.0.19/datafixerupper-9.0.19.jar": "9653A39CF5C4853CC98628D902DCB23FFEB2755122ECC563BDE38C8F60C0B61E", + "libraries/com/mojang/jtracy/1.0.37/jtracy-1.0.37.jar": "8BB68D0B65E8ECEB92DE2BD1D9E1443A8C9A9DD1B5818A5E54870B0F8253672A", + "libraries/com/mojang/logging/1.6.11/logging-1.6.11.jar": "5D803EB7348A27468C6172DAEF2F0D77260DD63CDE377EBFA73E36789AE8FCEE", + "libraries/com/nothome/javaxdelta/2.0.1/javaxdelta-2.0.1.jar": "A661DFCB749053FC26EADA2A0A8E619A8EAD1CA9EDA29E1698687AA2CDD80152", + "libraries/commons-io/commons-io/2.20.0/commons-io-2.20.0.jar": "DF90BBA0FE3CB586B7F164E78FE8F8F4DA3F2DD5C27FA645F888100CCC25DD72", + "libraries/de/siegmar/fastcsv/2.2.2/fastcsv-2.2.2.jar": "BFDA71ABCD7C2B62DB498DEBA24C14AEC0E9059A9C183477840AC1BAE9C98AD2", + "libraries/io/github/llamalad7/mixinextras-forge/0.5.4/mixinextras-forge-0.5.4.jar": "7922899A121A27F63A69A9FFE57470D8719CC52D239DFA9408E15D32A7B4C264", + "libraries/io/netty/netty-buffer/4.2.7.Final/netty-buffer-4.2.7.Final.jar": "B81613C8ED22B1CC39ECCF2A28BD0C1D4561FB2E5E542061617EF4CE1D228BF5", + "libraries/io/netty/netty-codec-base/4.2.7.Final/netty-codec-base-4.2.7.Final.jar": "6360415B7C87160AFCDE5E7D4946C35539B9F043E8166C91A7FFF95CC6067D9F", + "libraries/io/netty/netty-codec-compression/4.2.7.Final/netty-codec-compression-4.2.7.Final.jar": "EDD53AD34991804301A46571113D8FFB4D6F67E6D874674DAFBB915724275CB3", + "libraries/io/netty/netty-codec-http/4.2.7.Final/netty-codec-http-4.2.7.Final.jar": "2984DD38420A61C4DD68FCACC51FFC528EB46F73F7795CDAFDB3A4D6FCB76862", + "libraries/io/netty/netty-common/4.2.7.Final/netty-common-4.2.7.Final.jar": "2345BC0ED5843FA57AA49EBA67529485C3A1D420FCF0429324C8220C7A80E9A6", + "libraries/io/netty/netty-handler/4.2.7.Final/netty-handler-4.2.7.Final.jar": "21D063409C12DBCEC46D380C8856A0F7B6A5B68BB5F1D005EB065B4D64146CB3", + "libraries/io/netty/netty-resolver/4.2.7.Final/netty-resolver-4.2.7.Final.jar": "7E4D569867E6C08437FB21A220EA01C294B0E38F01149B683C895A1BCD928438", + "libraries/io/netty/netty-transport/4.2.7.Final/netty-transport-4.2.7.Final.jar": "AADC6FB05C14FB789368CA3F854721549C72F6C0D81798BBCCF9DE1BB716892B", + "libraries/io/netty/netty-transport-classes-epoll/4.2.7.Final/netty-transport-classes-epoll-4.2.7.Final.jar": "4492948E6A7694398E5197CF15ED2E21D6662A739D010AE666FA276E7222853C", + "libraries/io/netty/netty-transport-classes-kqueue/4.2.7.Final/netty-transport-classes-kqueue-4.2.7.Final.jar": "A3EB1C94AE1A6BE9E1D1EC52C3F5C510A75B04B8EF1EA86B7E77B3F1287AE3CC", + "libraries/io/netty/netty-transport-native-epoll/4.2.7.Final/netty-transport-native-epoll-4.2.7.Final-linux-aarch_64.jar": "2F0C7309D7DF48A88FB374C02CFB446E1A1E5FB597FB02D773C70AC07DA2C268", + "libraries/io/netty/netty-transport-native-epoll/4.2.7.Final/netty-transport-native-epoll-4.2.7.Final-linux-x86_64.jar": "7F395C8FDBE6990F7664408B57D919042401B893178FCBA48BEBF1A61BF78618", + "libraries/io/netty/netty-transport-native-kqueue/4.2.7.Final/netty-transport-native-kqueue-4.2.7.Final-osx-aarch_64.jar": "7647A69558E3D49A352781C2AE81A97E16B442D7C6565A06B4AE0F15298489D5", + "libraries/io/netty/netty-transport-native-kqueue/4.2.7.Final/netty-transport-native-kqueue-4.2.7.Final-osx-x86_64.jar": "A29C08B6895746AACA796E72A429BC705F40267FEB35F75CD8FBBCD4DE6DE99F", + "libraries/io/netty/netty-transport-native-unix-common/4.2.7.Final/netty-transport-native-unix-common-4.2.7.Final.jar": "736703B3F6D127E182F5BE888B6E15732B0C08D6634E00725352A6A9D19974B2", + "libraries/it/unimi/dsi/fastutil/8.5.18/fastutil-8.5.18.jar": "9094AE67D01D0AD246F886F11AD557FC2E79C72CBF3FEED83E1512A8AE90A74A", + "libraries/net/java/dev/jna/jna/5.17.0/jna-5.17.0.jar": "B3A9408E7C51E08EF0E3BFCC08F443F6EC0F6191BA8CD7C18D53D2B22E5BDBC0", + "libraries/net/java/dev/jna/jna-platform/5.17.0/jna-platform-5.17.0.jar": "B7E3D46C87BAD2EB409B0E704916BCD81206168E357312DFDDD0E253679CD9E0", + "libraries/net/minecraft/server/26.1.2/server-26.1.2-bundled.jar": "CD47E7C38328F64768FD17AF8FCD8B22496B40B63D4FFEE81E71AE059FEDCB42", + "libraries/net/minecraft/server/26.1.2/server-26.1.2-unpacked.jar": "4723380BD2A0A0206719B50F2E390383AFDAF82B0A76A0D573BAF788E6AA3E86", + "libraries/net/minecraftforge/accesstransformers/8.2.17/accesstransformers-8.2.17.jar": "D89305F8DE0C1BA48A0E898B7DA95E29262CB55D14970FCBBCADE0AC93631457", + "libraries/net/minecraftforge/binarypatcher/1.3.4/binarypatcher-1.3.4.jar": "F8CB33B4D4F038E19C70F5B7471127A407BE5DF2E899493828BFC17EE9859F23", + "libraries/net/minecraftforge/bootstrap/2.1.8/bootstrap-2.1.8.jar": "6B0641339BC8872C4EB6978E302935D74FCC93871E3EA2B1B81F03FB3B16E708", + "libraries/net/minecraftforge/bootstrap-api/2.1.8/bootstrap-api-2.1.8.jar": "B9B5B5B3B441A42920EEAF181395E858642874F09F64CD7F9E4A0CD7A34B8938", + "libraries/net/minecraftforge/coremods-api/5.3.1/coremods-api-5.3.1.jar": "5C2F1F88EBD1A74C56C4A8FCD6D9078FF9889B64ED5CA5869AF268B1E2E2F41E", + "libraries/net/minecraftforge/eventbus/7.0.5/eventbus-7.0.5.jar": "529C651F7C5D24F98D82322F169AE17084676EE814DF4D9F7F72A53C2F598709", + "libraries/net/minecraftforge/fmlcore/26.1.2-64.1.5/fmlcore-26.1.2-64.1.5.jar": "DF045E6E4661EB163407CECBD3C4D232D0AC5891E38DCB7E2F658DF205C8C9ED", + "libraries/net/minecraftforge/fmlearlydisplay/26.1.2-64.1.5/fmlearlydisplay-26.1.2-64.1.5.jar": "724944F82C86CA98E67110B5EE1C6BECDAC04D61F92D47D605558DD7D6A84DC5", + "libraries/net/minecraftforge/fmlloader/26.1.2-64.1.5/fmlloader-26.1.2-64.1.5.jar": "7299C7AF7AE427C44C9AE83A92508F7BB09764DC30D21A4E8E86C7FFF2B0C4BC", + "libraries/net/minecraftforge/forge/26.1.2-64.1.5/forge-26.1.2-64.1.5-server.jar": "C6B85C178B037519464D436A75B7C68408CBE591576270BA0E826542B50BF41B", + "libraries/net/minecraftforge/forge/26.1.2-64.1.5/forge-26.1.2-64.1.5-shim.jar": "0C74CB2A8E7A71AEB7055396C1E25864584A2D837D7DD42136A06D0B41ADD545", + "libraries/net/minecraftforge/forge/26.1.2-64.1.5/forge-26.1.2-64.1.5-universal.jar": "687C2698ECC1906CA2AD398A445C2498CF8BC09F1CBE6C2A6E24BFC9F90736C7", + "libraries/net/minecraftforge/forge/26.1.2-64.1.5/unix_args.txt": "292659817651988B6403FF829550635A263F059359F46B51C949E49C2BEB3549", + "libraries/net/minecraftforge/forge/26.1.2-64.1.5/win_args.txt": "292659817651988B6403FF829550635A263F059359F46B51C949E49C2BEB3549", + "libraries/net/minecraftforge/forge-transformers/26.1.2-64.1.5/forge-transformers-26.1.2-64.1.5.jar": "FED78F9B5E032041D4B5AC1B36F195B32FE424CBE15B41B104F387CE9BD9004D", + "libraries/net/minecraftforge/forgespi/8.0.0/forgespi-8.0.0.jar": "BD275F984EDEFA63769A752046587184AD8858767EB96E46FF58B3E03753C5E8", + "libraries/net/minecraftforge/hash-utils/0.2.3/hash-utils-0.2.3.jar": "CECF5E68A4DDCD94CB08DDE28FC7D19B3CB873343F8BFDF671F805D15CBBA45B", + "libraries/net/minecraftforge/installertools/1.4.5/installertools-1.4.5.jar": "76EC8D1ADBE13B675CB84A435C79DBE6CB1D2B49236182C3476273416A627F80", + "libraries/net/minecraftforge/JarJarFileSystems/0.4.2/JarJarFileSystems-0.4.2.jar": "D5D1B5464FC5E44E874EBEFEA2EB92CB1AE01C0B8EDDB8235E5B6206B69EDC70", + "libraries/net/minecraftforge/JarJarMetadata/0.4.2/JarJarMetadata-0.4.2.jar": "D6FFAD4EAC80785BD7AFF9D21CE26EBBEEF419E8F7A30182F072F9F5270F87F3", + "libraries/net/minecraftforge/JarJarSelector/0.4.2/JarJarSelector-0.4.2.jar": "1C667D815B35B39FB5878AD89D58B3BA15FC690EA7EAB3A7F8E9A3B057580F35", + "libraries/net/minecraftforge/javafmllanguage/26.1.2-64.1.5/javafmllanguage-26.1.2-64.1.5.jar": "35E8721221C0114B20836DA5CA3E4EE963AC7B1EA7EF12422E176C22E9828AB1", + "libraries/net/minecraftforge/lowcodelanguage/26.1.2-64.1.5/lowcodelanguage-26.1.2-64.1.5.jar": "08410223EF1B9FEBC2E9716D42B1EE02CBF3D6846A7FA7C0FD0B80034F11E412", + "libraries/net/minecraftforge/mclanguage/26.1.2-64.1.5/mclanguage-26.1.2-64.1.5.jar": "7C6D126232D53C20D0395D9E92FB2CF200CA3AA433C1064BAE309739B5AFADDA", + "libraries/net/minecraftforge/mergetool-api/1.0/mergetool-api-1.0.jar": "100F0C4D658B9DD73600CE4BB9C75657E0651F0F0F5E0B2F977F85D8F495350B", + "libraries/net/minecraftforge/modlauncher/10.2.6/modlauncher-10.2.6.jar": "7370C0A0D59B5BB8C52A04269616ABF0E3A4909E21C2B4DA8670CA252C2A0834", + "libraries/net/minecraftforge/roimfs/1.0.0/roimfs-1.0.0.jar": "ED862AA6927A05E2E2A2F7486858624C390527FB77A8A6D44D9B31EE8691F423", + "libraries/net/minecraftforge/securemodules/2.2.24/securemodules-2.2.24.jar": "40CB1FA55F48E3812EB3F823019CF906D9995BC4C9015F3F8F28471315C2A656", + "libraries/net/minecraftforge/srgutils/0.6.2/srgutils-0.6.2.jar": "F10B366CDC4DF20DE8A50511F49FDED5EA0FE8F7A476FF984A12F65A1602533E", + "libraries/net/minecraftforge/srgutils/0.6.6/srgutils-0.6.6.jar": "66DB62E296555A1261A5C8F55608CFD141480792064ECC869328446B3F68EEF7", + "libraries/net/minecraftforge/unsafe/0.9.2/unsafe-0.9.2.jar": "F5035AB2DD1C02AA354F948A28DAF2FF4215956194A98634F076AD71610FE3A4", + "libraries/net/minecrell/terminalconsoleappender/1.2.0/terminalconsoleappender-1.2.0.jar": "912E432C9355C4B2025D788ED87FD72BB95542E2FC967AD6875FBBEB6E1C7A1C", + "libraries/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar": "DF26CC58F235F477DB07F753BA5A3AB243EBE5789D9F89ECF68DD62EA9A66C28", + "libraries/net/sf/jopt-simple/jopt-simple/6.0-alpha-3/jopt-simple-6.0-alpha-3.jar": "D440CEB839D6ADB5CFA81AA321813D60255E68A3E9CA564C91126599310ABDE5", + "libraries/org/apache/commons/commons-lang3/3.19.0/commons-lang3-3.19.0.jar": "32733AB4BC90B45B63EB72677D886961003FD4ED113E07B1028F9877CB2AC735", + "libraries/org/apache/logging/log4j/log4j-api/2.25.2/log4j-api-2.25.2.jar": "9FD66C9FE0BEA06FA9666C147989A46CAFAA92B4A88753697D3945CC43338CBB", + "libraries/org/apache/logging/log4j/log4j-core/2.25.2/log4j-core-2.25.2.jar": "E50DB7701430FF907981850EF527A41D51A53DD0017F53A0860AFCBAB8570277", + "libraries/org/apache/logging/log4j/log4j-slf4j2-impl/2.25.2/log4j-slf4j2-impl-2.25.2.jar": "FAFFE8C867D59F813FB4BD1EDF5354BFE86125811A7C9B305E5D225DA8E7EA54", + "libraries/org/apache/maven/maven-artifact/3.8.8/maven-artifact-3.8.8.jar": "813233A8485CBAF97B1F9A2C17CEF723B068F7260A4319CF4958F221D04B9937", + "libraries/org/jline/jline-reader/3.25.1/jline-reader-3.25.1.jar": "26333A275DE502ADF1DD9E6EA50AA0B4021412C71490DF9ED5E88A648886EE89", + "libraries/org/jline/jline-terminal/3.25.1/jline-terminal-3.25.1.jar": "C0F5D70901255DA66A94E59778B265D19F9308342578E34C88FC92D1B0C65FEF", + "libraries/org/jline/jline-terminal-jna/3.25.1/jline-terminal-jna-3.25.1.jar": "58CA9D719C373206AF15775EE3CD5F268136EA0D0C4E009C3E96A6D4612D5C66", + "libraries/org/joml/joml/1.10.8/joml-1.10.8.jar": "BF19510145178DF82CD3BD37EDD514C13F411531EC5545299FD3ABCBC98FE7C2", + "libraries/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar": "1FAD6E6BE7557781E4D33729D49AE1CDC8FDDA6FE477BB0CC68CE351EAFDFBAB", + "libraries/org/ow2/asm/asm/9.10.1/asm-9.10.1.jar": "ED825D10AB1399C8C0CB669E688CF0C8C82629B4C8399B58352B68E92CA10FCB", + "libraries/org/ow2/asm/asm/9.9.1/asm-9.9.1.jar": "6F3828A215C920059A5EFA2FB55C233D6C54EC5CADCA99CE1B1BDD10077C7DDD", + "libraries/org/ow2/asm/asm-analysis/9.10.1/asm-analysis-9.10.1.jar": "DEDE75A21306B65974ECD8F87114FF6970F09FB794157A4CA09AB25C888C2BFC", + "libraries/org/ow2/asm/asm-commons/9.10.1/asm-commons-9.10.1.jar": "6D0ABEFB7CBF972EA16EDB37EC14835372505063A45F976AB7EA889ED9497895", + "libraries/org/ow2/asm/asm-commons/9.9.1/asm-commons-9.9.1.jar": "C2319E014CE7199F2B7F7D56D6BB991863168C3F4B6CD6C9F542A4937EF7EF88", + "libraries/org/ow2/asm/asm-tree/9.10.1/asm-tree-9.10.1.jar": "3DFB0D5B6A106CD40B5B250E39935FBF2F927F4477546A5369A3AC609CF0506B", + "libraries/org/ow2/asm/asm-tree/9.9.1/asm-tree-9.9.1.jar": "0F3555096B720B820BBACAB0B515589BEE0200BEE099BDA14C561738AE837BA1", + "libraries/org/ow2/asm/asm-util/9.10.1/asm-util-9.10.1.jar": "1BB99D091FBA2597DC6D51193E9BBCF0D8447E7ED96BD8F0198B18152F09655C", + "libraries/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar": "7B751D952061954D5ABFED7181C1F645D336091B679891591D63329C622EB832", + "libraries/org/spongepowered/mixin/0.8.7/mixin-0.8.7.jar": "87AAA7EE080465ED7EA13616679534ABCDBFDD636B4752D0EFF0A00017E235C0", + "libraries/trove/trove/1.0.2/trove-1.0.2.jar": "AD68A86CFD0255A2BC0525CE11EC49640CAEA7E34FF91EB89C3193DCCCD77F05", + "README.txt": "913D73D120725A6D017A0979F8711F210EC34C8825B6F3A0F9DCE60EC248362D", + "run.bat": "96EF9DA588B755F83802628D850F093CB06644D39366328719D31C63F8C4CEA2", + "run.sh": "4D1A96ED91CC9E387D5D301F8D8E26CF8431B4F06DCB7F284F1F5A1D7D1975E1", + "user_jvm_args.txt": "4FE1B4FA834BAE209AD75D37067244491A76519B19035B3FC915A47C3999A94C" +} \ No newline at end of file diff --git a/tools/native2612_worker_budget.json b/tools/native2612_worker_budget.json new file mode 100644 index 0000000..1793ed0 --- /dev/null +++ b/tools/native2612_worker_budget.json @@ -0,0 +1,37 @@ +{ + "minecraft": "26.1.2", + "forge": "64.1.5", + "reloads": 200, + "jvms": 4, + "qualified": true, + "jvm_args": [], + "controller_sha256": "3DAACC0F27F462CEBFCA4EFB357FE2C8586CE1C64D7E8C49579E58D164B9DC58", + "server_input_manifest_sha256": "9BA36A781FF5F4E1A01992A09072097B5A64F868A16FE1FB03CAADB5C159BCF7", + "shards": [ + { + "index": 1, + "rounds": 50, + "log": "86615889EBFF229C540C22F1B47BB7A7F7C54CF817E57F761677445343EB8610", + "mod": "C9760CCB4EA7AFB040DD3790BC022DAF21129AC58DA28360403FC56C9FC35232" + }, + { + "index": 2, + "rounds": 50, + "log": "70EF7185BBE2FFE93799A6C5C369F620C8E53831A5C9648C880E2944A323575A", + "mod": "C9760CCB4EA7AFB040DD3790BC022DAF21129AC58DA28360403FC56C9FC35232" + }, + { + "index": 3, + "rounds": 50, + "log": "C27F516C5A043C0E080A26545EB015BED6786FE634EAF6DF6BB7A04417DA8FE1", + "mod": "C9760CCB4EA7AFB040DD3790BC022DAF21129AC58DA28360403FC56C9FC35232" + }, + { + "index": 4, + "rounds": 50, + "log": "485209F591AE160AEBA21531CF4C40DBA358F089F27C975F1A096DC99AFD8962", + "mod": "C9760CCB4EA7AFB040DD3790BC022DAF21129AC58DA28360403FC56C9FC35232" + } + ], + "qualification_evidence_sha256": "07D491A6787C7EA894C5E6301CBB4B7B1E41B243B46DF94130969C4C4E5BA81E" +} diff --git a/tools/native_evidence.py b/tools/native_evidence.py index f2963d9..21651dd 100644 --- a/tools/native_evidence.py +++ b/tools/native_evidence.py @@ -1,7 +1,7 @@ -"""Independently audit Forge 61 local receipts, complete logs and diagnostic bytes. +"""Independently audit Forge 64 local receipts, complete logs and diagnostic bytes. Default auditing binds the recorded cwd to the actual owned process home. -Relocated auditing is a separately tested download contract; hosted Forge 61 +Relocated auditing is a separately tested download contract; hosted Forge 64 execution and promotion remain disabled and have not been exercised. """ from pathlib import Path,PurePosixPath @@ -18,10 +18,10 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): if receipt['timeout']!=(180 if refusal else 240):raise ValueError('Changed native deadline') if receipt['returncode']!=(1 if refusal else 0):raise ValueError('Unexpected native JVM exit') command=receipt['command'] - # Forge 61 was qualified with its default worker and GC settings. + # Forge 64 was qualified with its default worker and GC settings. if any(s.startswith(('-XX:ActiveProcessorCount','-XX:ParallelGCThreads','-XX:ConcGCThreads','-Dmax.bg.threads')) or (s.startswith(('-XX:+Use','-XX:-Use')) and s.endswith('GC')) for s in command): - raise ValueError('Unqualified Forge 61 worker or GC override') + raise ValueError('Unqualified Forge 64 worker or GC override') if relocated: # Downloaded artifacts retain their original machine cwd. Require the # complete case/condition suffix; all bytes are read only under home. @@ -34,8 +34,8 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): version=FORGE.removeprefix('forge-') expected={prefix+version+'/'+name for name in ('win_args.txt','unix_args.txt')} if relocated else { prefix+version+'/'+('win_args.txt' if __import__('os').name=='nt' else 'unix_args.txt')} - if len(arguments)!=1 or arguments[0] not in expected or '--nogui' not in command:raise ValueError('Receipt did not launch Forge 61 modules') - if not any(item.startswith('-Xlog:gc*,safepoint:file=') for item in command):raise ValueError('Missing Java 21 unified GC evidence') + if len(arguments)!=1 or arguments[0] not in expected or '--nogui' not in command:raise ValueError('Receipt did not launch Forge 64 modules') + if not any(item.startswith('-Xlog:gc*,safepoint:file=') for item in command):raise ValueError('Missing Java 25 unified GC evidence') if not refusal and '-Dlootablebodies.phase='+phase not in command:raise ValueError('Receipt belongs to another phase') for name in ('gc.txt','initial-threads.txt','initial-heap-gc.txt'): if name not in receipt['evidence'] or not (directory/name).is_file() or not (directory/name).stat().st_size: diff --git a/tools/native_runtime.py b/tools/native_runtime.py index 4137967..5b2c00c 100644 --- a/tools/native_runtime.py +++ b/tools/native_runtime.py @@ -1,4 +1,4 @@ -"""Disposable official Forge 61 launches, separate from the development bootstrap.""" +"""Disposable official Forge 64 launches, separate from the development bootstrap.""" import hashlib import json import os @@ -13,33 +13,34 @@ from command_logs import run_logged from runtime_diagnostics import run_monitored,validate_logging_inputs -FORGE = 'forge-1.21.11-61.2.4' -INSTALLER_SHA256 = 'B1C3EE57BADED63F7591293134FF079B69CACEF3317D2E6783B28D92CFDC2BF4' -SERVER_INPUTS = json.loads(Path(__file__).with_name('native12111_server_inputs.json').read_text(encoding='utf-8')) -WORKER_BUDGET_PATH = Path(__file__).with_name('native12111_worker_budget.json') +FORGE = 'forge-26.1.2-64.1.5' +INSTALLER_SHA256 = '6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648' +FLAT_INPUTS_PATH = Path(__file__).with_name('native2612_flat_inputs.json') +SERVER_INPUTS = json.loads(Path(__file__).with_name('native2612_server_inputs.json').read_text(encoding='utf-8')) +WORKER_BUDGET_PATH = Path(__file__).with_name('native2612_worker_budget.json') WORKER_BUDGET_PROOF = json.loads(WORKER_BUDGET_PATH.read_text(encoding='utf-8')) if WORKER_BUDGET_PATH.is_file() else {} -# Forge 61 uses its actual default CPU/worker/GC configuration. A new target's +# Forge 64 uses its actual default CPU/worker/GC configuration. A new target's # history cannot borrow an older engine's resource qualification. HISTORICAL_JVM_ARGS = () def require_historical_budget(): if not WORKER_BUDGET_PATH.is_file(): - raise RuntimeError('Forge 61 historical runtime policy has not been qualified') + raise RuntimeError('Forge 64 historical runtime policy has not been qualified') proof=json.loads(WORKER_BUDGET_PATH.read_text(encoding='utf-8')) - if tuple(proof.get(key) for key in ('minecraft','forge','reloads','jvms','qualified'))!=('1.21.11','61.2.4',200,4,True): - raise RuntimeError('Forge 61 historical runtime policy has not been qualified') + if tuple(proof.get(key) for key in ('minecraft','forge','reloads','jvms','qualified'))!=('26.1.2','64.1.5',200,4,True): + raise RuntimeError('Forge 64 historical runtime policy has not been qualified') if proof.get('jvm_args')!=[]: - raise RuntimeError('Unqualified Forge 61 worker or GC override') + raise RuntimeError('Unqualified Forge 64 worker or GC override') source=Path(__file__).resolve().parents[1]/'src/controlTest/java/buildcontrol/ResourceControl.java' - manifest=Path(__file__).with_name('native12111_server_inputs.json') + manifest=Path(__file__).with_name('native2612_server_inputs.json') if proof.get('controller_sha256')!=digest(source) or proof.get('server_input_manifest_sha256')!=digest(manifest): - raise RuntimeError('Forge 61 resource qualification source or SDK binding changed') + raise RuntimeError('Forge 64 resource qualification source or SDK binding changed') if not __import__('re').fullmatch('[A-F0-9]{64}',proof.get('qualification_evidence_sha256','')): - raise RuntimeError('Missing Forge 61 resource qualification identity') - evidence=Path(__file__).resolve().parents[1]/'.gradle/native12111-resource-qualification/proof.json' + raise RuntimeError('Missing Forge 64 resource qualification identity') + evidence=Path(__file__).resolve().parents[1]/'.gradle/native2612-resource-qualification/proof.json' if not evidence.is_file() or digest(evidence)!=proof['qualification_evidence_sha256']: - raise RuntimeError('Missing or changed actual local Forge 61 resource qualification evidence') + raise RuntimeError('Missing or changed actual local Forge 64 resource qualification evidence') return HISTORICAL_JVM_ARGS @@ -52,6 +53,17 @@ def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest().upper() +def sealed_runtime_jars(directory, logging): + from release_identity import MAIN_JAR + root=Path(directory).resolve() + identity=json.loads((root/'identity.json').read_text(encoding='utf-8')) + candidate,probe=(root/MAIN_JAR,root/'lootablebodies-build-tests.jar') + for path in (candidate,probe,logging): + if path.is_symlink() or not path.is_file() or digest(path)!=identity.get(path.name): + raise ValueError('Changed sealed runtime input: '+str(path)) + return candidate,probe + + def fingerprint(directory): return {p.relative_to(directory).as_posix(): digest(p) for p in sorted(directory.rglob('*')) if p.is_file()} @@ -60,20 +72,20 @@ def fingerprint(directory): def verify_server_inputs(distribution): """Pin the exact completed SDK after both official installer sides. - Forge 61 generated archives match the installer's declared expected hashes. + Forge 64 generated archives match the installer's declared expected hashes. No changed/truncated class, intermediate, resource or extra byte is accepted. """ actual={} for name,expected in SERVER_INPUTS.items(): artifact=Path(distribution)/name - if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing Forge 61 SDK input: '+name) + if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing Forge 64 SDK input: '+name) value=digest(artifact) - if value!=expected:raise RuntimeError('Incorrect official Forge 61 server input: '+name) + if value!=expected:raise RuntimeError('Incorrect official Forge 64 server input: '+name) actual[name]=value return actual -def seed_loader_defaults(home, deterministic=False, include_default_path=True): +def seed_loader_defaults(home, deterministic=False, include_default_path=True, *, current=False): """Seed an owned home; disable optional HTTP checks only for probes.""" config=home/'config/fml.toml' config.parent.mkdir(parents=True, exist_ok=True) @@ -81,6 +93,9 @@ def seed_loader_defaults(home, deterministic=False, include_default_path=True): with config.open('x', encoding='utf-8', newline='\n') as stream: stream.write('versionCheck = '+('false' if deterministic else 'true')+'\nsplashscreen = true\n' 'maxThreads = -1\n'+('defaultConfigPath = "defaultconfigs"\n' if include_default_path else '')) + if current: + stream.write('earlyWindowSkipGLVersions = []\nearlyWindowLogHelpMessage = true\n' + 'earlyWindowSquir = false\nearlyWindowShowCPU = false\n') except FileExistsError: pass # Retain existing choices, comments and even unreadable bytes. @@ -90,12 +105,12 @@ def prepare_server(home, distribution, candidate=None, probe=None): distribution = distribution.resolve() libraries = distribution / 'libraries' if not libraries.is_dir(): - raise FileNotFoundError('Incomplete official Forge 61 server installation') + raise FileNotFoundError('Incomplete official Forge 64 server installation') verify_server_inputs(distribution) home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() shutil.copy2(distribution/(FORGE+'-shim.jar'),home/(FORGE+'-shim.jar')) - seed_loader_defaults(home, deterministic=probe is not None) + seed_loader_defaults(home, deterministic=probe is not None,current=True) # Share this disposable installation's immutable libraries, not a user # installation or Gradle cache. Every matrix run fingerprints the inputs. link = home / 'libraries' @@ -134,10 +149,10 @@ def require_runtime_jdk(java): java=Path(java).resolve() release=java.parent.parent/'release' if not release.is_file(): - raise RuntimeError('Native runtime requires exact Temurin 21.0.7+6 JDK metadata') + raise RuntimeError('Native runtime requires exact Temurin 25.0.3+9 JDK metadata') version=dict(line.split('=',1) for line in release.read_text(encoding='utf-8').splitlines() if '=' in line) - if version.get('JAVA_RUNTIME_VERSION')!='"21.0.7+6-LTS"' or version.get('IMPLEMENTOR')!='"Eclipse Adoptium"': - raise RuntimeError('Native runtime requires exact Temurin 21.0.7+6') + if version.get('JAVA_RUNTIME_VERSION')!='"25.0.3+9-LTS"' or version.get('IMPLEMENTOR')!='"Eclipse Adoptium"': + raise RuntimeError('Native runtime requires exact Temurin 25.0.3+9') return java @@ -160,7 +175,7 @@ def run_java(java, home, phase, *, timeout=240, jvm_args=()): gc = diagnostics.relative_to(home) / 'gc.txt' arguments = 'win_args.txt' if os.name == 'nt' else 'unix_args.txt' command = [str(java), '-Dfile.encoding=UTF-8', '-Xms256m', '-Xmx2G', '-Xlog:gc*,safepoint:file=' + gc.as_posix() + ':time,uptime,level,tags', - *jvm_args, '@libraries/net/minecraftforge/forge/1.21.11-61.2.4/' + arguments, '--nogui'] + *jvm_args, '@libraries/net/minecraftforge/forge/26.1.2-64.1.5/' + arguments, '--nogui'] validate_logging_inputs(command) def initial_diagnostics(process): # Short refusals and successful phases must retain real thread/heap @@ -193,7 +208,7 @@ def initial_diagnostics(process): def seed_flat_fixture(home, header, expected_sha256): - """Copy one sealed native-codec header into a new owned fixture world.""" + """Copy the two sealed files needed by the native 26.1.2 flat-world codec.""" import gzip from nbt_regions import decode home, header = Path(home).resolve(), Path(header).resolve() @@ -204,21 +219,32 @@ def seed_flat_fixture(home, header, expected_sha256): raise FileExistsError('Refusing to overwrite an existing fixture world') if not header.is_file() or digest(header) != expected_sha256: raise RuntimeError('Native flat fixture header checksum mismatch') - _, root = decode(gzip.decompress(header.read_bytes())) - data = root.value.get('Data') + entries = json.loads(FLAT_INPUTS_PATH.read_text(encoding='utf-8')) + entry = entries.get(expected_sha256) + if entry is None or header.name != entry['header']: + raise RuntimeError('Flat fixture header is not an approved native 26.1.2 input') + settings = header.with_name(entry['worldgen']) + if not settings.is_file() or digest(settings) != entry['worldgen_sha256']: + raise RuntimeError('Native flat fixture world generator checksum mismatch') try: - version = data.value['DataVersion'] - generator = data.value['WorldGenSettings'].value['dimensions'].value[ + _, root = decode(gzip.decompress(header.read_bytes())) + _, config = decode(gzip.decompress(settings.read_bytes())) + version = root.value['Data'].value['DataVersion'] + config_version = config.value['DataVersion'] + generator = config.value['data'].value['dimensions'].value[ 'minecraft:overworld'].value['generator'].value['type'] - if version.kind != 3 or version.value != 4671 or generator.kind != 8 or generator.value != 'minecraft:flat': + if ((version.kind,version.value)!=(3,4790) or (config_version.kind,config_version.value)!=(3,4790) + or (generator.kind,generator.value)!=(8,'minecraft:flat')): raise ValueError('Wrong native version or generator') except (AttributeError, KeyError, ValueError) as error: - raise RuntimeError('Expected an explicit native4671 flat codec header') from error - world.mkdir() + raise RuntimeError('Expected native 4790 flat-world codec inputs') from error + stored_settings = world/'data/minecraft/world_gen_settings.dat' + stored_settings.parent.mkdir(parents=True) shutil.copy2(header, world/'level.dat') + shutil.copy2(settings, stored_settings) (home/'native-flat-header-input.json').write_text(json.dumps({ - 'sha256':expected_sha256, 'native_data_version':4671, 'generator':'minecraft:flat', - 'only_header_copied':True}, indent=2)+'\n', encoding='utf-8') + 'sha256':expected_sha256,'worldgen_sha256':entry['worldgen_sha256'], + 'native_data_version':4790,'generator':'minecraft:flat'},indent=2)+'\n',encoding='utf-8') def run_server(java, home, phase, *, timeout=240, extra=(), marker=None, message=None): @@ -253,7 +279,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.14.121111'}) + request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesRuntimeTests/2.4.15.2601021'}) with urllib.request.urlopen(request, timeout=60) as response: content = response.read() if hashlib.sha1(content).hexdigest() != expected: @@ -285,7 +311,7 @@ def validate_client_paths(paths): import re for path in paths: value=str(path).replace('\\','/').lower();name=path.name.lower() - # Forge 61's exact descriptor requires this dist-marker API archive. + # Forge 64's exact descriptor requires this dist-marker API archive. if (value.endswith('/net/minecraftforge/mergetool-api/1.0/mergetool-api-1.0.jar') and not path.is_symlink() and path.is_file() and digest(path) == '100F0C4D658B9DD73600CE4BB9C75657E0651F0F0F5E0B2F977F85D8F495350B'): @@ -301,13 +327,13 @@ def client_inputs(runtime, prepared): def prepare_assets(runtime, cache=None): - raise RuntimeError('Use pinned native 1.21.11 assets; inherited cache-copy hydration is disabled') + raise RuntimeError('Use pinned native 26.1.2 assets; inherited cache-copy hydration is disabled') def prepare_client(home, candidate=None, probe=None): home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() - seed_loader_defaults(home, deterministic=probe is not None) + seed_loader_defaults(home, deterministic=probe is not None,current=True) for artifact in (candidate, probe): if artifact is not None: if not artifact.is_file(): @@ -320,20 +346,20 @@ def prepare_client(home, candidate=None, probe=None): def client_command(java, home, runtime, assets, *, username='LootableTest', extra=(), prepared=None): from native120_client import command if prepared is None: - prepared=Path(__file__).resolve().parents[1]/'.gradle/native12111-client-inputs' + prepared=Path(__file__).resolve().parents[1]/'.gradle/native2612-client-inputs' return command(java,home,runtime,assets,prepared,username=username,extra=extra) def install(java, home, client=False): - """Install Forge 61 locally; its installer owns the server downloads.""" + """Install Forge 64 locally; its installer owns the server downloads.""" from prepare_migration_inputs import fetch home=home.resolve();home.mkdir(parents=True,exist_ok=False) installer=home/(FORGE+'-installer.jar') - fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.21.11-61.2.4/'+installer.name,installer,INSTALLER_SHA256) + fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/26.1.2-64.1.5/'+installer.name,installer,INSTALLER_SHA256) if client: from native120_client import BASE_VERSION,BASE_SHA1 base=home/'versions'/BASE_VERSION/(BASE_VERSION+'.json') - _download({'sha1':BASE_SHA1,'url':'https://piston-meta.mojang.com/v1/packages/'+BASE_SHA1+'/1.21.11.json'},base) + _download({'sha1':BASE_SHA1,'url':'https://piston-meta.mojang.com/v1/packages/'+BASE_SHA1+'/26.1.2.json'},base) metadata=json.loads(base.read_text());_download(metadata['downloads']['client'],base.with_suffix('.jar')) (home/'launcher_profiles.json').write_text('{"profiles":{}}\n') run_logged([str(java),'-Djava.awt.headless=true','-jar',installer.name, diff --git a/tools/qualification-policy.json b/tools/qualification-policy.json index 1f8ed0d..4f2c007 100644 --- a/tools/qualification-policy.json +++ b/tools/qualification-policy.json @@ -1,20 +1,20 @@ { "schema": 1, - "target": "1.21.11", - "status": "Forge 1.21.11 stays local until qualification and manual testing are complete.", + "target": "26.1.2", + "status": "Manual testing accepted; expensive qualification stays local. Hosted work checks the exact release archives and security.", "hosted": { "allowed": [ - "policy", - "production_archive", + "compile", + "archive", "security", - "release_contracts", - "wrapper_validation" + "lightweight contracts" ], "fork_gradle_build": false, "full_java_suite": false, "full_python_suite": false, "gameplay_or_migration": false, - "runtime_sdk_or_asset_install": false + "runtime_sdk_or_asset_install": false, + "local_only": false }, "local": { "development": "Focused changed-behaviour regression first; batch cheap Java/Python checks. Full runtime matrix is not an edit/commit hook.", diff --git a/tools/test_hosted_policy.py b/tools/test_hosted_policy.py index 0c69b1a..a434a41 100644 --- a/tools/test_hosted_policy.py +++ b/tools/test_hosted_policy.py @@ -6,7 +6,7 @@ class HostedPolicyTests(unittest.TestCase): def test_raw_jdk_version_is_rejected_before_java_preparation(self): text=(Path(__file__).resolve().parents[1]/'.github/workflows/native120.yml').read_text() - with self.assertRaises(ValueError):validate_workflow(text.replace('21.0.7+6.0.LTS','21.0.7+99')) + with self.assertRaises(ValueError):validate_workflow(text.replace('25.0.3+9.0.LTS','25.0.3+99')) def test_hosted_roots_allow_archive_builds_and_reject_every_test_or_game_entry(self): base='./gradlew assemble verifyProductionRuntimeClasspath prepareReleaseBundle ' @@ -29,17 +29,17 @@ def test_missing_fork_guard_or_pipe_failure_guard_is_rejected(self): with self.subTest(removed=removed),self.assertRaises(ValueError):validate_workflow(text.replace(removed,'')) def test_manifest_boolean_cannot_override_an_integrity_hold(self): - version='2.4.11.121111';base='LootableBodies-'+version + version='2.4.11.2601021';base='LootableBodies-'+version names=['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom'] - good={'version':version,'minecraft':'1.21.11','forge':'61.2.4','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in names}} + good={'version':version,'minecraft':'26.1.2','forge':'64.1.5','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in names}} require_qualified_manifest(good) for field,value in [('qualification_integrity_status','blocked_receipt_mismatch'),('qualification_integrity_status',None),('local_qualification_complete',False),('minecraft','1.17.1')]: changed=dict(good);changed[field]=value with self.subTest(field=field,value=value),self.assertRaises(ValueError):require_qualified_manifest(changed) def test_missing_or_extra_file_declaration_is_fatal(self): - version='2.4.11.121111';base='LootableBodies-'+version - good={'version':version,'minecraft':'1.21.11','forge':'61.2.4','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in ['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom']}} + version='2.4.11.2601021';base='LootableBodies-'+version + good={'version':version,'minecraft':'26.1.2','forge':'64.1.5','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in ['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom']}} for extra in (False,True): changed=json.loads(json.dumps(good)) if extra:changed['files']['extra.jar']='B'*64 diff --git a/tools/test_native120_build.py b/tools/test_native120_build.py index bfd3c7f..3d261d2 100644 --- a/tools/test_native120_build.py +++ b/tools/test_native120_build.py @@ -10,7 +10,7 @@ def test_missing_changed_extra_and_unqualified_inputs_are_fatal(self): root=Path(directory);bundle=root/'bundle';bundle.mkdir();manifest=root/'manifest.json' names=['CHANGELOG.txt','SHA256SUMS',MAIN_JAR,MAIN_JAR[:-4]+'-sources.jar',MAIN_JAR[:-4]+'-javadoc.jar',MAIN_JAR[:-4]+'.pom'] for name in names:(bundle/name).write_bytes(name.encode()) - record={'version':MOD_VERSION,'minecraft':'1.21.11','forge':'61.2.4','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{p.name:digest(p) for p in bundle.iterdir()}} + record={'version':MOD_VERSION,'minecraft':'26.1.2','forge':'64.1.5','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{p.name:digest(p) for p in bundle.iterdir()}} manifest.write_text(json.dumps(record));self.assertEqual(record['files'],check_bundle(manifest,bundle)) for mutation in ['unqualified','integrity-hold','wrong-target','missing-file','changed-file','extra-file','missing-identity']: with self.subTest(mutation=mutation): diff --git a/tools/test_native120_client_contracts.py b/tools/test_native120_client_contracts.py index 3e59680..f505f3c 100644 --- a/tools/test_native120_client_contracts.py +++ b/tools/test_native120_client_contracts.py @@ -27,14 +27,14 @@ def test_unknown_placeholder_fails_before_a_process_can_be_started(self): with self.assertRaises(KeyError): native120_client.expand(['${unrecognised}'],{},system='windows',architecture='amd64',os_version='10.0') - def test_forge_bootstrap_uses_installed_client_descriptor_and_exact_java21(self): + def test_forge_bootstrap_uses_installed_client_descriptor_and_exact_java25(self): with tempfile.TemporaryDirectory() as directory: root=Path(directory);java=root/'jdk/bin/java.exe';java.parent.mkdir(parents=True) - (java.parent.parent/'release').write_text('JAVA_RUNTIME_VERSION="21.0.7+6-LTS"\nIMPLEMENTOR="Eclipse Adoptium"\n') + (java.parent.parent/'release').write_text('JAVA_RUNTIME_VERSION="25.0.3+9-LTS"\nIMPLEMENTOR="Eclipse Adoptium"\n') index=root/'assets/indexes/16.json';index.parent.mkdir(parents=True);index.write_bytes(b'{}') base={'assetIndex':{'id':'16','sha1':hashlib.sha1(b'{}').hexdigest()},'arguments':{'jvm':['-cp','${classpath}'],'game':['--version','${version_name}','--accessToken','${auth_access_token}','--clientId','${clientid}','--xuid','${auth_xuid}']}} forge={'arguments':{'jvm':['-Djava.net.preferIPv6Addresses=system'],'game':['--launchTarget','forge_client']},'mainClass':'net.minecraftforge.bootstrap.ForgeBootstrap'} - record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'1.21.11.jar')],'natives':str(root/'natives'),'system':'windows','architecture':'amd64'} + record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'26.1.2.jar')],'natives':str(root/'natives'),'system':'windows','architecture':'amd64'} with patch('native120_client.load',return_value=(base,forge,record)): result=native120_client.command(java,root/'home',root/'sdk',root/'assets',root/'prepared',username='Fixture') with patch.dict('os.environ',{'LOOTABLEBODIES_TEST_SESSION_ENDPOINT':'http://127.0.0.1:51927'}): @@ -42,14 +42,14 @@ def test_forge_bootstrap_uses_installed_client_descriptor_and_exact_java21(self) with patch.dict('os.environ',{'LOOTABLEBODIES_TEST_SESSION_ENDPOINT':'https://example.invalid'}), self.assertRaisesRegex(ValueError,'loopback'): native120_client.command(java,root/'home',root/'sdk',root/'assets',root/'prepared',username='Fixture') self.assertIn('-Djava.net.preferIPv6Addresses=system',result) - self.assertIn(str(root/'1.21.11.jar'),result) + self.assertIn(str(root/'26.1.2.jar'),result) self.assertIn('--offlineDeveloperMode',result) self.assertIn('net.minecraftforge.bootstrap.ForgeBootstrap',result) self.assertEqual('forge_client',result[result.index('--launchTarget')+1]) self.assertFalse(any('ignoreList' in arg for arg in result)) self.assertIn('-Dstdout.encoding=UTF-8',result) self.assertIn('-Dstderr.encoding=UTF-8',result) - self.assertEqual('1.21.11-forge-61.2.4',result[result.index('--version')+1]) + self.assertEqual('26.1.2-forge-64.1.5',result[result.index('--version')+1]) self.assertEqual('0',result[result.index('--accessToken')+1]) self.assertEqual('',result[result.index('--clientId')+1]);self.assertEqual('',result[result.index('--xuid')+1]) self.assertIn('-Dminecraft.api.session.host=http://127.0.0.1:51927',owned) diff --git a/tools/test_native120_client_logs.py b/tools/test_native120_client_logs.py index 49d13e7..4749def 100644 --- a/tools/test_native120_client_logs.py +++ b/tools/test_native120_client_logs.py @@ -12,7 +12,7 @@ class Native120ClientDiagnosticTests(unittest.TestCase): def check(self,text): with tempfile.TemporaryDirectory() as directory: p=Path(directory)/'client.log';p.write_text(text,encoding='utf-8') - with patch.object(logs,'validated_baseline',return_value={'events':EVENTS,'proof_sha256':'A'*64}):return logs.audit_client(p) + with patch.object(logs,'validated_baseline',return_value={'events':EVENTS,'proof_sha256':'A'*64}):return logs.audit_client(p,minecraft="1.21.11") def test_complete_proved_native_batch_is_explicitly_classified(self): for text in [lines(),lines().replace('[12:00:00]','[12:00:00.123]')]: report=self.check(text+'[12:00:01] [Render thread/INFO] Completed synthetic fixture\n') @@ -21,7 +21,7 @@ def test_missing_or_changed_control_proof_cannot_qualify(self): with tempfile.TemporaryDirectory() as directory: p=Path(directory)/'client.log';p.write_text(lines(),encoding='utf-8') with patch.object(logs,'validated_baseline',side_effect=RuntimeError('Unqualified native client diagnostic proof')): - with self.assertRaisesRegex(RuntimeError,'Unqualified'):logs.audit_client(p) + with self.assertRaisesRegex(RuntimeError,'Unqualified'):logs.audit_client(p,minecraft="1.21.11") def test_unknown_mutated_partial_or_extra_native_warning_is_fatal(self): for text in [lines(EVENTS[:-1]),lines()+lines(EVENTS[:1]),lines().replace('Render thread/WARN','Worker-Main-1/WARN'),lines().replace('minecraft:item.goat_horn.play','anothermod:missing_sound')]: with self.subTest(text=text),self.assertRaises(RuntimeError):self.check(text) @@ -29,6 +29,12 @@ def test_actual_missing_resources_errors_exceptions_and_old_native_errors_stay_f for failure in ['[12:00:01] [Render thread/WARN] Missing texture: anothermod:lost\n','[12:00:01] [Render thread/WARN] File minecraft:sounds/missing.ogg does not exist, cannot add it to event anothermod:lost\n','[12:00:01] [Render thread/ERROR] actual failure\n','Caused by: java.lang.IllegalStateException: failed inventory\n','AL lib: (EE) alc_cleanup: 1 device not closed\n']: for text in [failure,lines()+failure]: with self.subTest(failure=failure,text=text),self.assertRaises(RuntimeError):self.check(text) + def test_new_target_cannot_reuse_the_previous_sound_control(self): + with tempfile.TemporaryDirectory() as directory: + log=Path(directory)/'client.log';log.write_text(lines(),encoding='utf-8') + with patch.object(logs,'validated_baseline') as old, self.assertRaisesRegex(RuntimeError,'26.1.2'): + logs.audit_client(log) + old.assert_not_called() def test_generic_and_server_auditors_still_reject_this_raw_baseline(self): with tempfile.TemporaryDirectory() as directory: p=Path(directory)/'server.log';p.write_text(lines(),encoding='utf-8') diff --git a/tools/test_native120_flat_header.py b/tools/test_native120_flat_header.py index 2e2582d..04b1d6f 100644 --- a/tools/test_native120_flat_header.py +++ b/tools/test_native120_flat_header.py @@ -1,32 +1,44 @@ import gzip import hashlib +import json from pathlib import Path import tempfile import unittest +from unittest.mock import patch from native_runtime import seed_flat_fixture from nbt_regions import Tag,encode from verify_native_respawn_control import require_forge_only_header class Native120FlatHeaderTests(unittest.TestCase): - def header(self,root,version=4671,generator='minecraft:flat'): + def header(self,root,version=4790,generator='minecraft:flat'): settings=Tag(10,{'dimensions':Tag(10,{'minecraft:overworld':Tag(10,{ 'generator':Tag(10,{'type':Tag(8,generator)})})})}) - data=encode('',Tag(10,{'Data':Tag(10,{'DataVersion':Tag(3,version),'WorldGenSettings':settings})})) - path=root/'native-header.dat';path.write_bytes(gzip.compress(data,mtime=0));return path,hashlib.sha256(path.read_bytes()).hexdigest().upper() + data=encode('',Tag(10,{'Data':Tag(10,{'DataVersion':Tag(3,version)})})) + path=root/'native-header.dat';path.write_bytes(gzip.compress(data,mtime=0)) + worldgen=root/'native-worldgen.dat' + worldgen.write_bytes(gzip.compress(encode('',Tag(10,{'DataVersion':Tag(3,version),'data':settings})),mtime=0)) + digest=hashlib.sha256(path.read_bytes()).hexdigest().upper() + (root/'flat-inputs.json').write_text(json.dumps({digest:{'header':path.name,'worldgen':worldgen.name, + 'worldgen_sha256':hashlib.sha256(worldgen.read_bytes()).hexdigest().upper()}})) + return path,digest def home(self,root): home=root/'home';home.mkdir();(home/'server.properties').touch();return home - def test_only_the_explicit_sealed_header_is_copied(self): + def test_only_the_two_explicit_sealed_inputs_are_copied(self): with tempfile.TemporaryDirectory() as directory: root=Path(directory);header,digest=self.header(root);before=header.read_bytes();home=self.home(root) - seed_flat_fixture(home,header,digest) + with patch('native_runtime.FLAT_INPUTS_PATH',root/'flat-inputs.json'): + seed_flat_fixture(home,header,digest) self.assertEqual(before,(home/'world/level.dat').read_bytes());self.assertEqual(before,header.read_bytes()) - self.assertEqual(['level.dat'],[p.name for p in (home/'world').iterdir()]) + files=sorted(p.relative_to(home/'world').as_posix() for p in (home/'world').rglob('*') if p.is_file()) + self.assertEqual(['data/minecraft/world_gen_settings.dat','level.dat'],files) + self.assertEqual((root/'native-worldgen.dat').read_bytes(),(home/'world'/files[0]).read_bytes()) def test_wrong_hash_target_or_generator_refuses_before_creating_world(self): - for version,generator,bad_hash in [(4671,'minecraft:flat',True),(3955,'minecraft:flat',False),(3839,'minecraft:flat',False),(3465,'minecraft:flat',False),(2586,'minecraft:flat',False),(2730,'minecraft:flat',False),(2975,'minecraft:flat',False),(4671,'minecraft:noise',False)]: + for version,generator,bad_hash in [(4790,'minecraft:flat',True),(4671,'minecraft:flat',False),(3955,'minecraft:flat',False),(3839,'minecraft:flat',False),(3465,'minecraft:flat',False),(2586,'minecraft:flat',False),(2730,'minecraft:flat',False),(2975,'minecraft:flat',False),(4790,'minecraft:noise',False)]: with self.subTest(version=version,generator=generator,bad_hash=bad_hash),tempfile.TemporaryDirectory() as directory: root=Path(directory);header,digest=self.header(root,version,generator);home=self.home(root) - with self.assertRaises(RuntimeError):seed_flat_fixture(home,header,'0'*64 if bad_hash else digest) + with patch('native_runtime.FLAT_INPUTS_PATH',root/'flat-inputs.json'), self.assertRaises(RuntimeError): + seed_flat_fixture(home,header,'0'*64 if bad_hash else digest) self.assertFalse((home/'world').exists()) def test_existing_world_is_never_overwritten(self): with tempfile.TemporaryDirectory() as directory: @@ -36,8 +48,8 @@ def test_existing_world_is_never_overwritten(self): self.assertEqual(b'existing world bytes',protected.read_bytes()) def test_actual_native20_codec_header_is_a_sealed_current_target_input(self): - header=Path(__file__).parent/'fixtures/native12111-flat-level.dat' - expected='33DBE405659FB4EF57D23A07485EBC9AFA57694D7B54357C5B534A3E8963F86F' + header=Path(__file__).parent/'fixtures/native2612-flat-level.dat' + expected='149C39C40EEEB0241C0B15A9113BF72055FA7CB41FC55C2126BE79EF90330543' self.assertEqual(expected,hashlib.sha256(header.read_bytes()).hexdigest().upper()) with tempfile.TemporaryDirectory() as directory: home=self.home(Path(directory));seed_flat_fixture(home,header,expected) @@ -45,12 +57,12 @@ def test_actual_native20_codec_header_is_a_sealed_current_target_input(self): def test_forge_only_control_rejects_native_header_with_the_mod_installed(self): fixtures=Path(__file__).parent/'fixtures' - forge_only=fixtures/'native12111-forge-only-flat-level.dat' - expected='4754C3F3D514B82E89FCF7F89F882A38611A456ABA9A9AB4C6564A13707F8026' + forge_only=fixtures/'native2612-forge-only-flat-level.dat' + expected='E64804965621AD9F107A12A55F29EA2CC97C2218BFC92E8DE73AFB7C80F23EA3' self.assertEqual(expected,hashlib.sha256(forge_only.read_bytes()).hexdigest().upper()) require_forge_only_header(forge_only) with self.assertRaisesRegex(ValueError,'Forge-only saved mod list'): - require_forge_only_header(fixtures/'native12111-flat-level.dat') + require_forge_only_header(fixtures/'native2612-flat-level.dat') if __name__=='__main__':unittest.main() diff --git a/tools/test_native120_infrastructure.py b/tools/test_native120_infrastructure.py index c26a4d6..e504bd5 100644 --- a/tools/test_native120_infrastructure.py +++ b/tools/test_native120_infrastructure.py @@ -5,18 +5,18 @@ class Native120InfrastructureTest(unittest.TestCase): def test_native_distribution_and_every_server_input_use_the_target(self): - self.assertEqual('forge-1.21.11-61.2.4',runtime.FORGE) - self.assertEqual('B1C3EE57BADED63F7591293134FF079B69CACEF3317D2E6783B28D92CFDC2BF4',runtime.INSTALLER_SHA256) - self.assertEqual(104,len(runtime.SERVER_INPUTS)) - self.assertEqual('A5D403505CFAB57AC0C8798928BDC08F867890719FB7F6EAB440D81D6E10A6F8', - runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.21.11-61.2.4/forge-1.21.11-61.2.4-server.jar']) - self.assertEqual('CE2AEB00434E5D6F9DB7801C615F37D3AB2E00913B784408173FEA2336FC2C4B', - runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.21.11-61.2.4/win_args.txt']) + self.assertEqual('forge-26.1.2-64.1.5',runtime.FORGE) + self.assertEqual('6E01323837051176762BD466340A601DA122F179FE6A00AC616E05429AE07648',runtime.INSTALLER_SHA256) + self.assertEqual(100,len(runtime.SERVER_INPUTS)) + self.assertEqual('C6B85C178B037519464D436A75B7C68408CBE591576270BA0E826542B50BF41B', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/26.1.2-64.1.5/forge-26.1.2-64.1.5-server.jar']) + self.assertEqual('292659817651988B6403FF829550635A263F059359F46B51C949E49C2BEB3549', + runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/26.1.2-64.1.5/win_args.txt']) self.assertFalse(any('1.16.5' in path for path in runtime.SERVER_INPUTS)) def test_development_assets_use_the_native_index(self): text=(Path(__file__).resolve().parents[1]/'gradle/assets.gradle').read_text() - self.assertIn("minecraftAssetIndex = '29'",text) - self.assertIn("1.21.11.json",text) + self.assertIn("minecraftAssetIndex = '30'",text) + self.assertIn("26.1.2.json",text) self.assertNotIn('e9d0adb8f642abe422909ede50f651b2b58a3573',text) def test_every_released115_history_extends_the_complete_earlier_matrix(self): from verify_world_upgrades import ORIGINS,PHASES diff --git a/tools/test_native120_launch.py b/tools/test_native120_launch.py index a952566..13393d7 100644 --- a/tools/test_native120_launch.py +++ b/tools/test_native120_launch.py @@ -11,9 +11,9 @@ class Native120LaunchTests(unittest.TestCase): def test_posix_native_diagnostics_fixture_uses_the_posix_tools(self): with patch.object(native_runtime,'os',SimpleNamespace(name='posix')): - self.test_packaged_server_uses_installer_module_arguments_and_java21_gc() + self.test_packaged_server_uses_installer_module_arguments_and_java25_gc() - def jdk(self, root, version='21.0.7+6-LTS'): + def jdk(self, root, version='25.0.3+9-LTS'): suffix='.exe' if native_runtime.os.name=='nt' else '' java = root/('jdk/bin/java'+suffix) java.parent.mkdir(parents=True) @@ -23,7 +23,7 @@ def jdk(self, root, version='21.0.7+6-LTS'): 'JAVA_RUNTIME_VERSION="'+version+'"\nIMPLEMENTOR="Eclipse Adoptium"\n') return java - def test_packaged_server_uses_installer_module_arguments_and_java21_gc(self): + def test_packaged_server_uses_installer_module_arguments_and_java25_gc(self): with tempfile.TemporaryDirectory() as directory: home=Path(directory); java=self.jdk(home) with patch('native_runtime.run_monitored', return_value=subprocess.CompletedProcess([],0)) as run: @@ -31,7 +31,7 @@ def test_packaged_server_uses_installer_module_arguments_and_java21_gc(self): command=run.call_args.args[0] self.assertNotIn('-jar',command) self.assertIn('-Xmx2G',command) - self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/1.21.11-61.2.4/') for arg in command)) + self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/26.1.2-64.1.5/') for arg in command)) gc=next((arg for arg in command if arg.startswith('-Xlog:')),None) self.assertIsNotNone(gc) self.assertIn('gc*,safepoint',gc) @@ -45,7 +45,7 @@ def test_runtime_rejects_a_different_jdk_before_launch(self): with tempfile.TemporaryDirectory() as directory: home=Path(directory); java=self.jdk(home,'17.0.1+12') with patch('native_runtime.run_monitored') as run: - with self.assertRaisesRegex(RuntimeError,'21.0.7'): + with self.assertRaisesRegex(RuntimeError,'25.0.3'): native_runtime.run_java(java,home,'fixture') run.assert_not_called() @@ -61,7 +61,7 @@ def launch(command, **options): options['process_started'](SimpleNamespace(pid=1234,poll=lambda:None)) return subprocess.CompletedProcess(command,0) def boot(_): - gc.write_text('[0.010s][info][gc,init] Version: 21.0.7+6-LTS\n') + gc.write_text('[0.010s][info][gc,init] Version: 25.0.3+9-LTS\n') def attach(command, **options): self.assertTrue(gc.is_file(),'Attach ran before the owned JVM initialized GC logging') self.assertEqual('1234',command[-1]) @@ -73,16 +73,16 @@ def attach(command, **options): self.assertEqual(['jstack','jstat'],started) def test_server_inputs_pin_native50_instead_of_the_old_launcher(self): - self.assertEqual('forge-1.21.11-61.2.4',native_runtime.FORGE) - for path in ('libraries/net/minecraftforge/forge/1.21.11-61.2.4/win_args.txt', - 'libraries/net/minecraftforge/forge/1.21.11-61.2.4/unix_args.txt', - 'libraries/net/minecraftforge/forge/1.21.11-61.2.4/forge-1.21.11-61.2.4-server.jar'): + self.assertEqual('forge-26.1.2-64.1.5',native_runtime.FORGE) + for path in ('libraries/net/minecraftforge/forge/26.1.2-64.1.5/win_args.txt', + 'libraries/net/minecraftforge/forge/26.1.2-64.1.5/unix_args.txt', + 'libraries/net/minecraftforge/forge/26.1.2-64.1.5/forge-26.1.2-64.1.5-server.jar'): self.assertIn(path,native_runtime.SERVER_INPUTS) self.assertFalse(any('1.16.5' in path for path in native_runtime.SERVER_INPUTS)) def test_native50_historical_workers_have_their_own_fresh_qualification_binding(self): proof=native_runtime.WORKER_BUDGET_PROOF - self.assertEqual(('1.21.11','61.2.4',200,4,True),tuple(proof[key] for key in + self.assertEqual(('26.1.2','64.1.5',200,4,True),tuple(proof[key] for key in ('minecraft','forge','reloads','jvms','qualified'))) self.assertIn('server_input_manifest_sha256',proof) self.assertRegex(proof['qualification_evidence_sha256'],r'^[A-F0-9]{64}$') diff --git a/tools/test_native120_persistence.py b/tools/test_native120_persistence.py index e64da18..c9ee057 100644 --- a/tools/test_native120_persistence.py +++ b/tools/test_native120_persistence.py @@ -6,8 +6,8 @@ from verify_world_upgrades import patch_stamps class Native120PersistenceTest(unittest.TestCase): - def body(self,number=1,marker=13):return Tag(10,{'id':Tag(8,'lootablebodies:corpse'),'LootableBodiesDataVersion':Tag(3,marker),'UUID':Tag(11,struct.pack('>iiii',0,0,0,number))}) - def chunk(self,bodies,version=4671,position=(8,8)): + def body(self,number=1,marker=14):return Tag(10,{'id':Tag(8,'lootablebodies:corpse'),'LootableBodiesDataVersion':Tag(3,marker),'UUID':Tag(11,struct.pack('>iiii',0,0,0,number))}) + def chunk(self,bodies,version=4790,position=(8,8)): return {'name':'','root':Tag(10,{'DataVersion':Tag(3,version),'Position':Tag(11,struct.pack('>ii',*position)),'Entities':Tag(9,(10,bodies))}),'compression':2,'timestamp':bytes(4)} def write(self,world,chunk,dimension=''): path=world/dimension/'entities/r.0.0.mca';path.parent.mkdir(parents=True,exist_ok=True);write_disposable_region(path,{264:chunk});return path @@ -16,7 +16,7 @@ def test_native20_saved_entities_count_all_dimensions_without_mutation(self): world=Path(directory);paths=[self.write(world,self.chunk([self.body(i+1)]),dim) for i,dim in enumerate(['','DIM-1','DIM1'])] before=[p.read_bytes() for p in paths];self.assertEqual(3,count_persisted(world,0));self.assertEqual(before,[p.read_bytes() for p in paths]) def test_previous_marker_native_version_or_wrong_entity_chunk_position_is_fatal(self): - for marker,version,position in [(12,4671,(8,8)),(13,3955,(8,8)),(13,4671,(9,8))]: + for marker,version,position in [(13,4790,(8,8)),(14,4671,(8,8)),(14,4790,(9,8))]: with self.subTest(marker=marker,version=version,position=position),tempfile.TemporaryDirectory() as directory: world=Path(directory);self.write(world,self.chunk([self.body(marker=marker)],version,position)) with self.assertRaises(ValueError):count_persisted(world,0) @@ -61,8 +61,8 @@ def test_valid_future_entity_fixture_preserves_original_and_rejects_wrong_source mutation=inject_future_entity_version(world) self.assertEqual(original,(home/'future-entity-original.mca').read_bytes()) converted = read_region(path)[264]['root'].value - self.assertEqual(Tag(3, 4672), converted['DataVersion']) - self.assertEqual(4672, mutation['injected_version']) + self.assertEqual(Tag(3, 4791), converted['DataVersion']) + self.assertEqual(4791, mutation['injected_version']) self.assertEqual(self.chunk([self.body(marker=8)], version)['root'].value['Entities'], converted['Entities']) self.assertEqual(8,mutation['corpse_marker']) diff --git a/tools/test_native_evidence.py b/tools/test_native_evidence.py index ae1b030..4b1e983 100644 --- a/tools/test_native_evidence.py +++ b/tools/test_native_evidence.py @@ -14,7 +14,7 @@ def fixture(self,home): record={'timed_out':False,'returncode':0,'pid':123,'timeout':240,'cwd':str(home), 'command':['fixture-java','-Dlootablebodies.phase='+phase, '-Xlog:gc*,safepoint:file=upgrade-near.diagnostics/gc.txt:time,uptime,level,tags', - '@libraries/net/minecraftforge/forge/1.21.11-61.2.4/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], + '@libraries/net/minecraftforge/forge/26.1.2-64.1.5/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], 'evidence':{name:digest(directory/name) for name in ['gc.txt','initial-threads.txt','initial-heap-gc.txt']},'captures':[],'metrics':[]} (directory/'result.json').write_text(json.dumps(record));return directory,record def test_complete_receipt_retains_all_logs_and_rehashes_every_native_file(self): @@ -64,7 +64,7 @@ def test_downloaded_linux_receipt_is_audited_independently_of_auditor_os(self): record['cwd']='/original/runner/build/migration-results/case' record['command']=[part.replace('/win_args.txt','/unix_args.txt') for part in record['command']] (folder/'result.json').write_text(json.dumps(record));audit_phase(home,'upgrade-near',relocated=True) - record['command'].append('@libraries/net/minecraftforge/forge/1.21.11-61.2.4/win_args.txt') + record['command'].append('@libraries/net/minecraftforge/forge/26.1.2-64.1.5/win_args.txt') (folder/'result.json').write_text(json.dumps(record)) with self.assertRaises(ValueError):audit_phase(home,'upgrade-near',relocated=True) diff --git a/tools/test_native_runtime.py b/tools/test_native_runtime.py index 50bd9d6..e1482d4 100644 --- a/tools/test_native_runtime.py +++ b/tools/test_native_runtime.py @@ -41,7 +41,9 @@ def test_new_owned_client_starts_with_all_four_native_loader_defaults(self): config=home/'config/fml.toml' self.assertTrue(config.is_file(), 'Prepare stock loader config before parallel loading starts') self.assertEqual({'versionCheck':True, 'splashscreen':True, - 'maxThreads':-1, 'defaultConfigPath':'defaultconfigs'}, + 'maxThreads':-1, 'defaultConfigPath':'defaultconfigs', + 'earlyWindowSkipGLVersions':[], 'earlyWindowLogHelpMessage':True, + 'earlyWindowSquir':False, 'earlyWindowShowCPU':False}, tomllib.loads(config.read_text(encoding='utf-8'))) if __name__=='__main__':unittest.main() diff --git a/tools/test_native_server_preparation.py b/tools/test_native_server_preparation.py index f84b296..571a55e 100644 --- a/tools/test_native_server_preparation.py +++ b/tools/test_native_server_preparation.py @@ -25,7 +25,7 @@ def test_wrong_installed_vanilla_bytes_refuse_before_gameplay_without_installer_ with tempfile.TemporaryDirectory() as directory: home=Path(directory)/'new-sdk';pins,installed=self.fixture(home,b'changed bytes') with patch('prepare_migration_inputs.fetch'),patch.object(runtime,'_download') as prefetch,patch.object(runtime,'run_logged',side_effect=installed) as installer,patch.object(runtime,'SERVER_INPUTS',pins): - with self.assertRaisesRegex(RuntimeError,'Incorrect official Forge 61 server input'):runtime.install(Path('fixture-java'),home) + with self.assertRaisesRegex(RuntimeError,'Incorrect official Forge 64 server input'):runtime.install(Path('fixture-java'),home) prefetch.assert_not_called();self.assertEqual(1,installer.call_count) self.assertFalse((home/'runtime-inputs.json').exists()) def test_missing_installed_input_is_fatal(self): diff --git a/tools/test_ordinary_server_setup.py b/tools/test_ordinary_server_setup.py index a838e03..5aec1ae 100644 --- a/tools/test_ordinary_server_setup.py +++ b/tools/test_ordinary_server_setup.py @@ -8,7 +8,9 @@ def test_complete_native_defaults_exist_before_gradle_starts_forge(self): with tempfile.TemporaryDirectory() as folder: home=Path(folder)/'server';prepare_development_home(home) self.assertEqual({'versionCheck':True,'splashscreen':True,'maxThreads':-1, - 'defaultConfigPath':'defaultconfigs'},tomllib.loads((home/'config/fml.toml').read_text())) + 'defaultConfigPath':'defaultconfigs', + 'earlyWindowSkipGLVersions':[], 'earlyWindowLogHelpMessage':True, + 'earlyWindowSquir':False, 'earlyWindowShowCPU':False},tomllib.loads((home/'config/fml.toml').read_text())) self.assertEqual([],list(home.rglob('*.jar'))) def test_existing_choices_are_byte_identical_when_preparing_the_owned_home(self): diff --git a/tools/test_owned_process.py b/tools/test_owned_process.py index f98cbe1..fad3ba5 100644 --- a/tools/test_owned_process.py +++ b/tools/test_owned_process.py @@ -12,10 +12,10 @@ class OwnedProcessTest(unittest.TestCase): - def start(self,root,script,timeout): + def start(self,root,script,timeout,*,initial_diagnostics=None): home=root/'home';home.mkdir() return OwnedProcess([sys.executable,'-u','-c',script],cwd=home,log=root/'client.log',timeout=timeout, - diagnostic_command=[sys.executable,'-c','print("owned capture")']) + diagnostic_command=[sys.executable,'-c','print("owned capture")'],initial_diagnostics=initial_diagnostics) def test_completed_process_preserves_same_stdout_alias_and_receipt(self): with tempfile.TemporaryDirectory() as directory: root=Path(directory);p=self.start(root,'print("real owned output")',5) @@ -32,7 +32,16 @@ def test_group_abort_reaps_its_owned_child_without_changing_runtime_deadline(sel self.assertFalse(record['timed_out']);self.assertIsNotNone(record['returncode']) def test_deadline_failure_still_keeps_a_failed_receipt_and_complete_stdout(self): with tempfile.TemporaryDirectory() as directory: - root=Path(directory);p=self.start(root,'import time; print("before deadline"); time.sleep(30)',.3) + root=Path(directory) + def wait_for_fixture_output(process): + log=root/'home/client.log' + ready_by=time.monotonic()+3 + while b'before deadline' not in log.read_bytes(): + if process.poll() is not None or time.monotonic()>=ready_by: + raise AssertionError('Synthetic child never wrote its startup line') + time.sleep(.01) + p=self.start(root,'import time; print("before deadline"); time.sleep(30)',.3, + initial_diagnostics=wait_for_fixture_output) with self.assertRaises(subprocess.TimeoutExpired):p.wait(timeout=5) record=json.loads((root/'home/client.diagnostics/result.json').read_text()) self.assertTrue(record['timed_out']);self.assertNotEqual(0,record['returncode']) @@ -113,11 +122,11 @@ def initial(process):raise RuntimeError('injected initial diagnostic failure') self.assertIsNotNone(receipt['returncode']);self.assertNotEqual(0,receipt['returncode']);self.assertFalse(receipt['timed_out']) def test_previous_runtime_and_wrong_patch_refuse_before_group_launch(self): - for version,vendor in [('16.0.2+7','Eclipse Foundation'),('17.0.20.1+1','Eclipse Adoptium'),('21.0.8+9-LTS','Eclipse Adoptium'),('21.0.7+6-LTS','Other Vendor')]: + for version,vendor in [('16.0.2+7','Eclipse Foundation'),('17.0.20.1+1','Eclipse Adoptium'),('21.0.8+9-LTS','Eclipse Adoptium'),('21.0.7+6-LTS','Eclipse Adoptium'),('25.0.3+8-LTS','Eclipse Adoptium'),('25.0.3+9-LTS','Other Vendor')]: with self.subTest(version=version),tempfile.TemporaryDirectory() as directory: root=Path(directory);java=root/'jdk/bin/java.exe';java.parent.mkdir(parents=True) (java.parent.parent/'release').write_text('JAVA_RUNTIME_VERSION="'+version+'"\nIMPLEMENTOR="'+vendor+'"\n') - with patch('owned_process.OwnedProcess') as launch,self.assertRaisesRegex(RuntimeError,'21.0.7'): + with patch('owned_process.OwnedProcess') as launch,self.assertRaisesRegex(RuntimeError,'25.0.3'): native_process([str(java)],cwd=root,log=root/'runtime.log',timeout=5) launch.assert_not_called() diff --git a/tools/test_runtime_diagnostics.py b/tools/test_runtime_diagnostics.py index d956d9c..3b68e5a 100644 --- a/tools/test_runtime_diagnostics.py +++ b/tools/test_runtime_diagnostics.py @@ -50,8 +50,8 @@ def run_child(self, home, code, *, timeout=1.2, collector=None, metric=None): [sys.executable, '-u', '-c', code], cwd=home, log=home/'phase.log', timeout=timeout, diagnostics=home/'phase.diagnostics', - diagnostic_command=collector or [sys.executable, '-c', - 'import sys; print("THREAD_CAPTURE_TARGET="+sys.argv[1], flush=True)'], + diagnostic_command=collector or [sys.executable, '-S', '-c', + 'import os,sys; print("THREAD_CAPTURE_TARGET="+sys.argv[1], flush=True); os._exit(0)'], metric_command=metric) except BaseException: if os.environ.get('LOOTABLEBODIES_UNIT_EVIDENCE'): @@ -62,25 +62,19 @@ def test_heap_metrics_are_captured_even_if_a_gc_log_has_no_events(self): with tempfile.TemporaryDirectory() as directory: home = Path(directory) with self.assertRaises(subprocess.TimeoutExpired): - self.run_child(home, 'import time; time.sleep(30)', metric=[ - sys.executable, '-S', '-c', 'import os,sys,time; ' - 'time.sleep(30) if os.path.exists("heap-seen") else None; ' - 'open("heap-seen","w").close(); print("HEAP_TARGET="+sys.argv[1],flush=True); os._exit(0)']) - # The first fake collector's only task is its flushed output. Avoid - # unrelated interpreter shutdown consuming its original0.24s budget. - # Later collectors still really block and must be killed on time. + self.run_child(home, 'import time; time.sleep(30)', collector=[ + sys.executable, '-S', '-c', 'import os,sys; print("THREAD_TARGET="+sys.argv[1],flush=True); os._exit(0)'], + metric=[sys.executable, '-S', '-c', + 'import os,sys; print("HEAP_TARGET="+sys.argv[1],flush=True); os._exit(0)']) + # This test proves one successful sample without a GC event. + # The separate stuck-collector test proves bounded termination. report = json.loads((home/'phase.diagnostics/result.json').read_text()) - self.assertTrue(report['metrics']) - self.assertTrue(any(m['returncode']==0 for m in report['metrics'])) - self.assertTrue(any(m['timed_out'] for m in report['metrics'])) - for metric in report['metrics']: - # A later collector must stop at the existing runtime deadline. - # Require actual heap output and retain every bounded stop. - self.assertTrue(metric['returncode']==0 or - (metric['timed_out'] and metric['returncode'] is not None)) - if metric['returncode']==0: - self.assertIn('HEAP_TARGET='+str(report['pid']), - (home/'phase.diagnostics'/metric['file']).read_text()) + self.assertTrue(report['timed_out']) + complete = [sample for sample in report['metrics'] if sample['returncode']==0] + self.assertTrue(complete) + for sample in complete: + self.assertIn('HEAP_TARGET='+str(report['pid']), + (home/'phase.diagnostics'/sample['file']).read_text()) def test_a_stuck_heap_collector_is_killed_without_extending_the_deadline(self): with tempfile.TemporaryDirectory() as directory: diff --git a/tools/verify_native_respawn_control.py b/tools/verify_native_respawn_control.py index e75a89f..29188f9 100644 --- a/tools/verify_native_respawn_control.py +++ b/tools/verify_native_respawn_control.py @@ -1,4 +1,4 @@ -"""Exercise Forge 61 delayed respawns and FIFO packets without Lootable Bodies.""" +"""Exercise Forge 64 delayed respawns and FIFO packets without Lootable Bodies.""" import argparse,gzip,json,socket,subprocess,time,os,re from pathlib import Path from native120_logs import audit @@ -9,7 +9,7 @@ from owned_process import native_process def control_error_counts(path): - """Retained native36 log-fixture contract; never used to qualify Forge 61.""" + """Retained native36 log-fixture contract; never used to qualify Forge 64.""" report=legacy_audit(path,native=True) _,count=forge36_dead_player_movement_blocks(path.read_text(errors='replace').splitlines(),position_only=True) if count!=8:raise RuntimeError('Native control did not reproduce eight delayed position packets') @@ -24,7 +24,7 @@ def require_forge_only_header(header): mods = [(entry.value['ModId'].value, entry.value['ModVersion'].value) for entry in entries] except (AttributeError, KeyError, TypeError, ValueError) as error: raise ValueError('Expected a Forge-only saved mod list') from error - if kind != 10 or sorted(mods) != [('forge', '61.2.4'), ('minecraft', '1.21.11')]: + if kind != 10 or sorted(mods) != [('forge', '64.1.5'), ('minecraft', '26.1.2')]: raise ValueError('Expected a Forge-only saved mod list') @@ -93,7 +93,7 @@ def start(name): server=(root/'server.log').read_text(errors='replace') batches=[int(value) for value in re.findall(r'NATIVE_FIFO_DELAY_RELEASE packets=([0-9]+)',client)] if server.count('NATIVE_DEATH ')!=8 or len(batches)!=8 or sum(batches)<=0: - raise RuntimeError('Forge 61 control did not exercise all eight real delayed respawn/FIFO actions') + raise RuntimeError('Forge 64 control did not exercise all eight real delayed respawn/FIFO actions') result={'lootable_bodies_installed':False,'respawns':8,'forge61_delayed_fifo_batches':batches, 'flat_header_sha256':digest(args.flat_header), 'legacy36_error_matcher_used':False,'forge61_complete_logs_audited':True,'no_unclassified_diagnostics':True, diff --git a/tools/verify_normal_server.py b/tools/verify_normal_server.py index 1250a57..a5ff7e0 100644 --- a/tools/verify_normal_server.py +++ b/tools/verify_normal_server.py @@ -15,7 +15,7 @@ def prepare_development_home(home): home.mkdir(exist_ok=True);(home/'eula.txt').write_text('eula=true\n') - seed_loader_defaults(home) + seed_loader_defaults(home,current=True) properties = 'level-name=world\nlevel-type=DEFAULT\nserver-ip=127.0.0.1\nserver-port=0\nonline-mode=false\n' if os.name == 'nt': properties += 'use-native-transport=false\n' @@ -74,7 +74,7 @@ def main(): remaining=whole_deadline-time.monotonic() if remaining<=0:raise subprocess.TimeoutExpired(command,210) receipt=subprocess.run([str(binary),option,str(pid)],stdout=stream,stderr=subprocess.STDOUT,timeout=min(10,remaining)) - if receipt.returncode or not target.stat().st_size:raise RuntimeError('Actual native17 diagnostic failed: '+str(target)) + if receipt.returncode or not target.stat().st_size:raise RuntimeError('Actual native 26.1.2 diagnostic failed: '+str(target)) diagnostics[target.name]=digest(target) process.stdin.write('stop\n');process.stdin.flush() remaining=whole_deadline-time.monotonic() diff --git a/tools/verify_packaged.py b/tools/verify_packaged.py index a138ca1..4573628 100644 --- a/tools/verify_packaged.py +++ b/tools/verify_packaged.py @@ -1,4 +1,4 @@ -"""Verify the exact production jar in disposable native Forge 52 servers.""" +"""Verify the exact production jar in disposable native Forge 64 servers.""" import argparse import json from pathlib import Path @@ -29,7 +29,7 @@ def main(): prepare(fresh, distribution, main_jar, probe) seed_flat_fixture(fresh,args.flat_header,args.flat_header_sha256) run(args.java, fresh, "fresh") - far = [fresh/('world/'+kind+'/r.8.8.mca') for kind in ('region','entities')] + far = [fresh/('world/dimensions/minecraft/overworld/'+kind+'/r.8.8.mca') for kind in ('region','entities')] before = {path.relative_to(fresh).as_posix():digest(path) for path in far} run(args.java, fresh, "reload") if any(digest(path)!=before[path.relative_to(fresh).as_posix()] for path in far): diff --git a/tools/verify_upgrade_abort.py b/tools/verify_upgrade_abort.py index 2f011b1..30e6345 100644 --- a/tools/verify_upgrade_abort.py +++ b/tools/verify_upgrade_abort.py @@ -167,7 +167,7 @@ def inject_future_entity_version(world): before=digest(path);original=world.parent/'future-entity-original.mca' if original.exists():raise ValueError('Future fixture original already exists') shutil.copy2(path,original) - future_version = 4672 # Immediately beyond Minecraft 1.21.11's 4671. + future_version = 4791 # Immediately beyond Minecraft 26.1.2's 4790. root['DataVersion'] = Tag(3, future_version) write_disposable_region(path, chunks) return {'region':path.relative_to(world).as_posix(),'chunk_index':index,'source_version':2975, @@ -233,12 +233,18 @@ def main(): parser.add_argument('--forge-home', type=Path, required=True) parser.add_argument('--source-world', type=Path, required=True) parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--runtime-inputs',type=Path,help='Use one sealed local candidate and probe bundle') parser.add_argument('--conditions',nargs='+',choices=['damaged-index','blocked-journal','future-entity-version'], default=['damaged-index','blocked-journal']) args = parser.parse_args() project = Path(__file__).resolve().parents[1] - candidate = project / 'build/libs' / MAIN_JAR - probe = project / 'build/integration-test-mod/lootablebodies-build-tests.jar' + inputs=getattr(args,'runtime_inputs',None) + if inputs is None: + candidate = project / 'build/libs' / MAIN_JAR + probe = project / 'build/integration-test-mod/lootablebodies-build-tests.jar' + else: + from native_runtime import sealed_runtime_jars + candidate,probe=sealed_runtime_jars(inputs,project/'src/integrationTest/resources/log4j2.xml') source = args.source_world.resolve() sealed = fingerprint(source) root = args.output.resolve() diff --git a/tools/verify_world_upgrades.py b/tools/verify_world_upgrades.py index f5c4c4d..a7e0bc8 100644 --- a/tools/verify_world_upgrades.py +++ b/tools/verify_world_upgrades.py @@ -214,10 +214,12 @@ def main(): parser.add_argument('--' + origin, type=Path) parser.add_argument('--origins', nargs='+', choices=list(ORIGINS), default=list(ORIGINS)) parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--runtime-inputs',type=Path,help='Use one sealed local candidate and probe bundle') parser.add_argument('--seed-only', action='store_true') parser.add_argument('--seeds', nargs='+', type=Path) parser.add_argument('--prepared-sources', type=Path, help='Exact sealed47-origin paths; reuse original writer worlds') parser.add_argument('--variants', nargs='+', choices=['normal', 'stamps'], default=['normal', 'stamps']) + parser.add_argument('--workers', type=int, choices=(1,2), default=1) parser.add_argument('--control-report', type=Path) args = parser.parse_args() project = Path(__file__).resolve().parents[1] @@ -634,17 +636,34 @@ def bridge_writer_inventory(source): return gzip.compress(encode('',Tag(10,rows)),mtime=0) +def relocated_region(world, relative): + """Find the same native region after vanilla moves the three default dimensions.""" + old = Path(relative) + parent = old.parts[0] + if parent in ('DIM-1','DIM1'): + dimension = 'the_nether' if parent == 'DIM-1' else 'the_end' + old = Path(*old.parts[1:]) + else: + dimension = 'overworld' + return world/'dimensions/minecraft'/dimension/old + + def verify(args, project): prepared=getattr(args,'prepared_sources',None) if not args.seeds and not prepared: raise ValueError('--prepared-sources or --seeds is required for the upgrade matrix') if prepared and args.seeds:raise ValueError('Choose one unambiguous source route') if not args.forge_home: - raise ValueError('--forge-home is required for the native19 upgrade matrix') + raise ValueError('--forge-home is required for the native 26.1.2 upgrade matrix') root = args.output.resolve() root.mkdir(parents=True, exist_ok=False) - candidate = project / 'build/libs' / MAIN_JAR - probe = project / 'build/integration-test-mod/lootablebodies-build-tests.jar' + inputs=getattr(args,'runtime_inputs',None) + if inputs is None: + candidate = project / 'build/libs' / MAIN_JAR + probe = project / 'build/integration-test-mod/lootablebodies-build-tests.jar' + else: + from native_runtime import sealed_runtime_jars + candidate,probe=sealed_runtime_jars(inputs,project/'src/integrationTest/resources/log4j2.xml') candidate_hash = digest(candidate) probe_hash = digest(probe) sources = {};prepared_proof=None @@ -711,7 +730,7 @@ def run_origin(origin): for relative, original in distant.items(): unopened = (relative.endswith('r.8.8.mca') and phase.startswith('upgrade-near')) or ( relative.endswith('r.16.16.mca') and phase in PHASES[:6]) - if unopened and digest(world / relative) != original: + if unopened and digest(relocated_region(world,relative)) != original: raise RuntimeError('Unopened distant region was modified: ' + relative) if fingerprint(source) != details['world'] or digest(candidate) != candidate_hash or digest(probe) != probe_hash: raise RuntimeError('Sealed source/candidate changed') From 7a40601c055d71f410841602c4f631e87676cea8 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 11 Nov 2026 22:48:07 +0000 Subject: [PATCH 2/2] Keep Java 25 documentation archives reproducible --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 00ee573..1572022 100644 --- a/build.gradle +++ b/build.gradle @@ -233,8 +233,8 @@ ext.releaseJar = tasks.named('jar', Jar) // Javadoc writes the index and its bundled SVG icons with the host line // separator. Keep the documentation bytes identical on Windows and Linux. tasks.named('javadocJar', Jar) { - inputs.property('javadocLineEndings', 'crlf-index-and-icons') - filesMatching(['element-list', 'copy.svg', 'link.svg']) { + inputs.property('javadocLineEndings', 'crlf-index-and-java25-icons') + filesMatching(['element-list', '**/copy.svg', '**/link.svg', '**/left.svg', '**/right.svg']) { filter(FixCrLfFilter, eol: FixCrLfFilter.CrLf.newInstance('crlf'), eof: FixCrLfFilter.AddAsisRemove.newInstance('asis')) }