From 4fffb8e4a12ff85b0c5162da4dfd1553052b32a4 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 9 Nov 2026 11:55:49 +0000 Subject: [PATCH 1/2] Update Lootable Bodies for Forge 1.21.11 --- .github/release120.json | 18 +- .github/scripts/test_release_workflow.py | 87 ++++- .github/workflows/artifact-audit-1.21.yml | 71 ++++- .github/workflows/codeql-analysis.yml | 4 +- .github/workflows/deploy-release.yml | 52 +-- .github/workflows/native120.yml | 8 +- .github/workflows/release-workflow-checks.yml | 4 +- CHANGELOG.txt | 4 +- README.md | 6 +- RELEASING.md | 2 +- TESTING.md | 4 +- build.gradle | 6 +- gradle.properties | 8 +- gradle/assets.gradle | 10 +- gradle/audit.gradle | 28 +- gradle/ide.gradle | 28 +- gradle/release.gradle | 6 +- gradle/runtime.gradle | 9 +- settings.gradle | 2 +- .../java/buildcontrol/ForgeControl.java | 52 +-- .../java/buildcontrol/ResourceControl.java | 20 +- src/controlTest/resources/META-INF/mods.toml | 2 +- src/controlTest/resources/pack.mcmeta | 12 +- .../lootable/buildtest/ClientChecks.java | 32 +- .../buildtest/ConfigClientChecks.java | 33 +- .../buildtest/ConnectedClientChecks.java | 34 +- .../buildtest/ContainerInteractionChecks.java | 108 ++++--- .../lootable/buildtest/DropClientChecks.java | 66 +++- .../lootable/buildtest/EffectHurtChecks.java | 26 +- .../cyano/lootable/buildtest/FixtureApi.java | 96 +++++- .../buildtest/FixtureConnections.java | 4 +- .../lootable/buildtest/FixtureItems.java | 12 +- .../buildtest/FixtureProfileCache.java | 37 ++- .../lootable/buildtest/FixtureStackData.java | 2 +- .../buildtest/HealthAttributeChecks.java | 23 +- .../buildtest/InventorySafetyChecks.java | 181 +++++------ .../lootable/buildtest/MultiplayerChecks.java | 70 ++-- .../buildtest/MultiplayerClientChecks.java | 20 +- .../buildtest/NameLifetimeChecks.java | 48 +-- .../buildtest/NamelessCorpseClientChecks.java | 33 +- .../buildtest/Native118BiomeChecks.java | 44 +-- .../buildtest/NativePreflightChecks.java | 42 +-- .../lootable/buildtest/NbtClientChecks.java | 42 ++- .../lootable/buildtest/NbtRecoveryChecks.java | 38 +-- .../lootable/buildtest/NbtWorldChecks.java | 46 +-- .../buildtest/PortBaselineChecks.java | 44 ++- .../buildtest/PositionMotionChecks.java | 9 +- .../cyano/lootable/buildtest/ProbeFrames.java | 22 ++ .../lootable/buildtest/Recovery112Checks.java | 2 +- .../buildtest/ReportedIssueChecks.java | 133 +++++--- .../buildtest/ReportedIssueClientChecks.java | 91 ++++-- .../buildtest/RespawnPacketChecks.java | 29 +- .../buildtest/RespawnPacketClientChecks.java | 23 +- .../buildtest/RotationFallChecks.java | 26 +- .../lootable/buildtest/RuntimeChecks.java | 70 ++-- .../lootable/buildtest/TestRegistries.java | 5 +- .../lootable/buildtest/UpgradeChecks.java | 181 +++++++---- .../lootable/buildtest/VisibilityChecks.java | 41 +-- .../buildtest/VisibilityClientChecks.java | 60 ++-- .../buildtest/VisibilitySkinChecks.java | 15 +- .../buildtest/VisibilitySkinClientChecks.java | 7 +- .../lootable/buildtest/WallEscapeChecks.java | 13 +- .../buildtest/WorldUpgradeChecks.java | 249 ++++++++------- .../buildtest/mixin/ProbeFrameMixin.java | 36 +++ .../buildtest/mixin/ProbePreflightMixin.java | 19 ++ .../resources/META-INF/coremods.json | 3 - .../resources/META-INF/mods.toml | 4 +- .../resources/coremods/observe-preflight.js | 34 -- .../lootablebodies-build-tests.mixins.json | 9 + src/integrationTest/resources/pack.mcmeta | 12 +- src/main/java/cyano/lootable/ClientProxy.java | 18 +- .../java/cyano/lootable/LootableBodies.java | 63 ++-- .../lootable/entities/EntityLootableBody.java | 293 +++++++++-------- .../entities/SavedEffectIdentity.java | 14 +- .../events/PlayerDeathEventHandler.java | 51 +-- .../lootable/graphics/CorpseConfigGui.java | 20 +- .../lootable/graphics/CorpseContainer.java | 12 +- .../lootable/graphics/CorpseGUIContainer.java | 7 +- .../lootable/graphics/CorpseRenderer.java | 135 ++++---- .../lootable/mixin/CapabilityDataMixin.java | 20 ++ .../mixin/CorpseRenderDispatcherMixin.java | 33 ++ .../cyano/lootable/mixin/DataFixersMixin.java | 17 + .../lootable/mixin/DedicatedWorldMixin.java | 24 ++ .../lootable/mixin/LegacyStructuresMixin.java | 12 + .../lootable/mixin/RegionStorageMixin.java | 40 +++ .../cyano/lootable/mixin/SaveAccessMixin.java | 27 ++ .../network/ClientLoginReadGuard.java | 4 +- .../lootable/network/CorpseProfiles.java | 4 +- .../lootable/upgrade/ChunkVersionContext.java | 7 +- .../lootable/upgrade/CorpseIdentity.java | 11 +- .../upgrade/EarlyWorldPreservation.java | 28 +- .../cyano/lootable/upgrade/ItemMigration.java | 298 +++++++++++++----- .../upgrade/LegacyProfessionRegistry.java | 6 +- .../upgrade/LegacyRegistryMappings.java | 198 +++++++----- .../cyano/lootable/upgrade/NativeNbt.java | 68 ++++ .../lootable/upgrade/NativeSafeFixer.java | 276 ++++++++++------ .../lootable/upgrade/SavedCorpsePosition.java | 9 +- .../lootable/upgrade/TerrainChunkData.java | 22 +- .../lootable/upgrade/UpgradeJournal.java | 148 ++++----- .../lootable/upgrade/UpgradePreservation.java | 73 ++++- src/main/resources/META-INF/coremods.json | 3 - src/main/resources/META-INF/mods.toml | 6 +- .../coremods/early-world-preservation.js | 145 --------- src/main/resources/lootablebodies.mixins.json | 12 + src/main/resources/pack.mcmeta | 12 +- .../buildtest/HistoricalUuidTest.java | 8 +- .../buildtest/Native115PacketTest.java | 8 +- .../buildtest/Native117EntityInboxTest.java | 9 +- .../buildtest/Native117ForgeConfigTest.java | 8 +- .../buildtest/Native117MenuProtocolTest.java | 17 +- .../buildtest/Native119Registries.java | 14 +- .../buildtest/Native120FixtureGenerator.java | 18 +- .../lootable/buildtest/TypedNbtTest.java | 12 +- .../lootable/contracts/BuildContractTest.java | 11 +- .../lootable/contracts/ConfigScreenTest.java | 25 +- .../contracts/NativeUpgradeBoundaryTest.java | 63 ++-- .../contracts/ReleaseContractTest.java | 6 +- .../contracts/UpgradeJournalTest.java | 74 +++-- .../entities/Native118EffectIdentityTest.java | 9 +- .../entities/Native119DamageTest.java | 9 +- .../network/Native117ProfileLookupTest.java | 10 +- .../upgrade/EarlyWorldPreservationTest.java | 6 +- .../upgrade/HookRegistrationOrderTest.java | 2 +- .../upgrade/ItemNbtConversionTest.java | 32 +- .../upgrade/Native114RegistryRenameTest.java | 16 +- .../upgrade/Native114UpgradeTest.java | 38 ++- .../upgrade/Native115UpgradeTest.java | 34 +- .../upgrade/Native116DamagedStampTest.java | 24 +- .../upgrade/Native116RegistryRenameTest.java | 20 +- .../upgrade/Native116UpgradeTest.java | 28 +- .../upgrade/Native117EntityChunkTest.java | 19 +- .../upgrade/Native117EntityRegionTest.java | 16 +- .../upgrade/Native117ExternalSidecarTest.java | 4 +- .../upgrade/Native117RegistryRenameTest.java | 22 +- .../upgrade/Native117VanillaTest.java | 18 +- .../upgrade/Native118BiomeRegistryTest.java | 14 +- .../upgrade/Native118EarlyHookTest.java | 103 ++---- .../upgrade/Native118EntityBoundaryTest.java | 22 +- .../upgrade/Native118SavedPositionTest.java | 16 +- .../upgrade/Native118TerrainBoundaryTest.java | 51 +-- .../upgrade/Native118WorldgenTest.java | 14 +- .../upgrade/Native119CoremodEngineTest.java | 227 +++++++------ .../Native119RegistrySnapshotTest.java | 15 +- .../upgrade/Native1206ComponentsTest.java | 153 +++++---- .../upgrade/Native120RegistryRenameTest.java | 52 +-- .../lootable/upgrade/Native120TargetTest.java | 82 +++-- .../upgrade/Native12111LegacyChestTest.java | 35 ++ .../Native12111LegacyEntityItemNameTest.java | 84 +++++ .../lootable/buildtest/HistoricalUuid.java | 10 +- .../buildtest/NativeMenuObservation.java | 4 +- .../buildtest/NativeRespawnDescription.java | 2 +- .../lootable/buildtest/TestNativeNbt.java | 73 +++++ .../cyano/lootable/buildtest/TypedNbt.java | 15 +- .../upgrade/Native117RegionFixtures.java | 8 +- tools/fixtures/forge61-server-default.toml | 7 + tools/fixtures/native12111-flat-level.dat | Bin 0 -> 43354 bytes .../native12111-flat-level.provenance.json | 14 + .../native12111-forge-only-flat-level.dat | Bin 0 -> 43275 bytes ...2111-forge-only-flat-level.provenance.json | 13 + tools/hosted_policy.py | 9 +- tools/native120_build.py | 2 +- tools/native120_client.py | 21 +- tools/native120_client_logs.py | 10 +- tools/native120_logs.py | 4 +- tools/native120_persistence.py | 12 +- tools/native12111_server_inputs.json | 106 +++++++ tools/native12111_worker_budget.json | 11 + tools/native_evidence.py | 10 +- tools/native_runtime.py | 68 ++-- tools/qualification-policy.json | 4 +- tools/test_historical_runtime_budget.py | 2 +- tools/test_hosted_policy.py | 8 +- tools/test_native120_build.py | 2 +- tools/test_native120_client_contracts.py | 8 +- tools/test_native120_client_logs.py | 4 +- tools/test_native120_flat_header.py | 14 +- tools/test_native120_infrastructure.py | 18 +- tools/test_native120_launch.py | 14 +- tools/test_native120_persistence.py | 12 +- tools/test_native_evidence.py | 4 +- tools/test_native_server_preparation.py | 2 +- tools/verify_native_respawn_control.py | 12 +- tools/verify_normal_server.py | 5 +- tools/verify_upgrade_abort.py | 6 +- 184 files changed, 4133 insertions(+), 2562 deletions(-) create mode 100644 src/integrationTest/java/cyano/lootable/buildtest/ProbeFrames.java create mode 100644 src/integrationTest/java/cyano/lootable/buildtest/mixin/ProbeFrameMixin.java create mode 100644 src/integrationTest/java/cyano/lootable/buildtest/mixin/ProbePreflightMixin.java delete mode 100644 src/integrationTest/resources/META-INF/coremods.json delete mode 100644 src/integrationTest/resources/coremods/observe-preflight.js create mode 100644 src/integrationTest/resources/lootablebodies-build-tests.mixins.json create mode 100644 src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/CorpseRenderDispatcherMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/DataFixersMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/RegionStorageMixin.java create mode 100644 src/main/java/cyano/lootable/mixin/SaveAccessMixin.java delete mode 100644 src/main/resources/META-INF/coremods.json delete mode 100644 src/main/resources/coremods/early-world-preservation.js create mode 100644 src/main/resources/lootablebodies.mixins.json create mode 100644 src/test/java/cyano/lootable/upgrade/Native12111LegacyChestTest.java create mode 100644 src/test/java/cyano/lootable/upgrade/Native12111LegacyEntityItemNameTest.java create mode 100644 src/testSupport/java/cyano/lootable/buildtest/TestNativeNbt.java create mode 100644 tools/fixtures/forge61-server-default.toml create mode 100644 tools/fixtures/native12111-flat-level.dat create mode 100644 tools/fixtures/native12111-flat-level.provenance.json create mode 100644 tools/fixtures/native12111-forge-only-flat-level.dat create mode 100644 tools/fixtures/native12111-forge-only-flat-level.provenance.json create mode 100644 tools/native12111_server_inputs.json create mode 100644 tools/native12111_worker_budget.json diff --git a/.github/release120.json b/.github/release120.json index 30cbd45..8b62032 100644 --- a/.github/release120.json +++ b/.github/release120.json @@ -1,16 +1,16 @@ { - "version": "2.4.13.121011", - "minecraft": "1.21.1", - "forge": "52.1.20", + "version": "2.4.14.121111", + "minecraft": "1.21.11", + "forge": "61.2.4", "local_qualification_complete": true, "qualification_integrity_status": "verified", "manual_accepted": true, "files": { - "CHANGELOG.txt": "A7A09C8F1C6CEC52E4861B97AE8D4637C45054931F2D8D07ADC60F503E3561F3", - "LootableBodies-2.4.13.121011-javadoc.jar": "4DA01E1F17CA8D17F41DA5BE15C6513359B64598167A467096E6E5992AA1270B", - "LootableBodies-2.4.13.121011-sources.jar": "EAFC9F737A70846A99A96D625C38485ED82248D74E7D907D62FF02F63622BEDD", - "LootableBodies-2.4.13.121011.jar": "F24183DF61AB677017915CE0D0B17816D6D6A6C095BC6FBF6E40070E33DC1845", - "LootableBodies-2.4.13.121011.pom": "4FAC990CA9B74FB3AEB8B734EFBEE8461D84682C33ACCB95B8DD4A7E0443E272", - "SHA256SUMS": "147108294CEAA421090E75CFBDBC537E7F6E087DB116A1638783D65C89739486" + "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" } } diff --git a/.github/scripts/test_release_workflow.py b/.github/scripts/test_release_workflow.py index 05fb4fe..08ee8a3 100644 --- a/.github/scripts/test_release_workflow.py +++ b/.github/scripts/test_release_workflow.py @@ -59,7 +59,7 @@ def select(self, version, repository=REPOSITORY, ref='refs/heads/master-1.12'): values = dict(line.split('=', 1) for line in output.read_text().splitlines()) if output.exists() else {} return result.returncode, values - def validate(self, minecraft, names=None, mutation=None, release_exists=False, wrong_tag=False, audit_success=True, qualification=None, local_receipt=None): + def validate(self, minecraft, names=None, mutation=None, release_exists=False, wrong_tag=False, audit_success=True, qualification=None, local_receipt=None, audit_run=None): suffix, branch, forge = { '1.10.2': ('110021', '1.10.2', '12.18.3.2511'), '1.11.2': ('111021', '1.11.2-dev', '13.20.1.2588'), @@ -73,13 +73,14 @@ def validate(self, minecraft, names=None, mutation=None, release_exists=False, w '1.19.4': ('119041', 'master-1.19.4', '45.4.5'), '1.20.1': ('120011', 'master-1.20.1', '47.4.28'), '1.20.6': ('120061', 'master-1.20.6', '50.2.14'), - '1.21.1': ('121011', 'master-1.21.1', '52.1.20')}[minecraft] + '1.21.1': ('121011', 'master-1.21.1', '52.1.20'), + '1.21.11': ('121111', 'master-1.21.11', '61.2.4')}[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') else '17' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16' if minecraft=='1.17.1' else '8', java_toolchain_version='21.0.7+6' if minecraft in ('1.20.6','1.21.1') else '17.0.20.1+1' if minecraft in ('1.18.2','1.19.4','1.20.1') else '16.0.2+7' if minecraft=='1.17.1' else '8.0.502+7', gradle_java_version='21' if minecraft in ('1.20.6','1.21.1') else '17', mod_id='lootablebodies', + 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', mod_artifact='LootableBodies', mod_group='zone.moddev.mc.lootablebodies', mod_license='LGPL-2.1-only', curseforge_project_id='240968', mod_version=version) if mutation: @@ -89,19 +90,24 @@ 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') 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') 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'}[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'}[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]}) if '/actions/workflows/' in args[-1]: - return json.dumps({'workflow_runs': [{'id': 1, 'display_title': 'Audit ' + minecraft + ' artifacts from ' + SHA, - 'conclusion': 'success' if audit_success else 'failure'}]}) + 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'} + if audit_run is not None: + audit.update(audit_run) + return json.dumps({'workflow_runs': [audit]}) return json.dumps({'check_runs': [dict(name=name, id=i, conclusion='success') for i, name in enumerate(checks)]}) def existing(args, **options): if '/releases/tags/' in args[-1] and release_exists: @@ -112,7 +118,7 @@ def existing(args, **options): code = script('Validate identity, exact commit and successful checks').split("python3 - <<'PY'\n", 1)[1].rsplit('\nPY', 1)[0] with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, { 'RELEASE_VERSION': version, 'GITHUB_REPOSITORY': REPOSITORY, - 'MINECRAFT_VERSION': minecraft, 'SOURCE_BRANCH': branch, + 'MINECRAFT_VERSION': minecraft, 'SOURCE_BRANCH': branch, 'GITHUB_SHA': 'c' * 40, 'GITHUB_OUTPUT': str(Path(directory) / 'output'), 'LOCAL_QUALIFICATION':json.dumps(receipt) if receipt else ''}), \ patch('pathlib.Path.read_text', side_effect=['\n'.join(k+'='+v for k,v in values.items()),json.dumps({'files':files,'local_qualification_complete':True,'qualification_integrity_status':'verified','manual_accepted':True})]), \ @@ -129,8 +135,8 @@ def test_1206_requires_exact_archives_and_java_21(self): {'local_receipt': {'manual_accepted': False}}): with self.subTest(options=options), self.assertRaises(SystemExit): self.validate('1.20.6', **options) - self.assertIn("(needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1') && 'Java 21'", WORKFLOW.read_text()) - self.assertEqual(2, WORKFLOW.read_text().count("(needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1') && '21.0.7+6.0.LTS'")) + self.assertIn("(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'", WORKFLOW.read_text()) + self.assertEqual(2, WORKFLOW.read_text().count("(needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || needs.preflight.outputs.minecraft_version == '1.21.11') && '21.0.7+6.0.LTS'")) def test_1211_requires_exact_archives_and_java_21(self): code, values = self.select('2.4.13.121011') @@ -143,8 +149,65 @@ def test_1211_requires_exact_archives_and_java_21(self): with self.subTest(options=options), self.assertRaises(SystemExit): self.validate('1.21.1', **options) - def test_future_12111_candidate_cannot_dispatch_publication(self): - self.assertNotEqual(0, self.select('2.4.14.121111')[0]) + def test_1211_audit_receives_registered_archive_workflow_completion(self): + audit = WORKFLOW.with_name('artifact-audit-1.21.yml').read_text(encoding='utf-8') + event_names = audit.split('workflows: [', 1)[1].split(']', 1)[0].split(',') + self.assertIn('LootableBodies 1.20 CI', [name.strip() for name in event_names]) + + def test_1211_only_accepts_an_audit_from_the_active_dispatcher(self): + self.validate('1.21.1', audit_run={'event': 'workflow_dispatch'}) + for changes in ({'event': 'push'}, {'head_branch': 'master-1.21.1'}, + {'head_sha': 'd' * 40}, {'path': '.github/workflows/artifact-audit-1.20.yml'}): + with self.subTest(changes=changes), self.assertRaises(SystemExit): + self.validate('1.21.1', audit_run=changes) + + def test_1211_audit_requires_the_successful_current_source_push(self): + path = WORKFLOW.with_name('artifact-audit-1.21.yml') + text = path.read_text(encoding='utf-8') + code = text.split("python3 - <<'PY'\n", 1)[1].split('\n PY', 1)[0] + code = textwrap.dedent(code) + good = {'id': 25, 'head_repository': {'full_name': REPOSITORY}, 'event': 'push', + 'head_branch': 'master-1.21.1', 'head_sha': SHA, 'path': '.github/workflows/native120.yml', + 'status': 'completed', 'conclusion': 'success'} + + def resolve(run, branch=SHA, run_id='25', source=SHA, target='1.21.1'): + def api(args): + if args[-1].endswith('/actions/runs/25'): + return json.dumps(run) + if args[-1].endswith('/git/ref/heads/master-' + target): + return json.dumps({'object': {'sha': branch}}) + raise AssertionError(args) + with tempfile.TemporaryDirectory() as folder, patch('subprocess.check_output', side_effect=api): + output = Path(folder) / 'output' + with patch.dict(os.environ, SOURCE_RUN=run_id, SOURCE_SHA=source, TARGET_VERSION=target, GITHUB_OUTPUT=str(output)): + exec(compile(code, 'audit-source', 'exec'), {}) + return output.read_text() + + self.assertEqual('run=25\nsha=' + SHA + '\nversion=1.21.1\n', resolve(good)) + self.assertEqual('run=25\nsha=' + SHA + '\nversion=1.21.11\n', + resolve(dict(good, head_branch='master-1.21.11'), target='1.21.11')) + with self.assertRaises(SystemExit): + resolve(good, target='1.21.11') + for changes in ({'event': 'pull_request'}, {'head_sha': 'b' * 40}, {'head_branch': 'master-1.20.6'}, + {'head_repository': {'full_name': 'SkyBlade1978/LootableBodies'}}, + {'path': '.github/workflows/native119.yml'}, {'conclusion': 'failure'}, {'id': 26}): + with self.subTest(changes=changes), self.assertRaises(SystemExit): + resolve(dict(good, **changes)) + with self.assertRaises(SystemExit): + resolve(good, branch='b' * 40) + with self.assertRaises(SystemExit): + resolve(good, run_id='25;false') + + def test_12111_requires_the_exact_archive_and_dispatcher(self): + self.assertEqual((0, {'minecraft_version': '1.21.11', 'source_branch': 'master-1.21.11'}), + self.select('2.4.14.121111')) + self.validate('1.21.11', audit_run={'event': 'workflow_dispatch'}) + for options in ({'audit_success': False}, {'qualification': {'head_branch': 'master-1.21.1'}}, + {'audit_run': {'head_sha': 'd' * 40}}, {'mutation': {'java_version': '17'}}, + {'local_receipt': {'manual_accepted': False}}): + with self.subTest(options=options), self.assertRaises(SystemExit): + self.validate('1.21.11', **options) + self.assertNotEqual(0, self.select('2.4.15.261021')[0]) def test_routes_all_supported_versions(self): for suffix, minecraft, branch in [('110021', '1.10.2', '1.10.2'), ('111021', '1.11.2', '1.11.2-dev'), ('112021', '1.12.2', 'master-1.12'), ('113021', '1.13.2', 'master-1.13.2'), ('114041', '1.14.4', 'master-1.14.4')]: diff --git a/.github/workflows/artifact-audit-1.21.yml b/.github/workflows/artifact-audit-1.21.yml index 6835f5c..e5593a0 100644 --- a/.github/workflows/artifact-audit-1.21.yml +++ b/.github/workflows/artifact-audit-1.21.yml @@ -1,9 +1,24 @@ name: Audit LootableBodies 1.21 artifacts -run-name: Audit 1.21.1 artifacts from ${{ github.event.workflow_run.head_sha }} +run-name: Audit ${{ github.event.workflow_run.head_branch == 'master-1.21.11' && '1.21.11' || inputs.minecraft_version || '1.21.1' }} artifacts from ${{ github.event.workflow_run.head_sha || inputs.source_sha }} on: workflow_run: - workflows: [LootableBodies 1.21.1 CI] + workflows: [LootableBodies 1.20 CI, LootableBodies 1.20.6 CI, LootableBodies 1.21.1 CI, LootableBodies 1.21.11 CI] types: [completed] + workflow_dispatch: + inputs: + minecraft_version: + description: Minecraft version of the merged archive build + type: choice + default: '1.21.1' + options: ['1.21.1', '1.21.11'] + 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 @@ -11,23 +26,61 @@ jobs: audit: if: >- github.repository == 'MinecraftModDevelopmentMods/LootableBodies' && - github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && - github.event.workflow_run.head_branch == 'master-1.21.1' && github.event.workflow_run.name == 'LootableBodies 1.21.1 CI' + 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-1.21.1' && github.event.workflow_run.name == 'LootableBodies 1.21.1 CI') || + (github.event.workflow_run.head_branch == 'master-1.21.11' && github.event.workflow_run.name == 'LootableBodies 1.21.11 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: ${{ github.event.workflow_run.head_branch == 'master-1.21.11' && '1.21.11' || inputs.minecraft_version || '1.21.1' }} + 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', '1.21.1') + if target not in ('1.21.1', '1.21.11'): + 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: ${{ github.event.workflow_run.head_sha }} + ref: ${{ steps.source.outputs.sha }} + persist-credentials: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: LootableBodies-1.21.1-${{ github.event.workflow_run.head_sha }} - run-id: ${{ github.event.workflow_run.id }} + 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: ${{ github.event.workflow_run.head_sha }} + SOURCE_SHA: ${{ steps.source.outputs.sha }} run: | python3 -B tools/native120_build.py --bundle audited python3 - <<'PY' @@ -43,6 +96,6 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: native120-independent-audit-${{ github.event.workflow_run.head_sha }} + 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 614f249..0fa98ea 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.1 + - master-1.21.11 - 'feature/**' pull_request: branches: - - master-1.21.1 + - master-1.21.11 schedule: - cron: '43 7 * * 4' diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index f6317e6..509dc98 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 or 120061. + description: Complete version from gradle.properties, ending in 110021, 111021, 112021, 113021, 114041, 115021, 116051, 117011, 118021, 119041, 120011, 120061, 121011 or 121111. 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 and 1.21.1 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 and 1.21.11 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)$ ]] + [[ "$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)$ ]] 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 ;; @@ -69,6 +69,7 @@ jobs: *.120011) minecraft_version=1.20.1; source_branch=master-1.20.1 ;; *.120061) minecraft_version=1.20.6; source_branch=master-1.20.6 ;; *.121011) minecraft_version=1.21.1; source_branch=master-1.21.1 ;; + *.121111) minecraft_version=1.21.11; source_branch=master-1.21.11 ;; *) exit 1 ;; esac echo "minecraft_version=$minecraft_version" >> "$GITHUB_OUTPUT" @@ -106,16 +107,17 @@ jobs: '1.19.4': ('master-1.19.4', '45.4.5', '119041'), '1.20.1': ('master-1.20.1', '47.4.28', '120011'), '1.20.6': ('master-1.20.6', '50.2.14', '120061'), - '1.21.1': ('master-1.21.1', '52.1.20', '121011')} + '1.21.1': ('master-1.21.1', '52.1.20', '121011'), + '1.21.11': ('master-1.21.11', '61.2.4', '121111')} 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') + archive_only = minecraft in ('1.18.2', '1.19.4', '1.20.1', '1.20.6', '1.21.1', '1.21.11') 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') else '17' if archive_only else '16' if native17 else '8', - 'java_toolchain_version': '21.0.7+6' if minecraft in ('1.20.6', '1.21.1') else '17.0.20.1+1' if archive_only else '16.0.2+7' if native17 else '8.0.502+7', 'gradle_java_version': '21' if minecraft in ('1.20.6', '1.21.1') else '17', + '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', 'mod_id': 'lootablebodies', 'mod_artifact': 'LootableBodies', 'mod_group': 'zone.moddev.mc.lootablebodies', 'mod_license': 'LGPL-2.1-only', 'curseforge_project_id': '240968'} @@ -155,7 +157,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'}[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'}[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'): @@ -179,11 +181,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'): + 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'): target = minecraft.rsplit('.', 1)[0] + event_query = '' if minecraft in ('1.21.1', '1.21.11') else '&event=workflow_run' audits = json.loads(subprocess.check_output(['gh', 'api', - f'repos/{repo}/actions/workflows/artifact-audit-{target}.yml/runs?event=workflow_run&per_page=100'], text=True))['workflow_runs'] - matching = [run for run in audits if run['display_title'] == f'Audit {minecraft} artifacts from {sha}'] + 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 run['head_branch'] == 'master-1.12' and run['head_sha'] == os.environ['GITHUB_SHA'] + and run['path'] == '.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) @@ -213,25 +219,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.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' + 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' 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.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' + 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' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} - name: Install Gradle Java - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' + 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' uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: ${{ needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} - name: Pin the production toolchain - if: needs.preflight.outputs.minecraft_version != '1.18.2' && needs.preflight.outputs.minecraft_version != '1.19.4' && needs.preflight.outputs.minecraft_version != '1.20.1' && needs.preflight.outputs.minecraft_version != '1.20.6' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' + 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' 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" @@ -242,7 +248,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.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' && needs.preflight.outputs.minecraft_version != '1.21.1' + 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' uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 - name: Download the qualified native117 six-file bundle if: needs.preflight.outputs.minecraft_version == '1.17.1' @@ -287,7 +293,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.1' + 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' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: LootableBodies-${{ needs.preflight.outputs.minecraft_version }}-${{ needs.preflight.outputs.release_sha }} @@ -295,10 +301,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.1' + 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' 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' + 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' run: | chmod +x ./gradlew ./gradlew clean build javadoc verifyEclipseProductionClasspath prepareReleaseBundle \ @@ -403,15 +409,15 @@ 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') && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.17.1' && '16.0.2+7' || '8.0.502+7' }} + java-version: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || 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') && '21.0.7+6.0.LTS' || needs.preflight.outputs.minecraft_version == '1.18.2' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.19.4' && '17.0.20+101' || needs.preflight.outputs.minecraft_version == '1.20.1' && '17.0.20+101' || '17.0.1+12' }} + java-version: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || 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' ]]; then + 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 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" @@ -471,7 +477,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') && 'Java 21' || needs.preflight.outputs.minecraft_version == '1.19.4' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.20.1' && 'Java 17' || needs.preflight.outputs.minecraft_version == '1.17.1' && 'Java 16' || 'Java 8' }} + java: ${{ (needs.preflight.outputs.minecraft_version == '1.20.6' || needs.preflight.outputs.minecraft_version == '1.21.1' || 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 62d4840..c47d825 100644 --- a/.github/workflows/native120.yml +++ b/.github/workflows/native120.yml @@ -1,10 +1,10 @@ -name: LootableBodies 1.21.1 CI +name: LootableBodies 1.21.11 CI on: push: - branches: [master-1.21.1] + branches: [master-1.21.11] pull_request: - branches: [master-1.21.1] + branches: [master-1.21.11] permissions: contents: read @@ -63,7 +63,7 @@ jobs: - name: Retain the exact six release files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: LootableBodies-1.21.1-${{ github.sha }} + name: LootableBodies-1.21.11-${{ 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 3d45efd..5acb79f 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.1'] + branches: ['feature/**', 'master-1.21.11'] pull_request: - branches: ['master-1.21.1'] + branches: ['master-1.21.11'] permissions: contents: read jobs: diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c019ce5..a403299 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,6 @@ -2.4.13.121011 - Minecraft 1.21.1, Forge +2.4.14.121111 - Minecraft 1.21.11, Forge -Updated for Minecraft 1.21.1. +Updated for Minecraft 1.21.11. 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 5c3aef1..8f136c2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lootable Bodies for Minecraft 1.21.1 +# Lootable Bodies for Minecraft 1.21.11 Your items stay on a corpse when you die. Right-click the body to recover them. -Use Forge 52.1.20 and Java 21. Install `LootableBodies-2.4.13.121011.jar` +Use Forge 61.2.4 and Java 21. Install `LootableBodies-2.4.14.121111.jar` in the client and server mods folders. Back up your world before upgrading. Created by Cyanobacterium (DrCyano), maintained by SkyBlade1978. @@ -12,7 +12,7 @@ Open Mods, select Lootable Bodies and click Config to change its settings. Appearance changes apply straight away on your client. World and server rules take effect after restarting Minecraft or the server. -Existing 1.10.2, 1.11.2, 1.12.2, 1.13.2, 1.14.4, 1.15.2, 1.16.5, 1.17.1, 1.18.2, 1.19.4, 1.20.1 and 1.20.6 corpses are preserved when upgrading, +Existing 1.10.2, 1.11.2, 1.12.2, 1.13.2, 1.14.4, 1.15.2, 1.16.5, 1.17.1, 1.18.2, 1.19.4, 1.20.1, 1.20.6 and 1.21.1 corpses are preserved when upgrading, including bodies in chunks you visit later. Keep the whole world backup, including its `data` folder. If preservation cannot finish safely, world loading stops with an explanation in the log. Items from missing mods are retained for diff --git a/RELEASING.md b/RELEASING.md index b6a7060..cd03b61 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 and 1.21.1 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 and 1.21.11 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 ee7e300..fa21e2c 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,6 +1,6 @@ # Testing -Run qualification for Forge 1.21.1 locally with Forge 52.1.20 and Java 21. +Run qualification for Forge 1.21.11 locally with Forge 61.2.4 and Java 21. Qualification runs locally on this branch. During development, reproduce the changed behaviour first, then batch the cheap @@ -32,7 +32,7 @@ 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/native1206-forge-only-flat-level.dat` +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. Changes confined to a test fixture require the affected proof and tool contracts, not an automatic replay of unrelated diff --git a/build.gradle b/build.gradle index 8f2e5e1..a2f1fe4 100644 --- a/build.gradle +++ b/build.gradle @@ -82,6 +82,7 @@ tasks.withType(JavaCompile).configureEach { tasks.named('compileTestJava', JavaCompile) { options.compilerArgs.add('-proc:none') } tasks.withType(Test).configureEach { useJUnitPlatform() + maxHeapSize = "1536m" javaLauncher = productionLauncher workingDir = projectDir } @@ -101,7 +102,7 @@ tasks.register('generateNative120Fixtures', JavaExec) { classpath = sourceSets.test.runtimeClasspath mainClass = 'cyano.lootable.buildtest.Native120FixtureGenerator' args providers.gradleProperty('nativeFixtureOutput').getOrElse('build/native121-fixture-preparation-01'), - file('tools/fixtures/native121-flat-level.dat').absolutePath + file('tools/fixtures/native12111-flat-level.dat').absolutePath } tasks.withType(JavaExec).configureEach { javaLauncher = productionLauncher } tasks.withType(Javadoc).configureEach { @@ -150,6 +151,7 @@ minecraft { providers.gradleProperty('gameRunDirectory').getOrElse('run')) jvmArgs '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED' systemProperty 'log4j.configurationFile', file('gradle/log4j2.xml').toURI().toString() + args '--mixin.config=lootablebodies.mixins.json' mods { lootablebodies { source sourceSets.main } } } register('client') @@ -229,7 +231,7 @@ tasks.named('jar', Jar) { 'Implementation-Version': project.version, 'Implementation-Vendor': project.mod_vendor, 'Maven-Artifact': "${project.group}:${base.archivesName.get()}:${project.version}", 'Implementation-License': project.mod_license, - 'Built-On-Java': '21', 'Built-On': "${minecraft_version}-${forge_version}") + 'Built-On-Java': '21', 'Built-On': "${minecraft_version}-${forge_version}", 'MixinConfigs': 'lootablebodies.mixins.json') filesMatching(archiveTextPatterns, normalizeArchiveText) } // Forge 52.1.20 ships its runtime with official names. diff --git a/gradle.properties b/gradle.properties index c3f385c..05e8d75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,10 +5,10 @@ org.gradle.caching=true org.gradle.parallel=false org.gradle.workers.max=2 net.minecraftforge.gradle.merge-source-sets=false -minecraft_version=1.21.1 -forge_version=52.1.20 +minecraft_version=1.21.11 +forge_version=61.2.4 mapping_channel=official -mapping_version=1.21.1 +mapping_version=1.21.11 # Release identity and dispatcher target metadata. loader_name=forge @@ -25,7 +25,7 @@ mod_vendor=Cyanobacterium mod_license=LGPL-2.1-only mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=2.4.13.121011 +mod_version=2.4.14.121111 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 fc2aa3e..1ea547f 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 = '17' +ext.minecraftAssetIndex = '29' // 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/native121-client-inputs/natives'))) + .getOrElse(localMinecraftSetup.getProperty('minecraftNativesDirectory','.gradle/native12111-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.1.json') - download('https://piston-meta.mojang.com/v1/packages/82ba6501da174a665c1787775f946487422f2432/1.21.1.json','82ba6501da174a665c1787775f946487422f2432',descriptor) + File descriptor=new File(root,'1.21.11.json') + download('https://piston-meta.mojang.com/v1/packages/35f8a9bc4874e2c95e51b3ad21e043a33776f7c2/1.21.11.json','35f8a9bc4874e2c95e51b3ad21e043a33776f7c2',descriptor) def version=new groovy.json.JsonSlurper().parse(descriptor) - if(version.id!='1.21.1' || version.assetIndex.id!=minecraftAssetIndex) + if(version.id!='1.21.11' || 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 2305cc9..602cf3a 100644 --- a/gradle/audit.gradle +++ b/gradle/audit.gradle @@ -55,29 +55,31 @@ ext.auditReleaseArchives = { File directory -> throw new GradleException("Incorrect release manifest in ${name}") if (index == 0) { ['META-INF/mods.toml', 'pack.mcmeta', 'cyano/lootable/LootableBodies.class', - 'META-INF/coremods.json', 'coremods/early-world-preservation.js', + '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/CapabilityDataMixin.class', 'cyano/lootable/mixin/CorpseRenderDispatcherMixin.class', 'cyano/lootable/upgrade/EarlyWorldPreservation.class', 'assets/lootablebodies/textures/gui/corpse.png', 'assets/lootablebodies/textures/entity/corpse/corpse.png'].each { 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.1]"'].each { + ['modId="lootablebodies"',"version=\"${project.version}\"",'authors="Cyanobacterium, SkyBlade1978"','modLoader="javafml"','versionRange="[1.21.11]"'].each { if(!info.contains(it))throw new GradleException("Incorrect production metadata: ${it}") } def pack=new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('pack.mcmeta')).getText('UTF-8')).pack - if(pack.pack_format!=34 || pack.supported_formats!=[min_inclusive:34,max_inclusive:48] || pack.containsKey('forge:server_data_pack_format') || + 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') || pack.containsKey('forge:resource_pack_format') || pack.containsKey('forge:data_pack_format')) - throw new GradleException('Native pack formats must be resource34/server-data48') - def coremods=new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('META-INF/coremods.json')).getText('UTF-8')) - if(coremods!=[lootablebodies_preservation:'coremods/early-world-preservation.js']) - throw new GradleException('Missing or changed early native save attachment') - String hooks=zip.getInputStream(zip.getEntry('coremods/early-world-preservation.js')).getText('UTF-8') - if (!hooks.contains('lootablebodies_fixer') || !hooks.contains('getDataFixer') || - manifest.getValue('FMLAT')!=null) - throw new GradleException('Missing or changed native data fixer attachment') - if (!hooks.contains('lootablebodies_dedicated_read') || !hooks.contains('readDedicatedWorld')) - throw new GradleException('Missing dedicated world refusal guard') + throw new GradleException('Forge mod pack metadata must use data format 94.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.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('/') }) + throw new GradleException('Missing or changed native world preservation mixins') def languages=names.findAll {it.startsWith('assets/lootablebodies/lang/') && !it.endsWith('/')} if(languages.size()!=14 || languages.any {!it.endsWith('.json') || it!=it.toLowerCase(Locale.ROOT)}) throw new GradleException('Incorrect native language resources') diff --git a/gradle/ide.gradle b/gradle/ide.gradle index b557349..23e5bd8 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -58,6 +58,10 @@ tasks.register('configureEclipseBuildship') { localJavaSetup.withOutputStream { toolchains.store(it, 'Local production Java toolchain') } } } +def eclipseRunLibraries = { String run -> + (tasks.named(run, JavaExec).get().classpath - sourceSets.main.output).files +} + tasks.register('isolateEclipseProductionRuns') { group = 'ide' dependsOn 'prepareEclipseResources', 'configureEclipseBuildship', 'prepareMinecraftAssets', 'prepareProductionRunDirectory' @@ -67,7 +71,7 @@ tasks.register('isolateEclipseProductionRuns') { if (!launch.isFile()) throw new GradleException("Generate ${name} with genEclipseRuns first") String contents = launch.getText('UTF-8') File nativeRoot=minecraftNatives.asFile - if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native 1.21.1 client inputs before generating Eclipse launches') + if(!nativeRoot.isDirectory())throw new GradleException('Prepare the official native 1.21.11 client inputs before generating Eclipse launches') contents=contents.replace('{asset_index}',minecraftAssetIndex) .replace('{assets_root}',minecraftAssets.asFile.absolutePath) .replace('{natives}',nativeRoot.absolutePath) @@ -87,7 +91,20 @@ tasks.register('isolateEclipseProductionRuns') { } } } - launch.setText(contents.replace('\r\n', '\n'), 'UTF-8') + // Buildship combines compile and runtime dependencies. Forge's runtime + // already embeds MixinExtras, so the compile-only common jar clashes. + def model = new groovy.xml.XmlParser().parseText(contents) + def classpath = model.listAttribute.find { it.@key == 'org.eclipse.jdt.launching.CLASSPATH' } + classpath.listEntry.findAll { entry -> + def memento = new groovy.xml.XmlParser().parseText(entry.@value) + !memento.@projectName && !memento.@containerPath?.startsWith('org.eclipse.jdt.launching.JRE_CONTAINER') + }.each { classpath.remove(it) } + eclipseRunLibraries(name - '.launch').each { library -> + def memento = new groovy.util.Node(null, 'runtimeClasspathEntry', + [externalArchive: library.absolutePath, type: '2', path: '5']) + classpath.appendNode('listEntry', [value: groovy.xml.XmlUtil.serialize(memento).trim()]) + } + launch.setText(groovy.xml.XmlUtil.serialize(model), 'UTF-8') } } } @@ -138,7 +155,7 @@ tasks.register('verifyEclipseProductionClasspath') { !arguments.contains(metadataJar) || (side == 'client' && !arguments.contains("--assetIndex ${minecraftAssetIndex}")) || xml.contains('{asset_index}') || xml.contains('{assets_root}') || xml.contains('{natives}')) throw new GradleException("Eclipse Forge metadata, target or client assets are unresolved for ${name}") - // Forge 52 discovers the processed mods.toml directly on the + // Forge discovers the processed mods.toml directly on the // project classpath. The launcher no longer reads MOD_CLASSES. def entries = launchModel.listAttribute.find { it.@key.text() == 'org.eclipse.jdt.launching.CLASSPATH' @@ -147,6 +164,11 @@ tasks.register('verifyEclipseProductionClasspath') { !entries.any { it.@containerPath.text().endsWith('/JavaSE-21') } || launchModel.mapAttribute.mapEntry.any { it.@key.text() == 'MOD_CLASSES' }) throw new GradleException("${name} must launch the production Eclipse project on Java 21") + 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 } || + libraries.any { it.name.startsWith('mixinextras-common-') }) + throw new GradleException("${name} must use the Forge runtime classpath without compile-only MixinExtras") String directory = launchModel.stringAttribute.find { it.@key.text() == 'org.eclipse.jdt.launching.WORKING_DIRECTORY' }.@value.text() diff --git a/gradle/release.gradle b/gradle/release.gradle index db16061..f957a46 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -5,8 +5,8 @@ def expectedIdentity = [ mod_id: 'lootablebodies', mod_artifact: 'LootableBodies', mod_group: 'zone.moddev.mc.lootablebodies', mod_vendor: 'Cyanobacterium', mod_name: "DrCyano's Lootable Bodies", mod_authors: 'Cyanobacterium,SkyBlade1978', - minecraft_version: '1.21.1', forge_version: '52.1.20', - mapping_channel: 'official', mapping_version: '1.21.1', + minecraft_version: '1.21.11', forge_version: '61.2.4', + mapping_channel: 'official', mapping_version: '1.21.11', loader_name: 'forge', loader_code: '1', curseforge_project_id: '240968', java_version: '21', java_toolchain_version: '21.0.7+6', gradle_java_version: '21', mod_license: 'LGPL-2.1-only', mod_license_name: 'GNU Lesser General Public License, Version 2.1', @@ -37,7 +37,7 @@ tasks.register('verifyReleaseConfiguration') { doLast { String source = file('src/main/java/cyano/lootable/LootableBodies.java').getText('UTF-8') if (!source.contains("VERSION = \"${project.version}\"") || !source.contains('MODID = "lootablebodies"') || - !source.contains('@Mod(LootableBodies.MODID)') || !source.contains('ResourceLocation.fromNamespaceAndPath(MODID,"corpse")')) + !source.contains('@Mod(LootableBodies.MODID)') || !source.contains('Identifier.fromNamespaceAndPath(MODID,"corpse")')) throw new GradleException('Java release version or persistent identity differs from release metadata') if (!file('README.md').getText('UTF-8').contains(releaseNames[0]) || !file('CHANGELOG.txt').getText('UTF-8').contains(project.version.toString())) diff --git a/gradle/runtime.gradle b/gradle/runtime.gradle index 6b6de69..7437cbe 100644 --- a/gradle/runtime.gradle +++ b/gradle/runtime.gradle @@ -18,6 +18,7 @@ def probeJar = tasks.register('integrationTestModJar', Jar) { group = 'verification' dependsOn 'compileIntegrationTestMod' archiveFileName = 'lootablebodies-build-tests.jar' + manifest.attributes('MixinConfigs': 'lootablebodies-build-tests.mixins.json') destinationDirectory = layout.buildDirectory.dir('integration-test-mod') from probeClasses from 'src/integrationTest/resources' @@ -70,10 +71,10 @@ def prepareOwnedLoaderConfig = { File root -> } } -// Use an actual Forge 52 codec header in each newly owned flat fixture world. +// Use an actual Forge 61 codec header in each newly owned flat fixture world. def prepareOwnedFlatWorld = { File root -> - File header = file('tools/fixtures/native121-flat-level.dat') - if (!header.isFile() || releaseDigest(header) != 'C85D57B04D1309304A2B28992E5B45B4E695039BE162682255A0CCCCB3EE30CD') + File header = file('tools/fixtures/native12111-flat-level.dat') + if (!header.isFile() || releaseDigest(header) != '33DBE405659FB4EF57D23A07485EBC9AFA57694D7B54357C5B534A3E8963F86F') throw new GradleException('Missing or changed native flat fixture header') File world = new File(root,'world') if (world.exists()) throw new GradleException('Refusing to replace an existing fixture world') @@ -108,7 +109,7 @@ spawn-animals=false spawn-monsters=false spawn-protection=0 max-tick-time=120000 -''' +''' + (System.getProperty('os.name').startsWith('Windows') ? 'use-native-transport=false\n' : '') } } diff --git a/settings.gradle b/settings.gradle index f478699..7d0fb6c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ plugins { } rootProject.name = 'LootableBodies' -// Local qualification and manual testing come before hosted checks. +// The MMD repository builds and audits release archives. 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 51332f4..083772c 100644 --- a/src/controlTest/java/buildcontrol/ForgeControl.java +++ b/src/controlTest/java/buildcontrol/ForgeControl.java @@ -1,21 +1,30 @@ package buildcontrol; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLEnvironment; +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; /** Independent native Forge control, without a Lootable Bodies dependency. */ @Mod("forgeplatformcontrol") public final class ForgeControl { - public ForgeControl() { - net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); - net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus().addListener(this::loadComplete); - net.minecraftforge.fml.DistExecutor.runWhenOn(net.minecraftforge.api.distmarker.Dist.CLIENT,()->()->net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(new ClientControl())); + public ForgeControl(FMLJavaModLoadingContext context) { + ServerStartedEvent.BUS.addListener(this::started); + TickEvent.ServerTickEvent.Post.BUS.addListener(this::deathTick); + FMLLoadCompleteEvent.getBus(context.getModBusGroup()).addListener(this::loadComplete); + if (FMLEnvironment.dist == Dist.CLIENT) TickEvent.ClientTickEvent.Post.BUS.addListener(new ClientControl()::tick); } private void loadComplete(net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent event) { int rounds=Integer.getInteger("lootablebodies.control.resourceReloads",0); if(rounds>0)event.enqueueWork(()->ResourceControl.run(rounds)); } - @SubscribeEvent public void started(ServerStartedEvent event) { + 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.loading.FMLEnvironment.dist==net.minecraftforge.api.distmarker.Dist.DEDICATED_SERVER) { if(Boolean.getBoolean("lootablebodies.control.tcpServer"))System.out.println("FORGE_PLATFORM_CONTROL_READY"); @@ -23,16 +32,18 @@ private void loadComplete(net.minecraftforge.fml.event.lifecycle.FMLLoadComplete } } private int checkTicks,checkDeaths; - @SubscribeEvent public void deathTick(TickEvent.ServerTickEvent event) { - if(event.phase!=TickEvent.Phase.END || !Boolean.getBoolean("lootablebodies.control.deaths"))return; + public void deathTick(TickEvent.ServerTickEvent.Post event) { + if(!Boolean.getBoolean("lootablebodies.control.deaths"))return; net.minecraft.server.MinecraftServer server=net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(); net.minecraft.server.level.ServerPlayer p=server.getPlayerList().getPlayerByName("ControlOnly"); - if(p==null || !p.isAlive())return; + if (p == null || !p.isAlive() || !p.connection.hasClientLoaded() || checkDeaths == 8) return; if(++checkTicks==1) { p.setGameMode(net.minecraft.world.level.GameType.SURVIVAL); double x=checkDeaths%2==0?96.5:-.4,z=checkDeaths%2==0?96.5:-.4; for(int a=-6;a<=6;a++)for(int b=-6;b<=6;b++)p.level().setBlockAndUpdate(net.minecraft.core.BlockPos.containing(x+a,80,z+b),net.minecraft.world.level.block.Blocks.OBSIDIAN.defaultBlockState()); - p.setRespawnPosition(p.level().dimension(),new net.minecraft.core.BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),0F,true,false); + var spawn = LevelData.RespawnData.of(p.level().dimension(), + new net.minecraft.core.BlockPos((int)Math.floor(x), 80, (int)Math.floor(z)-3), 0F, 0F); + p.setRespawnPosition(new ServerPlayer.RespawnConfig(spawn, true), false); p.connection.teleport(x,81,z,0,0); } if(checkTicks==50){ @@ -42,12 +53,15 @@ private void loadComplete(net.minecraftforge.fml.event.lifecycle.FMLLoadComplete p.level().removeBlock(new net.minecraft.core.BlockPos(px+ax,79,pz+az),false); p.level().removeBlock(new net.minecraft.core.BlockPos(px+ax,80,pz+az),false); } - p.kill();checkTicks=0;System.out.println("NATIVE_DEATH "+(++checkDeaths));} + p.kill(p.level()); + if (p.isAlive()) throw new AssertionError("Native Forge control kill did not take effect"); + checkTicks = 0; + System.out.println("NATIVE_DEATH " + (++checkDeaths)); + } } public static final class ClientControl { private int stage,ticks; - @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) { - if(event.phase!=TickEvent.Phase.END)return; + public void tick(TickEvent.ClientTickEvent.Post event) { net.minecraft.client.Minecraft mc=net.minecraft.client.Minecraft.getInstance(); if(Integer.getInteger("lootablebodies.control.port",0)>0) {tcp(mc);return;} if(stage==0 && mc.screen instanceof net.minecraft.client.gui.screens.TitleScreen) { @@ -61,7 +75,7 @@ public static final class ClientControl { }catch(java.io.IOException failure){throw new IllegalStateException(failure);} stage=4; }else if(stage==4 && ++ticks>=80) { - mc.level.disconnect();mc.disconnect();stage=2; + mc.disconnectFromWorld(Component.empty());stage=2; }else if(stage==2 && mc.level==null && !mc.hasSingleplayerServer())finish(mc); } private int connections,wait,deathTicks,respawns; @@ -110,7 +124,7 @@ private void tcp(net.minecraft.client.Minecraft mc) { destination=new net.minecraft.client.multiplayer.ServerData("Owned native control","127.0.0.1:"+Integer.getInteger("lootablebodies.control.port"),net.minecraft.client.multiplayer.ServerData.Type.OTHER); if(Boolean.getBoolean("lootablebodies.control.ping")) { pinger=new net.minecraft.client.multiplayer.ServerStatusPinger(); - try{pinger.pingServer(destination,()->{},()->{});}catch(java.net.UnknownHostException failure){throw new IllegalStateException(failure);} + try{pinger.pingServer(destination, () -> {}, () -> {}, EventLoopGroupHolder.remote(false));}catch(java.net.UnknownHostException failure){throw new IllegalStateException(failure);} stage=5; }else {net.minecraft.client.gui.screens.ConnectScreen.startConnecting(new net.minecraft.client.gui.screens.TitleScreen(),mc,net.minecraft.client.multiplayer.resolver.ServerAddress.parseString(destination.ip),destination,false,null);stage=1;} wait=0; @@ -127,7 +141,7 @@ private void tcp(net.minecraft.client.Minecraft mc) { wait=0;return; } System.out.println("NATIVE_FORGE_TCP_JOIN "+(++connections)); - mc.level.disconnect();mc.disconnect();mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen());stage=0;wait=0; + mc.disconnectFromWorld(Component.empty());mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen());stage=0;wait=0; }else if(stage==1 && mc.screen instanceof net.minecraft.client.gui.screens.DisconnectedScreen) { throw new AssertionError("NATIVE_FORGE_TCP_LOGIN_FAILURE joins="+connections); } @@ -138,10 +152,10 @@ 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 net.minecraft.world.level.GameRules(),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), + 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.levelgen.WorldOptions(4267321L,false,false), - access->access.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) - .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions(),new net.minecraft.client.gui.screens.TitleScreen()); + 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 30541ef..fdc858c 100644 --- a/src/controlTest/java/buildcontrol/ResourceControl.java +++ b/src/controlTest/java/buildcontrol/ResourceControl.java @@ -2,7 +2,9 @@ import java.nio.file.*; import java.util.concurrent.*; -import net.minecraft.Util; +import net.minecraft.TracingExecutor; +import net.minecraft.util.Util; +import net.minecraft.server.permissions.LevelBasedPermissionSet; import net.minecraft.commands.Commands; import net.minecraft.core.RegistryAccess; import net.minecraft.server.MinecraftServer; @@ -26,15 +28,15 @@ static void run(int rounds) { 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"); - Executor prepare=Util.backgroundExecutor(); + TracingExecutor prepare = Util.backgroundExecutor(); int cpus=Runtime.getRuntime().availableProcessors(); - if(!(prepare instanceof ForkJoinPool))throw new AssertionError("Forge 52 preparation executor changed"); - int workers=((ForkJoinPool)prepare).getParallelism(),limit=Integer.getInteger("max.bg.threads",255); - if(limit<1 || limit>255 || workers!=Math.max(1,Math.min(cpus-1,limit)))throw new AssertionError("Forge 52 worker configuration differs from its actual Util executor"); - System.out.println("NATIVE52_RESOURCE_CONTROL_BEGIN cpus="+cpus+" preparationWorkers="+workers+" rounds="+rounds); + if (!(prepare.service() instanceof ForkJoinPool pool)) throw new AssertionError("Forge 61 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); PackRepository packs=new PackRepository(new ServerPacksSource(new net.minecraft.world.level.validation.DirectoryValidator(path->false))); WorldLoader.InitConfig config=new WorldLoader.InitConfig( - new WorldLoader.PackConfig(packs,WorldDataConfiguration.DEFAULT,false,false),Commands.CommandSelection.DEDICATED,2); + new WorldLoader.PackConfig(packs,WorldDataConfiguration.DEFAULT,false,false),Commands.CommandSelection.DEDICATED, LevelBasedPermissionSet.GAMEMASTER); { for(int round=1;round<=rounds;round++) { // These are Main's real resources and native executors. No @@ -46,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("NATIVE52_RESOURCE_RELOAD_COMPLETE="+round); + if(round%10==0)System.out.println("NATIVE61_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("NATIVE52_RESOURCE_CONTROL_PASS rounds="+rounds); + System.out.println("NATIVE61_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 552d69d..04325ca 100644 --- a/src/controlTest/resources/META-INF/mods.toml +++ b/src/controlTest/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[52,)" +loaderVersion="[61,)" license="LGPL-2.1-only" [[mods]] modId="forgeplatformcontrol" diff --git a/src/controlTest/resources/pack.mcmeta b/src/controlTest/resources/pack.mcmeta index 694a10a..289fad6 100644 --- a/src/controlTest/resources/pack.mcmeta +++ b/src/controlTest/resources/pack.mcmeta @@ -1,7 +1,13 @@ { "pack": { - "pack_format": 34, - "supported_formats": {"min_inclusive": 34, "max_inclusive": 48}, - "description": "Disposable Forge control" + "description": "Disposable Forge control", + "min_format": [ + 94, + 1 + ], + "max_format": [ + 94, + 1 + ] } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java index b3b849a..dc8af53 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ClientChecks.java @@ -1,6 +1,5 @@ package cyano.lootable.buildtest; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerLevel; @@ -16,7 +15,6 @@ import net.minecraft.world.level.GameType; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; /** Automated real-client world, packet interaction, GUI render and reload check. */ @@ -27,6 +25,7 @@ static net.minecraft.core.HolderLookup.Provider fixtureRegistries() { } private int stage, ticks, frames; + private boolean fixtureProfilesInstalled; private int reportedStage=-1; private Future pending; private final ConnectedClientChecks connected = new ConnectedClientChecks(); @@ -36,24 +35,23 @@ static net.minecraft.core.HolderLookup.Provider fixtureRegistries() { private final VisibilitySkinClientChecks visibilitySkins = new VisibilitySkinClientChecks(); private final ConfigClientChecks configScreen = new ConfigClientChecks(); private final NamelessCorpseClientChecks namelessNames = new NamelessCorpseClientChecks(); - @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre event) { - event.setCanceled(true); - } - @SubscribeEvent public void keyboard(net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre event) { - event.setCanceled(true); - } - @SubscribeEvent public void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Color event) { + private void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Pre event) { if(Boolean.getBoolean("lootablebodies.configUiCheck"))try {configScreen.tooltip(event);}catch(Throwable error){fail(error);} } public void register() { if (Boolean.getBoolean("lootablebodies.clientCheck")) { - net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); - MinecraftForge.EVENT_BUS.register(this); + net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre.BUS.addListener(event -> true); + net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre.BUS.addListener(event -> true); + net.minecraftforge.client.event.RenderTooltipEvent.Pre.BUS.addListener(this::tooltip); + TickEvent.ClientTickEvent.Post.BUS.addListener(this::tick); } } - @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) { - if(event.phase != TickEvent.Phase.END) return; + private void tick(TickEvent.ClientTickEvent.Post event) { Minecraft mc = Minecraft.getInstance(); + if (!fixtureProfilesInstalled) { + FixtureProfileCache.install(name -> java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.empty())); + fixtureProfilesInstalled = true; + } // The verifier never needs to capture the user's mouse or keyboard. if(mc.isWindowActive()) mc.mouseHandler.releaseMouse(); net.minecraft.client.KeyMapping.releaseAll(); @@ -62,7 +60,7 @@ public void register() { " position="+(mc.player==null?"none":mc.player.blockPosition())+" focus="+mc.isWindowActive()); reportedStage=stage; } - if(ticks>0 && ticks%150==0) Screenshot.grab(mc.gameDirectory,"diagnostic-stage-"+stage+"-tick-"+ticks+".png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + if(ticks>0 && ticks%150==0) Screenshot.grab(mc.gameDirectory,"diagnostic-stage-"+stage+"-tick-"+ticks+".png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); if(++ticks > 12000) fail(new AssertionError("client timeout stage="+stage)); try { RuntimeChecks.require(!new File(mc.gameDirectory,"abort").exists(),"Disposable graphical check aborted"); @@ -106,7 +104,7 @@ public void register() { } } else if(stage==3 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { RuntimeChecks.require(((CorpseGUIContainer)mc.screen).getMenu().slots.size()==84,"rendered GUI slots"); - Screenshot.grab(mc.gameDirectory,"corpse-gui-fresh.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + Screenshot.grab(mc.gameDirectory,"corpse-gui-fresh.png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); mc.player.closeContainer(); stage=4; FixtureConnections.disconnect(mc); } else if(stage==4 && FixtureConnections.stopped(mc)) { @@ -125,7 +123,7 @@ public void register() { stage=7; frames=0; break; } } else if(stage==7 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { - Screenshot.grab(mc.gameDirectory,"corpse-gui-reload.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + Screenshot.grab(mc.gameDirectory,"corpse-gui-reload.png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); mc.player.closeContainer(); stage=20; connected.start(mc); } else if(stage==20 && connected.tick(mc)) { mc.player.closeContainer(); @@ -152,7 +150,7 @@ public void register() { } catch(Throwable error) { fail(error); } } private static void positionViewer(net.minecraft.server.level.ServerPlayer player) { - ServerLevel world=player.serverLevel(); + ServerLevel world=player.level(); RuntimeChecks.awaitChunk(world,0,0); for(int x=7;x<=9;x++)for(int z=6;z<=8;z++) world.setBlockAndUpdate(new net.minecraft.core.BlockPos(x,79,z),net.minecraft.world.level.block.Blocks.STONE.defaultBlockState()); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java index fa92fee..94b1d33 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConfigClientChecks.java @@ -19,15 +19,18 @@ /** Real native Mods button and widgets, without operating the user's desktop. */ final class ConfigClientChecks { private int stage,frames,step,tooltipsChecked;private File report;private ModListScreen mods;private CorpseConfigGui root;private Future pending; - void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Color event) { + void tooltip(net.minecraftforge.client.event.RenderTooltipEvent.Pre event) { Screen screen=Minecraft.getInstance().screen; if(!(screen instanceof CorpseConfigGui))return; int width=0,height=event.getComponents().size()==1?-2:0; for(net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent line:event.getComponents()) { - width=Math.max(width,line.getWidth(event.getFont()));height+=line.getHeight(); + width=Math.max(width,line.getWidth(event.getFont()));height+=line.getHeight(event.getFont()); } - RuntimeChecks.require(event.getX()>=3 && event.getX()+width+3<=screen.width && event.getY()>=4 && event.getY()+height+4<=screen.height, - "Native configuration tooltip clipped: "+event.getX()+","+event.getY()+" "+width+"x"+height+" screen="+screen.width+"x"+screen.height); + var position = event.getTooltipPositioner().positionTooltip( + screen.width, screen.height, event.getX(), event.getY(), width, height); + RuntimeChecks.require(position.x()>=3 && position.x()+width+3<=screen.width + && position.y()>=4 && position.y()+height+4<=screen.height, + "Native configuration tooltip clipped: "+position+" "+width+"x"+height+" screen="+screen.width+"x"+screen.height); tooltipsChecked++; } void tick(Minecraft mc)throws Exception { @@ -72,7 +75,7 @@ void tick(Minecraft mc)throws Exception { } else if(stage==6) { screenshot(mc,"05-world-rules-disabled.png");click(root,1);CorpseConfigGui appearance=(CorpseConfigGui)mc.screen;click(appearance,20);click(appearance,21);click(appearance,200);click(root,200); check(!LootableBodies.useLocalSkin && LootableBodies.displayNameTag,"appearance_changes_while_world_open"); - FixtureConnections.disconnect(mc);stage=5; + stage=5;FixtureConnections.disconnect(mc); } else if(stage==5 && FixtureConnections.stopped(mc)) { check(tooltipsChecked>0,"native_tooltip_bounds_checked_at_left_centre_right");finish(mc); } @@ -88,7 +91,8 @@ private void openMods(Minecraft mc)throws Exception { private void openConfig(Minecraft mc)throws Exception {click(mods,20);check(mc.screen instanceof CorpseConfigGui,"mods_button_opens_regular_config");root=(CorpseConfigGui)mc.screen;} private static Object get(Object target,String name)throws Exception {for(Class type=target.getClass();type!=null;type=type.getSuperclass())try{Field f=type.getDeclaredField(name);f.setAccessible(true);return f.get(target);}catch(NoSuchFieldException absent){}throw new NoSuchFieldException(name);} private static Button button(Screen screen,int id)throws Exception {if(screen instanceof ModListScreen && id==20)return (Button)get(screen,"configButton");for(Object value:screen.children())if(value instanceof CorpseConfigGui.SettingButton && ((CorpseConfigGui.SettingButton)value).id==id)return (CorpseConfigGui.SettingButton)value;throw new AssertionError("Missing GUI button "+id);} - private static void click(Screen screen,int id)throws Exception {Button button=button(screen,id);RuntimeChecks.require(button.active,"Disabled GUI button "+id);button.onClick(button.getX()+1,button.getY()+1);} + private static void click(Screen screen,int id)throws Exception {Button button=button(screen,id);RuntimeChecks.require(button.active,"Disabled GUI button "+id);button.onClick(new net.minecraft.client.input.MouseButtonEvent(button.getX()+1,button.getY()+1, + new net.minecraft.client.input.MouseButtonInfo(0,0)), false);} @SuppressWarnings("unchecked") private static EditBox field(CorpseConfigGui screen,String key)throws Exception { for(int attempt=0;attempt<10;attempt++){EditBox field=((Map)get(screen,"fields")).get(key);if(field!=null)return field;int page=(Integer)get(screen,"page");try{click(screen,4);}catch(AssertionError end){while((Integer)get(screen,"page")>0)click(screen,3);}if(page==(Integer)get(screen,"page") && attempt>0)break;} throw new AssertionError("Missing config text field "+key); @@ -98,29 +102,28 @@ private void openMods(Minecraft mc)throws Exception { @SuppressWarnings("unchecked") private static void screenshot(Minecraft mc,String name)throws Exception { // Capture the completed native frame before the tooltip exercise // redraws the same render target several times in a single tick. - Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString())); + Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); if(mc.screen instanceof CorpseConfigGui) { Screen screen=mc.screen;Map rows=(Map)get(screen,"rows"); Collection positions=rows.isEmpty()?java.util.Collections.singletonList(100):rows.values(); - var graphics=new net.minecraft.client.gui.GuiGraphics(mc,mc.renderBuffers().bufferSource()); BackgroundObserver observer=new BackgroundObserver(screen); - var bus=net.minecraftforge.common.MinecraftForge.EVENT_BUS; - bus.register(observer); + var bus=net.minecraftforge.client.event.ScreenEvent.BackgroundRendered.BUS; + var listener=bus.addListener(observer::rendered); 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; - screen.render(graphics,x,y+5,0); - graphics.flush(); - RuntimeChecks.require(observer.count==before+1,"Config background rendered twice, blurring its text"); + var graphics = new GuiGraphics(mc, new net.minecraft.client.gui.render.state.GuiRenderState(), x, y+5); + screen.renderWithTooltipAndSubtitles(graphics,x,y+5,0); + RuntimeChecks.require(observer.count==before+1, + "Native config background rendered "+(observer.count-before)+" times"); } - } finally {bus.unregister(observer);} + } finally {bus.removeListener(listener);} } } public static final class BackgroundObserver { private final Screen screen; private int count; BackgroundObserver(Screen screen){this.screen=screen;} - @net.minecraftforge.eventbus.api.SubscribeEvent public void rendered(net.minecraftforge.client.event.ScreenEvent.BackgroundRendered event) { if(event.getScreen()==screen)count++; } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java index 8640d4f..43fc707 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ConnectedClientChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.world.inventory.Slot; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.SimpleContainer; @@ -79,7 +81,7 @@ boolean tick(Minecraft mc) throws Exception { } else if(stage==8 && ready(mc) && frames>30) { schedule(mc,() -> { verifyReload(mc,"connected-after-recovery","connected-player-after-recovery"); - RuntimeChecks.require(player(mc).getInventory().items.get(9).isEmpty(),"Normal-click destination not empty"); + RuntimeChecks.require(player(mc).getInventory().getNonEquipmentItems().get(9).isEmpty(),"Normal-click destination not empty"); mark("first_save_reload_corpse_and_player"); }); next(9); } else if(stage==9 && done()) { @@ -127,23 +129,23 @@ private void prepareDeath(Minecraft mc) throws Exception { MinecraftServer server=mc.getSingleplayerServer(); ServerPlayer player=player(mc); for(int x=21;x<=27;x++) for(int z=5;z<=11;z++) player.level().setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); - player.level().getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); - player.level().getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); - player.setGameMode(GameType.SURVIVAL); player.getInventory().clearContent();player.getInventory().selected=0; + player.level().getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,false,null); + player.level().getGameRules().set(net.minecraft.world.level.gamerules.GameRules.SPAWN_MOBS,false,null); + player.setGameMode(GameType.SURVIVAL); player.getInventory().clearContent();player.getInventory().setSelectedSlot(0); for(int slot=0;slot<6;slot++)player.setItemSlot(EntityLootableBody.EQUIPMENT_SLOTS[slot],RuntimeChecks.stack(slot)); - for(int slot=9;slot<=11;slot++)player.getInventory().items.set(slot,RuntimeChecks.stack(slot+10)); + for(int slot=9;slot<=11;slot++)player.getInventory().getNonEquipmentItems().set(slot,RuntimeChecks.stack(slot+10)); player.connection.teleport(24.5,80,8.5,0,0); - player.containerMenu.broadcastChanges(); player.kill(); + player.containerMenu.broadcastChanges(); player.kill(player.level()); RuntimeChecks.require(!player.isAlive(),"Connected player did not die"); EntityLootableBody corpse=null; for(Entity entity:FixtureApi.entities(player.level())) - if(entity instanceof EntityLootableBody && player.getGameProfile().getName().equals(((EntityLootableBody)entity).getCustomNameTag())) corpse=(EntityLootableBody)entity; + if(entity instanceof EntityLootableBody && player.getGameProfile().name().equals(((EntityLootableBody)entity).getCustomNameTag())) corpse=(EntityLootableBody)entity; RuntimeChecks.require(corpse!=null,"Connected death did not create a corpse"); corpseId=corpse.getUUID();corpse.setNoGravity(true);corpse.setNoAi(true);corpse.setDeltaMovement(0,corpse.getDeltaMovement().y,corpse.getDeltaMovement().z);corpse.setDeltaMovement(corpse.getDeltaMovement().x,0,corpse.getDeltaMovement().z);corpse.setDeltaMovement(corpse.getDeltaMovement().x,corpse.getDeltaMovement().y,0); RuntimeChecks.disableVacuum(corpse); for(int slot=0;slot<6;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getItem(slot),RuntimeChecks.stack(slot)),"Connected death equipment slot "+slot); for(int slot=6;slot<=8;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(corpse.getItem(slot),RuntimeChecks.stack(slot+13)),"Connected death main slot "+slot); - RuntimeChecks.require(player.getInventory().save(new ListTag()).size()==0,"Death retained player inventory alongside corpse"); + RuntimeChecks.require(FixtureApi.saveInventory(player).size()==0,"Death retained player inventory alongside corpse"); System.out.println("CONNECTED_CORPSE_UUID "+corpseId); } private void verifyRecovery(Minecraft mc,String before,int slot,int item,String after) throws Exception { @@ -151,9 +153,9 @@ private void verifyRecovery(Minecraft mc,String before,int slot,int item,String RuntimeChecks.require((corpse.getItem(slot)).isEmpty(),"Network recovery left source stack"); checkPlayer(player(mc).getInventory(),item); CompoundTag expected=read(folder(mc),before),actual=new CompoundTag(); - ListTag equipment=expected.getList("Equipment",10); - for(int i=equipment.size()-1;i>=0;i--)if(equipment.getCompound(i).getByte("Slot")==slot)equipment.remove(i); - corpse.saveAsPassenger(actual);RuntimeChecks.compare(expected,actual); + ListTag equipment=typedList(expected, "Equipment", 10); + for(int i=equipment.size()-1;i>=0;i--)if(equipment.getCompoundOrEmpty(i).getByteOr("Slot", (byte) 0)==slot)equipment.remove(i); + FixtureApi.saveEntity(corpse,actual);RuntimeChecks.compare(expected,actual); RuntimeChecks.saveExpected(folder(mc),after,corpse);visibleExpected=actual.copy(); } private void verifyReload(Minecraft mc,String corpseName,String playerName) throws Exception { @@ -161,19 +163,19 @@ private void verifyReload(Minecraft mc,String corpseName,String playerName) thro EntityLootableBody corpse=corpse(mc);RuntimeChecks.require(corpse.isAlive(),"Saved corpse is no longer accessible"); corpse.baseTick();RuntimeChecks.verifyExpected(folder(mc),corpseName,corpse); CompoundTag expected=read(folder(mc),playerName); - RuntimeChecks.require(expected.get("Inventory").equals(player(mc).getInventory().save(new ListTag())),"Recovered player inventory changed on disk reload"); + RuntimeChecks.require(expected.get("Inventory").equals(FixtureApi.saveInventory(player(mc))),"Recovered player inventory changed on disk reload"); player(mc).connection.teleport(24.5,80,7.5,0,0); visibleExpected=read(folder(mc),corpseName); } private void savePlayer(Minecraft mc,String name) throws Exception { - CompoundTag expected=new CompoundTag();expected.put("Inventory",player(mc).getInventory().save(new ListTag())); + CompoundTag expected=new CompoundTag();expected.put("Inventory",FixtureApi.saveInventory(player(mc))); try(FileOutputStream out=new FileOutputStream(new File(folder(mc),name+".nbt"))){NbtIo.writeCompressed(expected,out);} mc.getSingleplayerServer().getPlayerList().saveAll();mc.getSingleplayerServer().saveAllChunks(false,true,false); } private void verifyGui(Minecraft mc) { RuntimeChecks.require(gui(mc) && mc.player.containerMenu.slots.size()==84,"Connected GUI missing slots"); - ItemStack[] expected=new ItemStack[48];ListTag list=visibleExpected.getList("Equipment",10); - for(int i=0;iSystem.out.println(message.getString()));} + private static void screenshot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString()));} private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} private void mark(String name) throws Exception { try(FileWriter out=new FileWriter(report,true)){out.write("PASS "+name+"\n");} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java index 94ad6e2..e55bda8 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ContainerInteractionChecks.java @@ -18,6 +18,7 @@ import java.io.*; import java.util.*; import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; @@ -39,7 +40,6 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.event.entity.item.ItemTossEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; /** Server packet/transaction tests; packets are decoded from their actual wire layout. */ final class ContainerInteractionChecks { @@ -123,8 +123,8 @@ 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;MinecraftForge.EVENT_BUS.register(reject); - try{packet(p,4,0,ClickType.THROW,null);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} + 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();} 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 { @@ -137,13 +137,13 @@ private static void throwing(boolean creative,int source,int button,int rejectio EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL); fill(b);fill(p);open(p,b);ItemStack original=item(120,17); if(source==0)b.setItem(6,original.copy()); - else if(source==1)p.getInventory().items.set(9,original.copy());else p.containerMenu.setCarried(original.copy()); + else if(source==1)p.getInventory().getNonEquipmentItems().set(9,original.copy());else p.containerMenu.setCarried(original.copy()); if(capture){p.captureDrops(new java.util.ArrayList<>());p.captureDrops().add(new ItemEntity(world,96.5,80,96.5,item(121,11)));} Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=rejection==1;reject.join=rejection==2; RecordingConnection connection=(RecordingConnection)p.connection;int initialSyncs=connection.resyncs; - MinecraftForge.EVENT_BUS.register(reject); + reject.install(); try{packet(p,source==2?-999:source==0?6:48,button,source==2?ClickType.PICKUP:ClickType.THROW,null);} - finally{MinecraftForge.EVENT_BUS.unregister(reject);} + 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); ItemStack remaining=rejection==0 && amount==17?null:item(120,rejection==0?17-amount:17); @@ -157,32 +157,32 @@ private static void throwing(boolean creative,int source,int button,int rejectio } private static void creativeFallback(boolean toss,boolean swap) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();fill(b);fill(p);p.setGameMode(GameType.CREATIVE);open(p,b); - if(swap){b.setItem(0,item(0,1));p.getInventory().items.set(0,pumpkins(64));} + 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;MinecraftForge.EVENT_BUS.register(reject); - try{if(swap)packet(p,0,0,ClickType.SWAP,null);else close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} + 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();} conserved(before,b,p);RuntimeChecks.require(b.isAlive(),"Creative fallback owner no longer alive"); } private static void collect(int mode) throws Exception { EntityLootableBody b=body();ServerPlayer p=player(),q=player();open(p,b);open(q,b); b.setItem(6,item(120,20));b.setItem(7,item(120,30));b.setItem(8,item(121,14)); ItemStack differentDamage=item(120,12);differentDamage.setDamageValue((differentDamage.getDamageValue()+1)%16);b.setItem(9,differentDamage); - p.getInventory().items.set(9,item(120,10));p.containerMenu.setCarried(mode==3?ItemStack.EMPTY:item(120,mode==2?64:5)); + 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);} Map before=total(b,p,q);int wanted=mode==3?0:mode==2?64:mode==5?15:64; packet(p,10,mode==1?1:0,ClickType.PICKUP_ALL,null); RuntimeChecks.require(((p.containerMenu.getCarried()).isEmpty()?0:p.containerMenu.getCarried().getCount())==wanted,"Double click count/capacity differs"); equal(b.getItem(8),item(121,14),"Double click collected different custom NBT");equal(b.getItem(9),differentDamage,"Double click collected different damage"); - if(mode==0)equal(p.getInventory().items.get(9),item(120,1),"Forward collection order differs"); + if(mode==0)equal(p.getInventory().getNonEquipmentItems().get(9),item(120,1),"Forward collection order differs"); if(mode==1)equal(b.getItem(6),item(120,1),"Reverse collection order differs"); conserved(before,b,p,q); } private static void fallbackOrder(boolean offhand) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b); - if(offhand){fill(p);p.getInventory().offhand.set(0,item(120,60));} - p.containerMenu.setCarried(item(120,10));Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=true;MinecraftForge.EVENT_BUS.register(reject); - try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} + if(offhand){fill(p);p.setItemSlot(EquipmentSlot.OFFHAND,item(120,60));} + p.containerMenu.setCarried(item(120,10));Map before=total(b,p);RejectedDrops reject=new RejectedDrops();reject.toss=true;reject.install(); + try{close(p);}finally{reject.close();} equal(p.getInventory().getItem(offhand?40:0),item(120,offhand?64:10),"Rejected-drop fallback changed vanilla player insertion priority");conserved(before,b,p); } private static void cloneStack(boolean creative,boolean occupied) throws Exception { @@ -199,14 +199,14 @@ private static void creativeDrag(boolean creative) throws Exception { } private static void creativePacket(boolean creative,boolean cancelled) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();p.setGameMode(creative?GameType.CREATIVE:GameType.SURVIVAL);open(p,b);b.setItem(6,item(120,17));CompoundTag unchanged=data(b); - RejectedDrops reject=new RejectedDrops();reject.toss=cancelled;MinecraftForge.EVENT_BUS.register(reject);var wire=new net.minecraft.network.RegistryFriendlyByteBuf(Unpooled.buffer(),p.registryAccess()); + RejectedDrops reject=new RejectedDrops();reject.toss=cancelled;reject.install();var wire=new net.minecraft.network.RegistryFriendlyByteBuf(Unpooled.buffer(),p.registryAccess()); try { ServerboundSetCreativeModeSlotPacket.STREAM_CODEC.encode(wire,new ServerboundSetCreativeModeSlotPacket(cancelled?-1:9,item(121,23))); p.connection.handleSetCreativeModeSlot(ServerboundSetCreativeModeSlotPacket.STREAM_CODEC.decode(wire)); RuntimeChecks.require(!wire.isReadable(),"Native creative packet left undecoded fields"); } - finally{wire.release();MinecraftForge.EVENT_BUS.unregister(reject);} - RuntimeChecks.require(unchanged.equals(data(b)),"Creative inventory packet changed corpse instead of player");equal(p.getInventory().items.get(9),creative&&!cancelled?item(121,23):null,"Creative player packet permission/target differs"); + finally{wire.release();reject.close();} + RuntimeChecks.require(unchanged.equals(data(b)),"Creative inventory packet changed corpse instead of player");equal(p.getInventory().getNonEquipmentItems().get(9),creative&&!cancelled?item(121,23):null,"Creative player packet permission/target differs"); if(cancelled)RuntimeChecks.require(FixtureApi.entities(world,6,6).stream().noneMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cancelled creative minted drop spawned anyway"); } @@ -231,18 +231,15 @@ private static CorpseContainer open(ServerPlayer p,EntityLootableBody b){ } private static ItemStack item(int id,int count){ItemStack s=RuntimeChecks.stack(id);s.setCount(count);return s;} private static ItemStack pumpkins(int count){ItemStack s=new ItemStack(Blocks.CARVED_PUMPKIN,count);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().items.set(i,item(100+i,64));} + 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 { packetWithState(p,slot,button,type,predicted,p.containerMenu.getStateId()); } private static void packetWithState(ServerPlayer p,int slot,int button,ClickType type,ItemStack predicted,int stateId) throws Exception { - it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap changed=new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(); - if(predicted!=null && slot>=0)changed.put(slot,predicted.copy()); // These server-side fixtures deliberately report pre-action predictions; // native broadcasts must repair them. Real clients cover normal prediction. - ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket( - p.containerMenu.containerId,stateId,slot,button,type,p.containerMenu.getCarried().copy(),changed); + ServerboundContainerClickPacket outgoing=FixtureApi.clickPacket(p,stateId,slot,button,type,predicted); var wire=new net.minecraft.network.RegistryFriendlyByteBuf(Unpooled.buffer(),p.registryAccess()); try { ServerboundContainerClickPacket.STREAM_CODEC.encode(wire,outgoing);ServerboundContainerClickPacket decoded=ServerboundContainerClickPacket.STREAM_CODEC.decode(wire); @@ -271,15 +268,15 @@ private static void drag(ServerPlayer p,int mode,int... slots) throws Exception packet(p,-999,2|(mode<<2),ClickType.QUICK_CRAFT,null); } private static void equal(ItemStack actual,ItemStack expected,String reason){RuntimeChecks.require(RuntimeChecks.sameStack(actual == null ? ItemStack.EMPTY : actual,expected == null ? ItemStack.EMPTY : expected),reason);} - private static CompoundTag data(EntityLootableBody b){CompoundTag n=new CompoundTag();b.addAdditionalSaveData(n);return n;} + private static CompoundTag data(EntityLootableBody b){CompoundTag n=new CompoundTag();FixtureApi.writeAdditional(b,n);return n;} private static void add(Map all,ItemStack s){ if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=FixtureStackData.save(s);n.remove("count"); String key=n.toString();all.put(key,all.getOrDefault(key,0)+s.getCount()); } private static Map total(EntityLootableBody b,ServerPlayer... players){ Map all=new TreeMap<>(); - for(int i=0;i<48;i++)add(all,b.getItem(i));ListTag aux=data(b).getList("Aux",10); - for(int i=0;iemptyList():p.captureDrops())add(all,e.getItem());} for(Entity e:FixtureApi.entities(world,6,6))if(e instanceof ItemEntity && !e.isRemoved())add(all,((ItemEntity)e).getItem()); return all; @@ -292,29 +289,29 @@ private static void conserved(Map expected,EntityLootableBody b, delta.values().removeIf(count->count==0); throw new AssertionError("Item count/type/damage/custom NBT lost or duplicated: "+delta); } - CompoundTag before=data(b),immutable=before.copy();EntityLootableBody copy=new EntityLootableBody(world);copy.readAdditionalSaveData(before); + CompoundTag before=data(b),immutable=before.copy();EntityLootableBody copy=new EntityLootableBody(world);FixtureApi.readAdditional(copy,before); RuntimeChecks.require(Objects.equals(before.get("Equipment"),data(copy).get("Equipment")) && Objects.equals(before.get("Aux"),data(copy).get("Aux")),"Post-interaction NBT round trip changed inventory"); RuntimeChecks.require(before.equals(immutable),"Post-interaction reader mutated saved input"); } private static void hotbar() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();open(p,b); for(int key=0;key<9;key++){ - p.getInventory().items.set(key,item(30+key,13));Map before=total(b,p); - click(p,6,key,ClickType.SWAP);equal(b.getItem(6),item(30+key,13),"Empty hotbar swap failed");equal(p.getInventory().items.get(key),null,"Hotbar source remained"); + 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"); b.setItem(6,item(19,23));before=total(b,p); - click(p,6,key,ClickType.SWAP);equal(b.getItem(6),item(30+key,13),"Occupied hotbar incoming differs");equal(p.getInventory().items.get(key),item(19,23),"Occupied hotbar outgoing differs"); - click(p,6,key,ClickType.SWAP);conserved(before,b,p);b.setItem(6,null);p.getInventory().items.set(key,ItemStack.EMPTY); + 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); } } private static void armorSwap() throws Exception { - EntityLootableBody b=body();ServerPlayer p=player();open(p,b);p.getInventory().items.set(0,item(4,1)); + 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().items.set(0,pumpkins(64));Map before=total(b,p);click(p,0,0,ClickType.SWAP); - equal(b.getItem(0),pumpkins(1),"Hotbar armor limit ignored");equal(p.getInventory().items.get(0),pumpkins(63),"Hotbar armor remainder lost");conserved(before,b,p); + p.getInventory().getNonEquipmentItems().set(0,pumpkins(64));Map before=total(b,p);click(p,0,0,ClickType.SWAP); + equal(b.getItem(0),pumpkins(1),"Hotbar armor limit ignored");equal(p.getInventory().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().items.set(9,pumpkins(64));open(p,b); + 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); 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); @@ -346,30 +343,39 @@ private static void fullPlayer() throws Exception { Map before=total(b,p);packet(p,6,0,ClickType.QUICK_MOVE,null);equal(b.getItem(6),item(19,23),"Full player inventory consumed corpse stack");conserved(before,b,p); } private static void partialPlayer() throws Exception { - EntityLootableBody b=body();b.setItem(6,item(19,23));ServerPlayer p=player();fill(p);p.getInventory().items.set(35,item(19,63));open(p,b); - Map before=total(b,p);click(p,6,0,ClickType.QUICK_MOVE);equal(p.getInventory().items.get(35),item(19,64),"Partial merge did not fill one space");equal(b.getItem(6),item(19,22),"Partial merge discarded remainder");conserved(before,b,p); + 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); } private static void fullCorpse() throws Exception { - EntityLootableBody b=body();fill(b);ServerPlayer p=player();p.getInventory().items.set(9,item(110,23));open(p,b); + 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(); RuntimeChecks.require(unchanged.get("Equipment").equals(data(b).get("Equipment")) && unchanged.get("Aux").equals(data(b).get("Aux")),"Full corpse overwrote indexed/Aux contents");conserved(before,b,p); } public static final class RejectedDrops { boolean join,toss; - @SubscribeEvent public void join(EntityJoinLevelEvent e){if(join && e.getEntity() instanceof ItemEntity)e.setCanceled(true);} - @SubscribeEvent public void toss(ItemTossEvent e){if(toss)e.setCanceled(true);} + private net.minecraftforge.eventbus.api.listener.EventListener joinListener, tossListener; + + void install() { + joinListener = EntityJoinLevelEvent.BUS.addListener(event -> join && event.getEntity() instanceof ItemEntity); + tossListener = ItemTossEvent.BUS.addListener(event -> toss); + } + + void close() { + EntityJoinLevelEvent.BUS.removeListener(joinListener); + ItemTossEvent.BUS.removeListener(tossListener); + } } 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().items.set(0,pumpkins(64));open(p,b);Map before=total(b,p); - RejectedDrops reject=new RejectedDrops();reject.join=rejectJoin;reject.toss=rejectToss;MinecraftForge.EVENT_BUS.register(reject); - try{click(p,0,0,ClickType.SWAP);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} - equal(b.getItem(0),pumpkins(1),"Displacement changed normal incoming swap");equal(p.getInventory().items.get(0),pumpkins(63),"Displacement changed hotbar remainder");conserved(before,b,p); + 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();} + 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"); } private static void closeCursor(boolean rejected,boolean full) throws Exception { EntityLootableBody b=body();ServerPlayer p=player();if(full){fill(b);fill(p);}open(p,b);p.containerMenu.setCarried(item(120,17));Map before=total(b,p); - RejectedDrops reject=new RejectedDrops();reject.join=rejected;MinecraftForge.EVENT_BUS.register(reject); - try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} + RejectedDrops reject=new RejectedDrops();reject.join=rejected;reject.install(); + try{close(p);}finally{reject.close();} equal(p.containerMenu.getCarried(),null,"Closing left a nonsaved cursor stack");conserved(before,b,p); if(!rejected)RuntimeChecks.require(FixtureApi.entities(world,6,6).stream().anyMatch(e->e instanceof ItemEntity && !e.isRemoved()),"Cursor close no longer performs normal accepted drop"); } @@ -380,9 +386,9 @@ private static void capturedCursor() throws Exception { } private static void dyingCursor() throws Exception { EntityLootableBody b=body();ServerPlayer p=player();fill(p);open(p,b);p.containerMenu.setCarried(item(120,17)); - b.kill();b.baseTick();Map before=total(b,p); - RejectedDrops reject=new RejectedDrops();reject.join=true;MinecraftForge.EVENT_BUS.register(reject); - try{close(p);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} + b.kill(world);b.baseTick();Map before=total(b,p); + RejectedDrops reject=new RejectedDrops();reject.join=true;reject.install(); + try{close(p);}finally{reject.close();} for(int i=0;i<15;i++)b.baseTick(); RuntimeChecks.require(b.isAlive() || b.isEmpty(),"Cursor fallback was put into an owner still scheduled for removal"); conserved(before,b,p); @@ -406,15 +412,15 @@ 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().items.set(0,item(20,7));else c.containerMenu.setCarried(item(20,7));Map before=total(b,a,c); + 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); 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;MinecraftForge.EVENT_BUS.register(reject); - try{close(a);}finally{MinecraftForge.EVENT_BUS.unregister(reject);} + 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(); + try{close(a);}finally{reject.close();} c.containerMenu.broadcastChanges();conserved(before,b,a,c); } private static void stale(boolean removed) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java index 5821363..a660af9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/DropClientChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.world.inventory.Slot; import net.minecraft.world.SimpleContainer; import net.minecraft.nbt.ListTag; @@ -33,6 +35,7 @@ private interface Action {void run() throws Exception;} private final ContainerInteractionChecks.RejectedDrops reject=new ContainerInteractionChecks.RejectedDrops(); private int stage,frames; private Future pending; + private Future openReady; private UUID playerId; private CompoundTag savedBody; private ListTag savedPlayer; @@ -46,7 +49,7 @@ else if(stage==2 && gui(mc) && frames>20){ equal(slot(mc,6),item(120,17),"Initial client source");click(mc,6,0,ClickType.THROW);next(3); } else if(stage==3 && frames>20){ equal(slot(mc,6),item(120,16),"Accepted client single throw"); - schedule(mc,()->{equal(body(mc).getItem(6),item(120,16),"Accepted server single throw");drop(mc,item(120,1));mark(mc,"network_accepted_throw");reject.toss=true;MinecraftForge.EVENT_BUS.register(reject);});next(4); + 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==5 && frames>20){ equal(slot(mc,6),item(120,16),"Cancelled throw did not correct client source");screenshot(mc,"drop-cancelled-source-resync.png"); @@ -60,7 +63,7 @@ else if(stage==8 && frames>20){ 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().items.set(9,item(120,5));refresh(mc);});next(11); + 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==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"); @@ -73,56 +76,85 @@ else if(stage==16 && frames>20){ click(mc,-999,8,ClickType.QUICK_CRAFT);click(mc,7,9,ClickType.QUICK_CRAFT);click(mc,9,9,ClickType.QUICK_CRAFT);click(mc,-999,10,ClickType.QUICK_CRAFT);next(17); } else if(stage==17 && frames>20){ equal(slot(mc,7),item(120,64),"Creative client fill first");equal(slot(mc,9),item(120,64),"Creative client fill second");equal(mc.player.containerMenu.getCarried(),null,"Creative fill vanilla cursor");screenshot(mc,"drop-creative-filled.png"); - schedule(mc,()->{equal(body(mc).getItem(7),item(120,64),"Creative server fill first");equal(body(mc).getItem(9),item(120,64),"Creative server fill second");equal(player(mc).getInventory().items.get(9),item(120,42),"Creative operation changed recovered inventory");mark(mc,"network_creative_clone_and_fill"); + 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"); for(int i=0;i<48;i++)body(mc).setItem(i,item(i,i<6?1:64));body(mc).addItem(item(70,31)); - for(int i=0;i<36;i++)player(mc).getInventory().items.set(i,item(100+i,64));player(mc).containerMenu.setCarried(item(122,17));reject.toss=true;refresh(mc);});next(18); + for(int i=0;i<36;i++)player(mc).getInventory().getNonEquipmentItems().set(i,item(100+i,64));player(mc).containerMenu.setCarried(item(122,17));reject.toss=true;refresh(mc);});next(18); } else if(stage==18 && done() && frames>20){equal(mc.player.containerMenu.getCarried(),item(122,17),"Creative close cursor fixture");mc.player.closeContainer();next(19);} else if(stage==19 && frames>20){ equal(mc.player.containerMenu.getCarried(),null,"Closed creative client cursor"); - schedule(mc,()->{equal(player(mc).containerMenu.getCarried(),null,"Closed creative server cursor");CompoundTag data=new CompoundTag();body(mc).addAdditionalSaveData(data);ListTag aux=data.getList("Aux",10);int count=0; - for(int i=0;i{equal(player(mc).containerMenu.getCarried(),null,"Closed creative server cursor");CompoundTag data=new CompoundTag();FixtureApi.writeAdditional(body(mc),data);ListTag aux=typedList(data, "Aux", 10);int count=0; + for(int i=0;imc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(22);} else if(stage==22 && ready(mc) && frames>20){schedule(mc,()->{ - RuntimeChecks.awaitChunk(player(mc).serverLevel(),2,0);body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"drop-creative-final",body(mc)); - RuntimeChecks.require(savedPlayer.equals(player(mc).getInventory().save(new ListTag())),"Creative player inventory changed on disk reload");player(mc).connection.teleport(40.5,80,7.5,0,0); + RuntimeChecks.awaitChunk(player(mc).level(),2,0);body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"drop-creative-final",body(mc)); + RuntimeChecks.require(savedPlayer.equals(FixtureApi.saveInventory(player(mc))),"Creative player inventory changed on disk reload");player(mc).connection.teleport(40.5,80,7.5,0,0); mark(mc,"creative_cancelled_close_full_inventory_disk_reload");});next(23); } else if(stage==23 && done()){if(open(mc))next(24);} else if(stage==24 && gui(mc) && frames>20){ - ItemStack[] expected=new ItemStack[48];ListTag list=savedBody.getList("Equipment",10);for(int i=0;iSystem.out.println(message.getString()));} + private static void screenshot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString()));} private void mark(Minecraft mc,String name) throws Exception {try(FileWriter out=new FileWriter(new File(mc.gameDirectory,"drop-client-checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("DROP_CLIENT_CHECK_PASS "+name);} private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java index 84c2a9d..e2f1ca4 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/EffectHurtChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.nbt.Tag; import net.minecraft.nbt.StringTag; @@ -53,14 +55,14 @@ static void registerPotion(){ // Forge 1.12 assigns IDs through registry events. Fill the isolated test // registry through its public API so unsigned-byte coverage still uses 200. for(int i=0;net.minecraft.core.registries.BuiltInRegistries.MOB_EFFECT.byId(199)==null && i<200;i++) - net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(net.minecraft.resources.ResourceLocation.parse("lootablebodiesbuildtest:padding_"+i),new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}); - net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(net.minecraft.resources.ResourceLocation.parse("lootablebodiesbuildtest:extended_effect"),extended); + net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(net.minecraft.resources.Identifier.parse("lootablebodiesbuildtest:padding_"+i),new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}); + net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(net.minecraft.resources.Identifier.parse("lootablebodiesbuildtest:extended_effect"),extended); RuntimeChecks.require(net.minecraft.core.registries.BuiltInRegistries.MOB_EFFECT.getId(extended)==200,"Fixture potion must exercise unsigned byte ID 200"); // Older probes saved a different number of padding effects. Retain their // registry names during upgrades without changing fresh-world ID tests. if(System.getProperty("lootablebodies.phase","").startsWith("upgrade-")) for(int i=0;i<200;i++) { - net.minecraft.resources.ResourceLocation name=net.minecraft.resources.ResourceLocation.parse("lootablebodiesbuildtest:padding_"+i); + net.minecraft.resources.Identifier name=net.minecraft.resources.Identifier.parse("lootablebodiesbuildtest:padding_"+i); if(!net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.containsKey(name)) net.minecraftforge.registries.ForgeRegistries.MOB_EFFECTS.register(name,new MobEffect(net.minecraft.world.effect.MobEffectCategory.BENEFICIAL,0){}); } @@ -103,7 +105,7 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex check("invalid_effect_and_hurt_fields_keep_complete_unresolved_inventory",EffectHurtChecks::inventory); check("native_icon_flag_survives_without_quarantine",()->{CompoundTag e=effect();e.putBoolean("ShowIcon",false);EntityLootableBody b=read(with(e));require(!b.getEffect(MobEffects.NIGHT_VISION).showIcon(),"Native icon visibility changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Native icon visibility quarantined");stable(b);}); check("missing_icon_flag_follows_particle_default",()->{CompoundTag e=effect();e.remove("ShowIcon");e.putBoolean("ShowParticles",false);EntityLootableBody b=read(with(e));require(!b.getEffect(MobEffects.NIGHT_VISION).showIcon(),"Missing native icon default changed");stable(b);}); - check("native_named_identity_survives_numeric_registry_remap",()->{CompoundTag e=effect();e.putByte("Id",(byte)1);EntityLootableBody b=read(with(e));require(b.getEffect(MobEffects.NIGHT_VISION)!=null && b.getEffect(MobEffects.MOVEMENT_SPEED)==null,"Native named identity did not take precedence");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy registry remap quarantined");stable(b);}); + check("native_named_identity_survives_numeric_registry_remap",()->{CompoundTag e=effect();e.putByte("Id",(byte)1);EntityLootableBody b=read(with(e));require(b.getEffect(MobEffects.NIGHT_VISION)!=null && b.getEffect(MobEffects.SPEED)==null,"Native named identity did not take precedence");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy registry remap quarantined");stable(b);}); check("native_named_identity_recovers_missing_legacy_number_with_raw_evidence",()->{CompoundTag e=effect();e.remove("Id");EntityLootableBody b=read(with(e));require(b.getEffect(MobEffects.NIGHT_VISION)!=null,"Known native name was lost");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);}); check("unknown_or_malformed_named_identity_cannot_activate_unrelated_numeric_fallback",()->{for(Tag value:new Tag[]{IntTag.valueOf(16),StringTag.valueOf("bad name?!"),StringTag.valueOf("missing:unknown_effect")}){CompoundTag e=effect();e.put("forge:id",value);EntityLootableBody b=read(with(e));require(b.getActiveEffects().isEmpty(),"Unreadable native identity activated a different effect");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);}}); check("metadata_only_does_not_prevent_empty_decay",EffectHurtChecks::empty); @@ -114,7 +116,7 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex private static void require(boolean c,String why){RuntimeChecks.require(c,why);} private static CompoundTag saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} private static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setItem(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(b);return saved(b);} - private static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.load(n);require(NameLifetimeChecks.same(before,n),"Input mutated");return b;} + private static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();EntityLootableBody b=new EntityLootableBody(world);FixtureApi.loadEntityData(b,n);require(NameLifetimeChecks.same(before,n),"Input mutated");return b;} static CompoundTag effect(Holder effect,int duration,int amplifier) { // Explicit legacy shape for malformed-old-input tests. Current native // save/reload is exercised independently by attributes() below. @@ -135,7 +137,7 @@ private static Holder extendedHolder() { private static CompoundTag with(CompoundTag e){CompoundTag n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(e));return n;} private static void stable(EntityLootableBody b){CompoundTag a=saved(b),z=saved(read(a));for(String k:new String[]{"Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY,"active_effects","HurtTime","Health","AbsorptionAmount","attributes"})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);require(RuntimeChecks.sameStack(b.getItem(6),NbtRecoveryChecks.item(120,17)),"Complete known inventory changed");} private static void badHurt(Tag raw){CompoundTag n=base();n.put("HurtTime",raw.copy());EntityLootableBody b=read(n);require(b.hurtTime==0 && b.isAlive(),"Bad hurt time activated");NameLifetimeChecks.retained(b,"HurtTime",raw);stable(b);} - private static void validHurt(Tag raw){CompoundTag n=base();n.put("HurtTime",raw);EntityLootableBody b=read(n);int v=((NumericTag)raw).getAsInt();require(b.hurtTime==v,"Valid countdown narrowed");stable(b);b.baseTick();require(b.hurtTime==v-1,"Countdown no longer ticks");} + private static void validHurt(Tag raw){CompoundTag n=base();n.put("HurtTime",raw);EntityLootableBody b=read(n);int v=((NumericTag)raw).intValue();require(b.hurtTime==v,"Valid countdown narrowed");stable(b);b.baseTick();require(b.hurtTime==v-1,"Countdown no longer ticks");} private static void badList(Tag raw){CompoundTag n=base();n.put("ActiveEffects",raw.copy());EntityLootableBody b=read(n);require(b.getActiveEffects().isEmpty(),"Bad effect list activated");NameLifetimeChecks.retained(b,"ActiveEffects",raw);stable(b);} private static void badEffect(String key,Tag raw){CompoundTag e=effect();if(key.equals("Id"))e.remove("forge:id");if(raw==null)e.remove(key);else e.put(key,raw.copy());EntityLootableBody b=read(with(e));require(b.getActiveEffects().isEmpty(),"Malformed legacy effect activated");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);} private static void badFlag(String key,Tag raw){CompoundTag e=effect();e.put(key,raw.copy());EntityLootableBody b=read(with(e));MobEffectInstance p=b.getEffect(MobEffects.NIGHT_VISION);require(p!=null && p.getDuration()==200 && p.getAmplifier()==1,"Bad optional flag lost valid effect");require(!p.isAmbient() && p.isVisible(),"Bad optional flag did not use defaults");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);} @@ -165,12 +167,12 @@ private static void badCurativeItems() { private static void validTail(){CompoundTag bad=effect();bad.putString("Duration","opaque");CompoundTag n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(bad,effect()));EntityLootableBody b=read(n);require(b.getEffect(MobEffects.NIGHT_VISION).getDuration()==200,"Bad first effect hid valid tail");NameLifetimeChecks.retained(b,"ActiveEffect",bad);stable(b);} private static void duplicate(){CompoundTag first=effect(),second=effect(MobEffects.NIGHT_VISION,900,3),n=base();n.put("ActiveEffects",NbtRecoveryChecks.list(first,second));EntityLootableBody b=read(n);require(b.getEffect(MobEffects.NIGHT_VISION).getDuration()==200,"Duplicate overwrote first effect");NameLifetimeChecks.retained(b,"ActiveEffect",second);stable(b);} private static void unknown(){CompoundTag e=effect();e.putString("Opaque","retain effect extension");EntityLootableBody b=read(with(e));require(b.getActiveEffects().size()==1,"Unknown sibling lost understood effect");NameLifetimeChecks.retained(b,"ActiveEffect",e);stable(b);} - private static void repeated(){EntityLootableBody b=read(with(effect(MobEffects.INVISIBILITY,200,0)));b.baseTick();require(b.isInvisible(),"Fixture not invisible");b.load(base());require(b.getActiveEffects().isEmpty() && !b.isInvisible(),"Previous effects or render state survived read");stable(b);} - private static void attributes(){EntityLootableBody original=new EntityLootableBody(world);original.setItem(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(original);original.addEffect(new MobEffectInstance(MobEffects.HEALTH_BOOST,200,1));original.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED,200,0));original.addEffect(new MobEffectInstance(MobEffects.ABSORPTION,200,0));original.setHealth(original.getMaxHealth());CompoundTag n=saved(original);EntityLootableBody b=read(n);require(b.getMaxHealth()==28 && b.getHealth()==28 && b.getAbsorptionAmount()==4 && b.getActiveEffects().size()==3,"Valid potion health/attributes/absorption changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy potion fields quarantined");stable(b);for(int i=0;i<200;i++)b.baseTick();require(b.getActiveEffects().isEmpty() && b.getMaxHealth()==20 && b.getAbsorptionAmount()==0 && b.getHealth()<=20,"Expiry did not clean up normal modifiers");} - private static void repeatedAttributes(){EntityLootableBody original=new EntityLootableBody(world);original.addEffect(new MobEffectInstance(MobEffects.HEALTH_BOOST,200,1));EntityLootableBody b=read(saved(original));b.load(base());require(b.getActiveEffects().isEmpty() && b.getMaxHealth()==20,"Obsolete effects/modifiers survived read");stable(b);} - private static void reusedOutput(){EntityLootableBody b=read(base());CompoundTag n=with(effect());b.addAdditionalSaveData(n);require(!n.contains("ActiveEffects"),"Writer retained obsolete ActiveEffects");stable(b);} - private static void inventory(){CompoundTag n=base(),missing=NbtRecoveryChecks.entry(121,11,7);missing.putString("id","absentfixture:missing");n.getList("Equipment",10).add(missing);CompoundTag raw=malformedFields();for(String k:raw.getAllKeys())n.put(k,raw.get(k).copy());EntityLootableBody b=read(n);require(b.isAlive(),"Bad fields made corpse inaccessible");NbtRecoveryChecks.retained(b,missing);verifyWorldFields(b);stable(b);stable(read(saved(b)));} + private static void repeated(){EntityLootableBody b=read(with(effect(MobEffects.INVISIBILITY,200,0)));b.baseTick();require(b.isInvisible(),"Fixture not invisible");FixtureApi.loadEntityData(b,base());require(b.getActiveEffects().isEmpty() && !b.isInvisible(),"Previous effects or render state survived read");stable(b);} + private static void attributes(){EntityLootableBody original=new EntityLootableBody(world);original.setItem(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(original);original.addEffect(new MobEffectInstance(MobEffects.HEALTH_BOOST,200,1));original.addEffect(new MobEffectInstance(MobEffects.SPEED,200,0));original.addEffect(new MobEffectInstance(MobEffects.ABSORPTION,200,0));original.setHealth(original.getMaxHealth());CompoundTag n=saved(original);EntityLootableBody b=read(n);require(b.getMaxHealth()==28 && b.getHealth()==28 && b.getAbsorptionAmount()==4 && b.getActiveEffects().size()==3,"Valid potion health/attributes/absorption changed");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy potion fields quarantined");stable(b);for(int i=0;i<200;i++)b.baseTick();require(b.getActiveEffects().isEmpty() && b.getMaxHealth()==20 && b.getAbsorptionAmount()==0 && b.getHealth()<=20,"Expiry did not clean up normal modifiers");} + private static void repeatedAttributes(){EntityLootableBody original=new EntityLootableBody(world);original.addEffect(new MobEffectInstance(MobEffects.HEALTH_BOOST,200,1));EntityLootableBody b=read(saved(original));FixtureApi.loadEntityData(b,base());require(b.getActiveEffects().isEmpty() && b.getMaxHealth()==20,"Obsolete effects/modifiers survived read");stable(b);} + private static void reusedOutput(){EntityLootableBody b=read(base());CompoundTag n=with(effect());FixtureApi.writeAdditional(b,n);require(!n.contains("ActiveEffects"),"Writer retained obsolete ActiveEffects");stable(b);} + private static void inventory(){CompoundTag n=base(),missing=NbtRecoveryChecks.entry(121,11,7);missing.putString("id","absentfixture:missing");typedList(n, "Equipment", 10).add(missing);CompoundTag raw=malformedFields();for(String k:raw.keySet())n.put(k,raw.get(k).copy());EntityLootableBody b=read(n);require(b.isAlive(),"Bad fields made corpse inaccessible");NbtRecoveryChecks.retained(b,missing);verifyWorldFields(b);stable(b);stable(read(saved(b)));} private static void empty(){CompoundTag n=base();n.put("Equipment",new ListTag());n.putString("HurtTime","opaque");EntityLootableBody b=read(n);require(b.isEmpty(),"Metadata counts as inventory");NameLifetimeChecks.retained(b,"HurtTime",n.get("HurtTime"));} static CompoundTag malformedFields(){CompoundTag n=new CompoundTag();n.putLong("HurtTime",65537);CompoundTag bad=effect();bad.putString("Duration","opaque saved duration");CompoundTag unknown=effect();unknown.remove("forge:id");unknown.putInt("Id",250);unknown.putString("Opaque","missing effect data");n.put("ActiveEffects",NbtRecoveryChecks.list(bad,unknown));return n;} - static void verifyWorldFields(EntityLootableBody b){require(b.hurtTime==0 && b.getActiveEffects().isEmpty(),"Malformed disk effect/hurt state activated");CompoundTag raw=malformedFields();NameLifetimeChecks.retained(b,"HurtTime",raw.get("HurtTime"));ListTag list=raw.getList("ActiveEffects",10);for(int i=0;i dimension) { - player.setRespawnPosition(dimension,position,0F,forced,notify); + var spawn = net.minecraft.world.level.storage.LevelData.RespawnData.of(dimension, position, 0F, 0F); + player.setRespawnPosition(new net.minecraft.server.level.ServerPlayer.RespawnConfig(spawn, forced), notify); } static net.minecraft.resources.ResourceKey dimensionKey(int id) { if(id==0)return Level.OVERWORLD;if(id==-1)return Level.NETHER;if(id==1)return Level.END; @@ -40,10 +45,10 @@ 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(),net.minecraft.world.level.WorldDataConfiguration.DEFAULT), + 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.levelgen.WorldOptions(4267321L,false,false), - access->access.registryOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET) - .getHolderOrThrow(net.minecraft.world.level.levelgen.presets.WorldPresets.FLAT).value().createWorldDimensions(),new net.minecraft.client.gui.screens.TitleScreen()); + 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()); } } static List entities(Level world) { @@ -53,32 +58,101 @@ static List entities(Level world) { return ClientEntities.read(world); } static Entity loadEntity(CompoundTag data, Level world) { - return net.minecraft.world.entity.EntityType.loadEntityRecursive(data, world, entity -> entity); + return net.minecraft.world.entity.EntityType.loadEntityRecursive(data, world, net.minecraft.world.entity.EntitySpawnReason.LOAD, entity -> entity); + } + static boolean saveEntity(Entity entity, CompoundTag data) { + var output = net.minecraft.world.level.storage.TagValueOutput.createWithContext( + net.minecraft.util.ProblemReporter.DISCARDING, entity.registryAccess()); + output.buildResult().merge(data.copy()); + boolean saved = entity.saveAsPassenger(output); + for (String key : java.util.Set.copyOf(data.keySet())) data.remove(key); + data.merge(output.buildResult()); + return saved; + } + + static void loadEntityData(Entity entity, CompoundTag data) { + entity.load(TestNativeNbt.input(entity, data)); + } + + static net.minecraft.nbt.ListTag saveInventory(net.minecraft.world.entity.player.Player player) { + var output = net.minecraft.world.level.storage.TagValueOutput.createWithContext( + net.minecraft.util.ProblemReporter.DISCARDING, player.registryAccess()); + player.getInventory().save(output.list("Inventory", net.minecraft.world.ItemStackWithSlot.CODEC)); + ListTag inventory = output.buildResult().getListOrEmpty("Inventory"); + var slots = new net.minecraft.world.entity.EquipmentSlot[]{ + net.minecraft.world.entity.EquipmentSlot.FEET, net.minecraft.world.entity.EquipmentSlot.LEGS, + net.minecraft.world.entity.EquipmentSlot.CHEST, net.minecraft.world.entity.EquipmentSlot.HEAD, + net.minecraft.world.entity.EquipmentSlot.OFFHAND}; + for (int index = 0; index < slots.length; index++) { + var stack = player.getItemBySlot(slots[index]); + if (stack.isEmpty()) continue; + CompoundTag item = FixtureStackData.save(stack); + item.putByte("Slot", (byte) (index == 4 ? 150 : 100 + index)); + inventory.add(item); + } + return inventory; + } + + static net.minecraft.nbt.ListTag saveEnderInventory(ServerPlayer player) { + var output = net.minecraft.world.level.storage.TagValueOutput.createWithContext( + net.minecraft.util.ProblemReporter.DISCARDING, player.registryAccess()); + player.getEnderChestInventory().storeAsSlots(output.list("EnderItems", net.minecraft.world.ItemStackWithSlot.CODEC)); + return output.buildResult().getListOrEmpty("EnderItems"); } + + 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) { + 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(); + if (predicted != null && slot >= 0) changed.put(slot, net.minecraft.network.HashedStack.create(predicted, hash)); + return new net.minecraft.network.protocol.game.ServerboundContainerClickPacket( + player.containerMenu.containerId, stateId, (short)slot, (byte)button, type, changed, + net.minecraft.network.HashedStack.create(player.containerMenu.getCarried(), hash)); + } + + static void readAdditional(cyano.lootable.entities.EntityLootableBody body, CompoundTag data) { + body.readAdditionalSaveData(TestNativeNbt.input(body, data)); + } + + static void writeAdditional(cyano.lootable.entities.EntityLootableBody body, CompoundTag data) { + var output = net.minecraft.world.level.storage.TagValueOutput.createWithContext( + net.minecraft.util.ProblemReporter.DISCARDING, body.registryAccess()); + output.buildResult().merge(data.copy()); + body.addAdditionalSaveData(output); + for (String key : java.util.Set.copyOf(data.keySet())) data.remove(key); + data.merge(output.buildResult()); + } + 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",3955); + root.putInt("xPos",chunk.getPos().x);root.putInt("zPos",chunk.getPos().z);root.put("sections",new net.minecraft.nbt.ListTag());root.putInt("DataVersion",4671); // 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,3955,3955); + net.minecraft.util.datafix.fixes.References.CHUNK,root,4671,4671); try { java.lang.reflect.Method nativeRead=null; - for(java.lang.reflect.Method method:net.minecraft.world.level.chunk.storage.ChunkSerializer.class.getDeclaredMethods()) - if(java.lang.reflect.Modifier.isStatic(method.getModifiers()) && !method.isSynthetic() && method.getReturnType()==net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor.class && java.util.Arrays.equals(method.getParameterTypes(),new Class[]{ServerLevel.class,CompoundTag.class})) { + for(java.lang.reflect.Method method:net.minecraft.world.level.chunk.storage.SerializableChunkData.class.getDeclaredMethods()) + if(java.lang.reflect.Modifier.isStatic(method.getModifiers()) && !method.isSynthetic() && method.getReturnType()==net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor.class && java.util.Arrays.equals(method.getParameterTypes(),new Class[]{ServerLevel.class,java.util.List.class,java.util.List.class})) { if(nativeRead!=null)throw new IllegalStateException("Ambiguous native entity admission");nativeRead=method; } if(nativeRead==null)throw new IllegalStateException("Missing native entity admission"); nativeRead.setAccessible(true); - net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor processor=(net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor)nativeRead.invoke(null,world,fixed); + net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor processor=(net.minecraft.world.level.chunk.LevelChunk.PostLoadProcessor)nativeRead.invoke(null,world, compounds(fixed, "entities"), compounds(fixed, "block_entities")); if(processor==null)throw new IllegalStateException("Native entity admission returned no processor"); processor.run(chunk); - java.util.UUID id=fixed.getList("entities",10).getCompound(0).getUUID("UUID"); + 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); }catch(ReflectiveOperationException failure){throw new IllegalStateException("Native chunk admission failed",failure);} } + private static List compounds(CompoundTag data, String key) { + return TestNativeNbt.typedList(data, key, 10).stream().map(CompoundTag.class::cast).toList(); + } + 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. diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java index c57bc0b..3b499ab 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureConnections.java @@ -16,8 +16,8 @@ private FixtureConnections() {} static void disconnect(Minecraft mc) { if(mc.getSingleplayerServer()!=null)retiringServer=mc.getSingleplayerServer().getRunningThread(); if(mc.player!=null)mc.player.closeContainer(); - if(mc.level!=null)mc.level.disconnect(); - mc.disconnect();mc.setScreen(new TitleScreen()); + mc.disconnectFromWorld(net.minecraft.network.chat.Component.translatable("menu.disconnect")); + mc.setScreen(new TitleScreen()); } static boolean stopped(Minecraft mc) { return ServerStopGate.ready(mc.level==null,mc.hasSingleplayerServer(),retiringServer); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java index 2ffcfea..746f69a 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureItems.java @@ -4,7 +4,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.core.Direction; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; import net.minecraftforge.common.capabilities.ICapabilitySerializable; @@ -12,19 +12,19 @@ /** Controlled dependency used only by sealed disposable upgrade worlds. */ public final class FixtureItems { public static Item keepsake; - public static void register(net.minecraftforge.eventbus.api.IEventBus bus) { - bus.register(new RegistryItems()); + public static void register(net.minecraftforge.eventbus.api.bus.BusGroup bus) { + net.minecraftforge.registries.RegisterEvent.getBus(bus).addListener(new RegistryItems()::register); } public static final class RegistryItems { - @net.minecraftforge.eventbus.api.SubscribeEvent public void register(net.minecraftforge.registries.RegisterEvent event) { if(!event.getRegistryKey().equals(net.minecraft.core.registries.Registries.ITEM))return; // Native18 creates an intrusive holder inside Item's constructor; // construction belongs to the same open registration event. - keepsake=new Item(new Item.Properties()) { + keepsake=new Item(new Item.Properties().setId(net.minecraft.resources.ResourceKey.create( + net.minecraft.core.registries.Registries.ITEM, Identifier.parse("lootablebodiesbuildtest:keepsake")))) { @Override public ICapabilityProvider getCapabilityProvider(ItemStack stack){return new Stored();} }; - event.register(net.minecraft.core.registries.Registries.ITEM,ResourceLocation.parse("lootablebodiesbuildtest:keepsake"),()->keepsake); + event.register(net.minecraft.core.registries.Registries.ITEM,Identifier.parse("lootablebodiesbuildtest:keepsake"),()->keepsake); } } private static final class Stored implements ICapabilitySerializable { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java index 037138b..3df72c3 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureProfileCache.java @@ -1,29 +1,42 @@ package cyano.lootable.buildtest; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.server.Services; +import net.minecraft.server.players.ProfileResolver; import com.mojang.authlib.GameProfile; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import net.minecraft.world.level.block.entity.SkullBlockEntity; /** An owned test cache at the actual native profile lookup seam. */ final class FixtureProfileCache { + static com.mojang.authlib.GameProfile withTexture(java.util.UUID id, String name, String texture) { + var entry = new com.mojang.authlib.properties.Property("textures", texture); + return new com.mojang.authlib.GameProfile(id, name, new com.mojang.authlib.properties.PropertyMap( + com.google.common.collect.ImmutableMultimap.of("textures", entry))); + } + private FixtureProfileCache() { } static AutoCloseable install(Function>> lookup) { try { - var field=SkullBlockEntity.class.getDeclaredField("profileCacheByName"); + Minecraft client = Minecraft.getInstance(); + var field = Minecraft.class.getDeclaredField("services"); field.setAccessible(true); - Object previous=field.get(null); - field.set(null,CacheBuilder.newBuilder().maximumSize(64).build( - new CacheLoader>>() { - @Override public CompletableFuture> load(String name) { - return lookup.apply(name); - } - })); - return ()->field.set(null,previous); + Services previous = client.services(); + ProfileResolver resolver = new ProfileResolver() { + @Override public Optional fetchByName(String name) { + return lookup.apply(name).join(); + } + + @Override public Optional fetchById(java.util.UUID id) { + throw new AssertionError("Unexpected UUID-only fixture profile lookup"); + } + }; + Services owned = new Services(previous.sessionService(), previous.servicesKeySet(), + previous.profileRepository(), previous.nameToIdCache(), resolver); + field.set(client, owned); + return () -> field.set(client, previous); } catch(ReflectiveOperationException failure) { throw new IllegalStateException("Cannot bind the native fixture profile cache",failure); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java index 61752cd..f271473 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/FixtureStackData.java @@ -20,7 +20,7 @@ static net.minecraft.core.HolderLookup.Provider registries() { } static CompoundTag save(ItemStack stack) { - return (CompoundTag)stack.save(registries()); + return TestNativeNbt.saveStack(stack, registries()); } static ItemStack load(CompoundTag raw) { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java index 54ebb5a..10d874d 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/HealthAttributeChecks.java @@ -1,4 +1,7 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.nbt.Tag; import net.minecraft.nbt.StringTag; @@ -108,7 +111,7 @@ private static void check(String name,Check test){ finally{LootableBodies.corpseHP=hp;for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))e.discard();} } static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setUserName("HealthFixture");b.setItem(6,NbtRecoveryChecks.item(120,17));RuntimeChecks.disableVacuum(b);CompoundTag legacy=NameLifetimeChecks.saved(b);legacy.remove("attributes");return legacy;} - static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.load(n);RuntimeChecks.require(NameLifetimeChecks.same(before,n),"Input NBT mutated");return b;} + static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();EntityLootableBody b=new EntityLootableBody(world);FixtureApi.loadEntityData(b,n);RuntimeChecks.require(NameLifetimeChecks.same(before,n),"Input NBT mutated");return b;} static CompoundTag attribute(String name,double base){CompoundTag n=new CompoundTag();n.putString("Name",name);n.putDouble("Base",base);return n;} private static void legacyUuid(CompoundTag n,UUID id){n.remove("UUID");n.putLong("UUIDMost",id.getMostSignificantBits());n.putLong("UUIDLeast",id.getLeastSignificantBits());} static CompoundTag modifier(double amount,int operation){CompoundTag n=new CompoundTag();n.putString("Name","Fixture modifier");n.putDouble("Amount",amount);n.putInt("Operation",operation);legacyUuid(n,MOD);return n;} @@ -137,17 +140,17 @@ private static void check(String name,Check test){ private static void overflow(){CompoundTag a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(modifier(Double.MAX_VALUE,2)));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),30,"Overflowing modifier activated");retained(b,"Attribute",a);stable(b);} private static void validModifiers(){CompoundTag a=attribute("generic.maxHealth",30);CompoundTag m0=modifier(-2,0),m1=modifier(.5,1),m2=modifier(.25,2);legacyUuid(m1,new UUID(0,142));legacyUuid(m2,new UUID(0,143));a.put("Modifiers",NbtRecoveryChecks.list(m0,m1,m2));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),52.5,"Valid signed/three-operation modifiers changed");RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid modifiers quarantined");stable(b);} private static void zeroUuid(){CompoundTag a=attribute("generic.maxHealth",30),m=modifier(5,0);m.putLong("UUIDMost",0);m.putLong("UUIDLeast",0);m.putByte("Operation",(byte)0);m.putInt("Amount",5);a.put("Modifiers",NbtRecoveryChecks.list(m));EntityLootableBody b=read(with(a));value(b.getMaxHealth(),35,"Zero UUID or lossless numbers rejected");stable(b);} - private static void validBases(){for(Tag raw:new Tag[]{ByteTag.valueOf((byte)30),ShortTag.valueOf((short)30),IntTag.valueOf(30),LongTag.valueOf(30),FloatTag.valueOf(30.5F),DoubleTag.valueOf(30.5)}){CompoundTag a=attribute("generic.maxHealth",30);a.put("Base",raw);CompoundTag n=with(a);n.putDouble("Health",12.5);EntityLootableBody b=read(n);value(b.getMaxHealth(),((NumericTag)raw).getAsDouble(),"Valid base changed");value(b.getHealth(),12.5,"Fractional Health changed");stable(b);}} - private static void repeated(){EntityLootableBody b=read(with(attribute("generic.maxHealth",30)));CompoundTag n=base();n.remove("Attributes");n.remove("Health");n.remove("AbsorptionAmount");b.setAbsorptionAmount(5);b.load(n);value(b.getMaxHealth(),20,"Missing Attributes retained previous max");value(b.getHealth(),20,"Missing Health retained previous value");value(b.getAbsorptionAmount(),0,"Missing absorption retained previous value");stable(b);} - private static void repeatedModifiers(){CompoundTag a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(modifier(5,0)));EntityLootableBody b=read(with(a));b.load(with(attribute("generic.maxHealth",20)));value(b.getMaxHealth(),20,"Repeated read kept obsolete modifier");stable(b);} - private static void pendingRemoval(){CompoundTag n=base();n.putFloat("Health",0);EntityLootableBody b=read(n);b.baseTick();n=base();n.putDouble("Health",Double.NaN);b.load(n);b.baseTick();RuntimeChecks.require(b.isAlive(),"Repaired owner still dead");inventoryKept(b);stable(b);n=base();n.putFloat("Health",3.5F);b.discard();RuntimeChecks.require(b.isRemoved(),"Native removal fixture was not applied");b.load(n);RuntimeChecks.require(b.isAlive(),"Reused entity kept stale dead flag");value(b.getHealth(),3.5,"Reset healed valid wounded corpse");} - private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);n.putDouble("Health",Double.NaN);CompoundTag a=attribute("generic.maxHealth",Double.NaN);n.put("Attributes",NbtRecoveryChecks.list(a,attribute("generic.followRange",24)));EntityLootableBody b=read(n);RuntimeChecks.require(b.isAlive(),"Invalid health made recoverable inventory inaccessible");NbtRecoveryChecks.retained(b,raw);retained(b,"Attribute",a);retained(b,"Health",n.get("Health"));stable(b);stable(read(NameLifetimeChecks.saved(b)));} + private static void validBases(){for(Tag raw:new Tag[]{ByteTag.valueOf((byte)30),ShortTag.valueOf((short)30),IntTag.valueOf(30),LongTag.valueOf(30),FloatTag.valueOf(30.5F),DoubleTag.valueOf(30.5)}){CompoundTag a=attribute("generic.maxHealth",30);a.put("Base",raw);CompoundTag n=with(a);n.putDouble("Health",12.5);EntityLootableBody b=read(n);value(b.getMaxHealth(),((NumericTag)raw).doubleValue(),"Valid base changed");value(b.getHealth(),12.5,"Fractional Health changed");stable(b);}} + private static void repeated(){EntityLootableBody b=read(with(attribute("generic.maxHealth",30)));CompoundTag n=base();n.remove("Attributes");n.remove("Health");n.remove("AbsorptionAmount");b.setAbsorptionAmount(5);FixtureApi.loadEntityData(b,n);value(b.getMaxHealth(),20,"Missing Attributes retained previous max");value(b.getHealth(),20,"Missing Health retained previous value");value(b.getAbsorptionAmount(),0,"Missing absorption retained previous value");stable(b);} + private static void repeatedModifiers(){CompoundTag a=attribute("generic.maxHealth",30);a.put("Modifiers",NbtRecoveryChecks.list(modifier(5,0)));EntityLootableBody b=read(with(a));FixtureApi.loadEntityData(b,with(attribute("generic.maxHealth",20)));value(b.getMaxHealth(),20,"Repeated read kept obsolete modifier");stable(b);} + private static void pendingRemoval(){CompoundTag n=base();n.putFloat("Health",0);EntityLootableBody b=read(n);b.baseTick();n=base();n.putDouble("Health",Double.NaN);FixtureApi.loadEntityData(b,n);b.baseTick();RuntimeChecks.require(b.isAlive(),"Repaired owner still dead");inventoryKept(b);stable(b);n=base();n.putFloat("Health",3.5F);b.discard();RuntimeChecks.require(b.isRemoved(),"Native removal fixture was not applied");FixtureApi.loadEntityData(b,n);RuntimeChecks.require(b.isAlive(),"Reused entity kept stale dead flag");value(b.getHealth(),3.5,"Reset healed valid wounded corpse");} + private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");typedList(n, "Equipment", 10).add(raw);n.putDouble("Health",Double.NaN);CompoundTag a=attribute("generic.maxHealth",Double.NaN);n.put("Attributes",NbtRecoveryChecks.list(a,attribute("generic.followRange",24)));EntityLootableBody b=read(n);RuntimeChecks.require(b.isAlive(),"Invalid health made recoverable inventory inaccessible");NbtRecoveryChecks.retained(b,raw);retained(b,"Attribute",a);retained(b,"Health",n.get("Health"));stable(b);stable(read(NameLifetimeChecks.saved(b)));} private static void empty(){CompoundTag n=base();n.put("Equipment",new ListTag());n.putDouble("Health",Double.NaN);EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Health evidence counted as inventory");retained(b,"Health",n.get("Health"));} private static void healthy(){CompoundTag nativeSave=NameLifetimeChecks.saved(read(base()));EntityLootableBody b=read(nativeSave);RuntimeChecks.require(!NameLifetimeChecks.saved(b).contains(NameLifetimeChecks.RECOVERY),"Healthy save gained recovery records");stable(b);} private static void nativeUuid(boolean conflict,boolean malformed){ CompoundTag modifier=modifier(5,0);modifier.remove("UUIDMost");modifier.remove("UUIDLeast"); - modifier.putUUID("UUID",MOD); + writeUuid(modifier, "UUID", MOD); if(conflict){modifier.putLong("UUIDMost",3);modifier.putLong("UUIDLeast",4);} if(malformed)modifier.putIntArray("UUID",new int[]{1,2,3}); CompoundTag attribute=attribute("minecraft:generic.max_health",30);attribute.put("Modifiers",NbtRecoveryChecks.list(modifier)); @@ -157,11 +160,11 @@ private static void nativeUuid(boolean conflict,boolean malformed){ stable(body); } private static void legacyForgeAttributes(){ - String[][] names={{"forge.swimSpeed","forge:swim_speed"},{"forge.nameTagDistance","forge:nametag_distance"},{"forge.entity_gravity","minecraft:generic.gravity"}}; + String[][] names={{"forge.swimSpeed","forge:swim_speed"},{"forge.nameTagDistance","forge:nametag_distance"},{"forge.entity_gravity","minecraft:gravity"}}; double[] bases={1.75D,70D,.12D};ListTag list=new ListTag();for(int i=0;i before=new ArrayList<>(FixtureApi.entities(world)); - String keep=world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).toString(); - String mobLoot=world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).toString(); + String keep=world.getGameRules().get(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY).toString(); + String mobLoot=world.getGameRules().get(net.minecraft.world.level.gamerules.GameRules.MOB_DROPS).toString(); try { - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).set(false,null); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,false,null); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.MOB_DROPS,false,null); check.run(); results.add("PASS "+name); System.out.println("INVENTORY_CHECK_PASS "+name); } catch(Throwable error) { @@ -84,8 +84,8 @@ private static void check(String name,Check check) { System.out.println("INVENTORY_CHECK_FAIL "+name+": "+error); error.printStackTrace(); } finally { - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(Boolean.parseBoolean(keep),null); - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).set(Boolean.parseBoolean(mobLoot),null); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,Boolean.parseBoolean(keep),null); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.MOB_DROPS,Boolean.parseBoolean(mobLoot),null); for(Entity entity:new ArrayList(FixtureApi.entities(world))) if(!before.contains(entity)) entity.discard(); } @@ -109,21 +109,21 @@ private static ItemStack item(int id,int count) { } private static List drops(ServerPlayer player,ItemStack... stacks) { List result=new ArrayList<>(); - for(ItemStack stack:stacks) result.add(new ItemEntity(player.getCommandSenderWorld(),player.getX(),player.getY(),player.getZ(),stack)); + for(ItemStack stack:stacks) result.add(new ItemEntity(player.level(),player.getX(),player.getY(),player.getZ(),stack)); return result; } private static void handle(PlayerDeathEventHandler handler,ServerPlayer player,List drops) { handler.entityDropEvent(new LivingDropsEvent(player,player.damageSources().fellOutOfWorld(),drops,false)); } private static CompoundTag data(EntityLootableBody body) { - CompoundTag result=new CompoundTag(); body.addAdditionalSaveData(result); return result; + CompoundTag result=new CompoundTag(); FixtureApi.writeAdditional(body,result); return result; } private static CompoundTag encoded(ItemStack stack) { return FixtureStackData.save(stack); } private static Map inventory(EntityLootableBody body) { Map result=new TreeMap<>(); for(int i=0;i result,ItemStack stack) { @@ -141,30 +141,33 @@ private static EntityLootableBody newest(Set before) { throw new AssertionError("No corpse spawned"); } private static void failedSpawn() { - InventoryTestPlayer player=player(); RejectingWorld rejected=new RejectingWorld(world); - player.setFixtureLevel(rejected); + InventoryTestPlayer player=player(); RejectCorpse rejected=new RejectCorpse(); ItemStack bones=new ItemStack(Items.BONE,9); bones.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,net.minecraft.network.chat.Component.literal("Saved bones")); List drops=drops(player,bones,item(19,23)); List before=new ArrayList<>(); for(ItemEntity e:drops) before.add(encoded(e.getItem())); PlayerDeathEventHandler handler=new PlayerDeathEventHandler(); handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().fellOutOfWorld())); - handle(handler,player,drops); + var veto=EntityJoinLevelEvent.BUS.addListener(rejected::join); + try { handle(handler,player,drops); } + finally { EntityJoinLevelEvent.BUS.removeListener(veto); } RuntimeChecks.require(rejected.attempts==1,"Failed spawn branch was not reached"); RuntimeChecks.require(drops.size()==before.size(),"Rejected corpse consumed vanilla death drops"); for(int i=0;i expected=new TreeMap<>(); add(expected,player.getInventory().items.get(0));add(expected,player.getInventory().items.get(9)); + ServerPlayer player=player(); player.getInventory().getNonEquipmentItems().set(0,item(4,1)); player.getInventory().getNonEquipmentItems().set(9,item(19,23)); + Map expected=new TreeMap<>(); add(expected,player.getInventory().getNonEquipmentItems().get(0));add(expected,player.getInventory().getNonEquipmentItems().get(9)); Set before=new HashSet<>(FixtureApi.entities(world)); - RejectCorpse reject=new RejectCorpse(); MinecraftForge.EVENT_BUS.register(reject); - try { player.die(player.damageSources().fellOutOfWorld()); } finally { MinecraftForge.EVENT_BUS.unregister(reject); } + RejectCorpse reject=new RejectCorpse(); var listener=EntityJoinLevelEvent.BUS.addListener(reject::join); + try { player.die(player.damageSources().fellOutOfWorld()); } finally { EntityJoinLevelEvent.BUS.removeListener(listener); } RuntimeChecks.require(reject.attempts==1,"Corpse cancellation not exercised"); for(Entity entity:FixtureApi.entities(world)) RuntimeChecks.require(before.contains(entity) || !(entity instanceof EntityLootableBody),"Cancelled corpse spawn bypassed Forge cancellation"); RuntimeChecks.require(expected.equals(dropped()),"Cancelled corpse did not fall back to exact vanilla drops"); @@ -195,63 +198,82 @@ private static void cacheLifecycle() throws Exception { handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().fellOutOfWorld())); handle(handler,player,drops(player,player.getItemBySlot(EquipmentSlot.HEAD))); RuntimeChecks.require(!cache(handler).containsKey(player),"Equipment cache retains completed death"); - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(true,null); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,true,null); handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().fellOutOfWorld())); RuntimeChecks.require(!cache(handler).containsKey(player),"keepInventory death cached equipment"); - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); - LivingDeathEvent canceled=new LivingDeathEvent(player,player.damageSources().fellOutOfWorld()); canceled.setCanceled(true); - handler.entityDeathEvent(canceled); - RuntimeChecks.require(!cache(handler).containsKey(player),"Cancelled death cached equipment"); - handler.entityDeathEvent(new LivingDeathEvent(player,player.damageSources().fellOutOfWorld())); - List original=drops(player,item(19,23)); - LivingDropsEvent canceledDrops=new LivingDropsEvent(player,player.damageSources().fellOutOfWorld(),original,false); - canceledDrops.setCanceled(true);handler.entityDropEvent(canceledDrops); - RuntimeChecks.require(!cache(handler).containsKey(player) && original.size()==1 - && RuntimeChecks.sameStack(original.get(0).getItem(),item(19,23)),"Cancelled drops retained stale cache or changed inventory"); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,false,null); + Runnable unregister = handler.register(); + try { + var source = player.damageSources().fellOutOfWorld(); + RuntimeChecks.require(!LivingDeathEvent.BUS.post(new LivingDeathEvent(player, source)), + "Ordinary death was cancelled by an equipment-cache listener"); + RuntimeChecks.require(cache(handler).containsKey(player), "Ordinary death did not cache equipment"); + var vetoDeath = LivingDeathEvent.BUS.addListener(Priority.HIGHEST, event -> event.getEntity() == player); + try { + RuntimeChecks.require(LivingDeathEvent.BUS.post(new LivingDeathEvent(player, player.damageSources().fellOutOfWorld())), + "Native death was not cancelled"); + } finally { + LivingDeathEvent.BUS.removeListener(vetoDeath); + } + RuntimeChecks.require(!cache(handler).containsKey(player), "Cancelled death cached equipment"); + handler.entityDeathEvent(new LivingDeathEvent(player, player.damageSources().fellOutOfWorld())); + List original = drops(player, item(19,23)); + var vetoDrops = LivingDropsEvent.BUS.addListener(Priority.HIGHEST, event -> event.getEntity() == player); + try { + RuntimeChecks.require(LivingDropsEvent.BUS.post(new LivingDropsEvent(player, player.damageSources().fellOutOfWorld(), original, false)), + "Native drops were not cancelled"); + } finally { + LivingDropsEvent.BUS.removeListener(vetoDrops); + } + RuntimeChecks.require(!cache(handler).containsKey(player) && original.size()==1 + && RuntimeChecks.sameStack(original.get(0).getItem(),item(19,23)), "Cancelled drops retained stale cache or changed inventory"); + } finally { + unregister.run(); + } } public static final class RejectItems { boolean rejectAll; String rejectFixture; - @SubscribeEvent public void join(EntityJoinLevelEvent event) { - if(event.getEntity() instanceof ItemEntity) { - ItemStack stack=((ItemEntity)event.getEntity()).getItem(); - if(rejectAll || FixtureStackData.hasCustom(stack) && rejectFixture.equals(FixtureStackData.custom(stack).getString("fixture"))) event.setCanceled(true); - } + boolean join(EntityJoinLevelEvent event) { + if (!(event.getEntity() instanceof ItemEntity item)) return false; + ItemStack stack = item.getItem(); + return rejectAll || FixtureStackData.hasCustom(stack) + && rejectFixture.equals(FixtureStackData.custom(stack).getStringOr("fixture", "")); } } private static void failedRelease() throws Exception { EntityLootableBody corpse=body(); corpse.setItem(6,item(19,23)); RuntimeChecks.require(world.addFreshEntity(corpse),"Rejected-release owner spawn"); Map expected=inventory(corpse); - RejectItems reject=new RejectItems(); reject.rejectAll=true; MinecraftForge.EVENT_BUS.register(reject); - try { corpse.kill(); for(int i=0;i<25;i++) corpse.baseTick(); } - finally { MinecraftForge.EVENT_BUS.unregister(reject); } + RejectItems reject=new RejectItems(); reject.rejectAll=true; var listener=EntityJoinLevelEvent.BUS.addListener(reject::join); + try { corpse.kill(world); for(int i=0;i<25;i++) corpse.baseTick(); } + finally { EntityJoinLevelEvent.BUS.removeListener(listener); } RuntimeChecks.require(expected.equals(inventory(corpse)),"Rejected item spawn erased corpse inventory"); RuntimeChecks.require(corpse.isAlive() && FixtureApi.entities(world).contains(corpse),"Rejected release removed the only inventory owner"); - CompoundTag saved=data(corpse); EntityLootableBody loaded=body();loaded.readAdditionalSaveData(saved); + CompoundTag saved=data(corpse); EntityLootableBody loaded=body();FixtureApi.readAdditional(loaded,saved); RuntimeChecks.require(expected.equals(inventory(loaded)),"Rejected items do not survive NBT reload"); - corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); + corpse.kill(world);for(int i=0;i<12;i++)corpse.baseTick(); RuntimeChecks.require(expected.equals(dropped()),"Retry did not drop exact saved stacks once"); } private static void partialRelease() { EntityLootableBody corpse=body();corpse.setItem(6,item(19,23));corpse.setItem(7,item(20,11)); RuntimeChecks.require(world.addFreshEntity(corpse),"Partial-release owner spawn"); Map expected=inventory(corpse); - RejectItems reject=new RejectItems();reject.rejectFixture="slot-20";MinecraftForge.EVENT_BUS.register(reject); - try { corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); } - finally { MinecraftForge.EVENT_BUS.unregister(reject); } + RejectItems reject=new RejectItems();reject.rejectFixture="slot-20";var listener=EntityJoinLevelEvent.BUS.addListener(reject::join); + try { corpse.kill(world);for(int i=0;i<12;i++)corpse.baseTick(); } + finally { EntityJoinLevelEvent.BUS.removeListener(listener); } Map combined=new TreeMap<>(dropped());inventory(corpse).forEach((k,v)->combined.put(k,combined.getOrDefault(k,0)+v)); RuntimeChecks.require(expected.equals(combined),"Partial release lost/duplicated a stack"); RuntimeChecks.require((corpse.getItem(6)).isEmpty() && RuntimeChecks.sameStack(corpse.getItem(7),item(20,11)),"Partial release did not retain only rejected slot"); - corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); + corpse.kill(world);for(int i=0;i<12;i++)corpse.baseTick(); RuntimeChecks.require(expected.equals(dropped()),"Partial retry duplicated previously accepted drops"); } private static void vanillaEquipment() { - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBLOOT).set(true,null); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.MOB_DROPS,true,null); EntityLootableBody corpse=body();corpse.setItem(0,item(0,1));corpse.setDropChance(EquipmentSlot.HEAD,2F); RuntimeChecks.require(world.addFreshEntity(corpse),"Equipment-release owner spawn"); Map expected=inventory(corpse); - corpse.kill();for(int i=0;i<12;i++)corpse.baseTick(); + corpse.kill(world);for(int i=0;i<12;i++)corpse.baseTick(); RuntimeChecks.require(expected.equals(dropped()),"Vanilla mob equipment path duplicates corpse inventory"); } private static void vacuumOnce() { @@ -276,10 +298,10 @@ private static void emptyRemoval() { private static void repeatedRead() { EntityLootableBody source=body();for(int i=6;i<48;i++)source.setItem(i,item(i,1));source.addItem(item(70,7)); CompoundTag input=data(source),immutable=input.copy(); - EntityLootableBody target=body(); target.readAdditionalSaveData(input); target.readAdditionalSaveData(input); + EntityLootableBody target=body(); FixtureApi.readAdditional(target,input); FixtureApi.readAdditional(target,input); RuntimeChecks.require(data(target).get("Equipment").equals(input.get("Equipment")) && data(target).get("Aux").equals(input.get("Aux")),"Repeated read appended overflow or changed slots"); CompoundTag sparse=new CompoundTag();ListTag list=new ListTag();CompoundTag slot=encoded(item(19,23));slot.putByte("Slot",(byte)47);list.add(slot);sparse.put("Equipment",list); - target.readAdditionalSaveData(sparse); + FixtureApi.readAdditional(target,sparse); RuntimeChecks.require((target.getItem(6)).isEmpty() && RuntimeChecks.sameStack(target.getItem(47),item(19,23)) && !data(target).contains("Aux"),"Sparse reread kept stale inventory/overflow"); RuntimeChecks.require(immutable.equals(input),"Reader mutated source NBT"); } @@ -288,11 +310,11 @@ private static void staleAccess() { RuntimeChecks.require(container.stillValid(player),"Near living access failed"); player.setPos(100,80,100);RuntimeChecks.require(!container.stillValid(player),"Distant container remained accessible"); player.setPos(64.5,80,64.5);corpse.discard();RuntimeChecks.require(!container.stillValid(player),"Removed corpse remained accessible"); - corpse=body();container=new CorpseContainer(player.getInventory(),corpse);player.setFixtureLevel(new RejectingWorld(world)); + corpse=body();container=new CorpseContainer(player.getInventory(),corpse);player.setServerLevel(server.getLevel(Level.NETHER)); RuntimeChecks.require(!container.stillValid(player),"Cross-world container remained accessible"); - player.setFixtureLevel(world);player.setHealth(0); + player.setServerLevel(world);player.setHealth(0); RuntimeChecks.require(!container.stillValid(player),"Dead player retained container access"); - player.setHealth(player.getMaxHealth());corpse.kill();corpse.baseTick(); + player.setHealth(player.getMaxHealth());corpse.kill(world);corpse.baseTick(); RuntimeChecks.require(!container.stillValid(player),"Dying corpse remained accessible"); } private static void overflow() { @@ -316,7 +338,7 @@ private static void twoViewers() { CorpseContainer ca=new CorpseContainer(a.getInventory(),corpse),cb=new CorpseContainer(b.getInventory(),corpse); RuntimeChecks.require(RuntimeChecks.sameStack(ca.quickMoveStack(a,6),item(19,23)),"First recovery failed"); RuntimeChecks.require((cb.quickMoveStack(b,6)).isEmpty(),"Second viewer recovered the same stack"); - Map all=inventory(corpse);for(ItemStack s:a.getInventory().items)add(all,s);for(ItemStack s:b.getInventory().items)add(all,s); + Map all=inventory(corpse);for(ItemStack s:a.getInventory().getNonEquipmentItems())add(all,s);for(ItemStack s:b.getInventory().getNonEquipmentItems())add(all,s); Map expected=new TreeMap<>();add(expected,item(19,23));RuntimeChecks.require(expected.equals(all),"Two viewer recovery changed inventory/NBT"); } private static void emptyValidation() { @@ -326,7 +348,7 @@ private static void emptyValidation() { } private static net.minecraft.world.level.chunk.LevelChunk cleanChunk(EntityLootableBody corpse) { net.minecraft.world.level.chunk.LevelChunk chunk=world.getChunkAt(corpse.blockPosition()); - chunk.setUnsaved(false); + chunk.tryMarkSaved(); RuntimeChecks.require(!chunk.isUnsaved(),"Dirty-chunk test precondition"); return chunk; } @@ -355,9 +377,9 @@ static void seedBlockedRelease(ServerLevel target,File folder) throws Exception corpse.addItem(RuntimeChecks.stack(70));corpse.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(corpse); RuntimeChecks.require(target.addFreshEntity(corpse),"Blocked-release fixture spawn"); Map expected=inventory(corpse); - RejectItems reject=new RejectItems();reject.rejectAll=true;MinecraftForge.EVENT_BUS.register(reject); - try {corpse.kill();for(int i=0;i<25;i++)corpse.baseTick();} - finally {MinecraftForge.EVENT_BUS.unregister(reject);} + RejectItems reject=new RejectItems();reject.rejectAll=true;var listener=EntityJoinLevelEvent.BUS.addListener(reject::join); + try {corpse.kill(world);for(int i=0;i<25;i++)corpse.baseTick();} + finally {EntityJoinLevelEvent.BUS.removeListener(listener);} RuntimeChecks.require(corpse.isAlive() && FixtureApi.entities(target).contains(corpse) && expected.equals(inventory(corpse)),"Blocked-release fixture lost inventory/owner"); RuntimeChecks.saveExpected(folder,"rejected-release",corpse); } @@ -367,48 +389,5 @@ private static final class InventoryTestPlayer extends ServerPlayer { super(server,world,profile,net.minecraft.server.level.ClientInformation.createDefault()); } - void setFixtureLevel(Level world) { - setLevel(world); - } - } - - private static final class RejectingWorld extends Level { - private final ServerLevel delegate; int attempts; - RejectingWorld(ServerLevel target) { - super((net.minecraft.world.level.storage.WritableLevelData)target.getLevelData(),target.dimension(),target.registryAccess(),target.dimensionTypeRegistration(),()->net.minecraft.util.profiling.InactiveProfiler.INSTANCE,false,false,target.getSeed(),target.getServer().getMaxChainedNeighborUpdates()); - delegate=target; - } - @Override public net.minecraft.core.RegistryAccess registryAccess(){return delegate.registryAccess();} - @Override public net.minecraft.world.flag.FeatureFlagSet enabledFeatures(){return delegate.enabledFeatures();} - @Override public float getShade(net.minecraft.core.Direction direction,boolean shade){return delegate.getShade(direction,shade);} - @Override public ChunkSource getChunkSource(){return delegate.getChunkSource();} - @Override public net.minecraft.core.Holder getUncachedNoiseBiome(int x,int y,int z){return delegate.getUncachedNoiseBiome(x,y,z);} - - @Override public net.minecraft.world.ticks.LevelTickAccess getBlockTicks(){return delegate.getBlockTicks();} - @Override public net.minecraft.world.ticks.LevelTickAccess getFluidTicks(){return delegate.getFluidTicks();} - @Override public net.minecraft.world.TickRateManager tickRateManager(){return delegate.tickRateManager();} - @Override public net.minecraft.world.item.alchemy.PotionBrewing potionBrewing(){return delegate.potionBrewing();} - @Override public net.minecraft.world.item.crafting.RecipeManager getRecipeManager(){return delegate.getRecipeManager();} - @Override public net.minecraft.world.scores.Scoreboard getScoreboard(){return delegate.getScoreboard();} - @Override public net.minecraft.server.MinecraftServer getServer(){return delegate.getServer();} - @Override public long getGameTime(){return delegate==null?0:delegate.getGameTime();} - - @Override public void sendBlockUpdated(net.minecraft.core.BlockPos pos,net.minecraft.world.level.block.state.BlockState oldState,net.minecraft.world.level.block.state.BlockState state,int flags){delegate.sendBlockUpdated(pos,oldState,state,flags);} - @Override public void playSound(net.minecraft.world.entity.player.Player player,double x,double y,double z,net.minecraft.sounds.SoundEvent sound,net.minecraft.sounds.SoundSource category,float volume,float pitch){delegate.playSound(player,x,y,z,sound,category,volume,pitch);} - @Override public void playSound(net.minecraft.world.entity.player.Player player,Entity entity,net.minecraft.sounds.SoundEvent sound,net.minecraft.sounds.SoundSource category,float volume,float pitch){delegate.playSound(player,entity,sound,category,volume,pitch);} - @Override public void playSeededSound(net.minecraft.world.entity.player.Player player,double x,double y,double z,net.minecraft.core.Holder sound,net.minecraft.sounds.SoundSource category,float volume,float pitch,long seed){delegate.playSeededSound(player,x,y,z,sound,category,volume,pitch,seed);} - @Override public void playSeededSound(net.minecraft.world.entity.player.Player player,Entity entity,net.minecraft.core.Holder sound,net.minecraft.sounds.SoundSource category,float volume,float pitch,long seed){delegate.playSeededSound(player,entity,sound,category,volume,pitch,seed);} - @Override public Entity getEntity(int id){return delegate.getEntity(id);} - @Override public net.minecraft.world.level.saveddata.maps.MapItemSavedData getMapData(net.minecraft.world.level.saveddata.maps.MapId id){return delegate.getMapData(id);} - @Override public void setMapData(net.minecraft.world.level.saveddata.maps.MapId id,net.minecraft.world.level.saveddata.maps.MapItemSavedData data){delegate.setMapData(id,data);} - @Override public net.minecraft.world.level.saveddata.maps.MapId getFreeMapId(){return delegate.getFreeMapId();} - @Override public void destroyBlockProgress(int id,net.minecraft.core.BlockPos pos,int progress){delegate.destroyBlockProgress(id,pos,progress);} - @Override public void levelEvent(net.minecraft.world.entity.player.Player player,int type,net.minecraft.core.BlockPos pos,int data){delegate.levelEvent(player,type,pos,data);} - @Override public void gameEvent(Entity source,net.minecraft.core.Holder event,net.minecraft.core.BlockPos pos){delegate.gameEvent(source,event,pos);} - @Override public void gameEvent(net.minecraft.core.Holder event,net.minecraft.world.phys.Vec3 position,net.minecraft.world.level.gameevent.GameEvent.Context context){delegate.gameEvent(event,position,context);} - @Override public java.util.List players(){return delegate.players();} - @Override public String gatherChunkSourceStats(){return delegate.gatherChunkSourceStats();} - @Override public net.minecraft.world.level.entity.LevelEntityGetter getEntities(){return delegate.getEntities();} - @Override public boolean addFreshEntity(Entity entity){attempts++;return false;} } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java index 9d616e4..ab3e001 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/MultiplayerChecks.java @@ -1,6 +1,5 @@ package cyano.lootable.buildtest; import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; import net.minecraft.world.SimpleContainer; import net.minecraft.server.level.ServerLevel; import net.minecraft.nbt.ListTag; @@ -21,10 +20,16 @@ import net.minecraft.nbt.NbtIo; import net.minecraft.server.MinecraftServer; import net.minecraft.core.BlockPos; -import net.minecraft.world.level.GameRules; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketType; +import net.minecraft.network.protocol.common.CommonPacketTypes; +import net.minecraft.network.protocol.common.ServerCommonPacketListener; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraftforge.event.network.CustomPayloadEvent; +import java.util.function.Consumer; +import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.level.GameType; -import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; import net.minecraftforge.network.ChannelBuilder; import net.minecraftforge.network.NetworkDirection; @@ -44,29 +49,54 @@ public static void registerChannel() { CHANNEL.messageBuilder(Step.class,0,NetworkDirection.PLAY_TO_SERVER) .encoder((message,buffer)->buffer.writeInt(message.number)) .decoder(buffer->new Step(buffer.readInt())) - .consumerMainThread((message,context)->acknowledge(message,context.getSender())).add(); + .consumer((message, context) -> enqueuePlayAction(context, + player -> acknowledge(message, player))).add(); CHANNEL.messageBuilder(FixtureDeath.class,1,NetworkDirection.PLAY_TO_SERVER) - .encoder((message,buffer)->{}) - .decoder(buffer->new FixtureDeath()) - .consumerMainThread((message,context)->{ - RuntimeChecks.require(Boolean.getBoolean("lootablebodies.issueClientCheck") - && context.getSender()!=null,"Unexpected fixture death signal"); - context.getSender().kill(); - }).add(); + .encoder((message,buffer) -> buffer.writeInt(message.selectedSlot())) + .decoder(buffer -> new FixtureDeath(buffer.readInt())) + .consumer((message, context) -> enqueuePlayAction(context, player -> { + RuntimeChecks.require(Boolean.getBoolean("lootablebodies.issueClientCheck"), + "Unexpected fixture death signal"); + RuntimeChecks.require(player.getInventory().getSelectedSlot() == message.selectedSlot(), + "Native carried-item packet did not precede the fixture death"); + player.kill(player.level()); + RuntimeChecks.require(!player.isAlive(), "Ordered native fixture death did not take effect"); + System.out.println("NATIVE_ORDERED_HELD_ITEM_DEATH_PASS slot=" + message.selectedSlot()); + })).add(); CHANNEL.build(); } - public static final class FixtureDeath { } + + private static void enqueuePlayAction(CustomPayloadEvent.Context context, Consumer action) { + var listener = context.getConnection().getPacketListener(); + RuntimeChecks.require(listener instanceof ServerGamePacketListenerImpl, "Missing native play listener"); + var server = net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(); + RuntimeChecks.require(server != null, "Missing native packet processor"); + server.packetProcessor().scheduleIfPossible((ServerCommonPacketListener) listener, new NativePlayAction(action)); + context.setPacketHandled(true); + } + + private record NativePlayAction(Consumer action) implements Packet { + @Override public PacketType> type() { + return CommonPacketTypes.SERVERBOUND_CUSTOM_PAYLOAD; + } + + @Override public void handle(ServerCommonPacketListener listener) { + action.accept(((ServerGamePacketListenerImpl) listener).player); + } + } + + public record FixtureDeath(int selectedSlot) { } public MultiplayerChecks(MinecraftServer server) throws Exception { this.server=server;sync=sync(); LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1; setPhase(0);Files.write(new File(sync,"server.ready").toPath(),"ready\n".getBytes(StandardCharsets.UTF_8)); - net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); + TickEvent.ServerTickEvent.Post.BUS.addListener(this::tick); } public static File sync(){return new File(System.getProperty("lootablebodies.multiplayer.sync"));} private ServerPlayer player(String role){return server.getPlayerList().getPlayerByName("Container"+role);} private boolean ack(String role,int step){return new File(sync,"Container"+role+".step-"+step).isFile();} - @SubscribeEvent public void tick(TickEvent.ServerTickEvent e){ - if(e.phase!=TickEvent.Phase.END || finishing)return; + public void tick(TickEvent.ServerTickEvent.Post e){ + if(finishing)return; try{ RuntimeChecks.require(++ticks<2400,"Two-client timeout phase="+phase); if(new File(sync,"abort").exists())throw new AssertionError("Runner aborted multiplayer fixture"); @@ -74,7 +104,7 @@ public MultiplayerChecks(MinecraftServer server) throws Exception { 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().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); + 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); body.setItem(6,stack(19,23));RuntimeChecks.require(w.addFreshEntity(body),"Shared corpse spawn"); a.setGameMode(GameType.SURVIVAL);b.setGameMode(GameType.SURVIVAL);a.getInventory().clearContent();b.getInventory().clearContent(); @@ -86,7 +116,7 @@ else if(phase==2 && ack("A",2) && ack("B",2)){ RuntimeChecks.require(count(a,19)==0 || count(b,19)==0,"Both clients recovered the same stack"); mark("two_tcp_clients_contested_shift_click"); a.getInventory().clearContent();b.getInventory().clearContent();body.clearContent(); - a.containerMenu.setCarried(stack(19,10));b.getInventory().items.set(0,stack(20,7));refresh(a);refresh(b);setPhase(3); + a.containerMenu.setCarried(stack(19,10));b.getInventory().getNonEquipmentItems().set(0,stack(20,7));refresh(a);refresh(b);setPhase(3); }else if(phase==3 && ack("A",3))setPhase(4); else if(phase==4 && ack("B",4)){ equal(body.getItem(6),stack(20,7),"TCP hotbar swap failed");setPhase(5); @@ -117,7 +147,7 @@ private void setPhase(int value) throws Exception { private static int count(ServerPlayer p,int index){int n=0;ItemStack expected=RuntimeChecks.stack(index);for(int i=0;i true); + net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre.BUS.addListener(event -> true); + TickEvent.ClientTickEvent.Post.BUS.addListener(this::tick); + }} + private void tick(TickEvent.ClientTickEvent.Post event){ + Minecraft mc=Minecraft.getInstance(); if(mc.isWindowActive())mc.mouseHandler.releaseMouse();net.minecraft.client.KeyMapping.releaseAll(); try{ if(finished){mc.stop();return;} @@ -47,7 +47,7 @@ final class MultiplayerClientChecks { if(phase==8){ try(FileWriter out=new FileWriter(new File(mc.gameDirectory,"multiplayer-client.pass"))){out.write("two-client packet/GUI checks passed\n");} System.out.println("LOOTABLE_BODIES_MULTIPLAYER_CLIENT_PASS role="+role); - finished=true;mc.player.closeContainer();mc.level.disconnect();mc.disconnect();mc.setScreen(new TitleScreen());return; + finished=true;mc.player.closeContainer();FixtureConnections.disconnect(mc);return; } 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)){ @@ -63,7 +63,7 @@ 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); }else if(phase==4 && role.equals("B")){ - if(!RuntimeChecks.sameStack(mc.player.getInventory().items.get(0),MultiplayerChecks.stack(20,7)))return; + 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);} else if(phase==6 && role.equals("A")){ @@ -76,5 +76,5 @@ else if(phase==6 && role.equals("A")){ }catch(Throwable error){error.printStackTrace();mc.stop();} } private void ack(int phase){completed.add(phase);MultiplayerChecks.CHANNEL.send(new MultiplayerChecks.Step(phase),net.minecraftforge.network.PacketDistributor.SERVER.noArg());} - private void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString()));} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java index 2751d3f..e2994bb 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NameLifetimeChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.nbt.Tag; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.ShortTag; @@ -96,42 +98,42 @@ private static void check(String name,Check test) { catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("NAME_LIFETIME_FAIL "+name+": "+e);e.printStackTrace();} finally{((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(clock);LootableBodies.ticksPerItemDecay=rate;LootableBodies.corpseDecayTime=life;LootableBodies.allowCorpseDecay=decay;LootableBodies.decayOnlyWhenEmpty=empty;for(Entity e:new ArrayList(FixtureApi.entities(world)))if(!before.contains(e))e.discard();} } - static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setCustomNameTag("Saved Owner");b.setItem(6,NbtRecoveryChecks.item(120,17));CompoundTag n=new CompoundTag();b.saveAsPassenger(n);n.putString("Name","Saved Owner");n.putByte("Vac",(byte)20);return n;} - static EntityLootableBody read(CompoundTag n){CompoundTag copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.load(n);RuntimeChecks.require(same(n,copy),"Input NBT mutated");return b;} - static CompoundTag saved(EntityLootableBody b){CompoundTag n=new CompoundTag();b.saveAsPassenger(n);return n;} - static void retained(EntityLootableBody b,String key,Tag raw){ListTag l=saved(b).getList(RECOVERY,10);boolean found=false;for(int i=0;i=0 && saved(b).getLong("DecayTime")>=0 && termination(b)<0,"Negative world clock caused invalid lifetime");} + private static void future(){CompoundTag n=base();n.putLong("DeathTime",Long.MAX_VALUE);n.putLong("DecayTime",Long.MAX_VALUE);EntityLootableBody b=read(n);LootableBodies.ticksPerItemDecay=10;((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(10011);b.baseTick();RuntimeChecks.require(saved(b).getLongOr("DecayTime", 0L)==10010,"Future timestamp stalled normal progression");} + private static void rollback(){EntityLootableBody b=timed(9999);((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(100);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=false;LootableBodies.ticksPerItemDecay=10;b.baseTick();RuntimeChecks.require(saved(b).getLongOr("DeathTime", 0L)==100 && saved(b).getLongOr("DecayTime", 0L)==100 && termination(b)<0,"Clock rollback did not rebase safely");} + private static void negativeClock(){EntityLootableBody b=timed(0);((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(Long.MIN_VALUE);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=false;LootableBodies.ticksPerItemDecay=10;b.baseTick();RuntimeChecks.require(saved(b).getLongOr("DeathTime", 0L)>=0 && saved(b).getLongOr("DecayTime", 0L)>=0 && termination(b)<0,"Negative world clock caused invalid lifetime");} private static EntityLootableBody tool(long now,long interval){((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(now);EntityLootableBody b=timed(0);b.clearContent();ItemStack tool=new ItemStack(Items.DIAMOND_PICKAXE);tool.setDamageValue(17);b.setItem(6,tool);LootableBodies.ticksPerItemDecay=interval;return b;} - private static void catchup(long now,long interval){EntityLootableBody b=tool(now,interval);b.baseTick();ItemStack s=b.getItem(6);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getCount()==1 && s.getDamageValue()==s.getMaxDamage()-1,"Catchup overflow repaired, erased or under-decayed tool");long stamp=now-now%interval;RuntimeChecks.require(saved(b).getLong("DecayTime")==stamp,"Catchup timestamp wrapped or lost remainder");b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==stamp && s.getDamageValue()==s.getMaxDamage()-1,"Catchup repeated");} + private static void catchup(long now,long interval){EntityLootableBody b=tool(now,interval);b.baseTick();ItemStack s=b.getItem(6);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getCount()==1 && s.getDamageValue()==s.getMaxDamage()-1,"Catchup overflow repaired, erased or under-decayed tool");long stamp=now-now%interval;RuntimeChecks.require(saved(b).getLongOr("DecayTime", 0L)==stamp,"Catchup timestamp wrapped or lost remainder");b.baseTick();RuntimeChecks.require(saved(b).getLongOr("DecayTime", 0L)==stamp && s.getDamageValue()==s.getMaxDamage()-1,"Catchup repeated");} private static void largeCatchup(){catchup((1L<<31)+1,1);} private static void maxCatchup(){catchup(Long.MAX_VALUE,6000);} - private static void longInterval(){EntityLootableBody b=tool(Long.MAX_VALUE,Long.MAX_VALUE/2);b.baseTick();RuntimeChecks.require(saved(b).getLong("DecayTime")==Long.MAX_VALUE-1 && b.getItem(6).getDamageValue()==19,"Wide interval arithmetic changed");} + private static void longInterval(){EntityLootableBody b=tool(Long.MAX_VALUE,Long.MAX_VALUE/2);b.baseTick();RuntimeChecks.require(saved(b).getLongOr("DecayTime", 0L)==Long.MAX_VALUE-1 && b.getItem(6).getDamageValue()==19,"Wide interval arithmetic changed");} private static void boundary(){EntityLootableBody b=tool(10,10);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=false;LootableBodies.corpseDecayTime=10;b.baseTick();RuntimeChecks.require(b.getItem(6).getDamageValue()==17 && termination(b)<0,"Strict > boundary changed");((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(11);b.baseTick();RuntimeChecks.require(b.getItem(6).getDamageValue()==18 && termination(b)>=0,"Elapsed threshold ignored");} - private static void occupied(){EntityLootableBody b=timed(0);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=true;LootableBodies.corpseDecayTime=40;b.baseTick();RuntimeChecks.require(saved(b).getLong("DeathTime")==10000 && termination(b)<0,"Occupied default no longer pauses expiration");} - private static void disabled(){EntityLootableBody b=tool(Long.MAX_VALUE,-1);b.baseTick();RuntimeChecks.require(b.getItem(6).getDamageValue()==17 && saved(b).getLong("DecayTime")==0 && saved(b).getLong("DeathTime")==0,"Disabled timer changed state");} + private static void occupied(){EntityLootableBody b=timed(0);LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=true;LootableBodies.corpseDecayTime=40;b.baseTick();RuntimeChecks.require(saved(b).getLongOr("DeathTime", 0L)==10000 && termination(b)<0,"Occupied default no longer pauses expiration");} + private static void disabled(){EntityLootableBody b=tool(Long.MAX_VALUE,-1);b.baseTick();RuntimeChecks.require(b.getItem(6).getDamageValue()==17 && saved(b).getLongOr("DecayTime", 0L)==0 && saved(b).getLongOr("DeathTime", 0L)==0,"Disabled timer changed state");} private static int termination(EntityLootableBody b){try{Field f=EntityLootableBody.class.getDeclaredField("terminate");f.setAccessible(true);return f.getInt(b);}catch(Exception e){throw new AssertionError(e);}} private static void metadataOnly(){CompoundTag n=base();n.put("Equipment",new ListTag());n.putString("DeathTime","opaque");EntityLootableBody b=read(n);RuntimeChecks.require(b.isEmpty(),"Scalar evidence counted as inventory");LootableBodies.allowCorpseDecay=true;LootableBodies.decayOnlyWhenEmpty=true;LootableBodies.corpseDecayTime=40;((net.minecraft.world.level.storage.ServerLevelData)world.getLevelData()).setGameTime(10041);b.baseTick();RuntimeChecks.require(termination(b)>=0,"Scalar evidence made empty corpse immortal");} - private static void inventory(){CompoundTag n=base();CompoundTag missing=NbtRecoveryChecks.entry(121,11,7);missing.putString("id","absentfixture:missing");n.getList("Equipment",10).add(missing);n.putString("DeathTime","bad");n.put("Name",new CompoundTag());EntityLootableBody b=read(n);NbtRecoveryChecks.retained(b,missing);stable(b);b.load(saved(b));stable(b);RuntimeChecks.require(saved(b).getList(RECOVERY,10).size()==2 && saved(b).getList(NbtRecoveryChecks.RECOVERY,10).size()==1,"Raw scalar/item evidence accumulated or disappeared");} - private static void unknownRecovery(){CompoundTag n=base(),record=new CompoundTag();record.putString("Future","opaque");n.put(RECOVERY,NbtRecoveryChecks.list(record));EntityLootableBody b=read(n);RuntimeChecks.require(saved(b).getList(RECOVERY,10).getCompound(0).equals(record),"Unknown metadata evidence changed");stable(b);} + private static void inventory(){CompoundTag n=base();CompoundTag missing=NbtRecoveryChecks.entry(121,11,7);missing.putString("id","absentfixture:missing");typedList(n, "Equipment", 10).add(missing);n.putString("DeathTime","bad");n.put("Name",new CompoundTag());EntityLootableBody b=read(n);NbtRecoveryChecks.retained(b,missing);stable(b);FixtureApi.loadEntityData(b,saved(b));stable(b);RuntimeChecks.require(saved(b).getListOrEmpty(RECOVERY).size()==2 && saved(b).getListOrEmpty(NbtRecoveryChecks.RECOVERY).size()==1,"Raw scalar/item evidence accumulated or disappeared");} + private static void unknownRecovery(){CompoundTag n=base(),record=new CompoundTag();record.putString("Future","opaque");n.put(RECOVERY,NbtRecoveryChecks.list(record));EntityLootableBody b=read(n);RuntimeChecks.require(saved(b).getListOrEmpty(RECOVERY).getCompoundOrEmpty(0).equals(record),"Unknown metadata evidence changed");stable(b);} private static void wrongRecovery(){CompoundTag n=base();StringTag raw=StringTag.valueOf("opaque");n.put(RECOVERY,raw);EntityLootableBody b=read(n);retained(b,"Recovery",raw);stable(b);} private static void healthy(){EntityLootableBody b=read(base());b.baseTick();stable(b);RuntimeChecks.require(!saved(b).contains(RECOVERY),"Healthy save gained invalid-field data");} // DoubleNBT.equals uses ==, so NaN copies are unequal without any mutation. diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java index 150d6b7..c944129 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NamelessCorpseClientChecks.java @@ -21,14 +21,13 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.nbt.CompoundTag; import net.minecraft.world.InteractionHand; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.client.Screenshot; import net.minecraft.core.BlockPos; -import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.level.GameType; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.eventbus.api.SubscribeEvent; /** Exercise the actual client locale, entity label, spawn egg and saved name. */ final class NamelessCorpseClientChecks { @@ -41,8 +40,13 @@ final class NamelessCorpseClientChecks { private boolean originalUnicode, originalBidi; private boolean renderedBody; - @SubscribeEvent public void rendered(RenderLivingEvent.Post event) { - if(event.getEntity().getUUID().equals(BODY)) renderedBody=true; + private void rendered(RenderLivingEvent.Post event) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level != null && event.getRenderer() instanceof cyano.lootable.graphics.CorpseRenderer + && event.getState() instanceof net.minecraft.client.renderer.entity.state.AvatarRenderState state + && mc.level.getEntity(state.id) instanceof EntityLootableBody body && body.getUUID().equals(BODY)) { + renderedBody = true; + } } static void englishContract(Minecraft mc) throws Exception { @@ -51,7 +55,7 @@ static void englishContract(Minecraft mc) throws Exception { private static String translation(Minecraft mc, String code) throws Exception { com.google.gson.JsonObject values; - try(Reader in=new InputStreamReader(mc.getResourceManager().getResource(ResourceLocation.fromNamespaceAndPath("lootablebodies","lang/"+code+".json")).orElseThrow().open(),StandardCharsets.UTF_8)) { + try(Reader in=new InputStreamReader(mc.getResourceManager().getResource(Identifier.fromNamespaceAndPath("lootablebodies","lang/"+code+".json")).orElseThrow().open(),StandardCharsets.UTF_8)) { values=new com.google.gson.JsonParser().parse(in).getAsJsonObject(); } String expected=values.get("entity.lootablebodies.Corpse.name").getAsString(); @@ -71,7 +75,7 @@ private static void assertLocale(Minecraft mc, String code) throws Exception { retained.putString("Source","FutureFixture"); retained.putString("Data","retain verbatim"); data.put("LootableBodiesUnresolved",NbtRecoveryChecks.list(retained)); body = (EntityLootableBody)FixtureApi.loadEntity(data,mc.level); - RuntimeChecks.require(body != null && ResourceLocation.parse("lootablebodies:corpse").equals(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(body.getType())), "Registry/display identity changed"); + RuntimeChecks.require(body != null && Identifier.parse("lootablebodies:corpse").equals(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getKey(body.getType())), "Registry/display identity changed"); CompoundTag before = NameLifetimeChecks.saved(body); RuntimeChecks.require(expected.equals(body.getName().getString()), "Untranslated nameless corpse in "+code+": "+body.getName().getString()); RuntimeChecks.require(expected.equals(body.getDisplayName().getString()), "Wrong rendered name in "+code); @@ -100,12 +104,14 @@ void tick(Minecraft mc) throws Exception { mc.options.pauseOnLostFocus=false;mc.options.framerateLimit().set(30);mc.options.renderDistance().set(4); originalLanguage=mc.getLanguageManager().getSelected(); originalUnicode=mc.options.forceUnicodeFont().get(); originalBidi=mc.font.isBidirectional(); - MinecraftForge.EVENT_BUS.register(this); + RenderLivingEvent.Post.BUS.addListener(this::rendered); FixtureApi.ClientWorlds.create(mc,"name-fixture","Name fixture",net.minecraft.world.level.GameType.CREATIVE,false);stage=1; - } else if(stage==1 && mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null) { + } else if (stage == 1 && mc.level != null && mc.player != null && mc.getSingleplayerServer() != null + && !(mc.screen instanceof net.minecraft.client.gui.screens.LevelLoadingScreen)) { pending=mc.getSingleplayerServer().submitAsync(() -> { ServerLevel world=mc.getSingleplayerServer().getLevel(FixtureApi.dimensionKey(0)); - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); + RuntimeChecks.awaitChunk(world, 1, 1); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.SPAWN_MOBS,false,null); for(int x=12;x<=24;x++)for(int z=12;z<=24;z++)world.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); EntityLootableBody body=new EntityLootableBody(world);body.setUUID(BODY);body.setPos(18.5,80,18.5);body.setNoGravity(true);RuntimeChecks.disableVacuum(body); for(int i=0;i<48;i++)body.setItem(i,RuntimeChecks.stack(i)); @@ -114,7 +120,8 @@ void tick(Minecraft mc) throws Exception { net.minecraft.server.level.ServerPlayer player=mc.getSingleplayerServer().getPlayerList().getPlayer(mc.player.getUUID()); player.getAbilities().flying=true;player.onUpdateAbilities();player.connection.teleport(18.5,80,14.5,0,12); });stage=2;frames=0; - } else if(stage==2 && pending.isDone() && ++frames>30) { + } else if (stage == 2 && pending.isDone() && ++frames > 30 + && FixtureApi.entities(mc.level).stream().anyMatch(entity -> entity.getUUID().equals(BODY))) { pending.get();stage=3;frames=0; } else if(stage==3) { // Keep the disposable view attached to the current network player. @@ -135,7 +142,7 @@ void tick(Minecraft mc) throws Exception { if(frames==15 && Arrays.asList("en_us","de_de","en_pt","es_es","zh_cn").contains(code)) { System.out.println("NAME_CAMERA player="+mc.player.blockPosition()+" view="+mc.getCameraEntity().blockPosition()+" yaw="+mc.player.getYRot()+" pitch="+mc.player.getXRot()+" rendered="+renderedBody); RuntimeChecks.require(renderedBody,"No corpse rendered for label screenshot "+code); - Screenshot.grab(mc.gameDirectory,"name-"+code+".png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + Screenshot.grab(mc.gameDirectory,"name-"+code+".png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); } if(frames>20) {frames=0;locale++;if(locale==LOCALES.length) { mc.getLanguageManager().setSelected(originalLanguage);mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager()); @@ -148,7 +155,7 @@ void tick(Minecraft mc) throws Exception { } } else if(stage==5 && mc.screen instanceof CorpseGUIContainer && ++frames>20) { for(int slot=0;slot<48;slot++)RuntimeChecks.require(RuntimeChecks.sameStack(mc.player.containerMenu.getSlot(slot).getItem(),RuntimeChecks.stack(slot)),"Name fix changed network inventory slot "+slot); - Screenshot.grab(mc.gameDirectory,"name-english-inventory.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + Screenshot.grab(mc.gameDirectory,"name-english-inventory.png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); new File(mc.gameDirectory,"client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_NAME_TRANSLATION_PASS locales="+LOCALES.length); mc.player.closeContainer();FixtureConnections.disconnect(mc);stage=6; } else if(stage==6 && FixtureConnections.stopped(mc)) {mc.stop();stage=7;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java index 5884e40..818aad8 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/Native118BiomeChecks.java @@ -1,5 +1,8 @@ package cyano.lootable.buildtest; +import static cyano.lootable.buildtest.TestNativeNbt.hasType; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import java.util.Optional; import net.minecraft.core.Holder; import net.minecraft.core.Registry; @@ -10,48 +13,51 @@ import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.PalettedContainer; -import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; +import net.minecraft.world.level.chunk.PalettedContainerFactory; +import net.minecraft.world.level.chunk.PalettedContainerRO; /** Compare every old quart sample by native biome identity, not obsolete registry IDs. */ final class Native118BiomeChecks { private Native118BiomeChecks() { } static void verify(ServerLevel world,CompoundTag record,int wave) { - if(record.contains("Sections",9)) {verifyNative18Sections(world,record,wave);return;} - int x=record.getInt("ChunkX"),z=record.getInt("ChunkZ"); + if(hasType(record, "Sections", 9)) {verifyNative18Sections(world,record,wave);return;} + int x=record.getIntOr("ChunkX", 0),z=record.getIntOr("ChunkZ", 0); CompoundTag root=new CompoundTag(),level=new CompoundTag();root.putInt("DataVersion",2730);root.put("Level",level); level.putInt("xPos",x);level.putInt("zPos",z);level.putString("Status","full");level.put("Sections",new ListTag()); - level.putIntArray("Biomes",HistoricalBiomes.nativeValues(record.getIntArray("Data"))); - ChunkStorage.injectDatafixingContext(root,world.dimension(),Optional.empty()); + level.putIntArray("Biomes",HistoricalBiomes.nativeValues(record.getIntArray("Data").orElseGet(() -> new int[0]))); + CompoundTag context = new CompoundTag(); + context.putString("dimension",world.dimension().identifier().toString()); + SimpleRegionStorage.injectDatafixingContext(root,context); CompoundTag nativeResult=cyano.lootable.upgrade.NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,root,2730,3465); - Registry registry=world.registryAccess().registryOrThrow(net.minecraft.core.registries.Registries.BIOME); - com.mojang.serialization.Codec>> codec=PalettedContainer.codecRW(registry.asHolderIdMap(),registry.holderByNameCodec(),PalettedContainer.Strategy.SECTION_BIOMES,registry.getHolderOrThrow(Biomes.PLAINS)); + var codec = PalettedContainerFactory.create(world.registryAccess()).biomeContainerCodec(); LevelChunk chunk=world.getChunk(x,z);int samples=0; - for(net.minecraft.nbt.Tag entry:nativeResult.getList("sections",10)) { - CompoundTag section=(CompoundTag)entry;int sectionY=section.getInt("Y"); + for(net.minecraft.nbt.Tag entry:typedList(nativeResult, "sections", 10)) { + CompoundTag section=(CompoundTag)entry;int sectionY=section.getIntOr("Y", 0); if(sectionY<0 || sectionY>=16)continue; // All 1024 samples from the saved 0..255 range. - PalettedContainer> expected=codec.parse(NbtOps.INSTANCE,section.getCompound("biomes")) .getOrThrow(message->{throw new AssertionError(message);}); + PalettedContainerRO> expected=codec.parse(NbtOps.INSTANCE,section.getCompoundOrEmpty("biomes")) .getOrThrow(message->{throw new AssertionError(message);}); for(int y=0;y<4;y++)for(int zz=0;zz<4;zz++)for(int xx=0;xx<4;xx++) { Holder actual=chunk.getNoiseBiome(xx,sectionY*4+y,zz); - RuntimeChecks.require(expected.get(xx,y,zz).unwrapKey().equals(actual.unwrapKey()),"Historical biome changed in dimension "+record.getInt("Dimension")+" wave "+wave+" section "+sectionY+" sample "+xx+","+y+","+zz); + RuntimeChecks.require(expected.get(xx,y,zz).unwrapKey().equals(actual.unwrapKey()),"Historical biome changed in dimension "+record.getIntOr("Dimension", 0)+" wave "+wave+" section "+sectionY+" sample "+xx+","+y+","+zz); samples++; } } RuntimeChecks.require(samples==1024,"Native biome conversion omitted saved quart samples"); } private static void verifyNative18Sections(ServerLevel world,CompoundTag record,int wave) { - LevelChunk chunk=world.getChunk(record.getInt("ChunkX"),record.getInt("ChunkZ")); - ListTag sections=record.getList("Sections",10);java.util.Set seen=new java.util.HashSet<>();int samples=0; + LevelChunk chunk=world.getChunk(record.getIntOr("ChunkX", 0),record.getIntOr("ChunkZ", 0)); + ListTag sections=typedList(record, "Sections", 10);java.util.Set seen=new java.util.HashSet<>();int samples=0; RuntimeChecks.require(sections.size()==chunk.getSectionsCount(),"Native18 saved biome section coverage changed"); for(net.minecraft.nbt.Tag entry:sections) { - CompoundTag section=(CompoundTag)entry;int y=section.getInt("Y"); - RuntimeChecks.require(section.contains("Y",3) && seen.add(y) && y>=chunk.getMinSection() && y=chunk.getMinSectionY() && y<=chunk.getMaxSectionY(),"Native18 biome section identity changed"); net.minecraft.nbt.Tag raw=section.get("Names"); - RuntimeChecks.require(raw instanceof ListTag && ((ListTag)raw).getElementType()==8 && ((ListTag)raw).size()==64,"Native18 biome samples are incomplete"); + RuntimeChecks.require(TestNativeNbt.typedList(section,"Names",8).size()==64,"Native18 biome samples are incomplete"); ListTag names=(ListTag)raw;int index=0; for(int yy=0;yy<4;yy++)for(int zz=0;zz<4;zz++)for(int xx=0;xx<4;xx++) { - String expected=names.getString(index++); - String actual=chunk.getNoiseBiome(xx,y*4+yy,zz).unwrapKey().orElseThrow().location().toString(); - RuntimeChecks.require(expected.equals(actual),"Native18 biome identity changed in dimension "+record.getInt("Dimension")+" wave "+wave+" section "+y+" sample "+xx+","+yy+","+zz); + String expected=names.getStringOr(index++, ""); + String actual=chunk.getNoiseBiome(xx,y*4+yy,zz).unwrapKey().orElseThrow().identifier().toString(); + RuntimeChecks.require(expected.equals(actual),"Native18 biome identity changed in dimension "+record.getIntOr("Dimension", 0)+" wave "+wave+" section "+y+" sample "+xx+","+yy+","+zz); samples++; } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java index 0ee8a29..4d424de 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NativePreflightChecks.java @@ -1,5 +1,7 @@ package cyano.lootable.buildtest; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import com.mojang.datafixers.*; import com.mojang.datafixers.schemas.Schema; import com.mojang.serialization.Dynamic; @@ -7,7 +9,7 @@ import java.util.concurrent.atomic.AtomicInteger; import net.minecraft.server.MinecraftServer; import net.minecraft.nbt.*; -import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; /** Controlled packaged pre-world conversion/refusal; separate from old writers. */ public final class NativePreflightChecks { @@ -15,16 +17,18 @@ public final class NativePreflightChecks { private static final AtomicInteger early=new AtomicInteger(),worlds=new AtomicInteger(),starts=new AtomicInteger(); private static String mode; private static boolean installed; - static void install(net.minecraftforge.eventbus.api.IEventBus bus) { + static void install(BusGroup bus) { mode=System.getProperty("lootablebodies.nativePreflight",""); if(mode.isEmpty())return; if(!mode.equals("refuse") && !mode.equals("convert"))throw new IllegalArgumentException("Unknown preflight control"); - net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(new NativePreflightChecks()); + var check = new NativePreflightChecks(); + net.minecraftforge.event.level.LevelEvent.Load.BUS.addListener(check::loaded); + net.minecraftforge.event.server.ServerAboutToStartEvent.BUS.addListener(check::starting); Runtime.getRuntime().addShutdownHook(new Thread(()->{ try {Files.writeString(Path.of("preflight-control.receipt"),"mode="+mode+" earlyConversions="+early.get()+" worldLoads="+worlds.get()+" serverStarts="+starts.get()+"\n",StandardOpenOption.CREATE_NEW);} catch(Exception failure){failure.printStackTrace();} },"PreflightControlReceipt")); - bus.addListener((net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent event)->event.enqueueWork(()->{ + net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent.getBus(bus).addListener(event->event.enqueueWork(()->{ installed=true; System.out.println("NATIVE121_PREFLIGHT_CONTROL_INSTALLED mode="+mode); })); @@ -48,8 +52,8 @@ public Dynamic update(DSL.TypeReference type,Dynamic input,int from,in } }; } - @SubscribeEvent public void loaded(net.minecraftforge.event.level.LevelEvent.Load event){worlds.incrementAndGet();} - @SubscribeEvent public void starting(net.minecraftforge.event.server.ServerAboutToStartEvent event){starts.incrementAndGet();} + public void loaded(net.minecraftforge.event.level.LevelEvent.Load event){worlds.incrementAndGet();} + public void starting(net.minecraftforge.event.server.ServerAboutToStartEvent event){starts.incrementAndGet();} static void verify(MinecraftServer server) throws Exception { RuntimeChecks.require(mode.equals("convert") && early.get()>0,"No actual pre-world entity conversion observed"); net.minecraft.server.level.ServerLevel world=server.overworld();RuntimeChecks.awaitChunk(world,0,0); @@ -62,22 +66,22 @@ static void verify(MinecraftServer server) throws Exception { // Existing placement rules preserve those invalid records in Aux. if(slot<4){RuntimeChecks.require(item.isEmpty(),"Invalid diamond entered an armour slot");continue;} RuntimeChecks.require(item.getItem()==net.minecraft.world.item.Items.DIAMOND && item.getCount()==slot+1 - && FixtureStackData.hasCustom(item) && FixtureStackData.custom(item).getString("keep").equals("slot-"+slot),"Preflight inventory changed at slot"+slot); + && FixtureStackData.hasCustom(item) && FixtureStackData.custom(item).getStringOr("keep", "").equals("slot-"+slot),"Preflight inventory changed at slot"+slot); } - CompoundTag saved=new CompoundTag();body.saveAsPassenger(saved); - RuntimeChecks.require(saved.getInt("LootableBodiesDataVersion")==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Preflight corpse marker was not advanced"); - ListTag aux=saved.getList("Aux",10);RuntimeChecks.require(aux.size()==5,"Preflight Aux record count changed"); + CompoundTag saved=new CompoundTag();FixtureApi.saveEntity(body,saved); + RuntimeChecks.require(saved.getIntOr("LootableBodiesDataVersion", 0)==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Preflight corpse marker was not advanced"); + ListTag aux=typedList(saved, "Aux", 10);RuntimeChecks.require(aux.size()==5,"Preflight Aux record count changed"); for(int index=0;index<5;index++) { - int sourceSlot=index<4?index:47;CompoundTag item=aux.getCompound(index); - RuntimeChecks.require(item.getString("id").equals("minecraft:diamond") && item.getInt("count")==sourceSlot+1 - && item.getCompound("components").getCompound("minecraft:custom_data").getString("keep").equals("slot-"+sourceSlot),"Preflight Aux changed at record"+index); + int sourceSlot=index<4?index:47;CompoundTag item=aux.getCompoundOrEmpty(index); + RuntimeChecks.require(item.getStringOr("id", "").equals("minecraft:diamond") && item.getIntOr("count", 0)==sourceSlot+1 + && item.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:custom_data").getStringOr("keep", "").equals("slot-"+sourceSlot),"Preflight Aux changed at record"+index); } - RuntimeChecks.require(saved.getList("LootableBodiesUnresolved",10).stream().anyMatch(value->value instanceof CompoundTag - && ((CompoundTag)value).getString("Source").equals("FixtureUnknown") - && ((CompoundTag)value).getCompound("Data").getString("id").equals("minecraft:diamond") - && ((CompoundTag)value).getCompound("Data").getByte("Count")==47 - && ((CompoundTag)value).getCompound("Data").getByte("Slot")==46 - && ((CompoundTag)value).getCompound("Data").getCompound("tag").getString("keep").equals("slot-46")),"Raw Source/Data recovery evidence changed"); + RuntimeChecks.require(typedList(saved, "LootableBodiesUnresolved", 10).stream().anyMatch(value->value instanceof CompoundTag + && ((CompoundTag)value).getStringOr("Source", "").equals("FixtureUnknown") + && ((CompoundTag)value).getCompoundOrEmpty("Data").getStringOr("id", "").equals("minecraft:diamond") + && ((CompoundTag)value).getCompoundOrEmpty("Data").getByteOr("Count", (byte) 0)==47 + && ((CompoundTag)value).getCompoundOrEmpty("Data").getByteOr("Slot", (byte) 0)==46 + && ((CompoundTag)value).getCompoundOrEmpty("Data").getCompoundOrEmpty("tag").getStringOr("keep", "").equals("slot-46")),"Raw Source/Data recovery evidence changed"); Files.writeString(Path.of("preflight-conversion.pass"),"48inputEquipmentRecords/Aux/Source/Data/entityStorage conserved\n",StandardOpenOption.CREATE_NEW); System.out.println("NATIVE121_PACKAGED_PREFLIGHT_CONVERSION_PASS"); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java index f27c963..bbc3cad 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtClientChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.world.inventory.Slot; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.SimpleContainer; @@ -44,16 +46,18 @@ boolean tick(Minecraft mc) throws Exception { frames++;RuntimeChecks.require(frames<300,"NBT graphical stage timeout "+stage); if(stage==0){schedule(mc,()->{ ServerPlayer p=player(mc);p.getInventory().clearContent();p.containerMenu.setCarried(ItemStack.EMPTY);p.setGameMode(GameType.SURVIVAL); - RuntimeChecks.awaitChunk(p.serverLevel(),COORDINATE>>4,COORDINATE>>4); + RuntimeChecks.awaitChunk(p.level(),COORDINATE>>4,COORDINATE>>4); for(int x=COORDINATE-3;x<=COORDINATE+11;x++)for(int z=COORDINATE-3;z<=COORDINATE+3;z++)p.level().setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); - RuntimeChecks.require(p.serverLevel().getEntities(null,new net.minecraft.world.phys.AABB(COORDINATE,79,COORDINATE-2,COORDINATE+16,83,COORDINATE+4)).isEmpty(),"Graphical malformed fixture chunk is not isolated"); - NbtWorldChecks.seed(p.serverLevel(),bodyId,COORDINATE,folder(mc),"graphical"); + RuntimeChecks.require(p.level().getEntities(null,new net.minecraft.world.phys.AABB(COORDINATE,79,COORDINATE-2,COORDINATE+16,83,COORDINATE+4)).isEmpty(),"Graphical malformed fixture chunk is not isolated"); + NbtWorldChecks.seed(p.level(),bodyId,COORDINATE,folder(mc),"graphical"); });next(1);} else if(stage==1 && done() && frames>20){quit(mc);next(2);} else if(stage==2 && FixtureConnections.stopped(mc)){mc.createWorldOpenFlows().openWorld("build-fixture",()->mc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(3);} else if(stage==3 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(COORDINATE+8.5,80,COORDINATE-.5,0,0));next(30);} - else if(stage==30 && done() && frames>20){schedule(mc,()->{ - RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions==1,"Missing exact first native graphical disk admission"); + else if(stage==30 && done() && frames>20 && trackedOnClient(mc)){schedule(mc,()->{ + RuntimeChecks.awaitChunk(player(mc).level(), COORDINATE >> 4, COORDINATE >> 4); + RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions == 1, + "Missing exact first native graphical disk admission: " + NbtWorldChecks.graphicalAdmissions); EntityLootableBody b=body(mc);for(int i=0;i<8;i++)b.baseTick();NbtWorldChecks.verify(b,false); before=NbtRecoveryChecks.items(b);visible=full(b);RuntimeChecks.saveExpected(folder(mc),"nbt-client-before",b); });next(4);} @@ -63,19 +67,21 @@ else if(stage==6 && frames>25){schedule(mc,()->{ Map after=NbtRecoveryChecks.items(body(mc));for(int i=0;imc.setScreen(new net.minecraft.client.gui.screens.TitleScreen()));next(9);} else if(stage==9 && ready(mc) && frames>20){schedule(mc,()->player(mc).connection.teleport(COORDINATE+8.5,80,COORDINATE-.5,0,0));next(90);} - else if(stage==90 && done() && frames>20){schedule(mc,()->{ - RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions==2,"Missing exact second native graphical disk admission"); + else if(stage==90 && done() && frames>20 && trackedOnClient(mc)){schedule(mc,()->{ + RuntimeChecks.awaitChunk(player(mc).level(), COORDINATE >> 4, COORDINATE >> 4); + RuntimeChecks.require(NbtWorldChecks.graphicalAdmissions == 2, + "Missing exact second native graphical disk admission: " + NbtWorldChecks.graphicalAdmissions); body(mc).baseTick();RuntimeChecks.verifyExpected(folder(mc),"nbt-client-after",body(mc)); RuntimeChecks.require(Objects.equals(visible.get(NbtRecoveryChecks.RECOVERY),NbtRecoveryChecks.saved(body(mc)).get(NbtRecoveryChecks.RECOVERY)),"Unresolved raw data changed on graphical disk reload"); NbtWorldChecks.verifyMetadata(body(mc));RuntimeChecks.require(NameLifetimeChecks.same(visible.get(NameLifetimeChecks.RECOVERY),full(body(mc)).get(NameLifetimeChecks.RECOVERY)),"Raw scalar evidence changed on graphical reload"); - RuntimeChecks.require(expectedPlayer.equals(player(mc).getInventory().save(new ListTag())),"Recovered player NBT changed on graphical disk reload");checkPlayer(player(mc).getInventory());mark(mc,"malformed_corpse_and_player_disk_reload"); + RuntimeChecks.require(expectedPlayer.equals(FixtureApi.saveInventory(player(mc))),"Recovered player NBT changed on graphical disk reload");checkPlayer(player(mc).getInventory());mark(mc,"malformed_corpse_and_player_disk_reload"); });next(10);} else if(stage==10 && done() && frames>20){if(open(mc))next(11);} else if(stage==11 && gui(mc) && frames>20){verifyGui(mc);checkPlayer(mc.player.getInventory());screenshot(mc,"nbt-after-recovery-reload.png");System.out.println("LOOTABLE_BODIES_NBT_CLIENT_PASS");return true;} @@ -89,21 +95,29 @@ else if(stage==90 && done() && frames>20){schedule(mc,()->{ } return false; } - private void verifyGui(Minecraft mc){ItemStack[] expected=new ItemStack[48];ListTag list=visible.getList("Equipment",10);for(int i=0;i actual=new TreeMap<>();for(int i=0;i expected=new TreeMap<>();add(expected,NbtRecoveryChecks.item(120,17));RuntimeChecks.require(actual.equals(expected),"Malformed GUI recovered player stack changed");} private static void add(Map map,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=FixtureStackData.save(s);n.remove("count");String key=n.toString();map.put(key,map.getOrDefault(key,0)+s.getCount());} private static void equal(ItemStack a,ItemStack b,String why){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),why);} - private static CompoundTag full(EntityLootableBody b){CompoundTag n=new CompoundTag();b.saveAsPassenger(n);return n;} + private static CompoundTag full(EntityLootableBody b){CompoundTag n=new CompoundTag();FixtureApi.saveEntity(b,n);return n;} private ServerPlayer player(Minecraft mc){return mc.getSingleplayerServer().getPlayerList().getPlayer(playerId);} private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(mc.getSingleplayerServer().getLevel(FixtureApi.dimensionKey(0)),bodyId);} - private File folder(Minecraft mc){return player(mc).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile();} - private static boolean ready(Minecraft mc){return mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null;} + private File folder(Minecraft mc){return player(mc).level().getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile();} + private static boolean ready(Minecraft mc) { + return mc.level != null && mc.player != null && mc.getSingleplayerServer() != null + && !(mc.screen instanceof net.minecraft.client.gui.screens.LevelLoadingScreen); + } private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} + private boolean trackedOnClient(Minecraft mc) { + if (mc.level == null || mc.screen instanceof net.minecraft.client.gui.screens.LevelLoadingScreen) return false; + return FixtureApi.entities(mc.level).stream().anyMatch(entity -> 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 void schedule(Minecraft mc,Action action){pending=mc.getSingleplayerServer().submitAsync(()->{try{action.run();}catch(Exception e){throw new RuntimeException(e);}});} private boolean done() throws Exception{if(!pending.isDone())return false;pending.get();return true;} private void next(int value){stage=value;frames=0;System.out.println("NBT_CLIENT_STAGE "+stage);} - private static void screenshot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private static void screenshot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString()));} private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} private static void mark(Minecraft mc,String name) throws Exception{try(FileWriter out=new FileWriter(new File(mc.gameDirectory,"nbt-client-checks.txt"),true)){out.write("PASS "+name+"\n");}System.out.println("NBT_CLIENT_CHECK_PASS "+name);} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java index ca41fc9..eb516a0 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/NbtRecoveryChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; import java.io.*; @@ -92,17 +94,17 @@ private static void check(String name,Check test){ static CompoundTag entry(int index,int count,int slot){CompoundTag n=FixtureStackData.save(item(index,1));n.putInt("count",count);n.putByte("Slot",(byte)slot);return n;} static ListTag list(CompoundTag... entries){ListTag l=new ListTag();for(CompoundTag n:entries)l.add(n.copy());return l;} static CompoundTag root(CompoundTag... entries){CompoundTag n=new CompoundTag();n.put("Equipment",list(entries));n.putByte("Vac",(byte)20);return n;} - static CompoundTag saved(EntityLootableBody b){CompoundTag n=new CompoundTag();b.addAdditionalSaveData(n);return n;} - static EntityLootableBody read(CompoundTag n){CompoundTag copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);b.readAdditionalSaveData(n);RuntimeChecks.require(n.equals(copy),"Reader modified source NBT");return b;} + static CompoundTag saved(EntityLootableBody b){CompoundTag n=new CompoundTag();FixtureApi.writeAdditional(b,n);return n;} + static EntityLootableBody read(CompoundTag n){CompoundTag copy=n.copy();EntityLootableBody b=new EntityLootableBody(world);FixtureApi.readAdditional(b,n);RuntimeChecks.require(n.equals(copy),"Reader modified source NBT");return b;} private static void equal(ItemStack a,ItemStack b,String why){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),why);} private static void add(Map sums,ItemStack s){if((s == null || s.isEmpty()) || s.getCount()<=0)return;CompoundTag n=FixtureStackData.save(s.copyWithCount(1));n.remove("count");String k=n.toString();sums.put(k,sums.getOrDefault(k,0)+s.getCount());} - static Map items(EntityLootableBody b){Map sums=new TreeMap<>();for(int i=0;i<48;i++)add(sums,b.getItem(i));ListTag aux=saved(b).getList("Aux",10);for(int i=0;i items(EntityLootableBody b){Map sums=new TreeMap<>();for(int i=0;i<48;i++)add(sums,b.getItem(i));ListTag aux=saved(b).getListOrEmpty("Aux");for(int i=0;i sums=new TreeMap<>();for(ItemStack s:expected)add(sums,s);RuntimeChecks.require(items(b).equals(sums),"Recoverable item/count/damage/full NBT changed: expected="+sums+" actual="+items(b)+" retained="+saved(b).get(RECOVERY));} - static void retained(EntityLootableBody b,Tag raw){ListTag records=saved(b).getList(RECOVERY,10);boolean found=false;for(int i=0;i>4,coordinate>>4);RawBody b=new RawBody(w);b.setUUID(id);b.setPos(coordinate+.5,80,coordinate+.5);b.setNoGravity(true);b.setUserName("NbtFixture");b.setRotation(37);RuntimeChecks.disableVacuum(b); for(int i=0;i<48;i++)b.setItem(i,known(i));RuntimeChecks.require(w.addFreshEntity(b),"Raw fixture spawn"); - CompoundTag n=new CompoundTag();RuntimeChecks.require(b.saveAsPassenger(n),"Raw fixture serialization"); + CompoundTag n=new CompoundTag();RuntimeChecks.require(FixtureApi.saveEntity(b,n),"Raw fixture serialization"); try(FileOutputStream out=new FileOutputStream(new File(folder,"raw-"+name+".nbt"))){NbtIo.writeCompressed(n,out);} } private static ItemStack known(int i){return i==6?NbtRecoveryChecks.item(120,17):NbtRecoveryChecks.item(i,i<6?1:64);} @@ -86,24 +91,25 @@ private static CompoundTag rawEquipment(){ n.putLong("DeathTime",Long.MIN_VALUE); n.putDouble("DecayTime",Double.NaN); CompoundTag name=new CompoundTag();name.putString("Opaque","saved malformed attribution");n.put("Name",name); - CompoundTag health=HealthAttributeChecks.malformedFields();for(String key:health.getAllKeys())n.put(key,health.get(key).copy()); - CompoundTag effects=EffectHurtChecks.malformedFields();for(String key:effects.getAllKeys())n.put(key,effects.get(key).copy()); - CompoundTag rotation=RotationFallChecks.malformedFields();for(String key:rotation.getAllKeys())n.put(key,rotation.get(key).copy()); + CompoundTag health=HealthAttributeChecks.malformedFields();for(String key:health.keySet())n.put(key,health.get(key).copy()); + CompoundTag effects=EffectHurtChecks.malformedFields();for(String key:effects.keySet())n.put(key,effects.get(key).copy()); + n.remove("fall_distance");CompoundTag rotation=RotationFallChecks.malformedFields();for(String key:rotation.keySet())n.put(key,rotation.get(key).copy()); return n; } private static CompoundTag missing(int index,int count,int slot){CompoundTag n=NbtRecoveryChecks.entry(index,count,slot);n.putString("id","absentfixture:missing");return n;} private static final class RawBody extends EntityLootableBody { RawBody(Level w){super(w);} - @Override public boolean saveAsPassenger(CompoundTag n){if(isRemoved())return false;n.putString("id","lootablebodies:corpse");saveWithoutId(n);return true;} - @Override public void addAdditionalSaveData(CompoundTag n){ - super.addAdditionalSaveData(n); + @Override public boolean saveAsPassenger(net.minecraft.world.level.storage.ValueOutput output){if(isRemoved())return false;output.putString("id","lootablebodies:corpse");saveWithoutId(output);return true;} + @Override public void addAdditionalSaveData(net.minecraft.world.level.storage.ValueOutput output){ + super.addAdditionalSaveData(output); // Replace the native fields so the synthetic legacy records are the records read from disk. - n.remove("attributes"); - n.remove("active_effects"); - CompoundTag raw=rawEquipment(); - for(String key:raw.getAllKeys())n.put(key,raw.get(key).copy()); + output.discard("attributes"); + output.discard("active_effects"); + output.discard("fall_distance"); + CompoundTag n=rawEquipment(); n.put("Pos",PositionMotionChecks.vector(Double.NaN,80,getZ())); n.put("Motion",PositionMotionChecks.vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY)); + cyano.lootable.upgrade.NativeNbt.writeRawTags(output, n); } } private static void settle(EntityLootableBody b){for(int i=0;i<8;i++)b.baseTick();} @@ -116,11 +122,11 @@ static void verify(EntityLootableBody b,boolean restored){ if(restored){CompoundTag raw=NbtRecoveryChecks.entry(123,9,7);raw.putString("id",RETURNING);ItemStack s=FixtureStackData.load(raw);RuntimeChecks.require((s != null && !s.isEmpty()) && s.getItem()==returning,"Returning fixture item not registered");add(expected,s);} RuntimeChecks.require(expected.equals(NbtRecoveryChecks.items(b)),"Disk recovery item/count/damage/custom NBT lost or duplicated"); CompoundTag source=rawEquipment();int rawCount=0; - for(String list:new String[]{"Equipment","Aux"}){ListTag entries=source.getList(list,10);for(int i=0;i=0 && saved.getLong("DeathTime")<=b.level().getGameTime() && saved.getLong("DecayTime")>=0 && saved.getLong("DecayTime")<=b.level().getGameTime(),"Malformed disk timers entered active state"); + RuntimeChecks.require(b.getCustomNameTag().equals("NbtFixture") && saved.getStringOr("Name", "").equals("NbtFixture"),"Malformed Name erased vanilla fallback attribution"); + RuntimeChecks.require(!saved.contains("Vac") && saved.getLongOr("DeathTime", 0L)>=0 && saved.getLongOr("DeathTime", 0L)<=b.level().getGameTime() && saved.getLongOr("DecayTime", 0L)>=0 && saved.getLongOr("DecayTime", 0L)<=b.level().getGameTime(),"Malformed disk timers entered active state"); for(String key:new String[]{"Name","Vac","DeathTime","DecayTime"})NameLifetimeChecks.retained(b,key,raw.get(key)); - RuntimeChecks.require(saved.getList(NameLifetimeChecks.RECOVERY,10).size()==16,"Disk metadata evidence grew or disappeared"); + RuntimeChecks.require(typedList(saved, NameLifetimeChecks.RECOVERY, 10).size()==16,"Disk metadata evidence grew or disappeared"); } private static void snapshot(File folder,String name,EntityLootableBody b) throws Exception {RuntimeChecks.saveExpected(folder,"nbt-"+name,b);} private static void verifySnapshot(File folder,String name,EntityLootableBody b) throws Exception { diff --git a/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java index 04e25ff..906c056 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PortBaselineChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.world.inventory.Slot; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.IntTag; @@ -23,7 +25,6 @@ import com.mojang.authlib.GameProfile; import net.minecraftforge.event.entity.EntityJoinLevelEvent; import net.minecraftforge.event.entity.living.LivingDropsEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; /** Small behavioral red/green gates, runnable against the unmodified 1.11 source. */ public final class PortBaselineChecks { @@ -47,7 +48,7 @@ public static void run(MinecraftServer server) throws Exception { require(RespawnPacketTrace.describe(sent).equals(RespawnPacketTrace.describe(received)),"Spawn observer differs before/after native decoding"); require(RespawnPacketTrace.describe(received).contains("mod=lootablebodies "),"Negative control cannot identify corpse spawns"); buffer.clear(); - net.minecraft.network.protocol.game.ClientboundRespawnPacket outgoing=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(new net.minecraft.network.protocol.game.CommonPlayerSpawnInfo(world.dimensionTypeRegistration(),world.dimension(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,java.util.Optional.empty(),0),net.minecraft.network.protocol.game.ClientboundRespawnPacket.KEEP_ALL_DATA); + net.minecraft.network.protocol.game.ClientboundRespawnPacket outgoing=new net.minecraft.network.protocol.game.ClientboundRespawnPacket(new net.minecraft.network.protocol.game.CommonPlayerSpawnInfo(world.dimensionTypeRegistration(),world.dimension(),0x13572468ABCDEF01L,GameType.SURVIVAL,GameType.CREATIVE,false,true,java.util.Optional.empty(),0,63),net.minecraft.network.protocol.game.ClientboundRespawnPacket.KEEP_ALL_DATA); net.minecraft.network.protocol.game.ClientboundRespawnPacket.STREAM_CODEC.encode(buffer,outgoing); net.minecraft.network.protocol.game.ClientboundRespawnPacket incoming=net.minecraft.network.protocol.game.ClientboundRespawnPacket.STREAM_CODEC.decode(buffer); require(RespawnPacketTrace.describe(outgoing).equals(RespawnPacketTrace.describe(incoming)),"Respawn observer differs before/after native decoding"); @@ -58,47 +59,44 @@ public static void run(MinecraftServer server) throws Exception { ServerPlayer player=new ServerPlayer(server,world,new GameProfile(UUID.randomUUID(),"PortFixture"),net.minecraft.server.level.ClientInformation.createDefault()); ArrayList drops=new ArrayList<>(); drops.add(new ItemEntity(world,8,80,8,new ItemStack(Items.DIAMOND,7))); - Object veto=new Object() { @SubscribeEvent public void join(EntityJoinLevelEvent e) { - if(e.getEntity() instanceof EntityLootableBody) e.setCanceled(true); - }}; - MinecraftForge.EVENT_BUS.register(veto); + var veto = EntityJoinLevelEvent.BUS.addListener(e -> e.getEntity() instanceof EntityLootableBody); try { new PlayerDeathEventHandler().entityDropEvent(new LivingDropsEvent(player,player.damageSources().generic(),drops,false)); } - finally { MinecraftForge.EVENT_BUS.unregister(veto); } + finally { EntityJoinLevelEvent.BUS.removeListener(veto); } require(drops.size()==1 && drops.get(0).getItem().getCount()==7,"original drops were discarded"); }); check("duplicate_slots_preserve_both_records", () -> { EntityLootableBody body=new EntityLootableBody(world); - CompoundTag nbt=new CompoundTag(); body.addAdditionalSaveData(nbt); + CompoundTag nbt=new CompoundTag(); FixtureApi.writeAdditional(body,nbt); ListTag equipment=new ListTag(); for(int i=0;i<2;i++) { CompoundTag item=FixtureStackData.save(new ItemStack(Items.DIAMOND,i+2)); item.putByte("Slot",(byte)10); equipment.add(item); } - nbt.put("Equipment",equipment); body.readAdditionalSaveData(nbt); - CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); + nbt.put("Equipment",equipment); FixtureApi.readAdditional(body,nbt); + CompoundTag saved=new CompoundTag();FixtureApi.writeAdditional(body,saved); int count=0; for(String key:Arrays.asList("Equipment","Aux")) { - ListTag list=saved.getList(key,10); - for(int i=0;i { - EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); + EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();FixtureApi.writeAdditional(body,nbt); CompoundTag item=FixtureStackData.save(new ItemStack(Items.DIAMOND,7));item.putString("id","missingfixture:keepsake");item.putByte("Slot",(byte)10); - ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);body.readAdditionalSaveData(nbt); - CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); - require(saved.getList("LootableBodiesUnresolved",10).size()==1,"missing item was erased"); + ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);FixtureApi.readAdditional(body,nbt); + CompoundTag saved=new CompoundTag();FixtureApi.writeAdditional(body,saved); + require(typedList(saved, "LootableBodiesUnresolved", 10).size()==1,"missing item was erased"); require(!body.isEmpty(),"recoverable item allowed empty-body decay"); }); check("invalid_slot_retains_inventory", () -> { - EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); + EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();FixtureApi.writeAdditional(body,nbt); CompoundTag item=FixtureStackData.save(new ItemStack(Items.EMERALD,9));item.putInt("Slot",12345); - ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);body.readAdditionalSaveData(nbt); - CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); + ListTag list=new ListTag();list.add(item);nbt.put("Equipment",list);FixtureApi.readAdditional(body,nbt); + CompoundTag saved=new CompoundTag();FixtureApi.writeAdditional(body,saved); require(!body.isEmpty(),"invalid slot discarded inventory"); }); check("malformed_name_and_timer_are_retained", () -> { - EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();body.addAdditionalSaveData(nbt); - nbt.put("Name",IntTag.valueOf(17));nbt.putDouble("DeathTime",Double.NaN);body.readAdditionalSaveData(nbt); - CompoundTag saved=new CompoundTag();body.addAdditionalSaveData(saved); - require(saved.getList("LootableBodiesInvalidFields",10).size()==2,"malformed fields lost without recovery"); + EntityLootableBody body=new EntityLootableBody(world); CompoundTag nbt=new CompoundTag();FixtureApi.writeAdditional(body,nbt); + nbt.put("Name",IntTag.valueOf(17));nbt.putDouble("DeathTime",Double.NaN);FixtureApi.readAdditional(body,nbt); + CompoundTag saved=new CompoundTag();FixtureApi.writeAdditional(body,saved); + require(typedList(saved, "LootableBodiesInvalidFields", 10).size()==2,"malformed fields lost without recovery"); }); System.out.println("PORT_BASELINE_RESULT failures="+failures); require(failures==0,"Port baseline regressions: "+failures); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java index df231af..f651b14 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/PositionMotionChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.nbt.Tag; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.ShortTag; @@ -33,7 +35,6 @@ import net.minecraft.world.phys.AABB; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.chunk.storage.ChunkStorage; /** Validate before vanilla builds geometry, then exercise actual chunk admission. */ final class PositionMotionChecks { @@ -93,16 +94,16 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex private static void signed(){CompoundTag n=base();n.put("Pos",vector(-160.25,500.5,-160.75));n.put("Motion",vector(-2.25,.125,-.5));EntityLootableBody b=read(n);sameValue(b.getX(),-160.25,"Negative X");sameValue(b.getY(),500.5,"High altitude");sameValue(b.getZ(),-160.75,"Negative Z");require(!saved(b).contains(NameLifetimeChecks.RECOVERY),"Valid signed position quarantined");stable(b);} 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");b.load(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 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 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);} private static void healthyChunk(){CompoundTag n=base();EntityLootableBody b=chunk(n,10,10);sameValue(b.getX(),160.5,"Healthy chunk position moved");RuntimeChecks.awaitChunk(world,11,10);b.setPos(176.5,80,160.5);sameValue(b.getX(),176.5,"Healthy later movement was anchored");require(!b.isRemoved() && FixtureApi.sectionContains(b,11,10),"Healthy later movement killed or failed native section indexing");stable(b);} - private static void notRevived(boolean previousInvalid){CompoundTag n=base();if(previousInvalid)n.put("Pos",vector(Double.NaN,80,160.5));EntityLootableBody b=read(n);if(previousInvalid)b.load(base());RuntimeChecks.awaitChunk(world,10,10);require(world.addFreshEntity(b),"Removal fixture was not admitted");b.discard();RuntimeChecks.awaitChunk(world,11,10);b.setPos(176.5,80,160.5);require(b.isRemoved() && b.getX()==176.5 && !FixtureApi.sectionContains(b,11,10),"Position hook revived ordinary removal or kept an obsolete repair");b.revive();stable(b);} + private static void notRevived(boolean previousInvalid){CompoundTag n=base();if(previousInvalid)n.put("Pos",vector(Double.NaN,80,160.5));EntityLootableBody b=read(n);if(previousInvalid)FixtureApi.loadEntityData(b,base());RuntimeChecks.awaitChunk(world,10,10);require(world.addFreshEntity(b),"Removal fixture was not admitted");b.discard();RuntimeChecks.awaitChunk(world,11,10);b.setPos(176.5,80,160.5);require(b.isRemoved() && b.getX()==176.5 && !FixtureApi.sectionContains(b,11,10),"Position hook revived ordinary removal or kept an obsolete repair");b.revive();stable(b);} private static void deadHealth(){CompoundTag n=base();n.put("Pos",vector(Double.NaN,80,160.5));n.putFloat("Health",0);EntityLootableBody b=chunk(n,10,10);require(b.getHealth()==0 && !b.isAlive(),"Position repair healed legitimate zero health");kept(b);} private static void empty(){CompoundTag n=base();n.put("Equipment",new ListTag());n.remove("Aux");n.putString("Motion","opaque");EntityLootableBody b=read(n);require(b.isEmpty(),"Geometry evidence counts as items");NameLifetimeChecks.retained(b,"Motion",n.get("Motion"));} - private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);n.put("Pos",vector(Double.NaN,80,160.5));n.put("Motion",vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY));EntityLootableBody b=chunk(n,10,10);require(b.isAlive(),"Corrupt geometry made inventory inaccessible");NbtRecoveryChecks.retained(b,raw);verifyWorldFields(b);stable(b);NbtRecoveryChecks.retained(read(saved(b)),raw);} + private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");typedList(n, "Equipment", 10).add(raw);n.put("Pos",vector(Double.NaN,80,160.5));n.put("Motion",vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY));EntityLootableBody b=chunk(n,10,10);require(b.isAlive(),"Corrupt geometry made inventory inaccessible");NbtRecoveryChecks.retained(b,raw);verifyWorldFields(b);stable(b);NbtRecoveryChecks.retained(read(saved(b)),raw);} static void verifyWorldFields(EntityLootableBody b){finite(b);sameValue(b.getDeltaMovement().x,0,"Disk motion X");sameValue(b.getDeltaMovement().y,0,"Disk motion Y");sameValue(b.getDeltaMovement().z,0,"Disk motion Z");NameLifetimeChecks.retained(b,"Pos",vector(Double.NaN,80,b.getZ()));NameLifetimeChecks.retained(b,"Motion",vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY));} private static void nativeCollision(){ CompoundTag raw=base();raw.put("Pos",vector(Double.NaN,80,160.5));raw.put("Motion",vector(Double.NaN,Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY)); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ProbeFrames.java b/src/integrationTest/java/cyano/lootable/buildtest/ProbeFrames.java new file mode 100644 index 0000000..fbb4dec --- /dev/null +++ b/src/integrationTest/java/cyano/lootable/buildtest/ProbeFrames.java @@ -0,0 +1,22 @@ +package cyano.lootable.buildtest; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.minecraft.client.Camera; +import net.minecraft.client.renderer.culling.Frustum; + +/** Native culling and completed frame observations, confined to the disposable probe. */ +public final class ProbeFrames { + public record Frame(Camera camera, Frustum frustum, int tick) { } + private static final List> observers = new ArrayList<>(); + + private ProbeFrames() { } + static void observe(Consumer observer) { observers.add(observer); } + + public static void afterRender(Camera camera, Frustum frustum, int tick) { + if (observers.isEmpty()) return; + Frame frame = new Frame(camera, frustum, tick); + for (Consumer observer : observers) observer.accept(frame); + } +} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/Recovery112Checks.java b/src/integrationTest/java/cyano/lootable/buildtest/Recovery112Checks.java index a9e1d91..cb06132 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/Recovery112Checks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/Recovery112Checks.java @@ -47,7 +47,7 @@ private static EntityLootableBody body(ServerLevel world,int coordinate,double y } private static void conserved(EntityLootableBody body) { RuntimeChecks.require(RuntimeChecks.sameStack(body.getItem(6),RuntimeChecks.stack(420)),"Recovery option changed complete item NBT"); - CompoundTag saved=new CompoundTag();body.saveAsPassenger(saved);EntityLootableBody reloaded=new EntityLootableBody(body.level());reloaded.load(saved); + CompoundTag saved=new CompoundTag();FixtureApi.saveEntity(body,saved);EntityLootableBody reloaded=new EntityLootableBody(body.level());FixtureApi.loadEntityData(reloaded,saved); RuntimeChecks.require(RuntimeChecks.sameStack(reloaded.getItem(6),RuntimeChecks.stack(420)),"Recovery option inventory changed on reload"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java index e7af7df..9f2ce8c 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.server.network.ServerGamePacketListenerImpl; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerChunkCache; @@ -38,7 +40,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.util.Mth; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.level.GameType; import net.minecraft.world.level.chunk.LevelChunk; @@ -90,9 +92,9 @@ private static ItemStack shovel() { static Map inventory(EntityLootableBody body) { Map total=new TreeMap<>(); for(int i=0;i<48;i++) add(total,body.getItem(i)); - CompoundTag n=new CompoundTag();body.addAdditionalSaveData(n); - ListTag aux=n.getList("Aux",10); - for(int i=0;i total,ItemStack stack) { @@ -112,24 +114,39 @@ private static Map tagged(ServerPlayer player) { return n; } private static EntityLootableBody death(ServerPlayer player) { - Set before=new HashSet<>(FixtureApi.entities(world));player.kill(); - for(Entity e:FixtureApi.entities(world))if(e instanceof EntityLootableBody && !before.contains(e))return (EntityLootableBody)e; - throw new AssertionError("Real player death did not create corpse"); + Set before = new HashSet<>(FixtureApi.entities(world)); + var source = player.damageSources().genericKill(); + boolean clientLoaded = player.connection.hasClientLoaded(); + boolean immune = player.isInvulnerableTo(player.level(), source); + float healthBefore = player.getHealth(); + // This is the same actual native damage call used by LivingEntity.kill. + boolean damaged = player.hurtServer(player.level(), source, Float.MAX_VALUE); + for (Entity entity : FixtureApi.entities(world)) { + if (entity instanceof EntityLootableBody body && !before.contains(body)) return body; + } + throw new AssertionError("Real player death did not create corpse: damage="+damaged+" immune="+immune + +" clientLoaded="+clientLoaded+" previousHealth="+healthBefore+" currentHealth="+player.getHealth() + +" removed="+player.isRemoved()+" dimension="+player.level().dimension().identifier()); + } + private static void acknowledgeNativeLoad(ServerPlayer player) { + RuntimeChecks.require(player.connection instanceof QuietConnection, "Synthetic loaded receipt on a live connection"); + player.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket()); + RuntimeChecks.require(player.connection.hasClientLoaded(), "The native loaded receipt did not complete"); } + private static ServerPlayer respawn(ServerPlayer player) { ServerGamePacketListenerImpl connection=player.connection; connection.handleClientCommand(new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN)); ServerPlayer replacement=connection.player; RuntimeChecks.require(replacement!=player && replacement.isAlive(),"Actual player recreation did not occur"); + 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 { QuietConnection observed=(QuietConnection)p.connection;int before=observed.menuEvents; // Server-side conservation fixture: omit predicted slots and require // native authoritative corrections. Real clients test normal prediction. - ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket(p.containerMenu.containerId, - p.containerMenu.getStateId(),slot,button,type,p.containerMenu.getCarried().copy(), - new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap()); + ServerboundContainerClickPacket outgoing=FixtureApi.clickPacket(p,p.containerMenu.getStateId(),slot,button,type,null); var wire=new net.minecraft.network.RegistryFriendlyByteBuf(Unpooled.buffer(),p.registryAccess()); try { ServerboundContainerClickPacket.STREAM_CODEC.encode(wire,outgoing);p.connection.handleContainerClick(ServerboundContainerClickPacket.STREAM_CODEC.decode(wire)); @@ -155,38 +172,40 @@ private static void addFixturePlayer(ServerPlayer player)throws ReflectiveOperat RuntimeChecks.require(server.getPlayerList().getPlayers().contains(player),"Fixture player list observation"); } private static void deathSequence() throws Exception { - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null);RuntimeChecks.awaitChunk(world,2,0); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,false,null);RuntimeChecks.awaitChunk(world,2,0); ServerPlayer p=new ServerPlayer(server,world,new GameProfile(UUID.randomUUID(),"IssueFive"),net.minecraft.server.level.ClientInformation.createDefault()); - p.connection=new QuietConnection(p);p.setGameMode(GameType.SURVIVAL);p.setPos(32.5,80,0.5); - // Vanilla clients need player-list information before a tracked player - // spawn, including the brief spawn-point visit during real respawn. - server.getPlayerList().broadcastAll(net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(java.util.List.of(p))); - RuntimeChecks.require(world.addFreshEntity(p),"Fixture player spawn");addFixturePlayer(p); - p.getInventory().selected=4;p.getInventory().items.set(4,shovel());p.getInventory().items.set(9,RuntimeChecks.stack(301)); - Map expected=tagged(p); - EntityLootableBody first=death(p);RuntimeChecks.require(expected.equals(tagged(first)),"First death lost inventory"); - p=respawn(p);CorpseContainer gui=new CorpseContainer(17,p.getInventory(),first);p.containerMenu=gui;p.initMenu(gui); - click(p,4,0,ClickType.SWAP); - RuntimeChecks.require(RuntimeChecks.sameStack(p.getInventory().items.get(0),shovel()),"Key1 did not recover original main hand"); - for(int slot=0;slot<48;slot++)if(gui.getSlot(slot).hasItem())click(p,slot,0,ClickType.QUICK_MOVE); - RuntimeChecks.require(first.isEmpty() && expected.equals(tagged(p)),"First recovery lost or duplicated tagged inventory: remaining="+inventory(first)+" expected="+expected+" player="+tagged(p)+" state="+gui.getStateId()); - FriendlyByteBuf close=new FriendlyByteBuf(Unpooled.buffer()); - try {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");} - finally {close.release();} - int active=-1;for(int slot=1;slot<9;slot++)if(!(p.getInventory().items.get(slot)).isEmpty() && FixtureStackData.hasCustom(p.getInventory().items.get(slot)) && FixtureStackData.custom(p.getInventory().items.get(slot)).contains("fixture"))active=slot; - RuntimeChecks.require(active>0,"Shift-click did not leave a different occupied hotbar slot"); - FriendlyByteBuf held=new FriendlyByteBuf(Unpooled.buffer()); - try {ServerboundSetCarriedItemPacket.STREAM_CODEC.encode(held,new ServerboundSetCarriedItemPacket(active));p.connection.handleSetCarriedItem(ServerboundSetCarriedItemPacket.STREAM_CODEC.decode(held));RuntimeChecks.require(!held.isReadable(),"Native held-item packet left undecoded fields");} - finally {held.release();} - // Deliberately do not tick/sync container or reopen inventory here. - EntityLootableBody second=death(p); - RuntimeChecks.require(expected.equals(tagged(second)),"Second death lost original key1 shovel or another exact stack"); - RuntimeChecks.require(RuntimeChecks.sameStack(second.getItem(4),RuntimeChecks.stack(301)),"Second death active hand differs"); - System.out.println("ISSUE5_REAL_RESPAWN_AND_HELD_ITEM_PACKET_PASS"); - p=respawn(p);server.getPlayerList().remove(p); - CompoundTag n=new CompoundTag();second.saveAsPassenger(n); - EntityLootableBody loaded=new EntityLootableBody(world);loaded.load(n); - RuntimeChecks.require(expected.equals(tagged(loaded)),"Second corpse NBT reload lost recovery"); + try (QuietConnection connection = new QuietConnection(p)) { + p.connection=connection;acknowledgeNativeLoad(p);p.setGameMode(GameType.SURVIVAL);p.setPos(32.5,80,0.5); + // Vanilla clients need player-list information before a tracked player + // spawn, including the brief spawn-point visit during real respawn. + server.getPlayerList().broadcastAll(net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(java.util.List.of(p))); + RuntimeChecks.require(world.addFreshEntity(p),"Fixture player spawn");addFixturePlayer(p); + p.getInventory().setSelectedSlot(4);p.getInventory().getNonEquipmentItems().set(4,shovel());p.getInventory().getNonEquipmentItems().set(9,RuntimeChecks.stack(301)); + Map expected=tagged(p); + EntityLootableBody first=death(p);RuntimeChecks.require(expected.equals(tagged(first)),"First death lost inventory"); + p=respawn(p);CorpseContainer gui=new CorpseContainer(17,p.getInventory(),first);p.containerMenu=gui;p.initMenu(gui); + click(p,4,0,ClickType.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); + 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");} + finally {close.release();} + int active=-1;for(int slot=1;slot<9;slot++)if(!(p.getInventory().getNonEquipmentItems().get(slot)).isEmpty() && FixtureStackData.hasCustom(p.getInventory().getNonEquipmentItems().get(slot)) && FixtureStackData.custom(p.getInventory().getNonEquipmentItems().get(slot)).contains("fixture"))active=slot; + RuntimeChecks.require(active>0,"Shift-click did not leave a different occupied hotbar slot"); + FriendlyByteBuf held=new FriendlyByteBuf(Unpooled.buffer()); + try {ServerboundSetCarriedItemPacket.STREAM_CODEC.encode(held,new ServerboundSetCarriedItemPacket(active));p.connection.handleSetCarriedItem(ServerboundSetCarriedItemPacket.STREAM_CODEC.decode(held));RuntimeChecks.require(!held.isReadable(),"Native held-item packet left undecoded fields");} + finally {held.release();} + // Deliberately do not tick/sync container or reopen inventory here. + EntityLootableBody second=death(p); + RuntimeChecks.require(expected.equals(tagged(second)),"Second death lost original key1 shovel or another exact stack"); + RuntimeChecks.require(RuntimeChecks.sameStack(second.getItem(4),RuntimeChecks.stack(301)),"Second death active hand differs"); + System.out.println("ISSUE5_REAL_RESPAWN_AND_HELD_ITEM_PACKET_PASS"); + p=respawn(p);server.getPlayerList().remove(p); + CompoundTag n=new CompoundTag();FixtureApi.saveEntity(second,n); + EntityLootableBody loaded=new EntityLootableBody(world);FixtureApi.loadEntityData(loaded,n); + RuntimeChecks.require(expected.equals(tagged(loaded)),"Second corpse NBT reload lost recovery"); + } } private static EntityLootableBody fullBody(int cx,int cz,double offset) {return fullBody(cx,cz,offset,null);} private static EntityLootableBody fullBody(int cx,int cz,double offset,UUID savedId) { @@ -226,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,pos);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().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 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,pos);} + @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);} } private static Support support(int axis,int direction) {return support(axis,direction,null);} private static Support support(int axis,int direction,UUID savedId) { @@ -242,7 +261,7 @@ private static Support support(int axis,int direction,UUID savedId) { f.platform=new BlockPos(floorX+(axis==0?direction:0),79,floorZ+(axis==1?direction:0)); // A body can straddle a chunk boundary while its centre remains in the ticketed chunk. // Remove only the two fixture columns' ground, leaving one thin supporting platform. - for(int y=world.getMinBuildHeight();y<79;y++){world.removeBlock(new BlockPos(floorX,y,floorZ),false);world.removeBlock(new BlockPos(f.platform.getX(),y,f.platform.getZ()),false);} + for(int y=world.getMinY();y<79;y++){world.removeBlock(new BlockPos(floorX,y,floorZ),false);world.removeBlock(new BlockPos(f.platform.getX(),y,f.platform.getZ()),false);} world.setBlockAndUpdate(f.platform,Blocks.STONE.defaultBlockState()); f.body=fullBody(f.cx,f.cz,8.5,savedId);f.body.setPos(x,80,z);f.expected=inventory(f.body); f.forced=world.setChunkForced(f.cx,f.cz,true); @@ -254,9 +273,9 @@ private static Support support(int axis,int direction,UUID savedId) { } private static void forcedSupport(int axis,int direction) { try(Support f=support(axis,direction)) { - CompoundTag before=new CompoundTag();f.body.saveAsPassenger(before); + CompoundTag before=new CompoundTag();FixtureApi.saveEntity(f.body,before); f.unload();f.frozen(); - CompoundTag after=new CompoundTag();f.body.saveAsPassenger(after); + CompoundTag after=new CompoundTag();FixtureApi.saveEntity(f.body,after); RuntimeChecks.require(before.equals(after),"Waiting for terrain changed healthy saved NBT"); f.resume(); } @@ -302,9 +321,27 @@ static void coldReload(MinecraftServer instance,ServerLevel target,File director world.setChunkForced(cx,cz,false); System.out.println("ISSUE17_COLD_RELOAD_DEFERRED_SUPPORT_AND_COMPLETE_NBT_PASS"); } - private static final class QuietConnection extends ServerGamePacketListenerImpl { + private static final class QuietConnection extends ServerGamePacketListenerImpl implements AutoCloseable { int menuEvents,menuState=-1;ItemStack carried=ItemStack.EMPTY; - QuietConnection(ServerPlayer p){super(ReportedIssueChecks.server,new Connection(PacketFlow.SERVERBOUND),p,net.minecraft.server.network.CommonListenerCookie.createInitial(p.getGameProfile(),false));} + private final io.netty.channel.embedded.EmbeddedChannel channel; + + QuietConnection(ServerPlayer player) { + this(player, new Connection(PacketFlow.SERVERBOUND)); + } + + private QuietConnection(ServerPlayer player, Connection connection) { + super(ReportedIssueChecks.server, connection, player, + net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(), false)); + // Native Forge command filtering reads the network context from the + // Netty channel during respawn. Keep the fixture local to this JVM. + channel = new io.netty.channel.embedded.EmbeddedChannel(connection); + } + + @Override + public void close() { + channel.finishAndReleaseAll(); + } + @Override public void send(Packet packet) { NativeMenuObservation observed=NativeMenuObservation.decode(packet,player.registryAccess());if(observed==null)return; if(observed.containerId==player.containerMenu.containerId){menuEvents++;menuState=observed.stateId;if(observed.content)carried=observed.carried;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java index 723e2ac..04134dd 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/ReportedIssueClientChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.client.gui.screens.TitleScreen; import net.minecraft.client.gui.screens.DeathScreen; import net.minecraft.world.SimpleContainer; @@ -30,7 +32,7 @@ import net.minecraft.client.Screenshot; import net.minecraft.world.InteractionHand; import net.minecraft.core.BlockPos; -import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.level.GameType; /** Exact issue5 sequence over a real connected client and two disk reloads. */ @@ -39,7 +41,9 @@ private interface Action {void run() throws Exception;} private int stage,frames,active; private boolean distantViewerObserved,approachPending; private Future pending; + private Future nativeLoaded; private UUID firstId,secondId; + private BlockPos firstOrigin, secondOrigin; private File report; private CompoundTag savedBody,savedPlayer; static ItemStack original() { @@ -55,13 +59,17 @@ void tick(Minecraft mc) throws Exception { FixtureApi.ClientWorlds.create(mc,"issue-fixture","Issue fixture",net.minecraft.world.level.GameType.SURVIVAL,true);next(1); } else if(stage==1 && ready(mc)) { schedule(mc,()->{ - ServerLevel w=player(mc).serverLevel();w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); - w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null); + ServerLevel w=player(mc).level(); + RuntimeChecks.awaitChunk(w, 2, 0); + w.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,false,null); + w.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.SPAWN_MOBS,false,null); for(int x=29;x<=36;x++)for(int z=-3;z<=4;z++)w.setBlockAndUpdate(new BlockPos(x,79,z),Blocks.STONE.defaultBlockState()); - ServerPlayer p=player(mc);p.setGameMode(GameType.SURVIVAL);p.getInventory().clearContent();p.getInventory().selected=4; - p.getInventory().items.set(4,original());p.getInventory().items.set(9,RuntimeChecks.stack(301)); + ServerPlayer p=player(mc);p.setGameMode(GameType.SURVIVAL);p.getInventory().clearContent();p.getInventory().setSelectedSlot(4); + p.getInventory().getNonEquipmentItems().set(4,original());p.getInventory().getNonEquipmentItems().set(9,RuntimeChecks.stack(301)); p.connection.teleport(32.5,80,.5,0,0);p.containerMenu.broadcastChanges(); - firstId=die(p,null).getUUID(); + EntityLootableBody first = die(p, null); + firstId = first.getUUID(); + firstOrigin = first.blockPosition(); });next(2); } else if(stage==2 && done() && mc.screen instanceof DeathScreen && frames>10) { mc.player.respawn();next(3); @@ -72,32 +80,37 @@ void tick(Minecraft mc) throws Exception { equal(mc.player.containerMenu.getSlot(4).getItem(),original(),"First corpse main hand");shot(mc,"issue5-first-main-hand.png"); mc.gameMode.handleInventoryMouseClick(mc.player.containerMenu.containerId,4,0,ClickType.SWAP,mc.player);next(6); } else if(stage==6 && frames>15) { - equal(mc.player.getInventory().items.get(0),original(),"Key1 original shovel recovery"); + 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); next(7); } else if(stage==7 && frames>20) { - equal(mc.player.getInventory().items.get(0),original(),"Original shovel changed before switch"); - active=-1;for(int slot=1;slot<9;slot++)if(RuntimeChecks.sameStack(mc.player.getInventory().items.get(slot),RuntimeChecks.stack(301)))active=slot; + equal(mc.player.getInventory().getNonEquipmentItems().get(0),original(),"Original shovel changed before switch"); + active=-1;for(int slot=1;slot<9;slot++)if(RuntimeChecks.sameStack(mc.player.getInventory().getNonEquipmentItems().get(slot),RuntimeChecks.stack(301)))active=slot; RuntimeChecks.require(active>0,"No different recovered hotbar item"); mark("first_death_respawn_key1_and_shift_recovery"); - mc.player.closeContainer();mc.player.getInventory().selected=active; + mc.player.closeContainer();mc.player.getInventory().setSelectedSlot(active); mc.getConnection().send(new ServerboundSetCarriedItemPacket(active)); // Same ordered connection: held-item change, then a build-only death signal. // This uses the vanilla death path without native command permissions. - MultiplayerChecks.CHANNEL.send(new MultiplayerChecks.FixtureDeath(),net.minecraftforge.network.PacketDistributor.SERVER.noArg());next(8); + MultiplayerChecks.CHANNEL.send(new MultiplayerChecks.FixtureDeath(active),net.minecraftforge.network.PacketDistributor.SERVER.noArg());next(8); } else if(stage==8 && mc.screen instanceof DeathScreen && frames>10) { mc.player.respawn();next(9); } else if(stage==9 && ready(mc) && mc.player.isAlive() && !(mc.screen instanceof DeathScreen)) { schedule(mc,()->{ ServerPlayer p=player(mc);EntityLootableBody second=null; - for(Entity e:FixtureApi.entities(p.level()))if(e instanceof EntityLootableBody && !e.getUUID().equals(firstId) && p.getGameProfile().getName().equals(((EntityLootableBody)e).getCustomNameTag()))second=(EntityLootableBody)e; - RuntimeChecks.require(second!=null,"Second actual death corpse absent");secondId=second.getUUID(); + for(Entity e:FixtureApi.entities(p.level()))if(e instanceof EntityLootableBody && !e.getUUID().equals(firstId) && p.getGameProfile().name().equals(((EntityLootableBody)e).getCustomNameTag()))second=(EntityLootableBody)e; + RuntimeChecks.require(second!=null,"Second actual death corpse absent"); + secondId = second.getUUID(); + secondOrigin = second.blockPosition(); verifyOriginals(second);equal(second.getItem(4),RuntimeChecks.stack(301),"Second selected main hand"); - RuntimeChecks.require(p.getInventory().save(new ListTag()).size()==0,"Second death duplicated player inventory"); - if(Boolean.getBoolean("lootablebodies.issueClient.offsetViewer")) - p.connection.teleport(64.5,4,64.5,0,0); - else p.connection.teleport(32.5,80,.5,0,0); + RuntimeChecks.require(FixtureApi.saveInventory(p).size()==0,"Second death duplicated player inventory"); + if (Boolean.getBoolean("lootablebodies.issueClient.offsetViewer")) { + RuntimeChecks.awaitChunk(p.level(), 4, 4); + for (int x = 63; x <= 65; x++) for (int z = 63; z <= 65; z++) + p.level().setBlockAndUpdate(new BlockPos(x, 79, z), Blocks.STONE.defaultBlockState()); + p.connection.teleport(64.5, 80, 64.5, 0, 0); + } else p.connection.teleport(32.5,80,.5,0,0); mark("second_death_without_inventory_resync_preserves_original_shovel_and_all_tags"); });next(10); } else if(stage==10 && done()) { @@ -147,7 +160,7 @@ void tick(Minecraft mc) throws Exception { } else if(stage==22 && FixtureConnections.stopped(mc)) {mc.stop();next(23);} } private EntityLootableBody die(ServerPlayer p,UUID skip) { - Set before=new HashSet<>(FixtureApi.entities(p.level()));p.kill(); + Set before=new HashSet<>(FixtureApi.entities(p.level()));p.kill(p.level()); for(Entity e:FixtureApi.entities(p.level()))if(e instanceof EntityLootableBody && !before.contains(e))return (EntityLootableBody)e; throw new AssertionError("Actual connected death absent"); } @@ -162,21 +175,40 @@ private static void verifyOriginals(EntityLootableBody b) { } private static void verifyClientOriginals(Minecraft mc){Map n=new TreeMap<>();for(int i=0;i<48;i++)addTagged(n,mc.player.containerMenu.getSlot(i).getItem());RuntimeChecks.require(originals().equals(n),"Connected GUI differs from exact originals");} private void save(Minecraft mc) throws Exception { - savedBody=new CompoundTag();body(mc).saveAsPassenger(savedBody);savedPlayer=new CompoundTag();savedPlayer.put("Inventory",player(mc).getInventory().save(new ListTag())); - File folder=player(mc).serverLevel().getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); + savedBody=new CompoundTag();FixtureApi.saveEntity(body(mc),savedBody);savedPlayer=new CompoundTag();savedPlayer.put("Inventory",FixtureApi.saveInventory(player(mc))); + File folder=player(mc).level().getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); try(FileOutputStream out=new FileOutputStream(new File(folder,"issue5-corpse-expected.nbt"))){NbtIo.writeCompressed(savedBody,out);} try(FileOutputStream out=new FileOutputStream(new File(folder,"issue5-player-expected.nbt"))){NbtIo.writeCompressed(savedPlayer,out);} mc.getSingleplayerServer().getPlayerList().saveAll();mc.getSingleplayerServer().saveAllChunks(false,true,false); } private void verifyDisk(Minecraft mc) { - RuntimeChecks.awaitChunk(player(mc).serverLevel(),2,0);CompoundTag actual=new CompoundTag();body(mc).saveAsPassenger(actual); + RuntimeChecks.awaitChunk(player(mc).level(),2,0);CompoundTag actual=new CompoundTag();FixtureApi.saveEntity(body(mc),actual); for(String key:Arrays.asList("id","UUID","UUIDMost","UUIDLeast","Equipment","Aux","Name"))RuntimeChecks.require(Objects.equals(savedBody.get(key),actual.get(key)),"Disk corpse differs: "+key); - RuntimeChecks.require(savedPlayer.getList("Inventory",10).equals(player(mc).getInventory().save(new ListTag())),"Complete player inventory differs after disk reload"); + RuntimeChecks.require(typedList(savedPlayer, "Inventory", 10).equals(FixtureApi.saveInventory(player(mc))),"Complete player inventory differs after disk reload"); } private ServerPlayer player(Minecraft mc){return mc.getSingleplayerServer().getPlayerList().getPlayer(mc.player.getUUID());} - private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(player(mc).serverLevel(),secondId);} + private EntityLootableBody body(Minecraft mc){return RuntimeChecks.find(player(mc).level(),secondId);} private static void equal(ItemStack a,ItemStack b,String reason){RuntimeChecks.require(RuntimeChecks.sameStack(a == null ? ItemStack.EMPTY : a,b == null ? ItemStack.EMPTY : b),reason);} - private static boolean ready(Minecraft mc){return mc.level!=null && mc.player!=null && mc.getSingleplayerServer()!=null;} + private boolean ready(Minecraft mc) throws Exception { + if (mc.level == null || mc.player == null || mc.getSingleplayerServer() == null + || mc.screen instanceof net.minecraft.client.gui.screens.LevelLoadingScreen) { + nativeLoaded = null; + return false; + } + if (nativeLoaded == null) { + UUID id = mc.player.getUUID(); + var server = mc.getSingleplayerServer(); + nativeLoaded = server.submit(() -> { + ServerPlayer player = server.getPlayerList().getPlayer(id); + return player != null && player.connection.hasClientLoaded(); + }); + return false; + } + if (!nativeLoaded.isDone()) return false; + boolean loaded = nativeLoaded.get(); + nativeLoaded = null; + return loaded; + } private static boolean gui(Minecraft mc){return mc.screen instanceof CorpseGUIContainer;} private boolean open(Minecraft mc,UUID id) throws Exception { // Sending an interaction does not mean the server has opened its GUI. @@ -188,10 +220,11 @@ private boolean open(Minecraft mc,UUID id) throws Exception { // into range, then wait for normal tracking and interaction. schedule(mc,()->{ ServerPlayer p=player(mc); - EntityLootableBody corpse=RuntimeChecks.find(p.serverLevel(),id); - if(p.distanceToSqr(corpse)>9) { - double x=corpse.getX(),z=corpse.getZ()-2; - int y=net.minecraft.util.Mth.floor(corpse.getY()); + BlockPos origin = id.equals(firstId) ? firstOrigin : secondOrigin; + RuntimeChecks.require(origin != null, "Missing observed corpse position"); + if (p.distanceToSqr(origin.getX() + 0.5, origin.getY(), origin.getZ() + 0.5) > 9) { + double x = origin.getX() + 0.5, z = origin.getZ() - 1.5; + int y = origin.getY(); // A respawn can leave the viewer far from the original // platform. Give this disposable fixture a stable place // beside the actual corpse before sending normal clicks. @@ -218,6 +251,6 @@ private boolean open(Minecraft mc,UUID id) throws Exception { private boolean done() throws Exception {if(!pending.isDone())return false;pending.get();return true;} private void next(int n){stage=n;frames=0;System.out.println("ISSUE5_CLIENT_STAGE "+n);} private void mark(String name) throws Exception {try(FileWriter out=new FileWriter(report,true)){out.write("PASS "+name+"\n");}System.out.println("ISSUE5_CLIENT_CHECK_PASS "+name);} - private static void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private static void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString()));} private static void quit(Minecraft mc){mc.player.closeContainer();FixtureConnections.disconnect(mc);} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java index c79976e..399527b 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketChecks.java @@ -1,6 +1,5 @@ package cyano.lootable.buildtest; import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; import net.minecraft.server.level.ServerLevel; import cyano.lootable.entities.EntityLootableBody; @@ -15,16 +14,16 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.server.MinecraftServer; import net.minecraft.core.BlockPos; -import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.level.GameType; import net.minecraft.world.level.Level; -import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; /** Death/respawn timing matrix, with an independently connected observer. */ public final class RespawnPacketChecks { private final MinecraftServer server; + private final net.minecraftforge.eventbus.api.listener.EventListener tickListener; private int index,phase,ticks,phaseTicks,startupTicks; private ServerPlayer dead; private ServerPlayer observerDead; @@ -38,7 +37,7 @@ public final class RespawnPacketChecks { RuntimeChecks.require(!LootableBodies.useLocalSkin&&LootableBodies.allowCorpseDecay&&LootableBodies.decayOnlyWhenEmpty &&LootableBodies.ticksPerItemDecay==6000&&LootableBodies.corpseDecayTime==72000&&LootableBodies.corpseHP==20 &&!LootableBodies.hurtByEnvironment&&!LootableBodies.hurtByAttacks&&!LootableBodies.hurtByMisc,"Respawn matrix requires production defaults"); - this.server=server;state(0);Files.write(file("server.ready").toPath(),new byte[]{1});net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); + this.server=server;state(0);Files.write(file("server.ready").toPath(),new byte[]{1});tickListener = TickEvent.ServerTickEvent.Post.BUS.addListener(this::tick); } static void verifySlowClientStartup(MinecraftServer server) throws Exception { String previous=System.getProperty("lootablebodies.multiplayer.sync"); @@ -55,7 +54,7 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { "Client startup consumed the death/respawn case deadline"); System.out.println("RESPAWN_STARTUP_CLOCK_PASS waiting_ticks=1000 case_ticks=0"); } finally { - if(fixture!=null)net.minecraftforge.common.MinecraftForge.EVENT_BUS.unregister(fixture); + if(fixture!=null)TickEvent.ServerTickEvent.Post.BUS.removeListener(fixture.tickListener); if(previous==null)System.clearProperty("lootablebodies.multiplayer.sync"); else System.setProperty("lootablebodies.multiplayer.sync",previous); } @@ -63,9 +62,9 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { static File file(String name){return new File(System.getProperty("lootablebodies.multiplayer.sync"),name);} private ServerPlayer player(String role){return server.getPlayerList().getPlayerByName("Container"+role);} private boolean ack(String role,int p){return file(role+".case-"+index+"-"+p).isFile();} - private void trace(ServerPlayer p){if(p!=null&&traced.add(p.getGameProfile().getName()))RespawnPacketTrace.install(p.connection.getConnection(),file(p.getGameProfile().getName()+"-server-packets.txt"),null);} - @SubscribeEvent public void tick(TickEvent.ServerTickEvent e) { - if(e.phase!=TickEvent.Phase.END||finished)return; + private void trace(ServerPlayer p){if(p!=null&&traced.add(p.getGameProfile().name()))RespawnPacketTrace.install(p.connection.getConnection(),file(p.getGameProfile().name()+"-server-packets.txt"),null);} + public void tick(TickEvent.ServerTickEvent.Post e) { + if(finished)return; try { RuntimeChecks.require(++ticks<20000,"Respawn matrix total timeout"); RuntimeChecks.require(!file("abort").exists(),"Respawn matrix aborted"); @@ -84,21 +83,21 @@ static void verifySlowClientStartup(MinecraftServer server) throws Exception { double x=kind==6?-.4:96.5,z=kind==6?-.4:96.5; FixtureArena.prepare(BlockPos.containing(x,79,z),w::setBlockAndUpdate); FixtureArena.prepare(new BlockPos(160,79,96),w::setBlockAndUpdate); - w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_DOMOBSPAWNING).set(false,null);w.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); + w.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.SPAWN_MOBS,false,null);w.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,false,null); a.setGameMode(GameType.SURVIVAL);b.setGameMode(GameType.SURVIVAL);a.getInventory().clearContent();b.getInventory().clearContent(); FixtureApi.spawn(a,new BlockPos(kind==5?160:(int)Math.floor(x),80,(int)Math.floor(z)-3),true,false,a.level().dimension()); FixtureApi.spawn(b,new BlockPos((int)Math.floor(x),80,(int)Math.floor(z)-3),true,false,b.level().dimension()); if(kind==7)FixtureApi.spawn(a,new BlockPos(96,80,93),true,false,a.level().dimension()); - a.getInventory().items.set(9,RuntimeChecks.stack(520+index));a.containerMenu.broadcastChanges(); - if(simultaneous){b.getInventory().items.set(9,RuntimeChecks.stack(620+index));b.containerMenu.broadcastChanges();} + a.getInventory().getNonEquipmentItems().set(9,RuntimeChecks.stack(520+index));a.containerMenu.broadcastChanges(); + if(simultaneous){b.getInventory().getNonEquipmentItems().set(9,RuntimeChecks.stack(620+index));b.containerMenu.broadcastChanges();} a.connection.teleport(x,80,z,0,0);b.connection.teleport(kind==4?160.5:x+2,80,z-2.5,0,25); state(1); } else if(phase==1&&ack("A",1)&&ack("B",1)&&phaseTicks>10) { - dead=a;Set before=new HashSet<>(FixtureApi.entities(a.level()));a.kill();body=null; + dead=a;Set before=new HashSet<>(FixtureApi.entities(a.level()));a.kill(a.level());body=null; for(Entity ent:FixtureApi.entities(a.level()))if(ent instanceof EntityLootableBody&&!before.contains(ent))body=(EntityLootableBody)ent; RuntimeChecks.require(body!=null,"No server death corpse");state(2); if(simultaneous) { - observerDead=b;before=new HashSet<>(FixtureApi.entities(b.level()));b.kill();observerBody=null; + observerDead=b;before=new HashSet<>(FixtureApi.entities(b.level()));b.kill(b.level());observerBody=null; for(Entity ent:FixtureApi.entities(b.level()))if(ent instanceof EntityLootableBody&&!before.contains(ent))observerBody=(EntityLootableBody)ent; RuntimeChecks.require(observerBody!=null,"No observer death corpse"); } @@ -148,11 +147,11 @@ private void transfer(ServerPlayer player,net.minecraft.resources.ResourceKey expected=new ArrayList<>();ServerLevel w=player.serverLevel(); + List expected=new ArrayList<>();ServerLevel w=player.level(); for(Entity e:FixtureApi.entities(w))if(e instanceof EntityLootableBody&&e.isAlive()&&FixtureApi.tracked(e).contains(player))expected.add(e.getId()+" "+e.getUUID()); Collections.sort(expected);Files.write(file("expected-"+index+"-"+role+".txt").toPath(),expected,StandardCharsets.UTF_8); } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java index c3ba96c..3c1f2b2 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RespawnPacketClientChecks.java @@ -2,7 +2,6 @@ import net.minecraft.client.gui.screens.TitleScreen; import net.minecraft.client.gui.screens.DeathScreen; import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; import net.minecraft.network.chat.Component; import cyano.lootable.entities.EntityLootableBody; @@ -22,12 +21,10 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.client.Screenshot; import net.minecraft.world.InteractionHand; -import net.minecraft.server.players.GameProfileCache; import net.minecraft.world.level.block.entity.SkullBlockEntity; import net.minecraft.world.level.Level; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; /** Respawns on the death notification, without waiting for a corpse spawn. */ @@ -40,9 +37,12 @@ final class RespawnPacketClientChecks { private final Set completed=new HashSet<>(); private File observations; private Level profileWorld; - void register(){if(Boolean.getBoolean("lootablebodies.respawn")&&role!=null){net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);MinecraftForge.EVENT_BUS.register(this);}} - @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre e){e.setCanceled(true);} - @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre e){e.setCanceled(true);} + void register(){if(Boolean.getBoolean("lootablebodies.respawn")&&role!=null){ + net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre.BUS.addListener(event -> true); + net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre.BUS.addListener(event -> true); + TickEvent.ClientTickEvent.Pre.BUS.addListener(event -> tick(true)); + TickEvent.ClientTickEvent.Post.BUS.addListener(event -> tick(false)); + }} private void respawn(Minecraft mc) { if((!role.equals("A")&&!Boolean.getBoolean("lootablebodies.respawn.simultaneous"))||mc.player==null)return; if(mc.screen instanceof DeathScreen) { @@ -66,9 +66,9 @@ private void respawn(Minecraft mc) { } } else if(mc.player.isAlive()){requested=false;deathTicks=0;} } - @SubscribeEvent public void tick(TickEvent.ClientTickEvent e) { + private void tick(boolean beforeClientTick) { Minecraft mc=Minecraft.getInstance(); - if(e.phase==TickEvent.Phase.START) { + if(beforeClientTick) { if(mc.level!=null&&mc.level!=profileWorld) { profileWorld=mc.level; // IPacket tests use cached synthetic profiles, never real accounts or @@ -76,10 +76,9 @@ private void respawn(Minecraft mc) { FixtureProfileCache.install(name->{ RuntimeChecks.require(name.matches("Container[AB]"),"Unexpected offline fixture profile"); UUID id=UUID.nameUUIDFromBytes(("OfflinePlayer:"+name).getBytes(StandardCharsets.UTF_8)); - var profile=new com.mojang.authlib.GameProfile(id,name); String texture="{\"timestamp\":0,\"profileId\":\""+id.toString().replace("-","")+"\",\"profileName\":\""+name+"\",\"textures\":{}}"; - profile.getProperties().put("textures",new com.mojang.authlib.properties.Property("textures",Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8)))); - return java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.of(profile)); + return java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.of( + FixtureProfileCache.withTexture(id, name, Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8))))); }); } return; @@ -125,7 +124,7 @@ else if(phase==1&&frames>20){ if(!found||++guiFrames<10)return; } observe(mc,"VERIFIED owner_or_observer exact_uuid_loaded_after_respawn"); - if(index%8==0||index%8==7)Screenshot.grab(mc.gameDirectory,"respawn-case-"+index+".png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + if(index%8==0||index%8==7)Screenshot.grab(mc.gameDirectory,"respawn-case-"+index+".png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); ack(key); } else if(phase==9) { new File(mc.gameDirectory,"respawn-client.pass").createNewFile();System.out.println("LOOTABLE_BODIES_RESPAWN_CLIENT_PASS "+role); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java index 73d608f..0a4c8ff 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RotationFallChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.nbt.Tag; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.ShortTag; @@ -36,7 +38,6 @@ import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.chunk.storage.ChunkStorage; /** Real legacy rotation/landing state, complete inventory and immutable source fixtures. */ final class RotationFallChecks { @@ -67,21 +68,30 @@ static void run(MinecraftServer server,ServerLevel target,File folder) throws Ex check("landing_keeps_default_environmental_damage_disabled",()->landing(false,false)); check("landing_keeps_enabled_environmental_damage",()->landing(true,false)); check("repaired_fall_distance_does_not_invent_landing_damage",()->landing(true,true)); + check("native_fall_distance_preserves_double_precision", () -> { + CompoundTag input = base(); + input.remove("FallDistance"); + double distance = Math.nextUp(12.5D); + input.putDouble("fall_distance", distance); + EntityLootableBody body = read(input); + value(body.fallDistance, distance, "Native double fall distance was narrowed"); + stable(body); + }); try(PrintWriter out=new PrintWriter(new File(folder,"rotation-fall-checks.txt"),"UTF-8")){for(String s:results)out.println(s);out.println("checks="+results.size()+" failures="+failures);} require(failures==0,"Rotation/fall regressions: "+failures+" / "+results.size());System.out.println("LOOTABLE_BODIES_ROTATION_FALL_PASS checks="+results.size()); } private static void check(String name,Check test){boolean env=LootableBodies.hurtByEnvironment,inv=LootableBodies.completelyInvulnerable;try{test.run();results.add("PASS "+name);System.out.println("ROTATION_FALL_PASS "+name);}catch(Throwable e){failures++;results.add("FAIL "+name+": "+e);System.out.println("ROTATION_FALL_FAIL "+name+": "+e);e.printStackTrace();}finally{LootableBodies.hurtByEnvironment=env;LootableBodies.completelyInvulnerable=inv;}} private static void require(boolean c,String why){RuntimeChecks.require(c,why);} private static CompoundTag saved(EntityLootableBody b){return NameLifetimeChecks.saved(b);} - private static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setUserName("RotationFixture");b.setRotation(37.25F);b.fallDistance=12.5F;for(int i=0;i<48;i++)b.setItem(i,RuntimeChecks.stack(i));b.addItem(RuntimeChecks.stack(70));b.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(b);return saved(b);} + private static CompoundTag base(){EntityLootableBody b=new EntityLootableBody(world);b.setPos(160.5,80,160.5);b.setNoGravity(true);b.setUserName("RotationFixture");b.setRotation(37.25F);b.fallDistance=12.5F;for(int i=0;i<48;i++)b.setItem(i,RuntimeChecks.stack(i));b.addItem(RuntimeChecks.stack(70));b.addItem(RuntimeChecks.stack(71));RuntimeChecks.disableVacuum(b);CompoundTag legacy=saved(b);legacy.put("FallDistance",legacy.remove("fall_distance"));return legacy;} private static EntityLootableBody read(CompoundTag n){CompoundTag before=n.copy();Entity e=FixtureApi.loadEntity(n,world);require(e instanceof EntityLootableBody,"Factory discarded corpse");require(NameLifetimeChecks.same(before,n),"Source NBT mutated");return (EntityLootableBody)e;} static ListTag rotation(float... angles){ListTag l=new ListTag();for(float a:angles)l.add(FloatTag.valueOf(a));return l;} static ListTag wideRotation(double... angles){ListTag l=new ListTag();for(double a:angles)l.add(DoubleTag.valueOf(a));return l;} - static void finite(EntityLootableBody b){for(float v:new float[]{b.getYRot(),b.getXRot(),b.yRotO,b.xRotO,b.yHeadRot,b.yHeadRotO,b.yBodyRot,b.yBodyRotO,b.fallDistance})require(Float.isFinite(v),"Nonfinite rotation/derived angle/fall distance");require(b.fallDistance>=0,"Negative fall distance");} - private static void value(float a,float z,String why){require(Float.isFinite(a) && a==z,why+" expected="+z+" actual="+a);} - private static void angles(EntityLootableBody b,float yaw){finite(b);for(float v:new float[]{b.getYRot(),b.yRotO,b.yHeadRot,b.yHeadRotO,b.yBodyRot,b.yBodyRotO})value(v,yaw,"Yaw/derived orientation differs");value(b.getXRot(),0,"Corpse pitch no longer flat");value(b.xRotO,0,"Previous pitch differs");} + static void finite(EntityLootableBody b){for(double v:new double[]{b.getYRot(),b.getXRot(),b.yRotO,b.xRotO,b.yHeadRot,b.yHeadRotO,b.yBodyRot,b.yBodyRotO,b.fallDistance})require(Double.isFinite(v),"Nonfinite rotation/derived angle/fall distance");require(b.fallDistance>=0,"Negative fall distance");} + private static void value(double a,double z,String why){require(Double.isFinite(a) && a==z,why+" expected="+z+" actual="+a);} + private static void angles(EntityLootableBody b,float yaw){finite(b);for(double v:new double[]{b.getYRot(),b.yRotO,b.yHeadRot,b.yHeadRotO,b.yBodyRot,b.yBodyRotO})value(v,yaw,"Yaw/derived orientation differs");value(b.getXRot(),0,"Corpse pitch no longer flat");value(b.xRotO,0,"Previous pitch differs");} private static void kept(EntityLootableBody b){CompoundTag n=base(),a=saved(b);for(String k:new String[]{"Equipment","Aux"})require(NameLifetimeChecks.same(n.get(k),a.get(k)),"Complete inventory changed "+k);require(b.getContainerSize()==48 && b.getCustomNameTag().equals("RotationFixture"),"Corpse identity/slots changed");} - private static void stable(EntityLootableBody b){finite(b);kept(b);CompoundTag a=saved(b),z=saved(read(a));for(String k:new String[]{"Rotation","FallDistance","Pos","Motion","Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);} + private static void stable(EntityLootableBody b){finite(b);kept(b);CompoundTag a=saved(b),z=saved(read(a));for(String k:new String[]{"Rotation","fall_distance","Pos","Motion","Equipment","Aux",NbtRecoveryChecks.RECOVERY,NameLifetimeChecks.RECOVERY})require(NameLifetimeChecks.same(a.get(k),z.get(k)),"Round trip changed "+k);} private static void badFall(Tag raw){CompoundTag n=base();n.put("FallDistance",raw.copy());EntityLootableBody b=read(n);value(b.fallDistance,0,"Bad fall distance activated");NameLifetimeChecks.retained(b,"FallDistance",raw);stable(b);} private static void badRotation(Tag raw){CompoundTag n=base();n.put("Rotation",raw.copy());EntityLootableBody b=read(n);angles(b,0);NameLifetimeChecks.retained(b,"Rotation",raw);stable(b);} private static void component(int axis,double raw){CompoundTag n=base();ListTag r=axis==0?wideRotation(raw,17):wideRotation(37.25,raw);n.put("Rotation",r);EntityLootableBody b=read(n);angles(b,axis==0?0:37.25F);NameLifetimeChecks.retained(b,"Rotation",r);stable(b);} @@ -92,11 +102,11 @@ 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");b.load(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,"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 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);} - private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");n.getList("Equipment",10).add(raw);CompoundTag fields=malformedFields();for(String k:fields.getAllKeys())n.put(k,fields.get(k).copy());EntityLootableBody b=read(n);require(b.isAlive(),"Bad fields made inventory inaccessible");verifyWorldFields(b);NbtRecoveryChecks.retained(b,raw);stable(b);NbtRecoveryChecks.retained(read(saved(b)),raw);} + private static void inventory(){CompoundTag n=base(),raw=NbtRecoveryChecks.entry(121,11,7);raw.putString("id","absentfixture:missing");typedList(n, "Equipment", 10).add(raw);CompoundTag fields=malformedFields();for(String k:fields.keySet())n.put(k,fields.get(k).copy());EntityLootableBody b=read(n);require(b.isAlive(),"Bad fields made inventory inaccessible");verifyWorldFields(b);NbtRecoveryChecks.retained(b,raw);stable(b);NbtRecoveryChecks.retained(read(saved(b)),raw);} private static void empty(){CompoundTag n=base();n.put("Equipment",new ListTag());n.remove("Aux");n.putString("Rotation","opaque");EntityLootableBody b=read(n);require(b.isEmpty(),"Rotation evidence counts as items");NameLifetimeChecks.retained(b,"Rotation",n.get("Rotation"));} private static void landing(boolean damage,boolean bad){LootableBodies.hurtByEnvironment=damage;LootableBodies.completelyInvulnerable=!damage;world.getChunk(10,10);world.setBlockAndUpdate(new BlockPos(160,79,160),Blocks.STONE.defaultBlockState());CompoundTag n=base();if(bad)n.putDouble("FallDistance",Double.NaN);EntityLootableBody b=read(n);if(bad)value(b.fallDistance,0,"Malformed distance entered landing");b.move(net.minecraft.world.entity.MoverType.SELF,new net.minecraft.world.phys.Vec3(0,-1,0));value(b.fallDistance,0,"Landing failed to reset distance");require(damage && !bad?b.getHealth()>0 && b.getHealth()<20:b.getHealth()==20,"Landing changed configured damage behavior");kept(b);} static CompoundTag malformedFields(){CompoundTag n=new CompoundTag();n.put("Rotation",wideRotation(37,Double.NaN));n.putDouble("FallDistance",Double.NaN);return n;} diff --git a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java index a720c7a..f761508 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/RuntimeChecks.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.world.level.biome.Biomes; import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.server.network.ServerGamePacketListenerImpl; @@ -29,7 +31,10 @@ import net.minecraft.server.level.ServerLevel; import net.minecraftforge.fml.common.*; import net.minecraftforge.event.server.ServerStartedEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.eventbus.api.listener.Priority; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.registries.RegisterEvent; /** Build-only verifier. Never included in the production source set or jars. */ @Mod("lootablebodiesbuildtest") @@ -41,34 +46,43 @@ public final class RuntimeChecks { private static boolean defaultsChecked; private static UUID pendingDeathId; private boolean rejectedFirstCorpseOpen; - public RuntimeChecks() { + public RuntimeChecks(FMLJavaModLoadingContext context) { MultiplayerChecks.registerChannel(); - net.minecraftforge.eventbus.api.IEventBus bus=net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext.get().getModEventBus(); + var bus = context.getModBusGroup(); NativePreflightChecks.install(bus); - bus.register(new TestRegistries());FixtureItems.register(bus); - net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); - net.minecraftforge.fml.DistExecutor.runWhenOn(net.minecraftforge.api.distmarker.Dist.CLIENT,()->()->{ - new ClientChecks().register();new MultiplayerClientChecks().register();new VisibilityClientChecks().register();new RespawnPacketClientChecks().register(); - }); + var registries = new TestRegistries(); + 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.EntityJoinLevelEvent.BUS.addListener(Priority.LOWEST,this::fixtureAdmission); + ServerStartedEvent.BUS.addListener(this::started); + if (FMLEnvironment.dist == net.minecraftforge.api.distmarker.Dist.CLIENT) { + new ClientChecks().register(); + new MultiplayerClientChecks().register(); + new VisibilityClientChecks().register(); + new RespawnPacketClientChecks().register(); + } } - @SubscribeEvent public void rejectFirstCorpseOpen(net.minecraftforge.event.entity.player.PlayerInteractEvent.EntityInteract event) { + private boolean rejectFirstCorpseOpen(net.minecraftforge.event.entity.player.PlayerInteractEvent.EntityInteract event) { if(Boolean.getBoolean("lootablebodies.rejectFirstCorpseOpen") && !rejectedFirstCorpseOpen - && !event.getLevel().isClientSide && event.getTarget() instanceof EntityLootableBody) { + && !event.getLevel().isClientSide() && event.getTarget() instanceof EntityLootableBody) { rejectedFirstCorpseOpen=true; - event.setCanceled(true); System.out.println("CORPSE_GUI_FIRST_INTERACTION_REJECTED_CONTROL"); + return true; } + return false; } - @SubscribeEvent(priority=net.minecraftforge.eventbus.api.EventPriority.LOWEST) public void fixtureAdmission(net.minecraftforge.event.entity.EntityJoinLevelEvent event) { + private void fixtureAdmission(net.minecraftforge.event.entity.EntityJoinLevelEvent event) { NbtWorldChecks.verifyGraphicalAdmission(event.getEntity()); - if(pendingDeathId!=null && event.getEntity() instanceof EntityLootableBody && !event.getLevel().isClientSide + 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().isAddedToWorld()) { 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()); } } - @SubscribeEvent public void started(ServerStartedEvent event) { + private void started(ServerStartedEvent event) { if ((net.minecraftforge.fml.loading.FMLEnvironment.dist==net.minecraftforge.api.distmarker.Dist.CLIENT)) return; MinecraftServer server = net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(); if(Boolean.getBoolean("lootablebodies.respawn")) { @@ -96,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.13.121011"), "identity/version"); + 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.13.121011") : "2.4.13.121011"), "loaded release version"); + System.getProperty("lootablebodies.expectedIssueVersion","2.4.14.121111") : "2.4.14.121111"), "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 && @@ -240,15 +254,15 @@ static EntityLootableBody fixture(ServerLevel world, UUID uuid, int coordinate, return body; } static void verifyRoundTrip(ServerLevel world, EntityLootableBody body) { - CompoundTag before = new CompoundTag(); body.saveAsPassenger(before); - require(before.getString("id").equals("lootablebodies:corpse"), "persistent entity name: " + before.getString("id")); + CompoundTag before = new CompoundTag(); FixtureApi.saveEntity(body,before); + require(before.getStringOr("id", "").equals("lootablebodies:corpse"), "persistent entity name: " + before.getStringOr("id", "")); Entity copy = FixtureApi.loadEntity(before.copy(), world); require(copy instanceof EntityLootableBody, "NBT factory registration"); - CompoundTag after = new CompoundTag(); copy.saveAsPassenger(after); + CompoundTag after = new CompoundTag(); FixtureApi.saveEntity(copy,after); compare(before,after); if (body.getUUID().equals(NEAR)) { - require(before.getList("Equipment",10).size()==48, "all indexed stacks"); - require(before.getList("Aux",10).size()==2, "ordered overflow"); + require(typedList(before, "Equipment", 10).size()==48, "all indexed stacks"); + require(typedList(before, "Aux", 10).size()==2, "ordered overflow"); } } static void death(MinecraftServer server, ServerLevel world, File folder) throws Exception { @@ -257,9 +271,9 @@ static void death(MinecraftServer server, ServerLevel world, File folder) throws player.connection = new net.minecraft.server.network.ServerGamePacketListenerImpl(server, new net.minecraft.network.Connection(net.minecraft.network.protocol.PacketFlow.SERVERBOUND), player,net.minecraft.server.network.CommonListenerCookie.createInitial(player.getGameProfile(),false)); player.setPos(32.5,80,0.5); awaitChunk(world,2,0); require(world.getChunkSource().hasChunk(2,0),"Death fixture must await native chunk admission before spawning its corpse"); - world.getGameRules().getRule(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY).set(false,null); - player.getInventory().items.set(0,stack(4)); - player.getInventory().items.set(9,stack(19)); + world.getGameRules().set(net.minecraft.world.level.gamerules.GameRules.KEEP_INVENTORY,false,null); + player.getInventory().getNonEquipmentItems().set(0,stack(4)); + player.getInventory().getNonEquipmentItems().set(9,stack(19)); player.setItemSlot(EquipmentSlot.HEAD,stack(0)); player.setItemSlot(EquipmentSlot.OFFHAND,stack(5)); // Assign the fixture ID before the native entity-section manager indexes @@ -297,16 +311,16 @@ static void disableVacuum(EntityLootableBody body) { } catch (Exception error) { throw new AssertionError(error); } } static void saveExpected(File folder,String name,EntityLootableBody body) throws Exception { - CompoundTag data = new CompoundTag(); body.saveAsPassenger(data); + CompoundTag data = new CompoundTag(); FixtureApi.saveEntity(body,data); NbtIo.writeCompressed(data,new FileOutputStream(new File(folder,name+".nbt"))); } static void verifyExpected(File folder,String name,EntityLootableBody body) throws Exception { CompoundTag expected = NbtIo.readCompressed(new File(folder,name+".nbt").toPath(),net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)); - CompoundTag actual = new CompoundTag(); body.saveAsPassenger(actual); compare(expected,actual); + CompoundTag actual = new CompoundTag(); FixtureApi.saveEntity(body,actual); compare(expected,actual); } static void compare(CompoundTag expected,CompoundTag actual) { for(String key : Arrays.asList("id","UUID","UUIDMost","UUIDLeast","Pos","Rotation","CustomName","Equipment","Aux","DeathTime","DecayTime")) require(Objects.equals(expected.get(key),actual.get(key)),"Saved field differs " + key + ": " + expected.get(key) + " != " + actual.get(key)); - if(expected.contains("Name")) require(expected.getString("Name").equals(actual.getString("Name")),"profile name"); + if(expected.contains("Name")) require(expected.getStringOr("Name", "").equals(actual.getStringOr("Name", "")),"profile name"); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java b/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java index 1d536e2..0094fb9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/TestRegistries.java @@ -3,14 +3,13 @@ import net.minecraft.world.item.Item; import net.minecraft.world.effect.MobEffect; import net.minecraftforge.registries.RegisterEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; /** Forge 1.12 registers controlled dependencies during their registry events. */ public final class TestRegistries { - @SubscribeEvent public void items(RegisterEvent event) { + public void items(RegisterEvent event) { if(event.getRegistryKey().equals(net.minecraft.core.registries.Registries.ITEM))NbtWorldChecks.registerItem(); } - @SubscribeEvent public void potions(RegisterEvent event) { + public void potions(RegisterEvent event) { if(event.getRegistryKey().equals(net.minecraft.core.registries.Registries.MOB_EFFECT))EffectHurtChecks.registerPotion(); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java index f5273bd..07072ec 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/UpgradeChecks.java @@ -1,4 +1,8 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.hasType; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; import net.minecraft.world.item.Items; import net.minecraft.world.inventory.Slot; import net.minecraft.nbt.Tag; @@ -31,6 +35,8 @@ /** Real Forge data-fixer gates. Synthetic inputs complement sealed old-jar worlds. */ final class UpgradeChecks { + private static int observedItemConversions; + static void registerOwnFixer() { NativeVanillaDataFixer.get(); // Capture the independent vanilla reference first. final com.mojang.datafixers.DataFixer nativeFixer=net.minecraft.util.datafix.DataFixers.getDataFixer(); @@ -39,14 +45,20 @@ static void registerOwnFixer() { public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic input,int source,int target) { com.mojang.serialization.Dynamic result=nativeFixer.update(type,input,source,target); if(type.typeName().equals("item_stack") && source mapping,String name) { @@ -123,11 +155,11 @@ private static Object mappingField(net.minecraftforge.registries.MissingMappings } private static void soundMapping() throws Exception { net.minecraftforge.registries.IForgeRegistry registry=net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS; - net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.parse("minecraft:entity.experience_orb.touch"),196); - net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.parse("absentfixture:other_sound"),197); + net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.Identifier.parse("minecraft:entity.experience_orb.touch"),196); + net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.Identifier.parse("absentfixture:other_sound"),197); net.minecraftforge.registries.MissingMappingsEvent event=new net.minecraftforge.registries.MissingMappingsEvent(registry.getRegistryKey(),registry,Arrays.asList(old,other)); - net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(event); - RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(net.minecraft.resources.ResourceLocation.parse("minecraft:entity.experience_orb.pickup")),"Known vanilla sound rename not remapped"); + net.minecraftforge.registries.MissingMappingsEvent.BUS.post(event); + RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(net.minecraft.resources.Identifier.parse("minecraft:entity.experience_orb.pickup")),"Known vanilla sound rename not remapped"); RuntimeChecks.require(mappingField(other,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.DEFAULT,"Unrelated missing mappings must retain Forge's normal handling"); } private static void nativeRenames() { @@ -139,45 +171,45 @@ private static void nativeRenames() { rename(net.minecraftforge.registries.ForgeRegistries.BLOCKS,"wall_sign","oak_wall_sign"); } private static void rename(net.minecraftforge.registries.IForgeRegistry registry,String oldName,String current) { - net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("minecraft",oldName),30000); - net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("absentfixture",oldName),30001); + net.minecraftforge.registries.MissingMappingsEvent.Mapping old=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft",oldName),30000); + net.minecraftforge.registries.MissingMappingsEvent.Mapping other=new net.minecraftforge.registries.MissingMappingsEvent.Mapping<>(registry,registry,net.minecraft.resources.Identifier.fromNamespaceAndPath("absentfixture",oldName),30001); net.minecraftforge.registries.MissingMappingsEvent event=new net.minecraftforge.registries.MissingMappingsEvent(registry.getRegistryKey(),registry,Arrays.asList(old,other)); - net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(event); - RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("minecraft",current)),"Native registry rename event not remapped: "+oldName); + net.minecraftforge.registries.MissingMappingsEvent.BUS.post(event); + RuntimeChecks.require(mappingField(old,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.REMAP && mappingField(old,"target")==registry.getValue(net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft",current)),"Native registry rename event not remapped: "+oldName); RuntimeChecks.require(mappingField(other,"action")==net.minecraftforge.registries.MissingMappingsEvent.Action.DEFAULT,"Foreign registry rename was consumed"); } private static void legacyProfessions() { - net.minecraft.resources.ResourceLocation name=net.minecraft.resources.ResourceLocation.parse("minecraft:villagerprofessions"); + net.minecraft.resources.Identifier name=net.minecraft.resources.Identifier.parse("minecraft:villagerprofessions"); net.minecraftforge.registries.IForgeRegistry registry=net.minecraftforge.registries.RegistryManager.ACTIVE.getRegistry(name); RuntimeChecks.require(registry!=null && registry.getKeys().size()==6,"Retired vanilla profession registry was not preserved"); - RuntimeChecks.require(!registry.containsKey(net.minecraft.resources.ResourceLocation.parse("absentfixture:profession")),"Unknown professions must retain native missing-mapping handling"); + RuntimeChecks.require(!registry.containsKey(net.minecraft.resources.Identifier.parse("absentfixture:profession")),"Unknown professions must retain native missing-mapping handling"); RuntimeChecks.require(!net.minecraftforge.registries.RegistryManager.ACTIVE.takeSnapshot(true).containsKey(name),"Retired registry must not be written into new saves"); } private static void bed(int source,int marker,int damage) throws Exception { CompoundTag input=body("lootablebodies:corpse"); - input.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").putString("id","minecraft:pig"); - CompoundTag item=new CompoundTag();item.putString("id","minecraft:bed");item.putByte("Count",(byte)2);item.putShort("Damage",(short)0);item.putByte("Slot",(byte)41);input.getList("Equipment",10).set(41,item); + typedList(input, "Equipment", 10).getCompoundOrEmpty(40).getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag").putString("id","minecraft:pig"); + CompoundTag item=new CompoundTag();item.putString("id","minecraft:bed");item.putByte("Count",(byte)2);item.putShort("Damage",(short)0);item.putByte("Slot",(byte)41);typedList(input, "Equipment", 10).set(41,item); if(marker>0)input.putInt("LootableBodiesDataVersion",marker); CompoundTag result=saved(process(chunk(input,IntTag.valueOf(source)))); - ListTag equipment=result.getList("Equipment",10);CompoundTag found=null; - for(int i=0;i inventory(CompoundTag body) { Map counts=new TreeMap<>();for(String key:Arrays.asList("Equipment","Aux")) { - ListTag list=body.getList(key,10);for(int i=0;i0,"malformed original fields were lost in vanilla fixing"); + RuntimeChecks.require(expectedInventory(expected,identity.equals("lootablebodies.Corpse")?512:922).equals(inventory(output)),"complete inventory or vanilla spawn-egg conversion changed. Retained raw records: "+output.get("LootableBodiesUnresolved")); + RuntimeChecks.require(output.getIntOr("LootableBodiesDataVersion", 0)==UpgradePreservation.DATA_VERSION,"missing per-corpse migration marker"); + RuntimeChecks.require(typedList(output, "LootableBodiesInvalidFields", 10).size()>0,"malformed original fields were lost in vanilla fixing"); } private static void missingItem() throws Exception { - CompoundTag body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20);raw.putString("id","absentfixture:keepsake"); - CompoundTag result=saved(process(chunk(body,IntTag.valueOf(512))));ListTag recovery=result.getList("LootableBodiesUnresolved",10); - RuntimeChecks.require(recovery.size()==1,"unavailable item not retained exactly once");CompoundTag record=recovery.getCompound(0); - RuntimeChecks.require(NameLifetimeChecks.same(raw,record.get("Data")) && record.getCompound("SourceVersions").getInt("DataVersion")==512,"unavailable item lost its original payload or source context"); + CompoundTag body=body("lootablebodies.Corpse"),raw=typedList(body, "Equipment", 10).getCompoundOrEmpty(20);raw.putString("id","absentfixture:keepsake"); + CompoundTag result=saved(process(chunk(body,IntTag.valueOf(512))));ListTag recovery=typedList(result, "LootableBodiesUnresolved", 10); + RuntimeChecks.require(recovery.size()==1,"unavailable item not retained exactly once");CompoundTag record=recovery.getCompoundOrEmpty(0); + RuntimeChecks.require(NameLifetimeChecks.same(raw,record.get("Data")) && record.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)==512,"unavailable item lost its original payload or source context"); } private static void repeated() throws Exception { CompoundTag raw=body("lootablebodies.Corpse"),fixed=process(chunk(raw,IntTag.valueOf(512))),once=saved(fixed); - CompoundTag again=nativeChunk(chunk(fixed.copy(),IntTag.valueOf(512))).getCompound("Level").getList("Entities",10).getCompound(0); + CompoundTag again=nativeChunk(chunk(fixed.copy(),IntTag.valueOf(512))).getListOrEmpty("entities").getCompoundOrEmpty(0); CompoundTag twice=saved(again);RuntimeChecks.require(inventory(once).equals(inventory(twice)),"repeated data fixing duplicated or lost items"); } private static void recovery() throws Exception { - CompoundTag body=body("lootablebodies.Corpse"),raw=body.getList("Equipment",10).getCompound(20).copy();raw.putString("id","absentfixture:keepsake"); + CompoundTag body=body("lootablebodies.Corpse"),raw=typedList(body, "Equipment", 10).getCompoundOrEmpty(20).copy();raw.putString("id","absentfixture:keepsake"); CompoundTag record=new CompoundTag();record.putString("Source","Aux");record.put("Data",raw.copy());ListTag records=new ListTag();records.add(record);body.put("LootableBodiesUnresolved",records); - CompoundTag output=saved(process(chunk(body,IntTag.valueOf(512))));CompoundTag kept=output.getList("LootableBodiesUnresolved",10).getCompound(0); - RuntimeChecks.require(kept.getString("Source").equals("Aux") && NameLifetimeChecks.same(raw,kept.get("Data")),"existing Source/Data payload changed"); - RuntimeChecks.require(kept.getCompound("SourceVersions").getInt("DataVersion")==512,"existing recovery extension lost source version context"); + CompoundTag output=saved(process(chunk(body,IntTag.valueOf(512))));CompoundTag kept=typedList(output, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(0); + RuntimeChecks.require(kept.getStringOr("Source", "").equals("Aux") && NameLifetimeChecks.same(raw,kept.get("Data")),"existing Source/Data payload changed"); + RuntimeChecks.require(kept.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)==512,"existing recovery extension lost source version context"); } private static CompoundTag fullSave(CompoundTag fixed) { Entity entity=FixtureApi.loadEntity(fixed.copy(),world);RuntimeChecks.require(entity instanceof EntityLootableBody,"Missing body for full save"); - CompoundTag result=new CompoundTag();entity.saveAsPassenger(result);return result; + CompoundTag result=new CompoundTag();FixtureApi.saveEntity(entity,result);return result; } private static void savedCounter() throws Exception { - CompoundTag source=body("lootablebodies.Corpse");source.getList("Equipment",10).getCompound(20).getCompound("tag").putInt("upgrade_counter",0); + CompoundTag source=body("lootablebodies.Corpse");typedList(source, "Equipment", 10).getCompoundOrEmpty(20).getCompoundOrEmpty("tag").putInt("upgrade_counter",0); + observedItemConversions=0; CompoundTag once=fullSave(process(chunk(source,IntTag.valueOf(512)))); CompoundTag counted=null; - for(Tag entry:once.getList("Equipment",10))if(((CompoundTag)entry).getByte("Slot")==20)counted=(CompoundTag)entry; - RuntimeChecks.require(counted!=null && counted.getCompound("tag").getInt("upgrade_counter")==1,"Item fixer did not run exactly once on first load: "+counted); - CompoundTag twice=fullSave(process(chunk(once,IntTag.valueOf(922))));RuntimeChecks.require(inventory(once).equals(inventory(twice)),"Old item version was applied again after saving migrated items"); + for(Tag entry:typedList(once, "Equipment", 10))if(((CompoundTag)entry).getByteOr("Slot", (byte) 0)==20)counted=(CompoundTag)entry; + RuntimeChecks.require(counted!=null && counted.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:custom_data").getIntOr("upgrade_counter", -1)==0 && observedItemConversions==1,"Item fixer did not run exactly once on first load: "+counted); + CompoundTag twice=fullSave(process(chunk(once,IntTag.valueOf(922))));RuntimeChecks.require(inventory(once).equals(inventory(twice)) && observedItemConversions==1,"Old item version was applied again after saving migrated items"); } - private static CompoundTag currentStamps(CompoundTag chunk) {chunk.putInt("DataVersion",UpgradePreservation.MINECRAFT_VERSION);chunk.put("ForgeDataVersion",new CompoundTag());chunk.getCompound("ForgeDataVersion").putInt("minecraft",-1);return chunk;} + private static CompoundTag currentStamps(CompoundTag chunk) {chunk.putInt("DataVersion",UpgradePreservation.MINECRAFT_VERSION);chunk.put("ForgeDataVersion",new CompoundTag());chunk.getCompoundOrEmpty("ForgeDataVersion").putInt("minecraft",-1);return chunk;} private static void migratedDamagedStamp() throws Exception { - CompoundTag source=body("lootablebodies:corpse");source.putInt("LootableBodiesDataVersion",1);source.getList("Equipment",10).getCompound(40).getCompound("tag").getCompound("EntityTag").putString("id","minecraft:pig"); + CompoundTag source=body("lootablebodies:corpse");source.putInt("LootableBodiesDataVersion",1);typedList(source, "Equipment", 10).getCompoundOrEmpty(40).getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag").putString("id","minecraft:pig"); CompoundTag result=saved(process(currentStamps(chunk(source,null))));RuntimeChecks.require(expectedInventory(source,922).equals(inventory(result)),"Damaged version stamp truncated an already migrated Equipment list"); } private static void forgedModStamp() throws Exception { CompoundTag source=body("lootablebodies.Corpse"),expected=source.copy(); - CompoundTag input=currentStamps(chunk(source,IntTag.valueOf(512)));input.getCompound("ForgeDataVersion").putInt("minecraft",512); + CompoundTag input=currentStamps(chunk(source,IntTag.valueOf(512)));input.getCompoundOrEmpty("ForgeDataVersion").putInt("minecraft",512); RuntimeChecks.require(expectedInventory(expected,512).equals(inventory(saved(process(input)))),"Incorrect mod stamp bypassed custom inventory preservation"); } private static void reusedOutput() throws Exception { CompoundTag fixed=process(chunk(body("lootablebodies.Corpse"),IntTag.valueOf(512))); - EntityLootableBody entity=(EntityLootableBody)FixtureApi.loadEntity(fixed.copy(),world);entity.clearContent();entity.addAdditionalSaveData(fixed); + EntityLootableBody entity=(EntityLootableBody)FixtureApi.loadEntity(fixed.copy(),world);entity.clearContent();FixtureApi.writeAdditional(entity,fixed); EntityLootableBody reloaded=(EntityLootableBody)FixtureApi.loadEntity(fixed.copy(),world); RuntimeChecks.require(reloaded.isEmpty(),"Reused output compound resurrected a consumed upgrade payload"); } @@ -298,15 +349,15 @@ private static void vanillaFalseStamp() throws Exception { versions.putInt("minecraft",922);versions.putInt("lootablebodies",1);versions.putInt("lootablebodiesbuildtest",1);input.put("ForgeDataVersion",versions); CompoundTag chest=new CompoundTag();chest.putString("id","Chest");chest.putInt("x",2);chest.putInt("y",80);chest.putInt("z",2); CompoundTag banner=new CompoundTag();banner.putString("id","minecraft:banner");banner.putByte("Count",(byte)2);banner.putShort("Damage",(short)0);banner.putByte("Slot",(byte)0); - CompoundTag tag=new CompoundTag(),block=new CompoundTag();block.putInt("Base",5);tag.put("BlockEntityTag",block);banner.put("tag",tag);ListTag items=new ListTag();items.add(banner);chest.put("Items",items);ListTag tiles=new ListTag();tiles.add(chest);input.getCompound("Level").put("TileEntities",tiles); - CompoundTag fixed=processChunk(input).getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); - RuntimeChecks.require(fixed.getString("id").equals("minecraft:purple_banner") && !fixed.contains("Damage") && fixed.getByte("Count")==2,"False version stamps skipped the vanilla banner conversion: "+fixed); + CompoundTag tag=new CompoundTag(),block=new CompoundTag();block.putInt("Base",5);tag.put("BlockEntityTag",block);banner.put("tag",tag);ListTag items=new ListTag();items.add(banner);chest.put("Items",items);ListTag tiles=new ListTag();tiles.add(chest);input.getCompoundOrEmpty("Level").put("TileEntities",tiles); + CompoundTag fixed=processChunk(input).getListOrEmpty("block_entities").getCompoundOrEmpty(0).getListOrEmpty("Items").getCompoundOrEmpty(0); + RuntimeChecks.require(fixed.getStringOr("id", "").equals("minecraft:purple_banner") && !fixed.contains("Damage") && fixed.getIntOr("count",0)==2,"False version stamps skipped the vanilla banner conversion: "+fixed); } private static Map expectedInventory(CompoundTag source,int version) { CompoundTag expected=source.copy(); for(String key:Arrays.asList("Equipment","Aux")) { - ListTag items=expected.getList(key,10); - for(int i=0;i(FixtureApi.entities(world()));a.kill();first=newCorpse();saveId("first",first);phase(2); + before=new HashSet<>(FixtureApi.entities(world()));a.kill(a.level());first=newCorpse();saveId("first",first);phase(2); } else if(phase==2 && both(2)) {mark("both_clients_receive_actual_death_before_respawn");phase(3); } else if(phase==3 && both(3) && a!=null && a.isAlive()) { RuntimeChecks.require(first.isAlive(),"Server lost corpse after respawn"); @@ -115,7 +118,7 @@ public VisibilityChecks(MinecraftServer server) throws Exception { } else if(phase==120 && both(120)) { mark(Integer.getInteger("lootablebodies.visibility.waitTicks",6200)==6200?"default_corpse_visible_after_6200_ticks_and_310_seconds":"short_fixture_control_default_corpse_visible"); a.connection.teleport(100.5,PLAYER_Y,96.5,0,65);b.connection.teleport(102.5,PLAYER_Y,93.5,0,25); - a.getInventory().items.set(9,RuntimeChecks.stack(411));a.containerMenu.broadcastChanges();phase(13); + a.getInventory().getNonEquipmentItems().set(9,RuntimeChecks.stack(411));a.containerMenu.broadcastChanges();phase(13); } else if(phase==13 && both(13)) { RuntimeChecks.require(file("A.predeath-render-13.json").isFile(),"TNT death began before the owner rendered its loaded viewing area"); before=new HashSet<>(FixtureApi.entities(world()));PrimedTnt tnt=new PrimedTnt(world(),a.getX(),a.getY(),a.getZ(),null);tnt.setFuse(0); @@ -133,7 +136,7 @@ public VisibilityChecks(MinecraftServer server) throws Exception { RuntimeChecks.require(has(first,410),"Earlier corpse inventory changed during TNT/reconnect checks"); a.closeContainer();b.closeContainer();File folder=world().getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); RuntimeChecks.saveExpected(folder,"visibility-first",first);RuntimeChecks.saveExpected(folder,"visibility-second",second); - CompoundTag state=new CompoundTag();state.putUUID("First",first.getUUID());state.putUUID("Second",second.getUUID());state.putUUID("Player",a.getUUID());state.put("Inventory",a.getInventory().save(new ListTag())); + CompoundTag state=new CompoundTag();writeUuid(state, "First", first.getUUID());writeUuid(state, "Second", second.getUUID());writeUuid(state, "Player", a.getUUID());state.put("Inventory",FixtureApi.saveInventory(a)); try(FileOutputStream out=new FileOutputStream(new File(folder,"visibility-state.nbt"))){NbtIo.writeCompressed(state,out);} server.getPlayerList().saveAll();server.saveAllChunks(false,true,false);mark("tagged_inventory_recovered_and_complete_nbt_saved");phase(18); } else if(phase==18 && server.getPlayerList().getPlayerCount()==0) { @@ -174,21 +177,21 @@ private void verifyInventories() throws Exception { 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(n.getUUID(key)),"Saved identity differs");verifySnapshot(w,key.toLowerCase(Locale.ROOT),b);} - CompoundTag p;try(FileInputStream in=new FileInputStream(new File(folder,"playerdata/"+n.getUUID("Player")+".dat"))){p=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));} 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 { CompoundTag n;try(FileInputStream in=new FileInputStream(new File(w.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(),"visibility-"+name+".nbt"))){n=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} - ListTag pos=n.getList("Pos",6);RuntimeChecks.awaitChunk(w,(int)Math.floor(pos.getDouble(0)/16),(int)Math.floor(pos.getDouble(2)/16)); - return RuntimeChecks.find(w,n.getUUID("UUID")); + ListTag pos=typedList(n, "Pos", 6);RuntimeChecks.awaitChunk(w,(int)Math.floor(pos.getDoubleOr(0, 0D)/16),(int)Math.floor(pos.getDoubleOr(2, 0D)/16)); + return RuntimeChecks.find(w,readUuid(n, "UUID")); } private static void verifySnapshot(ServerLevel w,String name,EntityLootableBody body) throws Exception { CompoundTag expected;try(FileInputStream in=new FileInputStream(new File(w.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(),"visibility-"+name+".nbt"))){expected=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024));} - CompoundTag actual=new CompoundTag();body.saveAsPassenger(actual); + CompoundTag actual=new CompoundTag();FixtureApi.saveEntity(body,actual); for(String key:new String[]{"id","UUIDMost","UUIDLeast","Equipment","Aux","Name","CustomName","LootableBodiesUnresolved","LootableBodiesInvalidFields"})RuntimeChecks.require(Objects.equals(expected.get(key),actual.get(key)),"Reloaded corpse differs: "+name+" "+key); // These live-default fixtures keep ticking between the snapshot and logout. // Timer equality belongs to the separate frozen-timer persistence tests. - for(String key:new String[]{"DeathTime","DecayTime"})RuntimeChecks.require(actual.getLong(key)>=expected.getLong(key)&&actual.getLong(key)<=w.getGameTime(),"Reloaded live timer out of range: "+key); + for(String key:new String[]{"DeathTime","DecayTime"})RuntimeChecks.require(actual.getLongOr(key, 0L)>=expected.getLongOr(key, 0L)&&actual.getLongOr(key, 0L)<=w.getGameTime(),"Reloaded live timer out of range: "+key); } } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java index 42d886a..70148e9 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilityClientChecks.java @@ -1,7 +1,6 @@ package cyano.lootable.buildtest; import net.minecraft.client.gui.screens.TitleScreen; import net.minecraft.client.gui.screens.DeathScreen; -import net.minecraft.world.entity.boss.enderdragon.phases.AbstractDragonPhaseInstance; import net.minecraft.network.chat.Component; import cyano.lootable.entities.EntityLootableBody; @@ -23,7 +22,6 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.client.event.RenderLivingEvent; -import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.TickEvent; /** Receives real Forge spawn/destroy/respawn packets; never takes native input focus. */ @@ -36,37 +34,46 @@ final class VisibilityClientChecks { private final Set deathRendered=new HashSet<>(); private net.minecraft.network.Connection traced; private net.minecraft.client.multiplayer.ClientLevel profileWorld; - void register(){if(Boolean.getBoolean("lootablebodies.visibility")&&role!=null){net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this);MinecraftForge.EVENT_BUS.register(this);}} - @SubscribeEvent public void mouse(net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre e){e.setCanceled(true);} - @SubscribeEvent public void key(net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre e){e.setCanceled(true);} - @SubscribeEvent public void rendered(RenderLivingEvent.Post e){ - if(e.getEntity() instanceof EntityLootableBody) { - rendered.add(e.getEntity().getUUID()); - if(Minecraft.getInstance().screen instanceof DeathScreen && !e.getEntity().isRemoved() && !e.getEntity().isInvisible())deathRendered.add(e.getEntity().getUUID()); - } + void register(){if(Boolean.getBoolean("lootablebodies.visibility")&&role!=null){ + net.minecraftforge.client.event.ScreenEvent.MouseButtonPressed.Pre.BUS.addListener(event -> true); + net.minecraftforge.client.event.ScreenEvent.KeyPressed.Pre.BUS.addListener(event -> true); + TickEvent.ClientTickEvent.Pre.BUS.addListener(event -> tick(true)); + TickEvent.ClientTickEvent.Post.BUS.addListener(event -> tick(false)); + RenderLivingEvent.Post.BUS.addListener(this::rendered); + net.minecraftforge.client.event.ScreenEvent.Render.Post.BUS.addListener(this::deathScreenDrawn); + net.minecraftforge.event.entity.EntityLeaveLevelEvent.BUS.addListener(this::removed); + ProbeFrames.observe(this::readyForRealDeath); + }} + private void rendered(RenderLivingEvent.Post event) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || !(event.getRenderer() instanceof cyano.lootable.graphics.CorpseRenderer) + || !(event.getState() instanceof net.minecraft.client.renderer.entity.state.AvatarRenderState state) + || !(mc.level.getEntity(state.id) instanceof EntityLootableBody body)) return; + rendered.add(body.getUUID()); + if (mc.screen instanceof DeathScreen && !body.isRemoved() && !body.isInvisible()) deathRendered.add(body.getUUID()); } - @SubscribeEvent public void readyForRealDeath(net.minecraftforge.client.event.RenderLevelStageEvent e) { - if(!role.equals("A") || (phase!=1&&phase!=13) || done.contains(phase) || frames<20 || - e.getStage()!=net.minecraftforge.client.event.RenderLevelStageEvent.Stage.AFTER_PARTICLES)return; + + private void readyForRealDeath(ProbeFrames.Frame frame) { + if(!role.equals("A") || (phase!=1&&phase!=13) || done.contains(phase) || frames<20) return; Minecraft mc=Minecraft.getInstance(); if(mc.level==null || mc.player==null || !mc.player.isAlive() || mc.screen!=null || - !mc.level.hasChunkAt(mc.player.blockPosition()) || e.getCamera().getEntity()!=mc.player || - e.getCamera().getLookVector().y()>-0.75F)return; + !mc.level.hasChunkAt(mc.player.blockPosition()) || frame.camera().entity()!=mc.player || + frame.camera().forwardVector().y()>-0.75F)return; // Client ticks can catch up before a world frame has actually drawn. // Native death removes the owner's tracking connection after20ticks; // begin that finite window only with a real, loaded, downward view. net.minecraft.world.phys.Vec3 p=mc.player.position(); net.minecraft.world.phys.AABB futureBody=new net.minecraft.world.phys.AABB( p.x-0.425D,p.y,p.z-0.425D,p.x+0.425D,p.y+1.25D,p.z+0.425D).inflate(0.5D); - if(!e.getFrustum().isVisible(futureBody))return; + if(!frame.frustum().isVisible(futureBody))return; try { - String receipt="{\"phase\":"+phase+",\"native_world_frame\":true,\"render_tick\":"+e.getRenderTick()+ + String receipt="{\"phase\":"+phase+",\"native_world_frame\":true,\"render_tick\":"+frame.tick()+ ",\"pitch\":"+mc.player.getXRot()+",\"corpse_box_in_native_frustum\":true,\"at_nanos\":"+System.nanoTime()+"}"; Files.write(VisibilityChecks.file("A.predeath-render-"+phase+".json").toPath(),receipt.getBytes(StandardCharsets.UTF_8)); System.out.println("VISIBILITY_PREDEATH_RENDER_READY "+receipt);ack(); }catch(Throwable failure){failure.printStackTrace();finished=true;mc.stop();} } - @SubscribeEvent public void deathScreenDrawn(net.minecraftforge.client.event.ScreenEvent.Render.Post e) { + private void deathScreenDrawn(net.minecraftforge.client.event.ScreenEvent.Render.Post e) { if(!role.equals("A") || !(e.getScreen() instanceof DeathScreen) || (phase!=2&&phase!=14) || done.contains(phase))return; Minecraft mc=Minecraft.getInstance(); try { @@ -79,16 +86,16 @@ final class VisibilityClientChecks { System.out.println("VISIBILITY_DEATH_RENDER_PROOF "+role+" "+receipt);ack(); } catch(Throwable error){error.printStackTrace();finished=true;mc.stop();} } - @SubscribeEvent public void removed(net.minecraftforge.event.entity.EntityLeaveLevelEvent e) { + private void removed(net.minecraftforge.event.entity.EntityLeaveLevelEvent e) { Entity entity=e.getEntity(); - if(entity instanceof EntityLootableBody && entity.level().isClientSide) { + if(entity instanceof EntityLootableBody && entity.level().isClientSide()) { System.out.println("VISIBILITY_CLIENT_REMOVAL "+role+" phase="+phase+" uuid="+entity.getUUID()+" reason="+entity.getRemovalReason()+" rendered="+rendered.contains(entity.getUUID())); StackWalker.getInstance().forEach(frame->System.out.println("VISIBILITY_REMOVAL_FRAME "+frame)); } } - @SubscribeEvent public void tick(TickEvent.ClientTickEvent e) { + private void tick(boolean beforeClientTick) { Minecraft mc=Minecraft.getInstance(); - if(e.phase==TickEvent.Phase.START) { + if(beforeClientTick) { if(mc.level!=null&&mc.level!=profileWorld) { profileWorld=mc.level; // These two offline fixture accounts use explicit empty native @@ -96,10 +103,9 @@ final class VisibilityClientChecks { FixtureProfileCache.install(name->{ RuntimeChecks.require(name.matches("Container[AB]"),"Unexpected offline fixture profile"); UUID id=UUID.nameUUIDFromBytes(("OfflinePlayer:"+name).getBytes(StandardCharsets.UTF_8)); - var profile=new com.mojang.authlib.GameProfile(id,name); String texture="{\"timestamp\":0,\"profileId\":\""+id.toString().replace("-","")+"\",\"profileName\":\""+name+"\",\"textures\":{}}"; - profile.getProperties().put("textures",new com.mojang.authlib.properties.Property("textures",Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8)))); - return java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.of(profile)); + return java.util.concurrent.CompletableFuture.completedFuture(java.util.Optional.of( + FixtureProfileCache.withTexture(id, name, Base64.getEncoder().encodeToString(texture.getBytes(StandardCharsets.UTF_8))))); }); } return; @@ -153,7 +159,7 @@ final class VisibilityClientChecks { } else if(phase==7 || phase==9 || phase==11) { EntityLootableBody b=body(mc,"first");if(b!=null&&!b.isRemoved()&&!b.isInvisible()&&rendered.contains(b.getUUID())){shot(mc,"tracked-"+phase+".png");ack();} } else if(phase==8 || phase==10) { - if(role.equals(phase==8?"A":"B")){mc.level.disconnect();mc.disconnect();mc.setScreen(new TitleScreen());connecting=false;} + if(role.equals(phase==8?"A":"B")){FixtureConnections.disconnect(mc);connecting=false;} ack(); } else if(phase==12 || phase==120) { EntityLootableBody b=body(mc,"first");if(b!=null&&!b.isRemoved()&&rendered.contains(b.getUUID()))ack(); @@ -180,5 +186,5 @@ private EntityLootableBody body(Minecraft mc,String name) throws Exception { for(Entity e:FixtureApi.entities(mc.level))if(e instanceof EntityLootableBody&&e.getUUID().equals(id))return (EntityLootableBody)e;return null; } private void ack() throws Exception {done.add(phase);Files.write(VisibilityChecks.file(role+".visibility-"+phase).toPath(),new byte[]{1});} - private static void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),message->System.out.println(message.getString()));} + private static void shot(Minecraft mc,String name){Screenshot.grab(mc.gameDirectory,name,mc.getMainRenderTarget(),1,message->System.out.println(message.getString()));} } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java index 1c2ff38..1a28f90 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinChecks.java @@ -11,7 +11,7 @@ import java.util.concurrent.CompletableFuture; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.*; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; /** Deterministic service faults; no real account or authentication service required. */ @@ -24,14 +24,15 @@ static void run(Minecraft mc) throws Exception { SkinManager original=(SkinManager)skinField.get(mc); GameProfile profile=new GameProfile(UUID.fromString("00000000-0000-4000-8000-000000000201"),"VisibilityFault"); try { - skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture").toPath(),mc.getMinecraftSessionService(),mc) { - @Override public PlayerSkin getInsecureSkin(GameProfile p) { + skinField.set(mc,new SkinManager(new File(mc.gameDirectory,"skin-fixture").toPath(),mc.services(), + 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 skin cache failure"); } }); try { - ResourceLocation texture=CorpseRenderer.getSkin(profile); - RuntimeChecks.require(DefaultPlayerSkin.get(profile).texture().equals(texture),"Failed skin lookup did not use vanilla fallback"); + Identifier texture=CorpseRenderer.getSkin(profile); + RuntimeChecks.require(DefaultPlayerSkin.get(profile).body().texturePath().equals(texture),"Failed skin lookup did not use vanilla fallback"); mark(report,"skin_cache_failure_has_visible_default",true); } catch(Throwable failure) {failures.add("skin cache: "+failure);mark(report,"skin_cache_failure_has_visible_default",false);} } finally {skinField.set(mc,original);} @@ -73,9 +74,9 @@ static void run(Minecraft mc) throws Exception { } finally { LootableBodies.useLocalSkin=local; } - delayed.complete(Optional.of(new GameProfile(profile.getId(),"VisibilitySlow"))); + delayed.complete(Optional.of(new GameProfile(profile.id(),"VisibilitySlow"))); RuntimeChecks.require(requests.get()==2,"Faults did not exercise the native profile lookup"); - RuntimeChecks.require(slow.getGameProfile().getName().equals("VisibilityRenamed"),"Late skin result overwrote renamed corpse"); + RuntimeChecks.require(slow.getGameProfile().name().equals("VisibilityRenamed"),"Late skin result overwrote renamed corpse"); mark(report,"late_profile_result_preserves_new_name",true); } if(!failures.isEmpty())throw new AssertionError("VISIBILITY_SKIN_FAILURES "+failures); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java index e9f6f5a..799f263 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/VisibilitySkinClientChecks.java @@ -29,12 +29,13 @@ void tick(Minecraft mc) throws Exception { VisibilitySkinChecks.run(mc); for(Field field:Minecraft.class.getDeclaredFields())if(field.getType()==SkinManager.class){skinField=field;skinField.setAccessible(true);} original=(SkinManager)skinField.get(mc); - skinField.set(mc,new SkinManager(mc.getTextureManager(),new File(mc.gameDirectory,"skin-fixture").toPath(),mc.getMinecraftSessionService(),mc) { - @Override public net.minecraft.client.resources.PlayerSkin getInsecureSkin(GameProfile p){throw new IllegalStateException("injected render skin cache failure");} + skinField.set(mc,new SkinManager(new File(mc.gameDirectory,"skin-fixture").toPath(),mc.services(), + 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; } else if(stage==2 && ++frames>30) { - Screenshot.grab(mc.gameDirectory,"skin-fallback-models.png",mc.getMainRenderTarget(),message->System.out.println(message.getString())); + Screenshot.grab(mc.gameDirectory,"skin-fallback-models.png",mc.getMainRenderTarget(),1,message->System.out.println(message.getString())); skinField.set(mc,original);new File(mc.gameDirectory,"client.pass").createNewFile(); System.out.println("LOOTABLE_BODIES_VISIBILITY_SKIN_PASS");mc.stop();stage=3; } diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java index f9f1ba8..ee8893f 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WallEscapeChecks.java @@ -1,4 +1,7 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.readUuid; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.CompoundTag; @@ -213,17 +216,17 @@ static void reload(ServerLevel target, File folder) throws Exception { for (boolean trapped : new boolean[]{false,true}) { CompoundTag expected; try (InputStream in = new FileInputStream(new File(folder,"wall-escape-"+trapped+".nbt"))) { expected = NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)); } - ListTag pos = expected.getList("Pos",6); - BlockPos savedPosition=BlockPos.containing(pos.getDouble(0),pos.getDouble(1),pos.getDouble(2)); + ListTag pos = typedList(expected, "Pos", 6); + BlockPos savedPosition=BlockPos.containing(pos.getDoubleOr(0, 0D),pos.getDoubleOr(1, 0D),pos.getDoubleOr(2, 0D)); // Terrain admission completes before the native entity-storage inbox. RuntimeChecks.awaitChunk(world,savedPosition.getX()>>4,savedPosition.getZ()>>4); - EntityLootableBody body = RuntimeChecks.find(world, expected.getUUID("UUID")); + EntityLootableBody body = RuntimeChecks.find(world, readUuid(expected, "UUID")); CompoundTag actual=NameLifetimeChecks.saved(body); if(!TypedNbt.sameEntity(expected,actual)) { try(OutputStream out=new FileOutputStream(new File(folder,"wall-reload-actual-"+trapped+".nbt"))){NbtIo.writeCompressed(actual,out);} - for(String key:expected.getAllKeys())if(!NameLifetimeChecks.same(expected.get(key),actual.get(key))) + for(String key:expected.keySet())if(!NameLifetimeChecks.same(expected.get(key),actual.get(key))) System.out.println("WALL_RELOAD_DIFF key="+key+" expected="+expected.get(key)+" actual="+actual.get(key)); - for(String key:actual.getAllKeys())if(!expected.contains(key))System.out.println("WALL_RELOAD_EXTRA key="+key+" value="+actual.get(key)); + for(String key:actual.keySet())if(!expected.contains(key))System.out.println("WALL_RELOAD_EXTRA key="+key+" value="+actual.get(key)); } RuntimeChecks.require(TypedNbt.sameEntity(expected,NameLifetimeChecks.saved(body)), "Cold reload changed wall fixture NBT"); body.jumpOutOfWall(); diff --git a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java index 686809a..adab120 100644 --- a/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java +++ b/src/integrationTest/java/cyano/lootable/buildtest/WorldUpgradeChecks.java @@ -1,4 +1,7 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.hasType; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.item.Items; import net.minecraft.world.inventory.Slot; @@ -27,7 +30,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayerGameMode; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; @@ -39,7 +42,7 @@ final class WorldUpgradeChecks { private static int checks; static void run(MinecraftServer server,String phase) throws Exception { folder=server.getLevel(FixtureApi.dimensionKey(0)).getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); - manifest=read(new File(folder,"upgrade-fixtures.nbt"));sourceVersion=HistoricalVersions.dataVersion(manifest.getString("Minecraft")); + manifest=read(new File(folder,"upgrade-fixtures.nbt"));sourceVersion=HistoricalVersions.dataVersion(manifest.getStringOr("Minecraft", "")); CompoundTag writerInventory=null; String writerHash=System.getProperty("lootablebodies.testWriterInventory"); if(writerHash!=null) { @@ -47,77 +50,77 @@ static void run(MinecraftServer server,String phase) throws Exception { require(writerHash.equals(HexFormat.of().withUpperCase().formatHex(backupDigest(writerFile.toPath()))), "Sealed bridge inventory oracle changed"); writerInventory=read(writerFile); - require(writerInventory.getAllKeys().size()==manifest.getList("Bodies",10).size(), + require(writerInventory.keySet().size()==typedList(manifest, "Bodies", 10).size(), "Missing sealed bridge corpse inventories"); } File stored=new File(folder,"upgrade-recovered.nbt");ledger=stored.isFile()?read(stored):new CompoundTag();checks=0; - if(!ledger.contains("Recoveries",10))ledger.put("Recoveries",new CompoundTag()); + if(!hasType(ledger, "Recoveries", 10))ledger.put("Recoveries",new CompoundTag()); LootableBodies.allowCorpseDecay=false;LootableBodies.ticksPerItemDecay=-1; int wave=phase.startsWith("upgrade-near")?0:phase.startsWith("upgrade-far")?1:phase.equals("upgrade-late")?2:-1; boolean recover=phase.equals("upgrade-near-recover") || phase.equals("upgrade-far-reload") || phase.equals("upgrade-late") || phase.equals("upgrade-return"); - ListTag bodies=manifest.getList("Bodies",10); + ListTag bodies=typedList(manifest, "Bodies", 10); for(int i=0;i=0 && record.getInt("Wave")!=wave)continue; - ServerLevel world=server.getLevel(FixtureApi.dimensionKey(record.getInt("Dimension")));CompoundTag source=record.getCompound("Body"); - int x=net.minecraft.util.Mth.floor(source.getList("Pos",6).getDouble(0)),z=net.minecraft.util.Mth.floor(source.getList("Pos",6).getDouble(2));RuntimeChecks.awaitChunk(world,x>>4,z>>4); + CompoundTag record=bodies.getCompoundOrEmpty(i);if(wave>=0 && record.getIntOr("Wave", 0)!=wave)continue; + ServerLevel world=server.getLevel(FixtureApi.dimensionKey(record.getIntOr("Dimension", 0)));CompoundTag source=record.getCompoundOrEmpty("Body"); + int x=net.minecraft.util.Mth.floor(typedList(source, "Pos", 6).getDoubleOr(0, 0D)),z=net.minecraft.util.Mth.floor(typedList(source, "Pos", 6).getDoubleOr(2, 0D));RuntimeChecks.awaitChunk(world,x>>4,z>>4); UUID uuid=HistoricalUuid.read(source,"UUID");EntityLootableBody body=find(world,uuid);String id=uuid.toString(); - if(ledger.getString("Removed").equals(id)) {require(body==null,"Journal resurrected a deliberately removed corpse");continue;} + if(ledger.getStringOr("Removed", "").equals(id)) {require(body==null,"Journal resurrected a deliberately removed corpse");continue;} require(body!=null,"Missing historical corpse "+uuid); - require(body.getUUID().equals(uuid) && FixtureApi.dimensionId(body.level())==record.getInt("Dimension"),"UUID/dimension changed"); - require(body.getCustomNameTag().equals(source.contains("Name",8)?source.getString("Name"):source.getString("CustomName")),"Owner attribution changed"); - require(body.getX()==source.getList("Pos",6).getDouble(0) && body.getY()==source.getList("Pos",6).getDouble(1) && body.getZ()==source.getList("Pos",6).getDouble(2),"Position changed in dimension "+FixtureApi.dimensionId(world)+": "+body.position()); - CompoundTag actual=save(body);require(actual.getInt("LootableBodiesDataVersion")==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Missing corpse migration marker"); + require(body.getUUID().equals(uuid) && FixtureApi.dimensionId(body.level())==record.getIntOr("Dimension", 0),"UUID/dimension changed"); + require(body.getCustomNameTag().equals(hasType(source, "Name", 8)?source.getStringOr("Name", ""):source.getStringOr("CustomName", "")),"Owner attribution changed"); + require(body.getX()==typedList(source, "Pos", 6).getDoubleOr(0, 0D) && body.getY()==typedList(source, "Pos", 6).getDoubleOr(1, 0D) && body.getZ()==typedList(source, "Pos", 6).getDoubleOr(2, 0D),"Position changed in dimension "+FixtureApi.dimensionId(world)+": "+body.position()); + CompoundTag actual=save(body);require(actual.getIntOr("LootableBodiesDataVersion", 0)==cyano.lootable.upgrade.UpgradePreservation.DATA_VERSION,"Missing corpse migration marker"); require(NameLifetimeChecks.same(source.get("LootableBodiesInvalidFields"),actual.get("LootableBodiesInvalidFields")),"Original invalid-field evidence changed"); CompoundTag rawSource=source; int rawSourceVersion=sourceVersion; Map bridgeBanners=Map.of(); if(writerInventory!=null) { - CompoundTag writer=writerInventory.getCompound(id); - require(writer.contains("Inventory",10) && writer.contains("DataVersion",3), + CompoundTag writer=writerInventory.getCompoundOrEmpty(id); + require(hasType(writer, "Inventory", 10) && hasType(writer, "DataVersion", 3), "Missing sealed writer inventory for "+id); - rawSource=writer.getCompound("Inventory"); - rawSourceVersion=writer.getInt("DataVersion"); + rawSource=writer.getCompoundOrEmpty("Inventory"); + rawSourceVersion=writer.getIntOr("DataVersion", 0); bridgeBanners=bridgeBannerExpectations(source,rawSource,rawSourceVersion); } Map expected=expectedInventory(source,bridgeBanners);Map observed=inventory(actual); - CompoundTag recovery=ledger.getCompound("Recoveries").getCompound(id); + CompoundTag recovery=ledger.getCompoundOrEmpty("Recoveries").getCompoundOrEmpty(id); if(!recovery.contains("Storage"))verifySlots(source,actual,bridgeBanners); Map recovered=storage(world,recovery); merge(observed,recovered);require(expected.equals(observed),"Inventory conservation mismatch "+uuid+" expected="+expected+" actual="+observed); verifyRaw(rawSource,actual,rawSourceVersion); if(recover) { - if(!ledger.getCompound("Recoveries").contains(id,10)) {ledger.getCompound("Recoveries").put(id,new CompoundTag());recovery=ledger.getCompound("Recoveries").getCompound(id);} + if(!hasType(ledger.getCompoundOrEmpty("Recoveries"), id, 10)) {ledger.getCompoundOrEmpty("Recoveries").put(id,new CompoundTag());recovery=ledger.getCompoundOrEmpty("Recoveries").getCompoundOrEmpty(id);} recover(world,body,recovery); require(inventory(save(body)).isEmpty(),"Supported inventory remained after GUI recovery"); require(expected.equals(storage(world,recovery)),"GUI recovery lost or duplicated complete item data"); verifyRaw(rawSource,save(body),rawSourceVersion); } - verifyChest(world,record.getInt("Wave")); - verifyBiomes(world,record.getInt("Wave")); + verifyChest(world,record.getIntOr("Wave", 0)); + verifyBiomes(world,record.getIntOr("Wave", 0)); } verifyPlayer(server); if(wave==0 || wave<0) verifyVanilla(server); - if(ledger.contains("RemovedRawBackupSha256",7)) { + if(hasType(ledger, "RemovedRawBackupSha256", 7)) { var backup=new File(folder,"upgrade-removed-original.nbt").toPath(); - require(Arrays.equals(ledger.getByteArray("RemovedRawBackupSha256"),backupDigest(backup)), + require(Arrays.equals(ledger.getByteArray("RemovedRawBackupSha256").orElseGet(() -> new byte[0]),backupDigest(backup)), "Removed raw-only fixture backup changed before reopen"); } - if(phase.equals("upgrade-final") && ledger.getString("Removed").isEmpty()) { + if(phase.equals("upgrade-final") && ledger.getStringOr("Removed", "").isEmpty()) { EntityLootableBody rawOnly=null; for(int i=0;i0 && !FixtureStackData.load(item).isEmpty(); + return hasType(item, "id", 8) && item.getIntOr("count", 0)>0 && !FixtureStackData.load(item).isEmpty(); } private static CompoundTag expectedCorpseItem(CompoundTag source) { return expectedCorpseItem(source,sourceVersion); @@ -165,46 +168,60 @@ private static CompoundTag expectedCorpseItem(CompoundTag source,int version) { static CompoundTag expectedItem(CompoundTag source,int version) { // Independent vanilla oracle. Production additionally checks conservation, // resolves registries and shields owned entity payloads; this path does not. - if(!source.contains("id",8) || !source.contains("Count"))return source.copy(); + if(!hasType(source, "id", 8) || !source.contains("Count"))return source.copy(); com.mojang.datafixers.DataFixer vanilla=NativeVanillaDataFixer.get(); CompoundTag input=source.copy(); - if(version<704 && (source.getString("id").equals("minecraft:spawn_egg") || source.getString("id").equals("minecraft:chest") || source.getString("id").equals("minecraft:banner"))) { + if(version<704 && (source.getStringOr("id", "").equals("minecraft:spawn_egg") || source.getStringOr("id", "").equals("minecraft:chest") || source.getStringOr("id", "").equals("minecraft:banner"))) { // These three sealed writer fixtures exercise the broken vanilla // namespace boundary. Their expected IDs are explicit, independent // of the production bridge and registry lookup. input=cyano.lootable.upgrade.NativeNbt.update(vanilla,net.minecraft.util.datafix.fixes.References.ITEM_STACK,input,512,703); - CompoundTag tag=input.getCompound("tag"); - if(source.getString("id").equals("minecraft:spawn_egg")) { - require(tag.getCompound("EntityTag").getString("id").equals("Pig"),"Unexpected historical egg fixture"); - tag.getCompound("EntityTag").putString("id","minecraft:pig"); - } else tag.getCompound("BlockEntityTag").putString("id",source.getString("id")); + CompoundTag tag=input.getCompoundOrEmpty("tag"); + if(source.getStringOr("id", "").equals("minecraft:spawn_egg")) { + require(tag.getCompoundOrEmpty("EntityTag").getStringOr("id", "").equals("Pig"),"Unexpected historical egg fixture"); + tag.getCompoundOrEmpty("EntityTag").putString("id","minecraft:pig"); + } else tag.getCompoundOrEmpty("BlockEntityTag").putString("id",source.getStringOr("id", "")); version=705; } + CompoundTag legacyEgg = input.getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag"); + boolean historicalPlainName = version >= 1458 && version < 4290 + && input.getStringOr("id", "").equals("minecraft:pig_spawn_egg") + && legacyEgg.getStringOr("id", "").equals("minecraft:pig") + && legacyEgg.getStringOr("CustomName", "").equals("Saved pig egg"); + if (historicalPlainName) { + // The sealed 1.13.2 writer stored this exact literal after the old name fix. + // Supply its independent JSON fixture to the native expected-value conversion. + legacyEgg.putString("CustomName", "\"Saved pig egg\""); + } CompoundTag result=cyano.lootable.upgrade.NativeNbt.update(vanilla,net.minecraft.util.datafix.fixes.References.ITEM_STACK,input,version,cyano.lootable.upgrade.UpgradePreservation.MINECRAFT_VERSION); - if(source.getInt("Count")<=0)return source.copy(); - require(result.contains("count",3) && source.getInt("Count")==result.getInt("count"),"Vanilla oracle changed an item count"); + if (historicalPlainName) { + require(result.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:entity_data") + .getStringOr("CustomName", "").equals("Saved pig egg"), "Native oracle changed the saved egg name"); + } + if(source.getIntOr("Count", 0)<=0)return source.copy(); + require(hasType(result, "count", 3) && source.getIntOr("Count", 0)==result.getIntOr("count", 0),"Vanilla oracle changed an item count"); require(Objects.equals(source.get("Slot"),result.get("Slot")),"Vanilla oracle changed an item slot"); ItemStack nativeStack=FixtureStackData.load(result); if(nativeStack.isEmpty())return result; - require(nativeStack.getCount()==result.getInt("count"),"Native oracle codec changed an item count"); + require(nativeStack.getCount()==result.getIntOr("count", 0),"Native oracle codec changed an item count"); CompoundTag saved=FixtureStackData.save(nativeStack); if(result.contains("Slot"))saved.put("Slot",result.get("Slot").copy()); return saved; } private static Map bridgeBannerExpectations(CompoundTag seed,CompoundTag writer,int writerVersion) { Map expected=new HashMap<>(); - ListTag original=seed.getList("Equipment",10),written=writer.getList("Equipment",10); + ListTag original=typedList(seed, "Equipment", 10),written=typedList(writer, "Equipment", 10); for(int i=0;i bridgeBannerExpectations(CompoundTag see return expected; } private static CompoundTag semanticBannerColour(CompoundTag source) { - CompoundTag item=canonicalItem(source),components=item.getCompound("components"); - String id=item.getString("id"); + CompoundTag item=canonicalItem(source),components=item.getCompoundOrEmpty("components"); + String id=item.getStringOr("id", ""); if(id.startsWith("minecraft:") && id.endsWith("_banner") - && components.getString("minecraft:base_color").equals(id.substring(10,id.length()-7))) { + && components.getStringOr("minecraft:base_color", "").equals(id.substring(10,id.length()-7))) { // A historical writer can move the same base colour into Damage. // Require the sealed writer's exact resulting components at runtime. components.remove("minecraft:base_color"); @@ -230,71 +247,71 @@ private static CompoundTag semanticBannerColour(CompoundTag source) { } private static void verifySlots(CompoundTag source,CompoundTag actual,Map bridgeBanners) { Map slots=new TreeMap<>(); - ListTag entries=source.getList("Equipment",10); + ListTag entries=typedList(source, "Equipment", 10); for(int i=0;i=48 || slots.containsKey(slot))continue; item.remove("Slot");slots.put(slot,item); } - Map saved=new TreeMap<>();ListTag entriesNow=actual.getList("Equipment",10); - for(int i=0;i saved=new TreeMap<>();ListTag entriesNow=typedList(actual, "Equipment", 10); + for(int i=0;i entry:slots.entrySet()) require(Objects.equals(TypedNbt.canonical(canonicalItem(entry.getValue())),TypedNbt.canonical(canonicalItem(saved.get(entry.getKey())))),"Historical inventory slot moved: "+entry.getKey()+" expected="+entry.getValue()+" actual="+saved.get(entry.getKey())); } private static CompoundTag canonicalItem(CompoundTag source) { if(source==null)return null; - CompoundTag item=source.copy();String id=item.getString("id"); + CompoundTag item=source.copy();String id=item.getStringOr("id", ""); String[] colours={"white","orange","magenta","light_blue","yellow","lime","pink","gray","light_gray","cyan","purple","blue","brown","green","red","black"}; for(int colour=0;colour map,CompoundTag source,boolean indexed) { - if(!source.contains("id",8) || !source.contains("count",3) || source.getInt("count")<=0)return; - CompoundTag item=canonicalItem(source);int count=item.getInt("count");item.remove("count");if(!indexed)item.remove("Slot"); + if(!hasType(source, "id", 8) || !hasType(source, "count", 3) || source.getIntOr("count", 0)<=0)return; + CompoundTag item=canonicalItem(source);int count=item.getIntOr("count", 0);item.remove("count");if(!indexed)item.remove("Slot"); String key=TypedNbt.canonical(item);map.put(key,map.getOrDefault(key,0)+count); } private static void merge(Map a,Map b){for(Map.Entry item:b.entrySet())a.put(item.getKey(),a.getOrDefault(item.getKey(),0)+item.getValue());} - private static Map inventory(CompoundTag body){Map map=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}){ListTag items=body.getList(key,10);for(int i=0;i inventory(CompoundTag body){Map map=new TreeMap<>();for(String key:new String[]{"Equipment","Aux"}){ListTag items=typedList(body, key, 10);for(int i=0;i expectedInventory(CompoundTag source,Map bridgeBanners) { Map expected=new TreeMap<>(); for(String key:new String[]{"Equipment","Aux"}) { - ListTag list=source.getList(key,10); + ListTag list=typedList(source, key, 10); for(int i=0;i expected=new ArrayList<>(); - for(String key:new String[]{"Equipment","Aux"}) {ListTag list=source.getList(key,10);for(int i=0;i contents(Container inventory,boolean indexed) { @@ -316,12 +333,12 @@ private static void addVanillaSeed(Map map,CompoundTag item) { } private static Map indexed(ListTag list) { Map map=new TreeMap<>(); - for(int i=0;i storage(ServerLevel world,CompoundTag recovery) {Map map=new TreeMap<>();ListTag list=storageList(recovery);for(int i=0;i storage(ServerLevel world,CompoundTag recovery) {Map map=new TreeMap<>();ListTag list=storageList(recovery);for(int i=0;i0) {destination=(Container)world.getBlockEntity(position(stored.getCompound(stored.size()-1)));for(int i=0;i0) {destination=(Container)world.getBlockEntity(position(stored.getCompoundOrEmpty(stored.size()-1)));for(int i=0;i expected=indexed(source.getList("Items",10)),actual=contents((Container)tile,true);if(!expected.equals(actual)){Map missing=new TreeMap<>(expected),unexpected=new TreeMap<>(actual);missing.entrySet().removeAll(actual.entrySet());unexpected.entrySet().removeAll(expected.entrySet());System.out.println("VANILLA_CHEST_DIFF missing="+missing+" unexpected="+unexpected);}require(expected.equals(actual),"Vanilla source chest slots/data changed");} + ListTag containers=typedList(manifest, "Containers", 10); + for(int i=0;i expected=indexed(typedList(source, "Items", 10)),actual=contents((Container)tile,true);if(!expected.equals(actual)){Map missing=new TreeMap<>(expected),unexpected=new TreeMap<>(actual);missing.entrySet().removeAll(actual.entrySet());unexpected.entrySet().removeAll(expected.entrySet());System.out.println("VANILLA_CHEST_DIFF missing="+missing+" unexpected="+unexpected);}require(expected.equals(actual),"Vanilla source chest slots/data changed");} } private static void verifyPlayer(MinecraftServer server) throws Exception { ServerLevel world=server.getLevel(FixtureApi.dimensionKey(0));UUID id=HistoricalUuid.read(manifest,"Player"); CompoundTag expected=read(new File(folder,"upgrade-player-source.nbt")); - ServerPlayer player=new ServerPlayer(server,world,new GameProfile(id,"UpgradeFixture"),net.minecraft.server.level.ClientInformation.createDefault());FixtureApi.playerData(server).load(player); + ServerPlayer player=new ServerPlayer(server,world,new GameProfile(id,"UpgradeFixture"),net.minecraft.server.level.ClientInformation.createDefault());FixtureApi.loadEntityData(player, FixtureApi.playerData(server).load(new net.minecraft.server.players.NameAndId(player.getGameProfile())).orElseThrow()); // PlayerInventory serializes armour as 100..103 and offhand as 150. - ListTag actual=player.getInventory().save(new ListTag());require(indexedPlayer(expected,"Inventory").equals(indexedCurrent(actual)),"Player inventory slots, counts or custom data changed"); - require(indexedPlayer(expected,"EnderItems").equals(indexedCurrent(player.getEnderChestInventory().createTag(world.registryAccess()))),"Ender chest slots/data changed"); + Map wanted = indexedPlayer(expected, "Inventory"); + Map observed = indexedCurrent(FixtureApi.saveInventory(player)); + if (!wanted.equals(observed)) { + Map missing = new TreeMap<>(wanted), unexpected = new TreeMap<>(observed); + missing.entrySet().removeAll(observed.entrySet()); + unexpected.entrySet().removeAll(wanted.entrySet()); + System.out.println("VANILLA_PLAYER_DIFF missing=" + missing + " unexpected=" + unexpected); + } + require(wanted.equals(observed), "Player inventory slots, counts or custom data changed"); + require(indexedPlayer(expected,"EnderItems").equals(indexedCurrent(FixtureApi.saveEnderInventory(player))),"Ender chest slots/data changed"); require(player.getUUID().equals(id),"Player UUID changed"); - if(manifest.contains("Recipe",8)) { - require(HistoricalUnlocks.sugarPair(manifest.getString("Recipe"),manifest.getString("Advancement")),"Unexpected historical recipe/advancement fixture"); - net.minecraft.world.item.crafting.RecipeHolder recipe=server.getRecipeManager().byKey(ResourceLocation.parse("minecraft:sugar_from_sugar_cane")).orElse(null); - require(recipe!=null && player.getRecipeBook().contains(recipe),"Native sugar recipe conversion lost its unlock"); - net.minecraft.advancements.AdvancementHolder advancement=server.getAdvancements().get(ResourceLocation.parse("minecraft:recipes/misc/sugar_from_sugar_cane")); + if(hasType(manifest, "Recipe", 8)) { + require(HistoricalUnlocks.sugarPair(manifest.getStringOr("Recipe", ""),manifest.getStringOr("Advancement", "")),"Unexpected historical recipe/advancement fixture"); + var recipeKey = net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE, + Identifier.parse("minecraft:sugar_from_sugar_cane")); + require(server.getRecipeManager().byKey(recipeKey).isPresent() && player.getRecipeBook().contains(recipeKey),"Native sugar recipe conversion lost its unlock"); + net.minecraft.advancements.AdvancementHolder advancement=server.getAdvancements().get(Identifier.parse("minecraft:recipes/misc/sugar_from_sugar_cane")); require(advancement!=null && player.getAdvancements().getOrStartProgress(advancement).isDone(),"Native sugar advancement conversion lost its completion"); player.getAdvancements().save(); } FixtureApi.playerData(server).save(player); } private static void verifyBiomes(ServerLevel world,int wave) { - ListTag samples=manifest.getList("BiomeSamples",10); + ListTag samples=typedList(manifest, "BiomeSamples", 10); for(int i=0;i indexedPlayer(CompoundTag player,String key) { - int version=player.contains("DataVersion",3)?player.getInt("DataVersion"):(sourceVersion); + int version=hasType(player, "DataVersion", 3)?player.getIntOr("DataVersion", 0):(sourceVersion); Map map=new TreeMap<>(); - for(Tag entry:player.getList(key,10))add(map,expectedItem((CompoundTag)entry,version),true); + for(Tag entry:typedList(player, key, 10))add(map,expectedItem((CompoundTag)entry,version),true); return map; } - private static Map indexedCurrent(ListTag list){Map map=new TreeMap<>();for(int i=0;i indexedCurrent(ListTag list){Map map=new TreeMap<>();for(int i=0;i>4,net.minecraft.util.Mth.floor(pos.getDouble(2))>>4);Entity found=null;for(Entity e:FixtureApi.entities(world))if(e.getUUID().equals(HistoricalUuid.read(source,"UUID")))found=e;require(found!=null,"Vanilla entity disappeared");CompoundTag actual=new CompoundTag();found.saveAsPassenger(actual);require(actual.getString("id").equals(record.getString("ExpectedType")),"Vanilla entity-name conversion failed");require(FixtureApi.dimensionId(found.level())==record.getInt("Dimension"),"Vanilla entity dimension changed");if(record.getString("ExpectedType").equals("minecraft:villager"))verifyVillager(source,actual);if(found instanceof Container)require(indexed(source.getList("Items",10)).equals(contents((Container)found,true)),"Vanilla minecart inventory changed");if(record.getString("ExpectedType").equals("minecraft:armor_stand"))require(indexedEquipment(source.getList("ArmorItems",10)).equals(indexedNativeEquipment(actual.getList("ArmorItems",10))),"Vanilla armour stand equipment changed");} + ListTag list=typedList(manifest, "Vanilla", 10); + for(int i=0;i>4,net.minecraft.util.Mth.floor(pos.getDoubleOr(2, 0D))>>4);Entity found=null;for(Entity e:FixtureApi.entities(world))if(e.getUUID().equals(HistoricalUuid.read(source,"UUID")))found=e;require(found!=null,"Vanilla entity disappeared");CompoundTag actual=new CompoundTag();FixtureApi.saveEntity(found,actual);require(actual.getStringOr("id", "").equals(record.getStringOr("ExpectedType", "")),"Vanilla entity-name conversion failed");require(FixtureApi.dimensionId(found.level())==record.getIntOr("Dimension", 0),"Vanilla entity dimension changed");if(record.getStringOr("ExpectedType", "").equals("minecraft:villager"))verifyVillager(source,actual);if(found instanceof Container)require(indexed(typedList(source, "Items", 10)).equals(contents((Container)found,true)),"Vanilla minecart inventory changed");if(record.getStringOr("ExpectedType", "").equals("minecraft:armor_stand"))require(indexedEquipment(typedList(source, "ArmorItems", 10)).equals(indexedNativeEquipment(actual.getCompoundOrEmpty("equipment"))),"Vanilla armour stand equipment changed");} } private static void verifyVillager(CompoundTag source,CompoundTag actual) { CompoundTag expected=cyano.lootable.upgrade.NativeNbt.update(NativeVanillaDataFixer.get(),net.minecraft.util.datafix.fixes.References.ENTITY,source.copy(),sourceVersion,cyano.lootable.upgrade.UpgradePreservation.MINECRAFT_VERSION); require(NameLifetimeChecks.same(expected.get("VillagerData"),actual.get("VillagerData")),"Vanilla villager profession, career or level changed"); Map observed=new TreeMap<>(); - for(Tag item:actual.getList("Inventory",10))addVanillaSeed(observed,(CompoundTag)item); - require(indexed(source.getList("Inventory",10)).equals(observed),"Vanilla villager inventory changed"); + for(Tag item:typedList(actual, "Inventory", 10))addVanillaSeed(observed,(CompoundTag)item); + require(indexed(typedList(source, "Inventory", 10)).equals(observed),"Vanilla villager inventory changed"); + } + private static Map indexedNativeEquipment(CompoundTag equipment) { + Map map = new TreeMap<>(); + List slots = List.of("feet", "legs", "chest", "head"); + for (int i = 0; i < slots.size(); i++) { + CompoundTag item = equipment.getCompoundOrEmpty(slots.get(i)).copy(); + item.putByte("Slot", (byte) i); + addVanillaSeed(map, item); + } + return map; } - private static Map indexedNativeEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;i indexedEquipment(ListTag list){Map map=new TreeMap<>();for(int i=0;inew ConfigScreenHandler.ConfigScreenFactory(new CorpseConfigGuiFactory())); - FMLJavaModLoadingContext.get().getModEventBus().addListener(ClientProxy::setup); - FMLJavaModLoadingContext.get().getModEventBus().addListener(ClientProxy::renderers); + FMLClientSetupEvent.getBus(context.getModBusGroup()).addListener(ClientProxy::setup); + EntityRenderersEvent.RegisterRenderers.BUS.addListener(ClientProxy::renderers); + } + public static java.util.concurrent.CompletableFuture> fetchProfile(String name) { + var partial = net.minecraft.world.item.component.ResolvableProfile.createUnresolved(name); + return partial.resolveProfile(Minecraft.getInstance().services().profileResolver()) + .thenApply(profile -> profile.equals(partial.partialProfile()) ? java.util.Optional.empty() : java.util.Optional.of(profile)); } private static void setup(net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent event) { // The client creates these readers before mod construction. Replace diff --git a/src/main/java/cyano/lootable/LootableBodies.java b/src/main/java/cyano/lootable/LootableBodies.java index 68082f5..3ffb952 100644 --- a/src/main/java/cyano/lootable/LootableBodies.java +++ b/src/main/java/cyano/lootable/LootableBodies.java @@ -10,15 +10,20 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.SpawnEggItem; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.registries.RegisterEvent; import net.minecraft.core.registries.Registries; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.DistExecutor; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.event.BuildCreativeModeTabContentsEvent; +import net.minecraftforge.event.entity.EntityAttributeCreationEvent; +import net.minecraftforge.event.level.LevelEvent; import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.eventbus.api.listener.Priority; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.registries.NewRegistryEvent; import net.minecraftforge.event.server.ServerStoppedEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.loading.FMLPaths; @@ -27,8 +32,8 @@ public final class LootableBodies { public static final String MODID = "lootablebodies"; public static final String NAME = "DrCyano's Lootable Bodies"; - public static final String VERSION = "2.4.13.121011"; - public static final ResourceLocation CORPSE_ID = ResourceLocation.fromNamespaceAndPath(MODID,"corpse"); + public static final String VERSION = "2.4.14.121111"; + public static final Identifier CORPSE_ID = Identifier.fromNamespaceAndPath(MODID,"corpse"); public static EntityType CORPSE; public static Item CORPSE_EGG; public static net.minecraft.world.inventory.MenuType CORPSE_CONTAINER; @@ -51,25 +56,30 @@ public final class LootableBodies { public static boolean voidPlatform = false; public static boolean dimensionSpecificPlatform = true; - public LootableBodies() { + public LootableBodies(FMLJavaModLoadingContext context) { INSTANCE=this; UpgradePreservation.register(net.minecraft.util.datafix.DataFixers.getDataFixer()); loadConfiguration(FMLPaths.CONFIGDIR.get().resolve("lootablebodies.cfg").toFile()); - net.minecraftforge.eventbus.api.IEventBus bus=FMLJavaModLoadingContext.get().getModEventBus(); - bus.addListener((net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent event) -> + var modBus = context.getModBusGroup(); + FMLCommonSetupEvent.getBus(modBus).addListener(event -> event.enqueueWork(cyano.lootable.upgrade.EarlyWorldPreservation::install)); - bus.addListener(cyano.lootable.upgrade.LegacyProfessionRegistry::create); - bus.addListener((net.minecraftforge.event.entity.EntityAttributeCreationEvent event)->event.put(corpseType(),EntityLootableBody.corpseAttributeDefaults().build())); - bus.addListener(this::registerEntities); - bus.addListener(this::registerItems); - bus.addListener(cyano.lootable.upgrade.LegacyRegistryMappings::retiredMemories); - bus.addListener(this::registerContainers); - bus.addListener((net.minecraftforge.event.BuildCreativeModeTabContentsEvent event) -> { - if(CreativeModeTabs.SPAWN_EGGS.equals(event.getTabKey()))event.accept(CORPSE_EGG); + NewRegistryEvent.getBus(modBus).addListener(cyano.lootable.upgrade.LegacyProfessionRegistry::create); + EntityAttributeCreationEvent.getBus(modBus).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 -> { + if (CreativeModeTabs.SPAWN_EGGS.equals(event.getTabKey())) event.accept(() -> CORPSE_EGG); }); - MinecraftForge.EVENT_BUS.register(new PlayerDeathEventHandler()); - MinecraftForge.EVENT_BUS.register(this); - DistExecutor.runWhenOn(Dist.CLIENT,()->ClientProxy::register); + new PlayerDeathEventHandler().register(); + cyano.lootable.upgrade.LegacyRegistryMappings.register(); + ServerAboutToStartEvent.BUS.addListener(this::beforeWorldLoading); + LevelEvent.Load.BUS.addListener(Priority.HIGHEST, this::worldLoaded); + LevelEvent.CreateSpawnPosition.BUS.addListener(Priority.HIGHEST, this::beforeSpawn); + ServerStoppedEvent.BUS.addListener(this::worldStopped); + if (FMLEnvironment.dist == Dist.CLIENT) ClientProxy.register(context); } private static synchronized EntityType corpseType() { if(CORPSE==null) { @@ -77,7 +87,7 @@ private static synchronized EntityType corpseType() { // Native 1.14 expresses tracking range in chunks: two retain 32 blocks. .sized(0.85F,0.75F).setTrackingRange(2).setUpdateInterval(1).setShouldReceiveVelocityUpdates(true); if(completelyInvulnerable || !hurtByEnvironment)type.fireImmune(); - CORPSE=type.build(CORPSE_ID.toString()); + CORPSE=type.build(ResourceKey.create(Registries.ENTITY_TYPE, CORPSE_ID)); } return CORPSE; } @@ -85,8 +95,9 @@ private void registerEntities(RegisterEvent event) { event.register(Registries.ENTITY_TYPE,CORPSE_ID,LootableBodies::corpseType); } private void registerItems(RegisterEvent event) { - event.register(Registries.ITEM,ResourceLocation.fromNamespaceAndPath(MODID,"corpse_spawn_egg"),()-> { - CORPSE_EGG=new SpawnEggItem(corpseType(),1,1,new Item.Properties()); + Identifier egg = Identifier.fromNamespaceAndPath(MODID, "corpse_spawn_egg"); + event.register(Registries.ITEM, egg, () -> { + CORPSE_EGG = new SpawnEggItem(new Item.Properties().setId(ResourceKey.create(Registries.ITEM, egg)).spawnEgg(corpseType())); return CORPSE_EGG; }); } @@ -101,7 +112,7 @@ private void registerContainers(RegisterEvent event) { }); } public static LootableBodies getInstance(){return INSTANCE;} - @SubscribeEvent public void beforeWorldLoading(ServerAboutToStartEvent event) { + public void beforeWorldLoading(ServerAboutToStartEvent event) { File level=event.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).toFile(); try { UpgradePreservation.register(event.getServer().getFixerUpper()); @@ -112,15 +123,13 @@ private void registerContainers(RegisterEvent event) { throw new IllegalStateException("Lootable Bodies could not preserve this world's corpses. World loading has been stopped; keep the original world and recovery journal.",unsafe); } } - @SubscribeEvent(priority=net.minecraftforge.eventbus.api.EventPriority.HIGHEST) public void worldLoaded(net.minecraftforge.event.level.LevelEvent.Load event) { if(event.getLevel() instanceof net.minecraft.server.level.ServerLevel)ChunkVersionContext.installWorld((net.minecraft.server.level.ServerLevel)event.getLevel()); } - @SubscribeEvent(priority=net.minecraftforge.eventbus.api.EventPriority.HIGHEST) public void beforeSpawn(net.minecraftforge.event.level.LevelEvent.CreateSpawnPosition event) { if(event.getLevel() instanceof net.minecraft.server.level.ServerLevel)ChunkVersionContext.installWorld((net.minecraft.server.level.ServerLevel)event.getLevel()); } - @SubscribeEvent public void worldStopped(ServerStoppedEvent event){UpgradePreservation.close();} + public void worldStopped(ServerStoppedEvent event){UpgradePreservation.close();} private static boolean or(boolean... values){for(boolean value:values)if(value)return true;return false;} /** Reads startup rules. GUI saves deliberately do not call this method. */ public static void loadConfiguration(File file) { diff --git a/src/main/java/cyano/lootable/entities/EntityLootableBody.java b/src/main/java/cyano/lootable/entities/EntityLootableBody.java index bb07ea0..f26393a 100644 --- a/src/main/java/cyano/lootable/entities/EntityLootableBody.java +++ b/src/main/java/cyano/lootable/entities/EntityLootableBody.java @@ -1,5 +1,10 @@ package cyano.lootable.entities; +import static cyano.lootable.upgrade.NativeNbt.hasType; +import static cyano.lootable.upgrade.NativeNbt.isListOf; +import static cyano.lootable.upgrade.NativeNbt.readUuid; +import static cyano.lootable.upgrade.NativeNbt.hasUuid; + import static cyano.lootable.ItemStacks.emptyStack; import cyano.lootable.upgrade.UpgradePreservation; import cyano.lootable.upgrade.ItemMigration; @@ -22,8 +27,17 @@ import net.minecraft.nbt.NumericTag; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.entity.ContainerUser; +import net.minecraft.world.level.storage.TagValueInput; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import cyano.lootable.upgrade.NativeNbt; import net.minecraft.world.effect.MobEffect; -import net.minecraft.world.level.block.entity.SkullBlockEntity; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.damagesource.DamageTypes; import net.minecraft.world.InteractionHand; @@ -33,7 +47,7 @@ import cyano.lootable.network.CorpseProfiles; import net.minecraft.core.NonNullList; import net.minecraft.sounds.SoundEvent; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.world.phys.AABB; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; @@ -99,7 +113,7 @@ public EntityLootableBody(Level w) { } public EntityLootableBody(Player player){ - this(player.getCommandSenderWorld()); + this(player.level()); this.setPos(player.getX(), player.getY()+0.5, player.getZ()); this.setDeltaMovement(player.getDeltaMovement().add(0D,0.0784000015258789D,0D)); @@ -123,7 +137,7 @@ private void log(Object o){ private String oldName = null; @Override public void tick() { - if(!getCommandSenderWorld().isClientSide) { + if(!level().isClientSide()) { // A Forge ticket can tick our centre chunk while neighbouring support // terrain is absent. Never treat those missing collision boxes as air. AABB bounds = getBoundingBox(); @@ -131,7 +145,7 @@ public void tick() { bounds.minZ + Math.min(0D,getDeltaMovement().z)); BlockPos to = BlockPos.containing(bounds.maxX + Math.max(0D,getDeltaMovement().x), 0, bounds.maxZ + Math.max(0D,getDeltaMovement().z)); - if(!getCommandSenderWorld().hasChunksAt(from,to)) return; + if(!level().hasChunksAt(from,to)) return; } super.tick(); } @@ -140,7 +154,7 @@ public void tick() { public void baseTick(){ //log("position=%s; terminate=%s; time=%s; age=%s; deathTimestamp=%s; decayTimestamp=%s; Health=%s; isDead=%s" ,getPosition(),terminate,getEntityWorld().getTotalWorldTime(),getEntityWorld().getTotalWorldTime() - deathTimestamp, deathTimestamp, decayTimestamp, getHealth(), isDead); super.baseTick(); - final boolean isClient = getCommandSenderWorld().isClientSide; + final boolean isClient = level().isClientSide(); final boolean isServer = !isClient; if(isServer && terminate >= 0){ @@ -178,7 +192,7 @@ public void baseTick(){ } // handle decay time - final long currentTime = Math.max(0L,getCommandSenderWorld().getGameTime()); + final long currentTime = Math.max(0L,level().getGameTime()); if(isServer) { // A clock rollback must not suspend decay indefinitely or overflow age. deathTimestamp = checkedTimestamp("DeathTime",deathTimestamp,currentTime); @@ -190,7 +204,7 @@ public void baseTick(){ } long age = currentTime - deathTimestamp; if(age > LootableBodies.corpseDecayTime){ - this.kill(); + this.kill((ServerLevel) level()); } } @@ -220,7 +234,7 @@ public void baseTick(){ // handle item vacuuming if(isServer && vacuumTime < VACUUM_TIMELIMIT){ vacuumTime++; - List items = getCommandSenderWorld().getEntitiesOfClass(ItemEntity.class, new AABB( + List items = level().getEntitiesOfClass(ItemEntity.class, new AABB( getX() - VACUUM_RADIUS, getY() - VACUUM_RADIUS, getZ() - VACUUM_RADIUS, getX() + VACUUM_RADIUS, getY() + VACUUM_RADIUS, getZ() + VACUUM_RADIUS)); for(ItemEntity item : items){ @@ -363,17 +377,17 @@ public ItemStack mergeItem(int slot, ItemStack stack) { @Override - protected void actuallyHurt(DamageSource src, float amount){ + protected void actuallyHurt(ServerLevel world, DamageSource src, float amount){ if(CorpseDamageRules.kind(src)==CorpseDamageRules.Kind.ATTACK && src.getEntity() instanceof Player){ Player player=(Player)src.getEntity(); ItemStack item=player.getItemInHand(InteractionHand.MAIN_HAND); if(!emptyStack(item) && (item.getItem() instanceof ShovelItem || item.canPerformAction(net.minecraftforge.common.ToolActions.SHOVEL_DIG))) - this.kill(); + this.kill(world); } if(CorpseDamageRules.isWall(src))this.jumpOutOfWall(); if(CorpseDamageRules.allows(src,LootableBodies.completelyInvulnerable, LootableBodies.hurtByAttacks,LootableBodies.hurtByEnvironment,LootableBodies.hurtByMisc)) - super.actuallyHurt(src,amount); + super.actuallyHurt(world, src, amount); } @@ -390,11 +404,11 @@ public void setGameProfile(GameProfile gp){ public void refreshClientSkin() { GameProfile profile = gpSwap.get(); - if (getCommandSenderWorld().isClientSide && !LootableBodies.useLocalSkin && profile != null && !CorpseProfiles.isResolved(profile)) + if (level().isClientSide() && !LootableBodies.useLocalSkin && profile != null && !CorpseProfiles.isResolved(profile)) resolveProfile(profile); } private void resolveProfile(GameProfile initial) { - CorpseProfiles.resolve(initial,SkullBlockEntity::fetchGameProfile) + CorpseProfiles.resolve(initial,cyano.lootable.ClientProxy::fetchProfile) .thenAccept(resolved->gpSwap.compareAndSet(initial,resolved)); } public void setUserName(String name){ @@ -414,7 +428,7 @@ public static net.minecraft.world.entity.ai.attributes.AttributeSupplier.Builder } private java.util.List corpseAttributes() { java.util.List result=new java.util.ArrayList<>(); - for(ResourceLocation name:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKeys()) { + for(Identifier name:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getKeys()) { Holder key=net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getHolder(name).orElse(null); if(key!=null && getAttributes().hasAttribute(key))result.add(getAttribute(key)); } @@ -440,7 +454,7 @@ private AttributeInstance attributeNamed(String name) { case "forge:step_height_addition":name="minecraft:generic.step_height";break; default:break; } - net.minecraft.resources.ResourceLocation id=net.minecraft.resources.ResourceLocation.tryParse(name); + Identifier id=cyano.lootable.upgrade.LegacyRegistryMappings.canonicalAttributeId(Identifier.tryParse(name)); Holder key=id==null?null:net.minecraftforge.registries.ForgeRegistries.ATTRIBUTES.getHolder(id).orElse(null); return key!=null && getAttributes().hasAttribute(key)?getAttribute(key):null; } @@ -460,7 +474,7 @@ public boolean removeWhenFarAway(double distanceToClosestPlayer) { } public void jumpOutOfWall(){ - if(level().isClientSide) return; + if(level().isClientSide()) return; AABB bounds = getBoundingBox(); // Collision queries omit unloaded chunks. They cannot establish an exit // until the complete local terrain (including tall neighbour shapes) exists. @@ -498,7 +512,7 @@ private boolean tryWallExit(double x,double y,double z) { @Override - public void kill() { + public void kill(ServerLevel world) { if(terminate < 0) terminate = DEATH_COUNTDOWN; this.hurt(damageSources().fellOutOfWorld(), this.getMaxHealth()); this.setChanged(); @@ -517,12 +531,12 @@ public void playAmbientSound() { @Override - protected int getBaseExperienceReward() { + protected int getBaseExperienceReward(ServerLevel world) { return 0; } @Override - protected void dropExperience(net.minecraft.world.entity.Entity attacker) { + protected void dropExperience(ServerLevel world, net.minecraft.world.entity.Entity attacker) { // Enchantments can add experience even when the base reward is zero. } @@ -532,13 +546,8 @@ protected SoundEvent getAmbientSound() { return null; } - @Override - protected net.minecraft.resources.ResourceKey getDefaultLootTable() { - return net.minecraft.world.level.storage.loot.BuiltInLootTables.EMPTY; - } - @Override - protected void dropFromLootTable(DamageSource source,boolean recentlyHit) { + protected void dropFromLootTable(ServerLevel world, DamageSource source, boolean recentlyHit) { // Native Mob.getLootTable is final and can select a saved LootTable. // Corpse contents are released only by the inventory recovery path. } @@ -657,8 +666,8 @@ public int getMaxStackSize() { @Override public void setChanged() { Level level = level(); - if(level != null && !level.isClientSide && isAddedToWorld() && level.hasChunkAt(blockPosition())) { - level.getChunkAt(blockPosition()).setUnsaved(true); + if(level != null && !level.isClientSide() && isAddedToWorld() && level.hasChunkAt(blockPosition())) { + level.getChunkAt(blockPosition()).markUnsaved(); } } @@ -670,7 +679,7 @@ public void setChanged() { @Override public boolean stillValid(Player player) { return player != null && player.isAlive() && this.isAlive() - && terminate < 0 && player.getCommandSenderWorld() == getCommandSenderWorld() + && terminate < 0 && player.level() == level() && player.blockPosition().distSqr(this.blockPosition()) < 25; } @@ -687,11 +696,11 @@ public net.minecraft.world.InteractionResult mobInteract(Player player, Interact public String getGuiID(){return "lootablebodies:corpse";} @Override - public void startOpen(Player player) { + public void startOpen(ContainerUser user) { } @Override - public void stopOpen(Player player) { + public void stopOpen(ContainerUser user) { } /** @@ -743,18 +752,19 @@ public boolean isEmpty(){ } private boolean dropAllItems(){ + if (!(level() instanceof ServerLevel world)) return false; for(int slot = 0; slot < getContainerSize(); slot++){ ItemStack stack = getItem(slot); - if(!emptyStack(stack) && getCommandSenderWorld().addFreshEntity( - new ItemEntity(getCommandSenderWorld(),getX(),getY(),getZ(),stack.copy()))) { + if(!emptyStack(stack) && world.addFreshEntity( + new ItemEntity(level(),getX(),getY(),getZ(),stack.copy()))) { setItem(slot,null); } } Iterator iterator = auxInventory.iterator(); while(iterator.hasNext()) { ItemStack stack = iterator.next(); - if(emptyStack(stack) || getCommandSenderWorld().addFreshEntity( - new ItemEntity(getCommandSenderWorld(),getX(),getY(),getZ(),stack.copy()))) iterator.remove(); + if(emptyStack(stack) || world.addFreshEntity( + new ItemEntity(level(),getX(),getY(),getZ(),stack.copy()))) iterator.remove(); } setChanged(); return isEmpty(); @@ -764,62 +774,69 @@ private boolean dropAllItems(){ @Override - public void addAdditionalSaveData(final CompoundTag root) { - UpgradePreservation.clearTransient(root); + public void addAdditionalSaveData(ValueOutput output) { + output.discard("ActiveEffects"); + output.discard("active_effects"); + super.addAdditionalSaveData(output); + final CompoundTag root = new CompoundTag(); root.putInt(UpgradePreservation.VERSION,UpgradePreservation.DATA_VERSION); - if(sourceVersions.isEmpty()) root.remove(UpgradePreservation.SOURCES); + UpgradePreservation.clearTransient(output); + if(sourceVersions.isEmpty()) output.discard(UpgradePreservation.SOURCES); else root.put(UpgradePreservation.SOURCES,sourceVersions.copy()); - root.remove("ActiveEffects"); - root.remove("active_effects"); - super.addAdditionalSaveData(root); final ListTag equipmentListTag = new ListTag(); for (int i = 0; i < this.getContainerSize(); i++) { ItemStack item = this.getItem(i); if (!emptyStack(item)) { - CompoundTag slotTag = (CompoundTag)item.save(registryAccess()); + CompoundTag slotTag = encodeItem(item); slotTag.putByte("Slot", (byte)i); equipmentListTag.add(slotTag); } } root.put("Equipment", equipmentListTag); - root.remove("Aux"); + output.discard("Aux"); if(!this.auxInventory.isEmpty()){ final ListTag nbtauxtaglist = new ListTag(); Iterator iter = this.auxInventory.iterator(); while (iter.hasNext()) { ItemStack i = iter.next(); if(emptyStack(i)) continue; - nbtauxtaglist.add(i.save(registryAccess())); + nbtauxtaglist.add(encodeItem(i)); } root.put("Aux", nbtauxtaglist); } - root.remove(UNRESOLVED_ITEMS_TAG); + output.discard(UNRESOLVED_ITEMS_TAG); if(!unresolvedInventory.isEmpty()) { ListTag retained = new ListTag(); for(CompoundTag record : unresolvedInventory) retained.add(record.copy()); root.put(UNRESOLVED_ITEMS_TAG,retained); } - root.remove(INVALID_FIELDS_TAG); + output.discard(INVALID_FIELDS_TAG); if(!invalidFields.isEmpty()) { ListTag retained = new ListTag(); for(CompoundTag record : invalidFields) retained.add(record.copy()); root.put(INVALID_FIELDS_TAG,retained); } - root.remove("Vac"); + output.discard("Vac"); if(vacuumTime < VACUUM_TIMELIMIT)root.putByte("Vac", vacuumTime); - root.remove("Name"); + output.discard("Name"); String name = getCustomNameTag(); if(name != null && !name.trim().isEmpty()) root.putString("Name",name); - else root.remove("CustomName"); + else output.discard("CustomName"); root.putLong("DeathTime", this.getDeathTimestamp()); root.putLong("DecayTime",this.decayTimestamp); + NativeNbt.writeRawTags(output, root); } + private CompoundTag encodeItem(ItemStack stack) { + var ops = registryAccess().createSerializationContext(NbtOps.INSTANCE); + return (CompoundTag) ItemStack.CODEC.encodeStart(ops, stack).getOrThrow(); + } + @Override - public void load(final CompoundTag supplied) { - final CompoundTag root=UpgradePreservation.restore(supplied); + public void load(final ValueInput supplied) { + final CompoundTag root=UpgradePreservation.restore(NativeNbt.rawTags(supplied)); // Entity reads geometry before our inventory helper. Sanitize a copy // before it builds bounding boxes or derives previous-tick positions. CompoundTag clean = root.copy(); @@ -828,13 +845,23 @@ public void load(final CompoundTag supplied) { clean.put("Pos",readVector(root.get("Pos"),false,badPosition)); clean.put("Motion",readVector(root.get("Motion"),true,badMotion)); clean.put("Rotation",readRotation(root.get("Rotation"),badRotation)); - Float distance = finiteFloat(root.get("FallDistance")); - boolean badFall = root.contains("FallDistance") && (distance == null || distance < 0F); - clean.putFloat("FallDistance",distance != null && distance >= 0F ? distance : 0F); + boolean legacyFall = !root.contains("fall_distance"); + String fallKey = legacyFall ? "FallDistance" : "fall_distance"; + Double distance = finiteNumber(root.get(fallKey)); + boolean validFall = distance != null && distance >= 0D; + if (validFall && legacyFall) { + float oldDistance = distance.floatValue(); + validFall = Float.isFinite(oldDistance) && (distance == 0D || oldDistance > 0F); + if (validFall) distance = (double) oldDistance; + } + boolean badFall = root.contains(fallKey) && !validFall; + clean.putDouble("fall_distance", validFall ? distance : 0D); // Vanilla expects JSON here. Our attribution reader owns compatibility // and retains malformed input without sending it to vanilla's parser. clean.remove("CustomName"); - super.load(clean); + try (var problems = new ProblemReporter.ScopedCollector(com.mojang.logging.LogUtils.getLogger())) { + super.load(TagValueInput.create(problems, registryAccess(), clean)); + } String fallback=readName(root,"CustomName"),owner=readName(root,"Name"); setUserName(owner!=null?owner:fallback!=null?fallback:""); // A repeated load must not interpolate from obsolete head/body angles. @@ -844,7 +871,8 @@ public void load(final CompoundTag supplied) { if(hasInvalidComponent(badPosition) && root.contains("Pos")) retainInvalid("Pos",root.get("Pos")); if(hasInvalidComponent(badMotion) && root.contains("Motion")) retainInvalid("Motion",root.get("Motion")); if(hasInvalidComponent(badRotation) && root.contains("Rotation")) retainInvalid("Rotation",root.get("Rotation")); - if(badFall) retainInvalid("FallDistance",root.get("FallDistance")); + if(badFall) retainInvalid(fallKey, root.get(fallKey)); + if (!legacyFall && root.contains("FallDistance")) retainInvalid("FallDistance", root.get("FallDistance")); } private static Float finiteFloat(Tag raw) { @@ -856,7 +884,7 @@ private static Float finiteFloat(Tag raw) { private static ListTag readRotation(Tag raw, boolean[] invalid) { ListTag list = raw instanceof ListTag ? (ListTag)raw : null; - invalid[2] = list == null || list.size() != 2 || list.getElementType() < 1 || list.getElementType() > 6; + invalid[2] = list == null || list.size() != 2 || list.stream().anyMatch(tag -> !(tag instanceof NumericTag)); ListTag clean = new ListTag(); for(int axis = 0; axis < 2; axis++) { Float angle = list != null && axis < list.size() ? finiteFloat(list.get(axis)) : null; @@ -875,7 +903,7 @@ private static boolean hasInvalidComponent(boolean[] invalid) { private static ListTag readVector(Tag raw, boolean motion, boolean[] invalid) { ListTag list = raw instanceof ListTag ? (ListTag)raw : null; - invalid[3] = list == null || list.size() != 3 || list.getElementType() < 1 || list.getElementType() > 6; + invalid[3] = list == null || list.size() != 3 || list.stream().anyMatch(tag -> !(tag instanceof NumericTag)); ListTag clean = new ListTag(); for(int axis = 0; axis < 3; axis++) { Double value = list != null && axis < list.size() ? finiteNumber(list.get(axis)) : null; @@ -890,12 +918,12 @@ private static ListTag readVector(Tag raw, boolean motion, boolean[] invalid) { } @Override - public void readAdditionalSaveData(final CompoundTag supplied) { - final CompoundTag root=UpgradePreservation.restore(supplied); - sourceVersions=root.contains(UpgradePreservation.SOURCES,10) ? root.getCompound(UpgradePreservation.SOURCES).copy() : new CompoundTag(); + public void readAdditionalSaveData(final ValueInput supplied) { + final CompoundTag root=UpgradePreservation.restore(NativeNbt.rawTags(supplied)); + sourceVersions=hasType(root, UpgradePreservation.SOURCES, 10) ? root.getCompoundOrEmpty(UpgradePreservation.SOURCES).copy() : new CompoundTag(); // Persisted Equipment/Aux are already current after the first save. // Only the transient protected original and retained raw records use old versions. - inventoryVersions=root.getBoolean(UpgradePreservation.ITEMS_PENDING) ? sourceVersions : new CompoundTag(); + inventoryVersions=root.getBooleanOr(UpgradePreservation.ITEMS_PENDING, false) ? sourceVersions : new CompoundTag(); // Replace, rather than append to, an existing inventory on a repeated load. this.clearContent(); // These flags are not saved. A reused entity must behave like a fresh @@ -911,7 +939,7 @@ public void readAdditionalSaveData(final CompoundTag supplied) { Tag metadata = root.get(INVALID_FIELDS_TAG); if(isInventoryList(metadata)) { ListTag records = (ListTag)metadata; - for(int i = 0; i < records.size(); i++) invalidFields.add(records.getCompound(i).copy()); + for(int i = 0; i < records.size(); i++) invalidFields.add(records.getCompoundOrEmpty(i).copy()); } else if(metadata != null) retainInvalid("Recovery",metadata); // NBT replaces state. Clear the old map without effect callbacks: saved // attributes and absorption are restored below, just as on a fresh entity. @@ -931,7 +959,9 @@ public void readAdditionalSaveData(final CompoundTag supplied) { // Health must be checked against the loaded, validated effective maximum. vanilla.remove("Health"); vanilla.putFloat("AbsorptionAmount",readLivingFloat(root,"AbsorptionAmount",0F,Float.MAX_VALUE)); - super.readAdditionalSaveData(vanilla); + try (var problems = new ProblemReporter.ScopedCollector(com.mojang.logging.LogUtils.getLogger())) { + super.readAdditionalSaveData(TagValueInput.create(problems, registryAccess(), vanilla)); + } updateInvisibilityStatus(); for(AttributeInstance attribute : corpseAttributes()) { if(!Double.isFinite(attribute.getValue())) { @@ -953,19 +983,19 @@ public void readAdditionalSaveData(final CompoundTag supplied) { if(isInventoryList(recovery)) { ListTag records = (ListTag)recovery; for(int i = 0; i < records.size(); i++) { - CompoundTag record = records.getCompound(i); - String source = record.getString("Source"); - if((record.getAllKeys().size() == 2 || (record.getAllKeys().size() == 3 && record.contains("SourceVersions",10))) - && record.contains("Source",8) && record.contains("Data",10) + CompoundTag record = records.getCompoundOrEmpty(i); + String source = record.getStringOr("Source", ""); + if((record.keySet().size() == 2 || (record.keySet().size() == 3 && hasType(record, "SourceVersions", 10))) + && hasType(record, "Source", 8) && hasType(record, "Data", 10) && (source.equals("Equipment") || source.equals("Aux"))) { - readInventoryEntry(source,record.getCompound("Data"),record); + readInventoryEntry(source,record.getCompoundOrEmpty("Data"),record); } else unresolvedInventory.add(record.copy()); } } else if(recovery != null) retainRaw("Recovery",recovery); if(!unresolvedInventory.isEmpty()) ModLog.warning( "Lootable Bodies retained %s unreadable inventory records for later recovery",unresolvedInventory.size()); - long now = Math.max(0L,getCommandSenderWorld().getGameTime()); + long now = Math.max(0L,level().getGameTime()); this.vacuumTime = (byte)readTimer(root,"Vac",VACUUM_TIMELIMIT,VACUUM_TIMELIMIT); this.setDeathTimestamp(readTimer(root,"DeathTime",now,now)); this.decayTimestamp = readTimer(root,"DecayTime",now,now); @@ -1008,7 +1038,7 @@ private ListTag readEffects(Tag raw) { Holder effect=SavedEffectIdentity.resolve(record,id); boolean legacy=!record.contains("id"); boolean[] changed={legacy && record.contains("forge:id") - && (record.getString("forge:id").isEmpty() || id==null || id<1 || id>255)}; + && (record.getStringOr("forge:id", "").isEmpty() || id==null || id<1 || id>255)}; CompoundTag accepted=effect==null?null:effectDetails(record,legacy,changed,0); if(accepted==null || !seen.add(effect)) { retainInvalid("ActiveEffect",record); @@ -1028,7 +1058,7 @@ private static CompoundTag effectDetails(CompoundTag raw,boolean legacy,boolean[ String iconKey=legacy?"ShowIcon":"show_icon", hiddenKey=legacy?"HiddenEffect":"hidden_effect"; Long duration=raw.contains(durationKey)?integral(raw.get(durationKey)):Long.valueOf(0); Long amplifier=raw.contains(amplifierKey)?integral(raw.get(amplifierKey)):Long.valueOf(0); - if(legacy && amplifier!=null && raw.contains(amplifierKey,1))amplifier=Math.max(0L,amplifier); + if(legacy && amplifier!=null && hasType(raw, amplifierKey, 1))amplifier=Math.max(0L,amplifier); int minimumDuration=legacy?0:-1; int maximumAmplifier=legacy?127:255; if(duration==null || durationInteger.MAX_VALUE @@ -1041,14 +1071,14 @@ private static CompoundTag effectDetails(CompoundTag raw,boolean legacy,boolean[ result.putBoolean("show_particles",particles); result.putBoolean("show_icon",effectFlag(raw,iconKey,particles,changed)); if(raw.contains(hiddenKey)) { - CompoundTag hidden=raw.contains(hiddenKey,10)?effectDetails(raw.getCompound(hiddenKey),legacy,changed,depth+1):null; + CompoundTag hidden=hasType(raw, hiddenKey, 10)?effectDetails(raw.getCompoundOrEmpty(hiddenKey),legacy,changed,depth+1):null; if(hidden==null)return null; result.put("hidden_effect",hidden); } // The Forge 50 effect codec has no CurativeItems field. Retain that // legacy payload with its original effect record for recovery. if(!Set.of("id","Id","forge:id",durationKey,amplifierKey,ambientKey,particlesKey,iconKey,hiddenKey) - .containsAll(raw.getAllKeys()))changed[0]=true; + .containsAll(raw.keySet()))changed[0]=true; return result; } @@ -1062,7 +1092,7 @@ private static boolean effectFlag(CompoundTag raw,String key,boolean fallback,bo private static Double finiteNumber(Tag raw) { if(!(raw instanceof NumericTag) || raw.getId() < 1 || raw.getId() > 6) return null; - double value = ((NumericTag)raw).getAsDouble(); + double value = ((NumericTag)raw).doubleValue(); return Double.isFinite(value) ? value : null; } @@ -1086,14 +1116,14 @@ private ListTag readAttributes(Tag raw,boolean legacy) { String nameKey=legacy?"Name":"id", baseKey=legacy?"Base":"base", modifiersKey=legacy?"Modifiers":"modifiers"; ListTag list = (ListTag)raw; for(int i = 0; i < list.size(); i++) { - CompoundTag record = list.getCompound(i); - AttributeInstance attribute = record.contains(nameKey,8) - ? attributeNamed(record.getString(nameKey)) : null; + CompoundTag record = list.getCompoundOrEmpty(i); + AttributeInstance attribute = hasType(record, nameKey, 8) + ? attributeNamed(record.getStringOr(nameKey, "")) : null; Double base = finiteNumber(record.get(baseKey)); if(attribute == null || base == null) { retainInvalid("Attribute",record); continue; } - if(legacy && record.getString(nameKey).equals("forge:step_height_addition")) { + if(legacy && record.getStringOr(nameKey, "").equals("forge:step_height_addition")) { Tag modifiers=record.get(modifiersKey); - if(base < -512D || base > 512D || !Set.of(nameKey,baseKey,modifiersKey).containsAll(record.getAllKeys()) + if(base < -512D || base > 512D || !Set.of(nameKey,baseKey,modifiersKey).containsAll(record.keySet()) || (modifiers!=null && (!(modifiers instanceof ListTag listValue) || !listValue.isEmpty()))) { retainInvalid("Attribute",record);continue; } @@ -1115,7 +1145,7 @@ private ListTag readAttributes(Tag raw,boolean legacy) { && !configuredHealth && !legacyNameDistance && !base.equals(defaultAttributeBases.get(name))) || !seen.add(name)) { retainInvalid("Attribute",record); continue; } - boolean changed = !Set.of(nameKey,baseKey,modifiersKey).containsAll(record.getAllKeys()); + boolean changed = !Set.of(nameKey,baseKey,modifiersKey).containsAll(record.keySet()); CompoundTag accepted = new CompoundTag(); accepted.putString("id",name); accepted.putDouble("base",base); Tag rawModifiers = record.get(modifiersKey); @@ -1123,13 +1153,13 @@ private ListTag readAttributes(Tag raw,boolean legacy) { ListTag modifiers = new ListTag(); if(!isInventoryList(rawModifiers)) changed = true; else { - Set ids = new HashSet<>(); + Set ids = new HashSet<>(); ListTag entries = (ListTag)rawModifiers; for(int m = 0; m < entries.size(); m++) { - CompoundTag entry = entries.getCompound(m); - String operation=legacy?legacyOperation(entry):entry.getString("operation"); - ResourceLocation id=legacy?legacyModifier(entry): - entry.contains("id",8)?ResourceLocation.tryParse(entry.getString("id")):null; + CompoundTag entry = entries.getCompoundOrEmpty(m); + String operation=legacy?legacyOperation(entry):entry.getStringOr("operation", ""); + Identifier id=legacy?legacyModifier(entry): + hasType(entry, "id", 8)?Identifier.tryParse(entry.getStringOr("id", "")):null; Double amount = finiteNumber(entry.get(legacy?"Amount":"amount")); if(operation==null || !Set.of("add_value","add_multiplied_base","add_multiplied_total").contains(operation) || id == null || id.getPath().isEmpty() || amount == null) { @@ -1144,7 +1174,7 @@ private ListTag readAttributes(Tag raw,boolean legacy) { ids.add(id); modifiers.add(modifier); Set fields=legacy?Set.of("Name","Amount","Operation","UUIDMost","UUIDLeast","UUID"): Set.of("id","amount","operation"); - if(!fields.containsAll(entry.getAllKeys())) changed = true; + if(!fields.containsAll(entry.keySet())) changed = true; } } accepted.put("modifiers",modifiers); @@ -1155,10 +1185,10 @@ private ListTag readAttributes(Tag raw,boolean legacy) { return clean; } - private static ResourceLocation legacyModifier(CompoundTag entry) { + private static Identifier legacyModifier(CompoundTag entry) { UUID uuid=attributeUuid(entry); - String name=entry.getString("Name"); - return uuid==null || !entry.contains("Name",8) || name.isEmpty()?null:ItemMigration.legacyModifierId(uuid,name); + String name=entry.getStringOr("Name", ""); + return uuid==null || !hasType(entry, "Name", 8) || name.isEmpty()?null:ItemMigration.legacyModifierId(uuid,name); } private static String legacyOperation(CompoundTag entry) { @@ -1174,8 +1204,8 @@ private static String legacyOperation(CompoundTag entry) { private static UUID attributeUuid(CompoundTag entry) { UUID current=null,legacy=null; if(entry.contains("UUID")) { - if(!entry.hasUUID("UUID"))return null; - current=entry.getUUID("UUID"); + if(!hasUuid(entry, "UUID"))return null; + current=readUuid(entry, "UUID"); } if(entry.contains("UUIDMost") || entry.contains("UUIDLeast")) { Long most=integral(entry.get("UUIDMost")),least=integral(entry.get("UUIDLeast")); @@ -1187,19 +1217,19 @@ private static UUID attributeUuid(CompoundTag entry) { private static boolean finiteAttributeValue(double base, ListTag modifiers) { double value = base; for(int i = 0; i < modifiers.size(); i++) { - CompoundTag m = modifiers.getCompound(i); - if(m.getString("operation").equals("add_value")) value += m.getDouble("amount"); + CompoundTag m = modifiers.getCompoundOrEmpty(i); + if(m.getStringOr("operation", "").equals("add_value")) value += m.getDoubleOr("amount", 0D); } if(!Double.isFinite(value)) return false; double additive = value; for(int i = 0; i < modifiers.size(); i++) { - CompoundTag m = modifiers.getCompound(i); - if(m.getString("operation").equals("add_multiplied_base")) value += additive * m.getDouble("amount"); + CompoundTag m = modifiers.getCompoundOrEmpty(i); + if(m.getStringOr("operation", "").equals("add_multiplied_base")) value += additive * m.getDoubleOr("amount", 0D); } if(!Double.isFinite(value)) return false; for(int i = 0; i < modifiers.size(); i++) { - CompoundTag m = modifiers.getCompound(i); - if(m.getString("operation").equals("add_multiplied_total")) value *= 1D + m.getDouble("amount"); + CompoundTag m = modifiers.getCompoundOrEmpty(i); + if(m.getStringOr("operation", "").equals("add_multiplied_total")) value *= 1D + m.getDoubleOr("amount", 0D); } return Double.isFinite(value); } @@ -1220,27 +1250,44 @@ private long checkedTimestamp(String key, long timestamp, long now) { return now; } - private String readName(CompoundTag root,String key) { - Tag raw=root.get(key);if(raw==null)return null; - if(raw.getId()==8) { - String name=root.getString(key); - if(key.equals("CustomName") && root.getInt(UpgradePreservation.VERSION)>=3) { - try { - net.minecraft.network.chat.Component text=net.minecraft.network.chat.Component.Serializer.fromJson(name,registryAccess()); - if(text==null){retainInvalid(key,raw);return null;}name=text.getString(); - }catch(RuntimeException malformed){retainInvalid(key,raw);return null;} + private String readName(CompoundTag root, String key) { + Tag raw = root.get(key); + if (raw == null) return null; + String name; + try { + if (key.equals("CustomName")) { + var json = registryAccess().createSerializationContext(com.mojang.serialization.JsonOps.INSTANCE); + 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; + // 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() + && !saved.isEmpty() && (saved.startsWith("\"") || saved.startsWith("{") || saved.startsWith("[")); + if (raw.getId() == Tag.TAG_STRING && (legacyJson || stagedJson)) { + name = ComponentSerialization.CODEC.parse(json, com.google.gson.JsonParser.parseString(saved)) + .getOrThrow().getString(); + } else { + name = ComponentSerialization.CODEC.parse(nbt, raw).getOrThrow().getString(); + } + } else { + name = raw.asString().orElseThrow(); } - String packet=net.minecraft.network.chat.Component.Serializer.toJson(net.minecraft.network.chat.Component.literal(name),registryAccess()); - if(name.length()<=32767 && name.getBytes(StandardCharsets.UTF_8).length<=32767 - && packet.getBytes(StandardCharsets.UTF_8).length<=32767)return name.trim().isEmpty()?"":name; + String packet = ComponentSerialization.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, + Component.literal(name)).getOrThrow().toString(); + if (name.length() <= 32767 && name.getBytes(StandardCharsets.UTF_8).length <= 32767 + && packet.getBytes(StandardCharsets.UTF_8).length <= 32767) return name.trim().isEmpty() ? "" : name; + } catch (RuntimeException malformed) { + // Keep original raw attribution for recovery. } - retainInvalid(key,raw);return null; + retainInvalid(key, raw); + return null; } - private static boolean isInventoryList(Tag data) { - return data instanceof ListTag && (((ListTag)data).size() == 0 || ((ListTag)data).getElementType() == 10); + return isListOf(data, 10); } private void retainRaw(String source, Tag data) { @@ -1255,21 +1302,21 @@ private void readInventoryTag(String source, Tag data) { if(data == null) return; if(!isInventoryList(data)) { retainRaw(source + "List",data); return; } ListTag list = (ListTag)data; - for(int i = 0; i < list.size(); i++) readInventoryEntry(source,list.getCompound(i),null); + for(int i = 0; i < list.size(); i++) readInventoryEntry(source,list.getCompoundOrEmpty(i),null); } private static Long integral(Tag data) { - return data instanceof NumericTag && data.getId() >= 1 && data.getId() <= 4 ? ((NumericTag)data).getAsLong() : null; + return data instanceof NumericTag && data.getId() >= 1 && data.getId() <= 4 ? ((NumericTag)data).longValue() : null; } private ItemStack decodeInventoryItem(CompoundTag raw) { // Only restore a component stack that the live registry can round-trip. // Unsupported components, legacy capability data and malformed counts // remain byte-for-byte in the existing Source/Data recovery records. - if(!Set.of("id","count","components","Slot").containsAll(raw.getAllKeys()) - || !raw.contains("id",8) || (raw.contains("components") && !raw.contains("components",10)))return null; + if(!Set.of("id","count","components","Slot").containsAll(raw.keySet()) + || !hasType(raw, "id", 8) || (raw.contains("components") && !hasType(raw, "components", 10)))return null; Long count=raw.contains("count")?integral(raw.get("count")):Long.valueOf(1); - ResourceLocation id=ResourceLocation.tryParse(raw.getString("id")); + Identifier id=Identifier.tryParse(raw.getStringOr("id", "")); if(id==null || count==null || count<=0 || count>127)return null; try { CompoundTag input=raw.copy(); @@ -1285,10 +1332,10 @@ private ItemStack decodeInventoryItem(CompoundTag raw) { // accessor silently clamps. Keep that original stack recoverable. Integer damage=stack.get(net.minecraft.core.component.DataComponents.DAMAGE); if(damage!=null && damage!=stack.getDamageValue())return null; - CompoundTag encoded=(CompoundTag)stack.save(registryAccess()); - if(!id.toString().equals(encoded.getString("id")) - || encoded.getInt("count")!=1 - || !ItemMigration.sameSavedComponents(input.getCompound("components"),encoded.getCompound("components")))return null; + CompoundTag encoded=encodeItem(stack); + if(!id.toString().equals(encoded.getStringOr("id", "")) + || encoded.getIntOr("count", 0)!=1 + || !ItemMigration.sameSavedComponents(input.getCompoundOrEmpty("components"),encoded.getCompoundOrEmpty("components")))return null; stack.setCount(count.intValue()); return stack; } catch(RuntimeException unreadable) { @@ -1297,7 +1344,7 @@ private ItemStack decodeInventoryItem(CompoundTag raw) { } private void readInventoryEntry(String source, CompoundTag raw, CompoundTag retained) { - CompoundTag context=retained!=null && retained.contains("SourceVersions",10) ? retained.getCompound("SourceVersions") : inventoryVersions; + CompoundTag context=retained!=null && hasType(retained, "SourceVersions", 10) ? retained.getCompoundOrEmpty("SourceVersions") : inventoryVersions; CompoundTag converted=ItemMigration.convert(raw,context); ItemStack stack = converted==null ? null : decodeInventoryItem(converted); if(emptyStack(stack)) { diff --git a/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java b/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java index 86cdc81..7c74f27 100644 --- a/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java +++ b/src/main/java/cyano/lootable/entities/SavedEffectIdentity.java @@ -1,8 +1,10 @@ package cyano.lootable.entities; +import static cyano.lootable.upgrade.NativeNbt.hasType; + import net.minecraft.core.Holder; import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.world.effect.MobEffect; import net.minecraftforge.registries.ForgeRegistries; @@ -25,18 +27,18 @@ static Holder resolve(CompoundTag record,Long legacyId) { return named(record,"id"); } if(record.contains("forge:id")) { - if(!record.contains("forge:id",8))return null; - if(!record.getString("forge:id").isEmpty())return named(record,"forge:id"); + if(!hasType(record, "forge:id", 8))return null; + if(!record.getStringOr("forge:id", "").isEmpty())return named(record,"forge:id"); } if(legacyId==null || legacyId<=0 || legacyId>=LEGACY_NAMES.length)return null; - return holder(ResourceLocation.withDefaultNamespace(LEGACY_NAMES[legacyId.intValue()])); + return holder(Identifier.withDefaultNamespace(LEGACY_NAMES[legacyId.intValue()])); } private static Holder named(CompoundTag record,String field) { - return record.contains(field,8)?holder(ResourceLocation.tryParse(record.getString(field))):null; + return hasType(record, field, 8)?holder(Identifier.tryParse(record.getStringOr(field, ""))):null; } - private static Holder holder(ResourceLocation id) { + private static Holder holder(Identifier id) { return id==null?null:ForgeRegistries.MOB_EFFECTS.getHolder(id).orElse(null); } } diff --git a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java index 101914a..d2ef52d 100644 --- a/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java +++ b/src/main/java/cyano/lootable/events/PlayerDeathEventHandler.java @@ -11,14 +11,12 @@ import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.item.ItemStack; import net.minecraft.world.entity.HumanoidArm; -import net.minecraft.world.level.Level; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraftforge.eventbus.api.listener.Priority; import net.minecraftforge.event.entity.living.LivingDeathEvent; -import net.minecraftforge.event.entity.EntityEvent; import net.minecraftforge.event.entity.living.LivingDropsEvent; import cyano.lootable.ModLog; -import net.minecraftforge.eventbus.api.Event; -import net.minecraftforge.eventbus.api.EventPriority; -import net.minecraftforge.eventbus.api.SubscribeEvent; import java.util.ArrayList; import java.util.IdentityHashMap; @@ -33,14 +31,26 @@ public class PlayerDeathEventHandler { private static final Map> equipmentCache = new WeakHashMap<>(); - @SubscribeEvent(priority= EventPriority.LOW, receiveCanceled = true) + public Runnable register() { + var death = LivingDeathEvent.BUS.addListener(Priority.LOW, this::entityDeathEvent); + var drops = LivingDropsEvent.BUS.addListener(Priority.LOWEST, this::entityDropEvent); + var deathCleanup = LivingDeathEvent.BUS.addListener((event, cancelled) -> { + if (cancelled) equipmentCache.remove(event.getEntity()); + }); + var dropCleanup = LivingDropsEvent.BUS.addListener((event, cancelled) -> { + if (cancelled) equipmentCache.remove(event.getEntity()); + }); + return () -> { + LivingDeathEvent.BUS.removeListener(deathCleanup); + LivingDeathEvent.BUS.removeListener(death); + LivingDropsEvent.BUS.removeListener(drops); + LivingDropsEvent.BUS.removeListener(dropCleanup); + }; + } + public void entityDeathEvent(LivingDeathEvent e){ - if(e.getEntity() instanceof Player - && !e.getEntity().getCommandSenderWorld().isClientSide) { - final Player player = (Player)e.getEntity(); - equipmentCache.remove(player); - if(e.isCanceled() || e.getResult() == Event.Result.DENY || player.isSpectator() - || player.getCommandSenderWorld().getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY)) return; + if (e.getEntity() instanceof Player player && player.level() instanceof ServerLevel world) { + if (player.isSpectator() || world.getGameRules().get(GameRules.KEEP_INVENTORY)) return; Map cache = new IdentityHashMap<>(); for(EquipmentSlot slot : EntityLootableBody.EQUIPMENT_SLOTS){ ItemStack stack = player.getItemBySlot(slot); @@ -56,19 +66,14 @@ public void entityDeathEvent(LivingDeathEvent e){ } } - @SubscribeEvent(priority= EventPriority.LOWEST, receiveCanceled = true) public void entityDropEvent(LivingDropsEvent e){ - if(e.getEntity() instanceof Player - && !e.getEntity().getCommandSenderWorld().isClientSide) { - final Player player = (Player)e.getEntity(); + if (e.getEntity() instanceof Player player && player.level() instanceof ServerLevel world) { Map cache = equipmentCache.remove(player); - if(e.isCanceled() || e.getResult() == Event.Result.DENY || player.isSpectator() - || player.getCommandSenderWorld().getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_KEEPINVENTORY)) return; + if (player.isSpectator() || world.getGameRules().get(GameRules.KEEP_INVENTORY)) return; if(cache == null) cache = Collections.emptyMap(); - final Level w = player.getCommandSenderWorld(); EntityLootableBody corpse = new EntityLootableBody(player); - corpse.setUserName(player.getGameProfile().getName()); + corpse.setUserName(player.getGameProfile().name()); corpse.setRotation(player.getYRot()); List items = new ArrayList<>(); @@ -87,11 +92,11 @@ public void entityDropEvent(LivingDropsEvent e){ corpse.initializeItems(items.toArray(new ItemStack[0])); if(LootableBodies.addBonesToCorpse){ - corpse.addItem(new ItemStack(Items.BONE,1+w.random.nextInt(3))); - corpse.addItem(new ItemStack(Items.ROTTEN_FLESH,1+w.random.nextInt(3))); + corpse.addItem(new ItemStack(Items.BONE,1+world.random.nextInt(3))); + corpse.addItem(new ItemStack(Items.ROTTEN_FLESH,1+world.random.nextInt(3))); } - if(w.addFreshEntity(corpse)) { + if(world.addFreshEntity(corpse)) { e.getDrops().clear(); } else { ModLog.warning("%s: Corpse spawn rejected; original death drops retained", LootableBodies.MODID); diff --git a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java index 33c9f21..0b5b455 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java +++ b/src/main/java/cyano/lootable/graphics/CorpseConfigGui.java @@ -27,7 +27,7 @@ public final class CorpseConfigGui extends Screen { private static List keys(){List result=new ArrayList<>(Arrays.asList(APPEARANCE));result.addAll(Arrays.asList(RULES));return result;} private static Property property(String key){for(String category:new String[]{"options","corpse damage","corpse decay","corpse recovery"}){Property value=LootableBodies.getConfiguration().getCategory(category).get(key);if(value!=null)return value;}throw new IllegalArgumentException(key);} private Button button(int id,int x,int y,int width,String label,Runnable click){return addRenderableWidget(new SettingButton(id,x,y,width,label,click));} - public static final class SettingButton extends Button { + public static final class SettingButton extends Button.Plain { public final int id; SettingButton(int id,int x,int y,int width,String label,Runnable action){super(x,y,width,20,net.minecraft.network.chat.Component.literal(label),button->action.run(),DEFAULT_NARRATION);this.id=id;} } @@ -41,12 +41,12 @@ public static final class SettingButton extends Button { int start=page*perPage,end=Math.min(keys.length,start+perPage); for(int i=start;i{capture();pending.put(key,Boolean.toString(!Boolean.parseBoolean(pending.get(key))));init(minecraft,width,height);}); + if(value.getType()=='B')button(20+i,width/2+70,y,85,booleanLabel(key),()->{capture();pending.put(key,Boolean.toString(!Boolean.parseBoolean(pending.get(key))));rebuildWidgets();}); else {EditBox field=new EditBox(font,width/2+60,y+1,95,18,net.minecraft.network.chat.Component.translatable(property(key).getLanguageKey()));field.setMaxLength(64);field.setValue(pending.get(key));fields.put(key,field);addWidget(field);} } if(page>0)button(3,left,height-56,80,"<",()->changePage(-1)); if(end",()->changePage(1)); - button(5,width/2-65,height-56,130,I18n.get("controls.reset"),()->{for(String key:keys)pending.put(key,property(key).getDefault());init(minecraft,width,height);}); + button(5,width/2-65,height-56,130,I18n.get("controls.reset"),()->{for(String key:keys)pending.put(key,property(key).getDefault());rebuildWidgets();}); } button(200,width/2-155,height-28,150,I18n.get("gui.done"),()->{ capture();if(!validPending()){invalid=true;return;} @@ -55,7 +55,7 @@ public static final class SettingButton extends Button { button(201,width/2+5,height-28,150,I18n.get("gui.cancel"),()->minecraft.setScreen(parent)); } private void capture(){for(Map.Entry entry:fields.entrySet())pending.put(entry.getKey(),entry.getValue().getValue());} - private void changePage(int change){capture();page+=change;init(minecraft,width,height);} + private void changePage(int change){capture();page+=change;rebuildWidgets();} private String booleanLabel(String key){return I18n.get(Boolean.parseBoolean(pending.get(key))?"options.on":"options.off");} private boolean validPending(){ for(String key:keys()){ @@ -66,17 +66,17 @@ private boolean validPending(){ } @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,0xffffff); - graphics.drawCenteredString(font,I18n.get(group==null?"lootablebodies.config.local_file":"lootablebodies.config."+group),width/2,28,0xcccccc); + 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); 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,0xffffff); + String text=I18n.get(property(row.getKey()).getLanguageKey());graphics.drawString(font,font.plainSubstrByWidth(text,205),width/2-155,row.getValue()+6,0xffffffff); } for(EditBox field:fields.values())field.render(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); - if(invalid)graphics.drawCenteredString(font,I18n.get("lootablebodies.config.invalid"),width/2,height-72,0xff5555); + 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); } @Override public void onClose(){capture();minecraft.setScreen(parent);} public static boolean isValidDecayTime(String value) { diff --git a/src/main/java/cyano/lootable/graphics/CorpseContainer.java b/src/main/java/cyano/lootable/graphics/CorpseContainer.java index 13df3ef..821b664 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseContainer.java @@ -12,11 +12,9 @@ import net.minecraft.world.Container; import net.minecraft.world.inventory.Slot; import net.minecraft.world.inventory.ClickType; -import net.minecraft.world.item.ArmorItem; import net.minecraft.world.item.ItemStack; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.item.ItemTossEvent; /** @@ -186,9 +184,9 @@ private void storeInPlayerInventory(Inventory inventory, ItemStack remainder) { while(remainder.getCount() > 0) { int destination = -1; if(!remainder.isDamaged()) { - if(canStore(inventory,inventory.selected,remainder)) destination = inventory.selected; + if(canStore(inventory,inventory.getSelectedSlot(),remainder)) destination = inventory.getSelectedSlot(); else if(canStore(inventory,40,remainder)) destination = 40; - else for(int i = 0; i < inventory.items.size(); i++) { + else for(int i = 0; i < inventory.getNonEquipmentItems().size(); i++) { if(canStore(inventory,i,remainder)) { destination = i; break; } } } @@ -218,7 +216,7 @@ private boolean canStore(Inventory inventory, int slot, ItemStack incoming) { private boolean tryRelease(Player player, ItemStack stack, boolean includeName) { if(emptyStack(stack) || stack.getCount() <= 0) return false; - if(player.level().isClientSide) { + if (!(player.level() instanceof net.minecraft.server.level.ServerLevel world)) { player.drop(stack.copy(),includeName); return true; } @@ -234,12 +232,12 @@ private boolean tryRelease(Player player, ItemStack stack, boolean includeName) } if(dropped == null) return false; ItemTossEvent event = new ItemTossEvent(dropped,player); - if(MinecraftForge.EVENT_BUS.post(event)) return false; + if(ItemTossEvent.BUS.post(event)) return false; if(capturing != null) { capturing.add(event.getEntity()); return true; } - return player.level().addFreshEntity(event.getEntity()); + return world.addFreshEntity(event.getEntity()); } @Override diff --git a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java index a4d1b6b..26c5521 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseGUIContainer.java @@ -3,10 +3,11 @@ import cyano.lootable.LootableBodies; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.Container; -import net.minecraft.resources.ResourceLocation; +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.renderer.RenderPipelines; import net.minecraft.network.chat.Component; /** @@ -15,7 +16,7 @@ @OnlyIn(Dist.CLIENT) public class CorpseGUIContainer extends net.minecraft.client.gui.screens.inventory.AbstractContainerScreen{ - private final ResourceLocation image = ResourceLocation.parse(LootableBodies.MODID+":textures/gui/corpse.png"); + private final Identifier image = Identifier.parse(LootableBodies.MODID+":textures/gui/corpse.png"); public CorpseGUIContainer(Inventory playerItems, Container entity) { this(new CorpseContainer(playerItems,entity),playerItems,entity instanceof net.minecraft.world.entity.Entity?((net.minecraft.world.entity.Entity)entity).getDisplayName():Component.literal(LootableBodies.NAME)); @@ -38,7 +39,7 @@ public CorpseGUIContainer(CorpseContainer container, Inventory playerItems, Comp protected void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) { int x = (width - imageWidth) / 2; int y = (height - imageHeight) / 2; - graphics.blit(image, x, y, 0, 0, imageWidth, imageHeight); + graphics.blit(RenderPipelines.GUI_TEXTURED, image, x, y, 0, 0, imageWidth, imageHeight, 256, 256); } @Override diff --git a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java index 0dd9853..07b9f57 100644 --- a/src/main/java/cyano/lootable/graphics/CorpseRenderer.java +++ b/src/main/java/cyano/lootable/graphics/CorpseRenderer.java @@ -2,116 +2,115 @@ import com.mojang.authlib.GameProfile; import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; import cyano.lootable.LootableBodies; import cyano.lootable.entities.EntityLootableBody; +import cyano.lootable.network.CorpseProfiles; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.MultiBufferSource; -import com.mojang.math.Axis; -import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.model.geom.ModelLayers; -import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.player.PlayerModel; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.ArmorModelSet; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.HumanoidMobRenderer; import net.minecraft.client.renderer.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.layers.ArrowLayer; -import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; import net.minecraft.client.renderer.entity.layers.CustomHeadLayer; +import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer; import net.minecraft.client.renderer.entity.layers.ItemInHandLayer; -import net.minecraft.client.model.HumanoidModel; -import net.minecraft.client.model.PlayerModel; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.state.CameraRenderState; import net.minecraft.client.resources.DefaultPlayerSkin; -import net.minecraft.client.resources.PlayerSkin; -import net.minecraft.resources.ResourceLocation; +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 ResourceLocation localSkin = ResourceLocation.fromNamespaceAndPath(LootableBodies.MODID, +public class CorpseRenderer extends LivingEntityRenderer { + private static final Identifier LOCAL_SKIN = Identifier.fromNamespaceAndPath(LootableBodies.MODID, "textures/entity/corpse/corpse.png"); - protected final PlayerModel thickArmsModel; - protected final PlayerModel thinArmsModel; + + private final PlayerModel thickArmsModel; + private final PlayerModel thinArmsModel; public CorpseRenderer(EntityRendererProvider.Context context) { - super(context, new FrozenPlayerModel(context.bakeLayer(ModelLayers.PLAYER_SLIM),true), 0.5F); + super(context, new PlayerModel(context.bakeLayer(ModelLayers.PLAYER_SLIM), true), 0.5F); thinArmsModel = model; - thickArmsModel = new FrozenPlayerModel(context.bakeLayer(ModelLayers.PLAYER),false); - addLayer(new HumanoidArmorLayer<>(this, - new FrozenArmorModel(context.bakeLayer(ModelLayers.PLAYER_INNER_ARMOR)), - new FrozenArmorModel(context.bakeLayer(ModelLayers.PLAYER_OUTER_ARMOR)),context.getModelManager())); - addLayer(new ItemInHandLayer<>(this,context.getItemInHandRenderer())); - addLayer(new ArrowLayer<>(context,this)); - addLayer(new CustomHeadLayer<>(this,context.getModelSet(),context.getItemInHandRenderer())); + thickArmsModel = new PlayerModel(context.bakeLayer(ModelLayers.PLAYER), false); + addLayer(new HumanoidArmorLayer<>(this, ArmorModelSet.bake(ModelLayers.PLAYER_ARMOR, + context.getModelSet(), part -> new PlayerModel(part, false)), context.getEquipmentRenderer())); + addLayer(new ItemInHandLayer<>(this)); + addLayer(new ArrowLayer<>(this, context)); + addLayer(new CustomHeadLayer<>(this, context.getModelSet(), context.getPlayerSkinRenderCache())); + } + + @Override + public AvatarRenderState createRenderState() { + return new AvatarRenderState(); } - public PlayerModel getMainModel() { return getModel(); } - public void setModel(boolean thinArms) { model = thinArms ? thinArmsModel : thickArmsModel; } + @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()); + state.id = body.getId(); + state.arrowCount = body.getArrowCount(); + state.bodyRot = body.getRotation(); + state.xRot = 0F; + state.yRot = 0F; + state.walkAnimationPos = 0F; + state.walkAnimationSpeed = 0F; + state.ageInTicks = 0F; + } - @Override public ResourceLocation getTextureLocation(EntityLootableBody entity) { - if (LootableBodies.useLocalSkin) return localSkin; - return getSkin(entity.getGameProfile()); + @Override + public Identifier getTextureLocation(AvatarRenderState state) { + return LootableBodies.useLocalSkin ? LOCAL_SKIN : state.skin.body().texturePath(); } - public static ResourceLocation getSkin(GameProfile profile) { - PlayerSkin skin=playerSkin(profile); - return skin==null?DefaultPlayerSkin.getDefaultTexture():skin.texture(); + public static Identifier getSkin(GameProfile profile) { + return playerSkin(profile).body().texturePath(); } private static PlayerSkin playerSkin(GameProfile profile) { - if (!cyano.lootable.network.CorpseProfiles.isResolved(profile)) return null; + if (!CorpseProfiles.isResolved(profile)) return DefaultPlayerSkin.getDefaultSkin(); try { - return Minecraft.getInstance().getSkinManager().getInsecureSkin(profile); + return Minecraft.getInstance().getSkinManager().createLookup(profile, false).get(); } catch (RuntimeException unavailable) { return DefaultPlayerSkin.get(profile); } } - @Override public void render(EntityLootableBody entity, float yaw, float partialTicks, - PoseStack matrices, MultiBufferSource buffers, int light) { - PlayerSkin skin=LootableBodies.useLocalSkin?null:playerSkin(entity.getGameProfile()); - setModel(skin!=null && skin.model()==PlayerSkin.Model.SLIM); + @Override + public void submit(AvatarRenderState state, PoseStack matrices, SubmitNodeCollector collector, CameraRenderState camera) { + model = !LootableBodies.useLocalSkin && state.skin.model() == PlayerModelType.SLIM ? thinArmsModel : thickArmsModel; try { - super.render(entity, 0F, partialTicks, matrices, buffers, light); + super.submit(state, matrices, collector, camera); } finally { - setModel(true); + model = thinArmsModel; } } - @Override protected void setupRotations(EntityLootableBody entity, PoseStack matrices, - float age, float yaw, float partialTicks, float scale) { + @Override + protected void setupRotations(AvatarRenderState state, PoseStack matrices, float bodyYaw, float scale) { matrices.mulPose(Axis.XP.rotationDegrees(90F)); - matrices.mulPose(Axis.ZP.rotationDegrees(entity.getRotation())); + matrices.mulPose(Axis.ZP.rotationDegrees(state.bodyRot)); matrices.translate(0D, -0.85D, -0.125D); - super.setupRotations(entity, matrices, 0F, 0F, partialTicks, scale); + super.setupRotations(state, matrices, 0F, scale); } - @Override protected float getAttackAnim(EntityLootableBody entity, float partialTicks) { return 0F; } - @Override protected float getBob(EntityLootableBody entity, float partialTicks) { return 0F; } - @Override protected boolean shouldShowName(EntityLootableBody entity) { - return LootableBodies.displayNameTag && super.shouldShowName(entity); - } - @Override protected void renderNameTag(EntityLootableBody entity, net.minecraft.network.chat.Component name, PoseStack matrices, - MultiBufferSource buffers, int light, float partialTicks) { - if (LootableBodies.displayNameTag) super.renderNameTag(entity, name, matrices, buffers, light, partialTicks); + @Override + protected boolean shouldShowName(EntityLootableBody body, double distanceSquared) { + return LootableBodies.displayNameTag && super.shouldShowName(body, distanceSquared); } - private static final class FrozenPlayerModel extends PlayerModel { - FrozenPlayerModel(ModelPart root,boolean thin) { super(root, thin); } - @Override public void prepareMobModel(EntityLootableBody body, float swing, float amount, float partial) { - super.prepareMobModel(body, 0F, 0F, 0F); - } - @Override public void setupAnim(EntityLootableBody body, float swing, float amount, - float age, float yaw, float pitch) { - super.setupAnim(body, 0F, 0F, 0F, 0F, 0F); - } - } - private static final class FrozenArmorModel extends HumanoidModel { - FrozenArmorModel(ModelPart root) { super(root); } - @Override public void prepareMobModel(EntityLootableBody body, float swing, float amount, float partial) { - super.prepareMobModel(body, 0F, 0F, 0F); - } - @Override public void setupAnim(EntityLootableBody body, float swing, float amount, - float age, float yaw, float pitch) { - super.setupAnim(body, 0F, 0F, 0F, 0F, 0F); - } + @Override + protected void submitNameTag(AvatarRenderState state, PoseStack matrices, + SubmitNodeCollector collector, CameraRenderState camera) { + if (LootableBodies.displayNameTag) super.submitNameTag(state, matrices, collector, camera); } } diff --git a/src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java b/src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java new file mode 100644 index 0000000..f56cf42 --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/CapabilityDataMixin.java @@ -0,0 +1,20 @@ +package cyano.lootable.mixin; + +import cyano.lootable.upgrade.UpgradePreservation; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.util.datafix.DataFixTypes; +import net.minecraft.world.level.storage.DimensionDataStorage; +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) +abstract class CapabilityDataMixin { + @Inject(method = "readTagFromDisk", at = @At("RETURN"), cancellable = true) + private void preserveOldCapabilities(String id, DataFixTypes fixer, int version, + CallbackInfoReturnable callback) { + if (fixer == null && id.equals("capabilities")) + callback.setReturnValue(UpgradePreservation.upgradeLevelCapabilities(callback.getReturnValue())); + } +} diff --git a/src/main/java/cyano/lootable/mixin/CorpseRenderDispatcherMixin.java b/src/main/java/cyano/lootable/mixin/CorpseRenderDispatcherMixin.java new file mode 100644 index 0000000..046052f --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/CorpseRenderDispatcherMixin.java @@ -0,0 +1,33 @@ +package cyano.lootable.mixin; + +import cyano.lootable.LootableBodies; +import cyano.lootable.graphics.CorpseRenderer; +import java.util.Map; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.world.entity.EntityType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = EntityRenderDispatcher.class, remap = false) +abstract class CorpseRenderDispatcherMixin { + @Shadow public Map, EntityRenderer> renderers; + + @Inject(method = "getRenderer(Lnet/minecraft/client/renderer/entity/state/EntityRenderState;)Lnet/minecraft/client/renderer/entity/EntityRenderer;", + at = @At("HEAD"), cancellable = true) + private void useCorpseRenderer(EntityRenderState state, CallbackInfoReturnable> result) { + // The corpse uses the avatar model and equipment layers. Vanilla's + // submission pass otherwise sends its state to the player renderer. + if (state instanceof AvatarRenderState && state.entityType == LootableBodies.CORPSE) { + if (!(renderers.get(state.entityType) instanceof CorpseRenderer renderer)) { + throw new IllegalStateException("Missing Lootable Bodies corpse renderer"); + } + result.setReturnValue(renderer); + } + } +} diff --git a/src/main/java/cyano/lootable/mixin/DataFixersMixin.java b/src/main/java/cyano/lootable/mixin/DataFixersMixin.java new file mode 100644 index 0000000..a07efd2 --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/DataFixersMixin.java @@ -0,0 +1,17 @@ +package cyano.lootable.mixin; + +import com.mojang.datafixers.DataFixer; +import cyano.lootable.upgrade.UpgradePreservation; +import net.minecraft.util.datafix.DataFixers; +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 = DataFixers.class, remap = false) +abstract class DataFixersMixin { + @Inject(method = "getDataFixer", at = @At("RETURN"), cancellable = true) + private static void wrapNativeFixer(CallbackInfoReturnable callback) { + callback.setReturnValue(UpgradePreservation.wrapper(callback.getReturnValue())); + } +} diff --git a/src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java b/src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java new file mode 100644 index 0000000..3a548ba --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/DedicatedWorldMixin.java @@ -0,0 +1,24 @@ +package cyano.lootable.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.serialization.Dynamic; +import cyano.lootable.upgrade.EarlyWorldPreservation; +import java.io.IOException; +import net.minecraft.server.Main; +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 { + 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)); + } +} diff --git a/src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java b/src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java new file mode 100644 index 0000000..daee56a --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/LegacyStructuresMixin.java @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..733f11a --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/RegionStorageMixin.java @@ -0,0 +1,40 @@ +package cyano.lootable.mixin; + +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; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = SimpleRegionStorage.class, remap = false) +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;", + at = @At("HEAD"), cancellable = true) + private void preserveRegion(CompoundTag input, int fallback, CompoundTag context, + CallbackInfoReturnable callback) { + if (dataFixType != DataFixTypes.CHUNK && dataFixType != DataFixTypes.ENTITY_CHUNK) return; + 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; + } +} diff --git a/src/main/java/cyano/lootable/mixin/SaveAccessMixin.java b/src/main/java/cyano/lootable/mixin/SaveAccessMixin.java new file mode 100644 index 0000000..5da5ee5 --- /dev/null +++ b/src/main/java/cyano/lootable/mixin/SaveAccessMixin.java @@ -0,0 +1,27 @@ +package cyano.lootable.mixin; + +import com.mojang.serialization.Dynamic; +import cyano.lootable.upgrade.EarlyWorldPreservation; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.storage.LevelStorageSource; +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.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); + } + + @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) { + 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 92dde9c..a47cb61 100644 --- a/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java +++ b/src/main/java/cyano/lootable/network/ClientLoginReadGuard.java @@ -9,15 +9,13 @@ import net.minecraft.network.Connection; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.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 { - @SubscribeEvent public void tick(TickEvent.ClientTickEvent event) { - if(event.phase!=TickEvent.Phase.END)return; + public void tick(TickEvent.ClientTickEvent.Post event) { Minecraft minecraft=Minecraft.getInstance(); if(!(minecraft.screen instanceof ConnectScreen))return; Connection manager=ObfuscationReflectionHelper.getPrivateValue( diff --git a/src/main/java/cyano/lootable/network/CorpseProfiles.java b/src/main/java/cyano/lootable/network/CorpseProfiles.java index aae6de8..dadc304 100644 --- a/src/main/java/cyano/lootable/network/CorpseProfiles.java +++ b/src/main/java/cyano/lootable/network/CorpseProfiles.java @@ -17,13 +17,13 @@ public static GameProfile placeholder(String name) { } public static boolean isResolved(GameProfile profile) { - return profile!=null && !UNRESOLVED.equals(profile.getId()); + return profile!=null && !UNRESOLVED.equals(profile.id()); } public static CompletableFuture resolve(GameProfile initial, Function>> lookup) { try { - return lookup.apply(initial.getName()).handle((value,failure) -> + return lookup.apply(initial.name()).handle((value,failure) -> failure==null?value.filter(CorpseProfiles::isResolved).orElse(initial):initial); } catch (RuntimeException unavailable) { return CompletableFuture.completedFuture(initial); diff --git a/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java b/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java index ae5e47c..04254a4 100644 --- a/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java +++ b/src/main/java/cyano/lootable/upgrade/ChunkVersionContext.java @@ -6,8 +6,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.chunk.storage.ChunkStorage; -import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; /** Installs preservation before native save handlers and chunk readers run. */ public final class ChunkVersionContext { private ChunkVersionContext() { } @@ -20,8 +19,8 @@ public static void installServer(MinecraftServer server) { } public static void installWorld(ServerLevel world) { Object loader=world.getChunkSource().chunkMap; - if(!(loader instanceof ChunkStorage))throw new IllegalStateException("Unsupported chunk reader; refusing unprotected world loading"); - installFixer(loader,chunk->LegacyStructureDataHandler.getLegacyStructureHandler(world.dimension(),world.getServer().getLevel(net.minecraft.world.level.Level.OVERWORLD).getDataStorage()).updateFromLegacy(chunk)); + if (!(loader instanceof SimpleRegionStorage) || !(loader instanceof UpgradePreservation.RegionAttached)) + throw new IllegalStateException("Unsupported chunk reader; refusing unprotected world loading"); } private static Object related(Object owner,Class expected) { java.util.List matches=new java.util.ArrayList<>(); diff --git a/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java b/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java index 6663aa2..2d496aa 100644 --- a/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java +++ b/src/main/java/cyano/lootable/upgrade/CorpseIdentity.java @@ -1,5 +1,8 @@ package cyano.lootable.upgrade; +import static cyano.lootable.upgrade.NativeNbt.hasType; +import static cyano.lootable.upgrade.NativeNbt.readUuid; + import java.util.UUID; import net.minecraft.nbt.CompoundTag; @@ -9,14 +12,14 @@ private CorpseIdentity() { } static UUID read(CompoundTag data) { UUID current=null,legacy=null; if(data.contains("UUID")) { - if(!data.contains("UUID",11) || data.getIntArray("UUID").length!=4) + if(!hasType(data, "UUID", 11) || data.getIntArray("UUID").orElseGet(() -> new int[0]).length!=4) throw new IllegalArgumentException("Malformed corpse UUID array"); - current=data.getUUID("UUID"); + current=readUuid(data, "UUID"); } if(data.contains("UUIDMost") || data.contains("UUIDLeast")) { - if(!data.contains("UUIDMost",4) || !data.contains("UUIDLeast",4)) + if(!hasType(data, "UUIDMost", 4) || !hasType(data, "UUIDLeast", 4)) throw new IllegalArgumentException("Malformed legacy corpse UUID"); - legacy=new UUID(data.getLong("UUIDMost"),data.getLong("UUIDLeast")); + legacy=new UUID(data.getLongOr("UUIDMost", 0L),data.getLongOr("UUIDLeast", 0L)); } if(current!=null && legacy!=null && !current.equals(legacy)) throw new IllegalArgumentException("Conflicting saved corpse UUIDs"); diff --git a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java index 64f1858..976a8ba 100644 --- a/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java +++ b/src/main/java/cyano/lootable/upgrade/EarlyWorldPreservation.java @@ -27,23 +27,20 @@ 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(!(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 { } + private static void verifyAttachment(Class reader) { - try { - if(!reader.getName().equals(LevelStorageSource.LevelStorageAccess.class.getName())) - throw new IllegalStateException("Unexpected native save reader"); - java.lang.reflect.Method marker=reader.getDeclaredMethod("lootablebodies$preservationAttached"); - if(marker.getReturnType()!=boolean.class || !marker.isSynthetic() || - !java.lang.reflect.Modifier.isStatic(marker.getModifiers()) || - !java.lang.reflect.Modifier.isPublic(marker.getModifiers()) || !Boolean.TRUE.equals(marker.invoke(null))) - throw new IllegalStateException("Native save preservation attachment is invalid"); - } catch(ReflectiveOperationException missing) { - throw new IllegalStateException("Native save preservation could not be attached. World loading has been stopped before reading or rewriting a world.",missing); - } + if (reader != LevelStorageSource.LevelStorageAccess.class || !Attached.class.isAssignableFrom(reader)) + throw new IllegalStateException("Native save preservation could not be attached. World loading has been stopped before reading or rewriting a world."); } + /** Called before Forge reads extra data or native level conversion. */ public static void beforeRead(LevelStorageSource.LevelStorageAccess save,boolean fallback) { try { @@ -64,9 +61,14 @@ public static void beforeRead(LevelStorageSource.LevelStorageAccess save,boolean } catch(IOException | RuntimeException unsafe){throw new UnsafeWorldUpgrade(unsafe);} } /** The dedicated startup entry point has no shutdown hook before world construction. */ - public static Dynamic readDedicatedWorld(LevelStorageSource.LevelStorageAccess save,boolean fallback) throws IOException { + @FunctionalInterface + public interface WorldReader { + Dynamic read() throws IOException; + } + + public static Dynamic readDedicatedWorld(LevelStorageSource.LevelStorageAccess save, WorldReader reader) throws IOException { try { - return fallback ? save.getDataTagFallback() : save.getDataTag(); + return reader.read(); } catch(UnsafeWorldUpgrade unsafe) { metadata.remove(save); try { diff --git a/src/main/java/cyano/lootable/upgrade/ItemMigration.java b/src/main/java/cyano/lootable/upgrade/ItemMigration.java index 8c36c30..07952f3 100644 --- a/src/main/java/cyano/lootable/upgrade/ItemMigration.java +++ b/src/main/java/cyano/lootable/upgrade/ItemMigration.java @@ -1,11 +1,16 @@ package cyano.lootable.upgrade; + +import static cyano.lootable.upgrade.NativeNbt.hasType; +import static cyano.lootable.upgrade.NativeNbt.typedList; +import static cyano.lootable.upgrade.NativeNbt.hasUuid; +import static cyano.lootable.upgrade.NativeNbt.writeUuid; import java.util.*; import net.minecraft.world.item.Item; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NumericTag; import net.minecraft.nbt.Tag; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import com.mojang.datafixers.DataFixer; /** Runs native item fixes on copies, retaining ambiguous originals. */ public final class ItemMigration { @@ -16,13 +21,14 @@ private ItemMigration() { } // legacy item format through the last complete schema before that change. private static final int LAST_LEGACY_ITEM_VERSION=3817; private static final int RELEASED_COMPONENT_VERSION=3839; + private static final int RELEASED_121_VERSION=3955; /** Null means the caller retains the original raw record for recovery. */ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { if(versions==null || versions.isEmpty())return raw.copy(); - if(versions.getBoolean("Ambiguous")) { + if(versions.getBooleanOr("Ambiguous", false)) { CompoundTag agreed=null; - for(int candidate:versions.getIntArray("CandidateDataVersions")) { + for(int candidate:versions.getIntArray("CandidateDataVersions").orElseGet(() -> new int[0])) { CompoundTag context=new CompoundTag(); context.putInt("DataVersion",candidate); CompoundTag fixed=convert(raw,context); @@ -31,10 +37,14 @@ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { } return agreed; } - if(!versions.contains("DataVersion",3))return null; - int source=versions.getInt("DataVersion"); + if(!hasType(versions, "DataVersion", 3))return null; + int source=versions.getIntOr("DataVersion", 0); if(source==UpgradePreservation.MINECRAFT_VERSION)return raw.copy(); - if(source==RELEASED_COMPONENT_VERSION)return convertReleasedComponents(raw); + if(source==RELEASED_COMPONENT_VERSION) { + CompoundTag intermediate = convertReleasedComponents(raw); + return intermediate == null ? null : convert121Components(intermediate); + } + if(source==RELEASED_121_VERSION)return convert121Components(raw); if(source<512 || source>LAST_LEGACY_ITEM_VERSION || pipeline==null)return null; CompoundTag legacy=convertNbt(raw,versions); if(legacy==null || !supportedLegacyStack(legacy))return null; @@ -42,32 +52,144 @@ public static CompoundTag convert(CompoundTag raw,CompoundTag versions) { CompoundTag input=legacy.copy(); input.remove("Damage"); CompoundTag fixed=NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK, - input,LAST_LEGACY_ITEM_VERSION,UpgradePreservation.MINECRAFT_VERSION); - return componentsSurvive(legacy,fixed,true)?fixed:null; + input,LAST_LEGACY_ITEM_VERSION,RELEASED_121_VERSION); + return componentsSurvive(legacy,fixed,true)?convert121Components(fixed):null; } catch(RuntimeException invalid) { return null; } } private static CompoundTag convertReleasedComponents(CompoundTag raw) { - if(pipeline==null || !Set.of("id","count","components","Slot").containsAll(raw.getAllKeys()) - || !raw.contains("id",8) || ResourceLocation.tryParse(raw.getString("id"))==null - || (raw.contains("components") && !raw.contains("components",10)))return null; + 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 fixed=NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK, - raw.copy(),RELEASED_COMPONENT_VERSION,UpgradePreservation.MINECRAFT_VERSION); + raw.copy(),RELEASED_COMPONENT_VERSION,RELEASED_121_VERSION); return componentStackSurvives(raw,fixed)?fixed:null; } catch(RuntimeException invalid) { return null; } } + private static CompoundTag convert121Components(CompoundTag raw) { + 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 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; + } catch (RuntimeException invalid) { + return null; + } + } + + private static boolean components121Survive(CompoundTag raw, CompoundTag fixed) { + if (raw.equals(fixed)) return true; + CompoundTag expected = raw.copy(); + if (raw.getStringOr("id", "").equals("minecraft:chain")) expected.putString("id", "minecraft:iron_chain"); + if (expected.equals(fixed)) return true; + if (!equalExcept(expected, fixed, "components") + || !hasType(expected, "components", 10) || !hasType(fixed, "components", 10)) return false; + CompoundTag old = expected.getCompoundOrEmpty("components"), updated = fixed.getCompoundOrEmpty("components"); + Set expectedKeys = new HashSet<>(old.keySet()); + Set hidden = new HashSet<>(); + for (String key : List.of("minecraft:attribute_modifiers", "minecraft:enchantments", "minecraft:stored_enchantments")) { + if (old.get(key) instanceof CompoundTag envelope && envelope.contains("show_in_tooltip")) { + if (!hasType(envelope, "show_in_tooltip", 1)) return false; + byte flag = envelope.getByteOr("show_in_tooltip", (byte)-1); + if (flag != 0 && flag != 1) return false; + if (flag == 0) hidden.add(key); + } + } + if (!hidden.isEmpty()) { + if (!expectedKeys.add("minecraft:tooltip_display") || !preservesHiddenComponents(updated, hidden)) return false; + } + if (!expectedKeys.equals(updated.keySet())) return false; + for (String key : old.keySet()) { + Tag before = old.get(key), after = updated.get(key); + if (before.equals(after)) continue; + boolean preserved = switch (key) { + case "minecraft:custom_name", "minecraft:item_name" -> componentTextSurvives(before, after); + case "minecraft:lore" -> loreSurvives(before, after); + case "minecraft:enchantments", "minecraft:stored_enchantments" -> + before instanceof CompoundTag levels && unwrappedTooltipField(levels, "levels") instanceof CompoundTag expectedLevels + && expectedLevels.equals(after); + case "minecraft:attribute_modifiers" -> attributeComponentsSurvive(before, after); + case "minecraft:entity_data" -> entityDataNameSurvives(before, after); + case "minecraft:container" -> componentItemsSurvive(before, after, true, true); + case "minecraft:bundle_contents", "minecraft:charged_projectiles" -> + componentItemsSurvive(before, after, false, true); + default -> false; + }; + if (!preserved) return false; + } + return true; + } + + private static Tag unwrappedTooltipField(CompoundTag envelope, String key) { + if (!Set.of(key, "show_in_tooltip").containsAll(envelope.keySet())) return null; + return envelope.get(key); + } + + private static boolean preservesHiddenComponents(CompoundTag updated, Set hidden) { + CompoundTag display = updated.getCompoundOrEmpty("minecraft:tooltip_display"); + if (!display.keySet().equals(Set.of("hide_tooltip", "hidden_components")) + || !hasType(display, "hide_tooltip", 1) || display.getByteOr("hide_tooltip", (byte)-1) != 0) return false; + ListTag values = typedList(display, "hidden_components", 8); + if (values.size() != hidden.size()) return false; + Set actual = new HashSet<>(); + for (Tag value : values) actual.add(((net.minecraft.nbt.StringTag)value).value()); + return actual.equals(hidden); + } + + private static boolean attributeComponentsSurvive(Tag before, Tag after) { + if (!(before instanceof CompoundTag envelope) + || !(unwrappedTooltipField(envelope, "modifiers") instanceof ListTag modifiers) + || !(after instanceof ListTag updated) || modifiers.size() != updated.size()) return false; + for (int i = 0; i < modifiers.size(); i++) { + if (!(modifiers.get(i) instanceof CompoundTag original) || !(updated.get(i) instanceof CompoundTag fixed)) return false; + if (original.equals(fixed)) continue; + Identifier id = Identifier.tryParse(original.getStringOr("type", "")); + Identifier canonical = LegacyRegistryMappings.canonicalAttributeId(id); + CompoundTag expected = original.copy(); + if (canonical != null && !canonical.equals(id)) expected.putString("type", canonical.toString()); + if (!expected.equals(fixed)) return false; + } + return true; + } + + private static boolean entityDataNameSurvives(Tag before, Tag after) { + return before instanceof CompoundTag old && after instanceof CompoundTag updated + && equalExcept(old, updated, "CustomName") + && updated.contains("CustomName") + && componentTextSurvives(old.get("CustomName"), updated.get("CustomName")); + } + + private static boolean componentTextSurvives(Tag before, Tag after) { + if (!(before instanceof net.minecraft.nbt.StringTag old)) return false; + var json = com.google.gson.JsonParser.parseString(old.value()); + var expected = com.mojang.serialization.JsonOps.INSTANCE.convertTo(net.minecraft.nbt.NbtOps.INSTANCE, json); + return expected.equals(after); + } + + private static boolean loreSurvives(Tag before, Tag after) { + if (!(before instanceof ListTag old) || !(after instanceof ListTag updated) || old.size() != updated.size()) return false; + for (int i = 0; i < old.size(); i++) { + if (!old.get(i).equals(updated.get(i)) && !componentTextSurvives(old.get(i), updated.get(i))) return false; + } + return true; + } + /** Resolve a legacy modifier name and UUID through the native 3945 mapping. */ - public static ResourceLocation legacyModifierId(UUID uuid,String name) { + public static Identifier legacyModifierId(UUID uuid,String name) { CompoundTag modifier=new CompoundTag(); - modifier.putUUID("uuid",uuid); + writeUuid(modifier, "uuid", uuid); modifier.putString("name",name); ListTag modifiers=new ListTag(); modifiers.add(modifier); @@ -78,17 +200,17 @@ public static ResourceLocation legacyModifierId(UUID uuid,String name) { carrier.putInt("count",1); carrier.put("components",components); CompoundTag converted=convertReleasedComponents(carrier); - return converted==null?null:ResourceLocation.tryParse(converted.getCompound("components") - .getCompound("minecraft:attribute_modifiers").getList("modifiers",10).getCompound(0).getString("id")); + return converted==null?null:Identifier.tryParse(converted.getCompoundOrEmpty("components") + .getCompoundOrEmpty("minecraft:attribute_modifiers").getListOrEmpty("modifiers").getCompoundOrEmpty(0).getStringOr("id", "")); } private static boolean componentStackSurvives(CompoundTag before,CompoundTag after) { if(before.equals(after))return true; if(!equalExcept(before,after,"components") - || !before.contains("components",10) || !after.contains("components",10))return false; - CompoundTag old=before.getCompound("components"), fixed=after.getCompound("components"); - if(!old.getAllKeys().equals(fixed.getAllKeys()))return false; - for(String key:old.getAllKeys()) { + || !hasType(before, "components", 10) || !hasType(after, "components", 10))return false; + CompoundTag old=before.getCompoundOrEmpty("components"), fixed=after.getCompoundOrEmpty("components"); + if(!old.keySet().equals(fixed.keySet()))return false; + for(String key:old.keySet()) { Tag previous=old.get(key), current=fixed.get(key); if(previous.equals(current))continue; boolean preserved=switch(key) { @@ -97,9 +219,9 @@ private static boolean componentStackSurvives(CompoundTag before,CompoundTag aft case "minecraft:container" -> componentItemsSurvive(previous,current,true); case "minecraft:bundle_contents","minecraft:charged_projectiles" -> componentItemsSurvive(previous,current,false); case "minecraft:food" -> previous instanceof CompoundTag a && current instanceof CompoundTag b - && equalExcept(a,b,"using_converts_to") && a.contains("using_converts_to",10) - && b.contains("using_converts_to",10) - && componentStackSurvives(a.getCompound("using_converts_to"),b.getCompound("using_converts_to")); + && equalExcept(a,b,"using_converts_to") && hasType(a, "using_converts_to", 10) + && hasType(b, "using_converts_to", 10) + && componentStackSurvives(a.getCompoundOrEmpty("using_converts_to"),b.getCompoundOrEmpty("using_converts_to")); default -> false; }; if(!preserved)return false; @@ -117,29 +239,37 @@ private static boolean equalExcept(CompoundTag before,CompoundTag after,String.. } private static boolean componentItemsSurvive(Tag previous,Tag current,boolean indexed) { + return componentItemsSurvive(previous, current, indexed, false); + } + + private static boolean componentItemsSurvive(Tag previous,Tag current,boolean indexed,boolean modern) { if(!(previous instanceof ListTag before) || !(current instanceof ListTag after) || before.size()!=after.size())return false; for(int i=0;i identities=new HashSet<>(); + Set identities=new HashSet<>(); for(int i=0;i new int[0])) { CompoundTag context=versions.copy();context.remove("Ambiguous");context.remove("CandidateDataVersions");context.putInt("DataVersion",candidate); CompoundTag converted=convertNbt(raw,context); if(converted==null || (agreed!=null && !agreed.equals(converted)))return null; @@ -158,14 +288,14 @@ static CompoundTag convertNbt(CompoundTag raw,CompoundTag versions) { } return agreed; } - if(pipeline==null || !versions.contains("DataVersion",3))return null; - int source=versions.getInt("DataVersion"); + if(pipeline==null || !hasType(versions, "DataVersion", 3))return null; + int source=versions.getIntOr("DataVersion", 0); if(source<512 || source>LAST_LEGACY_ITEM_VERSION || integral(raw.get("Count"))==null)return null; try { CompoundTag item=cyano.lootable.upgrade.NativeNbt.update(pipeline,net.minecraft.util.datafix.fixes.References.ITEM_STACK,raw.copy(),source,LAST_LEGACY_ITEM_VERSION); if(!Objects.equals(integral(raw.get("Count")),integral(item.get("Count"))) || !Objects.equals(raw.get("Slot"),item.get("Slot")) || !Objects.equals(raw.get("ForgeCaps"),item.get("ForgeCaps")))return null; if(!flatteningSurvives(raw,item,source))return null; - for(String key:raw.getAllKeys())if(!Arrays.asList("id","Count","Damage","Slot","tag").contains(key) && !Objects.equals(raw.get(key),item.get(key)))return null; + for(String key:raw.keySet())if(!Arrays.asList("id","Count","Damage","Slot","tag").contains(key) && !Objects.equals(raw.get(key),item.get(key)))return null; return nestedInventoriesSurvive(raw,item)?item:null; }catch(RuntimeException invalid){return null;} } @@ -177,15 +307,15 @@ private static boolean flatteningSurvives(Tag before,Tag after,int source) { Long damage=a.contains("Damage")?integral(a.get("Damage")):0L; if(damage==null || damage<0 || damage>32767)return false; if(damage!=0) { - String mapped=net.minecraft.util.datafix.fixes.ItemStackTheFlatteningFix.updateItem(a.getString("id"),damage.intValue()); - String base=net.minecraft.util.datafix.fixes.ItemStackTheFlatteningFix.updateItem(a.getString("id"),0); - boolean durability=Objects.equals(damage,integral(b.getCompound("tag").get("Damage"))); + String mapped=net.minecraft.util.datafix.fixes.ItemStackTheFlatteningFix.updateItem(a.getStringOr("id", ""),damage.intValue()); + String base=net.minecraft.util.datafix.fixes.ItemStackTheFlatteningFix.updateItem(a.getStringOr("id", ""),0); + boolean durability=Objects.equals(damage,integral(b.getCompoundOrEmpty("tag").get("Damage"))); // Vanilla silently falls back to variant zero for bad metadata. // Unknown mod metadata has no safe automatic interpretation. if(!durability && (mapped==null || mapped.equals(base)))return false; } } - for(String key:a.getAllKeys()) { + for(String key:a.keySet()) { Tag old=a.get(key),next=b.get(key); if(key.equals("ench") || key.equals("StoredEnchantments")) { if(!enchantmentsSurvive(old,b.get(key.equals("ench")?"Enchantments":key)))return false; @@ -203,23 +333,23 @@ private static boolean enchantmentsSurvive(Tag old,Tag next) { ListTag a=(ListTag)old,b=(ListTag)next;if(a.size()!=b.size())return false; for(int i=0;i=1 && nbt.getId()<=4 ? ((NumericTag)nbt).getAsLong() : null; + return nbt instanceof NumericTag && nbt.getId()>=1 && nbt.getId()<=4 ? ((NumericTag)nbt).longValue() : null; } private static boolean supportedLegacyStack(CompoundTag item) { - if(!Set.of("id","Count","Damage","Slot","tag").containsAll(item.getAllKeys()) - || !item.contains("id",8) || (item.contains("tag") && !item.contains("tag",10)))return false; + if(!Set.of("id","Count","Damage","Slot","tag").containsAll(item.keySet()) + || !hasType(item, "id", 8) || (item.contains("tag") && !hasType(item, "tag", 10)))return false; Long count=integral(item.get("Count")); if(count==null || count<=0 || count>127)return false; if(item.contains("Damage")) { Long old=integral(item.get("Damage")); - CompoundTag tag=item.getCompound("tag"); + CompoundTag tag=item.getCompoundOrEmpty("tag"); Long damage=tag.contains("Damage")?integral(tag.get("Damage")):Long.valueOf(0); if(!Objects.equals(old,damage))return false; } @@ -229,21 +359,21 @@ private static boolean supportedLegacyStack(CompoundTag item) { private static boolean componentsSurvive(CompoundTag legacy,CompoundTag fixed,boolean outerSlot) { if(!supportedLegacyStack(legacy) || !Objects.equals(integral(legacy.get("Count")),integral(fixed.get("count"))) - || !Objects.equals(ResourceLocation.tryParse(legacy.getString("id")),ResourceLocation.tryParse(fixed.getString("id"))) + || !Objects.equals(Identifier.tryParse(legacy.getStringOr("id", "")),Identifier.tryParse(fixed.getStringOr("id", ""))) || (outerSlot && !Objects.equals(legacy.get("Slot"),fixed.get("Slot"))))return false; - CompoundTag tag=legacy.getCompound("tag"), components=fixed.getCompound("components"); - CompoundTag custom=components.getCompound("minecraft:custom_data"); - for(String key:tag.getAllKeys()) { + CompoundTag tag=legacy.getCompoundOrEmpty("tag"), components=fixed.getCompoundOrEmpty("components"); + CompoundTag custom=components.getCompoundOrEmpty("minecraft:custom_data"); + for(String key:tag.keySet()) { Tag value=tag.get(key); if(value.equals(custom.get(key)))continue; boolean kept=switch(key) { case "Damage" -> sameDamage(value,components.get("minecraft:damage")); case "Enchantments" -> enchantmentLevelsSurvive(value,components.get("minecraft:enchantments"),components); case "StoredEnchantments" -> enchantmentLevelsSurvive(value,components.get("minecraft:stored_enchantments"),components); - case "display" -> displaySurvives(value,components,custom.getCompound("display")); + case "display" -> displaySurvives(value,components,custom.getCompoundOrEmpty("display")); case "BlockEntityTag" -> blockEntitySurvives(value,components); case "EntityTag" -> value.equals(components.get("minecraft:entity_data")); - case "Potion" -> potionSurvives(value,components.getCompound("minecraft:potion_contents")); + case "Potion" -> potionSurvives(value,components.getCompoundOrEmpty("minecraft:potion_contents")); case "RepairCost" -> sameDamage(value,components.get("minecraft:repair_cost")); case "CustomModelData" -> value.equals(components.get("minecraft:custom_model_data")); default -> false; @@ -255,13 +385,14 @@ private static boolean componentsSurvive(CompoundTag legacy,CompoundTag fixed,bo public static boolean sameSavedComponents(CompoundTag expected,CompoundTag encoded) { if(expected.equals(encoded))return true; - if(!expected.getAllKeys().equals(encoded.getAllKeys()))return false; - for(String key:expected.getAllKeys()) { + if(!expected.keySet().equals(encoded.keySet()))return false; + for(String key:expected.keySet()) { Tag before=expected.get(key), after=encoded.get(key); if(before.equals(after))continue; boolean same=switch(key) { case "minecraft:custom_name","minecraft:item_name" -> sameText(before,after); case "minecraft:lore" -> sameLore(before,after); + case "minecraft:tooltip_display" -> sameTooltipDisplay(before, after); default -> false; }; if(!same)return false; @@ -269,34 +400,35 @@ public static boolean sameSavedComponents(CompoundTag expected,CompoundTag encod return true; } + private static boolean sameTooltipDisplay(Tag before, Tag after) { + if (!(before instanceof CompoundTag saved) || !(after instanceof CompoundTag encoded)) return false; + CompoundTag canonical = saved.copy(); + if (hasType(saved, "hide_tooltip", 1) && saved.getByteOr("hide_tooltip", (byte)-1) == 0) { + canonical.remove("hide_tooltip"); + } + return canonical.equals(encoded); + } + private static boolean sameLore(Tag before,Tag after) { if(!(before instanceof ListTag a) || !(after instanceof ListTag b) || a.size()!=b.size())return false; for(int i=0;i seen=new HashSet<>(); for(Tag element:list) { if(!(element instanceof CompoundTag enchantment) - || !Set.of("id","lvl").containsAll(enchantment.getAllKeys()) - || !enchantment.contains("id",8))return false; - String name=enchantment.getString("id"); + || !Set.of("id","lvl").containsAll(enchantment.keySet()) + || !hasType(enchantment, "id", 8))return false; + String name=enchantment.getStringOr("id", ""); Long level=integral(enchantment.get("lvl")); if(!seen.add(name) || level==null || level<0 || level>255 || !Objects.equals(level,integral(levels.get(name))))return false; @@ -327,7 +459,7 @@ private static boolean enchantmentLevelsSurvive(Tag raw,Tag fixed,CompoundTag co private static boolean displaySurvives(Tag raw,CompoundTag components,CompoundTag custom) { if(!(raw instanceof CompoundTag display))return false; - for(String key:display.getAllKeys()) { + for(String key:display.keySet()) { Tag value=display.get(key); if(value.equals(custom.get(key)))continue; String component=switch(key) { @@ -342,15 +474,15 @@ private static boolean displaySurvives(Tag raw,CompoundTag components,CompoundTa private static boolean blockEntitySurvives(Tag raw,CompoundTag components) { if(!(raw instanceof CompoundTag block))return false; - CompoundTag remainder=components.getCompound("minecraft:block_entity_data"); - ListTag container=components.getList("minecraft:container",10); - for(String key:block.getAllKeys()) { + CompoundTag remainder=components.getCompoundOrEmpty("minecraft:block_entity_data"); + ListTag container=typedList(components, "minecraft:container", 10); + for(String key:block.keySet()) { Tag value=block.get(key); if(value.equals(remainder.get(key)))continue; if(key.equals("Base")) { Long colour=integral(value); if(colour==null || colour<0 || colour>15 - || !components.getString("minecraft:base_color").equals(net.minecraft.world.item.DyeColor.byId(colour.intValue()).getName())) + || !components.getStringOr("minecraft:base_color", "").equals(net.minecraft.world.item.DyeColor.byId(colour.intValue()).getName())) return false; continue; } @@ -358,14 +490,14 @@ private static boolean blockEntitySurvives(Tag raw,CompoundTag components) { if(items.size()!=container.size())return false; Set slots=new HashSet<>(); for(int i=0;i255 || !slots.add(slot) || !Objects.equals(slot,integral(fixed.get("slot"))) - || !componentsSurvive(legacy,fixed.getCompound("item"),false))return false; + || !componentsSurvive(legacy,fixed.getCompoundOrEmpty("item"),false))return false; } } else if(!key.equals("id") || !remainder.isEmpty() - || (!block.getString("id").startsWith("minecraft:") && !block.getString("id").equals("Chest")))return false; + || (!block.getStringOr("id", "").startsWith("minecraft:") && !block.getStringOr("id", "").equals("Chest")))return false; } return true; } @@ -374,13 +506,13 @@ private static boolean nestedInventoriesSurvive(Tag before,Tag after) { if(before instanceof CompoundTag) { if(!(after instanceof CompoundTag)) return false; CompoundTag a=(CompoundTag)before,b=(CompoundTag)after; - for(String key:a.getAllKeys()) { + for(String key:a.keySet()) { Tag old=a.get(key),next=b.get(key); if((key.equals("Items") || key.equals("Inventory")) && old instanceof ListTag) { if(!(next instanceof ListTag) || ((ListTag)old).size()!=((ListTag)next).size()) return false; ListTag x=(ListTag)old,y=(ListTag)next; for(int i=0;i builder = new RegistryBuilder() - .setName(ResourceLocation.parse("minecraft:villagerprofessions")) + .setName(Identifier.parse("minecraft:villagerprofessions")) .setIDRange(0, 32767) .disableSaving().disableSync(); // These are the six actual 1.10-1.13 vanilla entries, in original ID order. // Unknown mod professions remain missing and retain Forge's normal gate. event.create(builder, registry -> { for (String name : new String[]{"farmer", "librarian", "priest", "smith", "butcher", "nitwit"}) - registry.register(ResourceLocation.fromNamespaceAndPath("minecraft", name),new Entry()); + registry.register(Identifier.fromNamespaceAndPath("minecraft", name),new Entry()); }); } diff --git a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java index f5a0776..932ca5f 100644 --- a/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java +++ b/src/main/java/cyano/lootable/upgrade/LegacyRegistryMappings.java @@ -1,67 +1,100 @@ package cyano.lootable.upgrade; -import cyano.lootable.LootableBodies; import java.util.Set; import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; +import java.util.List; import net.minecraft.sounds.SoundEvent; import net.minecraftforge.registries.RegisterEvent; import net.minecraftforge.registries.MissingMappingsEvent; import net.minecraft.core.registries.Registries; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.eventbus.api.SubscribeEvent; /** Known vanilla registry renames, using the native conversion rules. */ -@Mod.EventBusSubscriber(modid=LootableBodies.MODID,bus=Mod.EventBusSubscriber.Bus.FORGE) public final class LegacyRegistryMappings { - public static ResourceLocation biomeReplacement(ResourceLocation old) { + public static Identifier biomeReplacement(Identifier old) { if(!old.getNamespace().equals("minecraft"))return null; String name=old.toString(); name=net.minecraft.util.datafix.fixes.BiomeFix.BIOMES.getOrDefault(name,name); if(name.equals("minecraft:nether"))name="minecraft:nether_wastes"; name=net.minecraft.util.datafix.fixes.CavesAndCliffsRenames.RENAMES.getOrDefault(name,name); if(name.equals("minecraft:deep_warm_ocean"))name="minecraft:warm_ocean"; - return name.equals(old.toString())?null:ResourceLocation.parse(name); + return name.equals(old.toString())?null:Identifier.parse(name); } - public static ResourceLocation itemReplacement(ResourceLocation old) { + public static Identifier itemReplacement(Identifier old) { if (!old.getNamespace().equals("minecraft")) return null; switch (old.getPath()) { - case "sign": return ResourceLocation.parse("minecraft:oak_sign"); - case "rose_red": return ResourceLocation.parse("minecraft:red_dye"); - case "cactus_green": return ResourceLocation.parse("minecraft:green_dye"); - case "dandelion_yellow": return ResourceLocation.parse("minecraft:yellow_dye"); - case "grass_path": return ResourceLocation.parse("minecraft:dirt_path"); - case "grass": return ResourceLocation.withDefaultNamespace("short_grass"); - case "zombie_pigman_spawn_egg":return ResourceLocation.parse("minecraft:zombified_piglin_spawn_egg"); - case "scute": return ResourceLocation.withDefaultNamespace("turtle_scute"); - case "pottery_shard_archer": return ResourceLocation.parse("minecraft:archer_pottery_sherd"); - case "pottery_shard_arms_up": return ResourceLocation.parse("minecraft:arms_up_pottery_sherd"); - case "pottery_shard_prize": return ResourceLocation.parse("minecraft:prize_pottery_sherd"); - case "pottery_shard_skull": return ResourceLocation.parse("minecraft:skull_pottery_sherd"); + case "sign": return Identifier.parse("minecraft:oak_sign"); + case "rose_red": return Identifier.parse("minecraft:red_dye"); + case "cactus_green": return Identifier.parse("minecraft:green_dye"); + case "dandelion_yellow": return Identifier.parse("minecraft:yellow_dye"); + case "grass_path": return Identifier.parse("minecraft:dirt_path"); + case "chain": return Identifier.withDefaultNamespace("iron_chain"); + case "grass": return Identifier.withDefaultNamespace("short_grass"); + case "zombie_pigman_spawn_egg":return Identifier.parse("minecraft:zombified_piglin_spawn_egg"); + case "scute": return Identifier.withDefaultNamespace("turtle_scute"); + case "pottery_shard_archer": return Identifier.parse("minecraft:archer_pottery_sherd"); + case "pottery_shard_arms_up": return Identifier.parse("minecraft:arms_up_pottery_sherd"); + case "pottery_shard_prize": return Identifier.parse("minecraft:prize_pottery_sherd"); + case "pottery_shard_skull": return Identifier.parse("minecraft:skull_pottery_sherd"); default: return null; } } - public static ResourceLocation blockReplacement(ResourceLocation old) { + public static Identifier blockReplacement(Identifier old) { if (!old.getNamespace().equals("minecraft")) return null; switch (old.getPath()) { - case "sign": return ResourceLocation.parse("minecraft:oak_sign"); - case "wall_sign": return ResourceLocation.parse("minecraft:oak_wall_sign"); - case "grass_path": return ResourceLocation.parse("minecraft:dirt_path"); - case "grass": return ResourceLocation.withDefaultNamespace("short_grass"); + case "sign": return Identifier.parse("minecraft:oak_sign"); + case "wall_sign": return Identifier.parse("minecraft:oak_wall_sign"); + case "grass_path": return Identifier.parse("minecraft:dirt_path"); + case "chain": return Identifier.withDefaultNamespace("iron_chain"); + case "grass": return Identifier.withDefaultNamespace("short_grass"); default: return null; } } - private static final ResourceLocation OLD_XP_SOUND=ResourceLocation.parse("minecraft:entity.experience_orb.touch"); - private static final ResourceLocation XP_SOUND=ResourceLocation.parse("minecraft:entity.experience_orb.pickup"); - private static final ResourceLocation OLD_JUNGLE_AND_FOREST_MUSIC = - ResourceLocation.parse("minecraft:music.overworld.jungle_and_forest"); - private static final Set RETIRED_PARROT_SOUNDS = Set.of( - ResourceLocation.withDefaultNamespace("entity.parrot.imitate.enderman"), - ResourceLocation.withDefaultNamespace("entity.parrot.imitate.panda"), - ResourceLocation.withDefaultNamespace("entity.parrot.imitate.polar_bear"), - ResourceLocation.withDefaultNamespace("entity.parrot.imitate.wolf"), - ResourceLocation.withDefaultNamespace("entity.parrot.imitate.zombie_pigman")); + private static final Identifier OLD_XP_SOUND=Identifier.parse("minecraft:entity.experience_orb.touch"); + private static final Identifier XP_SOUND=Identifier.parse("minecraft:entity.experience_orb.pickup"); + private static final Identifier OLD_JUNGLE_AND_FOREST_MUSIC = + Identifier.parse("minecraft:music.overworld.jungle_and_forest"); + private static final Set RETIRED_PARROT_SOUNDS = Set.of( + Identifier.withDefaultNamespace("entity.parrot.imitate.enderman"), + Identifier.withDefaultNamespace("entity.parrot.imitate.panda"), + Identifier.withDefaultNamespace("entity.parrot.imitate.polar_bear"), + Identifier.withDefaultNamespace("entity.parrot.imitate.wolf"), + Identifier.withDefaultNamespace("entity.parrot.imitate.zombie_pigman")); + // These old IDs split into multiple variants in vanilla's entity data + // fixes. There is no single correct registry target to remap them to. + private static final Set SPLIT_ENTITIES = Set.of( + Identifier.withDefaultNamespace("boat"), Identifier.withDefaultNamespace("chest_boat"), + Identifier.withDefaultNamespace("potion")); + private static final Set RETIRED_121_SOUNDS = Set.of( + Identifier.withDefaultNamespace("entity.goat.screaming.horn_break"), + Identifier.withDefaultNamespace("entity.puffer_fish.ambient"), + Identifier.withDefaultNamespace("entity.wolf.howl"), + Identifier.withDefaultNamespace("item.goat_horn.play")); private LegacyRegistryMappings() { } + + public static Identifier canonicalAttributeId(Identifier id) { + if (id == null || !id.getNamespace().equals("minecraft")) return id; + String path = id.getPath(); + for (String prefix : List.of("generic.", "horse.", "player.", "zombie.")) { + if (path.startsWith(prefix) && path.length() > prefix.length()) { + return Identifier.withDefaultNamespace(path.substring(prefix.length())); + } + } + return id; + } + + public static void register() { + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::items); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::blocks); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::enchantments); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::paintings); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::potions); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::biomes); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::retiredPointsOfInterest); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::entities); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::memories); + MissingMappingsEvent.BUS.addListener(LegacyRegistryMappings::sounds); + } static T requireMappingTarget(java.util.function.Supplier lookup) { try {T target=lookup.get();if(target!=null)return target;} catch(RuntimeException failure){throw new UnsafeRegistryUpgrade(failure);} @@ -76,50 +109,54 @@ private static final class UnsafeRegistryUpgrade extends Error { UnsafeRegistryUpgrade(Throwable cause){super("Lootable Bodies stopped an unsafe vanilla registry conversion. Keep the original world and upgrade journal.",cause);} } - public static ResourceLocation entityReplacement(ResourceLocation old) { - return old.equals(ResourceLocation.parse("minecraft:zombie_pigman"))?ResourceLocation.parse("minecraft:zombified_piglin"):null; + public static Identifier entityReplacement(Identifier old) { + return old.equals(Identifier.parse("minecraft:zombie_pigman"))?Identifier.parse("minecraft:zombified_piglin"):null; } - public static ResourceLocation soundReplacement(ResourceLocation old) { + public static Identifier soundReplacement(Identifier old) { if(old.equals(OLD_XP_SOUND))return XP_SOUND; + if(old.equals(Identifier.withDefaultNamespace("entity.leash_knot.break"))) + return Identifier.withDefaultNamespace("item.lead.untied"); + if(old.equals(Identifier.withDefaultNamespace("entity.leash_knot.place"))) + return Identifier.withDefaultNamespace("item.lead.tied"); if(!old.getNamespace().equals("minecraft"))return null; - if(old.getPath().equals("item.sweet_berries.pick_from_bush"))return ResourceLocation.parse("minecraft:block.sweet_berry_bush.pick_berries"); - if(old.getPath().equals("music.nether"))return ResourceLocation.parse("minecraft:music.nether.nether_wastes"); - if(old.getPath().equals("block.trial_spawner.ambient_charged"))return ResourceLocation.withDefaultNamespace("block.trial_spawner.ambient_ominous"); - if(old.getPath().equals("block.trial_spawner.charge_activate"))return ResourceLocation.withDefaultNamespace("block.trial_spawner.ominous_activate"); + if(old.getPath().equals("item.sweet_berries.pick_from_bush"))return Identifier.parse("minecraft:block.sweet_berry_bush.pick_berries"); + if(old.getPath().equals("music.nether"))return Identifier.parse("minecraft:music.nether.nether_wastes"); + if(old.getPath().equals("block.trial_spawner.ambient_charged"))return Identifier.withDefaultNamespace("block.trial_spawner.ambient_ominous"); + if(old.getPath().equals("block.trial_spawner.charge_activate"))return Identifier.withDefaultNamespace("block.trial_spawner.ominous_activate"); if (old.getPath().equals("item.brush.brushing")) - return ResourceLocation.parse("minecraft:item.brush.brushing.generic"); + return Identifier.parse("minecraft:item.brush.brushing.generic"); if (old.getPath().equals("item.brush.brush_sand_completed")) - return ResourceLocation.parse("minecraft:item.brush.brushing.sand.complete"); + return Identifier.parse("minecraft:item.brush.brushing.sand.complete"); for(String suffix:new String[]{"ambient","angry","death","hurt"}) - if(old.getPath().equals("entity.zombie_pigman."+suffix))return ResourceLocation.parse("minecraft:entity.zombified_piglin."+suffix); + 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. */ public static void retiredMemories(RegisterEvent event) { - event.register(Registries.MEMORY_MODULE_TYPE,ResourceLocation.parse("minecraft:golem_last_seen_time"), + 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())); } - @SubscribeEvent public static void items(MissingMappingsEvent event) { + public static void items(MissingMappingsEvent event) { for (MissingMappingsEvent.Mapping mapping : event.getAllMappings(Registries.ITEM)) { - ResourceLocation replacement = itemReplacement(mapping.getKey()); + Identifier replacement = itemReplacement(mapping.getKey()); if (replacement != null) mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.ITEMS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.ITEMS.getValue(replacement))); } } - @SubscribeEvent public static void blocks(MissingMappingsEvent event) { + public static void blocks(MissingMappingsEvent event) { for (MissingMappingsEvent.Mapping mapping : event.getAllMappings(Registries.BLOCK)) { - ResourceLocation replacement = blockReplacement(mapping.getKey()); + Identifier replacement = blockReplacement(mapping.getKey()); if (replacement != null) mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.BLOCKS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.BLOCKS.getValue(replacement))); } } - static ResourceLocation enchantmentReplacement(ResourceLocation old) { - return old.equals(ResourceLocation.withDefaultNamespace("sweeping")) - ?ResourceLocation.withDefaultNamespace("sweeping_edge"):null; + static Identifier enchantmentReplacement(Identifier old) { + return old.equals(Identifier.withDefaultNamespace("sweeping")) + ?Identifier.withDefaultNamespace("sweeping_edge"):null; } - @SubscribeEvent public static void enchantments(MissingMappingsEvent event) { + public static void enchantments(MissingMappingsEvent event) { for(var mapping:event.getAllMappings(Registries.ENCHANTMENT)) { - ResourceLocation replacement=enchantmentReplacement(mapping.getKey()); + Identifier replacement=enchantmentReplacement(mapping.getKey()); if(replacement==null)continue; // The old numeric snapshot does not own the 1.21 data-pack // registry. Native item conversion renames the actual enchantment. @@ -133,24 +170,24 @@ private static final class VanillaData { static final net.minecraft.core.HolderLookup.Provider LOOKUP=net.minecraft.data.registries.VanillaRegistries.createLookup(); } - @SubscribeEvent public static void paintings(MissingMappingsEvent event) { - for(var mapping:event.getAllMappings(Registries.PAINTING_VARIANT)) { + public static void paintings(MissingMappingsEvent event) { + for(var mapping:event.getAllMappings(Registries.PAINTING_VARIANT)) { if(vanillaPaintingVariant(mapping.getKey()))mapping.ignore(); } } - static boolean vanillaPaintingVariant(ResourceLocation key) { + static boolean vanillaPaintingVariant(Identifier key) { // A named vanilla painting now lives in the data-pack registry. // Retire only a numeric binding whose exact name the native data owns. return key.getNamespace().equals("minecraft") && VanillaData.LOOKUP.lookupOrThrow(Registries.PAINTING_VARIANT) .get(net.minecraft.resources.ResourceKey.create(Registries.PAINTING_VARIANT,key)).isPresent(); } - static boolean retiredPotion(ResourceLocation key) { - return key.equals(ResourceLocation.withDefaultNamespace("empty")); + static boolean retiredPotion(Identifier key) { + return key.equals(Identifier.withDefaultNamespace("empty")); } - @SubscribeEvent public static void potions(MissingMappingsEvent event) { + public static void potions(MissingMappingsEvent event) { for(var mapping:event.getAllMappings(Registries.POTION)) { // The native item fixer represents the old empty sentinel as an // absent potion holder. A real potion here would change its contents. @@ -159,11 +196,11 @@ static boolean retiredPotion(ResourceLocation key) { } } - @SubscribeEvent public static void biomes(MissingMappingsEvent event) { + public static void biomes(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping mapping:event.getAllMappings(Registries.BIOME)) { if(!mapping.getKey().getNamespace().equals("minecraft"))continue; - ResourceLocation replacement=biomeReplacement(mapping.getKey()); - ResourceLocation current=replacement==null?mapping.getKey():replacement; + Identifier replacement=biomeReplacement(mapping.getKey()); + Identifier current=replacement==null?mapping.getKey():replacement; // Native19 chunk palettes use names in the data-pack registry, not // the old Forge numeric biome snapshot. Resolve only keys proved // by the actual vanilla bootstrap; vanilla still fixes chunk data. @@ -172,45 +209,50 @@ static boolean retiredPotion(ResourceLocation key) { else if(replacement!=null)throw new UnsafeRegistryUpgrade(null); } } - @SubscribeEvent public static void retiredPointsOfInterest(MissingMappingsEvent event) { + public static void retiredPointsOfInterest(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping mapping: event.getAllMappings(Registries.POINT_OF_INTEREST_TYPE)) { // Native PoiTypeRemoveFix3107 removes exactly these unpopulated // vanilla records. Resolve their retired numeric bindings only. - if(mapping.getKey().equals(ResourceLocation.parse("minecraft:unemployed")) - || mapping.getKey().equals(ResourceLocation.parse("minecraft:nitwit")))mapping.ignore(); + if(mapping.getKey().equals(Identifier.parse("minecraft:unemployed")) + || mapping.getKey().equals(Identifier.parse("minecraft:nitwit")))mapping.ignore(); } } - @SubscribeEvent public static void entities(MissingMappingsEvent event) { + public static void entities(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping> mapping:event.>getAllMappings(Registries.ENTITY_TYPE)) { - ResourceLocation replacement=entityReplacement(mapping.getKey()); + if (SPLIT_ENTITIES.contains(mapping.getKey()) + && !BuiltInRegistries.ENTITY_TYPE.containsKey(mapping.getKey())) { + mapping.ignore(); + continue; + } + Identifier replacement=entityReplacement(mapping.getKey()); if(replacement!=null)mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.ENTITY_TYPES.getValue(replacement))); } } - @SubscribeEvent public static void memories(MissingMappingsEvent event) { + public static void memories(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping> mapping:event.>getAllMappings(Registries.MEMORY_MODULE_TYPE)) - if(mapping.getKey().equals(ResourceLocation.parse("minecraft:opened_doors"))) - mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.containsKey(ResourceLocation.parse("minecraft:doors_to_close")),()->net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.getValue(ResourceLocation.parse("minecraft:doors_to_close")))); + if(mapping.getKey().equals(Identifier.parse("minecraft:opened_doors"))) + mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.containsKey(Identifier.parse("minecraft:doors_to_close")),()->net.minecraftforge.registries.ForgeRegistries.MEMORY_MODULE_TYPES.getValue(Identifier.parse("minecraft:doors_to_close")))); } - @SubscribeEvent public static void sounds(MissingMappingsEvent event) { + public static void sounds(MissingMappingsEvent event) { for(MissingMappingsEvent.Mapping mapping:event.getAllMappings(Registries.SOUND_EVENT)) { - if (RETIRED_PARROT_SOUNDS.contains(mapping.getKey()) + if ((RETIRED_PARROT_SOUNDS.contains(mapping.getKey()) || RETIRED_121_SOUNDS.contains(mapping.getKey())) && !BuiltInRegistries.SOUND_EVENT.containsKey(mapping.getKey())) { - // Old saves retain these numeric bindings after vanilla removed the imitations. + // Retire the exact numeric bindings for vanilla events no longer present. mapping.ignore(); continue; } if (mapping.getKey().equals(OLD_JUNGLE_AND_FOREST_MUSIC)) { // Vanilla 1.20 defines separate music for the two biomes. // Retire only the obsolete Forge numeric binding. - if (!BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.jungle")) - || !BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.forest"))) + if (!BuiltInRegistries.SOUND_EVENT.containsKey(Identifier.parse("minecraft:music.overworld.jungle")) + || !BuiltInRegistries.SOUND_EVENT.containsKey(Identifier.parse("minecraft:music.overworld.forest"))) throw new UnsafeRegistryUpgrade(null); mapping.ignore(); continue; } - ResourceLocation replacement=soundReplacement(mapping.getKey()); + Identifier replacement=soundReplacement(mapping.getKey()); if(replacement!=null)mapping.remap(requireMappingTarget(net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS.containsKey(replacement),()->net.minecraftforge.registries.ForgeRegistries.SOUND_EVENTS.getValue(replacement))); } } diff --git a/src/main/java/cyano/lootable/upgrade/NativeNbt.java b/src/main/java/cyano/lootable/upgrade/NativeNbt.java index 4631620..5506a1e 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeNbt.java +++ b/src/main/java/cyano/lootable/upgrade/NativeNbt.java @@ -2,15 +2,83 @@ import com.mojang.datafixers.DSL; import com.mojang.datafixers.DataFixer; +import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.MapLike; +import com.mojang.serialization.RecordBuilder; +import java.util.UUID; +import java.util.stream.Stream; +import net.minecraft.core.UUIDUtil; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; import net.minecraft.nbt.NbtOps; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; /** Native data-fixer entry point for explicit vanilla type references. */ public final class NativeNbt { + // Keep the actual tags available for validation and raw recovery. Typed + // field readers alone cannot distinguish a missing field from a bad type. + private static final MapCodec RAW_TAGS = new MapCodec<>() { + @Override public DataResult decode(DynamicOps ops, MapLike input) { + return CompoundTag.CODEC.parse(ops, ops.createMap(input.entries())); + } + + @Override public RecordBuilder encode(CompoundTag value, DynamicOps ops, RecordBuilder output) { + for (String key : value.keySet()) { + output.add(key, NbtOps.INSTANCE.convertTo(ops, value.get(key))); + } + return output; + } + + @Override public Stream keys(DynamicOps ops) { + return Stream.empty(); + } + }; + + public static CompoundTag rawTags(ValueInput input) { + return input.read(RAW_TAGS).orElseThrow(() -> new IllegalStateException("Missing or invalid corpse data")); + } + + public static void writeRawTags(ValueOutput output, CompoundTag tags) { + output.store(RAW_TAGS, tags); + } + private NativeNbt() { } public static CompoundTag update(DataFixer fixer, DSL.TypeReference type, CompoundTag input, int source, int target) { return (CompoundTag) fixer.update(type, new Dynamic<>(NbtOps.INSTANCE, input), source, target).getValue(); } + + /** Check the stored tag, without the numeric coercion of native getters. */ + public static boolean hasType(CompoundTag tag, String key, int type) { + Tag raw = tag.get(key); + return raw != null && (type == 99 ? raw instanceof NumericTag : raw.getId() == type); + } + + /** The new native list codec accepts mixed types; these saved lists do not. */ + public static boolean isListOf(Tag value, int type) { + return value instanceof ListTag list && list.stream().allMatch(entry -> entry.getId() == type); + } + + public static ListTag typedList(CompoundTag tag, String key, int type) { + Tag raw = tag.get(key); + return isListOf(raw, type) ? (ListTag) raw : new ListTag(); + } + + public static boolean hasUuid(CompoundTag tag, String key) { + return tag.getIntArray(key).filter(value -> value.length == 4).isPresent(); + } + + public static UUID readUuid(CompoundTag tag, String key) { + return tag.read(key, UUIDUtil.CODEC).orElseThrow(() -> new IllegalArgumentException("Invalid UUID " + key)); + } + + public static void writeUuid(CompoundTag tag, String key, UUID uuid) { + tag.store(key, UUIDUtil.CODEC, uuid); + } } diff --git a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java index fa66cb3..41efade 100644 --- a/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java +++ b/src/main/java/cyano/lootable/upgrade/NativeSafeFixer.java @@ -1,5 +1,10 @@ package cyano.lootable.upgrade; +import static cyano.lootable.upgrade.NativeNbt.hasType; +import static cyano.lootable.upgrade.NativeNbt.typedList; +import static cyano.lootable.upgrade.NativeNbt.isListOf; +import static cyano.lootable.upgrade.NativeNbt.writeUuid; + import com.mojang.datafixers.*; import com.mojang.serialization.Dynamic; import com.mojang.datafixers.schemas.Schema; @@ -8,9 +13,12 @@ import java.util.function.UnaryOperator; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; +import net.minecraft.util.LenientJsonParser; import net.minecraft.util.datafix.fixes.References; import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSyntaxException; /** Shields owned payloads while vanilla converts its own chunk and item schemas. */ final class NativeSafeFixer implements DataFixer { @@ -28,7 +36,7 @@ final class NativeSafeFixer implements DataFixer { CompoundTag result; if(kind.equals("chunk") && source<1493 && target>1493) { result=convert(type,raw,source,1493); - if(result.getCompound("Level").getBoolean("hasLegacyStructureData") && structures!=null)result=structures.apply(result); + if(result.getCompoundOrEmpty("Level").getBooleanOr("hasLegacyStructureData", false) && structures!=null)result=structures.apply(result); result=convert(type,result,1493,target); } else result=convert(type,raw,source,target); return new Dynamic<>(input.getOps(),(T)result); @@ -49,8 +57,8 @@ private CompoundTag convert(DSL.TypeReference type,CompoundTag input,int source, // pre-1.16 fixes on a damaged stamp otherwise replaces the seed and // dimensions. Shield this field alone; vanilla still fixes the level. if(kind.equals("world_gen_settings") && currentWorldSettings(copy))return updateCurrentWorldSettings(copy,source,target); - Tag currentSettings=kind.equals("level") && copy.contains("WorldGenSettings",10) - && currentWorldSettings(copy.getCompound("WorldGenSettings"))?copy.get("WorldGenSettings").copy():null; + Tag currentSettings=kind.equals("level") && hasType(copy, "WorldGenSettings", 10) + && currentWorldSettings(copy.getCompoundOrEmpty("WorldGenSettings"))?copy.get("WorldGenSettings").copy():null; if(currentSettings!=null)copy.remove("WorldGenSettings"); Tag currentBiomes=null; if(kind.equals("chunk")) { @@ -58,8 +66,8 @@ private CompoundTag convert(DSL.TypeReference type,CompoundTag input,int source, // A damaged stamp can replay the old 2D biome expansion on an // already-current 3D array. Protect that field alone so older // vanilla entities and containers still receive their own fixes. - if(source<2202 && target>=2202 && level.contains("Biomes",11) - && level.getIntArray("Biomes").length==1024) { + if(source<2202 && target>=2202 && hasType(level, "Biomes", 11) + && level.getIntArray("Biomes").orElseGet(() -> new int[0]).length==1024) { currentBiomes=level.get("Biomes").copy();level.remove("Biomes"); } // Vanilla discards typed Sections/TileEntities from incomplete old @@ -67,31 +75,33 @@ private CompoundTag convert(DSL.TypeReference type,CompoundTag input,int source, if(source<1493 && (nonempty(level,"Sections") || nonempty(level,"Entities") || nonempty(level,"TileEntities"))) { level.putBoolean("TerrainPopulated",true);level.putBoolean("LightPopulated",true); } - shieldList(level.getList(TerrainChunkData.entityKey(copy),10),source,tokens,level.getInt("xPos"),level.getInt("zPos"),true); + shieldList(typedList(level, TerrainChunkData.entityKey(copy), 10),source,tokens,level.getIntOr("xPos", 0),level.getIntOr("zPos", 0),true); } else if(kind.equals("entity_chunk")) { - int[] position=copy.getIntArray("Position"); - shieldList(copy.getList("Entities",10),source,tokens,position[0],position[1],true); + int[] position=copy.getIntArray("Position").orElseGet(() -> new int[0]); + shieldList(typedList(copy, "Entities", 10),source,tokens,position[0],position[1],true); } else if(kind.equals("entity") || kind.equals("entity_tree"))copy=shieldEntity(copy,source,tokens,0,0,false); else if(kind.equals("player")) { - CompoundTag vehicle=copy.getCompound("RootVehicle"); - if(vehicle.contains("Entity",10))vehicle.put("Entity",shieldEntity(vehicle.getCompound("Entity"),source,tokens,0,0,false)); - for(String shoulder:new String[]{"ShoulderEntityLeft","ShoulderEntityRight"})if(copy.contains(shoulder,10))copy.put(shoulder,shieldEntity(copy.getCompound(shoulder),source,tokens,0,0,false)); + CompoundTag vehicle=copy.getCompoundOrEmpty("RootVehicle"); + if(hasType(vehicle, "Entity", 10))vehicle.put("Entity",shieldEntity(vehicle.getCompoundOrEmpty("Entity"),source,tokens,0,0,false)); + for(String shoulder:new String[]{"ShoulderEntityLeft","ShoulderEntityRight"})if(hasType(copy, shoulder, 10))copy.put(shoulder,shieldEntity(copy.getCompoundOrEmpty(shoulder),source,tokens,0,0,false)); } - protectEggs(copy,source,tokens); + // The 4290 fix expects a JSON string even when a legacy writer + // carried a plain spawn-egg entity name past the old 1458 text fix. + prepareEggs(copy,source,tokens,source>=1458 && source<4290 && target>=4290); CompoundTag result; if(currentBiomes!=null && target>=2832) { // Cross the old 2D expansion on a copy without the already-3D // array, then restore it before native18 consumes it into section // biome palettes. A detached post-Level tag would lose this data. result=nativeConvert(type,copy,source,2202); - if(!result.contains("Level",10))throw new IllegalStateException("Native biome prefix lost the legacy Level payload"); - result.getCompound("Level").put("Biomes",currentBiomes);currentBiomes=null; + if(!hasType(result, "Level", 10))throw new IllegalStateException("Native biome prefix lost the legacy Level payload"); + result.getCompoundOrEmpty("Level").put("Biomes",currentBiomes);currentBiomes=null; result=nativeConvert(type,result,2202,target); } else result=nativeConvert(type,copy,source,target); result=(CompoundTag)restore(result,tokens); if(currentBiomes!=null) { - if(!result.contains("Level",10))throw new IllegalStateException("Protected legacy biomes require staged native height conversion"); - result.getCompound("Level").put("Biomes",currentBiomes); + 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)); 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"); @@ -101,13 +111,13 @@ else if(kind.equals("player")) { // the proven conversion version, not a recovered stale input stamp. // chunkVersion has already rejected genuinely future flat inputs. result.putInt("DataVersion",Math.max(source,target)); - CompoundTag level=TerrainChunkData.payload(result);ListTag entities=level.getList(TerrainChunkData.entityKey(result),10); + CompoundTag level=TerrainChunkData.payload(result);ListTag entities=typedList(level, TerrainChunkData.entityKey(result), 10); if(!UpgradeJournal.corpses(entities).isEmpty()) { - if(!level.contains("xPos",3) || !level.contains("zPos",3))throw new IllegalStateException("Saved terrain has no containing coordinates for corpse recovery"); - SavedCorpsePosition.prepare(entities,level.getInt("xPos"),level.getInt("zPos")); + if(!hasType(level, "xPos", 3) || !hasType(level, "zPos", 3))throw new IllegalStateException("Saved terrain has no containing coordinates for corpse recovery"); + SavedCorpsePosition.prepare(entities,level.getIntOr("xPos", 0),level.getIntOr("zPos", 0)); } } else if(kind.equals("entity_chunk")) { - int[] position=result.getIntArray("Position");SavedCorpsePosition.prepare(result.getList("Entities",10),position[0],position[1]); + int[] position=result.getIntArray("Position").orElseGet(() -> new int[0]);SavedCorpsePosition.prepare(typedList(result, "Entities", 10),position[0],position[1]); } if(kind.equals("chunk") || kind.equals("entity_chunk"))result.putInt("DataVersion",Math.max(source,target)); return result; @@ -117,21 +127,85 @@ private CompoundTag nativeConvert(DSL.TypeReference type,CompoundTag input,int s // The old item hook writes Chest/Banner/Pig names back while the // 704/705 schemas expect namespaced IDs. Cross that boundary on // copies before later item and flattening fixes run. - CompoundTag result=NativeNbt.update(delegate,type,input,source,703); - bridgeIdentifiers(result,type.typeName()); - return NativeNbt.update(delegate,type,result,705,target); + input=NativeNbt.update(delegate,type,input,source,703); + bridgeIdentifiers(input,type.typeName()); + source=705; + } + if (type == References.CHUNK && source < 1458 && target >= 1458 + && legacyNamedInventoryPresent(input)) { + int beforeNames = Math.max(source, 1456); + if (source < beforeNames) input = NativeNbt.update(delegate, type, input, source, beforeNames); + input = crossBlockEntityNameBoundary(input, beforeNames); + source = 1458; } return source LEGACY_NAMEABLE_INVENTORIES = Set.of( + "minecraft:brewing_stand", "minecraft:chest", "minecraft:trapped_chest", + "minecraft:dispenser", "minecraft:dropper", "minecraft:furnace", + "minecraft:hopper", "minecraft:shulker_box"); + + private static boolean legacyNamedInventoryPresent(CompoundTag chunk) { + return typedList(chunk.getCompoundOrEmpty("Level"), "TileEntities", 10).stream() + .map(CompoundTag.class::cast).anyMatch(NativeSafeFixer::hasLegacyNamedInventory); + } + + private static boolean hasLegacyNamedInventory(CompoundTag block) { + return LEGACY_NAMEABLE_INVENTORIES.contains(block.getStringOr("id", "")) + && !typedList(block, "Items", 10).isEmpty(); + } + + private CompoundTag crossBlockEntityNameBoundary(CompoundTag chunk, int source) { + // The 1458 block-entity name fix can serialize nested entity names + // through the old string codec after the entity name fix has run. + // Apply the same native fix to each item independently across this + // boundary, then restore the list to its verified block entity. + var blocks = typedList(chunk.getCompoundOrEmpty("Level"), "TileEntities", 10); + Map inventories = new LinkedHashMap<>(); + Map identities = new HashMap<>(); + for (int i = 0; i < blocks.size(); i++) { + CompoundTag block = blocks.getCompoundOrEmpty(i); + if (!hasLegacyNamedInventory(block)) continue; + ListTag items = new ListTag(); + for (Tag entry : typedList(block, "Items", 10)) { + CompoundTag original = (CompoundTag) entry; + CompoundTag updated = NativeNbt.update(delegate, References.ITEM_STACK, original.copy(), source, 1458); + if (!hasType(updated, "id", 8) || !Objects.equals(original.get("Slot"), updated.get("Slot")) + || !Objects.equals(original.get("Count"), updated.get("Count"))) { + throw new IllegalStateException("Native legacy block inventory name conversion changed a stack"); + } + items.add(updated); + } + block.remove("Items"); + inventories.put(i, items); + identities.put(i, block.copy()); + } + CompoundTag result = NativeNbt.update(delegate, References.CHUNK, chunk, source, 1458); + var converted = typedList(result.getCompoundOrEmpty("Level"), "TileEntities", 10); + if (converted.size() != blocks.size()) throw new IllegalStateException("Native name conversion changed block entities"); + for (var entry : inventories.entrySet()) { + CompoundTag block = converted.getCompoundOrEmpty(entry.getKey()); + CompoundTag original = identities.get(entry.getKey()); + for (String key : List.of("id", "x", "y", "z")) { + if (!Objects.equals(block.get(key), original.get(key))) { + throw new IllegalStateException("Native name conversion changed block entity identity"); + } + } + if (block.contains("Items")) throw new IllegalStateException("Native name conversion replaced a detached inventory"); + block.put("Items", entry.getValue()); + } + return result; + } private static boolean currentWorldSettings(CompoundTag nbt) { - if(!nbt.contains("seed",4) || !nbt.contains("dimensions",10))return false; - CompoundTag dimensions=nbt.getCompound("dimensions"); + if(!hasType(nbt, "seed", 4) || !hasType(nbt, "dimensions", 10))return false; + CompoundTag dimensions=nbt.getCompoundOrEmpty("dimensions"); if(dimensions.isEmpty())return false; - for(String key:dimensions.getAllKeys()) { - if(!dimensions.contains(key,10))return false; - CompoundTag dimension=dimensions.getCompound(key); - if(!(dimension.contains("type",8) || dimension.contains("type",10)) - || !dimension.contains("generator",10) || !dimension.getCompound("generator").contains("type",8))return false; + for(String key:dimensions.keySet()) { + if(!hasType(dimensions, key, 10))return false; + CompoundTag dimension=dimensions.getCompoundOrEmpty(key); + if(!(hasType(dimension, "type", 8) || hasType(dimension, "type", 10)) + || !hasType(dimension, "generator", 10) || !hasType(dimension.getCompoundOrEmpty("generator"), "type", 8))return false; } return true; } @@ -144,12 +218,11 @@ private CompoundTag updateCurrentWorldSettings(CompoundTag settings,int source,i settings.copy(),provenSource,target):settings; } private int entityChunkVersion(CompoundTag root) { - int[] position=root.getIntArray("Position"); + int[] position=root.getIntArray("Position").orElseGet(() -> new int[0]); Tag entities=root.get("Entities"); - if(!root.contains("Position",11) || position.length!=2 || !(entities instanceof ListTag) - || (((ListTag)entities).size()>0 && ((ListTag)entities).getElementType()!=10)) + if(!hasType(root, "Position", 11) || position.length!=2 || !isListOf(entities, 10)) throw new IllegalStateException("Malformed saved entity chunk; refusing unsafe construction"); - int version=root.contains("DataVersion",3)?root.getInt("DataVersion"):-1; + int version=hasType(root, "DataVersion", 3)?root.getIntOr("DataVersion", 0):-1; if(version>UpgradePreservation.MINECRAFT_VERSION) throw new IllegalStateException("Saved entity chunk comes from a newer Minecraft version"); // Terrain Sections/Biomes cannot identify the separate entity format. @@ -157,14 +230,14 @@ private int entityChunkVersion(CompoundTag root) { // the minimum native source even when an outer stamp was damaged. UpgradeJournal journal=preserved!=null?preserved:UpgradePreservation.journal(); for(UpgradeJournal.LocatedCorpse body:UpgradeJournal.corpses((ListTag)entities)) { - if(body.entity.getString("id").equals("lootablebodies:corpse") - && body.entity.contains(UpgradePreservation.VERSION,3) - && body.entity.getInt(UpgradePreservation.VERSION)==UpgradePreservation.DATA_VERSION) + if(body.entity.getStringOr("id", "").equals("lootablebodies:corpse") + && hasType(body.entity, UpgradePreservation.VERSION, 3) + && body.entity.getIntOr(UpgradePreservation.VERSION, 0)==UpgradePreservation.DATA_VERSION) version=Math.max(version,UpgradePreservation.MINECRAFT_VERSION); else if(journal!=null && CorpseIdentity.present(body.entity))try { CompoundTag original=journal.originalFor(CorpseIdentity.read(body.entity),position[0],position[1]); if(original!=null) { - int proven=UpgradePreservation.chunkSourceVersion(original.getCompound("SourceVersions")); + int proven=UpgradePreservation.chunkSourceVersion(original.getCompoundOrEmpty("SourceVersions")); if(proven>=512 && proven<=UpgradePreservation.MINECRAFT_VERSION)version=Math.max(version,proven); } }catch(IOException failure){throw new IllegalStateException("Entity chunk source context could not be verified",failure);} @@ -175,14 +248,14 @@ else if(journal!=null && CorpseIdentity.present(body.entity))try { private void bridgeIdentifiers(CompoundTag nbt,String kind) { switch(kind) { case "chunk": - CompoundTag level=nbt.getCompound("Level"); + CompoundTag level=nbt.getCompoundOrEmpty("Level"); bridgeList(level,"Entities","entity_tree");bridgeList(level,"TileEntities","block_entity");break; case "player": bridgeList(nbt,"Inventory","item_stack");bridgeList(nbt,"EnderItems","item_stack"); - bridgeField(nbt.getCompound("RootVehicle"),"Entity","entity_tree"); + bridgeField(nbt.getCompoundOrEmpty("RootVehicle"),"Entity","entity_tree"); bridgeField(nbt,"ShoulderEntityLeft","entity_tree");bridgeField(nbt,"ShoulderEntityRight","entity_tree");break; case "item_stack": - CompoundTag tag=nbt.getCompound("tag"); + CompoundTag tag=nbt.getCompoundOrEmpty("tag"); bridgeField(tag,"EntityTag","entity_tree");bridgeField(tag,"BlockEntityTag","block_entity");break; case "entity": case "entity_tree": renameIdentifier(nbt,References.ENTITY_TREE); @@ -190,46 +263,46 @@ private void bridgeIdentifiers(CompoundTag nbt,String kind) { for(String key:new String[]{"Equipment","ArmorItems","HandItems","Items","Inventory"})bridgeList(nbt,key,"item_stack"); for(String key:new String[]{"Item","FireworksItem","Potion","SaddleItem","ArmorItem","DecorItem"})bridgeField(nbt,key,"item_stack"); bridgeField(nbt,"TileEntityData","block_entity"); - for(Tag entry:nbt.getCompound("Offers").getList("Recipes",10)) + for(Tag entry:typedList(nbt.getCompoundOrEmpty("Offers"), "Recipes", 10)) for(String key:new String[]{"buy","buyB","sell"})bridgeField((CompoundTag)entry,key,"item_stack"); bridgeSpawner(nbt);break; case "block_entity": renameIdentifier(nbt,References.BLOCK_ENTITY); bridgeList(nbt,"Items","item_stack");bridgeField(nbt,"RecordItem","item_stack");bridgeSpawner(nbt);break; case "structure": - for(Tag entry:nbt.getList("entities",10))bridgeField((CompoundTag)entry,"nbt","entity_tree"); - for(Tag entry:nbt.getList("blocks",10))bridgeField((CompoundTag)entry,"nbt","block_entity");break; + for(Tag entry:typedList(nbt, "entities", 10))bridgeField((CompoundTag)entry,"nbt","entity_tree"); + for(Tag entry:typedList(nbt, "blocks", 10))bridgeField((CompoundTag)entry,"nbt","block_entity");break; default: break; } } private void renameIdentifier(CompoundTag nbt,DSL.TypeReference type) { - if(!nbt.contains("id",8))return; - CompoundTag id=new CompoundTag();id.putString("id",nbt.getString("id")); + if(!hasType(nbt, "id", 8))return; + CompoundTag id=new CompoundTag();id.putString("id",nbt.getStringOr("id", "")); CompoundTag fixed=cyano.lootable.upgrade.NativeNbt.update(delegate,type,id,703,705); - if(!fixed.contains("id",8))throw new IllegalStateException("Vanilla identifier conversion lost its type"); - nbt.putString("id",fixed.getString("id")); + if(!hasType(fixed, "id", 8))throw new IllegalStateException("Vanilla identifier conversion lost its type"); + nbt.putString("id",fixed.getStringOr("id", "")); } private void bridgeField(CompoundTag parent,String key,String kind) { - if(parent.contains(key,10))bridgeIdentifiers(parent.getCompound(key),kind); + if(hasType(parent, key, 10))bridgeIdentifiers(parent.getCompoundOrEmpty(key),kind); } private void bridgeList(CompoundTag parent,String key,String kind) { - for(Tag entry:parent.getList(key,10))bridgeIdentifiers((CompoundTag)entry,kind); + for(Tag entry:typedList(parent, key, 10))bridgeIdentifiers((CompoundTag)entry,kind); } private void bridgeSpawner(CompoundTag nbt) { bridgeField(nbt,"SpawnData","entity_tree"); - for(Tag entry:nbt.getList("SpawnPotentials",10))bridgeField((CompoundTag)entry,"Entity","entity_tree"); + for(Tag entry:typedList(nbt, "SpawnPotentials", 10))bridgeField((CompoundTag)entry,"Entity","entity_tree"); } - private static boolean nonempty(CompoundTag nbt,String key){return nbt.contains(key,9) && !nbt.getList(key,10).isEmpty();} + private static boolean nonempty(CompoundTag nbt,String key){return hasType(nbt, key, 9) && !typedList(nbt, key, 10).isEmpty();} private void shieldList(ListTag entities,int source,Tokens tokens,int x,int z,boolean saved) { - for(int i=0;iUpgradePreservation.DATA_VERSION)throw new IllegalStateException("This corpse was saved by a newer Lootable Bodies format"); - CompoundTag safe=raw.copy();boolean current=raw.getString("id").equals("lootablebodies:corpse") && raw.contains(UpgradePreservation.VERSION,3) && raw.getInt(UpgradePreservation.VERSION)==UpgradePreservation.DATA_VERSION; + if(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"); + CompoundTag safe=raw.copy();boolean current=raw.getStringOr("id", "").equals("lootablebodies:corpse") && hasType(raw, UpgradePreservation.VERSION, 3) && raw.getIntOr(UpgradePreservation.VERSION, 0)==UpgradePreservation.DATA_VERSION; if(CorpseIdentity.present(raw)) { - safe.putUUID("UUID",CorpseIdentity.read(raw));safe.remove("UUIDMost");safe.remove("UUIDLeast"); + writeUuid(safe, "UUID", CorpseIdentity.read(raw));safe.remove("UUIDMost");safe.remove("UUIDLeast"); } safe.putString("id","lootablebodies:corpse");safe.putInt(UpgradePreservation.VERSION,UpgradePreservation.DATA_VERSION); if(current)return safe; @@ -267,13 +340,13 @@ static CompoundTag stageCorpse(CompoundTag raw,CompoundTag context) { retainInventory(records,"Equipment",raw.get("Equipment"),context); retainInventory(records,"Aux",raw.get("Aux"),context); if(!custom) { - retainMirror(records,raw.getList("ArmorItems",10),new int[]{3,2,1,0},context); - retainMirror(records,raw.getList("HandItems",10),new int[]{4,5},context); + retainMirror(records,typedList(raw, "ArmorItems", 10),new int[]{3,2,1,0},context); + retainMirror(records,typedList(raw, "HandItems", 10),new int[]{4,5},context); } Tag recovery=raw.get("LootableBodiesUnresolved"); if(list(recovery))for(Tag value:(ListTag)recovery) { CompoundTag record=((CompoundTag)value).copy(); - if(record.contains("Source",8) && record.contains("Data",10) && !record.contains("SourceVersions") && (record.getString("Source").equals("Equipment") || record.getString("Source").equals("Aux")))record.put("SourceVersions",context.copy()); + if(hasType(record, "Source", 8) && hasType(record, "Data", 10) && !record.contains("SourceVersions") && (record.getStringOr("Source", "").equals("Equipment") || record.getStringOr("Source", "").equals("Aux")))record.put("SourceVersions",context.copy()); records.add(record); }else if(recovery!=null)retain(records,"Recovery",recovery,context); safe.put("Equipment",new ListTag());safe.remove("Aux");safe.remove("ArmorItems");safe.remove("HandItems"); @@ -281,11 +354,11 @@ static CompoundTag stageCorpse(CompoundTag raw,CompoundTag context) { // These are durable existing recovery records, not a transient envelope: // Anvil may save this compound before an entity reader runs. safe.remove("LootableBodiesUpgradePayload");safe.remove("LootableBodiesInventorySource"); - if(safe.contains("CustomName",8) && context.getInt("DataVersion")<1514) - safe.putString("CustomName",new JsonPrimitive(safe.getString("CustomName")).toString()); + if(hasType(safe, "CustomName", 8) && context.getIntOr("DataVersion", 0)<1514) + safe.putString("CustomName",new JsonPrimitive(safe.getStringOr("CustomName", "")).toString()); return safe; } - private static boolean list(Tag nbt){return nbt instanceof ListTag && (((ListTag)nbt).isEmpty() || ((ListTag)nbt).getElementType()==10);} + private static boolean list(Tag nbt){return isListOf(nbt, 10);} private static void retainInventory(ListTag records,String source,Tag data,CompoundTag context) { if(data==null)return; if(list(data))for(Tag entry:(ListTag)data)retain(records,source,entry,context); @@ -293,41 +366,62 @@ private static void retainInventory(ListTag records,String source,Tag data,Compo } private static void retainMirror(ListTag records,ListTag items,int[] slots,CompoundTag context) { for(int i=0;i(item.getAllKeys()))protectEggs(item.get(key),source,tokens); - }else if(value instanceof ListTag)for(Tag entry:(ListTag)value)protectEggs(entry,source,tokens); + if (bridgeLegacyNames) bridgeSpawnEggName(item); + for(String key:new ArrayList<>(item.keySet()))prepareEggs(item.get(key),source,tokens,bridgeLegacyNames); + }else if(value instanceof ListTag)for(Tag entry:(ListTag)value)prepareEggs(entry,source,tokens,bridgeLegacyNames); + } + private static void bridgeSpawnEggName(CompoundTag item) { + String id = item.getStringOr("id", ""); + if (!id.startsWith("minecraft:") || !(id.endsWith("_spawn_egg") || id.equals("minecraft:spawn_egg"))) return; + CompoundTag entity; + if (hasType(item, "count", 99)) { + entity = item.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:entity_data"); + } else if (hasType(item, "Count", 99)) { + entity = item.getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag"); + } else { + return; + } + if (!hasType(entity, "id", 8) || !(entity.get("CustomName") instanceof StringTag name)) return; + try { + LenientJsonParser.parse(name.value()); + } catch (JsonSyntaxException plainText) { + // Match the native fallback's literal value without sending + // invalid JSON through the native converter. The source is sealed. + entity.putString("CustomName", new JsonPrimitive(name.value()).toString()); + } } private Tag restore(Tag value,Tokens tokens) { if(value instanceof CompoundTag) { CompoundTag nbt=(CompoundTag)value; - if(nbt.contains(tokens.key,8)) { - String id=nbt.getString(tokens.key);Protected original=tokens.values.get(id);if(original==null)throw new IllegalStateException("Unknown preservation token"); + if(hasType(nbt, tokens.key, 8)) { + String id=nbt.getStringOr(tokens.key, "");Protected original=tokens.values.get(id);if(original==null)throw new IllegalStateException("Unknown preservation token"); tokens.restored.merge(id,1,Integer::sum); if(!original.egg)return original.data.copy(); nbt.remove(tokens.key); - if(nbt.contains("count",99)) { - CompoundTag components=nbt.getCompound("components"); + if(hasType(nbt, "count", 99)) { + CompoundTag components=nbt.getCompoundOrEmpty("components"); components.put("minecraft:entity_data",original.data.copy()); nbt.put("components",components); } else { - CompoundTag tag=nbt.getCompound("tag"); + CompoundTag tag=nbt.getCompoundOrEmpty("tag"); tag.put("EntityTag",original.data.copy()); nbt.put("tag",tag); } } - for(String key:new ArrayList<>(nbt.getAllKeys()))nbt.put(key,restore(nbt.get(key),tokens));return nbt; + for(String key:new ArrayList<>(nbt.keySet()))nbt.put(key,restore(nbt.get(key),tokens));return nbt; } if(value instanceof ListTag){ListTag list=(ListTag)value;for(int i=0;i=512 && version<=UpgradePreservation.MINECRAFT_VERSION && (!legacy || version<1451) && (!palette || version>=1451); @@ -347,15 +441,15 @@ private static int chunkVersion(CompoundTag chunk,int requested) { if(TerrainChunkData.payload(chunk)==chunk)return TerrainChunkData.flattenedVersion(chunk); boolean legacy=sections(chunk,"Blocks",7),palette=sections(chunk,"Palette",9); if(legacy && palette)throw new IllegalStateException("Mixed old and flattened sections cannot be converted safely"); - int source=chunk.contains("DataVersion",3)?chunk.getInt("DataVersion"):requested; - UpgradeJournal journal=UpgradePreservation.journal();CompoundTag level=chunk.getCompound("Level"); + int source=hasType(chunk, "DataVersion", 3)?chunk.getIntOr("DataVersion", 0):requested; + UpgradeJournal journal=UpgradePreservation.journal();CompoundTag level=chunk.getCompoundOrEmpty("Level"); int preserved=-1; - for(UpgradeJournal.LocatedCorpse located:UpgradeJournal.corpses(level.getList("Entities",10))) { + for(UpgradeJournal.LocatedCorpse located:UpgradeJournal.corpses(typedList(level, "Entities", 10))) { try { - CompoundTag original=journal==null || !CorpseIdentity.present(located.entity)?null:journal.originalFor(CorpseIdentity.read(located.entity),level.getInt("xPos"),level.getInt("zPos")); + CompoundTag original=journal==null || !CorpseIdentity.present(located.entity)?null:journal.originalFor(CorpseIdentity.read(located.entity),level.getIntOr("xPos", 0),level.getIntOr("zPos", 0)); if(original!=null) { - CompoundTag context=original.getCompound("SourceVersions");int candidate=UpgradePreservation.chunkSourceVersion(context); - if(!context.getBoolean("Ambiguous") && compatible(candidate,legacy,palette)) { + CompoundTag context=original.getCompoundOrEmpty("SourceVersions");int candidate=UpgradePreservation.chunkSourceVersion(context); + if(!context.getBooleanOr("Ambiguous", false) && compatible(candidate,legacy,palette)) { if(preserved>=0 && preserved!=candidate)throw new IllegalStateException("Conflicting preserved chunk versions"); preserved=candidate; } @@ -367,8 +461,8 @@ private static int chunkVersion(CompoundTag chunk,int requested) { if(preserved>=0)source=preserved; else if(journal!=null) { CompoundTag world=journal.worldVersions();int candidate=UpgradePreservation.chunkSourceVersion(world); - if(!compatible(candidate,legacy,palette))candidate=world.getInt("WorldDataVersion"); - if(!compatible(candidate,legacy,palette) && world.getCompound("WorldVersion").contains("Id",3))candidate=world.getCompound("WorldVersion").getInt("Id"); + if(!compatible(candidate,legacy,palette))candidate=world.getIntOr("WorldDataVersion", 0); + if(!compatible(candidate,legacy,palette) && hasType(world.getCompoundOrEmpty("WorldVersion"), "Id", 3))candidate=world.getCompoundOrEmpty("WorldVersion").getIntOr("Id", 0); if(compatible(candidate,legacy,palette) && (!compatible(source,legacy,palette) || candidate512)throw new IllegalStateException("Saved corpse passenger tree is too deep"); if(UpgradeJournal.corpse(entity))repair(entity,x,z,root); - for(Tag rider:entity.getList("Passengers",10))visit((CompoundTag)rider,x,z,false,depth+1); + for(Tag rider:typedList(entity, "Passengers", 10))visit((CompoundTag)rider,x,z,false,depth+1); } private static void repair(CompoundTag entity,int x,int z,boolean root) { Tag raw=entity.get("Pos");ListTag list=raw instanceof ListTag?(ListTag)raw:null; @@ -25,7 +28,7 @@ private static void repair(CompoundTag entity,int x,int z,boolean root) { double[] position=new double[3]; for(int axis=0;axis<3;axis++) { Tag value=list!=null && axis 127 || (section.contains("block_states")&&!section.contains("block_states",10)) || - (section.contains("biomes")&&!section.contains("biomes",10))) + if((!hasType(section, "Y", 1)&&!hasType(section, "Y", 3)) || section.getIntOr("Y", 0) < -128 || section.getIntOr("Y", 0) > 127 || (section.contains("block_states")&&!hasType(section, "block_states", 10)) || + (section.contains("biomes")&&!hasType(section, "biomes", 10))) throw new IllegalStateException("Unreadable flattened terrain section"); } flattenedVersion(root); return root; } - static String entityKey(CompoundTag root) { return root.contains("Level",10)?"Entities":"entities"; } + static String entityKey(CompoundTag root) { return hasType(root, "Level", 10)?"Entities":"entities"; } static int flattenedVersion(CompoundTag root) { - int stamp=root.contains("DataVersion",3)?root.getInt("DataVersion"):-1; + int stamp=hasType(root, "DataVersion", 3)?root.getIntOr("DataVersion", 0):-1; if(stamp>UpgradePreservation.MINECRAFT_VERSION)throw new IllegalStateException("Terrain was saved by a newer Minecraft version"); // Valid root coordinates/sections prove the 2842 schema minimum, // not the current game release. Let genuine later native fixes run. @@ -48,7 +52,7 @@ static int flattenedVersion(CompoundTag root) { private static void compounds(CompoundTag parent,String key) { if(!parent.contains(key))return; Tag value=parent.get(key); - if(!(value instanceof ListTag) || (((ListTag)value).size()>0 && ((ListTag)value).getElementType()!=10)) + if(!isListOf(value, 10)) throw new IllegalStateException("Unreadable terrain list: "+key); } } diff --git a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java index cecf39b..7193479 100644 --- a/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java +++ b/src/main/java/cyano/lootable/upgrade/UpgradeJournal.java @@ -1,5 +1,9 @@ package cyano.lootable.upgrade; +import static cyano.lootable.upgrade.NativeNbt.hasType; +import static cyano.lootable.upgrade.NativeNbt.typedList; +import static cyano.lootable.upgrade.NativeNbt.isListOf; + import java.io.*; import java.nio.channels.FileChannel; import java.nio.file.*; @@ -48,8 +52,8 @@ static UpgradeJournal scan(Path world,Path levelHeader)throws IOException { UpgradeJournal journal=new UpgradeJournal(world,levelHeader); journal.inspect(); for(Path original:journal.active.values()) { - CompoundTag corpse=journal.readSnapshot(original).getCompound("Entity"); - if(corpse.contains(UpgradePreservation.VERSION,3) && corpse.getInt(UpgradePreservation.VERSION)>UpgradePreservation.DATA_VERSION) + CompoundTag corpse=journal.readSnapshot(original).getCompoundOrEmpty("Entity"); + if(hasType(corpse, UpgradePreservation.VERSION, 3) && corpse.getIntOr(UpgradePreservation.VERSION, 0)>UpgradePreservation.DATA_VERSION) throw new IOException("This world contains a newer Lootable Bodies corpse format; originals have been preserved"); } return journal; @@ -64,22 +68,22 @@ void validateEntityChunks(com.mojang.datafixers.DataFixer fixer) throws IOExcept List originals=new ArrayList<>(active.values());originals.sort(Comparator.comparing(Path::toString)); for(Path original:originals) { CompoundTag snapshot=readSnapshot(original); - Path relative=Paths.get(snapshot.getString("Region")); + Path relative=Paths.get(snapshot.getStringOr("Region", "")); if(relative.getParent()==null || !relative.getParent().getFileName().toString().equals("entities"))continue; - String key=relative.toString()+":"+snapshot.getString("OriginalChunk"); + String key=relative.toString()+":"+snapshot.getStringOr("OriginalChunk", ""); if(!checked.add(key))continue; Path region=world.resolve(relative).normalize(); if(relative.isAbsolute() || !region.startsWith(world))throw new IOException("Entity region is outside the world"); - String expected=snapshot.getString("RegionDigest"); + String expected=snapshot.getStringOr("RegionDigest", ""); if(!expected.equals(fileDigest(region)))throw new IOException("Entity region changed before conversion validation: "+relative); - byte[] bytes=Files.readAllBytes(chunks.resolve(snapshot.getString("OriginalChunk")+".bin")); - InputStream stream=snapshot.getByte("OriginalCompression")==1?new GZIPInputStream(new ByteArrayInputStream(bytes)): + byte[] bytes=Files.readAllBytes(chunks.resolve(snapshot.getStringOr("OriginalChunk", "")+".bin")); + InputStream stream=snapshot.getByteOr("OriginalCompression", (byte) 0)==1?new GZIPInputStream(new ByteArrayInputStream(bytes)): new InflaterInputStream(new ByteArrayInputStream(bytes)); CompoundTag root; try(DataInputStream input=new DataInputStream(stream)){root=NbtIo.read(input,NbtAccounter.create(MAX_NBT));} try { NativeNbt.update(fixer,net.minecraft.util.datafix.fixes.References.ENTITY_CHUNK,root, - root.contains("DataVersion",3)?root.getInt("DataVersion"):-1,UpgradePreservation.MINECRAFT_VERSION); + hasType(root, "DataVersion", 3)?root.getIntOr("DataVersion", 0):-1,UpgradePreservation.MINECRAFT_VERSION); } catch(RuntimeException unsafe) { throw new IOException("Cannot safely convert preserved entity chunk before world loading: "+relative,unsafe); } @@ -92,7 +96,7 @@ public CompoundTag originalFor(UUID id,int chunkX,int chunkZ) throws IOException Path path=active.get(id); if(path==null) return null; CompoundTag original=readSnapshot(path); - return original.getInt("ChunkX")==chunkX && original.getInt("ChunkZ")==chunkZ ? original : null; + return original.getIntOr("ChunkX", 0)==chunkX && original.getIntOr("ChunkZ", 0)==chunkZ ? original : null; } CompoundTag originalFor(UUID id) throws IOException { @@ -101,7 +105,7 @@ CompoundTag originalFor(UUID id) throws IOException { } static boolean corpse(CompoundTag entity) { - String id=entity.getString("id"); + String id=entity.getStringOr("id", ""); return id.equals("lootablebodies:corpse") || id.equals("lootablebodies.Corpse"); } @@ -111,14 +115,14 @@ static final class LocatedCorpse { } static List corpses(ListTag roots) { List result=new ArrayList<>(); - for(int root=0;root result) { if(path.length>512)throw new IllegalArgumentException("Corpse passenger tree is too deep"); if(corpse(entity))result.add(new LocatedCorpse(entity,root,path)); - ListTag passengers=entity.getList("Passengers",10); - for(int i=0;i readIndex() throws IOException { Path index=directory.resolve("index.nbt"); if(!Files.exists(index)) return records; CompoundTag envelope=readCompressed(index); - byte[] payload=envelope.getByteArray("Payload"); - if(!envelope.contains("Format",3) || envelope.getInt("Format")!=FORMAT - || !envelope.contains("Digest",8) || !envelope.contains("Payload",7) - || !digest(payload).equals(envelope.getString("Digest"))) + byte[] payload=envelope.getByteArray("Payload").orElseGet(() -> new byte[0]); + if(!hasType(envelope, "Format", 3) || envelope.getIntOr("Format", 0)!=FORMAT + || !hasType(envelope, "Digest", 8) || !hasType(envelope, "Payload", 7) + || !digest(payload).equals(envelope.getStringOr("Digest", ""))) throw new IOException("Lootable Bodies upgrade index checksum mismatch"); CompoundTag nbt; try(DataInputStream input=new DataInputStream(new GZIPInputStream(new ByteArrayInputStream(payload)))) { nbt=NbtIo.read(input,NbtAccounter.create(MAX_NBT)); } - if(!nbt.contains("Format",3) || nbt.getInt("Format")!=FORMAT || !compoundList(nbt.get("Regions"))) + if(!hasType(nbt, "Format", 3) || nbt.getIntOr("Format", 0)!=FORMAT || !compoundList(nbt.get("Regions"))) throw new IOException("Invalid Lootable Bodies upgrade index"); - ListTag regions=nbt.getList("Regions",10); + ListTag regions=typedList(nbt, "Regions", 10); for(int i=0;i0 && list.getElementType()!=8) throw new IOException("Invalid recovery snapshot list"); - if(records.put(record.getString("Path"),record)!=null) throw new IOException("Duplicate recovery region record"); - for(int j=0;j priorLevels=new HashMap<>(); Map matchingChunks=new HashMap<>(); if(previous!=null) { - ListTag originals=previous.getList("Snapshots",8); + ListTag originals=typedList(previous, "Snapshots", 8); for(int i=0;iUpgradePreservation.chunkSourceVersion(selected.getCompound("SourceVersions")))prior.put(key,original); + if(selected==null || UpgradePreservation.chunkSourceVersion(versions)>UpgradePreservation.chunkSourceVersion(selected.getCompoundOrEmpty("SourceVersions")))prior.put(key,original); } } try(RandomAccessFile input=new RandomAccessFile(file.toFile(),"r")) { @@ -250,20 +254,20 @@ private CompoundTag scanRegion(Path file,String relative,String hash,CompoundTag int x=(int)rx*32+(i%32),z=(int)rz*32+(i/32); CompoundTag level; if(entityRegion) { - int[] position=chunk.getIntArray("Position"); - if(!chunk.contains("Position",11) || position.length!=2 || position[0]!=x || position[1]!=z) + int[] position=chunk.getIntArray("Position").orElseGet(() -> new int[0]); + if(!hasType(chunk, "Position", 11) || position.length!=2 || position[0]!=x || position[1]!=z) throw new IOException("Entity chunk Position mismatch in "+relative); level=chunk; } else { level=TerrainChunkData.payload(chunk); - if(!level.contains("xPos",3) || !level.contains("zPos",3) || level.getInt("xPos")!=x || level.getInt("zPos")!=z) + if(!hasType(level, "xPos", 3) || !hasType(level, "zPos", 3) || level.getIntOr("xPos", 0)!=x || level.getIntOr("zPos", 0)!=z) throw new IOException("Chunk coordinate mismatch in "+relative); } String entityKey=entityRegion?"Entities":TerrainChunkData.entityKey(chunk); if(entityRegion && !level.contains(entityKey))throw new IOException("Missing native entity list in "+relative); if(!level.contains(entityKey)) continue; if(!compoundList(level.get(entityKey))) throw new IOException("Unreadable entity list in "+relative); - ListTag entities=level.getList(entityKey,10); + ListTag entities=typedList(level, entityKey, 10); String originalChunk=digest(payload); for(LocatedCorpse located:corpses(entities)) { CompoundTag entity=located.entity;int e=located.root; @@ -278,11 +282,11 @@ private CompoundTag scanRegion(Path file,String relative,String hash,CompoundTag snapshot.putInt("ChunkX",x);snapshot.putInt("ChunkZ",z);snapshot.put("Entity",entity.copy()); CompoundTag versions=sourceVersions(chunk,worldVersions,entity),priorSnapshot=prior.get(chunkKey(x,z)); if(priorSnapshot!=null) { - String savedChunk=priorSnapshot.getString("OriginalChunk"); + String savedChunk=priorSnapshot.getStringOr("OriginalChunk", ""); CompoundTag oldLevel=priorLevels.get(savedChunk); if(oldLevel==null) { byte[] originalBytes=Files.readAllBytes(chunks.resolve(savedChunk+".bin")); - InputStream originalStream=priorSnapshot.getByte("OriginalCompression")==1?new GZIPInputStream(new ByteArrayInputStream(originalBytes)):new InflaterInputStream(new ByteArrayInputStream(originalBytes)); + InputStream originalStream=priorSnapshot.getByteOr("OriginalCompression", (byte) 0)==1?new GZIPInputStream(new ByteArrayInputStream(originalBytes)):new InflaterInputStream(new ByteArrayInputStream(originalBytes)); try(DataInputStream originalInput=new DataInputStream(originalStream)){oldLevel=comparisonPayload(NbtIo.read(originalInput,NbtAccounter.create(MAX_NBT)),entityRegion);} priorLevels.put(savedChunk,oldLevel); } @@ -290,7 +294,7 @@ private CompoundTag scanRegion(Path file,String relative,String hash,CompoundTag // Never substitute old entities, Position or unknown payloads. long key=chunkKey(x,z);Boolean same=matchingChunks.get(key); if(same==null){same=sameNbt(oldLevel,comparisonPayload(chunk,entityRegion));matchingChunks.put(key,same);} - if(same)versions.putInt("VerifiedChunkDataVersion",UpgradePreservation.chunkSourceVersion(priorSnapshot.getCompound("SourceVersions"))); + if(same)versions.putInt("VerifiedChunkDataVersion",UpgradePreservation.chunkSourceVersion(priorSnapshot.getCompoundOrEmpty("SourceVersions"))); } snapshot.put("SourceVersions",versions); byte[] original=compressed(snapshot);String filename=digest(original)+".nbt";Path destination=snapshotPath(filename); @@ -318,7 +322,7 @@ private static CompoundTag comparisonPayload(CompoundTag chunk,boolean entityReg private CompoundTag readWorldVersions() throws IOException { CompoundTag context=new CompoundTag(); if(Files.isRegularFile(levelHeader)) { - CompoundTag root=readCompressed(levelHeader),data=root.getCompound("Data"); + CompoundTag root=readCompressed(levelHeader),data=root.getCompoundOrEmpty("Data"); if(data.contains("DataVersion")) context.put("WorldDataVersion",data.get("DataVersion").copy()); if(data.contains("Version"))context.put("WorldVersion",data.get("Version").copy()); if(root.contains("FML")) context.put("WorldFML",root.get("FML").copy()); @@ -333,12 +337,12 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co CompoundTag context=world.copy(); if(chunk.contains("DataVersion")) context.put("ChunkDataVersion",chunk.get("DataVersion").copy()); if(chunk.contains("ForgeDataVersion")) context.put("ForgeDataVersion",chunk.get("ForgeDataVersion").copy()); - int version=chunk.contains("DataVersion",3) ? chunk.getInt("DataVersion") : -1; - if(version<512 || version>UpgradePreservation.MINECRAFT_VERSION) version=world.contains("WorldDataVersion",3) ? world.getInt("WorldDataVersion") : -1; + int version=hasType(chunk, "DataVersion", 3) ? chunk.getIntOr("DataVersion", 0) : -1; + if(version<512 || version>UpgradePreservation.MINECRAFT_VERSION) version=hasType(world, "WorldDataVersion", 3) ? world.getIntOr("WorldDataVersion", 0) : -1; // Dotted IDs and our own per-corpse markers identify known source families. // A lowercase ID alone cannot distinguish original 1.11 and 1.12 saves. - boolean legacy=entity.getString("id").equals("lootablebodies.Corpse"); - int marker=entity.contains(UpgradePreservation.VERSION,3) ? entity.getInt(UpgradePreservation.VERSION) : 0; + boolean legacy=entity.getStringOr("id", "").equals("lootablebodies.Corpse"); + int marker=hasType(entity, UpgradePreservation.VERSION, 3) ? entity.getIntOr(UpgradePreservation.VERSION, 0) : 0; if(marker==1) version=922; else if(marker==2)version=1343; else if(marker==3)version=1631; @@ -350,16 +354,18 @@ private static CompoundTag sourceVersions(CompoundTag chunk,CompoundTag world,Co else if(marker==9)version=3337; else if(marker==10)version=3465; else if(marker==11)version=3839; - else if(marker==12)version=UpgradePreservation.MINECRAFT_VERSION; + else if(marker==12)version=3955; + else if(marker==13)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); int family=-1; - ListTag mods=UpgradePreservation.modList(world.getCompound("WorldFML")); + ListTag mods=UpgradePreservation.modList(world.getCompoundOrEmpty("WorldFML")); for(int i=0;i 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); + converted.remove("__context"); + return converted; + } + + /** Adapt the old Forge capability payload to the extra codec field. */ + public static CompoundTag upgradeLevelCapabilities(CompoundTag input) { + if (!hasType(input, "data", 10)) return 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; + } 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. + return input; + } + CompoundTag result = input.copy(); + CompoundTag wrapper = new CompoundTag(); + wrapper.put("data", result.getCompoundOrEmpty("data")); + result.put("data", wrapper); + return result; + } + public static CompoundTag chunkContext(CompoundTag chunk){return chunk;} public static void clearChunkContext(CompoundTag chunk){chunk.remove("LootableBodiesChunkContext");} public static CompoundTag restore(CompoundTag input) { - if(input.contains(PAYLOAD,10))return input.getCompound(PAYLOAD).copy();return input; + if(hasType(input, PAYLOAD, 10))return input.getCompoundOrEmpty(PAYLOAD).copy();return input; + } + public static void clearTransient(ValueOutput output) { + output.discard(PAYLOAD); + output.discard(ITEMS_PENDING); } - public static void clearTransient(CompoundTag output){output.remove(PAYLOAD);output.remove(ITEMS_PENDING);} static int chunkSourceVersion(CompoundTag versions) { - if(versions.contains("VerifiedChunkDataVersion",3))return versions.getInt("VerifiedChunkDataVersion"); - int version=versions.getInt("DataVersion"),family=-1; - ListTag mods=modList(versions.getCompound("WorldFML")); + if(hasType(versions, "VerifiedChunkDataVersion", 3))return versions.getIntOr("VerifiedChunkDataVersion", 0); + int version=versions.getIntOr("DataVersion", 0),family=-1; + ListTag mods=modList(versions.getCompoundOrEmpty("WorldFML")); for(int m=0;m=512 && writer<=MINECRAFT_VERSION) { // Forge can save the current level header before scanning old chunks. // A preserved owned writer marker proves this chunk is at least that @@ -93,7 +140,7 @@ static ListTag modList(CompoundTag fml) { // retained header intact and do not select a conflicting duplicate. if(fml.contains("ModList") && fml.contains("LoadingModList") && !fml.get("ModList").equals(fml.get("LoadingModList")))return new ListTag(); - return fml.getList(fml.contains("ModList")?"ModList":"LoadingModList",10); + return typedList(fml, fml.contains("ModList")?"ModList":"LoadingModList", 10); } } diff --git a/src/main/resources/META-INF/coremods.json b/src/main/resources/META-INF/coremods.json deleted file mode 100644 index 851135b..0000000 --- a/src/main/resources/META-INF/coremods.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "lootablebodies_preservation": "coremods/early-world-preservation.js" -} diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml index 7df573f..d87a826 100644 --- a/src/main/resources/META-INF/mods.toml +++ b/src/main/resources/META-INF/mods.toml @@ -1,5 +1,5 @@ modLoader="javafml" -loaderVersion="[52,)" +loaderVersion="[61,)" license="${mod_license}" [[mods]] modId="lootablebodies" @@ -11,12 +11,12 @@ description="Saves your items on a lootable corpse when you die." [[dependencies.lootablebodies]] modId="forge" mandatory=true -versionRange="[52.1.20,)" +versionRange="[61.2.4,)" ordering="NONE" side="BOTH" [[dependencies.lootablebodies]] modId="minecraft" mandatory=true -versionRange="[1.21.1]" +versionRange="[1.21.11]" ordering="NONE" side="BOTH" diff --git a/src/main/resources/coremods/early-world-preservation.js b/src/main/resources/coremods/early-world-preservation.js deleted file mode 100644 index 1d65746..0000000 --- a/src/main/resources/coremods/early-world-preservation.js +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: LGPL-2.1-only -// Preserve originals before the shared native level-data reader and -// retain our own field before Forge writes the world header. -function initializeCoreMod() { - var Opcodes = Java.type('org.objectweb.asm.Opcodes'); - var InsnList = Java.type('org.objectweb.asm.tree.InsnList'); - var InsnNode = Java.type('org.objectweb.asm.tree.InsnNode'); - var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); - var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode'); - var MethodNode = Java.type('org.objectweb.asm.tree.MethodNode'); - var save = 'net/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess'; - var hook = 'cyano/lootable/upgrade/EarlyWorldPreservation'; - var readDesc = '(L' + save + ';Z)V'; - var nativeReadDesc = '()Lcom/mojang/serialization/Dynamic;'; - var dedicatedReadDesc = '(L' + save + ';Z)Lcom/mojang/serialization/Dynamic;'; - var writeDesc = '(L' + save + ';Lnet/minecraft/nbt/CompoundTag;)V'; - var attachedName = 'lootablebodies$preservationAttached'; - var attachedFlags = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC; - var fixer = 'net/minecraft/util/datafix/DataFixers'; - var fixerType = 'Lcom/mojang/datafixers/DataFixer;'; - var wrapper = 'cyano/lootable/upgrade/UpgradePreservation'; - var wrapperDesc = '(' + fixerType + ')' + fixerType; - return { - lootablebodies_dedicated_read: { - target: {type: 'CLASS', name: 'net.minecraft.server.Main'}, - transformer: function(node) { - if (String(node.name) !== 'net/minecraft/server/Main') throw new Error('Unexpected dedicated server class'); - var main = null; - for (var m = 0; m < node.methods.size(); m++) { - var method = node.methods.get(m); - if (String(method.name) !== 'main' || String(method.desc) !== '([Ljava/lang/String;)V' || - (method.access & Opcodes.ACC_STATIC) === 0) continue; - if (main !== null) throw new Error('Ambiguous dedicated server entry point'); - main = method; - } - if (main === null) throw new Error('Missing dedicated server entry point'); - var first = null, fallback = null, guarded = 0; - for (var insn = main.instructions.getFirst(); insn !== null; insn = insn.getNext()) { - if (!(insn instanceof MethodInsnNode)) continue; - if (String(insn.owner) === hook && String(insn.name) === 'readDedicatedWorld' && - String(insn.desc) === dedicatedReadDesc && insn.getOpcode() === Opcodes.INVOKESTATIC) guarded++; - if (String(insn.owner) !== save || String(insn.desc) !== nativeReadDesc || - insn.getOpcode() !== Opcodes.INVOKEVIRTUAL) continue; - if (String(insn.name) === 'getDataTag' && first === null) first = insn; - else if (String(insn.name) === 'getDataTagFallback' && fallback === null) fallback = insn; - else throw new Error('Unexpected or duplicate dedicated world reader'); - } - if (guarded === 2 && first === null && fallback === null) return node; - if (guarded !== 0 || first === null || fallback === null) throw new Error('Dedicated world readers are missing or ambiguous'); - [first, fallback].forEach(function(reader, index) { - main.instructions.insertBefore(reader, new InsnNode(index === 0 ? Opcodes.ICONST_0 : Opcodes.ICONST_1)); - main.instructions.set(reader, new MethodInsnNode( - Opcodes.INVOKESTATIC, hook, 'readDedicatedWorld', dedicatedReadDesc, false)); - }); - main.maxStack += 1; - return node; - } - }, - lootablebodies_fixer: { - target: {type: 'CLASS', name: fixer.replace(/\//g, '.')}, - transformer: function(node) { - if (String(node.name) !== fixer) throw new Error('Unexpected native data fixer class'); - var accessor = null; - for (var m = 0; m < node.methods.size(); m++) { - var method = node.methods.get(m); - if (String(method.name) !== 'getDataFixer' || String(method.desc) !== '()' + fixerType || - (method.access & Opcodes.ACC_STATIC) === 0) continue; - if (accessor !== null) throw new Error('Ambiguous native data fixer accessor'); - accessor = method; - } - if (accessor === null) throw new Error('Missing native data fixer accessor'); - var returns = [], wraps = 0; - for (var insn = accessor.instructions.getFirst(); insn !== null; insn = insn.getNext()) { - if (insn.getOpcode() === Opcodes.ARETURN) returns.push(insn); - if (insn instanceof MethodInsnNode && String(insn.owner) === wrapper) { - if (String(insn.name) !== 'wrapper' || String(insn.desc) !== wrapperDesc) - throw new Error('Unexpected data fixer wrapper signature'); - wraps++; - } - } - if (returns.length !== 1 || wraps > 1) throw new Error('Ambiguous native data fixer return'); - if (wraps === 0) - accessor.instructions.insertBefore(returns[0], new MethodInsnNode( - Opcodes.INVOKESTATIC, wrapper, 'wrapper', wrapperDesc, false)); - return node; - } - }, - lootablebodies_preservation: { - target: {type: 'CLASS', name: save.replace(/\//g, '.')}, - transformer: function(node) { - if (String(node.name) !== save) throw new Error('Unexpected native save class'); - var read = null, readCount = 0, writes = [], oldRead = 0, oldWrite = 0, attached = 0; - for (var m = 0; m < node.methods.size(); m++) { - var method = node.methods.get(m); - if (String(method.name) === attachedName && String(method.desc) === '()Z' && (method.access & attachedFlags) === attachedFlags) attached++; - if ((String(method.name) === 'getDataTag' || String(method.name) === 'm_246049_') && - String(method.desc) === '(Z)Lcom/mojang/serialization/Dynamic;' && - (method.access & Opcodes.ACC_STATIC) === 0) { - read = method; readCount++; - } - for (var instruction = method.instructions.getFirst(); instruction !== null; instruction = instruction.getNext()) { - if (!(instruction instanceof MethodInsnNode)) continue; - if (String(instruction.owner) === hook) { - if (String(instruction.name) === 'beforeRead' && String(instruction.desc) === readDesc) oldRead++; - else if (String(instruction.name) === 'beforeWrite' && String(instruction.desc) === writeDesc) oldWrite++; - else throw new Error('Unexpected preservation hook signature'); - } - if (String(instruction.owner) === 'net/minecraftforge/common/ForgeHooks' && - String(instruction.name) === 'writeAdditionalLevelSaveData' && - String(instruction.desc) === '(Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/nbt/CompoundTag;)V') { - if ((method.access & Opcodes.ACC_STATIC) !== 0) throw new Error('Native save method became static'); - writes.push({method: method, instruction: instruction}); - } - } - } - if (readCount !== 1 || writes.length !== 1) throw new Error('Native early read/write boundary is missing or ambiguous'); - if (oldRead === 1 && oldWrite === 1 && attached === 1) return node; - if (oldRead !== 0 || oldWrite !== 0 || attached !== 0) throw new Error('Partial or duplicate preservation hooks'); - var beforeRead = new InsnList(); - beforeRead.add(new VarInsnNode(Opcodes.ALOAD, 0)); - beforeRead.add(new VarInsnNode(Opcodes.ILOAD, 1)); - beforeRead.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, 'beforeRead', readDesc, false)); - read.instructions.insert(beforeRead); - read.maxStack = Math.max(read.maxStack, 2); - // Existing stack: WorldData, CompoundTag. Keep those operands - // for Forge, while passing this save access and a duplicate tag. - var beforeWrite = new InsnList(); - beforeWrite.add(new InsnNode(Opcodes.DUP)); - beforeWrite.add(new VarInsnNode(Opcodes.ALOAD, 0)); - beforeWrite.add(new InsnNode(Opcodes.SWAP)); - beforeWrite.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook, 'beforeWrite', writeDesc, false)); - writes[0].method.instructions.insertBefore(writes[0].instruction, beforeWrite); - writes[0].method.maxStack = Math.max(writes[0].method.maxStack, 4); - // The native engine catches ordinary script exceptions. Serial - // setup must require this method before a world can be opened. - var marker = new MethodNode(attachedFlags, attachedName, '()Z', null, null); - marker.instructions.add(new InsnNode(Opcodes.ICONST_1)); - marker.instructions.add(new InsnNode(Opcodes.IRETURN)); - marker.maxStack = 1; marker.maxLocals = 0; - node.methods.add(marker); - return node; - } - } - }; -} diff --git a/src/main/resources/lootablebodies.mixins.json b/src/main/resources/lootablebodies.mixins.json new file mode 100644 index 0000000..0ee9e75 --- /dev/null +++ b/src/main/resources/lootablebodies.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8.7", + "package": "cyano.lootable.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": ["DataFixersMixin", "SaveAccessMixin", "RegionStorageMixin", "LegacyStructuresMixin", "CapabilityDataMixin"], + "server": ["DedicatedWorldMixin"], + "client": ["CorpseRenderDispatcherMixin"], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 943efa0..59a7b9e 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,7 +1,13 @@ { "pack": { - "pack_format": 34, - "supported_formats": {"min_inclusive": 34, "max_inclusive": 48}, - "description": "Lootable Bodies resources" + "description": "Lootable Bodies resources", + "min_format": [ + 94, + 1 + ], + "max_format": [ + 94, + 1 + ] } } diff --git a/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java b/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java index f7c7247..ba5c294 100644 --- a/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java +++ b/src/test/java/cyano/lootable/buildtest/HistoricalUuidTest.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; import java.util.UUID; import net.minecraft.nbt.CompoundTag; import org.junit.jupiter.api.Test; @@ -7,12 +9,12 @@ class HistoricalUuidTest { private final UUID id=UUID.fromString("13572468-abcd-4f01-9234-56789abcdef0"); private CompoundTag legacy(String name){CompoundTag n=new CompoundTag();n.putLong(name+"Most",id.getMostSignificantBits());n.putLong(name+"Least",id.getLeastSignificantBits());return n;} @Test void readsUnmodifiedHistoricalEntityAndPlayerPairs(){for(String key:new String[]{"UUID","Player"}){CompoundTag n=legacy(key),before=n.copy();assertEquals(id,HistoricalUuid.read(n,key));assertEquals(before,n);}} - @Test void readsNativeArraysWithoutChangingTheirBytes(){CompoundTag n=new CompoundTag();n.putUUID("UUID",id);int[] before=n.getIntArray("UUID").clone();assertEquals(id,HistoricalUuid.read(n,"UUID"));assertArrayEquals(before,n.getIntArray("UUID"));} + @Test void readsNativeArraysWithoutChangingTheirBytes(){CompoundTag n=new CompoundTag();writeUuid(n, "UUID", id);int[] before=n.getIntArray("UUID").orElseGet(() -> new int[0]).clone();assertEquals(id,HistoricalUuid.read(n,"UUID"));assertArrayEquals(before,n.getIntArray("UUID").orElseGet(() -> new int[0]));} @Test void malformedAndPartialSourceIdentitiesCannotBeGuessed(){ CompoundTag n=legacy("UUID");n.remove("UUIDLeast");assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID")); n.putString("UUIDLeast","opaque");assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID")); CompoundTag array=new CompoundTag();array.putIntArray("UUID",new int[]{1,2,3});assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(array,"UUID")); } - @Test void conflictingLegacyAndCurrentSourcesAreRejected(){CompoundTag n=legacy("UUID");n.putUUID("UUID",new UUID(0,1));assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID"));} - @Test void zeroAndSignedBitsArePreserved(){for(UUID value:new UUID[]{new UUID(0,0),new UUID(Long.MIN_VALUE,Long.MAX_VALUE)}){CompoundTag n=new CompoundTag();n.putUUID("UUID",value);assertEquals(value,HistoricalUuid.read(n,"UUID"));}} + @Test void conflictingLegacyAndCurrentSourcesAreRejected(){CompoundTag n=legacy("UUID");writeUuid(n, "UUID", new UUID(0,1));assertThrows(IllegalArgumentException.class,()->HistoricalUuid.read(n,"UUID"));} + @Test void zeroAndSignedBitsArePreserved(){for(UUID value:new UUID[]{new UUID(0,0),new UUID(Long.MIN_VALUE,Long.MAX_VALUE)}){CompoundTag n=new CompoundTag();writeUuid(n, "UUID", value);assertEquals(value,HistoricalUuid.read(n,"UUID"));}} } diff --git a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java index 0956e92..87d9e44 100644 --- a/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native115PacketTest.java @@ -18,10 +18,10 @@ class Native115PacketTest { static { net.minecraft.SharedConstants.tryDetectVersion();net.minecraft.server.Bootstrap.bootStrap(); } private static final long SEED=0x13572468ABCDEF01L; private ClientboundRespawnPacket packet(ResourceKey dimension,long seed) { - var registry=dimensions().registryOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE); - var type=registry.getHolderOrThrow(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD); + var registry=dimensions().lookupOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE); + var type=registry.getOrThrow(net.minecraft.world.level.dimension.BuiltinDimensionTypes.OVERWORLD); return new ClientboundRespawnPacket(new CommonPlayerSpawnInfo(type,dimension,seed, - GameType.SURVIVAL,GameType.CREATIVE,false,true,java.util.Optional.empty(),0), + GameType.SURVIVAL,GameType.CREATIVE,false,true,java.util.Optional.empty(),0,63), ClientboundRespawnPacket.KEEP_ALL_DATA); } private static RegistryAccess dimensions() { @@ -66,7 +66,7 @@ private static RegistryAccess build() { NativeRespawnDescription.describe(packet(Level.NETHER,SEED))); } @Test void observationKeepsUnknownDimensionKeysAndEveryDecodedField() throws Exception { - ResourceKey key=ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,net.minecraft.resources.ResourceLocation.parse("fixture:unhandled")); + ResourceKey key=ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION,net.minecraft.resources.Identifier.parse("fixture:unhandled")); ClientboundRespawnPacket packet=packet(key,SEED); java.util.Map before=new java.util.HashMap<>(); for(Field field:ClientboundRespawnPacket.class.getDeclaredFields()){field.setAccessible(true);before.put(field,field.get(packet));} diff --git a/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java b/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java index 8d3ba2b..48216d7 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117EntityInboxTest.java @@ -28,6 +28,7 @@ public void setLevelCallback(EntityInLevelCallback callback) { } public void setRemoved(Entity.RemovalReason reason) { } public boolean shouldBeSaved() { return true; } public boolean isAlwaysTicking() { return false; } + public boolean isRemoved() { return false; } } private static final class Storage implements EntityPersistentStorage { final CompletableFuture> read = new CompletableFuture<>(); @@ -85,22 +86,22 @@ public void onSectionChange(Saved e) { } private static final class BeforeGameLoop extends net.minecraft.util.thread.BlockableEventLoop { private final Thread owner=Thread.currentThread(); BeforeGameLoop(){super("NativeFixtureBeforeGameLoop");} - protected Runnable wrapRunnable(Runnable task){return task;} + public Runnable wrapRunnable(Runnable task){return task;} protected boolean shouldRun(Runnable task){return false;} protected Thread getRunningThread(){return owner;} } @Test void nativeManagedBlockAdmitsCompletionsGatedBeforeTheGameLoop() { Fixture f=new Fixture();BeforeGameLoop loop=new BeforeGameLoop();AtomicLong clock=new AtomicLong(); - loop.tell(()->f.storage.read.complete(new ChunkEntities<>(POS,Collections.singletonList(new Saved())))); + 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()); - loop.tell(()->{});assertFalse(loop.pollTask(),"Managed blocking must not leave the native task gate bypassed"); + 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, clock::getAndIncrement,3,loop::managedBlock)); - loop.tell(()->{});assertFalse(loop.pollTask());assertFalse(f.manager.areEntitiesLoaded(POS.toLong()));assertEquals(0,f.storage.stores); + loop.schedule(()->{});assertFalse(loop.pollTask());assertFalse(f.manager.areEntitiesLoaded(POS.toLong()));assertEquals(0,f.storage.stores); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java index a068877..112b52e 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117ForgeConfigTest.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -/** Execute the pinned native45 spec; config corrections precede mod world hooks. */ +/** Execute the pinned Forge 61 spec; config corrections precede mod world hooks. */ class Native117ForgeConfigTest { private ForgeConfigSpec spec() throws Exception { java.lang.reflect.Field field=ForgeConfig.class.getDeclaredField("serverSpec"); @@ -30,7 +30,7 @@ private Set keys(CommentedConfig config) { "fixAdvancementLoading","treatEmptyTagsAsAir","zombieBaseSummonChance","zombieBabyChance")),removed); assertEquals(new HashSet<>(Arrays.asList("removeErroringBlockEntities","fullBoundingBoxLadders", "advertiseDedicatedServerToLan", - "removeErroringEntities","permissionHandler","useItemWithDurationZero")),keys(config)); + "removeErroringEntities","permissionHandler")),keys(config)); assertEquals(false,config.get("server.removeErroringBlockEntities")); assertEquals(0,spec().correct(config),"Native correction must be stable"); } @@ -45,12 +45,12 @@ private Set keys(CommentedConfig config) { assertEquals(true,config.get("server.fullBoundingBoxLadders"));assertNull(config.get("server.zombieBaseSummonChance")); assertNull(config.get("server.zombieBabyChance"));assertNull(config.get("server.treatEmptyTagsAsAir")); assertNull(config.get("server.fixAdvancementLoading"));assertEquals(true,config.get("server.removeErroringBlockEntities")); - assertEquals(true,config.get("server.removeErroringEntities"));assertEquals(true,config.get("server.useItemWithDurationZero")); + assertEquals(true,config.get("server.removeErroringEntities"));assertNull(config.get("server.useItemWithDurationZero")); assertEquals(false,config.get("server.advertiseDedicatedServerToLan"));assertNull(config.get("server.skipEmptyShapelessCheck")); } @Test void actualNativeDefaultMatchesTheTargetSpecBaseline() throws Exception { CommentedConfig config=CommentedConfig.inMemory();spec().correct(config); - CommentedConfig expected=new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge50-server-default.toml"))); + CommentedConfig expected=new TomlParser().parse(Files.readString(Paths.get("tools/fixtures/forge61-server-default.toml"))); assertEquals(expected,config); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java index cee99fc..5cc3a1b 100644 --- a/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java +++ b/src/test/java/cyano/lootable/buildtest/Native117MenuProtocolTest.java @@ -2,7 +2,8 @@ import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.Arrays; +import net.minecraft.network.HashedPatchMap; +import net.minecraft.network.HashedStack; import net.minecraft.core.NonNullList; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.core.RegistryAccess; @@ -49,15 +50,19 @@ private RegistryAccess registries() { } } @Test void clickCodecRetainsStatePredictedSlotsAndCarriedStack() throws Exception { - Int2ObjectOpenHashMap changed=new Int2ObjectOpenHashMap<>();changed.put(6,stack(13));changed.put(47,ItemStack.EMPTY); - ServerboundContainerClickPacket outgoing=new ServerboundContainerClickPacket(7,32766,6,1,ClickType.PICKUP,stack(9),changed); + HashedPatchMap.HashGenerator hash = component -> component.value().hashCode(); + Int2ObjectOpenHashMap changed = new Int2ObjectOpenHashMap<>(); + 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)); 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.getContainerId());assertEquals(32766,decoded.getStateId());assertEquals(6,decoded.getSlotNum());assertEquals(1,decoded.getButtonNum()); - assertEquals(ClickType.PICKUP,decoded.getClickType());assertTrue(ItemStack.matches(stack(9),decoded.getCarriedItem())); - assertEquals(changed.keySet(),decoded.getChangedSlots().keySet());assertTrue(ItemStack.matches(stack(13),decoded.getChangedSlots().get(6)));assertTrue(decoded.getChangedSlots().get(47).isEmpty()); + assertEquals(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(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/Native119Registries.java b/src/test/java/cyano/lootable/buildtest/Native119Registries.java index 02d7208..0f2ce87 100644 --- a/src/test/java/cyano/lootable/buildtest/Native119Registries.java +++ b/src/test/java/cyano/lootable/buildtest/Native119Registries.java @@ -1,11 +1,8 @@ package cyano.lootable.buildtest; import java.util.List; -import java.util.Optional; import net.minecraft.core.HolderLookup; -import net.minecraft.core.Registry; import net.minecraft.core.RegistryAccess; -import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.levelgen.WorldDimensions; /** One native vanilla bootstrap per test JVM; never historical world evidence. */ @@ -19,11 +16,10 @@ public static RegistryAccess forDimensions(WorldDimensions dimensions) { var stems=new net.minecraft.core.MappedRegistry( net.minecraft.core.registries.Registries.LEVEL_STEM,com.mojang.serialization.Lifecycle.stable()); dimensions.dimensions().forEach((key,value)->stems.register(key,value,net.minecraft.core.RegistrationInfo.BUILT_IN)); - return new RegistryAccess.ImmutableRegistryAccess(List.of(stems.freeze())) { - @Override public Optional> lookup(ResourceKey> key) { - Optional> saved=super.lookup(key); - return saved.isPresent()?saved:Native119Registries.lookup().lookup(key); - } - }; + var types = new net.minecraft.core.MappedRegistry( + net.minecraft.core.registries.Registries.DIMENSION_TYPE, com.mojang.serialization.Lifecycle.stable()); + lookup().lookupOrThrow(net.minecraft.core.registries.Registries.DIMENSION_TYPE).listElements() + .forEach(holder -> types.register(holder.key(), holder.value(), net.minecraft.core.RegistrationInfo.BUILT_IN)); + return new RegistryAccess.ImmutableRegistryAccess(List.of(stems.freeze(), types.freeze())); } } diff --git a/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java b/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java index 60276de..271fe9c 100644 --- a/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java +++ b/src/test/java/cyano/lootable/buildtest/Native120FixtureGenerator.java @@ -5,7 +5,7 @@ import net.minecraft.nbt.*; import cyano.lootable.upgrade.Native117RegionFixtures; -/** Derive negative fixtures from a saved Forge 50 flat header. */ +/** Derive negative fixtures from a saved Forge 61 flat header. */ public final class Native120FixtureGenerator { public static void main(String[] args)throws Exception { if(args.length!=2)throw new IllegalArgumentException("Owned output root and sealed native header required"); @@ -13,18 +13,18 @@ public static void main(String[] args)throws Exception { new Native117TestEnvironment().launcherSessionOpened(null); Path source=Path.of(args[1]); CompoundTag header=NbtIo.readCompressed(source,NbtAccounter.create(64L*1024*1024)); - CompoundTag data=header.getCompound("Data"); - int target=net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); - CompoundTag generator=data.getCompound("WorldGenSettings").getCompound("dimensions") - .getCompound("minecraft:overworld").getCompound("generator"); - if(data.getInt("DataVersion")!=target || !generator.getString("type").equals("minecraft:flat")) + CompoundTag data=header.getCompoundOrEmpty("Data"); + int target=net.minecraft.SharedConstants.getCurrentVersion().dataVersion().version(); + CompoundTag generator=data.getCompoundOrEmpty("WorldGenSettings").getCompoundOrEmpty("dimensions") + .getCompoundOrEmpty("minecraft:overworld").getCompoundOrEmpty("generator"); + if(data.getIntOr("DataVersion", 0)!=target || !generator.getStringOr("type", "").equals("minecraft:flat")) throw new AssertionError("Wrong native flat header"); - Files.copy(source,root.resolve("native121-flat-level.dat")); + Files.copy(source,root.resolve("native12111-flat-level.dat")); Path world=root.resolve("controlled-entity-source");Files.createDirectory(world); - CompoundTag originalOwned=new CompoundTag();originalOwned.putString("unknownField","retained-native1206-header");originalOwned.putLongArray("unknownData",new long[]{Long.MIN_VALUE,1234567890123L,Long.MAX_VALUE}); + CompoundTag originalOwned=new CompoundTag();originalOwned.putString("unknownField","retained-native12111-header");originalOwned.putLongArray("unknownData",new long[]{Long.MIN_VALUE,1234567890123L,Long.MAX_VALUE}); header.put("lootablebodies",originalOwned);NbtIo.writeCompressed(header,world.resolve("level.dat")); CompoundTag corpse=Native117RegionFixtures.body(UUID.fromString("00000000-1171-4171-8171-000000000099"),7); Native117RegionFixtures.write(world,Native117RegionFixtures.chunk(2730,corpse)); - System.out.println("NATIVE1206_CODEC_FIXTURE_PREPARED version="+target+" markerSource=7/2730 synthetic=true root="+root); + System.out.println("NATIVE12111_CODEC_FIXTURE_PREPARED version="+target+" markerSource=7/2730 synthetic=true root="+root); } } diff --git a/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java b/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java index c23d7b3..578ada1 100644 --- a/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java +++ b/src/test/java/cyano/lootable/buildtest/TypedNbtTest.java @@ -1,5 +1,7 @@ package cyano.lootable.buildtest; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import net.minecraft.nbt.ByteTag; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.DoubleTag; @@ -68,9 +70,9 @@ private CompoundTag entity(boolean reverse) { @Test void native121AttributeOrderMayChangeButTypedValuesMayNot() { CompoundTag left=entity(false),right=entity(true); for(CompoundTag body : new CompoundTag[]{left,right}) { - ListTag attributes=body.getList("Attributes",10); + ListTag attributes=typedList(body, "Attributes", 10); for(int i=0;i locales=Files.newDirectoryStream(Paths.get("src/test/resources/legacy-translations"),"*.lang")){ diff --git a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java index 6fdd273..50132d0 100644 --- a/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java +++ b/src/test/java/cyano/lootable/contracts/ConfigScreenTest.java @@ -29,7 +29,7 @@ private void save() throws Exception { @Test void modsListUsesAClientOnlyFactory() throws Exception { String source = new String(Files.readAllBytes(Paths.get( "src/main/java/cyano/lootable/LootableBodies.java")), StandardCharsets.UTF_8); - assertTrue(source.contains("DistExecutor.runWhenOn")); + assertTrue(source.contains("if (FMLEnvironment.dist == Dist.CLIENT) ClientProxy.register(context);")); assertFalse(source.contains("import net.minecraft.client")); } @@ -107,6 +107,27 @@ private void save() throws Exception { assertNotNull(language.get(property.getLanguageKey() + ".tooltip"), property.getName()); } } + private java.lang.reflect.Field unitClient; + private Object previousClient; + + @BeforeEach void installConstructorFixture() throws Exception { + // The native Screen constructor reads Minecraft.font. These checks + // only exercise drafts and lifecycle methods, with no window or font. + Class unsafeType = Class.forName("sun.misc.Unsafe"); + var singleton = unsafeType.getDeclaredField("theUnsafe"); + singleton.setAccessible(true); + Class minecraft = net.minecraft.client.Minecraft.class; + unitClient = minecraft.getDeclaredField("instance"); + unitClient.setAccessible(true); + previousClient = unitClient.get(null); + Object client = unsafeType.getMethod("allocateInstance", Class.class).invoke(singleton.get(null), minecraft); + unitClient.set(null, client); + } + + @AfterEach void restoreConstructorFixture() throws Exception { + if (unitClient != null) unitClient.set(null, previousClient); + } + @Test void cancelledChildEditsCannotLeakIntoTheRootDraft()throws Exception { load("");cyano.lootable.graphics.CorpseConfigGui root=new cyano.lootable.graphics.CorpseConfigGui(null); java.lang.reflect.Field pending=root.getClass().getDeclaredField("pending");pending.setAccessible(true); @@ -126,7 +147,7 @@ private void save() throws Exception { } @Test void corpseLayoutKeepsItsOriginalUnlabelledForeground()throws Exception { cyano.lootable.graphics.CorpseGUIContainer screen=new cyano.lootable.graphics.CorpseGUIContainer( - (cyano.lootable.graphics.CorpseContainer)null,new net.minecraft.world.entity.player.Inventory(null),net.minecraft.network.chat.Component.literal("Saved owner")); + (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); // The original layout uses all seven rows. Native labels would draw diff --git a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java index 70379e6..2c87042 100644 --- a/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java +++ b/src/test/java/cyano/lootable/contracts/NativeUpgradeBoundaryTest.java @@ -1,5 +1,10 @@ package cyano.lootable.contracts; +import static cyano.lootable.buildtest.TestNativeNbt.hasType; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.readUuid; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import cyano.lootable.upgrade.UpgradePreservation; import com.mojang.datafixers.DataFixer; import java.io.*; @@ -28,7 +33,7 @@ private CompoundTag item(String id,int count,int damage,int slot) { CompoundTag tag=new CompoundTag();tag.putString("keepsake","slot-"+slot);item.put("tag",tag);return item; } private CompoundTag corpse() { - CompoundTag body=new CompoundTag();body.putString("id","lootablebodies.Corpse");body.putUUID("UUID",ID);body.putString("Name","Saved owner");body.putString("CustomName","Saved owner");body.putLong("DeathTime",700); + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies.Corpse");writeUuid(body, "UUID", ID);body.putString("Name","Saved owner");body.putString("CustomName","Saved owner");body.putLong("DeathTime",700); ListTag equipment=new ListTag();for(int i=0;i<48;i++)equipment.add(item("minecraft:stone",i+1,1,i));body.put("Equipment",equipment); ListTag aux=new ListTag();aux.add(item("minecraft:gold_ingot",17,0,-1));body.put("Aux",aux);body.putDouble("FallDistance",Double.NaN);return body; } @@ -45,62 +50,62 @@ private void preserve(CompoundTag nbt)throws Exception { } private CompoundTag update(CompoundTag chunk)throws Exception { preserve(chunk);DataFixer safe=UpgradePreservation.wrapper(VANILLA); - CompoundTag first=cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,chunk.copy(),chunk.getInt("DataVersion"),1493); + CompoundTag first=cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,chunk.copy(),chunk.getIntOr("DataVersion", 0),1493); return cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,first,1493,1976); } private CompoundTag findCorpse(CompoundTag root) { CompoundTag found=find(root);assertNotNull(found,"The original corpse must remain present");return found; } private CompoundTag find(Tag value) { - if(value instanceof CompoundTag){CompoundTag nbt=(CompoundTag)value;if(nbt.getString("id").equals("lootablebodies:corpse") || nbt.getString("id").equals("lootablebodies.Corpse"))return nbt;for(String key:nbt.getAllKeys()){CompoundTag found=find(nbt.get(key));if(found!=null)return found;}} + if(value instanceof CompoundTag){CompoundTag nbt=(CompoundTag)value;if(nbt.getStringOr("id", "").equals("lootablebodies:corpse") || nbt.getStringOr("id", "").equals("lootablebodies.Corpse"))return nbt;for(String key:nbt.keySet()){CompoundTag found=find(nbt.get(key));if(found!=null)return found;}} if(value instanceof ListTag){for(Tag entry:(ListTag)value){CompoundTag found=find(entry);if(found!=null)return found;}}return null; } @Test void legacyIdentityAndEveryRawInventoryEntrySurviveBothAnvilPasses()throws Exception { CompoundTag body=corpse(),source=chunk(body,true);byte[] before=encoded(source);CompoundTag result=update(source),saved=findCorpse(result); - assertEquals("lootablebodies:corpse",saved.getString("id"));assertEquals(12,saved.getInt("LootableBodiesDataVersion"));assertEquals(ID,saved.getUUID("UUID")); - ListTag records=saved.getList("LootableBodiesUnresolved",10);assertEquals(49,records.size()); - for(int i=0;i<48;i++){CompoundTag record=records.getCompound(i);assertEquals("Equipment",record.getString("Source"));assertArrayEquals(encoded(body.getList("Equipment",10).getCompound(i)),encoded(record.getCompound("Data")));assertEquals(512,record.getCompound("SourceVersions").getInt("DataVersion"));} - assertEquals("Aux",records.getCompound(48).getString("Source"));assertArrayEquals(before,encoded(source));assertFalse(saved.contains("LootableBodiesUpgradePayload"));assertFalse(saved.contains("LootableBodiesInventorySource")); + assertEquals("lootablebodies:corpse",saved.getStringOr("id", ""));assertEquals(13,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")); } @Test void occupiedPartlyGeneratedChunksKeepTerrainAndVanillaContainers()throws Exception { - CompoundTag result=update(chunk(corpse(),false)),level=result.getCompound("Level"); - assertEquals(1,level.getList("Sections",10).size());assertTrue(level.getList("Sections",10).getCompound(0).contains("Palette",9)); - ListTag tiles=level.getList("TileEntities",10);assertEquals(1,tiles.size());CompoundTag contents=tiles.getCompound(0).getList("Items",10).getCompound(0);assertEquals("minecraft:granite",contents.getString("id"));assertEquals(23,contents.getByte("Count"));assertNotEquals("empty",level.getString("Status")); + CompoundTag result=update(chunk(corpse(),false)),level=result.getCompoundOrEmpty("Level"); + assertEquals(1,typedList(level, "Sections", 10).size());assertTrue(hasType(level.getListOrEmpty("Sections").getCompoundOrEmpty(0), "Palette", 9)); + ListTag tiles=typedList(level, "TileEntities", 10);assertEquals(1,tiles.size());CompoundTag contents=typedList(tiles.getCompoundOrEmpty(0), "Items", 10).getCompoundOrEmpty(0);assertEquals("minecraft:granite",contents.getStringOr("id", ""));assertEquals(23,contents.getByteOr("Count", (byte) 0));assertNotEquals("empty",level.getStringOr("Status", "")); } @Test void vanillaContainersAreProtectedEvenWithoutACorpseInTheirChunk()throws Exception { - CompoundTag result=update(chunk(null,false));assertEquals(1,result.getCompound("Level").getList("Sections",10).size());assertEquals(1,result.getCompound("Level").getList("TileEntities",10).size()); + CompoundTag result=update(chunk(null,false));assertEquals(1,typedList(result.getCompoundOrEmpty("Level"), "Sections", 10).size());assertEquals(1,typedList(result.getCompoundOrEmpty("Level"), "TileEntities", 10).size()); } @Test void storedTerrainSurvivesAnIncompleteChunkWithoutEntitiesOrContainers()throws Exception { - CompoundTag source=chunk(null,false);source.getCompound("Level").put("TileEntities",new ListTag()); - CompoundTag level=update(source).getCompound("Level"); - assertEquals(1,level.getList("Sections",10).size()); - assertTrue(level.getList("Sections",10).getCompound(0).contains("Palette",9)); + CompoundTag source=chunk(null,false);source.getCompoundOrEmpty("Level").put("TileEntities",new ListTag()); + CompoundTag level=update(source).getCompoundOrEmpty("Level"); + assertEquals(1,typedList(level, "Sections", 10).size()); + assertTrue(hasType(level.getListOrEmpty("Sections").getCompoundOrEmpty(0), "Palette", 9)); } @Test void repeatedConversionCannotDuplicateOrResurrectRemovedCorpses()throws Exception { CompoundTag result=update(chunk(corpse(),true));DataFixer safe=UpgradePreservation.wrapper(VANILLA);CompoundTag again=cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,result.copy(),1976,1976); - assertEquals(49,findCorpse(again).getList("LootableBodiesUnresolved",10).size()); - again.getCompound("Level").put("Entities",new ListTag());assertNull(find(cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,again,1976,1976))); + assertEquals(49,findCorpse(again).getListOrEmpty("LootableBodiesUnresolved").size()); + again.getCompoundOrEmpty("Level").put("Entities",new ListTag());assertNull(find(cyano.lootable.upgrade.NativeNbt.update(safe,References.CHUNK,again,1976,1976))); } @Test void vanillaMinecartParentsAndNestedCorpsesBothUpgrade()throws Exception { - CompoundTag cart=new CompoundTag();cart.putString("id","MinecartRideable");cart.putUUID("UUID",UUID.fromString("00000000-3333-4333-8333-000000000099"));ListTag riders=new ListTag();riders.add(corpse());cart.put("Passengers",riders); - CompoundTag result=update(chunk(cart,true));assertEquals("minecraft:minecart",result.getCompound("Level").getList("Entities",10).getCompound(0).getString("id"));assertEquals(49,findCorpse(result).getList("LootableBodiesUnresolved",10).size()); + CompoundTag cart=new CompoundTag();cart.putString("id","MinecartRideable");writeUuid(cart, "UUID", UUID.fromString("00000000-3333-4333-8333-000000000099"));ListTag riders=new ListTag();riders.add(corpse());cart.put("Passengers",riders); + CompoundTag result=update(chunk(cart,true));assertEquals("minecraft:minecart",typedList(result.getCompoundOrEmpty("Level"), "Entities", 10).getCompoundOrEmpty(0).getStringOr("id", ""));assertEquals(49,findCorpse(result).getListOrEmpty("LootableBodiesUnresolved").size()); } @Test void corpseEggsKeepTheirTypeCountAndEntityPayload()throws Exception { for(String id:new String[]{"lootablebodies.Corpse","lootablebodies:corpse"}){ - CompoundTag egg=item("minecraft:spawn_egg",7,0,4),entity=new CompoundTag();entity.putString("id",id);entity.putString("Name","Egg owner");egg.getCompound("tag").put("EntityTag",entity);byte[] before=encoded(egg); + CompoundTag egg=item("minecraft:spawn_egg",7,0,4),entity=new CompoundTag();entity.putString("id",id);entity.putString("Name","Egg owner");egg.getCompoundOrEmpty("tag").put("EntityTag",entity);byte[] before=encoded(egg); CompoundTag fixed=cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),References.ITEM_STACK,egg.copy(),512,1976); - assertEquals("lootablebodies:corpse_spawn_egg",fixed.getString("id"));assertEquals(7,fixed.getByte("Count"));assertEquals("lootablebodies:corpse",fixed.getCompound("tag").getCompound("EntityTag").getString("id"));assertEquals("Egg owner",fixed.getCompound("tag").getCompound("EntityTag").getString("Name"));assertEquals(4,fixed.getByte("Slot"));assertArrayEquals(before,encoded(egg)); + assertEquals("lootablebodies:corpse_spawn_egg",fixed.getStringOr("id", ""));assertEquals(7,fixed.getByteOr("Count", (byte) 0));assertEquals("lootablebodies:corpse",fixed.getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag").getStringOr("id", ""));assertEquals("Egg owner",fixed.getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag").getStringOr("Name", ""));assertEquals(4,fixed.getByteOr("Slot", (byte) 0));assertArrayEquals(before,encoded(egg)); } } @Test void corpseEggsInsideVanillaPlayerInventoriesAlsoUpgrade()throws Exception { - CompoundTag egg=item("minecraft:spawn_egg",11,0,0),entity=new CompoundTag();entity.putString("id","lootablebodies.Corpse");egg.getCompound("tag").put("EntityTag",entity);ListTag items=new ListTag();items.add(egg);CompoundTag player=new CompoundTag();player.put("Inventory",items); - CompoundTag result=cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),References.PLAYER,player.copy(),512,1976);CompoundTag fixed=result.getList("Inventory",10).getCompound(0);assertEquals("lootablebodies:corpse_spawn_egg",fixed.getString("id"));assertEquals(11,fixed.getByte("Count")); + CompoundTag egg=item("minecraft:spawn_egg",11,0,0),entity=new CompoundTag();entity.putString("id","lootablebodies.Corpse");egg.getCompoundOrEmpty("tag").put("EntityTag",entity);ListTag items=new ListTag();items.add(egg);CompoundTag player=new CompoundTag();player.put("Inventory",items); + CompoundTag result=cyano.lootable.upgrade.NativeNbt.update(UpgradePreservation.wrapper(VANILLA),References.PLAYER,player.copy(),512,1976);CompoundTag fixed=typedList(result, "Inventory", 10).getCompoundOrEmpty(0);assertEquals("lootablebodies:corpse_spawn_egg",fixed.getStringOr("id", ""));assertEquals(11,fixed.getByteOr("Count", (byte) 0)); } @Test void aFalseCurrentMarkerCannotBypassLegacyIdentityConversion()throws Exception { CompoundTag body=corpse();body.putInt("LootableBodiesDataVersion",3); CompoundTag result=update(chunk(body,true)); - assertEquals(49,findCorpse(result).getList("LootableBodiesUnresolved",10).size()); - assertEquals(512,findCorpse(result).getCompound("LootableBodiesSourceVersions").getInt("DataVersion")); + assertEquals(49,findCorpse(result).getListOrEmpty("LootableBodiesUnresolved").size()); + assertEquals(512,findCorpse(result).getCompoundOrEmpty("LootableBodiesSourceVersions").getIntOr("DataVersion", 0)); } @Test void futureCorpseFormatsAreNeverOverwritten() { CompoundTag body=corpse();body.putString("id","lootablebodies:corpse");body.putInt("LootableBodiesDataVersion",99); @@ -112,9 +117,9 @@ private CompoundTag find(Tag value) { } @Test void aPlausibleFalseStampCannotSkipVanillaBannerConversion()throws Exception { CompoundTag source=chunk(corpse(),true);source.putInt("DataVersion",922); - CompoundTag banner=item("minecraft:banner",2,0,0),block=new CompoundTag();block.putInt("Base",5);banner.getCompound("tag").put("BlockEntityTag",block); - ListTag items=new ListTag();items.add(banner);source.getCompound("Level").getList("TileEntities",10).getCompound(0).put("Items",items); - CompoundTag converted=update(source).getCompound("Level").getList("TileEntities",10).getCompound(0).getList("Items",10).getCompound(0); - assertEquals("minecraft:purple_banner",converted.getString("id"));assertEquals(2,converted.getByte("Count")); + CompoundTag banner=item("minecraft:banner",2,0,0),block=new CompoundTag();block.putInt("Base",5);banner.getCompoundOrEmpty("tag").put("BlockEntityTag",block); + ListTag items=new ListTag();items.add(banner);typedList(source.getCompoundOrEmpty("Level"), "TileEntities", 10).getCompoundOrEmpty(0).put("Items",items); + CompoundTag converted=update(source).getCompoundOrEmpty("Level").getListOrEmpty("TileEntities").getCompoundOrEmpty(0).getListOrEmpty("Items").getCompoundOrEmpty(0); + assertEquals("minecraft:purple_banner",converted.getStringOr("id", ""));assertEquals(2,converted.getByteOr("Count", (byte) 0)); } } diff --git a/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java b/src/test/java/cyano/lootable/contracts/ReleaseContractTest.java index a14061f..5ed9697 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.13.121011", version); + assertEquals("2.4.14.121111", version); assertEquals("forge", values.getProperty("loader_name")); assertEquals("1", values.getProperty("loader_code")); assertTrue(text("src/main/java/cyano/lootable/LootableBodies.java") @@ -43,8 +43,8 @@ private String text(String path) throws Exception { assertEquals("240968", values.getProperty("curseforge_project_id")); String source = text("src/main/java/cyano/lootable/LootableBodies.java"); assertTrue(source.startsWith("package cyano.lootable;")); - assertTrue(source.contains("ResourceLocation.fromNamespaceAndPath(MODID,\"corpse\")")); - assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.21.1]\"")); + assertTrue(source.contains("Identifier.fromNamespaceAndPath(MODID,\"corpse\")")); + assertTrue(text("build/resources/main/META-INF/mods.toml").contains("versionRange=\"[1.21.11]\"")); } @Test void maintainedReleaseHasLgplAndRetainsTheOriginalNotice() throws Exception { diff --git a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java index e09f78d..f9d7dfd 100644 --- a/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java +++ b/src/test/java/cyano/lootable/contracts/UpgradeJournalTest.java @@ -1,5 +1,9 @@ package cyano.lootable.contracts; +import static cyano.lootable.buildtest.TestNativeNbt.hasType; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import java.io.*; import java.lang.reflect.*; import java.nio.file.*; @@ -25,12 +29,12 @@ private CompoundTag original(Object journal,UUID id,int x,int z) throws Exceptio return (CompoundTag)journal.getClass().getMethod("originalFor",UUID.class,int.class,int.class).invoke(journal,id,x,z); } private CompoundTag body(UUID id) { - CompoundTag body=new CompoundTag();body.putString("id","lootablebodies.Corpse");body.putUUID("UUID",id); + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies.Corpse");writeUuid(body, "UUID", id); body.putDouble("FallDistance",Double.NaN);body.putString("Name","Saved owner");body.putLong("DeathTime",700); ListTag equipment=new ListTag(); for(int i=0;i<48;i++) {CompoundTag item=new CompoundTag();item.putString("id","minecraft:diamond");item.putByte("Count",(byte)(i+1));item.putShort("Damage",(short)i);item.putByte("Slot",(byte)i);CompoundTag custom=new CompoundTag();custom.putString("keepsake","slot-"+i);item.put("tag",custom);equipment.add(item);} body.put("Equipment",equipment); - CompoundTag record=new CompoundTag();record.putString("Source","Equipment");record.put("Data",equipment.getCompound(47).copy()); + CompoundTag record=new CompoundTag();record.putString("Source","Equipment");record.put("Data",equipment.getCompoundOrEmpty(47).copy()); ListTag recovery=new ListTag();recovery.add(record);body.put("LootableBodiesUnresolved",recovery); return body; } @@ -49,14 +53,14 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t private byte[] encoded(CompoundTag nbt) throws Exception {ByteArrayOutputStream bytes=new ByteArrayOutputStream();NbtIo.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} @Test void native112VersionIsNotDowngradedTo111() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1343,b); - assertEquals(1343,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(1343,original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); } @Test void native116MarkerOverridesMissingAndOlderOuterStamps() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",6); regionVersion("",512,b); CompoundTag saved=original(scan(),ID,0,0); - assertEquals(2586,saved.getCompound("SourceVersions").getInt("DataVersion")); - assertArrayEquals(encoded(b),encoded(saved.getCompound("Entity"))); + assertEquals(2586,saved.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); + assertArrayEquals(encoded(b),encoded(saved.getCompoundOrEmpty("Entity"))); } @Test void released116MetadataIdentifiesUnstampedNativeItems() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); @@ -64,11 +68,11 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.7.116051"); ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} - assertEquals(2586,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(2586,original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); } @Test void ambiguousItemContextsIncludeTheNative116Schema() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); - int[] versions=original(scan(),ID,0,0).getCompound("SourceVersions").getIntArray("CandidateDataVersions"); + int[] versions=original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntArray("CandidateDataVersions").orElseGet(() -> new int[0]); assertTrue(java.util.Arrays.stream(versions).anyMatch(version->version==2586)); } @Test void nativeLowercaseFmlIdentifiesUnstampedItemsWithoutChangingOriginals() throws Exception { @@ -78,36 +82,36 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t ListTag mods=new ListTag();mods.add(mod);fml.put("LoadingModList",mods);root.put("fml",fml);root.put("Data",new CompoundTag()); try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} byte[] header=Files.readAllBytes(world.resolve("level.dat")); - CompoundTag preserved=original(scan(),ID,0,0),context=preserved.getCompound("SourceVersions"); - assertEquals(2230,context.getInt("DataVersion"));assertFalse(context.getBoolean("Ambiguous")); - assertEquals(fml,context.getCompound("WorldFML")); - assertArrayEquals(encoded(b),encoded(preserved.getCompound("Entity"))); + CompoundTag preserved=original(scan(),ID,0,0),context=preserved.getCompoundOrEmpty("SourceVersions"); + assertEquals(2230,context.getIntOr("DataVersion", 0));assertFalse(context.getBooleanOr("Ambiguous", false)); + assertEquals(fml,context.getCompoundOrEmpty("WorldFML")); + assertArrayEquals(encoded(b),encoded(preserved.getCompoundOrEmpty("Entity"))); assertArrayEquals(header,Files.readAllBytes(world.resolve("level.dat"))); } @Test void native113MarkerAndStampSurvivePreservation() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",3);regionVersion("",1631,b); - assertEquals(1631,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(1631,original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); assertNotNull(original(scan(),ID,0,0)); } @Test void released112MarkerDoesNotBorrowAFalselyNative113Stamp() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",2);regionVersion("",1631,b); - assertEquals(1343,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(1343,original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); } @Test void native113MarkerDisambiguatesMissingMinecraftStamps() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",3);regionVersion("",-1,b); - CompoundTag versions=original(scan(),ID,0,0).getCompound("SourceVersions"); - assertEquals(1631,versions.getInt("DataVersion"));assertFalse(versions.getBoolean("Ambiguous")); + CompoundTag versions=original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions"); + assertEquals(1631,versions.getIntOr("DataVersion", 0));assertFalse(versions.getBooleanOr("Ambiguous", false)); } @Test void actual112WriterMetadataRejectsAFalselyFlattenedStamp() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1631,b); CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.0");ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} - int source=original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion");assertTrue(source>=1139 && source<=1343); + int source=original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0);assertTrue(source>=1139 && source<=1343); } @Test void corpsesSavedAsVanillaVehiclePassengersArePreserved() throws Exception { - CompoundTag cart=new CompoundTag();cart.putString("id","minecraft:minecart");cart.putUUID("UUID",UUID.fromString("00000000-1111-4111-8111-000000000099")); + CompoundTag cart=new CompoundTag();cart.putString("id","minecraft:minecart");writeUuid(cart, "UUID", UUID.fromString("00000000-1111-4111-8111-000000000099")); ListTag riders=new ListTag();riders.add(body(ID));cart.put("Passengers",riders);region("",cart); - CompoundTag saved=original(scan(),ID,0,0);assertNotNull(saved);assertArrayEquals(encoded(body(ID)),encoded(saved.getCompound("Entity"))); + CompoundTag saved=original(scan(),ID,0,0);assertNotNull(saved);assertArrayEquals(encoded(body(ID)),encoded(saved.getCompoundOrEmpty("Entity"))); region("",cart.copy());assertNotNull(original(scan(),ID,0,0)); cart.remove("Passengers");region("",cart);assertNull(original(scan(),ID,0,0)); } @@ -117,42 +121,42 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t } @Test void nativeIdentityWithoutAnyVersionEvidenceIsExplicitlyAmbiguous() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); - assertTrue(original(scan(),ID,0,0).getCompound("SourceVersions").getBoolean("Ambiguous")); + assertTrue(original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getBooleanOr("Ambiguous", false)); } @Test void old111MarkerProvesItsOwnSourceDespiteAnIncorrectNewChunkStamp() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");b.putInt("LootableBodiesDataVersion",1);regionVersion("",1343,b); - assertEquals(922,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(922,original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); } @Test void rootFmlMetadataDistinguishesAnUnstampedOriginal112World() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",-1,b); CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.4.0");ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} - CompoundTag context=original(scan(),ID,0,0).getCompound("SourceVersions");assertTrue(context.getInt("DataVersion")>=1139);assertFalse(context.getBoolean("Ambiguous"));assertEquals(fml,context.getCompound("WorldFML")); + CompoundTag context=original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions");assertTrue(context.getIntOr("DataVersion", 0)>=1139);assertFalse(context.getBooleanOr("Ambiguous", false));assertEquals(fml,context.getCompoundOrEmpty("WorldFML")); } @Test void unmarkedOriginal111CorpseWithFalselyNewStampStillUses111ItemRules() throws Exception { CompoundTag b=body(ID);b.putString("id","lootablebodies:corpse");regionVersion("",1343,b); CompoundTag root=new CompoundTag(),fml=new CompoundTag(),mod=new CompoundTag();mod.putString("ModId","lootablebodies");mod.putString("ModVersion","2.3.0");ListTag mods=new ListTag();mods.add(mod);fml.put("ModList",mods);root.put("FML",fml);root.put("Data",new CompoundTag()); try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(root,output);} - assertEquals(922,original(scan(),ID,0,0).getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(922,original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); } @Test void stampOnlyChangesRetainPreviouslyVerifiedChunkVersion() throws Exception { CompoundTag b=body(ID);regionVersion("",512,b);scan(); regionVersion("",-1,b); - CompoundTag versions=original(scan(),ID,0,0).getCompound("SourceVersions"); - assertEquals(512,versions.getInt("VerifiedChunkDataVersion")); + CompoundTag versions=original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions"); + assertEquals(512,versions.getIntOr("VerifiedChunkDataVersion", 0)); } @Test void changedChunkPayloadCannotReusePreviousVersionEvidence() throws Exception { CompoundTag b=body(ID);regionVersion("",512,b);scan(); CompoundTag added=new CompoundTag();added.putString("id","minecraft:pig"); regionVersion("",-1,b,added); - assertFalse(original(scan(),ID,0,0).getCompound("SourceVersions").contains("VerifiedChunkDataVersion")); + assertFalse(original(scan(),ID,0,0).getCompoundOrEmpty("SourceVersions").contains("VerifiedChunkDataVersion")); } private List snapshots() throws Exception {try(java.util.stream.Stream files=Files.walk(world.resolve("data/lootablebodies-upgrade/snapshots"))){List result=new ArrayList<>();files.filter(p->p.toString().endsWith(".nbt")).forEach(result::add);return result;}} @Test void preservesCompleteTypedPayloadWithoutWritingSourceRegions() throws Exception { CompoundTag body=body(ID);Path region=region("",body);byte[] before=Files.readAllBytes(region);java.nio.file.attribute.FileTime stamp=Files.getLastModifiedTime(region); Object journal=scan();CompoundTag saved=original(journal,ID,0,0); - assertArrayEquals(encoded(body),encoded(saved.getCompound("Entity")));assertEquals(48,saved.getCompound("Entity").getList("Equipment",10).size()); - assertEquals(512,saved.getCompound("SourceVersions").getInt("DataVersion")); + assertArrayEquals(encoded(body),encoded(saved.getCompoundOrEmpty("Entity")));assertEquals(48,typedList(saved.getCompoundOrEmpty("Entity"), "Equipment", 10).size()); + assertEquals(512,saved.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); assertArrayEquals(before,Files.readAllBytes(region));assertEquals(stamp,Files.getLastModifiedTime(region)); } @Test void repeatedScanReusesOriginalBackups() throws Exception {region("",body(ID));scan();List first=snapshots();scan();assertEquals(first,snapshots());} @@ -162,8 +166,8 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t } @Test void staleIndexCannotOverwriteNewInventory() throws Exception { CompoundTag body=body(ID);region("",body);scan();Path index=world.resolve("data/lootablebodies-upgrade/index.nbt");byte[] stale=Files.readAllBytes(index); - body.getList("Equipment",10).getCompound(40).putByte("Count",(byte)99);region("",body);scan();Files.write(index,stale); - assertArrayEquals(encoded(body),encoded(original(scan(),ID,0,0).getCompound("Entity"))); + typedList(body, "Equipment", 10).getCompoundOrEmpty(40).putByte("Count",(byte)99);region("",body);scan();Files.write(index,stale); + assertArrayEquals(encoded(body),encoded(original(scan(),ID,0,0).getCompoundOrEmpty("Entity"))); } @Test void interruptedTemporaryWriteIsNeverTrusted() throws Exception { region("",body(ID));Path partial=world.resolve("data/lootablebodies-upgrade/.pending-interrupted");Files.createDirectories(partial.getParent());Files.write(partial,new byte[]{31,-117,8}); @@ -174,8 +178,8 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t @Test void readableIndexDamageCannotSilentlyOmitPreservedCorpses() throws Exception { region("",body(ID));scan();Path path=world.resolve("data/lootablebodies-upgrade/index.nbt"); CompoundTag index=NbtIo.readCompressed(path,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)); - if(index.contains("Payload",7)) {byte[] payload=index.getByteArray("Payload");payload[payload.length/2]^=1;index.putByteArray("Payload",payload);} - else index.getList("Regions",10).getCompound(0).put("Snapshots",new ListTag()); + if(hasType(index, "Payload", 7)) {byte[] payload=index.getByteArray("Payload").orElseGet(() -> new byte[0]);payload[payload.length/2]^=1;index.putByteArray("Payload",payload);} + else typedList(index, "Regions", 10).getCompoundOrEmpty(0).put("Snapshots",new ListTag()); try(OutputStream output=Files.newOutputStream(path)){NbtIo.writeCompressed(index,output);} assertThrows(IOException.class,()->scan(),"A readable damaged index must not be accepted as a complete scan"); } @@ -190,14 +194,14 @@ private Path regionVersion(String dimension,int version,CompoundTag... bodies) t Files.write(file,new byte[1]);assertThrows(IOException.class,()->scan(),"A nonempty truncated header must still stop loading"); } @Test void duplicateUuidAcrossDimensionsAbortsInsteadOfGuessing() throws Exception {region("",body(ID));region("DIM-1",body(ID));assertThrows(IOException.class,()->scan());} - @Test void newDimensionsAreScannedOnLaterStarts() throws Exception {region("",body(ID));scan();UUID id=UUID.fromString("00000000-1111-4111-8111-000000000003");region("DIM1",body(id));assertEquals("DIM1",original(scan(),id,0,0).getString("Dimension"));} + @Test void newDimensionsAreScannedOnLaterStarts() throws Exception {region("",body(ID));scan();UUID id=UUID.fromString("00000000-1111-4111-8111-000000000003");region("DIM1",body(id));assertEquals("DIM1",original(scan(),id,0,0).getStringOr("Dimension", ""));} @Test void chunkCoordinateMismatchCannotApplyAnOldPayload() throws Exception {region("",body(ID));assertNull(original(scan(),ID,32,0));} @Test void retainsExactCompressedChunkAsWellAsDecodedCorpse() throws Exception { Path region=region("",body(ID));byte[] source=Files.readAllBytes(region);int size=java.nio.ByteBuffer.wrap(source,8192,4).getInt(); byte[] originalChunk=Arrays.copyOfRange(source,8197,8192+4+size);CompoundTag snapshot=original(scan(),ID,0,0); - assertTrue(snapshot.contains("OriginalChunk",8),"A decoded NBT copy cannot retain every original byte encoding"); - assertEquals(2,snapshot.getByte("OriginalCompression"));assertEquals(0,snapshot.getInt("EntityIndex")); - assertArrayEquals(originalChunk,Files.readAllBytes(world.resolve("data/lootablebodies-upgrade/chunks/"+snapshot.getString("OriginalChunk")+".bin"))); + assertTrue(hasType(snapshot, "OriginalChunk", 8),"A decoded NBT copy cannot retain every original byte encoding"); + assertEquals(2,snapshot.getByteOr("OriginalCompression", (byte) 0));assertEquals(0,snapshot.getIntOr("EntityIndex", 0)); + assertArrayEquals(originalChunk,Files.readAllBytes(world.resolve("data/lootablebodies-upgrade/chunks/"+snapshot.getStringOr("OriginalChunk", "")+".bin"))); } @Test void diskCannotSupplyAProcessedTemporaryPayload() throws Exception { CompoundTag body=body(ID),fake=body(ID);fake.putInt("LootableBodiesDataVersion",1);fake.put("Equipment",new ListTag());body.put("LootableBodiesUpgradePayload",fake); diff --git a/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java b/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java index 6d55059..77d30f2 100644 --- a/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java +++ b/src/test/java/cyano/lootable/entities/Native118EffectIdentityTest.java @@ -15,11 +15,14 @@ private CompoundTag legacyEffect() { } @Test void nativeForgeSaveUsesARecognizedStableNamedIdentity() { - CompoundTag raw=(CompoundTag)new MobEffectInstance(MobEffects.NIGHT_VISION,200,1,false,true,false).save(); + CompoundTag raw=(CompoundTag)MobEffectInstance.CODEC.encodeStart( + cyano.lootable.buildtest.Native119Registries.lookup().createSerializationContext(NbtOps.INSTANCE), + new MobEffectInstance(MobEffects.NIGHT_VISION,200,1,false,true,false)).getOrThrow(); CompoundTag before=raw.copy(); - assertEquals("minecraft:night_vision",raw.getString("id")); + assertEquals("minecraft:night_vision",raw.getStringOr("id", "")); assertEquals(MobEffects.NIGHT_VISION,SavedEffectIdentity.resolve(raw,null)); - assertEquals(MobEffects.NIGHT_VISION,MobEffectInstance.load(raw).getEffect()); + assertEquals(MobEffects.NIGHT_VISION,MobEffectInstance.CODEC.parse( + cyano.lootable.buildtest.Native119Registries.lookup().createSerializationContext(NbtOps.INSTANCE), raw).getOrThrow().getEffect()); assertEquals(before,raw); } diff --git a/src/test/java/cyano/lootable/entities/Native119DamageTest.java b/src/test/java/cyano/lootable/entities/Native119DamageTest.java index c8e9302..a8ea686 100644 --- a/src/test/java/cyano/lootable/entities/Native119DamageTest.java +++ b/src/test/java/cyano/lootable/entities/Native119DamageTest.java @@ -11,7 +11,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.Packet; import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.world.damagesource.*; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; @@ -37,7 +37,7 @@ public HolderGetter lookup(ResourceKey> k } }); // Deliberately shares the vanilla anvil message, but has its own mod identity. - custom=registry.register(ResourceKey.create(Registries.DAMAGE_TYPE,ResourceLocation.parse("example:custom")), + custom=registry.register(ResourceKey.create(Registries.DAMAGE_TYPE,Identifier.parse("example:custom")), new DamageType("anvil",0),net.minecraft.core.RegistrationInfo.BUILT_IN); registry.freeze(); sources=new DamageSources(new RegistryAccess.ImmutableRegistryAccess(java.util.List.of(registry))); @@ -111,8 +111,9 @@ public HolderGetter lookup(ResourceKey> k private static final class SourceEntity extends Entity { SourceEntity(){super(EntityType.FALLING_BLOCK,null);} protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder){ } - protected void readAdditionalSaveData(CompoundTag tag){ } - protected void addAdditionalSaveData(CompoundTag tag){ } + protected void readAdditionalSaveData(net.minecraft.world.level.storage.ValueInput tag){ } + protected void addAdditionalSaveData(net.minecraft.world.level.storage.ValueOutput tag){ } + public boolean hurtServer(net.minecraft.server.level.ServerLevel level, DamageSource damage, float amount) { throw new UnsupportedOperationException("Damage source only"); } public Packet getAddEntityPacket(){throw new UnsupportedOperationException("No network in this unit fixture");} } } diff --git a/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java index 113e0ae..efd419a 100644 --- a/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java +++ b/src/test/java/cyano/lootable/network/Native117ProfileLookupTest.java @@ -10,11 +10,12 @@ class Native117ProfileLookupTest { private final GameProfile placeholder=CorpseProfiles.placeholder("TestName"); - private final GameProfile found=new GameProfile(new UUID(1,2),"TestName"); + private final GameProfile found=new GameProfile(new UUID(1,2),"TestName",new com.mojang.authlib.properties.PropertyMap( + com.google.common.collect.ImmutableMultimap.of("textures",new Property("textures","fixture-native-texture")))); @Test void unresolvedProfileUsesAValidAuthlibIdentity() { - assertEquals("TestName",placeholder.getName()); - assertNotNull(placeholder.getId()); + assertEquals("TestName",placeholder.name()); + assertNotNull(placeholder.id()); assertFalse(CorpseProfiles.isResolved(placeholder)); } @@ -37,10 +38,9 @@ class Native117ProfileLookupTest { return pending; }); assertFalse(result.isDone()); - found.getProperties().put("textures",new Property("textures","fixture-native-texture")); pending.complete(Optional.of(found)); assertSame(found,result.join()); assertTrue(CorpseProfiles.isResolved(result.join())); - assertEquals("fixture-native-texture",result.join().getProperties().get("textures").iterator().next().value()); + assertEquals("fixture-native-texture",result.join().properties().get("textures").iterator().next().value()); } } diff --git a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java index fa61d4a..8b0bd52 100644 --- a/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java +++ b/src/test/java/cyano/lootable/upgrade/EarlyWorldPreservationTest.java @@ -39,12 +39,12 @@ private void write(Path world,CompoundTag root)throws Exception { LevelStorageSource format=new LevelStorageSource(directory,directory.resolve("backups"),new DirectoryValidator(new PathAllowList(java.util.List.of())),NativeVanillaDataFixer.get()); try(LevelStorageSource.LevelStorageAccess save=format.createAccess("fixture")) { EarlyWorldPreservation.beforeRead(save,false); - UpgradeJournal early=UpgradePreservation.journal();assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); + UpgradeJournal early=UpgradePreservation.journal();assertEquals(2230,early.worldVersions().getIntOr("WorldDataVersion", 0)); write(world,level(2586));assertSame(early,UpgradePreservation.prepareForStart(world,net.minecraft.util.datafix.DataFixers.getDataFixer())); - assertEquals(2230,early.worldVersions().getInt("WorldDataVersion")); + assertEquals(2230,early.worldVersions().getIntOr("WorldDataVersion", 0)); CompoundTag output=new CompoundTag();EarlyWorldPreservation.beforeWrite(save,output); assertEquals(root.get("lootablebodies"),output.get("lootablebodies")); - output.getCompound("lootablebodies").putString("unknown","changed copy"); + output.getCompoundOrEmpty("lootablebodies").putString("unknown","changed copy"); CompoundTag again=new CompoundTag();EarlyWorldPreservation.beforeWrite(save,again); assertEquals(root.get("lootablebodies"),again.get("lootablebodies")); } diff --git a/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java b/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java index f4cbc38..d3680cb 100644 --- a/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java +++ b/src/test/java/cyano/lootable/upgrade/HookRegistrationOrderTest.java @@ -35,7 +35,7 @@ class HookRegistrationOrderTest { for(int round=0;round<1000;round++){EarlyWorldPreservation.install();EarlyWorldPreservation.install();} EarlyWorldPreservation.beforeRead(save,false); CompoundTag saved=new CompoundTag();EarlyWorldPreservation.beforeWrite(save,saved); - assertEquals(owned,saved.getCompound("lootablebodies")); + assertEquals(owned,saved.getCompound("lootablebodies").orElseThrow()); } finally {UpgradePreservation.close();installed.setBoolean(null,wasInstalled);} } @Test void theProductionConstructorMustDeferHookRegistrationToSerialLifecycleWork() throws Exception { diff --git a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java index 96821bf..f4bbfec 100644 --- a/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java +++ b/src/test/java/cyano/lootable/upgrade/ItemNbtConversionTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import java.io.*; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -16,21 +18,21 @@ class ItemNbtConversionTest { private CompoundTag item(String id,int damage){CompoundTag nbt=new CompoundTag();nbt.putString("id",id);nbt.putByte("Count",(byte)7);nbt.putShort("Damage",(short)damage);nbt.putByte("Slot",(byte)47);CompoundTag custom=new CompoundTag();custom.putString("keepsake","saved");nbt.put("tag",custom);return nbt;} private byte[] bytes(CompoundTag nbt)throws Exception{ByteArrayOutputStream bytes=new ByteArrayOutputStream();NbtIo.write(nbt,new DataOutputStream(bytes));return bytes.toByteArray();} @Test void blockVariantsFlattenWithoutLosingTheirCountSlotOrCustomTag()throws Exception { - CompoundTag raw=item("minecraft:stone",1);byte[] before=bytes(raw);CompoundTag result=ItemMigration.convertNbt(raw,context(512));assertNotNull(result);assertEquals("minecraft:granite",result.getString("id"));assertEquals(7,result.getByte("Count"));assertEquals(47,result.getByte("Slot"));assertEquals("saved",result.getCompound("tag").getString("keepsake"));assertArrayEquals(before,bytes(raw)); + CompoundTag raw=item("minecraft:stone",1);byte[] before=bytes(raw);CompoundTag result=ItemMigration.convertNbt(raw,context(512));assertNotNull(result);assertEquals("minecraft:granite",result.getStringOr("id", ""));assertEquals(7,result.getByteOr("Count", (byte) 0));assertEquals(47,result.getByteOr("Slot", (byte) 0));assertEquals("saved",result.getCompoundOrEmpty("tag").getStringOr("keepsake", ""));assertArrayEquals(before,bytes(raw)); } @Test void durabilityAndEnchantmentsUseTheirNativeFields() { - CompoundTag raw=item("minecraft:diamond_sword",123);CompoundTag enchantment=new CompoundTag();enchantment.putShort("id",(short)16);enchantment.putShort("lvl",(short)3);ListTag enchantments=new ListTag();enchantments.add(enchantment);raw.getCompound("tag").put("ench",enchantments); - CompoundTag result=ItemMigration.convertNbt(raw,context(1343));assertNotNull(result);assertEquals(123,result.getCompound("tag").getInt("Damage"));CompoundTag sharpness=result.getCompound("tag").getList("Enchantments",10).getCompound(0);assertEquals("minecraft:sharpness",sharpness.getString("id"));assertEquals(3,sharpness.getShort("lvl")); + CompoundTag raw=item("minecraft:diamond_sword",123);CompoundTag enchantment=new CompoundTag();enchantment.putShort("id",(short)16);enchantment.putShort("lvl",(short)3);ListTag enchantments=new ListTag();enchantments.add(enchantment);raw.getCompoundOrEmpty("tag").put("ench",enchantments); + CompoundTag result=ItemMigration.convertNbt(raw,context(1343));assertNotNull(result);assertEquals(123,result.getCompoundOrEmpty("tag").getIntOr("Damage", 0));CompoundTag sharpness=typedList(result.getCompoundOrEmpty("tag"), "Enchantments", 10).getCompoundOrEmpty(0);assertEquals("minecraft:sharpness",sharpness.getStringOr("id", ""));assertEquals(3,sharpness.getShortOr("lvl", (short) 0)); } @Test void nestedInventoriesAndCapabilitiesRemainComplete()throws Exception { - CompoundTag raw=item("minecraft:blue_shulker_box",0),nested=item("minecraft:stone",1);nested.putByte("Count",(byte)29);nested.putByte("Slot",(byte)3);ListTag contents=new ListTag();contents.add(nested);CompoundTag block=new CompoundTag();block.putString("id","minecraft:shulker_box");block.put("Items",contents);raw.getCompound("tag").put("BlockEntityTag",block);CompoundTag caps=new CompoundTag();caps.putByteArray("retained",new byte[]{1,4,9});raw.put("ForgeCaps",caps); - CompoundTag result=ItemMigration.convertNbt(raw,context(922));assertNotNull(result);assertArrayEquals(bytes(caps),bytes(result.getCompound("ForgeCaps")));CompoundTag recovered=result.getCompound("tag").getCompound("BlockEntityTag").getList("Items",10).getCompound(0);assertEquals("minecraft:granite",recovered.getString("id"));assertEquals(29,recovered.getByte("Count"));assertEquals(3,recovered.getByte("Slot")); + CompoundTag raw=item("minecraft:blue_shulker_box",0),nested=item("minecraft:stone",1);nested.putByte("Count",(byte)29);nested.putByte("Slot",(byte)3);ListTag contents=new ListTag();contents.add(nested);CompoundTag block=new CompoundTag();block.putString("id","minecraft:shulker_box");block.put("Items",contents);raw.getCompoundOrEmpty("tag").put("BlockEntityTag",block);CompoundTag caps=new CompoundTag();caps.putByteArray("retained",new byte[]{1,4,9});raw.put("ForgeCaps",caps); + CompoundTag result=ItemMigration.convertNbt(raw,context(922));assertNotNull(result);assertArrayEquals(bytes(caps),bytes(result.getCompoundOrEmpty("ForgeCaps")));CompoundTag recovered=typedList(result.getCompoundOrEmpty("tag").getCompoundOrEmpty("BlockEntityTag"), "Items", 10).getCompoundOrEmpty(0);assertEquals("minecraft:granite",recovered.getStringOr("id", ""));assertEquals(29,recovered.getByteOr("Count", (byte) 0));assertEquals(3,recovered.getByteOr("Slot", (byte) 0)); } @Test void currentNativeItemsAreNotFlattenedAgain()throws Exception { CompoundTag raw=item("minecraft:granite",0);raw.remove("Damage");assertArrayEquals(bytes(raw),bytes(ItemMigration.convertNbt(raw,context(3465)))); } @Test void bedColoursStillUseTheCorrectHistoricalContext() { - CompoundTag old=ItemMigration.convertNbt(item("minecraft:bed",0),context(922)),native112=ItemMigration.convertNbt(item("minecraft:bed",0),context(1343));assertNotNull(old);assertNotNull(native112);assertEquals("minecraft:red_bed",old.getString("id"));assertEquals("minecraft:white_bed",native112.getString("id")); + CompoundTag old=ItemMigration.convertNbt(item("minecraft:bed",0),context(922)),native112=ItemMigration.convertNbt(item("minecraft:bed",0),context(1343));assertNotNull(old);assertNotNull(native112);assertEquals("minecraft:red_bed",old.getStringOr("id", ""));assertEquals("minecraft:white_bed",native112.getStringOr("id", "")); } @Test void ambiguousConversionsRemainRawAndRetriable() { CompoundTag versions=context(922);versions.putBoolean("Ambiguous",true);versions.putIntArray("CandidateDataVersions",new int[]{922,1343});assertNull(ItemMigration.convertNbt(item("minecraft:bed",0),versions));assertNotNull(ItemMigration.convertNbt(item("minecraft:stone",1),versions)); @@ -43,27 +45,27 @@ class ItemNbtConversionTest { assertNull(ItemMigration.convertNbt(item("missing:variant",4),context(1343))); CompoundTag raw=item("minecraft:diamond_sword",0),enchantment=new CompoundTag(); enchantment.putShort("id",(short)200);enchantment.putShort("lvl",(short)5); - ListTag list=new ListTag();list.add(enchantment);raw.getCompound("tag").put("ench",list); + ListTag list=new ListTag();list.add(enchantment);raw.getCompoundOrEmpty("tag").put("ench",list); assertNull(ItemMigration.convertNbt(raw,context(1343))); } private CompoundTag original110(int slot)throws Exception { try(InputStream in=getClass().getResourceAsStream("/legacy-items/original-110.nbt")) { - ListTag list=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)).getList("Items",10); - for(Tag value:list){CompoundTag item=(CompoundTag)value;if(item.getByte("Slot")==slot)return item;} + ListTag list=NbtIo.readCompressed(in,net.minecraft.nbt.NbtAccounter.create(64L*1024*1024)).getListOrEmpty("Items"); + for(Tag value:list){CompoundTag item=(CompoundTag)value;if(item.getByteOr("Slot", (byte) 0)==slot)return item;} }throw new AssertionError("Missing sealed historical item"); } @Test void actualOriginalPigEggKeepsItsTypeAndSavedEntityName()throws Exception { CompoundTag raw=original110(6);byte[] before=bytes(raw);CompoundTag fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); - assertEquals("minecraft:pig_spawn_egg",fixed.getString("id"));assertEquals(raw.getByte("Count"),fixed.getByte("Count"));assertEquals("minecraft:pig",fixed.getCompound("tag").getCompound("EntityTag").getString("id")); - assertEquals("Saved pig egg",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("EntityTag").getString("CustomName"),cyano.lootable.buildtest.Native119Registries.lookup()).getString());assertArrayEquals(before,bytes(raw)); + assertEquals("minecraft:pig_spawn_egg",fixed.getStringOr("id", ""));assertEquals(raw.getByteOr("Count", (byte) 0),fixed.getByteOr("Count", (byte) 0));assertEquals("minecraft:pig",fixed.getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag").getStringOr("id", "")); + assertEquals("Saved pig egg",cyano.lootable.buildtest.TestNativeNbt.legacyJsonText(fixed.getCompoundOrEmpty("tag").getCompoundOrEmpty("EntityTag").getStringOr("CustomName", ""),cyano.lootable.buildtest.Native119Registries.lookup()).getString());assertArrayEquals(before,bytes(raw)); } @Test void actualOriginalChestKeepsItsNestedInventoryInNativeFormat()throws Exception { CompoundTag raw=original110(7),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed); - CompoundTag nested=fixed.getCompound("tag").getCompound("BlockEntityTag");assertEquals("minecraft:chest",nested.getString("id"));assertEquals("minecraft:emerald",nested.getList("Items",10).getCompound(0).getString("id"));assertEquals(17,nested.getList("Items",10).getCompound(0).getByte("Count"));assertFalse(fixed.contains("Damage")); - assertEquals("Original keepsake 7",net.minecraft.network.chat.Component.Serializer.fromJson(fixed.getCompound("tag").getCompound("display").getString("Name"),cyano.lootable.buildtest.Native119Registries.lookup()).getString()); + CompoundTag nested=fixed.getCompoundOrEmpty("tag").getCompoundOrEmpty("BlockEntityTag");assertEquals("minecraft:chest",nested.getStringOr("id", ""));assertEquals("minecraft:emerald",typedList(nested, "Items", 10).getCompoundOrEmpty(0).getStringOr("id", ""));assertEquals(17,typedList(nested, "Items", 10).getCompoundOrEmpty(0).getByteOr("Count", (byte) 0));assertFalse(fixed.contains("Damage")); + assertEquals("Original keepsake 7",cyano.lootable.buildtest.TestNativeNbt.legacyJsonText(fixed.getCompoundOrEmpty("tag").getCompoundOrEmpty("display").getStringOr("Name", ""),cyano.lootable.buildtest.Native119Registries.lookup()).getString()); } @Test void actualOriginalBannerKeepsItsBaseColour()throws Exception { - CompoundTag raw=original110(8),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed);assertEquals("minecraft:purple_banner",fixed.getString("id"));assertEquals(raw.getByte("Count"),fixed.getByte("Count"));// Vanilla 1.13 leaves its inverted, redundant Base field behind. - assertEquals(10,fixed.getCompound("tag").getCompound("BlockEntityTag").getInt("Base")); + CompoundTag raw=original110(8),fixed=ItemMigration.convertNbt(raw,context(512));assertNotNull(fixed);assertEquals("minecraft:purple_banner",fixed.getStringOr("id", ""));assertEquals(raw.getByteOr("Count", (byte) 0),fixed.getByteOr("Count", (byte) 0));// Vanilla 1.13 leaves its inverted, redundant Base field behind. + assertEquals(10,fixed.getCompoundOrEmpty("tag").getCompoundOrEmpty("BlockEntityTag").getIntOr("Base", 0)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java index ce5039f..aa6c57b 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114RegistryRenameTest.java @@ -1,7 +1,7 @@ package cyano.lootable.upgrade; import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.fixes.References; import org.junit.jupiter.api.Test; @@ -11,20 +11,20 @@ class Native114RegistryRenameTest { @Test void itemRegistryBridgesAgreeWithVanillasActualItemConversion() { for (String[] names : new String[][]{{"sign","oak_sign"},{"rose_red","red_dye"}, {"cactus_green","green_dye"},{"dandelion_yellow","yellow_dye"}}) { - ResourceLocation old = ResourceLocation.fromNamespaceAndPath("minecraft", names[0]); - ResourceLocation current = ResourceLocation.fromNamespaceAndPath("minecraft", names[1]); + Identifier old = Identifier.fromNamespaceAndPath("minecraft", names[0]); + Identifier current = Identifier.fromNamespaceAndPath("minecraft", names[1]); assertEquals(current, LegacyRegistryMappings.itemReplacement(old)); CompoundTag input = new CompoundTag(); input.putString("id", old.toString()); input.putByte("Count", (byte)11); CompoundTag converted = NativeNbt.update(DataFixers.getDataFixer(), References.ITEM_STACK, input, 1631, 1976); - assertEquals(current.toString(), converted.getString("id")); assertEquals(11, converted.getByte("Count")); + assertEquals(current.toString(), converted.getStringOr("id", "")); assertEquals(11, converted.getByteOr("Count", (byte) 0)); } } @Test void onlyTheTwoRetiredVanillaSignBlocksAreBridged() { - assertEquals(ResourceLocation.parse("minecraft:oak_sign"), LegacyRegistryMappings.blockReplacement(ResourceLocation.parse("minecraft:sign"))); - assertEquals(ResourceLocation.parse("minecraft:oak_wall_sign"), LegacyRegistryMappings.blockReplacement(ResourceLocation.parse("minecraft:wall_sign"))); + assertEquals(Identifier.parse("minecraft:oak_sign"), LegacyRegistryMappings.blockReplacement(Identifier.parse("minecraft:sign"))); + assertEquals(Identifier.parse("minecraft:oak_wall_sign"), LegacyRegistryMappings.blockReplacement(Identifier.parse("minecraft:wall_sign"))); for (String name : new String[]{"othermod:sign", "minecraft:unknown", "minecraft:oak_sign"}) { - assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse(name))); - assertNull(LegacyRegistryMappings.blockReplacement(ResourceLocation.parse(name))); + assertNull(LegacyRegistryMappings.itemReplacement(Identifier.parse(name))); + assertNull(LegacyRegistryMappings.blockReplacement(Identifier.parse(name))); } } } diff --git a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java index de033b7..3e9ecfc 100644 --- a/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native114UpgradeTest.java @@ -1,5 +1,9 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.readUuid; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import java.util.UUID; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -22,30 +26,30 @@ private CompoundTag item(int slot) { } private CompoundTag body(int marker) { CompoundTag body = new CompoundTag(); body.putString("id", "lootablebodies:corpse"); - body.putUUID("UUID", UUID.fromString("00000000-1144-4144-8144-000000000001")); + writeUuid(body, "UUID", UUID.fromString("00000000-1144-4144-8144-000000000001")); body.putInt(UpgradePreservation.VERSION, marker); ListTag equipment = new ListTag(); for (int i=0; i<48; i++) equipment.add(item(i)); body.put("Equipment", equipment); ListTag aux = new ListTag(); aux.add(item(47)); body.put("Aux", aux); return body; } @Test void currentFormatNamesThe119VanillaSchema() { - assertEquals(3955, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(12,UpgradePreservation.DATA_VERSION); + assertEquals(4671, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(13,UpgradePreservation.DATA_VERSION); } @Test void released113EquipmentAndOverflowAreRetainedBeforeNativeConversion() { CompoundTag original = body(3), before = original.copy(); CompoundTag staged = NativeSafeFixer.stageCorpse(original, context(1631)); - assertEquals(12,staged.getInt(UpgradePreservation.VERSION)); - assertEquals(original.getUUID("UUID"), staged.getUUID("UUID")); - assertTrue(staged.getList("Equipment", 10).isEmpty()); - ListTag records = staged.getList("LootableBodiesUnresolved", 10); assertEquals(49, records.size()); + assertEquals(13,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()); for (int i=0; i<48; i++) { - CompoundTag record = records.getCompound(i); - assertEquals("Equipment", record.getString("Source")); - assertEquals(original.getList("Equipment", 10).get(i), record.get("Data")); - assertEquals(1631, record.getCompound("SourceVersions").getInt("DataVersion")); + CompoundTag record = records.getCompoundOrEmpty(i); + assertEquals("Equipment", record.getStringOr("Source", "")); + assertEquals(typedList(original, "Equipment", 10).get(i), record.get("Data")); + assertEquals(1631, record.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); } - assertEquals("Aux", records.getCompound(48).getString("Source")); assertEquals(before, original); + assertEquals("Aux", records.getCompoundOrEmpty(48).getStringOr("Source", "")); assertEquals(before, original); } @Test void olderRecoveryPayloadsKeepTheirOriginalSourceAndData() { CompoundTag original = body(3), record = new CompoundTag(); @@ -53,9 +57,9 @@ 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(12,staged.getInt(UpgradePreservation.VERSION)); - assertEquals(record, staged.getList("LootableBodiesUnresolved", 10).getCompound(49)); - assertEquals(record, original.getList("LootableBodiesUnresolved", 10).getCompound(0)); + assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(record, typedList(staged, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(49)); + assertEquals(record, typedList(original, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(0)); } @Test void currentRecordsAreNotWrappedOrDuplicatedAgain() { CompoundTag original = body(UpgradePreservation.DATA_VERSION); @@ -65,8 +69,8 @@ private CompoundTag body(int marker) { UpgradePreservation.register(DataFixers.getDataFixer()); CompoundTag original = item(47), before = original.copy(); CompoundTag fixed = ItemMigration.convertNbt(original, context(1631)); - assertNotNull(fixed); assertEquals("minecraft:oak_sign", fixed.getString("id")); - assertEquals(11, fixed.getByte("Count")); assertEquals(47, fixed.getByte("Slot")); + assertNotNull(fixed); assertEquals("minecraft:oak_sign", fixed.getStringOr("id", "")); + assertEquals(11, fixed.getByteOr("Count", (byte) 0)); assertEquals(47, fixed.getByteOr("Slot", (byte) 0)); assertEquals(original.get("tag"), fixed.get("tag")); assertEquals(before, original); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java index 7a872f8..28dd6bd 100644 --- a/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native115UpgradeTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.IntTag; import net.minecraft.nbt.ListTag; @@ -33,22 +35,22 @@ private int[] currentBiomes() { return values; } @Test void currentFormatUsesThe119SchemaAndItsOwnMarker() { - assertEquals(3955, UpgradePreservation.MINECRAFT_VERSION); - assertEquals(12,UpgradePreservation.DATA_VERSION); + assertEquals(4671, UpgradePreservation.MINECRAFT_VERSION); + assertEquals(13,UpgradePreservation.DATA_VERSION); } @Test void currentBiomeValuesSurvivePlausibleOlderStampsAndRepeatedConversion() { int[] values = currentBiomes(); CompoundTag original = chunk(values), before = original.copy(); CompoundTag first = convert(original, 1976); - assertArrayEquals(values, first.getCompound("Level").getIntArray("Biomes")); + assertArrayEquals(values, first.getCompoundOrEmpty("Level").getIntArray("Biomes").orElseGet(() -> new int[0])); assertEquals(before, original); CompoundTag restamped = first.copy(); restamped.putInt("DataVersion", 1976); - assertArrayEquals(values, convert(restamped, 1976).getCompound("Level").getIntArray("Biomes")); + assertArrayEquals(values, convert(restamped, 1976).getCompoundOrEmpty("Level").getIntArray("Biomes").orElseGet(() -> new int[0])); } @Test void currentBiomeValuesSurviveMissingAndWronglyTypedStamps() { for (Tag stamp : new Tag[]{null, StringTag.valueOf("damaged"), IntTag.valueOf(-1)}) { int[] values = currentBiomes(); CompoundTag original = chunk(values); if (stamp == null) original.remove("DataVersion"); else original.put("DataVersion", stamp); - assertArrayEquals(values, convert(original, 1631).getCompound("Level").getIntArray("Biomes")); + assertArrayEquals(values, convert(original, 1631).getCompoundOrEmpty("Level").getIntArray("Biomes").orElseGet(() -> new int[0])); } } @Test void biomeProtectionStillRunsOlderVanillaEntityFixes() { @@ -56,16 +58,16 @@ private int[] currentBiomes() { original.putInt("DataVersion", 1631); CompoundTag villager = new CompoundTag(); villager.putString("id", "minecraft:villager"); villager.putInt("Profession", 1); villager.putInt("Career", 1); villager.putInt("CareerLevel", 3); - original.getCompound("Level").getList("Entities", 10).add(villager); + typedList(original.getCompoundOrEmpty("Level"), "Entities", 10).add(villager); CompoundTag before = original.copy(), fixed = convert(original, 1631); - assertArrayEquals(values, fixed.getCompound("Level").getIntArray("Biomes")); - assertEquals("minecraft:librarian", fixed.getCompound("Level").getList("Entities", 10) - .getCompound(0).getCompound("VillagerData").getString("profession")); + assertArrayEquals(values, fixed.getCompoundOrEmpty("Level").getIntArray("Biomes").orElseGet(() -> new int[0])); + assertEquals("minecraft:librarian", typedList(fixed.getCompoundOrEmpty("Level"), "Entities", 10) + .getCompoundOrEmpty(0).getCompoundOrEmpty("VillagerData").getStringOr("profession", "")); assertEquals(before, original); } @Test void historical256BiomeValuesStillUseNativeSampling() { int[] values = new int[256]; for (int i=0; i new int[0]); assertEquals(1024, fixed.length); for (int y=0; y<64; y++) for (int z=0; z<4; z++) for (int x=0; x<4; x++) assertEquals(values[(z*4+2)*16+x*4+2], fixed[y*16+z*4+x]); @@ -78,10 +80,10 @@ 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(12,staged.getInt(UpgradePreservation.VERSION)); - CompoundTag record = staged.getList("LootableBodiesUnresolved", 10).getCompound(0); - assertEquals(item, record.get("Data")); assertEquals("Equipment", record.getString("Source")); - assertEquals(1976, record.getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(13,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)); assertEquals(before, raw); } @Test void released114MarkerWinsOverAnOlderOuterEntityContext() { @@ -92,8 +94,8 @@ private int[] currentBiomes() { raw.put("Equipment", equipment); CompoundTag before = raw.copy(); CompoundTag fixed = NativeNbt.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), net.minecraft.util.datafix.fixes.References.ENTITY_TREE, raw, 1631, 2586); - assertEquals(1976, fixed.getList("LootableBodiesUnresolved", 10).getCompound(0) - .getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(1976, typedList(fixed, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(0) + .getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); assertEquals(before, raw); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java index 8531a6f..dedaf70 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116DamagedStampTest.java @@ -10,7 +10,8 @@ import net.minecraft.nbt.NbtIo; import net.minecraft.util.datafix.fixes.References; import net.minecraft.world.level.Level; -import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; +import net.minecraft.util.datafix.DataFixTypes; import org.junit.jupiter.api.*; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.CleanupMode; @@ -35,7 +36,7 @@ private void preserve(CompoundTag chunk)throws Exception { try(OutputStream output=Files.newOutputStream(world.resolve("level.dat"))){NbtIo.writeCompressed(header,output);} ByteArrayOutputStream bytes=new ByteArrayOutputStream();try(DataOutputStream output=new DataOutputStream(new DeflaterOutputStream(bytes))){NbtIo.write(chunk,output);} int sectors=(bytes.size()+5+4095)/4096;ByteBuffer region=ByteBuffer.allocate(8192+sectors*4096); - CompoundTag level=chunk.getCompound("Level");int index=(level.getInt("xPos")&31)+(level.getInt("zPos")&31)*32; + CompoundTag level=chunk.getCompoundOrEmpty("Level");int index=(level.getIntOr("xPos", 0)&31)+(level.getIntOr("zPos", 0)&31)*32; region.putInt(index*4,(2<<8)|sectors);region.position(8192);region.putInt(bytes.size()+1);region.put((byte)2);region.put(bytes.toByteArray()); Path file=world.resolve("region/r.0.0.mca");Files.createDirectories(file.getParent());Files.write(file,region.array());UpgradePreservation.prepare(world); } @@ -49,17 +50,24 @@ private void preserve(CompoundTag chunk)throws Exception { context.putInt("VerifiedChunkDataVersion",2586);assertEquals(2586,UpgradePreservation.chunkSourceVersion(context)); } @Test void bothNativeAnvilPassesRetainNestedChestDataWithMissingChunkStamps()throws Exception { - CompoundTag source=original();assertEquals(2230,source.getInt("DataVersion")); - CompoundTag nativeInput=source.copy();ChunkStorage.injectDatafixingContext(nativeInput,Level.OVERWORLD,java.util.Optional.empty()); - CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeInput,2230,3955); + CompoundTag source=original();assertEquals(2230,source.getIntOr("DataVersion", 0)); + CompoundTag context = new CompoundTag(); + 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 damaged=source.copy();damaged.remove("DataVersion");preserve(damaged); byte[] originalRegion=Files.readAllBytes(world.resolve("region/r.0.0.mca")); - try(ChunkStorage loader=new ChunkStorage(new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture",net.minecraft.world.level.Level.OVERWORLD,"chunk"),world.resolve("reader"),UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),false)) { - CompoundTag actual=loader.upgradeChunkTag(Level.OVERWORLD,()->{throw new AssertionError("Current source must not replay legacy structure conversion");},damaged.copy(),java.util.Optional.empty()); + try(SimpleRegionStorage loader = new SimpleRegionStorage( + new net.minecraft.world.level.chunk.storage.RegionStorageInfo("fixture", Level.OVERWORLD, "chunk"), + 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")); 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"))); - assertEquals(source.getCompound("Level").get("TileEntities"),original().getCompound("Level").get("TileEntities")); + assertEquals(source.getCompoundOrEmpty("Level").get("TileEntities"),original().getCompoundOrEmpty("Level").get("TileEntities")); } } } diff --git a/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java index 79ac1d6..4353cba 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116RegistryRenameTest.java @@ -1,28 +1,28 @@ package cyano.lootable.upgrade; import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.util.datafix.DataFixers; import net.minecraft.util.datafix.fixes.References; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class Native116RegistryRenameTest { @Test void zombiePigmanMappingsAgreeWithNativeEntityAndItemFixes(){ - ResourceLocation old=ResourceLocation.parse("minecraft:zombie_pigman"),current=ResourceLocation.parse("minecraft:zombified_piglin"); + Identifier old=Identifier.parse("minecraft:zombie_pigman"),current=Identifier.parse("minecraft:zombified_piglin"); assertEquals(current,LegacyRegistryMappings.entityReplacement(old)); - assertEquals(ResourceLocation.parse("minecraft:zombified_piglin_spawn_egg"),LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("minecraft:zombie_pigman_spawn_egg"))); + assertEquals(Identifier.parse("minecraft:zombified_piglin_spawn_egg"),LegacyRegistryMappings.itemReplacement(Identifier.parse("minecraft:zombie_pigman_spawn_egg"))); CompoundTag entity=new CompoundTag();entity.putString("id",old.toString()); - assertEquals(current.toString(),NativeNbt.update(DataFixers.getDataFixer(),References.ENTITY_TREE,entity,2230,2586).getString("id")); + assertEquals(current.toString(),NativeNbt.update(DataFixers.getDataFixer(),References.ENTITY_TREE,entity,2230,2586).getStringOr("id", "")); CompoundTag item=new CompoundTag();item.putString("id","minecraft:zombie_pigman_spawn_egg");item.putByte("Count",(byte)7); - assertEquals("minecraft:zombified_piglin_spawn_egg",NativeNbt.update(DataFixers.getDataFixer(),References.ITEM_STACK,item,2230,2586).getString("id")); + assertEquals("minecraft:zombified_piglin_spawn_egg",NativeNbt.update(DataFixers.getDataFixer(),References.ITEM_STACK,item,2230,2586).getStringOr("id", "")); } @Test void onlyExactRetiredVanillaSoundsAndBiomesAreRenamed(){ - for(String sound:new String[]{"ambient","angry","death","hurt"})assertEquals(ResourceLocation.parse("minecraft:entity.zombified_piglin."+sound),LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:entity.zombie_pigman."+sound))); - assertEquals(ResourceLocation.parse("minecraft:music.nether.nether_wastes"),LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:music.nether"))); - for(String key:new String[]{"othermod:zombie_pigman","minecraft:unrecognised","minecraft:zombified_piglin"})assertNull(LegacyRegistryMappings.entityReplacement(ResourceLocation.parse(key))); - assertNull(LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("othermod:music.nether"))); + for(String sound:new String[]{"ambient","angry","death","hurt"})assertEquals(Identifier.parse("minecraft:entity.zombified_piglin."+sound),LegacyRegistryMappings.soundReplacement(Identifier.parse("minecraft:entity.zombie_pigman."+sound))); + assertEquals(Identifier.parse("minecraft:music.nether.nether_wastes"),LegacyRegistryMappings.soundReplacement(Identifier.parse("minecraft:music.nether"))); + for(String key:new String[]{"othermod:zombie_pigman","minecraft:unrecognised","minecraft:zombified_piglin"})assertNull(LegacyRegistryMappings.entityReplacement(Identifier.parse(key))); + assertNull(LegacyRegistryMappings.soundReplacement(Identifier.parse("othermod:music.nether"))); } @Test void nativeBiomesUseTheirOwnDynamicRegistry() { - assertEquals(ResourceLocation.parse("minecraft:worldgen/biome"),net.minecraftforge.registries.ForgeRegistries.Keys.BIOMES.location()); + assertEquals(Identifier.parse("minecraft:worldgen/biome"),net.minecraftforge.registries.ForgeRegistries.Keys.BIOMES.identifier()); } @Test void failedMappingsEscapeTheNativeLevelReaderFallback() { Error absent=assertThrows(Error.class,()->LegacyRegistryMappings.requireMappingTarget(()->null)); diff --git a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java index f88b7b1..568c2cc 100644 --- a/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native116UpgradeTest.java @@ -1,5 +1,9 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.readUuid; +import static cyano.lootable.buildtest.TestNativeNbt.hasUuid; + import com.mojang.serialization.Dynamic; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -20,7 +24,7 @@ class Native116UpgradeTest { private static final UUID CORPSE=UUID.fromString("00000000-1111-4111-8111-000000000001"); private CompoundTag settings() throws Exception { - return TagParser.parseTag(new String(Files.readAllBytes( + return TagParser.parseCompoundFully(new String(Files.readAllBytes( Paths.get("src/test/resources/native116-worldgen.snbt")),StandardCharsets.UTF_8)); } private CompoundTag legacy() { @@ -36,23 +40,23 @@ private CompoundTag legacy() { body.put("Equipment",inventory);return body; } @Test void currentFormatUsesThe119SchemaAndMarker() { - assertEquals(3955,UpgradePreservation.MINECRAFT_VERSION);assertEquals(12,UpgradePreservation.DATA_VERSION); + assertEquals(4671,UpgradePreservation.MINECRAFT_VERSION);assertEquals(13,UpgradePreservation.DATA_VERSION); } @Test void legacyCorpseUuidSurvivesShieldingWithoutTruncatingCustomInventory() { CompoundTag body=legacy(),before=body.copy(),context=new CompoundTag();context.putInt("DataVersion",2230); CompoundTag fixed=NativeSafeFixer.stageCorpse(body,context); - assertTrue(fixed.hasUUID("UUID"));assertEquals(CORPSE,fixed.getUUID("UUID")); - assertEquals(48,fixed.getList("LootableBodiesUnresolved",10).size()); - for(int slot=0;slot<48;slot++)assertEquals(body.getList("Equipment",10).getCompound(slot), - fixed.getList("LootableBodiesUnresolved",10).getCompound(slot).getCompound("Data")); + assertTrue(hasUuid(fixed, "UUID"));assertEquals(CORPSE,readUuid(fixed, "UUID")); + assertEquals(48,typedList(fixed, "LootableBodiesUnresolved", 10).size()); + for(int slot=0;slot<48;slot++)assertEquals(typedList(body, "Equipment", 10).getCompoundOrEmpty(slot), + typedList(fixed, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(slot).getCompoundOrEmpty("Data")); assertEquals(before,body); } @Test void released115MarkerKeepsItsOwnItemContextUnderAnOlderOuterStamp() { CompoundTag body=legacy(),before=body.copy(); CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), References.ENTITY_TREE,body,512,2586); - assertEquals(2230,fixed.getList("LootableBodiesUnresolved",10).getCompound(0) - .getCompound("SourceVersions").getInt("DataVersion")); + assertEquals(2230,typedList(fixed, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(0) + .getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); assertEquals(before,body); } @Test void actualNativeCurrentSettingsSurviveOldAndMissingStampContexts() throws Exception { @@ -61,13 +65,13 @@ private CompoundTag legacy() { CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(DataFixers.getDataFixer()), References.WORLD_GEN_SETTINGS,raw,source,2586); assertEquals(before,fixed,"Source version "+source);assertEquals(before,raw); - assertEquals(4267321L,fixed.getLong("seed")); - assertEquals("minecraft:flat",fixed.getCompound("dimensions").getCompound("minecraft:overworld") - .getCompound("generator").getString("type")); + assertEquals(4267321L,fixed.getLongOr("seed", 0L)); + assertEquals("minecraft:flat",fixed.getCompoundOrEmpty("dimensions").getCompoundOrEmpty("minecraft:overworld") + .getCompoundOrEmpty("generator").getStringOr("type", "")); } } @Test void nonSingletonNbtOpsArePreservedWithCurrentWorldSettings() throws Exception { - NbtOps ops=new NbtOps(){};CompoundTag raw=settings(),before=raw.copy(); + var ops=cyano.lootable.buildtest.Native119Registries.lookup().createSerializationContext(NbtOps.INSTANCE);CompoundTag raw=settings(),before=raw.copy(); Dynamic result=UpgradePreservation.wrapper(DataFixers.getDataFixer()).update( References.WORLD_GEN_SETTINGS,new Dynamic(ops,raw),-1,2586); assertSame(ops,result.getOps());assertEquals(before,result.getValue());assertEquals(before,raw); diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java index 47104de..6d4cd75 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityChunkTest.java @@ -1,5 +1,8 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import java.nio.file.*; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -23,16 +26,16 @@ private CompoundTag convert(CompoundTag root,int source) { return NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,root,source,3955); } @Test void nativeEntityChunkShieldsAllSlotsAuxRecoveryAndPassengers() throws Exception { - CompoundTag corpse=body(ID,6),cart=new CompoundTag();cart.putString("id","minecraft:chest_minecart");cart.putUUID("UUID",new UUID(12,13)); + CompoundTag corpse=body(ID,6),cart=new CompoundTag();cart.putString("id","minecraft:chest_minecart");writeUuid(cart, "UUID", new UUID(12,13)); ListTag passengers=new ListTag();passengers.add(corpse);cart.put("Passengers",passengers); CompoundTag raw=chunk(2586,cart),before=raw.copy();Path region=write(world,raw);byte[] bytes=Files.readAllBytes(region); UpgradePreservation.prepare(world); - CompoundTag fixed=convert(raw,2586).getList("Entities",10).getCompound(0).getList("Passengers",10).getCompound(0); - assertEquals(12,fixed.getInt(UpgradePreservation.VERSION));assertEquals(2586,fixed.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); - ListTag records=fixed.getList("LootableBodiesUnresolved",10);assertEquals(50,records.size()); - for(int slot=0;slot<48;slot++) {assertEquals(corpse.getList("Equipment",10).getCompound(slot),records.getCompound(slot).getCompound("Data"));assertEquals("Equipment",records.getCompound(slot).getString("Source"));} - assertEquals(corpse.getList("Aux",10).getCompound(0),records.getCompound(48).getCompound("Data")); - assertEquals(corpse.getList("LootableBodiesUnresolved",10).getCompound(0),records.getCompound(49)); + 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)); + 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")); + assertEquals(typedList(corpse, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(0),records.getCompoundOrEmpty(49)); assertEquals(before,raw);assertArrayEquals(bytes,Files.readAllBytes(region)); } @Test void currentMarkerProtectsNativeRootAgainstDamagedStamps() { @@ -40,7 +43,7 @@ private CompoundTag convert(CompoundTag root,int source) { for(int version:new int[]{-1,0,512,2230,2586,2730,2975,3337,3465,3839,3955}) { CompoundTag raw=chunk(version,corpse),before=raw.copy(); CompoundTag fixed=convert(raw,version); - assertEquals(corpse,fixed.getList("Entities",10).getCompound(0));assertEquals(before,raw); + assertEquals(corpse,typedList(fixed, "Entities", 10).getCompoundOrEmpty(0));assertEquals(before,raw); } } @Test void malformedEntityChunkCannotReachNativeConstruction() { diff --git a/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java index 7584d89..e548213 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117EntityRegionTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import java.io.*; import java.nio.ByteBuffer; import java.nio.file.*; @@ -17,7 +19,7 @@ class Native117EntityRegionTest { @TempDir(cleanup=org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS) Path world; private static final UUID ID=UUID.fromString("00000000-1171-4171-8171-000000000001"); private CompoundTag body(UUID id) { - CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse");body.putUUID("UUID",id); + CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse");writeUuid(body, "UUID", id); body.putInt(UpgradePreservation.VERSION,7);body.putString("Name","Native 17 owner"); ListTag inventory=new ListTag(); for(int slot=0;slot<48;slot++) { @@ -51,9 +53,9 @@ private CompoundTag original(UpgradeJournal journal,UUID id,int x,int z) throws UpgradeJournal journal=UpgradeJournal.scan(world);assertEquals(4,journal.size()); for(int i=0;i file:before.entrySet())assertArrayEquals(file.getValue(),Files.readAllBytes(file.getKey())); } @@ -62,7 +64,7 @@ private CompoundTag original(UpgradeJournal journal,UUID id,int x,int z) throws CompoundTag saved=original(UpgradeJournal.scan(world),ID,0,0); int offset=ByteBuffer.wrap(before).getInt()>>>8,base=offset*4096,size=ByteBuffer.wrap(before,base,4).getInt(); byte[] original=Arrays.copyOfRange(before,base+5,base+4+size); - assertArrayEquals(original,Files.readAllBytes(world.resolve("data/lootablebodies-upgrade/chunks/"+saved.getString("OriginalChunk")+".bin"))); + assertArrayEquals(original,Files.readAllBytes(world.resolve("data/lootablebodies-upgrade/chunks/"+saved.getStringOr("OriginalChunk", "")+".bin"))); try(DataInputStream input=new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(original)))) { assertEquals(root,NbtIo.read(input,NbtAccounter.create(64L*1024*1024))); } @@ -94,10 +96,10 @@ private CompoundTag original(UpgradeJournal journal,UUID id,int x,int z) throws try(java.util.stream.Stream saved=Files.list(world.resolve("data/lootablebodies-upgrade/snapshots"))) {assertTrue(saved.findAny().isPresent());} } @Test void nestedPassengersArePreservedBeforeConstruction() throws Exception { - CompoundTag cart=new CompoundTag();cart.putString("id","minecraft:chest_minecart");cart.putUUID("UUID",new UUID(7,8)); + CompoundTag cart=new CompoundTag();cart.putString("id","minecraft:chest_minecart");writeUuid(cart, "UUID", new UUID(7,8)); ListTag passengers=new ListTag();passengers.add(body(ID));cart.put("Passengers",passengers); write("","entities",0,0,chunk(0,0,cart));CompoundTag saved=original(UpgradeJournal.scan(world),ID,0,0); - assertEquals(body(ID),saved.getCompound("Entity"));assertArrayEquals(new int[]{0},saved.getIntArray("PassengerPath")); + assertEquals(body(ID),saved.getCompoundOrEmpty("Entity"));assertArrayEquals(new int[]{0},saved.getIntArray("PassengerPath").orElseGet(() -> new int[0])); } @Test void unsupportedExternalStreamsRefuseSafelyAndRetainTheSidecar() throws Exception { Path file=write("","entities",0,0,chunk(0,0,body(ID)));byte[] raw=Files.readAllBytes(file); diff --git a/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java index 367a828..7a79cb3 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117ExternalSidecarTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import java.io.*; import java.nio.ByteBuffer; import java.nio.file.*; @@ -25,7 +27,7 @@ class Native117ExternalSidecarTest { assertEquals("358E4B91F290AFB219D85B09435B4A641035412686E31A09CF5C69F5A28515CB",headerHash); Files.copy(header,world.resolve("level.dat")); CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse"); - UUID identity=UUID.fromString("00000000-1171-4171-8171-000000000099");body.putUUID("UUID",identity); + UUID identity=UUID.fromString("00000000-1171-4171-8171-000000000099");writeUuid(body, "UUID", identity); body.putInt(UpgradePreservation.VERSION,7);body.putString("Name","Native external control"); ListTag equipment=new ListTag(); for(int slot=0;slot<48;slot++) { diff --git a/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java index 352512d..e444bc9 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117RegistryRenameTest.java @@ -1,28 +1,28 @@ package cyano.lootable.upgrade; import net.minecraft.core.Registry; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class Native117RegistryRenameTest { - final ResourceLocation oldPath=ResourceLocation.parse("minecraft:grass_path"); - final ResourceLocation dirtPath=ResourceLocation.parse("minecraft:dirt_path"); + final Identifier oldPath=Identifier.parse("minecraft:grass_path"); + final Identifier dirtPath=Identifier.parse("minecraft:dirt_path"); @Test void grassPathBlockUsesTheActualNativeDirtPath() { assertEquals(dirtPath,LegacyRegistryMappings.blockReplacement(oldPath)); - assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH,net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(dirtPath)); - assertNull(LegacyRegistryMappings.blockReplacement(ResourceLocation.parse("anothermod:grass_path"))); + assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH,net.minecraft.core.registries.BuiltInRegistries.BLOCK.get(dirtPath).orElseThrow().value()); + assertNull(LegacyRegistryMappings.blockReplacement(Identifier.parse("anothermod:grass_path"))); } @Test void grassPathItemUsesTheActualNativeDirtPathItem() { assertEquals(dirtPath,LegacyRegistryMappings.itemReplacement(oldPath)); - assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH.asItem(),net.minecraft.core.registries.BuiltInRegistries.ITEM.get(dirtPath)); - assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("anothermod:grass_path"))); + assertSame(net.minecraft.world.level.block.Blocks.DIRT_PATH.asItem(),net.minecraft.core.registries.BuiltInRegistries.ITEM.get(dirtPath).orElseThrow().value()); + assertNull(LegacyRegistryMappings.itemReplacement(Identifier.parse("anothermod:grass_path"))); } @Test void berryPickingSoundUsesTheActualNativeBlockSound() { - ResourceLocation old=ResourceLocation.parse("minecraft:item.sweet_berries.pick_from_bush"); - ResourceLocation current=ResourceLocation.parse("minecraft:block.sweet_berry_bush.pick_berries"); + Identifier old=Identifier.parse("minecraft:item.sweet_berries.pick_from_bush"); + Identifier current=Identifier.parse("minecraft:block.sweet_berry_bush.pick_berries"); assertEquals(current,LegacyRegistryMappings.soundReplacement(old)); - assertSame(net.minecraft.sounds.SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES,net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.get(current)); - assertNull(LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("anothermod:item.sweet_berries.pick_from_bush"))); + assertSame(net.minecraft.sounds.SoundEvents.SWEET_BERRY_BUSH_PICK_BERRIES,net.minecraft.core.registries.BuiltInRegistries.SOUND_EVENT.get(current).orElseThrow().value()); + assertNull(LegacyRegistryMappings.soundReplacement(Identifier.parse("anothermod:item.sweet_berries.pick_from_bush"))); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native117VanillaTest.java b/src/test/java/cyano/lootable/upgrade/Native117VanillaTest.java index 161c02a..d1fbe18 100644 --- a/src/test/java/cyano/lootable/upgrade/Native117VanillaTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native117VanillaTest.java @@ -11,15 +11,15 @@ class Native117VanillaTest { private CompoundTag flatSettings() throws Exception { - CompoundTag settings=TagParser.parseTag(Files.readString(Paths.get("src/test/resources/native116-worldgen.snbt"))); - settings.getCompound("dimensions").getCompound("minecraft:overworld").getCompound("generator") - .getCompound("settings").getList("layers",10).getCompound(2).putString("block","minecraft:grass_path"); + CompoundTag settings=TagParser.parseCompoundFully(Files.readString(Paths.get("src/test/resources/native116-worldgen.snbt"))); + settings.getCompoundOrEmpty("dimensions").getCompoundOrEmpty("minecraft:overworld").getCompoundOrEmpty("generator") + .getCompoundOrEmpty("settings").getListOrEmpty("layers").getCompoundOrEmpty(2).putString("block","minecraft:grass_path"); return settings; } private CompoundTag nativeSettings(CompoundTag input) { CompoundTag fixed=NativeNbt.update(NativeVanillaDataFixer.get(),References.WORLD_GEN_SETTINGS,input,2586,2730); - assertEquals("minecraft:dirt_path",fixed.getCompound("dimensions").getCompound("minecraft:overworld") - .getCompound("generator").getCompound("settings").getList("layers",10).getCompound(2).getString("block")); + assertEquals("minecraft:dirt_path",fixed.getCompoundOrEmpty("dimensions").getCompoundOrEmpty("minecraft:overworld") + .getCompoundOrEmpty("generator").getCompoundOrEmpty("settings").getListOrEmpty("layers").getCompoundOrEmpty(2).getStringOr("block", "")); return fixed; } @Test void completeSettingsMigrateFlatLayersEvenWithDamagedStamps() throws Exception { @@ -35,16 +35,16 @@ private CompoundTag nativeSettings(CompoundTag input) { level.put("WorldGenSettings",raw);level.putString("CustomKeepTag","retain me"); CompoundTag before=level.copy(); CompoundTag actual=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.LEVEL,level,-1,2730); - assertEquals(expected,actual.getCompound("WorldGenSettings")); - assertEquals("retain me",actual.getString("CustomKeepTag"));assertEquals(before,level); + assertEquals(expected,actual.getCompoundOrEmpty("WorldGenSettings")); + assertEquals("retain me",actual.getStringOr("CustomKeepTag", ""));assertEquals(before,level); } @Test void vanillaBlockAndItemRenamesUseTheActualNativeOracle() { CompoundTag block=new CompoundTag();block.putString("Name","minecraft:grass_path"); CompoundTag item=new CompoundTag();item.putString("id","minecraft:grass_path");item.putByte("Count",(byte)3); CompoundTag blockBefore=block.copy(),itemBefore=item.copy(); com.mojang.datafixers.DataFixer safe=UpgradePreservation.wrapper(NativeVanillaDataFixer.get()); - assertEquals("minecraft:dirt_path",NativeNbt.update(safe,References.BLOCK_STATE,block,2586,2730).getString("Name")); - assertEquals("minecraft:dirt_path",NativeNbt.update(safe,References.ITEM_STACK,item,2586,2730).getString("id")); + assertEquals("minecraft:dirt_path",NativeNbt.update(safe,References.BLOCK_STATE,block,2586,2730).getStringOr("Name", "")); + assertEquals("minecraft:dirt_path",NativeNbt.update(safe,References.ITEM_STACK,item,2586,2730).getStringOr("id", "")); assertEquals(blockBefore,block);assertEquals(itemBefore,item); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java index 0fdf49e..aa188a9 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118BiomeRegistryTest.java @@ -4,7 +4,7 @@ import net.minecraft.core.registries.Registries; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.StringTag; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.util.datafix.fixes.BiomeFix; import net.minecraft.util.datafix.fixes.CavesAndCliffsRenames; import net.minecraft.util.datafix.fixes.References; @@ -20,28 +20,28 @@ private String nativeName(String name,int source){ @Test void everyCavesAndCliffsAliasMatchesTheActualNativeFixAndRegisteredTarget(){ CavesAndCliffsRenames.RENAMES.forEach((old,current)->{ assertEquals(current,nativeName(old,2730),old); - ResourceLocation target=ResourceLocation.parse(current); + Identifier target=Identifier.parse(current); assertTrue(cyano.lootable.buildtest.Native119Registries.lookup().lookupOrThrow(Registries.BIOME) .get(net.minecraft.resources.ResourceKey.create(Registries.BIOME,target)).isPresent(),current); - assertEquals(target,LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(old)),old); + assertEquals(target,LegacyRegistryMappings.biomeReplacement(Identifier.parse(old)),old); }); } @Test void earlierNativeBiomeRenamesChainIntoTheCurrentNativeBiomeNames(){ BiomeFix.BIOMES.forEach((old,renamed)->{ String current=CavesAndCliffsRenames.RENAMES.getOrDefault(renamed,renamed); if(current.equals("minecraft:deep_warm_ocean"))current="minecraft:warm_ocean"; - ResourceLocation target=ResourceLocation.parse(current); + Identifier target=Identifier.parse(current); assertTrue(cyano.lootable.buildtest.Native119Registries.lookup().lookupOrThrow(Registries.BIOME) .get(net.minecraft.resources.ResourceKey.create(Registries.BIOME,target)).isPresent(),current); - assertEquals(target,LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(old)),old); + assertEquals(target,LegacyRegistryMappings.biomeReplacement(Identifier.parse(old)),old); }); } @Test void replacedNetherAndDeepWarmOceanFollowTheirSeparateActualNativeRules(){ for(String old:new String[]{"minecraft:nether","minecraft:deep_warm_ocean"}) - assertEquals(ResourceLocation.parse(nativeName(old,2230)),LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(old))); + assertEquals(Identifier.parse(nativeName(old,2230)),LegacyRegistryMappings.biomeReplacement(Identifier.parse(old))); } @Test void healthyUnknownAndOtherModNamesDoNotReceiveGuessedReplacements(){ for(String name:new String[]{"minecraft:plains","minecraft:forest","minecraft:unknown_saved_biome","anothermod:wooded_hills"}) - assertNull(LegacyRegistryMappings.biomeReplacement(ResourceLocation.parse(name)),name); + assertNull(LegacyRegistryMappings.biomeReplacement(Identifier.parse(name)),name); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java index c3e442d..bf9bde1 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EarlyHookTest.java @@ -1,81 +1,44 @@ package cyano.lootable.upgrade; -import com.google.gson.JsonParser; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.util.Map; -import javax.script.Invocable; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; +import java.lang.reflect.InvocationTargetException; import org.junit.jupiter.api.Test; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.*; -import org.objectweb.asm.util.CheckClassAdapter; +import org.objectweb.asm.tree.MethodInsnNode; import static org.junit.jupiter.api.Assertions.*; -/** Execute the shipped transformer against actual native18 bytecode. */ class Native118EarlyHookTest { - private static final String SAVE="net/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess"; - private static final String HOOK="cyano/lootable/upgrade/EarlyWorldPreservation"; - private ScriptEngine engine; - private Object transformer()throws Exception { - String path; - try(InputStream input=getClass().getClassLoader().getResourceAsStream("META-INF/coremods.json")) { - assertNotNull(input,"Early native18 protection must be attached before native header rewrite"); - com.google.gson.JsonObject config=JsonParser.parseReader(new InputStreamReader(input,StandardCharsets.UTF_8)).getAsJsonObject(); - assertTrue(config.has("lootablebodies_preservation"),"Production early-read attachment is missing"); - path=config.get("lootablebodies_preservation").getAsString(); - } - engine=Native119CoremodEngineTest.actualEngine();assertNotNull(engine,"Use the actual target Forge JavaScript engine/filter"); - try(InputStream input=getClass().getClassLoader().getResourceAsStream(path)) { - assertNotNull(input);engine.eval(new InputStreamReader(input,StandardCharsets.UTF_8)); - } - Map map=(Map)((Invocable)engine).invokeFunction("initializeCoreMod"); - Map spec=(Map)map.get("lootablebodies_preservation"); - Map target=(Map)spec.get("target");assertEquals("CLASS",target.get("type"));assertEquals(SAVE.replace('/','.'),target.get("name")); - return spec; + private static final String SAVE = "net/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess"; + private static final String HOOK = "cyano/lootable/upgrade/EarlyWorldPreservation"; + + @Test void requiredSaveHooksUseTheActualEarlyReadAndHeaderWriteSeams() throws Exception { + 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 write = Native119CoremodEngineTest.method(nativeClass, "saveDataTag", + "(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/nbt/CompoundTag;)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,"preserveBeforeWrite"), HOOK, "beforeWrite")); } - private ClassNode original()throws Exception { - ClassNode node=new ClassNode(Opcodes.ASM9); - try(InputStream input=getClass().getClassLoader().getResourceAsStream(SAVE+".class")) { - assertNotNull(input);new ClassReader(input).accept(node,0); + + @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"); + for (var instruction : write.instructions.toArray()) { + if (instruction instanceof MethodInsnNode call + && call.owner.equals("net/minecraftforge/common/ForgeHooks") + && call.name.equals("writeAdditionalLevelSaveData")) write.instructions.remove(instruction); } - return node; - } - private ClassNode apply(Object transformer,ClassNode node)throws Exception { - return (ClassNode)((Invocable)engine).invokeMethod(transformer,"transformer",node); + assertThrows(AssertionError.class, () -> Native119CoremodEngineTest.requiredCall( + write, "net/minecraftforge/common/ForgeHooks", "writeAdditionalLevelSaveData")); } - private int calls(ClassNode node,String name) { - int count=0;for(MethodNode method:node.methods)for(AbstractInsnNode instruction:method.instructions) - if(instruction instanceof MethodInsnNode){MethodInsnNode call=(MethodInsnNode)instruction;if(call.owner.equals(HOOK)&&call.name.equals(name))count++;} - return count; - } - @Test void actualNativeClassGetsExactlyOneEarlyReadAndOneHeaderWriteHook()throws Exception { - ClassNode before=original();Object transformer=transformer();ClassNode after=apply(transformer,before); - assertEquals(1,calls(after,"beforeRead"));assertEquals(1,calls(after,"beforeWrite")); - MethodNode read=after.methods.stream().filter(m->m.name.equals("getDataTag")&&m.desc.equals("(Z)Lcom/mojang/serialization/Dynamic;")).findFirst().orElseThrow(); - AbstractInsnNode first=read.instructions.getFirst();while(first.getOpcode()<0)first=first.getNext(); - assertEquals(Opcodes.ALOAD,first.getOpcode()); - assertEquals(Opcodes.ILOAD,first.getNext().getOpcode()); - assertTrue(first.getNext().getNext() instanceof MethodInsnNode); - assertEquals("beforeRead",((MethodInsnNode)first.getNext().getNext()).name); - ClassWriter writer=new ClassWriter(ClassWriter.COMPUTE_MAXS);after.accept(writer);assertTrue(writer.toByteArray().length>0); - StringWriter errors=new StringWriter();CheckClassAdapter.verify(new ClassReader(writer.toByteArray()),getClass().getClassLoader(),false,new PrintWriter(errors)); - assertEquals("",errors.toString(),"Actual native bytecode must pass operand-stack verification"); - apply(transformer,after);assertEquals(1,calls(after,"beforeRead"));assertEquals(1,calls(after,"beforeWrite")); - } - @Test void missingNativeWriteSeamIsFatalRatherThanAnUnprotectedLaunch()throws Exception { - Object transformer=transformer();ClassNode node=original(); - for(MethodNode method:node.methods)for(AbstractInsnNode i=method.instructions.getFirst();i!=null;){AbstractInsnNode next=i.getNext();if(i instanceof MethodInsnNode&&((MethodInsnNode)i).owner.equals("net/minecraftforge/common/ForgeHooks")&&((MethodInsnNode)i).name.equals("writeAdditionalLevelSaveData"))method.instructions.remove(i);i=next;} - assertThrows(Exception.class,()->apply(transformer,node)); - } - @Test void aDifferentClassCannotBeSilentlyTransformed()throws Exception { - Object transformer=transformer();ClassNode node=original();node.name="wrong/SaveAccess"; - assertThrows(Exception.class,()->apply(transformer,node)); + + @Test void attachmentOfADifferentClassCannotAuthorizeWorldReads() throws Exception { + var verifier = EarlyWorldPreservation.class.getDeclaredMethod("verifyAttachment",Class.class); + verifier.setAccessible(true); + class DifferentReader implements EarlyWorldPreservation.Attached { } + assertInstanceOf(IllegalStateException.class, assertThrows(InvocationTargetException.class, + () -> verifier.invoke(null, DifferentReader.class)).getCause()); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java index 3fa52a0..7b1efe0 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118EntityBoundaryTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import java.io.*; import java.nio.file.*; import java.util.UUID; @@ -23,15 +25,15 @@ class Native118EntityBoundaryTest { UpgradeJournal journal=UpgradePreservation.prepareEarly(world,world.resolve("level.dat")); assertEquals(1,journal.size()); CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3955); - CompoundTag actual=converted.getList("Entities",10).getCompound(0); - assertEquals(12,actual.getInt(UpgradePreservation.VERSION)); - assertEquals(2730,actual.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); - assertEquals(50,actual.getList("LootableBodiesUnresolved",10).size()); - for(int slot=0;slot<48;slot++)assertEquals(corpse.getList("Equipment",10).getCompound(slot),actual.getList("LootableBodiesUnresolved",10).getCompound(slot).getCompound("Data")); + CompoundTag actual=typedList(converted, "Entities", 10).getCompoundOrEmpty(0); + assertEquals(13,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",4000);root.putInt("xPos",0);root.putInt("zPos",0); + CompoundTag root=new CompoundTag();root.putInt("DataVersion",4672);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);} @@ -40,7 +42,7 @@ class Native118EntityBoundaryTest { assertNull(UpgradePreservation.journal());assertArrayEquals(original,Files.readAllBytes(file)); } @Test void futureSeparateEntityVersionRefusesEvenWhenItsCorpseHasAnOlderKnownMarker() throws Exception { - CompoundTag raw=chunk(3956,body(new UUID(118,172),7)),before=raw.copy(); + CompoundTag raw=chunk(4672,body(new UUID(118,172),7)),before=raw.copy(); Path file=write(world,raw);byte[] original=Files.readAllBytes(file); assertThrows(IOException.class,()->UpgradePreservation.prepareEarly(world,world.resolve("level.dat"))); assertNull(UpgradePreservation.journal());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); @@ -51,9 +53,9 @@ class Native118EntityBoundaryTest { UpgradePreservation.close();Path folder=world.resolve("typed-"+i); CompoundTag raw=chunk(2730,body(new UUID(118,173+i),7));raw.put("DataVersion",stamps[i]);CompoundTag before=raw.copy(); Path file=write(folder,raw);byte[] original=Files.readAllBytes(file);UpgradePreservation.prepareEarly(folder,folder.resolve("level.dat")); - CompoundTag converted=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.ENTITY_CHUNK,raw,-1,3955).getList("Entities",10).getCompound(0); - assertEquals(12,converted.getInt(UpgradePreservation.VERSION));assertEquals(2730,converted.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); - assertEquals(50,converted.getList("LootableBodiesUnresolved",10).size());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); + 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(50,typedList(converted, "LootableBodiesUnresolved", 10).size());assertEquals(before,raw);assertArrayEquals(original,Files.readAllBytes(file)); } } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java b/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java index 79bdcc6..0a6fa92 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118SavedPositionTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import java.util.UUID; import net.minecraft.nbt.*; import net.minecraft.util.datafix.fixes.References; @@ -28,13 +30,13 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { } return root; } - private CompoundTag first(CompoundTag root,boolean separate){return root.getList(separate?"Entities":"entities",10).getCompound(0);} + private CompoundTag first(CompoundTag root,boolean separate){return typedList(root, separate?"Entities":"entities", 10).getCompoundOrEmpty(0);} @Test void malformedAxisUsesContainingRegionBeforeEitherNativeLoaderConstructsAnEntity() { for(boolean separate:new boolean[]{false,true}) { CompoundTag body=body(ID,UpgradePreservation.DATA_VERSION);body.put("Pos",pos(Double.NaN,80,160.5)); CompoundTag raw=root(body,separate,-11,10),before=raw.copy();CompoundTag converted=fixed(raw,separate),actual=first(converted,separate); assertEquals(pos(-167.5,80,160.5),actual.get("Pos"));assertEquals(before,raw); - CompoundTag record=actual.getList(RECOVERY,10).getCompound(0);assertEquals("Pos",record.getString("Source"));assertEquals(body.get("Pos"),record.get("Data")); + CompoundTag record=typedList(actual, RECOVERY, 10).getCompoundOrEmpty(0);assertEquals("Pos",record.getStringOr("Source", ""));assertEquals(body.get("Pos"),record.get("Data")); for(String key:new String[]{"UUID","Name","Equipment","Aux","LootableBodiesUnresolved"})assertEquals(body.get(key),actual.get(key),key); assertEquals(converted,fixed(converted,separate),"Conversion accumulated records or moved an already repaired body"); } @@ -52,10 +54,10 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { } @Test void rawRecoveryTagSurvivesPositionRepairExactlyOnce() { CompoundTag body=body(ID,UpgradePreservation.DATA_VERSION);body.putString("Pos","opaque position");body.putString(RECOVERY,"future raw recovery"); - CompoundTag result=fixed(root(body,true,0,0),true),actual=first(result,true);ListTag records=actual.getList(RECOVERY,10); - assertEquals(2,records.size());assertEquals("Recovery",records.getCompound(0).getString("Source")); - assertEquals(StringTag.valueOf("future raw recovery"),records.getCompound(0).get("Data")); - assertEquals("Pos",records.getCompound(1).getString("Source"));assertEquals(StringTag.valueOf("opaque position"),records.getCompound(1).get("Data")); + CompoundTag result=fixed(root(body,true,0,0),true),actual=first(result,true);ListTag records=typedList(actual, RECOVERY, 10); + assertEquals(2,records.size());assertEquals("Recovery",records.getCompoundOrEmpty(0).getStringOr("Source", "")); + assertEquals(StringTag.valueOf("future raw recovery"),records.getCompoundOrEmpty(0).get("Data")); + assertEquals("Pos",records.getCompoundOrEmpty(1).getStringOr("Source", ""));assertEquals(StringTag.valueOf("opaque position"),records.getCompoundOrEmpty(1).get("Data")); assertEquals(result,fixed(result,true)); } @Test void impossibleContainingCoordinatesRefuseBeforeNativeConstruction() { @@ -67,7 +69,7 @@ private CompoundTag root(CompoundTag body,boolean separate,int x,int z) { CompoundTag body=body(ID,UpgradePreservation.DATA_VERSION);body.put("Pos",pos(8.5,y,8.5));CompoundTag raw=root(body,separate,0,0),before=raw.copy(); CompoundTag actual=first(fixed(raw,separate),separate); assertEquals(pos(8.5,64,8.5),actual.get("Pos"));assertEquals(before,raw); - assertEquals(body.get("Pos"),actual.getList(RECOVERY,10).getCompound(0).get("Data")); + assertEquals(body.get("Pos"),typedList(actual, RECOVERY, 10).getCompoundOrEmpty(0).get("Data")); for(String key:new String[]{"UUID","Equipment","Aux","LootableBodiesUnresolved"})assertEquals(body.get(key),actual.get(key)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java index 91b6ce8..475f3a4 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118TerrainBoundaryTest.java @@ -1,5 +1,9 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.readUuid; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import cyano.lootable.buildtest.NativeVanillaDataFixer; import java.io.DataOutputStream; import java.io.IOException; @@ -10,7 +14,7 @@ import net.minecraft.util.datafix.fixes.References; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; -import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; import net.minecraft.world.level.chunk.storage.RegionFile; import org.junit.jupiter.api.*; import org.junit.jupiter.api.io.*; @@ -18,6 +22,11 @@ /** Synthetic native-codec cases, distinct from genuine historical writer worlds. */ class Native118TerrainBoundaryTest { + private static CompoundTag overworldContext() { + CompoundTag context = new CompoundTag(); + context.putString("dimension", Level.OVERWORLD.identifier().toString()); + return context; + } @TempDir(cleanup=CleanupMode.ON_SUCCESS) Path world; @AfterEach void close(){UpgradePreservation.close();System.out.println("NATIVE118_TERRAIN_FIXTURE="+world);} private CompoundTag root(CompoundTag... bodies) { @@ -46,7 +55,7 @@ private CompoundTag legacy() { } @Test void missingStampOnModernThreeDimensionalBiomesStillRunsNativeHeightFix()throws Exception { CompoundTag old=legacy();int[] biomes=new int[1024];for(int i=0;iUpgradeJournal.scan(world));assertArrayEquals(original,Files.readAllBytes(file)); } @Test void actualNativeHeightAndRootConversionRetainsTheProtectedCorpse()throws Exception { - CompoundTag old=legacy(),level=old.getCompound("Level"); + CompoundTag old=legacy(),level=old.getCompoundOrEmpty("Level"); CompoundTag corpse=Native117RegionFixtures.body(UUID.randomUUID(),7);ListTag bodies=new ListTag();bodies.add(corpse);level.put("Entities",bodies); CompoundTag before=old.copy();Path file=write(old);byte[] original=Files.readAllBytes(file);UpgradePreservation.prepare(world); - ChunkStorage.injectDatafixingContext(old,Level.OVERWORLD,Optional.empty()); - CompoundTag vanilla=old.copy();vanilla.getCompound("Level").put("Entities",new ListTag()); + SimpleRegionStorage.injectDatafixingContext(old, overworldContext()); + CompoundTag vanilla=old.copy();vanilla.getCompoundOrEmpty("Level").put("Entities",new ListTag()); CompoundTag expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,vanilla,2730,3955); 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(fixed.getList("sections",10).size()>=16); - CompoundTag staged=fixed.getList("entities",10).getCompound(0);assertEquals(12,staged.getInt(UpgradePreservation.VERSION)); - assertEquals(50,staged.getList("LootableBodiesUnresolved",10).size());assertEquals(2730,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + 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)); + 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)); } @Test void nativeHeightConversionNormalizesARecoveredLegacyStampBeforeValidatingTheFlattenedResult()throws Exception { CompoundTag old=legacy(),body=Native117RegionFixtures.body(UUID.randomUUID(),7); - ListTag bodies=new ListTag();bodies.add(body);old.getCompound("Level").put("Entities",bodies); + ListTag bodies=new ListTag();bodies.add(body);old.getCompoundOrEmpty("Level").put("Entities",bodies); old.putInt("DataVersion",Integer.MAX_VALUE); Path file=write(old);byte[] original=Files.readAllBytes(file);UpgradePreservation.prepare(world); - ChunkStorage.injectDatafixingContext(old,Level.OVERWORLD,Optional.empty());CompoundTag before=old.copy(); + SimpleRegionStorage.injectDatafixingContext(old, overworldContext());CompoundTag before=old.copy(); // The native schema conversion leaves DataVersion to ChunkStorage's // caller. This stale stamp does not describe the newly converted root. - CompoundTag nativeCopy=old.copy();nativeCopy.getCompound("Level").put("Entities",new ListTag()); + CompoundTag nativeCopy=old.copy();nativeCopy.getCompoundOrEmpty("Level").put("Entities",new ListTag()); CompoundTag nativeResult=NativeNbt.update(NativeVanillaDataFixer.get(),References.CHUNK,nativeCopy,2730,3955); - assertFalse(nativeResult.contains("Level"));assertEquals(Integer.MAX_VALUE,nativeResult.getInt("DataVersion")); + assertFalse(nativeResult.contains("Level"));assertEquals(Integer.MAX_VALUE,nativeResult.getIntOr("DataVersion", 0)); CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,old,2730,3955); - assertFalse(fixed.contains("Level"));assertEquals(3955,fixed.getInt("DataVersion")); + assertFalse(fixed.contains("Level"));assertEquals(3955,fixed.getIntOr("DataVersion", 0)); assertEquals(nativeResult.get("sections"),fixed.get("sections")); - assertEquals(body.getUUID("UUID"),fixed.getList("entities",10).getCompound(0).getUUID("UUID")); + assertEquals(readUuid(body, "UUID"),readUuid(fixed.getListOrEmpty("entities").getCompoundOrEmpty(0), "UUID")); assertEquals(before,old);assertArrayEquals(original,Files.readAllBytes(file)); } @Test void genuinelyFutureFlattenedTerrainStillRefusesBeforeConversion()throws Exception { - CompoundTag future=root();future.putInt("DataVersion",3956);CompoundTag before=future.copy(); + CompoundTag future=root();future.putInt("DataVersion",4672);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,3956,3955)); + assertThrows(Error.class,()->NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.CHUNK,future,4672,4671)); assertEquals(before,future);assertArrayEquals(original,Files.readAllBytes(file)); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native118WorldgenTest.java b/src/test/java/cyano/lootable/upgrade/Native118WorldgenTest.java index be61c8b..a9fd97d 100644 --- a/src/test/java/cyano/lootable/upgrade/Native118WorldgenTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native118WorldgenTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import java.nio.file.*; import net.minecraft.nbt.*; import net.minecraft.util.datafix.fixes.References; @@ -9,8 +11,8 @@ class Native118WorldgenTest { private CompoundTag source()throws Exception { - CompoundTag settings=TagParser.parseTag(Files.readString(Paths.get("src/test/resources/native116-worldgen.snbt"))); - settings.getCompound("dimensions").getCompound("minecraft:overworld").getCompound("generator").getCompound("settings").getList("layers",10).getCompound(2).putString("block","minecraft:grass_path"); + CompoundTag settings=TagParser.parseCompoundFully(Files.readString(Paths.get("src/test/resources/native116-worldgen.snbt"))); + typedList(settings.getCompoundOrEmpty("dimensions").getCompoundOrEmpty("minecraft:overworld").getCompoundOrEmpty("generator").getCompoundOrEmpty("settings"), "layers", 10).getCompoundOrEmpty(2).putString("block","minecraft:grass_path"); return settings; } @Test void actualNative18GeneratorConversionRunsDespiteDamagedCompleteSettingsStamps()throws Exception { @@ -20,16 +22,16 @@ private CompoundTag source()throws Exception { CompoundTag actual=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.WORLD_GEN_SETTINGS,raw,stamp,2975); assertEquals(expected,actual,"stamp="+stamp);assertEquals(before,raw); } - ListTag layers=expected.getCompound("dimensions").getCompound("minecraft:overworld").getCompound("generator").getCompound("settings").getList("layers",10); + ListTag layers=typedList(expected.getCompoundOrEmpty("dimensions").getCompoundOrEmpty("minecraft:overworld").getCompoundOrEmpty("generator").getCompoundOrEmpty("settings"), "layers", 10); // Genuine 1.18 height conversion prepends 64 air blocks, retaining the // original third layer at index3 and the same world-space height. - assertEquals("minecraft:air",layers.getCompound(0).getString("block"));assertEquals(64,layers.getCompound(0).getInt("height")); - assertEquals("minecraft:dirt_path",layers.getCompound(3).getString("block")); + assertEquals("minecraft:air",layers.getCompoundOrEmpty(0).getStringOr("block", ""));assertEquals(64,layers.getCompoundOrEmpty(0).getIntOr("height", 0)); + assertEquals("minecraft:dirt_path",layers.getCompoundOrEmpty(3).getStringOr("block", "")); } @Test void levelProtectionKeepsUnknownTagsWhileItsGeneratorReachesNative18()throws Exception { CompoundTag raw=source(),level=new CompoundTag();level.put("WorldGenSettings",raw);level.putLong("CustomKeepTag",1234567890123L); CompoundTag before=level.copy(),expected=NativeNbt.update(NativeVanillaDataFixer.get(),References.WORLD_GEN_SETTINGS,raw.copy(),2586,2975); CompoundTag actual=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()),References.LEVEL,level,-1,2975); - assertEquals(expected,actual.get("WorldGenSettings"));assertEquals(1234567890123L,actual.getLong("CustomKeepTag"));assertEquals(before,level); + assertEquals(expected,actual.get("WorldGenSettings"));assertEquals(1234567890123L,actual.getLongOr("CustomKeepTag", 0L));assertEquals(before,level); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java index 395cf28..06e9e6c 100644 --- a/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119CoremodEngineTest.java @@ -1,146 +1,131 @@ package cyano.lootable.upgrade; +import com.google.gson.JsonParser; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.lang.reflect.InvocationTargetException; -import java.nio.file.Path; -import java.util.List; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.*; -import javax.script.Invocable; -import javax.script.ScriptEngine; +import java.util.Arrays; import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; import static org.junit.jupiter.api.Assertions.*; -/** Use the actual target CoreMods engine and its Java class-access filter. */ +/** Forge 61 uses required Mixins. Packaged runs separately exercise their actual application. */ class Native119CoremodEngineTest { - static void blackboardFixture()throws Exception { - Class launcher=Class.forName("cpw.mods.modlauncher.Launcher"); - if(launcher.getField("INSTANCE").get(null)==null) { - // Plain JUnit is not a ModLauncher module layer. The target engine - // only needs its native blackboard for one compatibility flag. - // Seed that fixture without running launcher services or any game. - Class unsafeType=Class.forName("sun.misc.Unsafe");Field singleton=unsafeType.getDeclaredField("theUnsafe");singleton.setAccessible(true); - Object fixture=unsafeType.getMethod("allocateInstance",Class.class).invoke(singleton.get(null),launcher); - Field blackboard=launcher.getDeclaredField("blackboard");blackboard.setAccessible(true); - blackboard.set(fixture,Class.forName("cpw.mods.modlauncher.api.TypesafeMap").getConstructor().newInstance()); - launcher.getField("INSTANCE").set(null,fixture); + static ClassNode classFile(String name) throws Exception { + try (var input = Native119CoremodEngineTest.class.getClassLoader().getResourceAsStream(name+".class")) { + assertNotNull(input, name); + ClassNode node = new ClassNode(); + new ClassReader(input).accept(node, 0); + return node; } } - static ScriptEngine actualEngine()throws Exception { - blackboardFixture(); - Class factory=Class.forName("net.minecraftforge.coremod.NashornFactory"); - String source=factory.getProtectionDomain().getCodeSource().getLocation().toString(); - assertTrue(source.endsWith("coremods-5.2.6.jar"),source);System.out.println("COREMOD_ENGINE_SOURCE="+source); - Method create=factory.getDeclaredMethod("createEngine");create.setAccessible(true); - return (ScriptEngine)create.invoke(null); - } - private Object actualTransformer(String className)throws Exception { - blackboardFixture();Class type=Class.forName("net.minecraftforge.coremod.CoreModEngine");Object engine=type.getConstructor().newInstance(); - Method load=java.util.Arrays.stream(type.getDeclaredMethods()).filter(m->m.getName().equals("loadCoreMod")).findFirst().orElseThrow();load.setAccessible(true); - Class api=load.getParameterTypes()[0];java.util.List readers=new java.util.ArrayList<>(); - Object file=Proxy.newProxyInstance(api.getClassLoader(),new Class[]{api},(proxy,method,args)->{ - switch(method.getName()) { - case "getPath": return Path.of("coremods/early-world-preservation.js"); - case "getOwnerId": return "lootablebodies"; - case "readCoreMod": - java.io.Reader reader=new InputStreamReader(getClass().getClassLoader().getResourceAsStream("coremods/early-world-preservation.js"),StandardCharsets.UTF_8);readers.add(reader);return reader; - case "toString": return "Local production coremod fixture"; - default: throw new AssertionError("Unexpected coremod file API: "+method); - } - }); - try { - load.invoke(engine,file);List transforms=(List)type.getMethod("initializeCoreMods").invoke(engine); - assertEquals(3,transforms.size(),"Actual native engine must install the three shipped transformers"); - return transforms.stream().map(item->(cpw.mods.modlauncher.api.ITransformer)item) - .filter(item->item.targets().stream().anyMatch(target->target.getClassName().replace('/','.').equals(className))) - .findFirst().orElseThrow(); - } finally {for(var reader:readers)reader.close();} - } - private static final String READER="net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess"; - private static final String FIXER="net.minecraft.util.datafix.DataFixers"; - private static final String DEDICATED_MAIN="net.minecraft.server.Main"; - private ClassNode nativeClass(String name)throws Exception { - ClassNode node=new ClassNode(Opcodes.ASM9); - try(var input=getClass().getClassLoader().getResourceAsStream(name.replace('.','/')+".class")) { - assertNotNull(input); - new ClassReader(input).accept(node,0); - } + static ClassNode mixin(String name, String target) throws Exception { + ClassNode node = classFile("cyano/lootable/mixin/"+name); + var declaration = node.invisibleAnnotations.stream() + .filter(annotation -> annotation.desc.equals("Lorg/spongepowered/asm/mixin/Mixin;")) + .findFirst().orElseThrow(); + assertTrue(declaration.values.stream().anyMatch(value -> value instanceof java.util.List types + && types.equals(java.util.List.of(Type.getObjectType(target)))), name); return node; } - private ClassNode transform(Object transformer,ClassNode node)throws Exception { - Method apply=transformer.getClass().getMethod("transform",Object.class,Class.forName("cpw.mods.modlauncher.api.ITransformerVotingContext")); - return (ClassNode)apply.invoke(transformer,node,null); + + static MethodNode method(ClassNode node, String name, String descriptor) { + var methods = node.methods.stream().filter(m -> m.name.equals(name) && m.desc.equals(descriptor)).toList(); + assertEquals(1, methods.size(), node.name+"."+name+descriptor); + return methods.getFirst(); + } + + static MethodNode named(ClassNode node, String name) { + var methods = node.methods.stream().filter(m -> m.name.equals(name)).toList(); + assertEquals(1, methods.size(), node.name+"."+name); + return methods.getFirst(); } - private Class define(ClassNode node) { - ClassWriter writer=new ClassWriter(ClassWriter.COMPUTE_MAXS);node.accept(writer);byte[] bytes=writer.toByteArray(); - class ReaderLoader extends ClassLoader {ReaderLoader(){super(Native119CoremodEngineTest.class.getClassLoader());}Class define(){return defineClass(node.name.replace('/','.'),bytes,0,bytes.length);}} - return new ReaderLoader().define(); + + static long calls(MethodNode method, String owner, String name) { + return Arrays.stream(method.instructions.toArray()).filter(instruction -> + instruction instanceof MethodInsnNode call && call.owner.equals(owner) && call.name.equals(name)).count(); } - private void verify(Class reader)throws Exception { - Method verify=EarlyWorldPreservation.class.getDeclaredMethod("verifyAttachment",Class.class);verify.setAccessible(true);verify.invoke(null,reader); + + static void requiredCall(MethodNode method, String owner, String name) { + assertEquals(1, calls(method, owner, name), method.name+" -> "+owner+"."+name); } - @Test void productionScriptInitializesUnderTheActualForgeClassFilter()throws Exception { - ScriptEngine engine=actualEngine(); - try(var input=getClass().getClassLoader().getResourceAsStream("coremods/early-world-preservation.js")) { - assertNotNull(input);engine.eval(new InputStreamReader(input,StandardCharsets.UTF_8)); + + @Test void packagedConfigRequiresEveryPreservationHook() throws Exception { + var loader = getClass().getClassLoader(); + assertNull(loader.getResource("META-INF/coremods.json")); + try (var input = loader.getResourceAsStream("lootablebodies.mixins.json")) { + assertNotNull(input); + 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"), + 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()); + assertEquals("CorpseRenderDispatcherMixin", config.getAsJsonArray("client").get(0).getAsString()); } - assertNotNull(((Invocable)engine).invokeFunction("initializeCoreMod")); } - @Test void nativeEngineTransformsActualReaderAndItsAttachmentIsVerifiable()throws Exception { - ClassNode transformed=transform(actualTransformer(READER),nativeClass(READER)); - verify(define(transformed)); + + @Test void nativeAccessorHasExactlyOneWrapper() throws Exception { + String target = "net/minecraft/util/datafix/DataFixers"; + method(classFile(target), "getDataFixer", "()Lcom/mojang/datafixers/DataFixer;"); + var mixin = mixin("DataFixersMixin",target); + requiredCall(named(mixin,"wrapNativeFixer"), "cyano/lootable/upgrade/UpgradePreservation","wrapper"); } - @Test void nativeEngineWrapsTheDataFixerAccessorExactlyOnce()throws Exception { - Object transformer=actualTransformer(FIXER); - ClassNode result=transform(transformer,nativeClass(FIXER)); - result=transform(transformer,result); - MethodNode accessor=result.methods.stream().filter(method->method.name.equals("getDataFixer") - && method.desc.equals("()Lcom/mojang/datafixers/DataFixer;")).findFirst().orElseThrow(); - int calls=0; - for(AbstractInsnNode instruction:accessor.instructions) { - if(instruction instanceof MethodInsnNode invoke && invoke.owner.equals("cyano/lootable/upgrade/UpgradePreservation")) { - assertEquals("wrapper",invoke.name); - assertEquals("(Lcom/mojang/datafixers/DataFixer;)Lcom/mojang/datafixers/DataFixer;",invoke.desc); - assertEquals(Opcodes.ARETURN,invoke.getNext().getOpcode()); - calls++; - } + + @Test void dedicatedReaderGuardsBothActualHeaderReads() throws Exception { + 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"); + var mixin = mixin("DedicatedWorldMixin",target); + for (String handler : java.util.List.of("readWorld","readFallback")) { + requiredCall(named(mixin,handler),"cyano/lootable/upgrade/EarlyWorldPreservation","readDedicatedWorld"); } - assertEquals(1,calls); } - @Test void nativeDedicatedReaderGuardsBothHeaderReads()throws Exception { - Object transformer=actualTransformer(DEDICATED_MAIN); - ClassNode result=transform(transformer,nativeClass(DEDICATED_MAIN)); - result=transform(transformer,result); - MethodNode main=result.methods.stream().filter(method->method.name.equals("main") - && method.desc.equals("([Ljava/lang/String;)V")).findFirst().orElseThrow(); - java.util.ArrayList guardedReads=new java.util.ArrayList<>(); - for(AbstractInsnNode instruction:main.instructions) { - if(instruction instanceof MethodInsnNode invoke && invoke.owner.equals("cyano/lootable/upgrade/EarlyWorldPreservation")) { - assertEquals("readDedicatedWorld",invoke.name); - assertEquals(Opcodes.INVOKESTATIC,invoke.getOpcode()); - guardedReads.add(invoke.getPrevious().getOpcode()); - } - } - assertEquals(List.of(Opcodes.ICONST_0,Opcodes.ICONST_1),guardedReads); - define(result); + + @Test void 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;"); + 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 serialSetupRefusesAnUntransformedReaderBeforeAnyWorldCanOpen()throws Exception { - Field installed=EarlyWorldPreservation.class.getDeclaredField("installed");installed.setAccessible(true);boolean before=installed.getBoolean(null); - try {installed.setBoolean(null,false);assertThrows(IllegalStateException.class,EarlyWorldPreservation::install);assertFalse(installed.getBoolean(null));} - finally {installed.setBoolean(null,before);} + + @Test void 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;"); + requiredCall(named(mixin("CapabilityDataMixin",target),"preserveOldCapabilities"), + "cyano/lootable/upgrade/UpgradePreservation","upgradeLevelCapabilities"); + } + + @Test void corpseSubmissionHookUsesTheNativeStateDispatcher() throws Exception { + String target = "net/minecraft/client/renderer/entity/EntityRenderDispatcher"; + var dispatch = method(classFile(target), "getRenderer", + "(Lnet/minecraft/client/renderer/entity/state/EntityRenderState;)Lnet/minecraft/client/renderer/entity/EntityRenderer;"); + assertTrue(Arrays.stream(dispatch.instructions.toArray()).anyMatch(instruction -> + instruction instanceof org.objectweb.asm.tree.TypeInsnNode type + && type.getOpcode() == org.objectweb.asm.Opcodes.INSTANCEOF + && type.desc.equals("net/minecraft/client/renderer/entity/state/AvatarRenderState"))); + requiredCall(named(mixin("CorpseRenderDispatcherMixin", target), "useCorpseRenderer"), + "org/spongepowered/asm/mixin/injection/callback/CallbackInfoReturnable", "setReturnValue"); } - @Test void nativeWrapperCannotTurnAFailedTransformIntoAnAcceptedAttachment()throws Exception { - ClassNode node=nativeClass(READER);Object transformer=actualTransformer(READER); - for(MethodNode method:node.methods)for(AbstractInsnNode i=method.instructions.getFirst();i!=null;){AbstractInsnNode next=i.getNext();if(i instanceof MethodInsnNode&&((MethodInsnNode)i).owner.equals("net/minecraftforge/common/ForgeHooks")&&((MethodInsnNode)i).name.equals("writeAdditionalLevelSaveData")){method.instructions.insertBefore(i,new InsnNode(Opcodes.POP2));method.instructions.remove(i);}i=next;} - ClassNode returned=transform(transformer,node); - InvocationTargetException refused=assertThrows(InvocationTargetException.class,()->verify(define(returned))); - assertInstanceOf(IllegalStateException.class,refused.getCause()); + + @Test void serialSetupRefusesAnUnattachedNativeReaderBeforeAnyWorldCanOpen() throws Exception { + var installed = EarlyWorldPreservation.class.getDeclaredField("installed"); + installed.setAccessible(true); + boolean before = installed.getBoolean(null); + try { + installed.setBoolean(null,false); + assertThrows(IllegalStateException.class,EarlyWorldPreservation::install); + assertFalse(installed.getBoolean(null)); + } finally { + installed.setBoolean(null,before); + } } } diff --git a/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java b/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java index e69b65d..4d894eb 100644 --- a/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native119RegistrySnapshotTest.java @@ -6,20 +6,19 @@ import net.minecraft.core.registries.Registries; import net.minecraft.nbt.*; import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.util.datafix.fixes.References; -import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.registries.MissingMappingsEvent; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; /** Actual Forge45 mapping actions and native PoI data fixes, without a world. */ class Native119RegistrySnapshotTest { + @org.junit.jupiter.api.BeforeAll static void register() { LegacyRegistryMappings.register(); } private MissingMappingsEvent.Action action(ResourceKey> registry,String name)throws Exception { - MissingMappingsEvent.Mapping mapping=new MissingMappingsEvent.Mapping<>(null,null,ResourceLocation.parse(name),17); + MissingMappingsEvent.Mapping mapping=new MissingMappingsEvent.Mapping<>(null,null,Identifier.parse(name),17); MissingMappingsEvent event=new MissingMappingsEvent(registry,null,List.of(mapping)); - for(java.lang.reflect.Method method:LegacyRegistryMappings.class.getDeclaredMethods()) - if(method.isAnnotationPresent(SubscribeEvent.class) && Arrays.equals(method.getParameterTypes(),new Class[]{MissingMappingsEvent.class}))method.invoke(null,event); + MissingMappingsEvent.BUS.post(event); java.lang.reflect.Field field=mapping.getClass().getDeclaredField("action");field.setAccessible(true); return (MissingMappingsEvent.Action)field.get(mapping); } @@ -27,7 +26,7 @@ private MissingMappingsEvent.Action action(ResourceKey> re List> keys=cyano.lootable.buildtest.Native119Registries.lookup() .lookupOrThrow(Registries.BIOME).listElements().map(net.minecraft.core.Holder.Reference::key).toList(); assertTrue(keys.size()>50); - for(ResourceKey key:keys)assertEquals(MissingMappingsEvent.Action.IGNORE,action(Registries.BIOME,key.location().toString()),key.location().toString()); + for(ResourceKey key:keys)assertEquals(MissingMappingsEvent.Action.IGNORE,action(Registries.BIOME,key.identifier().toString()),key.identifier().toString()); for(String old:net.minecraft.util.datafix.fixes.CavesAndCliffsRenames.RENAMES.keySet()) assertEquals(MissingMappingsEvent.Action.IGNORE,action(Registries.BIOME,old),old); } @@ -50,7 +49,7 @@ private MissingMappingsEvent.Action action(ResourceKey> re section.put("Records",records);sections.put("0",section);root.put("Sections",sections);CompoundTag before=root.copy(); Tag fixed=net.minecraft.util.datafix.DataFixers.getDataFixer().update(References.POI_CHUNK,new Dynamic<>(NbtOps.INSTANCE,root),2975,3465).getValue(); assertEquals(before,root,"Native conversion must operate on copies"); - ListTag result=((CompoundTag)fixed).getCompound("Sections").getCompound("0").getList("Records",10); - assertEquals(1,result.size());assertEquals("minecraft:farmer",result.getCompound(0).getString("type")); + ListTag result=((CompoundTag)fixed).getCompoundOrEmpty("Sections").getCompoundOrEmpty("0").getListOrEmpty("Records"); + assertEquals(1,result.size());assertEquals("minecraft:farmer",result.getCompoundOrEmpty(0).getStringOr("type", "")); } } diff --git a/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java index f80c635..d0bd478 100644 --- a/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native1206ComponentsTest.java @@ -1,9 +1,15 @@ package cyano.lootable.upgrade; +import cyano.lootable.buildtest.NativeVanillaDataFixer; + +import static cyano.lootable.buildtest.TestNativeNbt.typedList; +import static cyano.lootable.buildtest.TestNativeNbt.writeUuid; + import net.minecraft.SharedConstants; import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.ListTag; import net.minecraft.util.datafix.DataFixers; import net.minecraft.world.item.ItemStack; @@ -33,10 +39,10 @@ private static CompoundTag legacy(String id,int count,int slot) { return stack; } - @Test void targetUsesTheActual3955NativeSchema() { - assertEquals(3955,SharedConstants.getCurrentVersion().getDataVersion().getVersion()); - assertEquals(3955,UpgradePreservation.MINECRAFT_VERSION); - assertEquals(12,UpgradePreservation.DATA_VERSION); + @Test void targetUsesTheActual4671NativeSchema() { + assertEquals(4671,SharedConstants.getCurrentVersion().dataVersion().version()); + assertEquals(4671,UpgradePreservation.MINECRAFT_VERSION); + assertEquals(13,UpgradePreservation.DATA_VERSION); } @Test void released120ItemShapePreservesDamageEnchantmentsAndTypedCustomData() { @@ -57,29 +63,63 @@ private static CompoundTag legacy(String id,int count,int slot) { CompoundTag before=original.copy(); CompoundTag converted=ItemMigration.convert(original,context(3465)); assertNotNull(converted); - assertEquals(47,converted.getByte("Slot")); - assertEquals(1,converted.getInt("count")); + assertEquals(47,converted.getByteOr("Slot", (byte) 0)); + assertEquals(1,converted.getIntOr("count", 0)); assertFalse(converted.contains("Count")); - CompoundTag components=converted.getCompound("components"); - assertEquals(27,components.getInt("minecraft:damage")); - assertEquals(3,components.getCompound("minecraft:enchantments").getCompound("levels").getInt("minecraft:sharpness")); - assertArrayEquals(tag.getLongArray("keepsake"),components.getCompound("minecraft:custom_data").getLongArray("keepsake")); + CompoundTag components=converted.getCompoundOrEmpty("components"); + assertEquals(27,components.getIntOr("minecraft:damage", 0)); + assertEquals(3,components.getCompoundOrEmpty("minecraft:enchantments").getIntOr("minecraft:sharpness", 0)); + assertArrayEquals(tag.getLongArray("keepsake").orElseGet(() -> new long[0]),components.getCompoundOrEmpty("minecraft:custom_data").getLongArray("keepsake").orElseGet(() -> new long[0])); HolderLookup.Provider registry=net.minecraft.data.registries.VanillaRegistries.createLookup(); - ItemStack stack=ItemStack.parse(registry,converted).orElseThrow(); + ItemStack stack=ItemStack.CODEC.parse(registry.createSerializationContext(NbtOps.INSTANCE), converted).getOrThrow(); assertSame(Items.DIAMOND_SWORD,stack.getItem()); assertEquals(27,stack.getDamageValue()); - CompoundTag encoded=((CompoundTag)stack.save(registry)).getCompound("components"); - assertEquals("\"Old Blade\"",encoded.getString("minecraft:custom_name")); + CompoundTag encoded=cyano.lootable.buildtest.TestNativeNbt.saveStack(stack,registry).getCompoundOrEmpty("components"); + assertEquals("Old Blade",stack.getHoverName().getString()); assertTrue(ItemMigration.sameSavedComponents(components,encoded)); CompoundTag changed=components.copy(); - changed.putString("minecraft:custom_name","{\"text\":\"Old Blade\",\"italic\":false}"); + CompoundTag explicitStyle = new CompoundTag(); + explicitStyle.putString("text","Old Blade"); + explicitStyle.putBoolean("italic",false); + changed.put("minecraft:custom_name",explicitStyle); assertFalse(ItemMigration.sameSavedComponents(changed,encoded)); changed=components.copy(); - changed.getCompound("minecraft:custom_data").putString("extra","must retain"); + changed.getCompoundOrEmpty("minecraft:custom_data").putString("extra","must retain"); assertFalse(ItemMigration.sameSavedComponents(changed,encoded)); assertEquals(before,original); } + @Test void original110SpawnEggKeepsNativeEntityDataAndCustomName() { + CompoundTag egg = legacy("spawn_egg",3,40); + egg.putShort("Damage",(short)0); + CompoundTag pig = new CompoundTag(); + pig.putString("id","Pig"); + pig.putString("CustomName","Saved pig"); + CompoundTag tag = new CompoundTag(); + tag.put("EntityTag",pig); + tag.putString("keepsake","old egg"); + egg.put("tag",tag); + CompoundTag original = egg.copy(); + + CompoundTag legacy = ItemMigration.convertNbt(egg,context(512)); + assertNotNull(legacy); + CompoundTag at121 = NativeNbt.update(NativeVanillaDataFixer.get(), + net.minecraft.util.datafix.fixes.References.ITEM_STACK,legacy.copy(),3817,3955); + CompoundTag at12111 = NativeNbt.update(NativeVanillaDataFixer.get(), + net.minecraft.util.datafix.fixes.References.ITEM_STACK,at121.copy(),3955,4671); + CompoundTag converted = ItemMigration.convert(egg,context(512)); + assertNotNull(converted, () -> at121+" -> "+at12111); + assertEquals("minecraft:pig_spawn_egg",converted.getStringOr("id","")); + assertEquals(3,converted.getIntOr("count",0)); + assertEquals(40,converted.getByteOr("Slot",(byte)-1)); + var registry=cyano.lootable.buildtest.Native119Registries.lookup(); + var stack=ItemStack.CODEC.parse(registry.createSerializationContext(NbtOps.INSTANCE),converted).getOrThrow(); + var encoded=cyano.lootable.buildtest.TestNativeNbt.saveStack(stack,registry); + assertTrue(ItemMigration.sameSavedComponents(converted.getCompoundOrEmpty("components"), + encoded.getCompoundOrEmpty("components"))); + assertEquals(original,egg); + } + @Test void original110BannerKeepsItsFlattenedColourAndComponentData() { CompoundTag original=legacy("banner",2,8); original.putShort("Damage",(short)0); @@ -96,13 +136,13 @@ private static CompoundTag legacy(String id,int count,int slot) { assertNotNull(flattened); CompoundTag migrated=ItemMigration.convert(original,context(512)); assertNotNull(migrated,flattened::toString); - assertEquals("minecraft:purple_banner",migrated.getString("id")); - assertEquals(2,migrated.getInt("count")); - assertEquals(8,migrated.getByte("Slot")); - CompoundTag components=migrated.getCompound("components"); - assertEquals("purple",components.getString("minecraft:base_color")); - assertEquals(1234567890123L,components.getCompound("minecraft:custom_data").getLong("typed_long")); - assertArrayEquals(new byte[]{0,-1,17},components.getCompound("minecraft:custom_data").getByteArray("typed_bytes")); + assertEquals("minecraft:purple_banner",migrated.getStringOr("id", "")); + assertEquals(2,migrated.getIntOr("count", 0)); + assertEquals(8,migrated.getByteOr("Slot", (byte) 0)); + CompoundTag components=migrated.getCompoundOrEmpty("components"); + assertEquals("purple",components.getStringOr("minecraft:base_color", "")); + assertEquals(1234567890123L,components.getCompoundOrEmpty("minecraft:custom_data").getLongOr("typed_long", 0L)); + assertArrayEquals(new byte[]{0,-1,17},components.getCompoundOrEmpty("minecraft:custom_data").getByteArray("typed_bytes").orElseGet(() -> new byte[0])); assertEquals(before,original); } @@ -129,12 +169,12 @@ private static CompoundTag legacy(String id,int count,int slot) { original.put("tag",tag); CompoundTag converted=ItemMigration.convert(original,context(3465)); assertNotNull(converted); - ListTag slots=converted.getCompound("components").getList("minecraft:container",10); + ListTag slots=typedList(converted.getCompoundOrEmpty("components"), "minecraft:container", 10); assertEquals(1,slots.size()); - assertEquals(3,slots.getCompound(0).getInt("slot")); - assertEquals("minecraft:emerald",slots.getCompound(0).getCompound("item").getString("id")); - assertEquals(29,slots.getCompound(0).getCompound("item").getInt("count")); - assertEquals(3,children.getCompound(0).getByte("Slot")); + assertEquals(3,slots.getCompoundOrEmpty(0).getIntOr("slot", 0)); + assertEquals("minecraft:emerald",slots.getCompoundOrEmpty(0).getCompoundOrEmpty("item").getStringOr("id", "")); + assertEquals(29,slots.getCompoundOrEmpty(0).getCompoundOrEmpty("item").getIntOr("count", 0)); + assertEquals(3,children.getCompoundOrEmpty(0).getByteOr("Slot", (byte) 0)); } @Test void legacyCapabilityPayloadAndDuplicateNestedSlotsAreLeftForRecovery() { @@ -160,17 +200,17 @@ private static CompoundTag legacy(String id,int count,int slot) { } @Test void registryAliasesAgreeWithNative1206Fixers() { - var from=net.minecraft.resources.ResourceLocation.withDefaultNamespace("grass"); + var from=net.minecraft.resources.Identifier.withDefaultNamespace("grass"); var block=new CompoundTag(); block.putString("Name",from.toString()); var fixed=NativeNbt.update(DataFixers.getDataFixer(),net.minecraft.util.datafix.fixes.References.BLOCK_STATE, block,3465,3955); - assertEquals(fixed.getString("Name"),LegacyRegistryMappings.blockReplacement(from).toString()); + assertEquals(fixed.getStringOr("Name", ""),LegacyRegistryMappings.blockReplacement(from).toString()); for(String name:new String[]{"grass","scute"}) { - var key=net.minecraft.resources.ResourceLocation.withDefaultNamespace(name); + var key=net.minecraft.resources.Identifier.withDefaultNamespace(name); var migrated=ItemMigration.convert(legacy(name,2,47),context(3465)); assertNotNull(migrated); - assertEquals(migrated.getString("id"),LegacyRegistryMappings.itemReplacement(key).toString()); + assertEquals(migrated.getStringOr("id", ""),LegacyRegistryMappings.itemReplacement(key).toString()); } var item=legacy("diamond_sword",1,6); CompoundTag tag=new CompoundTag(), enchant=new CompoundTag(); @@ -182,14 +222,14 @@ private static CompoundTag legacy(String id,int count,int slot) { item.put("tag",tag); var migrated=ItemMigration.convert(item,context(3465)); assertNotNull(migrated); - var sweeping=net.minecraft.resources.ResourceLocation.withDefaultNamespace("sweeping"); + var sweeping=net.minecraft.resources.Identifier.withDefaultNamespace("sweeping"); var replacement=LegacyRegistryMappings.enchantmentReplacement(sweeping); - assertEquals(2,migrated.getCompound("components").getCompound("minecraft:enchantments").getCompound("levels").getInt(replacement.toString())); - assertNull(LegacyRegistryMappings.itemReplacement(net.minecraft.resources.ResourceLocation.tryParse("other:scute"))); + assertEquals(2,migrated.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:enchantments").getIntOr(replacement.toString(), 0)); + assertNull(LegacyRegistryMappings.itemReplacement(net.minecraft.resources.Identifier.tryParse("other:scute"))); } @Test void emptyPotionRemainsEmptyWithoutAnArtificialRegistryAlias() { - var key=net.minecraft.resources.ResourceLocation.withDefaultNamespace("empty"); + var key=net.minecraft.resources.Identifier.withDefaultNamespace("empty"); assertTrue(LegacyRegistryMappings.retiredPotion(key)); assertFalse(BuiltInRegistries.POTION.containsKey(key)); CompoundTag original=legacy("potion",1,6), tag=new CompoundTag(); @@ -197,8 +237,8 @@ private static CompoundTag legacy(String id,int count,int slot) { original.put("tag",tag); var migrated=ItemMigration.convert(original,context(3465)); assertNotNull(migrated); - assertFalse(migrated.getCompound("components").getCompound("minecraft:potion_contents").contains("potion")); - assertEquals("minecraft:empty",original.getCompound("tag").getString("Potion")); + assertFalse(migrated.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:potion_contents").contains("potion")); + assertEquals("minecraft:empty",original.getCompoundOrEmpty("tag").getStringOr("Potion", "")); } @Test void validCurrentComponentItemIsCopiedWithoutLegacyConversion() { @@ -206,9 +246,9 @@ private static CompoundTag legacy(String id,int count,int slot) { ItemStack stack=new ItemStack(Items.DIAMOND,31); stack.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("Saved keepsake")); - CompoundTag current=(CompoundTag)stack.save(registry); + CompoundTag current=cyano.lootable.buildtest.TestNativeNbt.saveStack(stack,registry); current.putByte("Slot",(byte)47); - CompoundTag converted=ItemMigration.convert(current,context(3955)); + CompoundTag converted=ItemMigration.convert(current,context(4671)); assertEquals(current,converted); assertNotSame(current,converted); } @@ -218,17 +258,24 @@ private static CompoundTag legacy(String id,int count,int slot) { CompoundTag original=old.copy(); CompoundTag migrated=ItemMigration.convert(old,context(3839)); assertNotNull(migrated); - assertEquals(47,migrated.getByte("Slot")); - CompoundTag modifier=migrated.getCompound("components").getCompound("minecraft:attribute_modifiers") - .getList("modifiers",10).getCompound(0); - assertEquals("minecraft:00000000-0000-0065-0000-0000000000ca",modifier.getString("id")); - assertEquals(2.75,modifier.getDouble("amount")); - assertEquals("add_value",modifier.getString("operation")); - assertEquals("mainhand",modifier.getString("slot")); + assertEquals(47,migrated.getByteOr("Slot", (byte) 0)); + CompoundTag migratedComponents = migrated.getCompoundOrEmpty("components"); + CompoundTag modifier = migratedComponents.getListOrEmpty("minecraft:attribute_modifiers").getCompoundOrEmpty(0); + assertEquals("minecraft:attack_damage", modifier.getStringOr("type", "")); + CompoundTag tooltip = migratedComponents.getCompoundOrEmpty("minecraft:tooltip_display"); + assertFalse(tooltip.getBooleanOr("hide_tooltip", true)); + assertEquals("minecraft:attribute_modifiers", tooltip.getListOrEmpty("hidden_components").getStringOr(0, "")); + assertEquals("minecraft:00000000-0000-0065-0000-0000000000ca",modifier.getStringOr("id", "")); + assertEquals(2.75,modifier.getDoubleOr("amount", 0D)); + assertEquals("add_value",modifier.getStringOr("operation", "")); + assertEquals("mainhand",modifier.getStringOr("slot", "")); assertFalse(modifier.contains("uuid")); assertEquals(original,old); HolderLookup.Provider registry=net.minecraft.data.registries.VanillaRegistries.createLookup(); - assertSame(Items.DIAMOND_SWORD,ItemStack.parse(registry,migrated).orElseThrow().getItem()); + ItemStack actual = ItemStack.CODEC.parse(registry.createSerializationContext(NbtOps.INSTANCE), migrated).getOrThrow(); + assertSame(Items.DIAMOND_SWORD, actual.getItem()); + CompoundTag encoded = cyano.lootable.buildtest.TestNativeNbt.saveStack(actual,registry).getCompoundOrEmpty("components"); + assertTrue(ItemMigration.sameSavedComponents(migratedComponents, encoded), () -> migratedComponents+" -> "+encoded); } @Test void legacyEntityModifiersUseTheNativeUuidAndNameMapping() { @@ -252,11 +299,11 @@ private static CompoundTag legacy(String id,int count,int slot) { box.put("components",components); CompoundTag upgraded=ItemMigration.convert(box,context(3839)); assertNotNull(upgraded); - CompoundTag migrated=upgraded.getCompound("components").getList("minecraft:container",10).getCompound(0); - assertEquals(19,migrated.getInt("slot")); - assertEquals("minecraft:diamond_sword",migrated.getCompound("item").getString("id")); - ListTag modifiers=sword.getCompound("components").getCompound("minecraft:attribute_modifiers").getList("modifiers",10); - modifiers.add(modifiers.getCompound(0).copy()); + CompoundTag migrated=typedList(upgraded.getCompoundOrEmpty("components"), "minecraft:container", 10).getCompoundOrEmpty(0); + assertEquals(19,migrated.getIntOr("slot", 0)); + assertEquals("minecraft:diamond_sword",migrated.getCompoundOrEmpty("item").getStringOr("id", "")); + ListTag modifiers=typedList(sword.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:attribute_modifiers"), "modifiers", 10); + modifiers.add(modifiers.getCompoundOrEmpty(0).copy()); CompoundTag original=box.copy(); assertNull(ItemMigration.convert(box,context(3839))); assertEquals(original,box); @@ -267,7 +314,7 @@ private static CompoundTag releasedModifierStack() { item.putString("id","minecraft:diamond_sword"); item.putInt("count",1); item.putByte("Slot",(byte)47); - modifier.putUUID("uuid",new java.util.UUID(101,202)); + writeUuid(modifier, "uuid", new java.util.UUID(101,202)); modifier.putString("name","Old modifier"); modifier.putString("type","minecraft:generic.attack_damage"); modifier.putDouble("amount",2.75); diff --git a/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java b/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java index d60650d..73ed2fa 100644 --- a/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native120RegistryRenameTest.java @@ -5,7 +5,7 @@ import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvents; import net.minecraft.util.datafix.DataFixers; @@ -17,62 +17,62 @@ class Native120RegistryRenameTest { @Test void released119PotteryItemsMatchTheVanillaItemStackConversion() { for (String design : List.of("archer", "arms_up", "prize", "skull")) { - ResourceLocation old = ResourceLocation.parse("minecraft:pottery_shard_" + design); - ResourceLocation replacement = ResourceLocation.parse("minecraft:" + design + "_pottery_sherd"); + Identifier old = Identifier.parse("minecraft:pottery_shard_" + design); + Identifier replacement = Identifier.parse("minecraft:" + design + "_pottery_sherd"); CompoundTag item = new CompoundTag(); item.putString("id", old.toString()); item.putByte("Count", (byte) 3); item.put("tag", new CompoundTag()); - item.getCompound("tag").putString("LootableBodiesFixture", design); + item.getCompoundOrEmpty("tag").putString("LootableBodiesFixture", design); CompoundTag before = item.copy(); CompoundTag fixed = NativeNbt.update(DataFixers.getDataFixer(), References.ITEM_STACK, item, 3337, 3465); - assertEquals(replacement.toString(), fixed.getString("id")); - assertEquals(3, fixed.getByte("Count")); - assertEquals(before.getCompound("tag"), fixed.getCompound("tag")); + assertEquals(replacement.toString(), fixed.getStringOr("id", "")); + assertEquals(3, fixed.getByteOr("Count", (byte) 0)); + assertEquals(before.getCompoundOrEmpty("tag"), fixed.getCompoundOrEmpty("tag")); assertEquals(before, item); assertTrue(BuiltInRegistries.ITEM.containsKey(replacement)); assertEquals(replacement, LegacyRegistryMappings.itemReplacement(old)); } - assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("anothermod:pottery_shard_archer"))); - assertNull(LegacyRegistryMappings.itemReplacement(ResourceLocation.parse("minecraft:pottery_shard_unknown"))); + assertNull(LegacyRegistryMappings.itemReplacement(Identifier.parse("anothermod:pottery_shard_archer"))); + assertNull(LegacyRegistryMappings.itemReplacement(Identifier.parse("minecraft:pottery_shard_unknown"))); } @Test void released119BrushSoundsUseTheNative120Registrations() { - ResourceLocation brushing = ResourceLocation.parse("minecraft:item.brush.brushing.generic"); - ResourceLocation complete = ResourceLocation.parse("minecraft:item.brush.brushing.sand.complete"); - assertSame(SoundEvents.BRUSH_GENERIC, BuiltInRegistries.SOUND_EVENT.get(brushing)); - assertSame(SoundEvents.BRUSH_SAND_COMPLETED, BuiltInRegistries.SOUND_EVENT.get(complete)); - assertEquals(brushing, LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:item.brush.brushing"))); - assertEquals(complete, LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("minecraft:item.brush.brush_sand_completed"))); - assertNull(LegacyRegistryMappings.soundReplacement(ResourceLocation.parse("anothermod:item.brush.brushing"))); + Identifier brushing = Identifier.parse("minecraft:item.brush.brushing.generic"); + Identifier complete = Identifier.parse("minecraft:item.brush.brushing.sand.complete"); + assertSame(SoundEvents.BRUSH_GENERIC, BuiltInRegistries.SOUND_EVENT.get(brushing).orElseThrow().value()); + assertSame(SoundEvents.BRUSH_SAND_COMPLETED, BuiltInRegistries.SOUND_EVENT.get(complete).orElseThrow().value()); + assertEquals(brushing, LegacyRegistryMappings.soundReplacement(Identifier.parse("minecraft:item.brush.brushing"))); + assertEquals(complete, LegacyRegistryMappings.soundReplacement(Identifier.parse("minecraft:item.brush.brush_sand_completed"))); + assertNull(LegacyRegistryMappings.soundReplacement(Identifier.parse("anothermod:item.brush.brushing"))); } @Test void splitBiomeMusicOnlyResolvesItsRetiredNumericSoundBinding() throws Exception { - ResourceLocation retired = ResourceLocation.parse("minecraft:music.overworld.jungle_and_forest"); + Identifier retired = Identifier.parse("minecraft:music.overworld.jungle_and_forest"); assertFalse(BuiltInRegistries.SOUND_EVENT.containsKey(retired)); - assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.jungle"))); - assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(ResourceLocation.parse("minecraft:music.overworld.forest"))); + assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(Identifier.parse("minecraft:music.overworld.jungle"))); + assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(Identifier.parse("minecraft:music.overworld.forest"))); assertEquals(MissingMappingsEvent.Action.IGNORE, soundAction(retired)); - assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(ResourceLocation.parse("anothermod:music.overworld.jungle_and_forest"))); - assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(ResourceLocation.parse("minecraft:music.overworld.unknown"))); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(Identifier.parse("anothermod:music.overworld.jungle_and_forest"))); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(Identifier.parse("minecraft:music.overworld.unknown"))); } @Test void retiredParrotImitationsOnlyResolveTheFiveObsoleteVanillaSoundBindings() throws Exception { for (String mob : List.of("enderman", "panda", "polar_bear", "wolf", "zombie_pigman")) { String path = "entity.parrot.imitate." + mob; - ResourceLocation retired = ResourceLocation.withDefaultNamespace(path); + Identifier retired = Identifier.withDefaultNamespace(path); assertFalse(BuiltInRegistries.SOUND_EVENT.containsKey(retired), path); assertEquals(MissingMappingsEvent.Action.IGNORE, soundAction(retired), path); - assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(ResourceLocation.fromNamespaceAndPath("othermod", path)), path); + assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(Identifier.fromNamespaceAndPath("othermod", path)), path); } - ResourceLocation piglin = ResourceLocation.withDefaultNamespace("entity.parrot.imitate.piglin"); + Identifier piglin = Identifier.withDefaultNamespace("entity.parrot.imitate.piglin"); assertTrue(BuiltInRegistries.SOUND_EVENT.containsKey(piglin)); assertEquals(MissingMappingsEvent.Action.DEFAULT, soundAction(piglin)); assertEquals(MissingMappingsEvent.Action.DEFAULT, - soundAction(ResourceLocation.withDefaultNamespace("entity.parrot.imitate.unknown"))); + soundAction(Identifier.withDefaultNamespace("entity.parrot.imitate.unknown"))); } - private static MissingMappingsEvent.Action soundAction(ResourceLocation key) throws Exception { + private static MissingMappingsEvent.Action soundAction(Identifier key) throws Exception { MissingMappingsEvent.Mapping mapping = new MissingMappingsEvent.Mapping<>(null, null, key, 17); LegacyRegistryMappings.sounds(new MissingMappingsEvent(Registries.SOUND_EVENT, null, List.of(mapping))); Field action = mapping.getClass().getDeclaredField("action"); diff --git a/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java b/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java index 338c0d5..b851287 100644 --- a/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java +++ b/src/test/java/cyano/lootable/upgrade/Native120TargetTest.java @@ -1,5 +1,7 @@ package cyano.lootable.upgrade; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import java.io.DataInputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -16,17 +18,29 @@ class Native120TargetTest { @AfterEach void closeJournal() { UpgradePreservation.close(); } private static int targetDataVersion() { net.minecraft.SharedConstants.tryDetectVersion(); - return net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + return net.minecraft.SharedConstants.getCurrentVersion().dataVersion().version(); } @Test void targetMatchesTheActualNativeWorldVersion() { net.minecraft.SharedConstants.tryDetectVersion(); - assertEquals("1.21.1",net.minecraft.SharedConstants.getCurrentVersion().getName()); - assertEquals(34,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); - assertEquals(48,net.minecraft.SharedConstants.getCurrentVersion().getPackVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); - assertEquals(3955,targetDataVersion()); + assertEquals("1.21.11",net.minecraft.SharedConstants.getCurrentVersion().name()); + assertEquals(net.minecraft.server.packs.metadata.pack.PackFormat.of(75,0), + net.minecraft.SharedConstants.getCurrentVersion().packVersion(net.minecraft.server.packs.PackType.CLIENT_RESOURCES)); + assertEquals(net.minecraft.server.packs.metadata.pack.PackFormat.of(94,1), + net.minecraft.SharedConstants.getCurrentVersion().packVersion(net.minecraft.server.packs.PackType.SERVER_DATA)); + assertEquals(4671,targetDataVersion()); assertEquals(targetDataVersion(),UpgradePreservation.MINECRAFT_VERSION); - assertEquals(12,UpgradePreservation.DATA_VERSION); + assertEquals(13,UpgradePreservation.DATA_VERSION); + } + @Test void unstampedEmptyLegacyCapabilityDataGetsTheCurrentEnvelope() { + CompoundTag legacy = new CompoundTag(); + legacy.put("data", new CompoundTag()); + CompoundTag before = legacy.copy(); + CompoundTag current = UpgradePreservation.upgradeLevelCapabilities(legacy); + assertEquals(before, legacy); + assertEquals(before, current.getCompoundOrEmpty("data")); + assertSame(current, UpgradePreservation.upgradeLevelCapabilities(current)); } + @Test void released16MarkerIsMigratedWithAllSlotsAndOriginalsRetained() { assertEquals(2586,cyano.lootable.buildtest.HistoricalVersions.dataVersion("1.16.5")); CompoundTag body=new CompoundTag();body.putString("id","lootablebodies:corpse"); @@ -39,13 +53,13 @@ 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(12,staged.getInt(UpgradePreservation.VERSION)); - assertEquals(48,staged.getList("LootableBodiesUnresolved",10).size()); + assertEquals(13,staged.getIntOr(UpgradePreservation.VERSION, 0)); + assertEquals(48,typedList(staged, "LootableBodiesUnresolved", 10).size()); for(int slot=0;slot<48;slot++) { - CompoundTag record=staged.getList("LootableBodiesUnresolved",10).getCompound(slot); - assertEquals(items.getCompound(slot),record.getCompound("Data")); - assertEquals("Equipment",record.getString("Source")); - assertEquals(2586,record.getCompound("SourceVersions").getInt("DataVersion")); + CompoundTag record=typedList(staged, "LootableBodiesUnresolved", 10).getCompoundOrEmpty(slot); + assertEquals(items.getCompoundOrEmpty(slot),record.getCompoundOrEmpty("Data")); + assertEquals("Equipment",record.getStringOr("Source", "")); + assertEquals(2586,record.getCompoundOrEmpty("SourceVersions").getIntOr("DataVersion", 0)); } assertEquals(before,body); } @@ -63,19 +77,19 @@ private static int targetDataVersion() { }; CompoundTag staged=NativeNbt.update(UpgradePreservation.wrapper(identity), net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); - assertEquals(2586,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); - assertEquals(6,body.getInt(UpgradePreservation.VERSION)); + assertEquals(2586,staged.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); + assertEquals(6,body.getIntOr(UpgradePreservation.VERSION, 0)); } @Test void released17MarkerRetainsItsSourceAndEveryIndexedOriginal() { 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(12,staged.getInt(UpgradePreservation.VERSION)); - assertEquals(2730,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); - ListTag records=staged.getList("LootableBodiesUnresolved",10);assertEquals(50,records.size()); + assertEquals(13,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++) { - assertEquals("Equipment",records.getCompound(slot).getString("Source")); - assertEquals(before.getList("Equipment",10).getCompound(slot),records.getCompound(slot).getCompound("Data")); + assertEquals("Equipment",records.getCompoundOrEmpty(slot).getStringOr("Source", "")); + assertEquals(typedList(before, "Equipment", 10).getCompoundOrEmpty(slot),records.getCompoundOrEmpty(slot).getCompoundOrEmpty("Data")); } assertEquals(before,body); } @@ -86,7 +100,7 @@ private static int targetDataVersion() { public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic value,int from,int to){return value;} }; CompoundTag staged=NativeNbt.update(UpgradePreservation.wrapper(identity),net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); - assertEquals(2730,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(2730,staged.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); assertEquals(before,body); } @Test void released18MarkerRetainsItsSourceAndEveryIndexedOriginal() { @@ -94,12 +108,12 @@ 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(12,staged.getInt(UpgradePreservation.VERSION)); - assertEquals(2975,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); - ListTag records=staged.getList("LootableBodiesUnresolved",10);assertEquals(50,records.size()); + assertEquals(13,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++) { - assertEquals("Equipment",records.getCompound(slot).getString("Source")); - assertEquals(before.getList("Equipment",10).getCompound(slot),records.getCompound(slot).getCompound("Data")); + assertEquals("Equipment",records.getCompoundOrEmpty(slot).getStringOr("Source", "")); + assertEquals(typedList(before, "Equipment", 10).getCompoundOrEmpty(slot),records.getCompoundOrEmpty(slot).getCompoundOrEmpty("Data")); } assertEquals(before,body); } @@ -110,7 +124,7 @@ private static int targetDataVersion() { public com.mojang.serialization.Dynamic update(com.mojang.datafixers.DSL.TypeReference type,com.mojang.serialization.Dynamic value,int from,int to){return value;} }; CompoundTag staged=NativeNbt.update(UpgradePreservation.wrapper(identity),net.minecraft.util.datafix.fixes.References.ENTITY_TREE,body,512,targetDataVersion()); - assertEquals(2975,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(2975,staged.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); assertEquals(before,body); } @@ -123,19 +137,19 @@ private static int targetDataVersion() { UpgradePreservation.prepare(world); CompoundTag fixed=NativeNbt.update(UpgradePreservation.wrapper(NativeVanillaDataFixer.get()), net.minecraft.util.datafix.fixes.References.ENTITY_CHUNK,root,512,targetDataVersion()); - CompoundTag staged=fixed.getList("Entities",10).getCompound(0); - assertEquals(12,staged.getInt(UpgradePreservation.VERSION)); - assertEquals(3337,staged.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + CompoundTag staged=typedList(fixed, "Entities", 10).getCompoundOrEmpty(0); + assertEquals(13,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()); - assertEquals(3337,detached.getCompound(UpgradePreservation.SOURCES).getInt("DataVersion")); + assertEquals(3337,detached.getCompoundOrEmpty(UpgradePreservation.SOURCES).getIntOr("DataVersion", 0)); assertArrayEquals(original,Files.readAllBytes(region)); - ListTag records=staged.getList("LootableBodiesUnresolved",10); + ListTag records=typedList(staged, "LootableBodiesUnresolved", 10); assertEquals(50,records.size()); for(int slot=0;slot<48;slot++) { - CompoundTag record=records.getCompound(slot); - assertEquals("Equipment",record.getString("Source")); - assertEquals(before.getList("Equipment",10).getCompound(slot),record.getCompound("Data")); + CompoundTag record=records.getCompoundOrEmpty(slot); + assertEquals("Equipment",record.getStringOr("Source", "")); + assertEquals(typedList(before, "Equipment", 10).getCompoundOrEmpty(slot),record.getCompoundOrEmpty("Data")); } assertEquals(before,body); } diff --git a/src/test/java/cyano/lootable/upgrade/Native12111LegacyChestTest.java b/src/test/java/cyano/lootable/upgrade/Native12111LegacyChestTest.java new file mode 100644 index 0000000..6c5d791 --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native12111LegacyChestTest.java @@ -0,0 +1,35 @@ +package cyano.lootable.upgrade; + +import cyano.lootable.buildtest.NativeVanillaDataFixer; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.util.datafix.fixes.References; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class Native12111LegacyChestTest { + @Test + void oldChestAndNestedItemNamesUseTheNativeTextFix() throws Exception { + CompoundTag source = TagParser.parseCompoundFully(""" + {DataVersion:1456,Level:{xPos:0,zPos:0,Sections:[],Entities:[],TileEntities:[ + {id:"minecraft:chest",x:1,y:64,z:2,CustomName:"Old storage", + Items:[{id:"minecraft:pig_spawn_egg",Slot:3b,Count:7b, + tag:{EntityTag:{id:"minecraft:pig",CustomName:"Saved pig"},display:{Name:"Old keepsake"},opaque:[B;0b,-1b,17b]}}]}]}} + """); + CompoundTag before = source.copy(); + CompoundTag chest = source.getCompoundOrEmpty("Level").getListOrEmpty("TileEntities").getCompoundOrEmpty(0); + CompoundTag item = chest.getListOrEmpty("Items").getCompoundOrEmpty(0); + var vanilla = NativeVanillaDataFixer.get(); + CompoundTag fixedItem = NativeNbt.update(vanilla, References.ITEM_STACK, item.copy(), 1456, 1458); + CompoundTag noItems = chest.copy(); + noItems.remove("Items"); + CompoundTag expected = NativeNbt.update(vanilla, References.BLOCK_ENTITY, noItems, 1456, 1458); + var items = new net.minecraft.nbt.ListTag(); + items.add(fixedItem); + expected.put("Items", items); + CompoundTag fixed = NativeNbt.update(new NativeSafeFixer(vanilla, null), References.CHUNK, source, 1456, 1458); + assertEquals(before, source); + assertEquals(expected, fixed.getCompoundOrEmpty("Level").getListOrEmpty("TileEntities").getCompoundOrEmpty(0)); + } +} diff --git a/src/test/java/cyano/lootable/upgrade/Native12111LegacyEntityItemNameTest.java b/src/test/java/cyano/lootable/upgrade/Native12111LegacyEntityItemNameTest.java new file mode 100644 index 0000000..9db3a1f --- /dev/null +++ b/src/test/java/cyano/lootable/upgrade/Native12111LegacyEntityItemNameTest.java @@ -0,0 +1,84 @@ +package cyano.lootable.upgrade; + +import com.mojang.datafixers.DSL; +import cyano.lootable.buildtest.NativeVanillaDataFixer; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.util.datafix.fixes.UnflattenTextComponentFix; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class Native12111LegacyEntityItemNameTest { + @Test + void keepsAPlainNameFromASealedLegacyChestWithoutNativeTextErrors() throws Exception { + CompoundTag source = TagParser.parseCompoundFully(""" + {id:"minecraft:chest",x:1,y:64,z:2,Items:[ + {id:"minecraft:pig_spawn_egg",Slot:3b,Count:7b, + tag:{EntityTag:{id:"minecraft:pig",CustomName:"Saved pig egg"},opaque:"Saved pig egg"}}]} + """); + CompoundTag fixed = withoutFallbackError(source, References.BLOCK_ENTITY, 2230); + CompoundTag stack = fixed.getListOrEmpty("Items").getCompoundOrEmpty(0); + assertEquals(7, stack.getIntOr("count", 0)); + assertEquals(3, stack.getIntOr("Slot", -1)); + assertEquals("minecraft:pig_spawn_egg", stack.getStringOr("id", "")); + CompoundTag components = stack.getCompoundOrEmpty("components"); + assertEquals("Saved pig egg", components.getCompoundOrEmpty("minecraft:entity_data").getStringOr("CustomName", "")); + assertEquals("Saved pig egg", components.getCompoundOrEmpty("minecraft:custom_data").getStringOr("opaque", "")); + } + + @Test + void keepsThePlainNameThroughTheComponentBoundaryAndLeavesRichTextAlone() throws Exception { + CompoundTag stack = TagParser.parseCompoundFully(""" + {id:"minecraft:pig_spawn_egg",count:7,Slot:3b,components:{ + "minecraft:entity_data":{id:"minecraft:pig",CustomName:"Saved pig egg"}}} + """); + CompoundTag fixed = withoutFallbackError(stack, References.ITEM_STACK, 3955); + assertEquals(stack, fixed); + + CompoundTag rich = stack.copy(); + rich.getCompoundOrEmpty("components").getCompoundOrEmpty("minecraft:entity_data") + .putString("CustomName", "{\"text\":\"Named pig\",\"color\":\"red\"}"); + var vanilla = NativeVanillaDataFixer.get(); + CompoundTag expected = NativeNbt.update(vanilla, References.ITEM_STACK, rich.copy(), 3955, 4671); + assertEquals(expected, NativeNbt.update(new NativeSafeFixer(vanilla, null), References.ITEM_STACK, rich, 3955, 4671)); + assertEquals(stack, NativeNbt.update(new NativeSafeFixer(vanilla, null), References.ITEM_STACK, stack, 4671, 4671)); + } + + private static CompoundTag withoutFallbackError(CompoundTag input, DSL.TypeReference type, int version) { + var vanilla = NativeVanillaDataFixer.get(); + CompoundTag before = input.copy(); + List errors = new ArrayList<>(); + Logger logger = (Logger) LogManager.getLogger(UnflattenTextComponentFix.class); + var capture = new AbstractAppender("NativeTextFixture", null, null, true, Property.EMPTY_ARRAY) { + @Override + public void append(LogEvent event) { + if (event.getLevel() == Level.ERROR) errors.add(event.getMessage().getFormattedMessage()); + } + }; + capture.start(); + logger.addAppender(capture); + try { + CompoundTag nativeResult = NativeNbt.update(vanilla, type, input.copy(), version, 4671); + assertTrue(errors.stream().anyMatch(message -> message.contains("Saved pig egg")), errors.toString()); + errors.clear(); + CompoundTag fixed = NativeNbt.update(new NativeSafeFixer(vanilla, null), type, input, version, 4671); + assertEquals(before, input); + assertEquals(nativeResult, fixed); + assertTrue(errors.isEmpty(), errors.toString()); + return fixed; + } finally { + logger.removeAppender(capture); + capture.stop(); + } + } +} diff --git a/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java b/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java index b73fe6b..d28f0ec 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java +++ b/src/testSupport/java/cyano/lootable/buildtest/HistoricalUuid.java @@ -1,4 +1,6 @@ package cyano.lootable.buildtest; + +import static cyano.lootable.buildtest.TestNativeNbt.hasType; import java.util.UUID; import net.minecraft.nbt.CompoundTag; /** Independent source-fixture identity reader; never rewrites historical NBT. */ @@ -7,13 +9,13 @@ private HistoricalUuid() { } public static UUID read(CompoundTag n,String key) { UUID array=null,pair=null; if(n.contains(key)) { - if(!n.contains(key,11) || n.getIntArray(key).length!=4)throw new IllegalArgumentException("Malformed fixture identity array"); - int[] values=n.getIntArray(key); + if(!hasType(n, key, 11) || n.getIntArray(key).orElseGet(() -> new int[0]).length!=4)throw new IllegalArgumentException("Malformed fixture identity array"); + int[] values=n.getIntArray(key).orElseGet(() -> new int[0]); array=new UUID(((long)values[0]<<32)|(values[1]&0xffffffffL),((long)values[2]<<32)|(values[3]&0xffffffffL)); } if(n.contains(key+"Most") || n.contains(key+"Least")) { - if(!n.contains(key+"Most",4) || !n.contains(key+"Least",4))throw new IllegalArgumentException("Malformed fixture identity pair"); - pair=new UUID(n.getLong(key+"Most"),n.getLong(key+"Least")); + if(!hasType(n, key+"Most", 4) || !hasType(n, key+"Least", 4))throw new IllegalArgumentException("Malformed fixture identity pair"); + pair=new UUID(n.getLongOr(key+"Most", 0L),n.getLongOr(key+"Least", 0L)); } if(array!=null && pair!=null && !array.equals(pair))throw new IllegalArgumentException("Conflicting fixture identities"); if(array==null && pair==null)throw new IllegalArgumentException("Missing fixture identity"); diff --git a/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java index d87b59b..1f69147 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeMenuObservation.java @@ -27,8 +27,8 @@ public static NativeMenuObservation decode(Packet packet,RegistryAccess regis if(packet instanceof ClientboundContainerSetContentPacket content) { ClientboundContainerSetContentPacket.STREAM_CODEC.encode(wire,content); var decoded=ClientboundContainerSetContentPacket.STREAM_CODEC.decode(wire); - result=new NativeMenuObservation(true,decoded.getContainerId(),decoded.getStateId(),-1, - List.copyOf(decoded.getItems()),decoded.getCarriedItem(),ItemStack.EMPTY); + result=new NativeMenuObservation(true,decoded.containerId(),decoded.stateId(),-1, + List.copyOf(decoded.items()),decoded.carriedItem(),ItemStack.EMPTY); } else { ClientboundContainerSetSlotPacket.STREAM_CODEC.encode(wire,(ClientboundContainerSetSlotPacket)packet); var decoded=ClientboundContainerSetSlotPacket.STREAM_CODEC.decode(wire); diff --git a/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java b/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java index 6b15263..19040a7 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java +++ b/src/testSupport/java/cyano/lootable/buildtest/NativeRespawnDescription.java @@ -8,6 +8,6 @@ private NativeRespawnDescription() { } static String describe(ClientboundRespawnPacket packet) { var spawn=packet.commonPlayerSpawnInfo(); - return "RESPAWN dimension="+spawn.dimension().location()+" hashedSeed="+spawn.seed(); + return "RESPAWN dimension="+spawn.dimension().identifier()+" hashedSeed="+spawn.seed(); } } diff --git a/src/testSupport/java/cyano/lootable/buildtest/TestNativeNbt.java b/src/testSupport/java/cyano/lootable/buildtest/TestNativeNbt.java new file mode 100644 index 0000000..cc05505 --- /dev/null +++ b/src/testSupport/java/cyano/lootable/buildtest/TestNativeNbt.java @@ -0,0 +1,73 @@ +package cyano.lootable.buildtest; + +import java.util.UUID; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.UUIDUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.TagValueInput; +import net.minecraft.world.level.storage.TagValueOutput; +import net.minecraft.world.level.storage.ValueInput; + +/** Native test-side NBT observations, independent of the production converter. */ +public final class TestNativeNbt { + private TestNativeNbt() { } + + public static boolean hasType(CompoundTag data, String name, int type) { + Tag value = data.get(name); + if (value == null) return false; + return type == 99 ? value instanceof NumericTag : value.getId() == type; + } + + public static ListTag typedList(CompoundTag data, String name, int type) { + if (!(data.get(name) instanceof ListTag list)) return new ListTag(); + for (Tag entry : list) { + if (entry.getId() != type) return new ListTag(); + } + return list; + } + + public static boolean hasUuid(CompoundTag data, String name) { + return data.getIntArray(name).filter(array -> array.length == 4).isPresent(); + } + + public static UUID readUuid(CompoundTag data, String name) { + return data.read(name, UUIDUtil.CODEC).orElseThrow(); + } + + public static void writeUuid(CompoundTag data, String name, UUID uuid) { + data.store(name, UUIDUtil.CODEC, uuid); + } + + public static ValueInput input(Entity entity, CompoundTag data) { + return TagValueInput.create(ProblemReporter.DISCARDING, entity.registryAccess(), data); + } + + public static CompoundTag save(Entity entity) { + TagValueOutput output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, entity.registryAccess()); + if (!entity.saveAsPassenger(output)) throw new AssertionError("Native entity did not save"); + return output.buildResult(); + } + + public static CompoundTag saveWithoutId(Entity entity) { + TagValueOutput output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, entity.registryAccess()); + entity.saveWithoutId(output); + return output.buildResult(); + } + + public static net.minecraft.network.chat.Component legacyJsonText(String text, HolderLookup.Provider registries) { + return net.minecraft.network.chat.ComponentSerialization.CODEC.parse( + registries.createSerializationContext(com.mojang.serialization.JsonOps.INSTANCE), + com.google.gson.JsonParser.parseString(text)).getOrThrow(); + } + + public static CompoundTag saveStack(ItemStack stack, HolderLookup.Provider registries) { + return (CompoundTag) ItemStack.CODEC.encodeStart(registries.createSerializationContext(NbtOps.INSTANCE), stack).getOrThrow(); + } +} diff --git a/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java b/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java index 3961f50..522ff2e 100644 --- a/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java +++ b/src/testSupport/java/cyano/lootable/buildtest/TypedNbt.java @@ -1,5 +1,8 @@ package cyano.lootable.buildtest; +import static cyano.lootable.buildtest.TestNativeNbt.hasType; +import static cyano.lootable.buildtest.TestNativeNbt.typedList; + import java.util.TreeSet; import net.minecraft.nbt.ByteArrayTag; import net.minecraft.nbt.CompoundTag; @@ -27,11 +30,11 @@ private static CompoundTag entityCopy(CompoundTag input) { } private static void sortAttributes(CompoundTag copy,String key,String identity) { if(!(copy.get(key) instanceof ListTag raw))return; - ListTag attributes=copy.getList(key,10); + ListTag attributes=typedList(copy, key, 10); if(attributes.size()!=raw.size())return; java.util.List entries=new java.util.ArrayList<>(); for(Tag value:attributes) { - if(!(value instanceof CompoundTag compound) || !compound.contains(identity,8))return; + if(!(value instanceof CompoundTag compound) || !hasType(compound, identity, 8))return; entries.add(value); } entries.sort(java.util.Comparator.comparing(TypedNbt::canonical)); @@ -42,16 +45,16 @@ public static String canonical(Tag tag) { StringBuilder text=new StringBuilder().append(tag.getId()).append(':'); if(tag instanceof CompoundTag) { CompoundTag compound=(CompoundTag)tag; - for(String key:new TreeSet<>(compound.getAllKeys()))text.append(key.length()).append(':').append(key).append('=').append(canonical(compound.get(key))).append(';'); + for(String key:new TreeSet<>(compound.keySet()))text.append(key.length()).append(':').append(key).append('=').append(canonical(compound.get(key))).append(';'); } else if(tag instanceof ListTag) { - ListTag list=(ListTag)tag;text.append(list.getElementType()).append('/').append(list.size()).append('['); + ListTag list=(ListTag)tag;text.append(list.size()).append('['); for(int i=0;i@eG5CWo1snVFfHnd!^xRtpNcB>DI9zGvTg{?)3guCA`G?ylb2YHe+GSEj+1 z)-IQa?%1ELUJ;Hj_v2)1tJUi6(Auj0dbJnDemH9_owj`XY0IabwnMA0{_7><)|RP1 z^G1=olC&^#aJGUjpIHF=D`B+i5zx*Gv3#2Pztc}U4K%t-5Qd3647_1D_Pto~xAo56 zs^Tb8f7{yHyZGk0ak!X`9dF&6C0klcZs5DI!t4L-*jgF};b_ep>%VPjb^Wm-Z7nT# zTkWgWf7xM&R{OHn@nuu)bm*&QmWOWa28lEFg2auN-SOB@6svJ7quY|w9aTrCB$($U zTbq$g!*HCFETts-aFUU`?&Y8zQ_u+(P~=bJaONnLdvU^L*eNAEx|R?Mv2#NjD8(+4 zj%Qvn@gir#jT6tJ<6R|%VLHg(Zp}y_ce|$~Cvg&GRDlz_%COemAn-OhgL|Z;$2K5^ zJnq?;3bMIxN_7OMQkL&U8*bu7%j&OQloRZi5*%Jlu+2|${QeE_P>cgo{E4-AuC@bH z%2RIw<#Oakg}on?lAnE3$QMBpxejb(FUcl1+-UBN#WAzDWMG&MDr%o*B(Sp&rX;Bk zTXv_)iZ|oLF@GIKp1YQl9MXtnH1Vf}4h~I8j;tYBQMQ;<97c`MN--&o>hMNf%>#E- zaCHP79##^ivT&xlI;T3akt}NP7u|AHN^y)xF$xy3HYKFxqpAB7h@jGrsnJ1B1JrhG zL*9_$j%&ypRKoEBZ?loMP&FkDBR|F}I9bp%CV>h#Nw6AB0mMJC5m!j~6B=;^MPE)S z4&)T;et;!DB}E==M~>Cljl#_wduob3z>bAVIV}b5ZwCThoL<#npeBnPct({xNLOc8 zH3?O8RuzwnxM*;g;tpcC?QY7!_nSw}j{ zDIaQ|LLuci3?oGsmX7RU_9>JRg>$d?&|~YRTk&Qi&skOB3jW;n_0j=S@X^lyQp)8j z%85H$Rc7bJ{^|_bQn!^7pWK)j7*@AkFD;@it*qO*i9AD->-^tM5t7zvc zo{Sff=ctdSy+rKrF=-QL-1H_2GkG{&rSlfl8fDIH@)2~UQb7pq@&?J9Mv)ibPdrZY zrYRIQ`go~Pox#8!>&a51DHJ-ZCrJ}TiUfY@iIrbRF6s%DUx(K5Ou6ca-?+ zV4d--6n%(Xv{A|Im8t~YojX%de!)N$;xZy=k>;>Ma_`-_>jdBkzdI9 zVtW*uLs9PFCDqNsw82ZOn~f))TQvK!>Si~>sIZ<_NX-TTTl>8{#qMXtf++Qs!nE$M zI$BRE99qE^{3@aQAOW=FR}0f3!~<)7O#^zfm)(L zSG>2D6Vws!tK|ebm@f7`1;F~GIag1Ow90p2>VX&Ca^1sc?Jkz&? zDH4$_<{NG_jveBuzAa4aR$6}+`{P0x-$_YNsV3c+_)#zq!+?JHyFySPP~%Vk-tI3) z%C9bBXC=@M6Wh=qnoyv={-eFGNA8-ZvxCk^^m>ODkK@4Dx#tN(~o z4ediEn=If^w8Ny%D9}K>BkkE&0G{i(cIZWV6E3GZyq15Ym?LWWhsrrhP~*h)@xrEk zw9ttu5~}1Fdt%D~fi{k<`ZN;hxT;S>Y0eNl&_ro;)0S9jpJ|^zFa?zEEMb%&0wCqt zO;|$G-mD2rQ0irSRFM(bvM}vaD!Ix@pxmcca>esD?b8}@g(N?{5m!+3<12fEWqR5t z2xAH0q0lD^V+p{4#3$7=gTy_#o*5|Xh4wH4z@!q`2dfGR)2S|`o%=EO+wTFZvF*L06>ZKJjJ?&fEM4pk! zopTd;hW2-Bd!`4{)?|9xx3On>>YFHf>;ikH2ex3%^t5kfUq@L9p^4m1nx*u%GWx>p zh2Ujk_N>!mG8yy|X+qOb`1L#3S1Y55uTvFj)P~_&Jn;g&xNhH32sAhuG|D?kW1Ud} zbG);pS4O0k7hY7*-9<75fo@riU}c&~sl=-qe_2OW#frLpb>lA^I{xLmHSQHO`0m=c zSKzR_yEG|=QMsxbdu~z$=J$}MYDlpfh54GeeJ{as!{LTE#Fpg!=*@xo7Nnzen6)OZHRE66orGRNrUSXoy=6V%Wo ziCWPvCvLRr6*N=9YBL&^&}|F*siSZ{Cz*-1u%nqlq1A4atTsggtKF#lIi7~4B%IeXgTxH$nSrt;PiLjbk#yvjZ@cp(0B8}NhVeog~^puo?UW{nhY z59?o0=|!v&{R<_wA~&#XjkI4Rsj0+6t-e@tvLzfS^(FPpAf3Lno*AgpR|^?y{bx1z zr2o#buMuXn47=j`g^ceIw64{r*N%E$67N{I-zoTROac}8F2UX~1<>WY8+~H3DC3m( zHKOp#J&5*u8cB}2?EQ^M;<;Pw6Sm*mh%eaD9~R=4D4gSd_^jO`RCqNWF5(Fi_H%YN z#-27ewH|WjA?fAM+xrYtz)t>x-TpBF=;JTim5K3qrTHbh+qRK7(8#~iM0(WIU$r|h zOc761_*`Rpr4Ljqzl5ON*&=KFOV*_dC`*mzU z3BP7vF%@8E7^$TjN#uWB(mf%9@_s`yCPD+m{$@koko4bb$Qx9`59(+VKcJf#Kdh6- z&_EUZNHTIv0yOaBW{e>P{G=IUQ2w6^CR37V;NA{|J*)}mf*l$ z%tl2<iL4FXrZ+;txvtLn_HTHzak`tb;clYH zrE@ELzia$Fj1o7%@CziCa=5b+xl<1&)H=tBELoTIo#RE8Y%BWC9R&EMr~VMf;S{%i zr62);J3%;G&1vpvpF05!h{8Ho)S@B#m)q@#pbv*kw zSaK?8C9ks&OHKs(|MgATLw4|nrtCrGz0t1b)j$UfNcuO~JJ}$i#BWZ4?7)`+0x928 z^=Ty8TdO_|rTLQ3WW=QTve0BirTK~=O+=*mYSpKaG+(RwG?eD+_R*q%>mI%~+xdq0 zbq$5`d{g|oMgoDpRrz%!(YGtV4n-33536c|8XMyT!RB}EYd*bIt>-hvLRt%>IF;S` znLRYmkFZ2xm=H7gx&0HE0;=y9_Amnz04@Hq2}?+sziPq~)ah@k3D`-c&Tp$_pr?~M zzq1E~!il8L?;G%gB>r^+et^IObBpNIO8kbqO*uVZU$k*YD3RCsiy)5Yu5UV{Fd|<6 zulC6pz@e`GW;bwv2AcYNEhk7%|ET2zXsLUcpj9=0CGwY?WKj_<;D?`Aj75)D>r-TT@-R|>XnzI7e@z{eEu6RY+qCUD1Na%7`Bu}?7> z6n2Gg*KvjfopDX&*HL$@Rel|INMH~1^;G3W#JHvs0|pC)nn?^8D3B_wW&nvbuVw(s zWzLGQP3u6P*>TFsFAIFYx*IhnLyW}|W9u%-5_A*$2uPtUOVC}|$7zZ#uXlCV?e0fk z$a4Zep`~iN+wJZL;$d%Y3OoZi=+SNU%pm1$)H4Hn_JM+FdFviN6*t(lyN|#RHl4Xk zy6|y=jG->#LYG$ECkag#XsEs?3MO8Ff%2YE#|C2h@pWuK#XaA??wqPHBnsC(OSsv6 zf!(JP5-95n?LL)I0D)iBh$|%ZiyLtTMK{M3*mlO$8~clCg*$j@b2`M~%j`C)@Gf2m zbzg4x{ssvZ`wIIEYk)wXudMnsV)8XrpN85qcb(ah{S;VtX1;5VaIho$Rm8=7`+kAL zkgSfo9}tui;>B_IgLZeC;(!i6RPkM;!Vgz`7i#dWc9*De&PZ2!!u382hzQxo*5+MJL{Q&vVKI+2_0J>75ZX*bEhkV+b3le|k1F(d)2W&lJ}>x18zB_^ z3-<8JN(Yqi#hbtDM(7$`};Rz3`*ZWpc!LO{sZk} zEhAx~A7meE01l--*gon38VD@h`37Jlt8nKVz@c&9s!}uYJPH?Ny!UQX*=0EiH1Ati z8edKU?E9idTp!WM#jR`?)!BX zf!-25iBaOY9wQk2I`=&@S&L; zH81dz@{1<(etHAS4d29fq=8vumbZsGC7;vz^>lheXxnBG&h`FWPPbBPQ)#`I9KKo$ zk7wl@1igviN=rCn=sf!-#B4M07gOv;FR&*GGbnVS=i5_Z84|e9TULG@xz1ZuejU2a z)9gx{ySf^MLsLq<)9vFBz@cbo*vBD21M$wR~n>shal)5&$^J(7d;v8I+iLUp+HWL0^}ciPJ?zI7{@Y zi5zVX7qR`84DD@8yrMFG$GoD$j;_r^eH3Ht6zA7*Y`(MI&2cLICr9tr<&>tN-OXvlO$%;RJcQOi%sy%&&m7})k}w)gmWPXI7A}f3?BVu6 zCGuiYU9x|KJxnr4mj*Ho@yVs5DvQVw0dSx51wAn3_Jn+Bw-Nn^UhBb z1_3+gY>Jz(4PbV@`6*SOMv=git3D0Q@n&{loR#h_yX+i0k0}uLl_~4UHmOq_=%iaz zGzS(6{c|h$L%n!jMT1b_TiUH79=R(kVKAoETKnhQtpnnr_!rns6~ciQZdK0=67s@& zW}vLMvD-^LS_C*X*}t`Y0c?=iuUstjND76jxUJA5DG(?@sPbrVNc@YcxQyqzE?W*2 zZb@ZOprcFdxr!9177Z#&+0^&GgD}j~mHkVbkwE=kW)I;A3Dn-@_KAp405x9Gh%2Pe zD;seIwRNW|u3~Q%hxo8&{|@$<3lXII7-hDpe@A=T&`RT|2&mxh&eCuKgiyzKk%kk% z0}bD`f$&JbS2f@VYWM1@E{rFU@2!!;uzg`~gpfd??_McIP65QeMmChrh3yvrhiba7ec=MoKt)b1CrCZQ4hDcjF;@jn(rTroFk@dh zy3gy>_JzWg2FWTu_OB5fDy?MOUlZIuDPG$Ts=hy61c~p&8*ZV6X_a6|yR)i>vx*ss zS9tSK7^J2cta34dhZvbxDFXJJQ1Q_K6L$@>kw|Q)m~G1MhxfNhQkHnwD;p9sB^>mP za3mk1k;)hK%s|Zug&yOHvVVQLE8K?mr~B7R&6jwvEj@UaeGyZ0RjvO#fvdou+IF)0 zHws(hgB5%-xq8O;O9XZef?N)j5KZ6|A{a|kbGVLSHe;fC+hmY zuJ|tMz28)P7xvjJg^`-SVvnVHtuQ?oXeiQa>{-7O4CH!s9UDlvSJklrC45~~(;;~# zy8ouCrbGPvbN`K15{Bpr=>8k(*gz7#zK#tj;adbYbk>T-&~FunT!w|xy-kQhGANMk z?bQq*$=*@T0F>(=QnTS~)mwIlYDqE7vBEQ085XJFFOpm-6l&nF!e}!{pa9_&undUg z|9j=vq4@6?9Pm7h(rGR&vV}VF`)*;U+A>-I=omovZK> zZ-|^LcsLD&J0Vh%PUFB9$T=0?MO$q*tN1SLzW)gw%{HkA9qH3#OLoweK8?3w2feCK zqs_CWs!zkMLZNV8Rx-|h+;|`q&a)8}0j(*V$BwlcnwoQ^15>Gv9c!&3E;h-{B~-SB z8ZY9xKk~yx?AYp~Tqo#-QNbMewSrz45y*!>pecJOH!d7l;Z!i|pVYF)B7@F0A;2Uw zPzxJ^Ga)2E2kXrkLmF5#V+`85aEc+`@Vq2_G%u_Ah6eF2g6UBr%gcL%og01-oy;-E zCQhS7N#whMlTWAaFkB=~5}N-27vbGq?8+qc_)h$;b|I}aPN-l?zd3E!-rblM9fUFu zKeEF+n8H*&RLf&f?6|vA6UwodcxqX!l)70`?qOf`VM^~BO$e2_WS{0R89cQm)YQZT z;M~^EcDIP}nZ_NG&+6`GPcjo?U9OSnvEQW6kX3GNqCakoJ2nxoBvXT8wK&V#-NEh} zZN$?sj~KhXCS3;I!XfquUJ=YU(kheswV8ZDIXF~o-k{msC6v{n1jR&ocnv$sya7AM z*e7~^!si5gjKL1}9B&V+=y@LDNlku?=h%wx!d^I0P?N&h&QJ_avM=l*9*T9cpj7|| z(g|k)AQ}lNoCSb*DC?#6Rp7e22-s^&gUjrzr3{Sx_~3GTfM~EteOK5$fI-@P@8C+i zjD~{c+6O`zTmxs0b|4g3GH}?DLZKrA1YLL+dlwpse}2JUK~s~H}qTL)L!oq-{; zINrh4RUL#(%4{IA^Q8tE83jh3e)C(`_=j`d;BHmAgm#z+K%cH;7J|}!vXEJ~JC76x z`evizyGRM!D!vOBmx8hB`=qw}TX(TL0|18$%{$viD?o$grcetRfYG8;sN@Xbu)m&d zcLnJQVS2#vQu_c`q32xZZ2800y({g;j47aEuCNa=OaOFqc@vh9dM<0i64cO*f<9He z&1-rNo-g!lfrhsI0zvr&7#R2q>)1e6{-QcIpoA|L3@km)#*$)#m(;??aK-XcnQ@*+ z8@x>h>Q3?KZ}?#alN@WJ0IzKg{9d&PI5 zAiuZAyc(xw6Dc9{2m9n@r78kVp^9ck#BSIQej{`sz{4HPKc;YY|1E>pWqkf*UpQ1! z7oYyEeF>ZqTkj7KerL}oP#P%VKg7<*$)v$Q#m-0hqrtyKqk}1l0VzA2H$VyB zt!;H_aD|}Y-L6)Tnm$6NVcx30Ui5UUUys*^AqxC}&RZ7zBnpee$E}-71kuzT`#}(r z>AiIk9gK`%7%hr2hOP5z;YgEbQ^(HWUU5UVbu;Rz85pYm9O~e#XcgLy7o+0EBJrGYS!TL*ZtWMqXlz{|4Y2~fm}!oj z6@OM%s%%|YtJsVzPTaCP*kQ<$DvUIgB!pY+e4>Q___P)~pJ>4Y|H95ETHw&fwXUMh zupo%g7JKVzk?{|3bbikk9U+5N006U%)%@rvSXGxPfl`GF#7;c=SBX<+p2o@*hG4Vp)CRLkBIK~SjNH&r%@9P*n5PdE&f5zO7Oc5HAYyhR$L z0FNBuTLnir3|CjZF;(2#1V@;mkv`roNolZ9A@8VeHrB7a=Gb>uHw)vBk4ZKWd7e@< zx=GXeyu^b6BhfxCDJ{pY1YS`@p!EfbV}pi@_>>T1WKcTqeNys!0KOW9i_$(mQLPex zHgSvVUaikmGk(otTB!Q7)lg`npRQIkGXKxjV8KHOtuIz%Hzs~m#ByJfhDnC?XS&W@ z6c?{pb_BhR+^I*eWVfZ|n*oo@z@jZJ-%6yT?@?{$_`aa#3=eFyJrF#nftIs$d#RFJ z_(=2)f`$z;PQ6Qx-BS7axO_!*Yo$nOoSZ{&7os%{oA^bsw)TNlI55x;s=&<`QSr9z!4)dfI4|7Dq5FvTqj&Nm zchey{y;SPqi9cHNX0!_~lN@vgM{2!X;yVL-t9e}6zC!YO85FAR${H-R$ookwgf!@C zyOOnpIO1v#rEb$(0_EL+_NW2}<$1W|7Q^}6R6h2mn1hE&y$mVfC!Q$edJsXHW&~-d zwz>6xLIbKrEyX-I`wZr`m82Ogb{B0nT;qw#fSFwP1GLG zivvmRhc+Sr=K5qIK>-Pzp*ISrp3ZG0iy{zsYy;If;WmHlZzn5=_G6lSv>=6M^|+fv z2{rTRn??y#WS%)axtelA%jZQ@(nmH?Qb`L%z2T;jLRlYi6G?%vFB9F@26bQTT@nCA zN?`Aj03ek3l@++D9~3Xbzp4TUWqF-67<9H+S8*UGg4(Z@ru%{bYv2viR94_&TDARp zA*WKn*%_tw8>L_ikd%ska~(e`=`tvXODTc^j(icjpTy8W^>3>MZ@9(c=k0ZDCmViw zVXiHuLJIg|hUNT#%#1FFigoxwX|B(}&|p6#D=@{*BY#@#5Ve0Hh&c{}`NU^|&o70g z1%aciPy1H_M+Ffw$X^SJ$2hyr2xLT1kDYx42adqF{e7iw%1D4lJM7gH3l_J&e+~CA=hiu(h81`b=fFB_IUYI( z3vqzHxtyLM$bp9lYXUFOwfj~VZZ!5?>IXZA3c-w>3MS1vhY8sMLONPRD)a0r)o5_6Be%kxNQZSwcv2QY@0?Sm7>!Q6 zz|(b5a{}oclOa&+e5#pqE7{isNHynGvajJ#H`2+T5)Hlbg%Wx_En7htzetkQphs@3 z52}{WCUkBs*-S>DG&zjQ#YyKj!X%O5ky>smL<<=f>g0C9Jf1=4kvCTc;Gj^pbNgmQ z@nSkHFWz3#jA$ba)c?zgE|wf{O607lh%Ux)cIQ$d{!#Ic7y0SKK*KOe0uPVj%Ov(n zvM?H^_c|Aaja(%V=^$$(jMkhe9IffcBvtrZsQhYS0<#h+5sLuiZo=eD3C-)s=$ocO z+Nn2%6n^<0LNLL}p&z|xL+v2nelMZ>D3MyG&en>JlFq#w>Lw$D!Z|`nwc^JU6nouI zWGzk$-weayP+4;1CCXrmqZ%DoR0ASIYoyLl2nPreGGX>|c}^2^g9y98?Z|jv!zT20iy&A#y69c)9;P!IzbAeJ*UFyiJ-h$jNT3 zQ42JY=L>Otjx{C5pweF;m`Oo_T;vODc!92)-Kom46E)M(VfA83>nRxZ=1YVTXrNFV zFO|4WkvJ-s_60I5oGxEp$-stNTInl<2sK3pJmKDXrDRbB0aC@Qq|PhgP#v$XZ5%m} zw+pN4)gau+?%_u6R4pgQV|ZPp^A2I|rF5vs$vYbop#miD5&{rP6nRtq#xZ2_-AzeD z{ah9zeUCK8G--VQh0hXq>}4QMfx4Bw48-xUU*9iTSqazwED1g!jf;?-5b1nSGWL)h z`RIH|nD0^~@S2~Hq|-Qk+sQ$e@nKU>fN^HaESQ5HY(DIruPRH%PGE!i!m0X_8@VI@c?U?+T5 zavGR~b;swLeF}DrIrqvpya|b=i@qSN1_>qatS?p>4UCe$CmD^91OC3`{;IOSKaeb~ zAVQ1uA4=oHn)3aT)FE=R_s7zNZ%gz3qz(+N>Zg+FimBZ{2<%VX`Fzu{h9G|w+$bc0 zD(ddpfWT9ZH>@+f_E_t5HwfphAfDrzQ* zX?D1Kn%Hg%h8jP;8mg}k=Fl@MSNlqpf(_x<@_1*qyq?U9s*xC0zposB88hJjX*_-@Be4EqHMctb3pO_Mt_3 z!f5bm3FGdHFlVGlw4v0!pAfakDUPlysnMR@=nm`gsMO;~ve~MfMt9tRilsBU4-&>~ z`rgLO?0e|?Yj7ddeX#H;LX=DvdY?C^dWc9h(~s3Fs(F~G0fM$zx(^lRk|0o~O+C=V zgnrM!FkE?fB@~`T>pr3qibl*M1@CI0AOd-mV9N#wN7bV%kzmuK`$F-KJKQMb_Zb0Z>va-a`aif)TKcfOE!uspkoF+(<#go1!*t!(*L?`D=H4L5ABN^d zo;+^J3En6S)RF+0|C@xctOSExPxmbn>jfHKMCra+n1KPjEK&`>^JO8Vn=S&jGWv=z zgbWsXny*$Nk)!!q6%q}zuM64M6bbbF4Z$C!I5?!fSp@{+qPL6S5QGvTy+MhHfo!(%ZY1F`9Pk?IYN% zrLakF-#WDdi|9$mb>(sv zDE2z0N$b4iL{Dl+1TT8^o-D*Qgs3bIkP|*d2u&$rcEc(D+xe6j9BsmDExx#!H5wu{i!Ms5dP%d+o-1{_tEDbq+Dy2}U58XJYetnlkeXAyyjj1+ zexMeIE9LsQ_ljn+#cN*RCFKBpWwSApzEPUfysBBZsWo7kp6b22;fHk5kDTT;LX1pk z*1c^x#cP|XZ{44b{gIm#+2P*nn#q*DM^Gs5_0430_m6vTX!s#tU$4)J%;KN#O!sMpB=muk{w)G#)C zhZIj5&qjl@cS^xo28U~ccS*5lhDDLwyK5Q-SKfN>slmb!?Y%Wj#2aOq$KLxSz4`P^ zRqy@Ms>H_!sd^s}QZE_^8I_LCX&lIk2mu;@uA<1#G0PSyL26yf>xhb(#@uVxH&&?n_iCxK#^VixVl+r6+e;2KnZom;Prx@bV3pm zprSFopH=r#N|DvB$Vq;llJG+jkc7UhnbZD48bXFP@YcPc*pu)5vRZz2%BT0M>i$KC ze0sl@mi!RBJn>g2`lL$f{Gvw?Vsx5&~r;iOaWY9rzta1eW@Pie6g~kFDL8!qeFe~Azb84 z^*fTKQ<}p5`d!KX014DnU#f=-3Bd8^zVw_4!~@0lrRPi_9+nmF?;wn~$Q`PPo>)_V zi!gk0G(2_G-zxaL42<-$qcCJLEEI94YAD!(?C&C&S(e|R((NkhB7-CGb`w;cqiJ-| z-(6JRItvc^dx*+gr?EkQ?;30r7F*u^J!`P={q((R)E6g zU?f8$Wk@G`Gb|i1XIDd^+FYSO;)#klD7!1BD!nD;tGJ75dVVwhRh- zZM)EGS$d8N_8?IU1`eOz>OZ&!jP&pjsfRN#)Wkyt-wUx$%1U8w536Y&y#LjIcn#n1 z-e&(1g3rytWK!;5C(KS6UWGS7;K3J9`wy&^wx9qCdZhG`B7I6b_EC};B?aoPM@x27 z;vqwisfRD{^<{547KIT#qrS8K3- znV;@|Q|kT<8;|sVD3$o_8imB3-kSvk->Un7{x`r&=QImEaKKV(B%Y}yUv{ekJKvv) z+!PeZ#C|TCSb>L{|7CsiC`9~46Fiunek4t13ZIrCg~#jrzm?GG-Y8X#RLwR+Bencq zvT}okD)~b-6n5tyB~PAaZSorg@El(MPmM^BR{kvwJ4=0s{(mIyS!sv1&yv_unDU|mJeoZ?TT-eaK_c8tupEem zS>?ew!VHxWc&n?Zrpw^w^?3E)i|~4IZaseDmM2&S=Lvow!$VcxqPpocXojLn&md=b zU52fEglBMmH9DTCt|~_N;DYLI&CrOQTUAR6)-!{<3($3sRv;YQLl{*WiXI;vm-5uD<>Wixjmy^U0W zXL&uyfm_FgnmCI~UxSh0yb5$$Z!;~iP^@_e<3>cog&H^|k;zB$>t&coUl$8kBX-lp$v4PxSVFS5zUGcto+U-rQthYghu(yv_NuB<_mckLKb*P9oGnX5lj`Br)Kn@ zOVYJQdSW z`YgFlBBW!SiShuUI~g5f?ph&Z$f^8T#kOjFS;j`!30BT&b)`&rV{%|p-uzIPKx)=n zIk9y2oDoB{K3EtajHqoNy=m)+I$fM~(k zBZLVghhv+1q%in$G&m+Uc$CPz!KRr{Tp4X3T{OyQkflnu)N`y~gintZCf^L{sac^6 zMXnb{a|YHKm1J11yaqQ2lXZ>^GD7wEbe0ro>r|R zLx4lnr&qHMmdt}^RI5gHn(|_S(v3e_E5_}sBpLKu%!U<|TN=DlvJODN9&#SMS{e)b z=sq3puaSI73T6}GYb6z>a5Q*dR}BS~ln2sd^#%&Zm-JY@K_cZz55}2`1ym1j65@yq zoi0k9VGx!}%{NQdYbcN|-XgO-A(PfytDA-S+rhhPu%Jlq;B7(>NA6iry}VsAbV7#2 zeTU?12oaR~oegQ=D3o5P+X#adD#(W<8`D_C{fDcN@HL&mM}#$PiiE;RHDpuZ+%36$ zf7Q3uvVN@?i65wH5-RCqRZVV8)PK&AUzWyzLYfJPD)lSUbXQU!)qb@ej}-hh$vc&J zsOGQN!=bXjQO^z1&-bhFLv~MO@B_&M<#?nUsp@KhMw6{IxZ&fKXBSG4cwof4e($9CQOgJNe8v1SnJk-(m8Zd;k^zkY!%@>xNM}tpPeIp}5 zTKZ%I0;Hu+RY{!@KrMZ`0Um1UGYuF*TKa6&2V$4*GY&piW!M^z^z->TG*ZqNs+@^N zL(P1#4h+@ur8+*4PW~&Px7!K=52Q`f?YtoH;C}+s+bsowms-^gLq#vOg)neCtLeMc zsfL0HeyJ-eLaTr%VyQ30fjK%`v+J^oywnqtd6-OBg7q-rK*()ngeI`I&@Bm> zn2Zh`{=)?#mUa-kBqc>%vPFn_3aq}d1b1qdb`^UjrGT9%)uzgz{%pl75+h58SGNm$ ze5(*E!@J0|-*>DO)ZtZ{mv(AGft0zk;HD5onOIrcMeu_N4;8w5rSUpKVX>t>gp8*^ z2Dd|(_N>;mt<2ccUX_Ez5vz?YnREPnPsK_Ii;gXs+Ftw(IhhiDcwb?e$H-xK@7I)D zT8V6Fe___Al)zRFXxIrtihFrWM^|^Vbz^hsn1&yMnm@K74fJXUSIrdqYO!OziM}K~rp)P}cRREx71G6F zRSJYuP#MQDN4l=amZ#y+@mBN{usn3fPB<-YD=eMB#s{T{TpyDh&ooX+lE`-hNYgj| zdVflz+fLut$#A_>>4fUf5-oT*bAv!lj?;OP>$P=jx=Kn5DW@CG=8X%TxGYL@v@WGn z28W09vSd{2Hgw8h__6O!!?M!g#kC*sg3Tn#&o;GgTl<0R3A3Ewb~OCr1YuCHbNgDf z4!v^pCAG|C8_GGs9jK`>0#a?Hbs04^PT}b2tei470o@ zM6US7`TEvJsS~BZtb`i6HQ)M*wTFk%LSL`d|I`&P$te4p)l4Z(I=&7!)-9I*o-S0`{rz+q-3vzKDyuP{9wa&KOaE zx?8Ou)x*It(AJOby$|8@a6=c;(+A3Ps-M(Lk)7YnDSm1%v#E+60iopT%XJ?$&Ru zZWTeD71uA+`8&aWQhfFfL0DvP+V`}M>5+;&7O}GuE&PPuv}}LG>bOTrPF*mehzZ*t zwGJwUi{oLq0d8WpKW0^6feb=d7b14NVyL%2ZtYk`G4;WD?Di+DK93Q_iCYT$$?6ZO zS#7kIZ38=}eo;hC<| zTArKe+{OBBU4kUr76;9atX8?AqVz=hW3>YD;KbW`n{||%=RRj^PV9~Vx_g|pasAAn zPG9JA2Fm)EPtD=SH-hJ#**&2VJnzQtiH+cS2X;?t1V1x}pWFz3Rt{fo1V1~6pVA0^ zvmCy!HRPFu-f~h-2;F0?4s4=)(590`Dexdd_ZVy6r+77O4^22%ys~$O)mf)mD+k4% zIl&#R6Pd-TuCuy173+`jb1~OiS)J}<7IGiqWg5~;GMdo^i{5S;(&L5o?$f$fL)EWVdU^v{Vbnk>n|6aRJOE{=R!3t_pEMVhbR)-hQH&+8a#`R5SXAy(jxBnD6EF&nJ z9_(#ZL=x(M0@yxQQ%&!w&0v9z!M;|{4}jQl4ED3CA;(7^SZ)vYxAsPX_2-HDQ(-xS z0~*0|>lqx_2%cNe;2^89<#2Q&&Lmh55uxx2uED#lQ=vH;lg@4Ep4NNn&?9(D>%CSH z0UDJS9N4WN!0}UY-=d~8RW>i&@it_FoF!*|WK;XNs+ zTr+e{{j$9%V%04wQFs4hhfKZMB8U8yPE-bBCXVHy8@oZ`jJ+Un3;zE`=MF?Niu`G; zYxjjwd4#vuuKY|fO{vJasCU|UHbH*O{bECl11S+4nov^?f{d^@0W4f8Xvl z0F|<`P289Nsq4#l!z~{N+nI(22A$49s3K4IyH-c-sDO90d|DBoeXj~tEP20Ub>{|E ztXx?}7J$+yw-EUU)~UuIN9EIMKcrC$giZXSNNo4FbTWrPJ%5!?!2Zr^3<&C&|1=Eq zCk47cs^E8}d=>A{5_K4AFM3h}015QGyyhyI$zDgYYl z=>fiU__K<3=l&{u&hqya?am`_hTn=ycn$W{X_ME#9i|oHKQzoA_4t9IuaG(WTEEl#ZcdGpO>W(E}}kI zA%pb&MlbM2i8I#=$ufjzuXpEg&cxuhbfhV4I?0X}y3PCxsqqx5xtLBPgvj!p+fkpZ zNRm*6S^6fu3Y+x8k!&6f%y#nL7Mfy@zEv+qjZqUn*xRw5ID;mR|3hg(YX*gb}u1+@R-j>#q8~ARV z!FOyerQX;8MnJj08|%MqX?6W^yjA^aJu{w*sYu1E2l`^GvY%q}?jkyO*xpBInFvDQ{)1?$)l1FM~~Vio5vixzzL# zDs31xdH~~cJ@m(<%{?e6eZh}_rqoxMH^`i{${YP`R0Z~D%EHUMLH0c4L3S`^A`#jR zJa=8|6NVD4nN2JOhII9-{k z-&ge`YRX-W^g&%Z67EAgW9+Xoh24fa#cb?m3J9HasG^6;vr`*~Aev+m4Hds!3H#8H zNFPFub<6PnY|AVNcLlJ;bXthQvaPG`c1e$i(cmgA%qFX8L8UQx<&qO!Dp0pxBoSJt zQ1HZ!orzBJ&$w<6q0_g{_>P^Sr?;6wqX*NDnxv1jI`C1_mPs1y0zhHe!fn}?6iXIa z_oFsus_GQK?@v1```t0e@N(GREGMSE7P+G|&PY$07rn8&1XU(NJk7o6TFTW97s!qsVGOB%*gA=3l@un?r!30ZZ$d*A zJ(e652d^#Wf(>`*RCrA|RBos|)N%ye(F0`jrtV+Xx9$Nr=UOMcRmP(r$&|x|ahlP}vQ}2;o@pn}SmSiVe3ouJOYp|#K z$AlklS+1(JFJ(W64ofRMkal2>nS^oTmu;Dy|C2b*^o4>4V>9ok9tO?}Tcl4I(n7MU zXyBORB5&^LK)leVIXrnLN1Col-A%@qvM^oiCXFiLfzYGrB%C*~?#I5SPnDmeucX-= zE7~z{bru5Mfd&$GD)csGc51WsQ^YDRrSmnV(lyB(^puT!1=k5pr z_M&cL4$f~LN5_$tK?N}G7(2}_^$_zgJvQzJ4xKMfrO?Qo3aXo8RZ6VZ z2eYxO7M)ao4h@XbIDoOYMtd$RgY4ki)-9ig1Ub&zHV?u`&wQc!yo!;EZVBl0las*u zsI6=0kj+tf+(J~qN`aMyUYKwrIt&2T-dkhp5EupOGgi^j?2>1zrd%UA`h04H2JOvO z)8pG9zP<%xBSo$Ro@eH7+%T(IiQ%y23uxG4pmE|R<>C1gsNT}}FkK`GL-2TNMkz(M zBmjQ!)->;@d2^CzGvzbPma>)=-2JFIC*eXNY5$!=gSjMfXR$snnzj}T?DMqc%va0q zSox)N$(rL>@zljE`&FzUKMKy-`X5EOv2K&MZ?1Ch4K4 zLz-g8HA@ffjsTPLghRF@hrgo=p4lo6^sc2?XEurN20*YbXSV79KcfzEDRjGxY3f}CpZ@QW-(`vMw4XDAin&Oy(&%z?Ed z7;Zzq%4_SdAJO$QB!Ixn1X=Wv<(x})I!CAV*VB_R5xBKx2KLSXR-7emy^#7i#fQJh z*D2jEurEy2jJ+8rs8nS4OH8U9HVPIoIO_Cd*4}KCRwNmF!>CMTv6x903kKlH1p?L94UG6BYGOy+EyR;&S!Z_Sq=P zvGc`rzUCuz@2;$iw!3P0<2BX3^k?D$Uc}64yB@B?U{T~Ey8F-%nUkY<6~7Pt8CgR6 zLzJ5wgq-RPl$#6%o$AAc&mpI}ojO$rLQeHBw0||V@=MHCgc@H<;sM6G`I3zyeEl#}aY?D@~nU_X2A zsEk36;7j0f1U&EKYSz%*$5*eRJI|nTsQNvL!mzv{a3u}b^!|D7obJUe#$R>xDQ)F` za*OHRn~hQcLZ0#6yt!yJA>QK=G@3CW=sd1v_LoAC?}(WJlo;eXe$57Y3bYy3J?;Es zUwj5rxdD~I*sk_wum>10r(!_XQ0zc9*itYV|8KRc4GsS<+qDlz|J?*gIr!U8wM&S{ z{w|%fjtaB?SMxalo4c*zST<=`R`rClYYpL(E zH#2f)RMv zkIWUM1f1VH+WAdQ2Y32&nDGQz0mYEws1bLx9Y?)cFtxKSPHZZo%VnE z0H#nha(>IrNpBjlfpfW)SIfBJ@kk0RhfU`NY}jbqMTel?$QlX5H8_iP{>AjG4{od8 zjfy;U=MQX9l^DGK?J+F?&;+4gpTOK82Gg{l$Qb?n@de@5?2;_ltfdd7fCZ@@#q6oK8rXL#3?Amk5@ zlA41Kfu%Vz5b+4C-aUp9m+%2Llv1?h6I!pfGcjGnlc;?6rF|!ELlA}xv-1YKo-97% zW^8^nNSl-RKQ_N|EbN30K&eY&t!#A;0$(U)&zmX8+58$ zPfD~|>cRZ%nCr7S>UjDEFy6AOa=XJHgAvHwu_wIM@97}`WjOIjnWYnZv$(tlcn>;L zj@;>7*Fnkwuj3*?Pw{^0=(&$*zn$|OMPLPdduo;JZdcyYv2+Y8;o%TXPO5fv`GIg7a~R47J&B=guu&mHY4V!a-F`1&=6jz zZ=nM{M_E_uhcFW|XtGp)o1i|lR{yHd4{)*04%z1zkgDqna^mIst!NaKq3C-3pLF`j zaN>gf$IOxe0R&zy$O4w^EmNp&SA;3NXZuTBQh%rHezP$+#%a_rDRGrQ8i zzXgiw{`MlVNnKPShRNJl)3rtc&5l!`xU6foc#g5a=V73?-t`bC3(xjG1o9sM+H))DJ!U?qVcs_Ph6sqvJADd;%lbXH9GQX(ra*Z8tQ{VE4<04$_9M8O1 zNFtW(p2i*o5*AZzc1&V#1kfcF#(q({qc>z6g*uwy=G$Y3QW+BZp5BOg3l4{FsOQzU z4_7tp5RidGyXtt%0hJiYY<8dfAOVQHs_w~ueL6Ep z+pPX|ZY0yW8^Ll6*VBcYhJQ0&uT%mEx!V^%MkAZ@{SCt$`!ktm{J*Pb+-H9BIW6r#{t@?18bZn`u} zyul*U6jLUN-(hV8XMR4(jHbk(6!GCgV=zPfS*C^*LlVT_Wv&IHNPd_ViA=%bR_SZ4 z;#e}dXR^LXfwn~MzuAn+peT>~Yi73^&w0Jn%Bv-d`!p6+rob|Z+x{|DyeXYY7Ua*P z?TqyizM#BR)_y-_U6(0zLjCA9N?kpMe=ozqYMq6VeU;G6~Q4@ z?Li$C10#)ngKKOvy=|10fGJ1OUzvUAE7q2s*k3aa2oR^E*!!jo4mMWU1DOm=wqiPe zV*;iWmTjZoumMAnc<-pid;r4(&bKmyV`##9>*35#8j@m%sPlJbK^mbqT(D@rwX#~` zuwZTG2nia9wIf{@QY`I{e^^=8k1Al6e`jU6#9@{@R+a%8u)LF%Wq_qD|HaC3vKSIA z|H{g8iNki!-Tw*&A3*@GCQU_VZnyduU~M`-1TPkL&thSo`5mpu zk7H3*4niyPi=?(-QrgU#yFgHpbm}~lIRW*f11qff91+hw{oqz^K98ZRXj73MJXGEO zEK{6;z|_{~*dR9;n%V07k~gMbJ6)Z)MJl-SGv1iSta-s4XQ=PU%&4eaj`Hq5WZfFU zJVE=()PVAlj#_yfWu5zj_Jb+wYW<-iQkGZjJc~7^EK#phHj3S+_9JQEXbif;-F_7R zUJh|qqViRM&f};GG_bsq-hMQ-6#Y9oBfCs}Zh_zvN|(J%!@y`PTv*pO7!#eR*krcl zi=SZ8G1+#=3dh5z#<$JWEk67HaaJySKMNjN)Ya4(Vi??v;jiLSEZA>xn4&Ni?Yo>) z3kLfy?BNmuhWjw)1j5lG*0vvWTn#g5UxsaT6ENPN>9JCn1WuNa+4~mh`5uc|iJALUNJUQNa7@aXzN=0Q)x56LG6JDMFPFRma55~4L zC|ONPu(OSFm+as(Pgl+;yXhl`fv@~akrzCg3av4Sjn}ddg=7ZKDopJ`ARvBYWjB;p znSuE2G-^ZH?`ZBiirQ`gLUY!+Qd=;7+kvSj(%M5(8PB*I!j#Vsch0diu}Si~3#OP9 zQzr7?W^Du~@;^=u9$-)+|4^YZn8^P$4XF%|H^2${^$QghH|zQ_k&3L-}HLpot%7_piU@-EY8(QMvmm&sx`=g`S3L)r}I ze>4qb!6M7~J-3LoMmOX4N;|(6+j$mqum%Z^@?Xj!KHh@t-IosRJmC&^!8`j>1Dj}n z1lMltc5(WYrk?u?9hkS;kEM_ePX|UiA7|;X+KJN&?Rd8_7v#u)ac~wXWN_<0|iOS@S$y%&YKU?wA7^CVeXPZ@a zF3H$&zV;nx|CQj1gF6fAjteT9<7{6^*C7RT8piRaRf5Z?o(mep`xVs5U>IriuGHMq zj2>9zwO&U}#UP-u-Ny=nE4p^q`W?7Cb`p&QNN%yS6CJ-PfSy2OIjn2+t=fY_6g-414N$!0ilGx#UeBSQiRyi||{ZUp~?E~s^qGXL?X zw9nHFCOm!K`VJjHfjcgPm+l?t0K(%hdvXER^G~-2(DGu1`V6{-(ET#?xItQoxU507 zDC9krie#WL$8Z!?2`D?)emL!W^9&rmmepeg#?k^NfuDe5(3dgQ=x0v~Hrj2vstvsI zNu17(>{lIASd|}m zkKF{TvjnfH5Y}RXXV^_p&KUg5Zh|#=19lz(P;nV9sgS{9f={)ZAk8GSziMZ_dim93 z1x;*e?9NHt{|0VR+4fazLh^%6T_!u3c=+k(Hp_ga(0RGxcs83$bnSSok(f6!MgIFO z)<*EK`6!Jm00s@1gN4T6Q28X+;*|U8Z_?EmLXqL|PHrXB3g7%nt2mY+!JgbOK-*wo z6{I*64FmQZYKrH)-qy;i#q^)VVxbfWG9b@l2vVB6dm9$tlvN`-XHaLX57EvNm+&#C zCQ!gin|q_w6gImLQKyPw$np-5n}o*qF{;i2M(povv_C13n4rl*AIb5e6~0W8&SHos zQ>R&AZMOIqI>A`c$Q*w{Ej=gUe6x39bEw5A*)`Py3*v$2SuAGi;snA6>2R6qmqo*M zx}d(EI+p?mm(&mC#*(4XqI#1{3Snqj{b$}o8P>K?yPqfyURvLr>o3QZi|c*F#_;lb zCvMU?&bq)poZCc!59jXcqP$9WuBcFSf#_W#46Vk$DRv@UkFydCB?_E8X7^-D6k3zt znudA>j;_l8%STs+6xZcH7d6RN=2wbRfwg(}E6g&zvA>ww*0t>kjYraI!uhWOUG=hB z!@YYlNJ#eidRMa{tRcwtw%*S)oF+1Pa<$7I31mpLsd*?3TrF_a*UzeNAJ(xu(GHi+ z2OVr@M?N8R^ZNE-5$W49W0DfWt>-dh3eyLF=YCxJ%${*ZAeTNZlOhu~Vi#jBbDI@H z1u!J*E#&r~jBK-}fEBWxM*~C!8E-mP$J4$GQ$$|t3YLydXskTl9C_gKW&1j+dUy>O z&mBIsb_9jGdmtUl7>U$(%gR=v2Ct78)9YdFIaQ?nX4o(3_>?Y%-QC7Dj!90V*-3I0Y-d{&I1=g6 zaU@>=+n<_%Em56z1({Y!_B>!rKSvfeIbnW zyK-dt5Hh7_sB$U!2B9ImlKdbuIEX?E$^FF!;X3l8OwJ5NmXTkjb34J2RpeV~?2y65 zMdV8b!7OXYljww*Lv2gQKhx!`4UAThKc;>-qu^C4U&6*ohFk=GvB7kaAed#5$qujQ z&~lC0dO5>vQ%32GT|dtAZQZ{!ABbpI!?FXQo}| zO$Znrnm)H44Ngt(U&jbMHf_4=7#VCkH+@1KKj`4}UFq_bqRGkW!{zR@AD#X)4a7FGC4&qHWrRG?6tzKwP|hM_3x57b@@ ztZnXgL~(FDbq;lhC9Vvr_7)q%k=2fLkd!!Ucy*Yyb1X5|dqj1jKxMV3%>qC2VsE}Be0**c?gq0D0?8YgqE>b?}5ySJU0rwIDLJa##5bT zn!K5N;VhrAI%CG1mqh6!#-NjQ@CQ$4T|HX zud<44iRVsZou2}2(cC|nS71;S%l(3lJdNj(+{@T}lHy<-cRQ;%mMHGYbP;EOAcix3 ze>aE#X{^@uT)FyZWVlabzSd7)1$9PISg_0r9_AoaT=SFcbLqR@Xh40632!iTK=F4% z=izvJA$ji(%){x&mG!aF{8DJ%&=N;xrRV7TcV{xwrAQRN97_Wp3mkdn!fMeyM3 zRoxre1T2=6?V5G$ag|t0M)t)i*$IuvP`d3U&A$f|#?NQdOkMrI?kgLSpbB}WKH^E$ zB=&RUFBdygcpTD)M%%9~1tN_G>`7;JZHy2mwYSxBFvA5sg3-B@& zE7@P-@iqIC*z`|;NbiTpY}hW|XHV?f*dVvXju+XmT^l=4WW#=c>|4x(=6F~Z`P*WL zr)7~L1K(kjGKQkBf490_*lo{e;gwp56$fzM!9ti&tzW|;bZ@_aYcbJo+4@Lr`;}ZP z1`Xd!?QQ4W8t2l@9GbLr}4;;AZ<}d^o3I zR+{cbe1K*^YqhlYTcw6cEvNQNS(Iad;8b+)0W|lMW9VIB*zofOtyvH8x8KMec#4(} z&%H#T5YI5QpDWliS6}aX9y=MRHQC*M4G(b)oaa5;ua~%B`O5Z-g-#ci@a@+LCe4EO z_RCqAoFSoie6t``lqQt$kY*QF0TzAvd4gHQ^Jp<*X7L!Yr z`bn4~4`=8`xo;9Xv{vvnhJT}KIqIp2~zqYad$WUb#<()!9IFIrvW_b{WGAT!h z4Z>W?=a{lG6v?K1k9kUrBl(o~v1!`C#r?0>34&SjC1k!vaP*lix5y zGZdWX%LJY+Ns*^BH;_TW@yRE0dyCZn;ktfLvh}J0&HW&73-wQJpIe}fj1DU(m1EHA z|2vc^%bPFV*V3kP5W0ZM-dM>{nDuKY>lq5>K-q43jzMcEvwQ%Dpn-?%ZKD%e)tq6d#gGa>n&1P1frcd$uf@%+!W^&Pyz^i2y#mimBf zv$TK(?zD(Zcd?8YT3@rhoA^ceZcCe$aVTK?jTd$osKl1lyLu1iR^`uDR6Ca3V4D?E zioCh!mbYs=&*r031N1u8`m)hFomsLM#vq2hE&*O4LsQT;Zu0<$rqJzGZNh+bgLfW# z0<9g|r5FW^!V0=iX?sD9jeqJo^4u>_R)#x~|(kDwt zZsG>z8?4=*(@wR$Z2B8}TWSXLy|r@f`ztzM8`!#6ELuAIFu8PXH(U{857vyr%$1Rq z_dp&^2cLqZWx;X)J2Vn2!%CcSwCNz+Lm59Q4r01%9HVD$w156$j$xC=hnW`Fyuj02 zhw<8?9A+P(eyxC+lK=qQ*}^_+u3f^I;;YmZsKusg$8v?EcOS-K3e~%Rx_m(1;^iFX zC#*nV*IXdj4Z_0GT6*i$C##>sWP-QtI_$-u3}r&<{8Q`#B|@Q~=}7A22^8fCSe5P! zY>QvIC(yN=4UBj(C%})cqd+W$%+nYe#?C1;+M__k$`N$7;%LOijQNcW51pq=!z2ho z0o^Z{AFZCkIKYy_wU{*(o#-?#WCkTm%kzq`CDGHYI2je8%~XOmvOF zPON>fwHYw?v=6Z(!eSBFKAg#<5hw^cg84ZO;Q`P`>8dTAaLX`o#K!78oUIm-5xdu- zeonW3Q2zN{9F=BM@v*vxv#vHMc&P3HOvMI+j?_iWx&R0rsQVRjsRn5~h;C z<}H|^8WcS`XI?Bhg5ku2b3b7I5D-A%?FCuji^~sYvLq_**u)pvTC6S(a(b(bA^UNW zHhV`|J|J>lRhuwIznJyBrPSIU0tgP%c85)U@NJQIS@w$p8MK~JjguPi?z$q zi(K38zu!&d+IDxkbsIirGdR%(J9USEu*qAC{MycPUoG-$J22e3rKn{-x7|_HGIy$7 zN8}^h=fsA0AF=HW4xih66q{BO_5H-0O96@P7qW@7z^#Yn0h0ZhA6u-tkly#qaGW(S(Q|Ga$FwexjwB6yQw4uox!)WWB-O`C?U-g(tqcU1p2c*S&UT zy(>QK)%_Y>0m5T`KcIstJ%z`2O@2s+C&Zy>Mxg(SFp(wd;12e)8e@Au2pu2zOb8pG_+G%ri?1W+-@#ZyR+r z`4J!ZCit~fatK0Cg1Z7^_(kwP(b1Amp_cceS($O`bbCJmg`WeD1QdP^e4WN8Im&uE zd?Y9kmGXI?076fJvy?&(0@_F<3>bnB6r{w1@ZM5OATHTXT}OtY%UxE~KEsKRfIpJD zUq}FfL&5S`y^7Y6bnI?`ZLIdgsJBRerRTo7|Hr$2nRyK<6GM`ri1v51(B#WT%7PE=tj}(x$E6nVXL`(tiFJDFdHiDAN%SQ^` zX1%%G+l3onie}fA_Ys=4KlrokTu?-AqlX`gBtM#d)X{pPpqM^S?NiQ*hAK87OBV7kPxdqs>T59*1I$J zwTOyY@!mC`v0Jk_mvDR&Ds&ad?5Z6t(b?|e_b%h%z%~KfHIIHDN3k`;rbgbM;v_d1UNB^ID7! z4UK!*yO+Yz*f^^x4Hz6_yABu)8XdQ6Oaz9g zjZHFDqvEuBG#C%}W5cv^B(N%()nnj*T3(=aT*wsP#O{x;WeHjQU09q`W9vs$bgaFE z(O+0BU18t7istQ>-C~ZV-li{Mi->A#4%k}X^X4oG#$IGcDEtu3m}Q^5#6jmMTAyP} z?TO+t&)u=7HO<{Fd|-D*Q-B$$Jc0judo%b9{s-7%6@btw{Qacn;5qz{uq=TAk(2me zVWS1X$XWciur5u};w7Dz*k!UD#XFI8>mG`oUOXfzxoHVB8qCusUFN0IUH4eHqmjtq!* zP*cp{;)q~(=Q9G!kT`|9^c-p%6Mv&F-3CU3;%C&3GYZc0#RAV3lYb($)f@`$FMOP0 zwujT9@1goRpP(Z)!<6-NwzRf{>xxx>tmcOFe_L^-@}c9ctazhjOKZsud^gTuJGPe6 z9Hp{I^}j8xu0M{qsy`i5|^k)9m$8|^c9k@!Km$-rz}nd5ljC(ayhb00?e(q}i|R}ZFh!Z-}(6MvTO zf|svw??}f=`hdB9nBSR9M{m*Y^O+soX^0@Lkh|n&=Dd97DB}KYp15*z@jYLpyQC?i75cFTo7|Ic#St zo<1LGFFM)0Psz2#)X+<@$ zT12$jo8CZq$^_%`Irv?vG_&;CBrQ?N$I-NnFx(EzVGsKAJoMv|`UpB`BG<>mY71o| z@>V_Ne2_A>Qhjl|-G@SA72D%h4y2!owHI5_1zSpi+SH;n2#&WF9RTs%EA6ev$dkyM zt(GRlP8*wp22dpjP@OJju=97KBiU>f8D|3|*`2bYLUrUhj-o?d0oFkSN3xJqIXu0L zi&jg4_oU34u*2%g;c*^i?M|{e_ZoWgEy(i0=Y~FzFf}A=&hz zMyI=p{T^&b8GRNK=;A*MS(R=yb;qhZLfqGTBSe@kcW0!(Avgc8vFiYnrKr|Dq2b<3 zx+KXtM?pj}A{hk{1;K=4?@aIRurt%cgx$Lr45)~rf*4Q{bIuXPoO8}O=bWGVe9EiT zUFV#?YIfiE;e(yOE1p!RPMtFvRgWRub`xsJwiLZ$(&NNNM(YAx?X2}*xDebOkQ8HO z;0_d1{k9$amgiht5P@T(Vd$tJUd_hhz^yqt6u! zM4s%744&t{k6onMYJYAK(ZnSa8EiZ2`=G$xiT#tYLG-BZ$t!{FNQC3U`qnE&<#G!m#}-Coo}L?}TzDIc4 z$?jE78gVT9Kucc7DX*x69qP>PQU(X>xr<49$Ad4Iw5z0URQH{ zQZb`1K_^KnbS5Q_|GBeVB1sb`hfZiKg(B6FR?!uS-Xe!S-yNH0p>xIDoFCO)Bniz| zUWnwb(u~j%g5{5=NNq4b9Szh;68QgaQot_gvtaq_J>b_a@#4ZmC4J0nDRSty=0{2r zm$-XLF>P?;E2dR6xqBi)CbK~J`Bb^;l1=U|se3US4p&=jD))gysE((snmA3)Wzot( zxn8Hk(YY9TJL(KM4XTRHl*^zjnW;Xe`TagiPDF=fkFqvB0i6xa=-f0+UY{d}(IxLs z&V}=@-Y}uyp)3LJYOG*AmBi;sUUnFi+2`YFIvX#xmIg%yxxeHYe_1hCR1vf;Q2{H@ zs=W=S$XY=c@&dV)%1Z552kQsGfb_50I;eWOP(o~#%=UDyS|mv4;)`UG$v;Rcw+e*| zb&(XpeeLH{yj0EWVoB+Jr7As*L0u(D;+H@VMZwbaxaU2DzWSu_8fJ?1h!q17!oCs)8B$AbWQD@v zKT49{P+4ge`DN0$7-q_x_r}E>_-ypCrfR$mpD!xG?RT&v70a+&O*Mz6yHrxnOZnkS z63mUJ-Vj)T^|UU*YQbLsotQlA<*<8E)6 z&l2j1m$yYFU;l95aKmz7obf%-a+FT!#4ou7Z7LN5HOReXS{#ZsOYH(NZ0jFZv0~KSruUhe6@+ zSlA0{Ej=(?=4fgs+96>Y4bSo-DU!GAf4MwthB6800B(f9*q`^5^U1RF?C~$bPD%#bqJK z;3|2!JLsqpO#H)zr%Q5O(Yd0HL1O1z0_r**L(-qg3u)BBo9Rn64~GNnUwog;M?s^TSo zAcc6Q4Ad$e#-ria;jlWGrmupIMz-i-prghr%&`oXXpP(6GuNDgB=I;wJ54Bgg5eeE ztAYJT{c&qL438508q^WvDl${5u)!EgQj^QG=3_x@Iv5cD=YjZ5*`xC355j)isz_W? z{uWjNig`6ZFXh!-rh|TLb+VEcc95fCrM~c39mu6t|EC;s31Yeo@;%0)RvNp@$vGs| zWm#lkOqbm|(k1G$KT8U9$dE2OAu`>5G0wW}a)5ivV+untS^rVVQv%BBP8=8Moh!f% zk^@RqFqOv@kp(B4QiB_7hhf_9=aGh}_Jin^5<^zxBaumR?RT^kFAgQNUwS{J@zJP? z*m2SDJM*8r{&m~s_&G^&jERuvvI%g#i({vaVL*|41@`;j?1Awl~DM1dB+X6kawadwvjJeJXl3 zM<7J0IEElO^BFth$(p{mBQ_Y z1+lkO^$jr)eIGT2`CE{99(4^UAqL~Gquk3~m}O_RYR_H*A0!d6=Tk0~&K?r4Y<}Wv zs6QBt1cSmiR&{V1Ib^7Cj6EzKJQK@aSp6Ni-h6=14u1wzWq{<-RN4(fXqw~na?qW@ zlFsQ;aCHL7e{0{I(IP#nye6=Uojhoor{@QDPzhZZI95_lCqT1XC3!s}Kwb(&va!I4 z%pyRE{s)ASE@jJE_P(Gi4aRfqp5aCq!$KN6gW>$zj9@6cUV*DP#mBu@gV?4hv6?3b z=jVCVW!I1ZzlyH7z<~k~<>d$B9#3=0%C8P~+V%G{zYeYzt3vFmUF|I}0dcS&s9ysN zNrU51a!Du?27g9UoQ9qt{|JeC%HIq_I#U}J4^*An*Vo5TU~kg0hwI6x+i&?QA$gyw zaF=6^T52Sn`WYaKWbLWu-C+_M($A}K;%16HR7p>3_rlMMC2AR@HuUf)iWqgYCDI+} zi~GE+k*Bs`LMz!kM!LG@yj$QZ9MVel{`uKJW}{43QKE@hZvesSG5mP_A|M8WBW{uAJ!Miap{|BKz|;^bWV-w#OAg1q?OE7A;;{}A+IL0kc( zF%Z=NBQV%3NT`6vgTcmNLI>O>JWHYkHii*I3(SFFG>E7gcn}hk#&bQ8{3ASFS=sRk zNX8V#x$r^AuNoZC3;TnCW04`na6Fiah-I4LFUX_%15^z^MpD;6Y9`)@Jm-^<*PjD~ zHJzL5iFDG>03Ap`d;wn!Bd1Uz$NmN!NIk7CRF}2W>6r5I&Z}Qi-}cxJ<*wJ^&_{!y z2unT(!k(0W0>$pqcwIfAUNh@4H4SlVY_1fgZ;g^z$)bHIn zol~gO3eMT*_#%cO86FZ-am z`n{zPF;P3V<_7qgJ>TNny6rBLve03e-|p^_ zF41xKd)!=u40-O}8<{S5-91%Gdn`9|RHPy9ylVoLI#kemcNgFwix%#?J4cp}`R{%l z*$3*t+ckzEhTsPx^W-i(sNLaE(uen!NRPycS36JYQJP}TWiVXpeXUFG6n$UY3=p*W z9j_Xoez`*0qDkbMw+01}sw9B&{N&zs>=l6WG=Nt}n}qVaual?$3K}X=o!W;BhX>%p zqSsY})e=#-xN1K}Qg$B&s?68#9vxArT4W8Sxrakp-Q<(em4Sx0MjH;)0&_undj!y= zwuXza+dqO5<;BjzCX10YSkeZvv_*}k)nk_0nGq1pyH|rdskRU4Z`S1rLjTwz4=-F6 zQ_6hwaqKUFYkDRfbH1AM>W8kf1RaLgOYVW&Zv0374EH{u+cd=2#l{d_1sIKUS`Rt3 zmf5ikGhUY$#sw79nHJi-M&5JT%?_M_WPvL0l1G#PoE@^ivv&LB!0m#Acb9O+6bFJa z;xSD4UkHpwfozzNHH<>(06z_uX)(8wSJ^I)!ttwJ)6c>NmDk`P=}Gd*5nMWaMq3>b zTmn^3Gz8@SClH0kV)KukqRLc}wC_jPEQU)fqRvFYvJXy&wTvF3%ln z!DK8ed$<&rxX__2V&IvPly8JOP!rD##ZVzi{hTzQY( z{7IxW_Ux|-!RHReUiZ2D*qb@3dV|tmTzaQPC*^?Nu8XU6jU(s)j!zwu(>3jwB3k+nn( z{oCNivtyu@!iQ!;UG6f2GcAI3Tx4nG+2zl;3?p9bu(^cuTS3k87%%}wnMok#H9EH; z9|s;O_&{xLb$qS87Kp&%gQtNBSV;~YI1Gq^t%4=&J`I4I4*<2K|JR>)&IcOO|7(mh z>Snqh!fZAo{ldi`0AC4wTEalsoC>nZrY{QLV>Q1HMCITliE8szAUz!jTvPwT`PhoG z+yiq5?A5}kV`UEL&w-d{kvDF|kb5#tOdSZSN+d1lrfg7v}M9gu>EAERA)0oy9 zqivCzY{!7T?m#l!^B!;#8>y3?zq?HrL4J-_^_~}V<1S6%GY|4pCdW_EJ047lpP=_9 zk_f+kj}S8Gf`GKZfMi4X6zsi%L!02{kRd!~Bvky3D0D5eDP7^k1q9qvcpz1La8~y)qWN}< zqloJJV06y>BH{=s?<<@%=ie6Xmt02F?;PxRIO>l3*{JIo_!wjthGRa!yGkhrV5Y-< zj&oWyiP4n(KgrwPcT|5K?4UA9TY+$?>tF-|$i4Y5huKNE4>+hlABJndfpCYX0)a{- zAI2+y=K$}=k$e$koJISXjGJL|HNqsLVB>sRuwRekqkJVu5rHOa^@89C7S+L$kKpOS z5qKOQ!K;EJAZRjzTY@9NO6N}rjv!L*y#BJ_2!b`w(|rN3C{Nd=t`@T zYp&aw*avv!j&Hyn7tZ?6AR)6OH)GRxBa;X)^MHSEt+r`g+@q0GgJ*x2wQ}DOP+p0g z6(0b2Y+Ulg143YBwD*fahoaAmqw{{QHk6^yiH~`q;LtPTP?PkMK<|$9Eqtiu&6DgI-pbg)}fu;y`^J?7hPX59(`UOT%5LyT`VK`A}aIMG+^eOS8iW zsTh%Xxle-4YJX>3^G+PM12J~_MyxG*gJ>1Tjuhl?*tP>5pnlXYyi)6@cfzF}w{_y% zg}zQtaB|twx3DF(dV{e)u3in6j?F@j;cNwvmdQtszQ|3mB{nm;P5M$U)2-5#;!vc= zZvfF*2ys%@pNtrt?2IN=Zo*x?#VqB$>Q1f!ldB;7Qh#q)`B8W1T*cIS_%=}7h}BTM z3D(`FWyj^Tw*=%FMrS(6*AoDcK10qsZ!Z=s!$s4+;0-c=o zqKPhQZ$HE?HdvP91pl4@sM< zLIe!PH{qHHFcWrPU#o3u9K0hm=g^(Zy8;U+>$JW5FqwVurRjb002KMrADmkK>7w)o-MZ_iAcGkF6%G-HccDQ80w%ovZ#ntxBpo`r1G9~$} zH1p>DcBQsN0tsQA?c5g}?e)Wy^d-3Nq`rp&5AVH(@i_in%;O@Q=hXnzGS4Yg085$Z=~60S)xsP?@Bw`DxgIV6~vy3#4cGej(Bx_XNHyGJ4_&{IwM86$Jc%Ujj0=Q0WG|ixmG37xV%)K+`+4aPYl2GGXrD zg96Sain{k+6a!K3UR_FGWke5%ckiRni2a=%eBTChD?#kQ`-NCr)VsG2(EP}VOzOut zRXR%)#;;EEUx6l8MJUZ#z%~ZsLuqykH^M?`9uvgyP#P$_>M%5vrVpELFd~%Z(I65j zN`%s!7@QvorMV1LiUveOY3?7`yHy!VbAPF-4JZ&wvlnoq0fs_pj*_MhLRl!y?}2+Y z9z$uq2cll%XtI~vamslvL$c1pW(>#^(%z9~7$rS14C15&6&OkorKK_Qq5%nFdIAV9 z1{2hD*YGR}Ie`~=?XJQ#m{T1n2e}F{w z6Hr73NYg|>)wu;_#j&Wt=4*gEqHg1N!p2MoZ0pjqV53bo=ZG=*W{@!!9n5?Tmlb8` z6OEE>-YN$`N;je@_fI*YMJ;t{qUyWL%`kD_y4>evQE8m&kd(efl7s~Yk8*F7Bw?`n z)@3Q|^t)7UqxxW3OJjelY?Ip@At5WdcFO3;g-{e$^PMm~Q#c0$$Gg7f12x~kHn+a9mti#JI42XFqu2MJ_=A}{` z2E$;QLmb*DP>djbne;&{7VCRan!Zgk^hY3u6xD5~u)mQMW-Kw&~0?>Xj9zF}h@ zNuk7H;_1o}(n#h9-^Qz?v1MXc$V0&=Nft8z`b_X)lIAUf9vD9zhR_!YHv}Qnqu=XC13}{mxm$bc-S~F zxJu$Or1>dm2z);Ck}ogUX;uq)4KtA%cG_f}vdmkWaxi|c=p zQz|hm2z_2!p0T>djh!NW(g1XL1ne*@{QQ(iml%BhTS=7;845jrcVxOe@cb!q2TPO> zJ3k`Q5Dz-9OTk*Af+6R32CA@V5paIj$nvpp^B+d`fd-pHcA^qPLe1Y7nI{i4hw@Yo zCGQE}6zP#>ys_CD$xRoDu1TTn*U~^44tZxGr|}rI2S~>0WgT(~xJWV%=ey+Na+o%x zlIb5fqgsqQ;Z1wpvZ3>(+K{oC=~JWu(9@S|Enk&pS_LTWEUD5$$wgq}e{kT-nMnBX zd%3mp=MMC zLzVbGsXI%oPB%RXx1AMmR_?UV!lyA*Nu9YY}H^J%aG1nHeC zn`br1JSx)YsC5^(RQ-aIqH$ht9+bugh^W)_}NPY_5*OgRrRo<-S0S z{v+XpfnaI^4R~U9mq%#|54mFwljn%o2h1n)YpH0+SLm35{2~NReSZHN-ur+9`RwrK zNH_BCvl?Us3N?=ilKTN`Gm(@)2VnvYx|bglCeT?<<~z+;145vW3=`;Vhw}wt0u8yH zF9;K8%=x@`m_Xw$=%d2~x`rbfUI>z}kU+m5CQ#{=uKy=Imf23F`u9OW1pB`t%6@0HxdQOAT-aHqvLZ9}53j0As4Ff*3CE z{~N(jMuHcv79&{bN_iq?D8W2;49<_R&BKwb`!gi^d0?JtxL}p@zzWcy0F&&2yWNk- zZ+kE}Xcm+)#&40-`yrYA9WW&|j{4Gf#M$Ua%3V{SmK2?BZiOy&4SIC%EkKt=hdI4c zNG;s+DX^QLlt1=KU5&pV5pkRb!V>2eaV1s}TK}?h#&XrqUejzyGT&W^UT!1NdlOnyshWO%8sZfJx z#u~qpTvvXA-0@Ax?j@9Ji&FzjtLUDkumhio#prxF6=Al#B?@DHoS#KEk$Q6O z5JwST&WEFm!@W62NsGF|Nq^4Uqx}+(PSXZB?LbkLLkHse`+283?S~v7wdYHV%mn+D zy*wB=*Mu<^fu>Gqbm52bO5v{uZ&`AfU>?J%wGAaZe>o(AkvT&|We z5;*%bXt^j{O$oRY+eUhf^i2_yceNB|t07F+)$RlIU@n~S60H6~X`}&?MM}L=m4tQZkY$NC!ScM_M3$|sYD$Q%FCn5H@4{kz-Jz0n6@I1{c$xaWV^Be zQb=t~VT;{SC4~9 z9Qf;@Jpy7TcI{Bf{tQZm^&6xOz>!Q|e|cuBRAfT}giwOi8BmQE-X&^!SWeaLL!gJfLwh!sHZbf_Z58 zA;Fu&yQx1AVItSZM>|%3C?$0Ro=_7Uiz&Ioj0S07$s1?jNOVbRqxg;s;ikidkuGE# zeT=#w%`N+_jyx;r{&xT=B#AJ_sARwzGhE_rsx~CYa};e!FCbcnZQ3r8EmTLyjo0uAjMD#n~b_yueDU&1|`3Mj)(aGYsNJHDg|~i z(m6+kfBaO~8i(@k@f~9gG4D9MiFGLM9RJ@aiu%SEV2TbUT;nV9_$+Y3Gk#2DuEa5Z zsZ<{h67`Fp1KVpq5N>hE;pSigulTM~eV5RXQ{0}54w(&RK5-~II0Vd<2qPQj$?^GKDj2C zWd3-Tv~?tyrT+Nh+GvvdW1qDKLX&@dQ=X_LIF$Y4gKLe4rT=({JXlM#2s}L@zMbI& zAQ1cGKY$D%SHv3+rvR}J0uNo|1C|5iM<80tPl9*-o1|zN+K_$nhTzHw#Jk!W--*I10GcxptSG)C)h0-c@Rlc;n33C zHQF;Z_aF^Wg<>|}J)<3?;rB<$A{91d{k=arb#4MaO^Q{I^L8M3M=B7%hT2B*rN;#= z!c&6V9WV>;9$ijm8~!x9H`G9c{D~e#tVAfv<5A31JTk}xA$t+dtQC^pE?tHF4mudk z(6phgJ5e0TqW3%;)u_D~$fDOA;~to5!C+!u5A>%KfaT-1POK;m&H7F-yu27xF+>MQ z14Oa*s5`GS3yOVcTn6LQspX1(tDpDhMkI~KYhlU`2vhFCMsx$oP0oP%cFkM8@I4mf z6E4xCX(+MTH>DBc;Iz!lZ7}n0@UdX?&9Iu?UN2h+ z8jzZ3)~mGq`MBZwDHDrmd=E=6$zbp5pOM#3@ z3Xo3#txF`kQ@&Liy8$@!*DnD*RfWg0an`GLu@9C|v|4d$-*+B46N}djsnv8xuj?~5 zhi|XHKXL()ULo!LHEXWb|FEu4sO?&}4;qf0{YlXLN(a%+B%jh1EY>qTzINYQPaiZz z%qEa+HtC)S;o0cFyN({{;8W!063_E$e*laN0b~PzLwp#aOxp=0 zZLaVl0nmG+!w6*pdTn$V!IVJB&LF=_JgvCBOLW6c#cg_W24|Fjvfg)_-7e+SCJG-@ z1wK%yJNXqDwhCJswvBIsZ6Rvhehw7}mGW8OXCLa^_y^8OTQ;rnKxcJhF71SAZvPtwSV6>l$!maz zJQCuMItIKm1^0(bl^e17ZLq8C@2z1~YPoqaNCAgop^X2BsL`+|?+4Dsm9Qv(IN2lC z5Ide+9YOK8SdA!(9#J5@NQn}Ml*fUo#c|@8a_7ig$wB2&pj<5^dQ`a({3!MVaX$d1 z;TJ2%o2cOYFOMZOIwDI58T9J7l!VPgizR|#V2Qy}=-q4>-WlQVND zI$4-p zRO&Ld`G$^TKaO?P__b36HQypBd6X>&eKZY7*D%Uf#GS)~amp*zr4S8)c&fBVG*HJj zl`UN&B|-_{?OYpbN2d!Z+qpQ@4y3thZU+O;+r_fhcojcsbl|)^TrEAhH&A(Q zO9Yoq-A&59V6|b0&y7|ZuHRFtPXT4q{@m<#>6|Z?Hi?0v`aUqUHbLEeg0x-$$rLv{ zh#ZAzGx?qL5gHivBK|bAC)A1f`2c{u;`m}gd{1>#{aU4330wB~!cKMS5E&8~6Q@d~YZF#g=@ zdlv&7#O%-F6!|2%K@J6;=zBpEDTqn?T`uIa zq*bjZG*JtZoSu`X--&eKa_>2N`0tQOBSw zmGU8JbyO(1U{<*tPmtZ7iHup#k?VE9q21nc#MRZjS^84OOL;G!R4FLogD(T4l^=|k zrhcx^ms-!mfx;pNq*Nzlp3J8$mI*O#LG` zM}lHD{##*NIg}}=<09bDL8JaY=}!xR*;p(~9JbaMY)Hq0a<$*Ua6SyuO#s&i1- zdmva-gQCix3e{Zgtq|p4c%y6}k*vP|1jVhuL}sLq0!}HWJ!Q4@hq~xkiMkl}O{2IS zQG+U!21IPdS~rX+D(_sa5A)XucH+9PbgY<3`%IcBj)r-AqSJ}%pty3Sn{&{UEO6I+ zIWUcd0Oh<8s9~t9#I47G*$&oU{j78wyE9Ba3_@{O461^6$^T#@>LlCZ&o>%q9}Gv+ zHc$HUCv3@LSgh?F@RUVir0ouoK53loLxH#~hDF)lCDJ9v*!~*W*djv_w(p8em&ey$ zi=<*vKDzd>NJBifHUZ)4P{GJrNZ#krBChrxk>z7iwcn5I1C6PLJjo@79MawsnJ2#v zIT(x#i;~f_P$1snXe{jkAi;*Ktc2UO&{?nsS>tVRe4BU=Nd2oYG5zami$}B7unn)c z8n=LWGa8GenLijLN;uA`_6jgfqLEEsoYf}vv#bE21frMViB2G1sqw<_ME$GX#tU#c zO$-xz|62bysIqvZQ{(3F$k+qe8r&CP02HK{b`bJDw*KZwUyRD1A8Cj_pnYHv)1H## z#myH00Z&ZgN_kj$4~RgeT0;7$@eWWIlaZGg$@4%$s-I1;OxDiY-HZyhm*W<3EaBDU zbs*nL@LblPD+*pD>?Wk~65Un$o{r+B(GgoH9mMmaSRGNS3^~&7E26V=SlaMwz}T(8 zO2v=$DRXtWm{s!oh0)b0@gxCEa&r{j$wo`^HHXcObKopA9r|;lYl3OyUlv^x%#5Dg z5*>9Y722z#qYfqMN}i39&B4WLyg0-e6_7Uhg6McdZ)RQ<9dGE_NAjHLDx<;hlISX< z4oqGf9mk5k#Ow&ddr`D)2d9aL#sTRwZBM2nakX+LSR)l~x#(8v`ktU6rwcwV0d9oN zullyO(h1vxZkY~j@!WEH_k!J?Dto)#)$!J>&R}v+n43qEb4fZAN764{23l<;&nL1J zYa&-_l68iK316b__b1C~lrZqWe+3&f{uS;ctZ$Z!YhVoWU8xu*I$wWf{cnU8;39g8 zkg>>7vMkxaK)M5^J+-aQ>R(x@N6Ix#ltx?EWz5HgZs4pyu4QyUn^3&=dYu`#996_~orMwn(mNfn< zHGFA?23%~nMuxym)`KIc!79n9m)tRe;$F5*(#9`QK`-0?MOK_;M!shR#C&YOjO;J< zvF#nh5Z}+oBeUdE@R_leh;0w4ryWN4;p(SF2nzGh{T&y=)T*UQG+D+C*pkQ6uGHY2 zr9(q5m1HC(4o6bE@>8=LB{^4~J+o1kaOF#DKuNY0xWi&lnrg*9t~(@x4CPt*(9B`5 zBr6YhLsZ|+ZIl*vK57+K|^kp$48nCCsuikG{6we*ud)XBP2fGGAzbXnoGvv zb*a8$1p2h_4skF``t#}aeL<-3IjGkHUodL?@;W~uRJq@}#zm>~+o(JPUxcXi!|NIz zqgERkcla8g2&%o)8Uzrg-YZe%v9ExNpRh(ZVQRJ|qmEk7SL{iheK2xZ?7LvXpdX`B z-V(tkw(!W261!H0tZA5)BYCo}4^zObY+0XO=O=`7o*Q1OnVGY`y54UH?;Nncp;6|s z-j-w6`Xt0XA6?%F8T&jUOrmS#)w=Uqq!4DHM~6kpj2v6ntkqGNg|3#Tm>Hij9{O2W zn#|0)m23$!?20YL80U-GSWkz) zV{G!%u+;EhhWI44R}3f6uU6-~`Y66G=WPe1$65^bC@?o8to0PoQL~up6KkUhJ3SM) zd?q7(VI6S7LVK?{7Ro%|fPGsJ+k9ZnAu)!5W>UhZ&;> za%pXr_#vD@W->$aUpTPm`B1v`l8;EI%}Sd2i`Jbs+!j67EeLf<3&1coGZZ`N94b1JTEqPL5T2u$+< zKHvKTl9d6{2Yg=#+BFzj>hUJfK;7QD+^n?*3_yKp@4hO!{wQJwT36`n%Mt@g?N@XX zF?7TK727iJNcBstvaPQGsaZ_;eZUJrEJ?KVh~EOcMqi{3n|H_)5__h<;T z^h|82(B5oPj{t779E1dR-H4qTh^TDv3}6)tLAKH9rRZjk@Mp zfi7fGT^N2Gni_S@+dB$^VA@q(j%P*4(9`8ol_)PnA$3v-VN&$U2#Sl*|3pz#j$SS~ zphO8lx>Z^M0w*Nt@sYU_QTi|`o*X19OYbM?$bBG$>HVay)4>AL^bXRHD4`*7dbs4+ z5SYo+-$=3UzCeKr33VKjmN9>?x?>DgNJ=ejvdGWueJR_MeOcq&4mAWz6sPjzdP=Rsc{%=##JTseQjb!gj%bUQ9 z%|+lv`m-|@b;yZ=?Nv)Qo!OnA@3yDC$!e{ZY&%jLt-~dnN%P`vKX=Wgjby_)SwFvc zb!h&yG1>kiQ|{sPk$%#$LB{|m>3pHo&ZcDZrNyY%IVT@nluz_E?zon0H&+bQ3R=5f zHLZ;^)9`8g-L01FypSsqq7%-|E6Z&=kg7t>KY-;ivhuL_k|1!Or4TAWf5RJS!K)Ty-^ zC9&Z755aM&@$bz4o)0>)!ILm0gAyaR-C%QT4=wPG=C19|9Ayi|;B38(wPcqI^kL&7 zb)?KMQVL2lP;H~;m`T2pt&STyWuN+ISItT4pIvm!huL3EElIMi4qwqx9y*Fc{j;;_ zLUUP?Y<3*Do%&~+M2FGo2NQ}SYRUF?4Cg5!qPVG+Y`$=mFYDmE-m3eXYGvC?bRoQA zplJ7^K{ix|hNhZr6!+@Tyk=i9LnEo*S7CQKRn7SV#pKDhCmw&N6OTXfMAg~0XJ)HZ z4k%Vu@?p}=dfHj21jfcov+@0lPRIPm4w;tdXV{bsTB+<6l0IkViuID@vJWXyamI8! zDf*`uK}MMk*Q5SF*jXk z>0A_Q#n=OBt-1Aqw$)dPR!L>uz<#3p@;-H}wAN@z0rr&R>nnLRvBMJKaru^^smiW? zA?mW**JX6I>~mQe)UXa<(n=@u?JKVC>ty6gg0`1<+hfI*S;ro#)h~z2#W4M95-`i6 z=Ge|wYYko?dW$0LNq4f^Qh(_8wfs+#J!{lz(6YU&3sUR8?5M=j&ztDWOqcVnu{n%$ zeI{{vI_4Fp%w=W{WCRPnVory{70L!|<%SY?8+mV4?YCTq26cezhj}fx7>YEyrv8{| z&<XV zEuFCGFX>zr-GP=U#Ylg+2P{oC85KRfGAx1Jpi}f)6BGA2w8m66(d<@k3wRoS%qFBj^Z;Tw}}&l>12I~&ZHx7Ayq4vEP|CAo|e&XZmAzxIm4 z^f0FcTCP6ZaAtQ*zO$67@eG528WrMnVFfHneof(RtpNcB>DI9-e;e0{?)3guCA`G?ylb2YHe+GSEs?2 z)-IQa?#Q2wuLwt%`*E_h)oOKiXl+&hd9@eCemH9_owj`XY0IabwnMA0{`QhlYs=K1 zdBezEOQh_q?2T39k?+M@TYDGe=f>e;HgddmZK=} zI3q7e+<4g?jr>Hh7Pm6GEh*hmb#y@(&U2Ej%}AzUILb+uQj&c*$tvloB3YO9+M7xgiadVi!rrGcTEVk+b2(iD%LAu9Ctq9b|8}W+afi z-BXg2IEgZUDSnF;Oc$=KTJyOzR8<0XC_iRiB+1xj!I)YOv%lD!UH}Rrn^`Bmp z6YQ4~99~VZ%};Xt{tfU@i~~~qiM4pHwgXejQ*Q$0a^yyZy&sg4pM6uv7eNxa4s2vE z$tE}4Xzq=~F|)U1V3-anYM*8#u(J=QB&iQucBjgUH{--H|G5%*?pjWANF$Qr#Ge*A zI5Z_WvW8?;*rtVK5f=WB4Mh7_!P}{K$c|(djt|4zw3C9b(&4$`S)s(an`7u_( z$%3Xa2~@yIg4JLOApVJsxI)68(16dJ{IgLMLO#L);%&VCW4hC3MimYCtMo=kn@@*EFp2% zny>_=Uf5N+qGqfx#Nw{oRhdDdoE!Fz%#c99ZIxd~l5JLg9g4Kw?n?|(MO*VgEn2W> z53s9^kO0Q7ZS)N!z;*V{&qy{@EP#1>V2VGi3J-PupcH;+6&&#Y;Cf~d^AD+K26n+C zX@t$-&dMT5!rAgL@P}(oxVoB?K8gmJb)>VL@}c%A6jF}DFj913>Bt^tpF$Z?IQNPV zJ+@xDRc|)*oUsa5@aL|tmkyAEk9Pi-QZ83fPTbj8nVl2+;~BE0ZYw1|xiK*?tZutr zT0~n~S+{c&d4?v}`M;aUGqk@)r?h8Q(au#o880HwQ6EiviP+&|(k9Nh=}i=7@^HFJ z=Pjr;%ADKeBj`${f)Lu}4U#vFA}_$7c%0--Qz&fo@lvBYgMmHPlch#eD0Eg&k|u~0 z3H;O(E5D9h)DtSd4z1&vf~)l0p>iwkF!9&HI^$U>`Va|iR(3h~*@9m+;E_ige~x{I z&+zkb!;75Q3%p_Rg%=4L@w|z@?k$fZzmW6A_9!-oqTInts+)yrgO^q}8&5p9X!d2* z&2EHIVLh*qnhgTB_Ir7X-Oq{zQR*v&Y26+3)3RR18aUw z1AdTAzqSECz_L5L3sWG(C%(=+h8xAqvWF1MFrr0aSsxNSRN%(R>LQvc5tdhds3jbB#(RW;TB1Q$ytkGU)DiEiN0H&cS+7{Ov?B~VHXgT-`~|JS_CGkr^#A`#hQzTrlr*dd zJLn8m@0BL~C%fAky1~#_K>_0BZ+35(W1%ts)jpf$P+-Y_sb&C)_UCE_fDN~Iwhxe@ zJDuy1y^Ke8dl&oQGrA(PWP4YMjT8)3*51wT0W+w#El~q%;m4C4zq_Qj6c04q-oqY) zWXMv%?L8~Gh5BvpRrz(O-a{nyu6xe9`b(T@Xdf!sWC4ew9VT@~fd=9oY0tg_@Lb2W zD_*2G;c}|OYxzftIii+-sGOq&HBMX~FKpUJ3!RuEp-PUiC$1p4#UOIcGr+ssK zfCMSUOi%k>SxRp!qc7ZE2wo;;&pJIOlR+<$CNvF&U%!KWwKAOe zI#r=YZ5Xb_6EDDv>-HUmK!cM(qr8(e))^Hr$2&`UWkhOu;Y9`AT_jTw=$6$8R;HPh zO1!G^mvvNCtf<>pH~zAr<6pj8<6c37@2-t|1rEErOOs+4m8+_e=O#s9eh+D?h7{u{ z%-6*2dkK~su55TK*s>g9I2b6jruzyuZjiu&?o;`7q``YvejRFUB+RYhbnZrOl6nW# zyE)twCVB-A(|vt!I#2R_==Q1*^cUz=ztqE67*z=D#EaojIli1|Qpb<#lTpEsF9|yv zE&PP0S=-l0jc0JYf?N|ObBtb&m30*~K@Clks1@yU;znbypqUC*o6)d@Zd=$-9ftEc z$xO6`9nB01t#+GawJ8!|#SaZ2l?2TyEgw zFz}{2mzyj|&gvP$Ol+Zp1L0}HOl%CKsh1LWD2wmXholJjgoudX)6;0MY5oCf?rfuAqU8Y$i$*1w?Ai&!K27fNhJZeZCOX}?HPQ;COK zeX-T|D9uBBg|+ScGdL@8Q&pjU8_y69reB> z-mz}KQ}ErG1S;}fg1uo1pv!kR`ov^W#wqV>MB$fv5bgIgk{or}`x}wObGO(hY`?b= zU$CP;EW|BQILH0)S-VB3@M^TOh$l$c&)L}+dD`66ddQiFq?bQ$?=wsRJNXNC`^N;J zkH2VFCdT8H=9lbl+eYF*BmYVh=}}LA)$YJBMLbdAZ*dx+Eo@*C8qm|Zn=gs4Q z%?aPJFI7r3?2PY9wvmHDM|`i24Wyax*RcU5{F;5mRDhk8NG;t+BLC}>?gr)lvOnbnKNC!>Ab?~0=k{r|1PAV7HY_qK{}Hql`D%cUCvMDE&Hojg4ySO^ zOEo#&KLvjX=unscTMz``VR!#ueeupIxY=L?>uX$iv$z*Q_ai_`l; zIZ<1%#K@n=iT#{Qr!QC{p#eJU^u*3bB;^q4bel1T&R}+$F$R@3kUDcy2iI{-Y_fB# zovkdrIn}n9xGRAN&UJOpw|~Q%jndte4R;eoE}dK1`(5MbVU)N5hF>7Dl*66X$ensH zq1HK0WXZau?;J0(WLwd9?jXQ7J@tn;4yU;FD+LJ%+zGgstM(pXG{9j9IbueFJjSFUZ_s5tP*8Ccjst%@!IMkpLG~%N zL_@J3Y!4nwFp&2lb!;F(A6mx-lop&DW|v4W;?IeY7axx`(gLcD^BgT|=Qf-xR;DkwBntRel{w^zF*8Ly?61 z!&psFBV(K(*!-@2&8N4j^?asSNNZsfr?NXgvxnyS5tb+n6JiEGw|@dtK=u8?9%f(y zpv7M{VF@YoS4~)gI{i&G0XwPG`E9if^mJ0^clLl#IFZ!(eFJ`w#J_I94-j}@ZV{bY ziQjOyDW~V_i#F~MCGt9d5ybJ_^-X6OM#Ss?)jk;mIMmhO>;?|dKvRFO`u^+ zLUMicD!cbjflwP}^}()1r8uC5yHtD^$$#gH??OSYw)cX@@m<*N-R#C$qQOe7dtbZf zO2O96x2|I!_?QBIVr-vc0(UebM>e_>`xKKwVORKe9cM_;8P`;P9d*ZA<=0_{1okjr zPgPz-jB6?}V6afAnZ$sB0;$4k29Q|uY6hTO=BxY z3{u`kJu|RpA1IiXx9;Inaf3~}`w0AC)0w-Z3m+%Q80sP}bZOOnlF)R4hU$BwVB!TB zDDMe%Y#^o|U&jVi-1F`0&Z!DRqHx`_gqz(L*nKJ?fwI2P?o$Z`5coxnxI$9DxDi)S zbaPCBZD&lqk-wN$xPzBAr$ZdR%x$?CZK0YNz-UL1EnXm_V64(RYh72icF z{BXs0p$6Y-cZnM340WY9oVo12&F&H*9*XsLyVHPhAl*CanL$Fnvz{3!>qi8g(6RMV zp)VHb4paAIDQsUG49N6x`J+gdPsksIGJH}Hc`>qW-giGG=)Z(Ri9T(2+5im%`%En- zNWRb3astIP2V~gxs6vl6ovL}^^MYTr5kk?wU=OdXbU+DTya{|FbqHr?tb{P@e1Lt1 z$qI#9OrUp&ecrkcvL22}>v=eP9!ofYkkc?5W?3U>pV`hh`i4``V{( zN&_VC?^h{FK~l=z-@h4SQ2PD>%@~97A7~$I83_~pAp2MYa47Y`_E8VeKw#m{Hvl79 zg*)E>4vqU(m70m?QMe%Ey?2|+F3U-vdEdIy_;Lzh-xoFF3K{r?jktoMA6hx+Ht4C( z-r?1FdOx>!L}kCx`?|DM~ znn!p&bJ?El0YOTZx9JH7K^PH8-Sn30r4tSe^iHXlPB=X9FMHq&Ddnv9zwLpuNDQ;O z|EZT&%7*{FjD`eG`S59A}R-U<=kdm6Ot&;8^tg zRC&x44yC?#)tsB6fx!2w&Yau+`a3D9rg-$>*@<(H3Z)&XNG%K}Ctpt2}hATF8yFExHC>Y_s4->(1pTW$?4)XFYuD`izf4adIQQ0-^6#Mfmvggw}(0< zpVRsEbb3Q*+h!2X_5NH=w_0mcX}y;mKCXqwv+@ms-b8SvC7dyIo_!Nywi)<~DR!e5 z*pq}A6uQv!?WwQ~3Ebx`E5D9h=PfF~4&CNycBRc-U5&z_DW%@&_HhW{P_#4b;}D>M zcxTpffm)%Lt{n?r`*V$E)vw6250l>pl&3gn1 z032j!-doQMO3b{ko*Ag1uS?9t>7pW>CHmAvjy8vj*nUff_O>NnQ5nBuUeRGk*XE%< ziZODE^XoV^-`VcwIFkA8<~M;-9|{Sbp}Po1QF(b_7kh*+v;*%pbWEYV zb8iZ%orfI-vtSP9uM&rh47#nqNgOgN;J*Iem_y{o{?V92c;4w$drVhQC5u&D@MmzEJzIn+fD#6$ z3;qTXl=2UR%6b?NMIQ*2^)LlAf`1CriMcj{PXYA)CAcU9PSS&jIriU`4JVO59+zR? ze>ptdlME83fsd?e5-H=TswSZt zj<#EB3bgH2^pCN33nG9#A8YRxga^s%;~MaTBo(S4A_6Gz2~uCC*9RPTrb|VU5!XM_ zKJO5s;Y6SG$!Sh%^r4YAR|}DX=w!RAB}B28IPfZ2e_2pM7^d@%Pg2wUQ-n?dc-V)h z+6OL#gDyO+o*C4Gr`IzBPXIqj7zF&h^V5Vuz|J|F;wEeZn4NEaO4X-PB=F>_PeXIO znH?BsrMt^6JIBsr3WR-S$~v-5>J$e$=@u2ufrUc<+zS3sFP>M?AQbqPcI$|T?&@k7 zjA*si{`q$6fOshW1$I+~aG-@-)iZ;Hys(}bDC=$P_7V>l0ZvW!Z*5-y8zlBC7YjX- zLZK>dEA&VT1WFL9JQ^Gl|Dq}`g1nE9T znQiLd(VjN6(l{ytD!99|G@Jk-)bU-U;RNtN!*^{UJksw~4fuiDy}GIk<4NRuYveF& zU)UQVBv9zPS4xpn0I~1Uh$|%cJu7=XCozA>Y3|iX5~!|wS9RaIN5foU`vt(En(k{~ zxBxUzkyFbFQjf5M0pL)~vA{`Mt&|jI>m2CTKg4-v> zYx_af_os^>@tt_XEwnJL5)5f~R@HD;F(dH`ZypMR)D(kNE++60Bl9Xnz_cgT4`tUC%gYVfh&NAJ@9;?2LK%Ozzgb`K|S!odS+k` zyhu>dR52_cDeu3yF&Sd+CBh7nl3Aaq>c3Q&4KNAR@5_XF4O0LWzq}DwNRU@F;tDGI zixO9UGST4+E&9^`k`Uug!*u%7|H`315Nu@Tudb?L;f=@U`I;R6!+JcdqR{`oU{e4; z56bB4$AV*|X}kVUgc*Y5>-xVEmXbJ8*Z+0JcTw;CrsBJ>&t55v)ch5DEX`|$>A65d zkzQlZ`jucH*Q@K;K*GJMjtwZ`>#CX#$urUYH&rzq;^&|HZ>*9qL{C8X-%!T}lJNC) zY(NR$BCw&eRy2lwt1#p;ER^nTLKKogfoyNDW&la{j%o&=T>p@o4QFF-*fG4s3y(Q}JE2)poOr z@51i;pU}~4lX}pRK25e{2VLpYcpG-mtNJwBJX@;zG~6l_3g=}de` znK6wY>rVsMguVGr?8tdj+;0yvOPI12#LNJ!x<0K`LCFSV}%*WE?HURxSm zW?wC3VC2UKm)iqGgGK7Q!tMbK(&l>ySK4JX6fD<15X#^hICHcEp}>-X!;TaR9T_0# z!n@eJ(BOPitsgRQSNmMe@G#vvxXSJf43Wk04z8~1AY@Wz1CgCCHOR;)F!J=9-@3*> zoa+X6tI{R3!$bi3bS1M8lfxobh4P@mn zs$&C6_+r7p(&KC_DK>aXEqnx5EH9NA=Xtcj%Vfq`E^YAgT27EsUQx>l)XJaj9)(>H zAN0Dxdwa~QacVY^5;A|VPhM84BG447Xl6w0 zhV9@tLI(mo+`;@~3TOAEGIyz!|ai{_x;;_Iv`RffD{h?0lR| z8vIl2e3U;L{7W=Cm@*ptyW+dZ=>AjjU1)TB(lmGWq{DJD38NYGY^@eu2dXK@Pd1m8 z6z*pEB#EC)V06`L(}lZ8rt=z*vcq`;l2nycqYW1k;BV-!pjs5kar(6Ac zygm$3;0JWxvfw9CSR6iX-CQDwrtZiOf{;w_t&8YjWCSbGq9|k7I3bTrp$blYWMrV5~CF%)t#_sqg zB;QSdLHKSM(NuM2zh*d1rGA~Jjwhroa2tiI>hyh`*W=q z3SL8#%#~aBXJzuO^@0k);-oyL-g=$XYT$0C?<=)lFT_^{tgr6n$TvuRkRp+4*?S@g z3YGh&%0`hxezV{SS3+e3b9ba28ypF5k;W*%BS-jF!4a;6#N`4Q($5FT_?c)>GD)DC%x47=r`b;(B*DR)msy|x|g(mvxYDFXS z|6C0gJcQ8tVl{SS;zva+_a$kVWLSTu>&!)Q@rq?f(A&_Rdh|+mTUx#u@Td$d+S2l^ zL^}E&)n<C5 zIdsQLPT`xWI|<{cM5c!-a&!2*Fpg5VGg-uI)Dixp>H~%X^mFT*W&P z8Q2@=ab^1o$>(KIsJ1I>u+Sp!C$SLHpsVdl))L}~tGyz1o8A&A?*_Do6*wr*!zH&E z&gZ7`u{XsWJWT3kNC7|bL?PFM2+}koNJF*Ft^X4mP|ZR__;_LIiwG5sNt}8Go8~Qz zlbX?~e*@BIHBlmraNEypL;)&3w6AZX_IO?#NNPW{5dkpQCkqJ*NZ_n^!*J^9+*YzE z0)fXiP@NNQ^GE)6vVv$orpZSOQfO9>yGfK#GmpM$lt4x1nbVW2DL1ryUPL8*WD_No zv{2L=ZW<|+^$|Cb6bSn=(S2=D_r=~N0Z^m__AUtkLWy5lft&h4@gn@IDsWJi*GYpx zXNz?e2XZ2){aR_dF9@&(-XKk71s?I8t49Ar&_m+l0Mg;ZP*++2T2#nj`SL&vW1ZcFw zUOlm3aqIina1V2CodarEffsQOtizV$p>wbh2k4v2=^26?c!;nj@B&@CZ@h4$k?&GJ z*f~@PX6#fjY2G2Z3Jj= zu`<+%!rR0f4INgTl)(dcF)L!8&Mhk?n5#u*fXCVyRUer0QFf*&& zVjO38E*0V*74LYFpFRw<5++ID;W2!f#9m1jMl0#P&P8D(R|!Nq$l3^_H75#(Yx*%s z75)|~zgn2UtVBx0A^^FYFga5~^Exv6rm2v2>P;bqU%rPBOmK4ONAKBCJIJ@+OXxmI zq?W0(wPK^BbMJ<_$;hB^ju29<`tbzCUiTAOi_^k4E8)tDvgF81l))57H9D@S21JI| zNSzfS93VuYY8$4S|8XiHa$A9$$$Ll|921>=UY(fK&7nrOwyD zL^WF^&^yl%rrWVMQ|^7JYK)js2uzpLBJdg zdhWSGcx`QQ!whymk1-!K%q8XDsh`4aa1nt3uIV0UB0}Mfep8`(pLx( zYKjba!oBlK$)XAZq>5Kbomar2I$mAdIC3Cw7gp8dAl%6A;YRLMEhk1JcwMCP4q@)4 zbg0P5I~x+A0wnJe0uV|Rc~kwyF=X=HO-V!jToxjIk2J3mQ!_K+O;=zK_+?@}c2nxBxQ(>Q(G$w8L!VPR%6 zaG-{dNP{6vk0+A}@1s@Cg3Y+j$E40n52j`-sHk)#AFmQm;T(TDFCS{^d{PMWQ@C+a z7C-SRAyg$)sDC~!*)664J@px3B}gb>Cwx|N8kmH2$LE@T3U-V+_sTcC35lhPz96gy z2_^5WFIE{1jFP@58I6zw{=Ve?s`&bJeABUpAb%9xC?tU@>h9Tqz*CMlm$_@US5B)Wn5gimx)JEQW7V%O~DoN|-s%f!i_1e-+EKboXkY^u-LRWIw?uC!u~) zlNQO^UzoUZG}vqG9v~Q4j)h~RYqo=yCGaqut$UGv$0Z4!^y$o1zu5Qg9#qX0vKi^% zVTJ|z@3Nvu7^+hqrLXQDTHP>kPTf;Q%|tQH4tGxz+fBhxG*# zZR8R`*M1NzVn4JLF}zd=V*mx)mFZq4*cKo{f?h6|F2F-^uMh%k0H69%6h@9-B<57+ zL^MAW38?bGaz**qk)CsN+B*p0rG?gfIVWUqs^oX%^rm3c&O`p*Yl$VFYbYL?^EACv`9}F4L&Vl++7vsj1-ABl)Cp5q82&D(RC#? z+Or$om3llX^*EGlwkoI59W|h0>5T4!gfW}Gw=pyO9=iS-TnKd^EPRR(C6k5T=gp}e zB2vxtWA%z^9wuslpe>f}Lxs5{2$X445A-mh-!m`_R~}vog=f*akEn#A5%WmFyBa8n zKprL7vH`+T_2^0@*!1W=rWy%GlC+JHAvfG2sDGAVyUMLhU1zvhA+z}nLMKzalTOt+ z)w2ah0H~l!o+B77f#YvT>3To}L$n(OX9nOptyabk&lB?di~zIsI*Bd)7jBf6J}hsG zc3&%`JqUa`9eMCDUHA2MAHu7-H%Rh_p?Q%fk6UtrHwpu_Bmn0BCLt^!uLLYtYZPuYtrOk&51~yj; zc9+X?B;>wn?>a$?865^cLU1q!3{IEy9wH2LgB-eQ!0Ms{g&96j(4xU3g+EAeH3kaI z_2HG=!j55Y!6D>apNyMJ=o1^X~~jD2t=7qAHK!vt4nkQQ8;WfHDPda+)WKy^d+pIxjiVlNu7ii(b7a3vmr0DvJZ; zgijGdQ%acKFw4oFD#Y@fOkW4fsh-x1D!XctQ$1Zs!4oQcravb(M~(P)J|zan+jG)q zif)&aPJw@XR>W|L*| zD<^z@Gs1H9loP(NnSEt`E2nxvLn>W$wRohv_o8N6ocmCVFK%XyhDgn#i_)rI(yX)R zN}cYw)a6T?2^YERkm_a4sImuAbE=m&>$lhs)Z%cpTp#yd(M+~@%?rGw9H6gkHfGW{ zN^_c5HS0FD1}xK4y;nE)4WEAkqOPZw=Ji5Z8P<)`?HZhbdw@G+F8XF1YP@KX*B!hOv7mEr)hMM-|tDs;&K|SgpW!c;js zMepO)jG+$tq}=J`kVWqk)!MnnbA$4swce+y(fCM4@6*+6z>fWFWyhKjfp*pBq^Sqd zzz#FT1JA6csP2Z(H~A>)j4udjWJCeGguQibBY?{LvS^3Khf>9TMU)}KGt2#|*zXwy zlK5+qgUZoR+HXqCq?NF<{a&@gD&KJVmJ~)IinUodTw5vkihJLdf;~hw3(mxV zA}SRUh2^U8yV4>8(`=O2e|q0*q=gNTTVt<^V+wL0rS}7=PcnD}JMf3nz|Fvms9^j@ zl3OxRWEVfKZWda_PoyzWLY)zKy`U$Zkc0%NXiV>C)xDHbWVI`DlAos}{7?iWp)YIZ zw7-yskf9B{buTFPHVs@f6*bI-mj%4KLjsN{P9GeR4JXGR3Rp(1KH}| zR+}fef!6z-)O}&#MoOD5b7H@jya1zcbTRRq><^N!EXk0~{!x;n#6!FNQ#~AdZs~|A zfa~itWk#wm)x(@GR#)}qWPN{hMc;b}7dcb?j%4YSrm(+$SF%4q0`=6F>fu5HaQwM1 zJ!b;(K(T%4ITMJ7WySkD2;(hsS5!n#tf{|67(O`~o;vDp6?|R>Mta#%7%~|ainvoX z6l_8EcM;4i%WqKWb`^Dz!I5~o398P~G&<<-E-G)G1qb~-MCGm1*r3064K@mkE${xG zHCXt5`d&5ai<8By%*^)B5Y!jD!ITw9>7OYulA)0@q?5fF77m!RtD#VBuFxOxL`58w z-4&_b6pXkX3hOxog&d438-+&|`qGiM3<`Q}yU=S{dX5VAAW;hj4xirYKez^r^zaa= zhchtL#6tz&3$aehN?~pft7#s*|J8qZ4d3wIX8#d_&&|PPQtn?T%uX3zg*QRq!52^a z53H89pa2Sbr1X&@eM&p_QIZ%X1?sLxOLkM@Aw!R;heNA;ti(;0;iKGNFXZ0}JX(Lk zTxY9cMdYrKC%E6B9+C~3KLa1%0LPgX1j~8Mj zLW{KZi*nhS(L!<;yt)G*|Vu8dhKc})~7;t^QvgNd&P|MG))EzoD z+5dv%0Ra}b{&i{m=|UlNw)&3b&vLLo9)q~~yVBsnaOIiG*yI~hm*zB)8Z&XZknDd& zGM9{?Od|HbS0ihBw=M^NO;URf2FCK$8mwRDr~BWOx`#f2fAS?);!|VU4 z5ed@DzolVksqfJLkHkGI?a=?PhOthJJ=#|n}Q*?gRK&` zDHL!!kY2bkNZ5ZLkp@c=*$nREvS{4mq#c(W4E*XxYuZJA^%GKGt^*j-Rbj@7kH z%jDZYN|EGnXm@)Fc9+Z&T%CMiYS{AA0*RWO{h=E;mbKPEs$YwVkYbJ!rW;HG)pK+s z3Mlni5?cyWUQ~cbvj=BON;M=%gqsPL1F%qD8 z_=#JdU>Tez_<;-$Re6i*rqiGqiYh&WoZ)pDw(=34!THtbc%r(h7~O*ls=GBqBX(|8 zEh$*f4DK#K*F9Q+aBvS{RB0%Bd~jecl=4Fp8i{Pm+$7~k95fL2$GwIAShsKH4(=li zo^@$6cW_^!Pu6XlxdTUVitCom+=28qQURXj^&kgs9T#fiEG~TwhJy1d&}qHRw8TQO z<{gY05v?rLz$uALK9XNA!$kVJSU8T{IiOq>R!)R6(1qf1nsFnVRV7&Yok>c=cfJUX z@|kIY4qtcULhZ%o_2Rgkca*{c=ErLJ9_xZFufHoFT#$VFoG)H@*Cz&0sE$>ikqB^#k`8 zEvcYjo*)u^O+kZvfponI;Gs7#346ZHyy5DzJa|U6YE-8wFBT}>_`|hg z+`dYZLBGXpSV6g^!7C-}00itI=fSI`v7nFc)8YOa$(N*HHW9v7Qc(&=gZFjSP*6#E zAU#%Zpm2OikJTF_Ql9i+oVi#)_3$Pkj>ypIqSRRl!g8tkX32UD1=7V^WVR<{(t2xk zvoL=m2!I zX$&Z&nSiKLzamX{B?VILSL^Xe!C#ZSQ;COa{(3zeD*GGt+#vmYzY4#??uiV3AbFr1 zk8~qdT`kZ^Ge44?e2#{Ck?O4$V5pU!)bW9I@>6L})94|;j4=3FRUZxMrGSC7V^+eE zTBO6nB^>JIm$hi9m|xYhf;96@DFD&%akQAu*>L(+l`=CDq#bGd6cQl)e5cBUGXkig z?>4|g9eu9>Lr6;>uhP`a&F-qq8-;E~_{S zi#x?jJt3Kg$#f-H4-*cA+*U?t0&5H1l8}kX=+NOmTp(g;2eC_1Qq(0|gqWwm>KjXN zr)Ftav1d{W*ojhYstoGSR=pxIvUGTLyRgT%3b8W0i%k1{$4WsRUZr_yrzR9gnL7(^ z3Q?4am8D$-KZx*9p}SWauOk!|TiQd&cnV~2J9KH!YF*pPj4ka|IanOA+Srmg$Itgv ztc0-W*pjL3#ov&VDba`b6_$C79Cr79O}V9&$d>jOW_?NtY~_H4{Q%A7z=r)m%abh~ z)bL|q*L3NKYTFdHC6|t@=CRzJ%pG1%cvLlyCE=pDm$!6ubvIi#HkXcR_#vqIV;j;y zuXb?NOrft9JJy@%OVVS?oDO=oLz_||T^v@WKu86ZaU64`>xyi78m>6rs-6OtSKN^k zPK(4vj;<3cAci_#peOX-xs;o-b28P&QCoiZ4HT>z80-RuN-|zEi>7Ma!zmuYHEytR2yksMoo=V zc=`)vagWyJwJJ4-N^<-awKD4nKS$q@8Ztm}*Q`+it4a!H)zx+lc?EsIuE?k#J_4Cc!V|v1x>$BV}b;iQ(os7t7V$-Ojl_w&rNjhV*R!*L6U8YgJy?Tt6Wu4dZPTX zT7h_Q;_bZ6I?By+pR+Y5c837nJuk)5)R~ zco3m`jJ5AmyqdOGOgLA(vUi5nS*KYm2gRN_!5ys=nZ>HEv${DI>yPkrG1pq-(-fD0 zcjq2p{cZ|d%pmZ3t20cW`p_qN=N@~O@5xrbrXRD86CKv-XvdA?WpHw$`*iEzow~#H zRjzpAfOmhMVKobY9Y@+Qa`-c?j*P*biRZ2tseqHM^H9FVaLmOT2a3I9hR!K9B<0H* zh`M)}RVcI0ayIk>8*nn~ZCG_2s{hunQsj2*$Mf>M*V2hr9UHz#ANE(R=8@hkjP)t3 z0vK6mRTbNM!DtyCj_JphNO`lhq9h{5gKe+nIz5tL02_O>b_3H3h#Y#*zsruWolu)xM(U#sT_ zKW3P=Qeat>pgYoA-tvaUaN=zjYl43x zxbNFEf+%1kzgR7G|40K@gF=x^*@w5Y$+=!p`IJa6ud+^DEcxdXLqP65{A-G8L;<166GJ!Em0 zyI%IrQk@P~)#^qUhl8r_{RiuYq0JOP+aaXO0W;aU-w zcYi{cs{julF#KtScvCM3!ik?le@$Z*01fr@0AD)%Sw*{Ze+-|q{C!2c^T-?ebH99D z^*0s$7I{IjJJJ1lg#zNlEfnyJiUxH7j3Qq6Lj^F=>#`j9%Ze{=xaCUcSCzQpu4n(v zR6oi)B>qs}5Z9Bq7Z1zrvB8D3(S^UfvPjb6Z2IO)j=K%@cnViu4A%Mn74?C{UGp3@ zqb!0V0sTEX5*6WOq2}zxioV}IFH0j_M18PA2I>2aUf>NAXRa5LWeCq+@6O?ziNS5@ zNK@E!k{vE|oB0<~<0({gF`Y&Tk>xwLqdr%WB%unk^i6sdHtB^U**qGU?c}{JG{qMC z;1cTd3QSr0cEBopZJ3Q^9mG1TpeFL*xwN?)8I{}mgB$6HgrH@2MNjHmT3htopqC$U zEB)VAG=@DD+HX43aI{ceoovp%Ev+Rt@ZC6r@7P*Oy|Fja|Fxyn^+)km^`}K|o49BD5QL?z+||3?*7Kn^+1A_bOT;jbk9l?1BJR z?_Z53T9^ym8nm(%JtA`Oq15rDZz-y|eK=8K1w{|X{;Zg@vg0T~b;TV%M-3JiT&ix} zoU)$n@Z?N83AnwOom|v`_Pa`A3~0G%kche%Pa_Y1+c|*T8P53tz&n) zq{qW(aFrHjlht$zm1g3`&P32}LaYxRIr+1FaXRlfZQ{7L}_f{SF&S=Zz z0d@hPux#PB>`RIz3;X*~voVEoir@FA`p^SeWvV|rNrW+^q+#nM znkZ73L?4_eC$tHTKJ@T#RA{@lmpnOl@%?s|2v_dw* za;eZdigr#0iPrRcrz%bh^CduQm0DQfYtSrAJh<1;GNoaT6LEsT9_=$jX5L-ML2S=6 z2-PSlCOA4_FEi;1_*BiYpipXS7uwS*{G>B`kF9AvFCF$uOQp>W=rEmzD}|JoQ}2=8 z+ILk{m1Kui3oy$iYp|#K$3zrvS%IpxFJ(W6u9Q}IAnm{$GYR9wFWWMk`6qFl=?eu9 zMrJBcJq&d9Tcl4I(n7MUXe^lHB5&^L*t*cBIS+XzN1Col-A%@qvM^n~CJhwf8PB8X zyqY($?#I5SPnDmeucQeYE7~#7Zx#aGfyM@QD)iiBb`Z1nQ^YDRrBg7a(q+aR^gL*G zaP@eZM$Z{@IlMFjihOQT}I+E(op@C5v2Qc>5XwPMpjU8OBy5-Z5Ajf&z=0O*~vNqW zjqhwBlxrkMpHGd@puO2RJ&_IK>sv53Qsipjd1n5`4YM+o7!F&$fQBsw8Yga2o`*ky z>Me~A({+z91dpd?lu~3%0^kR4O;dTAHz$cUQ$E9Nb!u6Z-H)1c5-t>y_TM=)m`fse z7V9IRX=|~-KF>tXe6^B}m0wC%p*fD#LtV^Lj#Iy#s9cYaKCfcH*4SZv>Ia4AS>81f8@=A0h)%?PPpo%tHm_nYo2o4 z8)XT=ViyO00@@a z%vK%XXH-=)o5H|h2UE zg+)}f2Wuxkx`>w;AoBw&YG%tg&>`6p4JRW27uoR%?K6sfp)8CA8Y3k*v!DhvlV}T7 zL3-kX+~1B=7+RAv3uEEH1LZeuBLq0?bQ5>%%}!bIRd@aW47-D&qsSbJ3glM3I96Vw z;h5W18WLp8b!vu8x5TG!DTI*yvkvi+#6)J4uj~bdzUrOqt>sHJ#^eGwbKHx2%Htm-jgz|$u zsyNt0hjL*^YvfG@YbI~+@#ahI_flrvp;{!mj@h%#_~|@~_CkgN)j2v3q`Jv4t6u4j zItv;uLy$8r%L-<9k`89bi3;A*Ycc+qtjD+zdc&uTNY0{5${ZESt~rC~v)4sr*=EmU z<|q{-#EU3h?I$F!z8^!!?i}uKy;)G78ZzZ2)vpNs0F=?WhlB~-l^oL%KKw<#PU(JuePOC*?9Dhq)gQZGVp8R>VX%n7>7^&L_GZJh$jHcB ziONJ4i*2Z*EQ(x2 zcOUv8a~Kq_;`gCHBTHz1h;oyIkW;;Za+9H;Q+=55IpkEgQ>O|+$f^E?_D_bi4buGt zA*NyM7GX}up^I6BLQaQ0G(@1#8e8l#R|dwOcxP%uIm+gVyX-WMR)=!x0sGe`h{-HZ9Kh@#?ReP=6+sMXJY z;quyta&mn#J^%R`>}Ss%mNDoNd;8&DaH?P_lZdw>CRDh6Z?#SUbHEd`VD|5m%&(D47V zUHfqK-%Ws&gTMV$yM%b`-;#N)6hwotzteo1fr%skhs>pvc!;~iE(VCeI=5nC1p2Vq ziaVQ?>z(UZe;ZJF)AW7zW`^#pyfokaJR3y-r2bV@L+%_aHK*^a=AbV#H6!i%H$Xr26!1=wSo!``SaHl_q8BdVage=ZKKidj{>9+2T%v1{q zigek5Rft2e?sF?z1krBycFbXHFQ#987F+dhROF#Me_(^E#NhRBk7)rQSBrdOcNb<-0GBGG1h*f;tTggA zva%?}UCPcX(?+Drlfb$5{%krh5abumwzmUZLhJP`RE@x{W9OFqGkRwdckg!AGX_+8 z10DjV2<#3%!@H&gA%Ad`)EsmOEX|RDh(};0>@keEgb%Qxl%ge{(0a9?l14n5Wuu z#XN#7*A~Tfl{QQ94BcUQQliaL59Vjb+=$Il$I~x>@s{0w+a3NG3_<3Oy~?eAPY(eo z!-$dxwepsZ zrDIqj*UP+K4tppUY7vaeQkUKRIbhubm0)^zqUN6h*42L&cU|vHEvAGxu%b%mIdtf! zHA(gDs;R!Ej`Dl&V-{kre&ZGTqnXkT3NFzfM8{o*L2LANrg;EDi}c@eAyTAm0eBxl z2)s;ZGh&V^*Xer*4dI3Q7CO*#ly#+k2s0srCQJ3V3F<>@^{)#302k}*kbRB;sk*Kp zCtj}Kibg>himuoHNvDqtCob53%q$rYK;Y$qEMUprGPUP+MVP{Sw!g$B_2;_q0!;R| z-bAMZh0+%vM-B}%vs?Q6TcD_vZ!Z#?)O8YKn9O}OU27E3>^KFA%erQZ=NJoo9tL{r zT@P`x@N9pV+iXU3|AlgX+i{q2XOSa)lrl~W%YnupAU3W>gz`Ly|LjbsO*5=vR!_Ku zrVCuG7pE;AtBt`-HSz0;cT=hYl~jR6|Ehh zitEc&=x^D`$S{E0-YHT;i=fcTeT9Z#x%J-7L=dRt!tVBDATXo#E?~ij!I;a8>1pJ` z{8@4~$AXt7yLVzifB|U-QC{57>M`xbVCC3tA1Qn;(Yd6Yr$gB!m4V16+fjUwr?_+? zhAp;U!8wa&$P4yjHn4S7EHqA$bL}N;>A(o|)rZ2EUc+XC3~!w#O^_Kp)zr*F@qhOr{Hq2AjH$c9TvJ|u3#i~b`y=7pQ(6c+1+MI08CvNd+UxLjx;c=S#OE9!x=q}g{mcy)@`*TT=a2vPvb_s>T zRd(v4Kmn!1A_#0rTqntj^N#G|X$dQ~g}a+#|%t}BLq%HXF1kAW75=AuZ+LMN} zMu%*aLKIp{p3CObO_yegH&{fPV#*}(JFJc1%+Dv8(UcgJB0gMb3}%Qw%hZr!NP_sg z%(Xxi$q%!-k11H(Dt)b097`tmOx71E(3Z&kH=9uz6y&PX5O3(8An?e|mGb>%@P)bCiM)YW77_c9zzUOkvPw*rOI zR~s}g#4wb=`aO+S3al+9a$iv#oWx=wZiy??SbK_%;Y8L}YAz+tn#wwqnn8gl$*gyX z>O|?RuZw*H6I%C@QNRtcExQthmT4DK$6Da%+{0=m7DzF%#Y(_o5(vFQlnS0l+0`o5 zJo4kIzL{Sb=HAp%5gbz09@J4WFw)pJxW+cq+eTScmvR*SmDz{BVr|)p{War&0C75s zJz2`&U}J^7dC9#=Wf`CW z%R5L44CWJzWLE-2x> zKyF$mb&yr8$*M3|WE9hymZ42t!^fp9wbXhL9}H${LlPZ5%Jv9 zZ)oM_^BB5{Hbv&a+tcmOGQ}APOl^IR4RV8_nXS$*d1Lwk)A7VDQo)^{@y0Y}%?su@ zLw!ePMnx%dlz0Cj>(&V73EEGl29%F<)XL*1>)ao-A52+S>kk!?vb@xMa1%gi~UG~@v z1EaBUVO`r`Omv=Nli8Lpeu72EWZNOD1rJ{j-!@OT`0V?~Sq1Fw!e4eK4^re6Big@c`bjDaM6_vfl z3V$q5cy<0m-3?!j{D}@dX(2M8!^FM@VReb+WtaZYZxZ1M%Bw)P}O((cE|nU+BO*zkV!*YM!*R~+0~PG-(DAIEO~=veyz8jfAFSo44feGlcuyaz_MiXTY_bcTW(#n;jK zCBvX?;)J?N2tu30zoVlpL)tbM**<87#T&$D($39M<@WGyLPL0Sc!?Tjj&1=An;N_ z7Pt}o6S|<*Ny_}kpVB^0Gnnx7dFwlL00r)-3|_i-paTexzwF5cSkFJ*9ze^BRq8Y7 z7DD&S)Z+$eA>y(I)uNF1R4S5z!W_dIg)@6F3@_*zzv6&Onkm;` zRHI)qDcESY>8dvH%2#eWJF;JOOp*V3CUq2MU)^%)o}Dy=$TFIKQ$>R~r2a5n z3w)sKaS>vTbVJ#{Vx4lb!5%8eyMp+)s3mlVR#vii@whcc{fp>{t}9K5u? zIoDr~D;L-Mh>hXp^-kQRbDVX7eK@y?0w2!Z@uIv+cCM&Ubb;tyA`Gp@zbSSiT#vI7 z3?&MjJZAS~N)%d?-HeweeE_0g|LIp4+>n-H=pp0y@rhpZ)oks&i1sQERR>#x63sXd1>k5{RO=zq< z-5h%0@@4xvs(N@07|$I(wRQxBx_clU%NU8&cgxCFp$4y|65DPpXK%{w&rsy>LHih? zHGE?4TLO1ky$_I<8&NaJkg=Ls7G<|Rh`N;wwozVB?mSQUsK(%Js3+Nl)#n;etZ@)E zK7-TyI=SAa3=+~--bo?sqFC$O)ZWM)vG##9VyD-`+H3cI_FYaElD zMzfRTD%j4pCU7LuqvJ@v0Jc9h0n6R5ciYs2B8o5CG!27^tE>QUiS^?)sqnON9u3xP zSG@j8Jop3l&U8RqA!Sr(P zF$_2NW|o>V?Dd5(((lTV7Z;H)6$G=aAy1+cW)8J2A^%L5vo3`bTw(m_(3*%oq%Y-E_Md zZ)`@NtYC|2gDE37_EJa+z;PQ(*8>cS+71yKgE8BuSpTFL60v=YPJ0MN;spm%hp>vL&87jdgwsv_*6OU|xYiQ7rchHu5x{M{+M?^GS+>aop{! z;#i`%C(}io0fHFL`2F1=0;I88*K_6SpON7{jrm$XeHGLhMq$A+D|ncLP;t#qvd^XO zdZPjLDJHzZ&;iBY37vv~%gD=3#P^@HsiO1LMPh!(Q0V2I0BC}z;c%MD7Yh#1l7CT;K!**@#K#>jm z{jqN`51Qj)S>$hv9iEm&h75d%P0ARGzW&|nc44mFXO{G1+&t0FX96<16r%4wcjc=Olmo`U&^8!0|cj{dk>(wpBzK)3d4q< zFKEqrh`;?t?!Z&De0c690)=>nq5WLJp1JyZ*YnuPK&{E{_G@^EW8ggR*?zsm1f5ediMe8{1Gx?>qBSCn(`Gd}M0y3>hg zVQ~fN5nQAKL+>55*aP!(tsN~)S#7Apw(fY@zr9!ol|}qm+q7?yQ?&66}AHmwDEhrumE9o%7fnw}|Ee-T&Ig`XfV?S(JAQ z4dFb>rAsdWm4na)RQAS7hQh30Ls`#IFbB$Z({l`3Lz(3R zI0OwmWN#at$Rc;6*qDR69_H-mei+Ege)KkMFm`{oa{+Y(Q5w5LQ@NJ9r4&*+iuc-@ zz+Qq4>Joxn#4+}!pnv!h?f(=)oWqakGz&q*Gkl27JCPZM_J_arcw55I#=5{u`5 zwyp2r6{c@mII`3SWSgZ0EO4hqWV(xGywLiZ?cKyL!gpKRtc*he<8QpMyFewjtlrgo zFt;jywyN5(~#t73K^P$wsD&W zKs1GJuWAzpq#L~R*b`{&&@RO=SQJ*!eJbzZw04){&h+zQd3QLhDYYI-QUOw0B9Xer&-{bTGvo==9E?_uU3cDzJV%Haz?k3+k>h7TT|KNp}V@OruA}F+WjR> zDyEN;=D+@$eJFjhbm%5-P`<(1{W%F`Ey`i`5$e|pm^ldmu$?XJv*y|*j48fKU4dF`s&*__IC}SC z9Hvmc`=`qXI#A z3Yw0jPM$zfo`6;9&cL?#rF#Nh%h|w)7jpvq=sF6-Qph}wp<(QtLZdwjM64V^XDg0I zY|NP7$nem4x-?9JAQaI3f*JD6UtLw>2NY4>lX-HGq}rP~4V|4r1L2;`l)^>ekVl$Z zA7N91M!{#C@5n^g2<*h#2V0u~b5HvaJ0dI=f$hVYOd5fLpd*-{(-0m2eUz@+(h0W= z1BYy^&coTbh>Y017WH$w^@H-y@8YO5n~IOsJ)Cv5LBT_H4`3=b7<8mAV%7yf=s?}C zm`gQC+d-^*2twe)bf>ef$x!7{x?O~Z@Iksh8>Jb_dW??UKDOqoTkjRrhmO#FN$3Z7 zfR5d_%P}DId5Rz>jxuk-4Ar3M(K+*C$q@`E9-R9D^M`-{0&g$K0$*HyFq0)wamOaU z$kt+Yagfs+GluNPMcV8gW%+=}c~xz~82w__^OjO;dk7#nOxqnc^~nc4I`?D4flV-n z+dqosyn^+S1`QXWYEkP}Y%nKL_VBgY4hLiKE?fwVg`Iy}HWYKP*}cX!q0Z;Mm!s2N zY{12xM2-ymm_td&TQ2niSz^s1x>P zZ2{*M;O;EygqI#`T`Y2KyZ?SSk!#!C>DF!dn9bls8|>5_0>UP5E%IwS%YC)TukFBa z>z1OH`P_C#QOn$^b{&zAY@ZVw-hITjGdO&1^HFSCN!0ffb1nrWwqM95&H}d{mIp}o zXMSukb|ameE?nJ-ENolggBq>lS${5KwS4u%MeLxu)U9I}_azd9irsrLQ`+!6wQ@@b zIZSpuii(+-^&4LGc21#UrZBWHW{(S|5VFkf-ax~~MAxz_7H`L~@6b$Umpz`6crm$8 z`&eq2Da4Vky%OvIiE6mBZ%$XZ-5j`oZ34V~y?I?_X9-W`~34qR1X=ecp zJpbGN3RAI)g);A5UXOYhNBOSm2^7D#pF|TPddz^_X8VbjW>SDN3BzKT{FC(td*_Q) zRTiH3>UNneT3`3tnf0#tuvhnMbOi{H`Tc+nrt}mZ-!=Im9i9+}qLCj}v6jCIV03U75>y{`VM_*O{$FovO1 z>b-~4VYBWoiX=|&I;!$@FB-X)dnoNG?P2|`oTL4yk!#oODE;Ka^Fvg4wh->J2tS)t z@R?_p7R*ra8s9eRYVspK@J;Y*spJrZo&i!?^`fZ+cqq%(# z&fGT7dQEwWF0(Kao^d#%x>0bQd8M*ZbeH+O8Yq~lSs_^jg}0bZ9Dk&Mv|V9lk0fFW zcz^jS>bDV;TwXp>=r-%k<=!sb_);{xw!Dwfto^~KJ?PSuqtO5~m9dVc`08iJdRy#z zu|Ga`PkRqlJVAD5GYVjZ#{V5vLxHUE77a;&RUXucFWAdw;d){Xh0RJ&YQ!G0RD}`HbC~&AEi*n^2*vKxS9%Xo=2t7r%EI4+pjh*sgi> z`#6fNDL&ELY9Go%35t8$Eug9p#n}x>z{v6Js;VqXaeh-8Fm?`Z#u_v-lSDkJj>I;4 zmYZ>ihR@yYCR$Un;^;b4un(Xe?XIMve8rx1AF>Z2^C0gDjmXd#y3+1VDhpb4|6?Dp z8=kAry2&GB2b$MnbZBVY%ig^dj>g7WO=-a37}<5eXwc}mWn&^RJPv8d9kd~nay_}0 z#x_7sZpb7WA$PNzAR{6}@EY)u|cv|?WgRcWHZ$b_$GFLd@W1J z;_t%ZoElp{qM~E%C5-;UV(AL|?o~8zx9k>kEcG^h30p)|TXVqH`kpsuNig;zJ3`@y zXhtmi{|Xx|2u9B0zlC*aiWV>Fyu>b(96mwQ ze>sYbhi_2NPH<#Eyn~uz1{X&JyE~r|Scb$Y)TQT8+nD$pb?G)R8WcaHcAQago-Y=7 zwwU}AsjcQvaDU!j0thj1L zGcDeQ&JLP^8DS;xXCpN|kqbJ&%vK^jIp#OoXYL~LgL0FB*|;*t(ZWxhIo#$xjPj+= zZosb|Oy`7A7|bXBEZqe!U*X=7j+OKQbA4^VZ1CndQ(6K(Os%>a9D3`}xpm^k<;l6d z=q#qXHPEMx!>|bET8GkULAj~0xc9H`0hUv_iN4{xrVBOE7uq?7YA1jEG6%5d-OYw} z;*8uW_)K4d8UAzF&Qv^qw(iINiXZq%ev>{c+JqiZoX)WZ_M?!%TlZBk3hdFGk${i4 zEXutEw&|<>q{F3E)x@}nXtOuHf%23I#^rPHyHaUp>9a{%qLPoJX&Yg<9hk!&^yhi# z$0hX`6mGPY8Eal74zLShx$<5mu&pNh2?Th#?yN`czcqBIDO zw-y}$@!Tuzt;fid$eWEz6Jn>0%|QdGk^`tt7cLLC|3r)j70pWvx-RlK zHS@l}E2!~opEki%?9CG8o^Fb^Z$#{Zrn2*#D`Bym%2a+J!=&qXgk)2P8hP#}_Gz#k zWzgAluM7JuWHq_b)Ey~Rhqxd2Mu;$7%Fak%LT;j{xag2|C9ni{TNs2%fDH+7n;l*? zQtQ9L4sLITgc#%Bc7~YZw;kDUo`+ola3m`Nos#?6S2dk4jF7G?fZbg-Q=>EU)zh*aV11|m#f~$=3wXc-@)N#gb zX4YTDKnFT3n+(dZIsgtdLwt6BgqDK~^ss_kipx4z*pF+&&OVCM@ z3Z1#gV|(r@mq^mY$)VHNN})(~q}6XlqPNPS&*x+FjvPi#-jJCF?uSA&y&3DFetOn$CGk8UTiN7iVE^T$us^UVy>v7V_gygR(e%?5=;TL zf-d9*ax0aU+OH1H4}t;dU$u2m^>m?x*b11P>0Gr)kgCNOz$BA@h*WMB3K!}kDTMpl zho^X{n%BjW()&tPdK!b8Ns`1bfgXy2rQmT-cL;s;Nm0rFlI=@>*ux}1>iUPvjSj2% zM}Wtoll45g*w`6b2u1y)ehOkpSv=UiOpa-oD%K-b3`GC>Di>r(ZMl&Z3Wxt_Nq$3R zrB&pYO5fWE?Hk|N~N2)HhGLxhvjR1!tz&3b?7iC93BgML9M07 z!LMpkmF~UtBmZYN?KeNatyACm-9ha zjbP#*B|Ke{>x#|;Z444S=Ms2>9OsHMbN#9RMoD1-f5|f>Wvu8T80D`wK?l}{>wbQw zw1LIVa*2DE+`004U;u~>-3Lq^r3%%W3O)x1bP40^z9|9|JhMMoMi$1G=^%M1 za;VU1?IPuw!^-tIG=>w)vJ;q!4ksyncS*hu7^L+b4?lD;zSElnF9{m8p4H60Pf{DT)bv zj*K+K-yK_{;{ZejlW3hRWktA*4n*UYF#7WNhmn0Ci{a8O1u!HR)cYgze{C!xd9*XGt!MG_9U^lO_L&c7{p{x>?3 zDm5j!%4l6LC1VBaDC^h@8uvsx)ZTcJSfURB*r1c^;+WlRZj@Icsu)7aVU-5<>VZa>asL4Fs94y8|e~t*`Fi@ zI%G(foe-IBzZj=^rySs(^5DTxOxAx?@|1wGDiOy;dglsogXDk`6->NwMP$LrrqtjL z+F_XX`&pzRs{J4yrNodG`AB4vT>Bj@#fw7;?bmt$()egp#o)Nu^_}_8xqpjxIetzO z5@XupxpV?t@1o9WBU#!h#QiTFL)IM;UDuIHXD-Xy1M=)_1xAr&C_FEb=TU_!H?P&j zF=UJRawYn=4UA!;bF-9y4N_E=fc7}cy5+XDHU@_3U_})_*m?*K)xRLw4u~;dfUMLV zX-z`NS^ZDgj)5@2)s=QeBrdJ7W4J>qwjK_|Zy-!+-96kN3awv*h%vyB%(^W+QLa>; zEDwc{xbpCDGhFu6K%7}n(DsI?m0-!olfui!WY71*t4~GG<_LsHp7(}l$OX?KKNxdfgCba z=EWWs51xr-@1gz%TyH+WM}j{EsxmvGVY!2-;!rQqrWQuWrpF{4F# zRC#q^6}wr`G;f_B*g++9T_{*dIo$xwZk6Qq$N+gM5XqeaCo+owCHij=M!N7UXW9FM zt~40Wv3rIaVGIjN=M0APYcqnO?0N;R;uN3aUISvAqQq*R9Gsu$RhJ7x0{kkvQUC`E zK$MpsjC(xIBrE@Su+!Y%xcn-(TC8TUt4+1Hzy!p>exQC0FeD9*L&+tfOc?wLNpTu_ zF#JO#>M4KW3&}@qoI6l;%f6L9h5~z&wmn==MtQ&OYkFjTs={4HG-|1lhO}!BWtH2`ThVw$yTu;VBv>&wn`xh(KwpPlwx~ z3G+{YgBnfS+WZf8qf3c%>Hh#ANelAgf3HY0O#VX{iv@87(29Ym1{i_CWB>5cPe3xJFwTV!L4MWXfL_=i z3>=FLDTd?0Ohhcx41Y!*)gPd0_z{x222wNeCgeGvI=ucIAgt-!Tu&r_eg^130^$q! zVi-Av5;^u);6UnWb-}l+oo?3@dv{*_lKQsCb|`nf7Kc6>1Vvc#SrGQ5pgaUCchb(J ziq5IQA;kXNW;w!Lx~&~4s!B28LS*RsqqNr{7Y@IWOr(C#=X3_3ZaYxDt@#{D_x{lt zTDt?OeJmJsLR;EiqK;B)8P|zyNAolIGYJQF8SnRzKevlPTVINKUD0}A068fZ)Y*J- zmDMA2Sp)ZE$Yt#zXLYrkkm|S-aPT!DURWf=ZN5wjSMpX5nycSe3K5f&Dp)A8@p0H9 zjqRs-Prv1_QYSaS&+Ku=nZ%QarGyCY1Y!QH^V0jg;W-6qPt> zp`;J*&5<676R&oj)T1;ZoXa=3){9w}iYfYHwizI3^E+NOK(%p&v?Y$nHE#{d z6;&zw?j(IY%k;(dVwe0Iuno@GmbJ`|Wtj20yf7|An9j7& z<~8!3%SCqJ3?vIwd6zt*1mNtD{q42eCkJj9?z_8$Gp0BYj1iAv!v8{GGzw(HgdATK zN(Xp5Sf<6?N>=5jJPOCJc1=I@8dP3`gQO?Pr#o=T>ltlzMQ{n!DA5p*`=3A*8jH<8 z;O@-Gu2BCHj;=pUN91*gO9;NRfK+GP*8PEp)$auf&~jPnSPLe$6c1F|+~9x3=hZEkgZt-S__ zz~O_ZfeBbi4jniQh=HwwCG0*8fSV5jwWR;opLd|_z4>2R_P+K2gxUN;`pt?z0KO9V zw1k1MITd7+O-K~J$7+5Jh|0l9%G2hnKzce5xTgMz^Re|`xd-M>*sFz6$I2Ygp93+^ z%Bi?C1jQC<}wTgVB8V25%<`^f}WUv z#TJqlPpd&1<*IZq^~C%BbkrZKHIMcX13)Q$mr-GOAd=e^)0 zHc}@&e|Otj1o=5y)q7sdjdPlmXCCCIOl6;-cRZNDK0)tIByD~DULj=A1p!HX0m+8& zDcE}jher1YR--7El>c8~MXjdtCiEQG3xn|&p_swW0N72<>bg8=&kS z@()1x8jshI--pAW#?gG-Ka{&-^Br?{W%4uGPFKH!>G_A3t=GwYQ;3Dw%M`GCLMi*| zf0L8+D5mOq()f?3_G|1C?V0MuM@hk1p_pR4SF~fibojlru_|mx=e;jFb$;>ibV;zG z^6n6*UxB#JYe^#XxS-O5Na+eME+F8Z!UL(|gR{Da5zV(-97R;$2cvW57ZFEDd0*kA zIsevZzvMEae%D~X!%;r&r=#36@G;0P499$ccb8HOz)Xkz4Cl0JQk^OLf0A{)@2LJf z*g<8Iwgcf(*TV<|kbCoA4zrVRA8=5=1%_+DfpCYX0)a{-AI2+y=K$}=k$eGUoJISX zjOWAVYJ^EI!N&QtV80&6NBK&SA_7g;>IK0OEUJSgAHmauBk(vrf>#AcK+t3aw+2Um zb;+L+96_WwdHrR<5d-nQmudAK;Zcz5#b!IO{)+gv^fI zj7{H-Od`O{1ODB$+NN=Fk3miip8Z`O%6&sXc_ngId;s9Fap?>X2!WN+-Y*7Siasxn z&ia|!P=-DyKIVmjL(hmqO&UuA0Xrzf*L@(;R0-{+!+cD!_m6L#GU=`H?cw#$#GBTR z6-E{Bi7$o4GNSn28ZA?rcr3DF$i34z9ht$Z0s6AM0a?A=wwAxY4%p6ZyZ6_*EQos` zhB>c47e&!tiz@?%H3&`d1k6*M1gw(qW~O74^3R2ED8< zuV`SL#DV%+*n5W&9@N*wmWI1fcaLoe^P#>viXu)_mpF$JQZXX&a-Rg7)&9=7=AAfh z2V(5<^;lc<2GJ^v9Vy7)ux$rAK>es)c%{~F-36C=+|j9Q7y3Fq!O3My-^7;G>J7&J zxOxp(IySF3hO-qwS|-Cd`XV>Mme^e5HjPWUOz)7c6o(=`ejSL;LWq;H{$#}H^kp6d%8&A)a}`tT;afm)BUbV7W>|NdXdRc+-V%^!7~Sb0 zTTcK$>lt$1S!c0e87`Xk1#d8)d(*z1AwGVd&mBhEut!vyDZRIH zhWpxEu`zQ~Jkf?FNl9Hh16EA|{S%Ga+a!zY->Czy{vl~oRfvGW_(oh40cOJPYiqSl zjf1y`<{Y|{c~@WoWmVPp2L@V|2tetz7+59+&-~MbM}#}268@lO#{~Cj#H~y_l0-H#| z;)4D7=q845gBO6BOFZ0a6w#)`w;R@*ddO@%^ALSso=M6tLZx|I)=Nanzw7!J>(Gfc zdjg}?F4R{&|Lm;s4k>TvZIR%T1v_#B=M`7mH-j#6-^-NbGt$hP_uJL7MGr^_>wK;> ztWKC$3}P=3n~-j?a=r28zNRQD?QYgioMK6)EAm}Gbz9{Wt z4V&++Qg>GK+tR!W*BWVlN7@P%PEl30R}f%XRwN;mbOH3(%O;&gsni?R7~P5D@+xGt zaV55*er=~l!DaqX^Kxm->EMfX8FOb!!8Gry-3lUUy+Jaz!3N*c-&eAojRuKOAH0@y zXyU;7JdllnnCDn5t<{I5NRdE(uh85ATV8BIYx#}9o?1P2!w*p(50pD90>KqGO4~uh z462kW7l6=U`3M^S1&b0cUDJHKq$6{XK||m&xe*pO@h9U}PudC#xw2^kVe!dFC27-p zz~-YR-`cA`W^U9V>J07&o4HhG3%y*2eCiGFXFU@q;2-9#R-HL?COeE$uVVcSB{8uIBBq0m1q|wHHXyVsL5#i$1axUX%J7K^Re0NFvid)#e-FwnmniDqdr=HTy?b>j zeU%YCAl|)?LL>HfcJO^G%&i2m1MlZ!ZBg&uK0xy$BQmKU<5cM^Q5e5E&3^`(SQVi( zX93$7j1Q&RE!+qTrMWtY;h{88O4VU#C`}(W-C#s0&0|0$Qj`d#IWagt5=wI!s1yx| zhSEGRuy?C6l;(j_RU1$slx8pBMgt6m(i|mC9fYz_n%@EUYCMM0d>2H$#?fRici@z> zOon8ghs_v}C#1b2%`i%OVi?3p32G{oAWBPPF(iK5^@43h($!{ z=^`X~jTZ!U5hx0S3Q!c(mUqYyN!hZK2hYSZn)(aUwf+E!>c^mn43MUYfI4yu%8FxA zgUwe1cSPOB?}Uw+4%pVU&Vr3L*_6%(q>ix0!?BrAP0c#IyuyH(XW}Y_V_{w@#bGcEra8o+X}b`PvoDiA zh{a-kFG^c)l}x=@tn|W1>*vClL%Bq1Hir(mfAS~5n$0PBB>BzN=V{M+0A0I6vcLZf zpOH=^eS2{H8Axqeu0!<|JZrbA?63f|UyR4A!RCi@?W%9s*hf+*ahQ0za)dOJ`N6mG zDrsz)*cI|n@JW)z41hipe7K}}i=YR_+rtq0BH_j$gnINFpOlpDAoAn|7wHB^-}K!G z#8MTWMMd=qDXk2cALAh5pvCw@+%F~9I{+Nvo*Y~UdbVyr0Kb9AGv3KkhZt1&HQipp zDe}|$>A@+I)A}o=#5MqNNPI=GCw@fSHMj)qfcWw-gd7hW2L@M3T!u711r34EXI}E< z5!q&^LIz4%LC7!B6qMv`LOdNA`S7N^STtQB`O$l zepjFhixvUrcaJO|3pf8kWFKg-IbP5~E5#^HRId|VFGhEy{B17}o=Q761eEg+;J--`XWuadQa+ysD?pImxw3gygUrLSbq12TS!(|` z6G?q7IjlcH3WtlmI5>1BR(M^Go3RGO^ zQ+UW7bC^6w#6Dm?nO{joL%u@C4CEIfXzKI(-|*fC9LQ&fH%GdWcc0ZDBT%S$M3CGM zSeuEY1Ud*4XwbcUb(lbBIhpS=V+{y_J}OM0vmMSCgb6g{cD^7?pfTt3-eCfbyP%H_ z6X+U_Xm}w=zCr^1UYJ0oQ@Z}|@K|O$mFnLE1r-Pks_%fZyH_1@lRx8z^j1r^+j83L zWo4eR8XV*dbm-F~{2i2T-j^ES3~Z#&WIq)CvjE0aR|PR#-v1|pp^OACTrEbh(3SE; z%us@P?i`#SVVg%FS@&m1^z*;S&19!V0k>B=UaL_C$V~pP*srN%N z``chjY8>^Y?})R}kCby$a+Z{lZEl4wbyazE?=3)=MVC3fT98_}=Tl%eKPi9gle!vz zmBLcL%N|TpLH75P)L6kh?TEeg$?n=p;Dm+(Tp{IDY>rv1i9lIlHE%v z)fT4)mR8X{OJN5-6N}OLaw@`pmlSLcqaSpFs}B-G^wYt@s95H1n7`xxlqlwi8RGsp z9J*rQEqR5d&X`9dy*m_h$J{5{F*;=ah=k#=A(zYtqEqKinHzA6OPqJh92;$nJ7%^; zt}Jmu*UUYT6f1l&Oc$n=07-6)E+|Wvb4nD({5U_2ZX)&M+$oMCzMKz77l(Uuj*=F2 zg_HiAw?+FU9-XEQaN2>QDu)il_4l)GKJAAbAhlbhMP`Ei%3dA{oNGe?NqNBLLn0Uw zF4*{Az~sFp?n7x@BCQqkPyW)b^){GRK8Rc#w@w3gDK1yb7zvzx8nj##uBHUsiESf2 zM*60R$-7z$W~(7g*VXO^^k6QW@Di;4L20A`l0{0rL7qJVa27Ll``Yc3=&3rq?17sy zHk{XmhZBFZ^trX$r>~Z_fDnmY+_m10G%+2;C@47)7|b&b)Du*{;oNJl1bfEA*qpx@ zZ;#IPDj>ZQRtfZHfEt~4yQ}S>xNKg5-S$x1)~*GCR2E#J>Ap~G5mc4R>*xR~@3hSW z*%MDJQTxq8om8TZ2j%5am4lJ7YJ?JKY3HJ6(GXuqj?`_ekf>V4>O)@o^ZDXCF%*jPs#!wK4W@H1N4f; z(qrE`7KyTg7TQBR&b!r-;?6VL9p(#)n5q>%VMe>k60v^VjC~try{Zs&>xdZ( zK;DS9j-1ipSSj^N7o9i*sGkH>6nM;xo>$T;`Ecu~nK&QiVQt3E6i4jd8pIUzS34QW zeAxWG1PoGrj83b=EL^Tt*VfwJs$8B=lw0ay^5w+$Bu~?XqUFF}2kj9MGqG!jO7>?^ zDy&~GZ2*pB^7>0NTcsi!5+H;Uq$U^24Rt7)%jCh4eqDV8g8Vn^j75%>X+8BC;G5D? zDPiQ9Ry%@OA@NjT%@7Q~;(K#=L3E5ZNel9*@PcR*Ywc?@aP(sAD^gN;9A1rmS@Np~ z^JtQ{#OFdnN`52F4vKzH8}Njh;8;w_on|yh154g814p7uS~iOBxDakSTo~y>w$aC^3)0-O-|EP-lJ0*C zkV29u)Y+0r3;!V7zJAzZ+U<>O9yn2q5a_-Jw}=m%%|& z7lx7yzAn@e%`fehW-BwHz{u*>(CTi=&rsDw>MdD?3)RkuOO&!OXCHo%%O&A&*2U5s?jQQ;py6}HBqynB4d zSVPP^4sT)|iaW>uH;SUZ@dcQoLkZXTiab6GobZev6PYVWY)_~Vmnf=T9&XGvQ}f?4X1FRYCw zxj*(t*8G(3LTjN_&I}H^9O^CAIAencVk~mfE z4J?rxC3_eox^K6Y`p)B+Om4uVN&}Sk-Twr;MI#R)X(}9Adb>t@rsf``0jf~U=DT;a zV>JB!AX%ishOEE$N2ktBz^6&E>T%u<1n)=%;@42yNWS#Aphb8}aJvI$;XR|v$!x=) zME8aoh>$w=(qb>e{Mw5XuJld?0_)k9&AJ&NN#cl%r`f0^}_d9kWaWokEWr-X5WxTh=bEI zGq=Ia^WbB_=9^$O^Ik7q2<1up2lhKy5=Qu$AomT%o8VAd)&iIT{+J+!o8NatFx2=C z?3Km{)B7rEpfHp$yiW+ukC@$$#JyC2sL_2sh)@mZCU*@|9~zLFXxgi^{Q0=y`Y9S3 z<$L0W50=6v`H>)!0m=;W--7tjcr?esbz*VU82^qu%Nq!dV6pDL%@i^AYB~8E?)^m9 z6lO(HTif8uI+PcxUja-nUFB7sTJHcF>M6;#8or|3A4XnuxBA$%r-Cdha+3+IN{U*J z46;Hg`wU|(%22a znZJGs=&33^mX6b2wTpeQgre1oTlRhDku$M)&Cs%%?&x)W#^&&?_4h|EAX-;QJAciZ zYxUo+>l13b*6o9aV`qO7G{0Jd=w^~n=?WI>86ID|Z?2~gnj&Ts$hPtWNcev{$Tom7 z;=N6}Cqj5O`tPix2Rir`a&w92d9^?w zKq9`B4aOYZ4M+sg%&~D2C@O)k?n{3Nd*qQWg>;4e0nk4wzeBQo*WfxoQq~}s|3lPh*pv4I=i*9Ols}y85o?GYPp*le_*<+-6h)6H zkY1!ji9^ccz|`V6aZI^uWUl0(@@P=577{(GTnK&?`+>M0fYR^|7C4IRi{xaXp~J|r zpnwBlb{zR5(zW>l4#m&>+VrU|cdQQ8m4oldP%u zg_P}F9BK#B+%&g?foGj!S!=vb(I0wI(Mn(us?6VITjxvVWFTG>>=SH)sex-l5UvBZ z4MV5`m`D~i5F!1aC>1^h3DJK_Xl6w2cco1>vcfYg^e+aoS1>N|lYfM!P=*ngTo1v7 z6vQ)geLOV6SO8%C?b2#QG&8ckvS#ZvZ2olMyewQTJ-IJXd1gxlmrdP6%DrH~`s#FP1imfuj08Ftj#7-FFMkXkDaaYd;m0-D)pHu`tesxc+_wLqmdJf+D@bFr(qcNWKKf zDE+0g+N43gkOr9mtJZD}&!W?tR{PqT;+9#(oDQ@XpoHBjeTbucZawNhIRM6>tQ_OO z@u9ps0y7ljj8%bnI80Zc7YFV6Ugpn&XGybboOZoJYaWb0_xj$&00%Mqvp7XQNp6rs zfhYQ2(w-vXH2H*_Fo@Fq6N#6j%gZdV;5x`YJS|FgjA%KOp^z6kG~o(tei<`7CKws|iii0`Qg4e$Iw?r~GbdxajbOSPI_OPS~j~gi|i; zm$1`fG--ELGjx5EPs2VM9NU8wG1{nOSNdlmM%ojtJwbXn411OGA!&6~D7j!(xg1ZB zdCx?~tmnw}THw&Uw;XYGHJ>kiDdVNAmrbe^l<>iqfYHhi#!FK_*IT64^DsHt`@wj1 zLy=_8?*ZyH;2`zF!`ZP*$>Js6S7CE4gjxfy1A?>3K;%5GgDLCRYr!|r+ruInsJ&@2+on9n2rAy*j5f@3hKBB zICRjczfbzpLSQx)%MypJ^#vQ!@t|DoH!z$JgLD%>Hk3C@dlk`a7~vjurfUxdYidwb`BR~qtGyMX z91L%i4J4A)_aC6R6`073)}w(_ifK<-E&ZV`I#!}ChJDj0Zb#Ih3Z(%NTd|gh5k=*l ztMy_28o^Fn_m_?pGijem6UEUm>r8YyaUB#_u5@z_dXfe1nlA^Yu@In~7Xmd5b(OgF z7%swU8&d#E?VUdm{7X*C7Xkkzr9XnidMgI~qK6<6a{5N}3fku>uMgG33( zIn`bPrb#ri=?k;kq<)qaAe2D#5?Pe(Fe2-4q`e}lDxS20wCatNn9xpEAIgj zs8mZxA2r?%>S8kT5+ivYNJ#aw36@DaX(vyqV0$@k5yuizZkz*Wq3O_{8(kAjBmc7KnqX%1*qw}e4*xCU>R(Fw;VExwyL{gPaeCcaAo2cJpIjq|0 zlIH1oHTkPF0nD$Aq+2bZ1&?7dcC9_6Oq>+DluI$+o({UJUdcCak}^Z*RMO9CgNb$P zzmlY_ZFQ#zefiz*N#>Nf1X|z4b~Hkq(NB_Rcw@;_%U=Iwpq1f@K2tCHk{ZoJ5LGC) z)rG@ISml4DG-N2jluwa5o`Yf8o(1fnGp@2qXg9!)RS+&=UkGgOfuz*@_R@bu1F<0i zjER$m^F^`;5FEj=5bQl?v`E9|cLy@Ba8+A70Qt!1@EwD#@Dc44Y=vI^JqdSZG*Ob3 zn8+(YI~KxI6m0?ax2UnQ;43%L^|-+nB<1)Wu)!gQk zT+T+_H1K@0Wewq@DRRrY?p)p~*G7K`$B}1B^L;t`ViCOOYQ9~?RXQ;r#4A5v(zjlm zvL(xJ_ynFT66LJksVXYo_>9+I{liu%Gj zlHeSM`NAMw7zI&ZSP~o9yt3q{)j`(2l{`3tYU@gQZR#v({6%W`(hLo_*xnHt0ykL? zj-UpsB%@w(=Lm{>*)~ZVzeEMSZ2uQoah4hR-VqSlztqRJcML;(KOc|Gl1ss7 z##$n_J*1v?7~zMjpB5n~%s=-xTnJOEmMYO?88=`{7E8NQgLjq=4Y^d3k(4+bN$tu{ z&2E(BTzU4)Mp?p@FRlS4*;e2Vi$Q6s75ljEkO(rAXXQgPhryDpJWL8+1{E1rPK<0@ zIK9eN6pjWBxm6w?X*Qf#g$Y2h8>V3zdf z_Vs;1sPH+c*8*QKYW&hVKOt1P-@3*{sqsq7`W}wG}MahgDTi34DQJ96Uk*Am$pD`Z#X;_-f%(|6q2{Y`9EyWn; zjEVHNz;}!_o*v#_31__V>Ggg?nB(T~8o)P6;Y@d0bdehreTN@{_RC@Lz`bq-C!d zPM}|{&gc3lzAoo&7o^8p4EAU+HzTa|6wpz#nCcU2qX|1b6S#aPBYl1yaKb`+uQ?XV zJYR=>TMyfOV9g;hhJoazYq83mXKZYkNfyBrpTQo(8O?_oqX%+nZI<`}oIz$XL-HRu zuxHs&y7iKeNT=jVrr)=cIIhNR((|{nnE6<$cfpBY|H_}>{~6Bbizb^pdvtkU=|FWL z|LhX(O&o$fyo7iX3*z~~{uu+&$Cprh-hssPOY|!+n0SDBbaP2 zmx0tQCj36&g&>wBT6)B9h23#q$WWfGu<}K3p4A&@PJnwf1X_9~wp3_uHmOGdw^ABUz!xp{j> zK@d#4s>|`L2pM|1T&fb~g(##>Dj`gYUKv4gG5YT)iptT;B?pu!AxO7MD?s3cBt1Sd zS0YLuF2$3BL}lp%BptaAgfM-8^mRH|K$_l38WJTmBu)>P92){NdHQQ9*4-BEG?V7Vc|UW_ZPQ3LoRjvmi&ux{PaBi%FEZsGP9Nzf zEgN(UaMGGDl-k*pY`(M@^}6R|gNw3>zQ!HblI`Y-fm%Up*Q=(rab_AmZNGcelARYa zB|>z5_L_u*UMPt*Qh#bB+bo(I<%x~cazJtqtDiYjRNbQp zeVTY9dl|!<+?ed@L&o?%swM5EOZs0nD0QZno^(5}Y=!FYq4iN~NU!=AGnW{GuGSP;|ZS+;4603kLhe3-|DuT+}gF>BJt5FgQp8pUWryBpx{O{SI z8yh?cQ!*$qa@!3yxAxEi-)QdI?#xlTPz=u2+gM9>xj-K_E>cIz>>{P0Gy~N(YL1y? zE9vUEp;Pv$|L&UT>@P{Oi;np)`>UxXNw(GDD>}+UM{%hC?rgfyT$Uu89S3fwxiHaT zbo#-BqKI0uy&c1ON{A?KswJB*9A(QoIIp+r{-#>l_7YtPuNWxWy=agQm7$@jW*f!5 zIyA4@SIp2z>i1XJT~1YVzCbZ~vh9h--{r*PPdrg|w(XheDwP9@m6d#${+8$gO=^NE=aBWvV#^&KX0NhQ(exRPjQ^-Gl|R7F|RmfE;UmmBUtDa zb2=QZP&QyYGnByF$a<@4zvVJDr~_O-%xk;FP^6KY`eUX+J6!D!tWjHWew1ZfGe2bF z4H;+!q6VbQiJo~v>V%GFq#Ca~I~QBvvn_H~XA$7Y<;^k)l-x97DCs!_Q(Tpp@F?1lt8C&SvPA?lQOcu2mY|wY`;ngw~ODzT3 zQ^Vr6ctNeY>oMiyNUKUm{`%kFi`f?P59^PA6(2fy^zIzO|aDQPri4H$x8~ zm0Z05y@7}G>6%xvX{R$)3frto+uJ5JYTNemey#(F&A(|0QKJ-Y1iuy{RyMn>n~n`x zm<`n9BDT0G5^L47K3^?cEtN3ls-&i1u7u5SNvEU82U=(pqgB3L(?PCV){k5V%Fa$| z{EePjnshQMdU|D80=q%C=(i^(?sI63)drV#TG~`ZKp4;o(vo%Wgd{pq`jm@5w(*s2 zfuY(sJKZYT#Nnv8T=8qJ)TKTgx91d}EV-6yH6AjeW0GLad2^Y$tT6R2#%(oT9p0-K zU$a$BamxmMKrj|T?ZVp|$#xSZ7}OW`FU9%F%+N__J33>OvPk^d;`~C_;86{1?(|gS zms&P0O$)UeYCvjDm8Lsui;^SS_E$txo*)!plk3UGa&KD_O!9#DHwn&mY^;tNK|{KgG*;7<#GZS0 zx=7a=QN~Hv87#Jm9nIP2{Cqy^tf~nn+l-ByMYVmtQfED5R<%rPYR&Y??2>Z)yPT?Q zTK$&`b 65535: raise ValueError('An owned client fixture endpoint must be loopback HTTP') auth = ['-Dminecraft.api.session.host='+session, '-Dminecraft.api.services.host='+session] + # The native offline mode avoids calling Realms with a disposable token. + # A deliberately owned session endpoint retains the native auth path. + offline = ['--offlineDeveloperMode'] if session is None else [] return [str(java),'-Dfile.encoding=UTF-8','-Dstdout.encoding=UTF-8','-Dstderr.encoding=UTF-8', - '-Xmx2G',*jvm,*auth,*extra,forge['mainClass'],*game] + '-Xmx2G',*jvm,*auth,*extra,forge['mainClass'],*game,*offline] diff --git a/tools/native120_client_logs.py b/tools/native120_client_logs.py index 5caf86d..0ee865f 100644 --- a/tools/native120_client_logs.py +++ b/tools/native120_client_logs.py @@ -1,18 +1,18 @@ -"""Explicit Forge 52 client diagnostics, separate from strict server/error audits.""" +"""Explicit Forge 61 client diagnostics, separate from strict server/error audits.""" from pathlib import Path import gzip,hashlib,json,re,tempfile from native120_logs import audit as strict_audit def validated_baseline(policy_path=None,proof_path=None): project=Path(__file__).resolve().parents[1] - policy_path=Path(policy_path) if policy_path else Path(__file__).with_name('native121_client_diagnostics.json') - proof_path=Path(proof_path) if proof_path else project/'.gradle/native121-client-sound-control-01/proof.json' + policy_path=Path(policy_path) if policy_path else Path(__file__).with_name('native12111_client_diagnostics.json') + proof_path=Path(proof_path) if proof_path else project/'.gradle/native12111-client-sound-control-01/proof.json' if not policy_path.is_file() or not proof_path.is_file():raise RuntimeError('Missing native client diagnostic qualification') policy=json.loads(policy_path.read_text(encoding='utf-8')) raw=proof_path.read_bytes() if hashlib.sha256(raw).hexdigest().upper()!=policy.get('proof_sha256'):raise RuntimeError('Changed native client diagnostic qualification') proof=json.loads(raw) - if tuple(policy.get(k) for k in ('minecraft','forge','platform'))!=('1.21.1','52.1.20','Windows amd64') or policy.get('qualified') is not True:raise RuntimeError('Wrong native client diagnostic target') + if tuple(policy.get(k) for k in ('minecraft','forge','platform'))!=('1.21.11','61.2.4','Windows amd64') or policy.get('qualified') is not True:raise RuntimeError('Wrong native client diagnostic target') if any(proof.get(k)!=policy.get(k) for k in ('minecraft','forge','platform','events','control_sha256')):raise RuntimeError('Native client proof identity mismatch') if proof.get('qualified') is not True or proof.get('no_candidate') is not True or proof.get('no_world') is not True or type(proof.get('native_client_exit')) is not int or proof.get('native_client_exit')!=0 or proof.get('original_strict_audit_passed') is not False:raise RuntimeError('Incomplete independent native client control') for group in ('critical_files','raw_control_logs'): @@ -49,7 +49,7 @@ def audit_client(path,**options): # Preserve original bytes and their strict failure. Only a derived audit # view excludes these proved native WARN records; every generic error and # referenced missing-resource check still runs unchanged on that view. - with tempfile.TemporaryDirectory(prefix='native121-diagnostic-audit-') as directory: + with tempfile.TemporaryDirectory(prefix='native12111-diagnostic-audit-') as directory: view=Path(directory)/'client.log';view.write_text(residual,encoding='utf-8',newline='') try:result=strict_audit(view,**options) except RuntimeError as error:raise RuntimeError(str(path)+'\n'+str(error)) from error diff --git a/tools/native120_logs.py b/tools/native120_logs.py index b9bc4e0..191c609 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 52 qualification') + raise RuntimeError('Legacy login-retry classification is not Forge 61 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.1' + result['target_minecraft']='1.21.11' result['legacy_native_controls_used']=False return result diff --git a/tools/native120_persistence.py b/tools/native120_persistence.py index cfcb264..2b9144f 100644 --- a/tools/native120_persistence.py +++ b/tools/native120_persistence.py @@ -1,4 +1,4 @@ -"""Read-only native 1.21.1 saved entity inspection, distinct from old-writer proof.""" +"""Read-only native 1.21.11 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,3955):raise ValueError('Wrong native 1.21.1 entity-chunk data version') + if root.get('DataVersion')!=Tag(3,4671):raise ValueError('Wrong native 1.21.11 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.1 entity-chunk Position/index') - if entities is None or entities.kind!=9 or entities.value[0] not in (0,10):raise ValueError('Wrong native 1.21.1 root Entities list') + 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') 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,12):raise ValueError('Wrong native 1.21.1 corpse identity/marker') + 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') identity=entity.value.get('UUID') - if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native 1.21.1 corpse UUID type') + if identity is None or identity.kind!=11 or len(identity.value)!=16:raise ValueError('Wrong native 1.21.11 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/native12111_server_inputs.json b/tools/native12111_server_inputs.json new file mode 100644 index 0000000..e8c1921 --- /dev/null +++ b/tools/native12111_server_inputs.json @@ -0,0 +1,106 @@ +{ + "libraries/at/yawk/lz4/lz4-java/1.8.1/lz4-java-1.8.1.jar": "1DE8FF91B90FBA9D4896B8617ED3594C2E4390D321D5A92C34F80E62E406C1B0", + "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.61/authlib-7.0.61.jar": "09A152617BD23C86BF59DB1F9BA82EDE23EC8F691A824D6C5DEC4F0175F82AA4", + "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/oceanlabs/mcp/mcp_config/1.21.11-20251223.124241/mcp_config-1.21.11-20251223.124241-srg2off.jar": "CB357DE5457645457BEB308EB61F238382B3307172A1044E5BAECF42AC0CAAB8", + "libraries/de/oceanlabs/mcp/mcp_config/1.21.11-20251223.124241/mcp_config-1.21.11-20251223.124241.zip": "1D389D1400F44E51C81487D9FBDD7ABC098095129FF415BCA9039AB1DE252A14", + "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/1.21.11/server-1.21.11-bundled.jar": "F83B8E093865806F931C7E34AAE41B177D4C076335263DD124C75D6D65DD1726", + "libraries/net/minecraft/server/1.21.11/server-1.21.11-mappings.tsrg": "418D3F8B64AAD72A60F115766C9EF41C5A03605B87F412725CD9A0806DDE7BA4", + "libraries/net/minecraft/server/1.21.11/server-1.21.11-official.jar": "C733636BED52398F6EB00784357E15B30585C5EDB5C22B9F0A1BE7A11CF31749", + "libraries/net/minecraft/server/1.21.11/server-1.21.11-unpacked.jar": "EC47239A8DE246335E1D54F6AC319BD35641778EB4B6A6DA06372840D02FCEBC", + "libraries/net/minecraftforge/accesstransformers/8.2.17/accesstransformers-8.2.17.jar": "D89305F8DE0C1BA48A0E898B7DA95E29262CB55D14970FCBBCADE0AC93631457", + "libraries/net/minecraftforge/binarypatcher/1.3.1/binarypatcher-1.3.1.jar": "8EE0B8E648507BCBA52BC58670BCF5C0F88B71A218CA426EB558644E4A1D7EFD", + "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/1.21.11-61.2.4/fmlcore-1.21.11-61.2.4.jar": "DBA4D36FBDD01D8F0BA12633A9DC45E48888264D52118D9C6EA32AA2451B9EFD", + "libraries/net/minecraftforge/fmlearlydisplay/1.21.11-61.2.4/fmlearlydisplay-1.21.11-61.2.4.jar": "9A05E742456ED4081A1BA572400C87A3685091714CC92FC3D7F1649FF652E75C", + "libraries/net/minecraftforge/fmlloader/1.21.11-61.2.4/fmlloader-1.21.11-61.2.4.jar": "7950B3E79C994C17BE71470CD2EC1DB1E13B67472F8E24DABB6769BA6C346C0E", + "libraries/net/minecraftforge/forge/1.21.11-61.2.4/forge-1.21.11-61.2.4-server.jar": "A5D403505CFAB57AC0C8798928BDC08F867890719FB7F6EAB440D81D6E10A6F8", + "libraries/net/minecraftforge/forge/1.21.11-61.2.4/forge-1.21.11-61.2.4-shim.jar": "4C3CF5A26E1AC2023FE5895D4743F36B5556E811B146C39E0ED9F424D6565B91", + "libraries/net/minecraftforge/forge/1.21.11-61.2.4/forge-1.21.11-61.2.4-universal.jar": "27AC9A872A18DDCD99B34FD210501F589E3CD65F075FCB0F9D90AEF6FF591B56", + "libraries/net/minecraftforge/forge/1.21.11-61.2.4/unix_args.txt": "CE2AEB00434E5D6F9DB7801C615F37D3AB2E00913B784408173FEA2336FC2C4B", + "libraries/net/minecraftforge/forge/1.21.11-61.2.4/win_args.txt": "CE2AEB00434E5D6F9DB7801C615F37D3AB2E00913B784408173FEA2336FC2C4B", + "libraries/net/minecraftforge/forge-transformers/1.21.11-61.2.4/forge-transformers-1.21.11-61.2.4.jar": "976D067FE20D8F249754411902FD9E4022529C6A20E02FBCFA353DFAB01B2FA3", + "libraries/net/minecraftforge/ForgeAutoRenamingTool/1.0.6/ForgeAutoRenamingTool-1.0.6.jar": "072368476225FF6D29468D07C49CD04AA5EAE11B8AB99AB3DD05802B07BC22C4", + "libraries/net/minecraftforge/forgespi/8.0.0/forgespi-8.0.0.jar": "BD275F984EDEFA63769A752046587184AD8858767EB96E46FF58B3E03753C5E8", + "libraries/net/minecraftforge/installertools/1.4.3/installertools-1.4.3.jar": "AA8F649A671C13E3335804C4A0ADD53D0F4FDBFC4AE7F3FA7717CA7784C9FBE9", + "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/1.21.11-61.2.4/javafmllanguage-1.21.11-61.2.4.jar": "41BE818C71539BBB13FFB275D8532BF73B930CEAB2B335963ED6213A5496715F", + "libraries/net/minecraftforge/lowcodelanguage/1.21.11-61.2.4/lowcodelanguage-1.21.11-61.2.4.jar": "2C3F57D3805F26E4D20B508017CAA4DF84888A2754A0EDB04B2CCCA74D34F1A6", + "libraries/net/minecraftforge/mclanguage/1.21.11-61.2.4/mclanguage-1.21.11-61.2.4.jar": "EFE48C0117BB6A410A629EB52B17D9EB032E8331FB2EBFC1DE5955A2E9A4DCB3", + "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.5.1/srgutils-0.5.1.jar": "B9BDE328A5787C43A58E29305B6BCAB31E63EA52244E3AC83A07A845AB3AAF30", + "libraries/net/minecraftforge/srgutils/0.5.10/srgutils-0.5.10.jar": "46E73138A2718DC2A378C2A0AC71756CE627819729C462B81DD54A60FB97387A", + "libraries/net/minecraftforge/srgutils/0.6.5/srgutils-0.6.5.jar": "5FE171CBCD4039629FD55AC9AF539982226FF7E49149F26064512780D3FEEB81", + "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.5/asm-9.5.jar": "B62E84B5980729751B0458C534CF1366F727542BB8D158621335682A460F0353", + "libraries/org/ow2/asm/asm/9.7/asm-9.7.jar": "ADF46D5E34940BDF148ECDD26A9EE8EEA94496A72034FF7141066B3EEA5C4E9D", + "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.5/asm-commons-9.5.jar": "72EEE9FBAFB9DE8D9463F20DD584A48CEEB7E5152AD4C987BFBE17DD4811C9AE", + "libraries/org/ow2/asm/asm-commons/9.7/asm-commons-9.7.jar": "389BC247958E049FC9A0408D398C92C6D370C18035120395D4CBA1D9D9304B7A", + "libraries/org/ow2/asm/asm-tree/9.10.1/asm-tree-9.10.1.jar": "3DFB0D5B6A106CD40B5B250E39935FBF2F927F4477546A5369A3AC609CF0506B", + "libraries/org/ow2/asm/asm-tree/9.5/asm-tree-9.5.jar": "3C33A648191079AEAEAEB7C19A49B153952F9E40FE86FBAC5205554DDD9ACD94", + "libraries/org/ow2/asm/asm-tree/9.7/asm-tree-9.7.jar": "62F4B3BC436045C1ACB5C3BA2D8EC556EC3369093D7F5D06C747EB04B56D52B1", + "libraries/org/ow2/asm/asm-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", + "forge-1.21.11-61.2.4-shim.jar": "4C3CF5A26E1AC2023FE5895D4743F36B5556E811B146C39E0ED9F424D6565B91" +} diff --git a/tools/native12111_worker_budget.json b/tools/native12111_worker_budget.json new file mode 100644 index 0000000..f7278d3 --- /dev/null +++ b/tools/native12111_worker_budget.json @@ -0,0 +1,11 @@ +{ + "minecraft": "1.21.11", + "forge": "61.2.4", + "reloads": 200, + "jvms": 4, + "qualified": true, + "jvm_args": [], + "controller_sha256": "9F86C68BC9BE2C2546F5C5637B8E754413F1F3F45B06C0023428A550402350B8", + "server_input_manifest_sha256": "19E7190F1F6B5CC878DBA9F6C8CAD3DD8DABEA8E6C8D911ABA8F567C5EF07E10", + "qualification_evidence_sha256": "6B7EF355AD5049855D53CEF791C64E190CFD1A6FBCBEBCA46A1FF3DC282164B8" +} diff --git a/tools/native_evidence.py b/tools/native_evidence.py index be23576..f2963d9 100644 --- a/tools/native_evidence.py +++ b/tools/native_evidence.py @@ -1,7 +1,7 @@ -"""Independently audit Forge 52 local receipts, complete logs and diagnostic bytes. +"""Independently audit Forge 61 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 52 +Relocated auditing is a separately tested download contract; hosted Forge 61 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 52 was qualified with its default worker and GC settings. + # Forge 61 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 52 worker or GC override') + raise ValueError('Unqualified Forge 61 worker or GC override') if relocated: # Downloaded artifacts retain their original machine cwd. Require the # complete case/condition suffix; all bytes are read only under home. @@ -34,7 +34,7 @@ def audit_phase(home,phase,*,refusal=False,relocated=False): version=FORGE.removeprefix('forge-') expected={prefix+version+'/'+name for name in ('win_args.txt','unix_args.txt')} if relocated else { prefix+version+'/'+('win_args.txt' if __import__('os').name=='nt' else 'unix_args.txt')} - if len(arguments)!=1 or arguments[0] not in expected or '--nogui' not in command:raise ValueError('Receipt did not launch Forge 52 modules') + 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 not refusal and '-Dlootablebodies.phase='+phase not in command:raise ValueError('Receipt belongs to another phase') for name in ('gc.txt','initial-threads.txt','initial-heap-gc.txt'): diff --git a/tools/native_runtime.py b/tools/native_runtime.py index 19d1bb1..4137967 100644 --- a/tools/native_runtime.py +++ b/tools/native_runtime.py @@ -1,4 +1,4 @@ -"""Disposable official Forge 52 launches, separate from the development bootstrap.""" +"""Disposable official Forge 61 launches, separate from the development bootstrap.""" import hashlib import json import os @@ -13,33 +13,33 @@ from command_logs import run_logged from runtime_diagnostics import run_monitored,validate_logging_inputs -FORGE = 'forge-1.21.1-52.1.20' -INSTALLER_SHA256 = '6B83521A70AAF5371AF8C4C9F97D930D51EC4266AC6EC3BC975182FBA2EF9F62' -SERVER_INPUTS = json.loads(Path(__file__).with_name('native121_server_inputs.json').read_text(encoding='utf-8')) -WORKER_BUDGET_PATH = Path(__file__).with_name('native121_worker_budget.json') +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') WORKER_BUDGET_PROOF = json.loads(WORKER_BUDGET_PATH.read_text(encoding='utf-8')) if WORKER_BUDGET_PATH.is_file() else {} -# Forge 52 uses its actual default CPU/worker/GC configuration. A new target's +# Forge 61 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 52 historical runtime policy has not been qualified') + raise RuntimeError('Forge 61 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.1','52.1.20',200,4,True): - raise RuntimeError('Forge 52 historical runtime policy has not been qualified') + 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 proof.get('jvm_args')!=[]: - raise RuntimeError('Unqualified Forge 52 worker or GC override') + raise RuntimeError('Unqualified Forge 61 worker or GC override') source=Path(__file__).resolve().parents[1]/'src/controlTest/java/buildcontrol/ResourceControl.java' - manifest=Path(__file__).with_name('native121_server_inputs.json') + manifest=Path(__file__).with_name('native12111_server_inputs.json') if proof.get('controller_sha256')!=digest(source) or proof.get('server_input_manifest_sha256')!=digest(manifest): - raise RuntimeError('Forge 52 resource qualification source or SDK binding changed') + raise RuntimeError('Forge 61 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 52 resource qualification identity') - evidence=Path(__file__).resolve().parents[1]/'.gradle/native121-resource-qualification/proof.json' + raise RuntimeError('Missing Forge 61 resource qualification identity') + evidence=Path(__file__).resolve().parents[1]/'.gradle/native12111-resource-qualification/proof.json' if not evidence.is_file() or digest(evidence)!=proof['qualification_evidence_sha256']: - raise RuntimeError('Missing or changed actual local Forge 52 resource qualification evidence') + raise RuntimeError('Missing or changed actual local Forge 61 resource qualification evidence') return HISTORICAL_JVM_ARGS @@ -60,15 +60,15 @@ def fingerprint(directory): def verify_server_inputs(distribution): """Pin the exact completed SDK after both official installer sides. - Forge 52 generated archives match the installer's declared expected hashes. + Forge 61 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 52 SDK input: '+name) + if artifact.is_symlink() or not artifact.is_file():raise FileNotFoundError('Missing Forge 61 SDK input: '+name) value=digest(artifact) - if value!=expected:raise RuntimeError('Incorrect official Forge 52 server input: '+name) + if value!=expected:raise RuntimeError('Incorrect official Forge 61 server input: '+name) actual[name]=value return actual @@ -90,7 +90,7 @@ def prepare_server(home, distribution, candidate=None, probe=None): distribution = distribution.resolve() libraries = distribution / 'libraries' if not libraries.is_dir(): - raise FileNotFoundError('Incomplete official Forge 52 server installation') + raise FileNotFoundError('Incomplete official Forge 61 server installation') verify_server_inputs(distribution) home.mkdir(parents=True, exist_ok=False) (home / 'mods').mkdir() @@ -118,10 +118,14 @@ def prepare_server(home, distribution, candidate=None, probe=None): # Existing copied headers remain authoritative. Fresh flat fixtures need # an explicitly prepared native-codec header before their first launch. (home / 'server.properties').write_text( - 'level-name=world\nlevel-seed=4267321\nlevel-type=DEFAULT\nonline-mode=false\n' + 'level-name=world\nlevel-seed=4267321\nlevel-type=minecraft\\:normal\nonline-mode=false\n' 'server-ip=127.0.0.1\nserver-port=0\ngenerate-structures=false\n' 'spawn-animals=false\nspawn-monsters=false\nspawn-protection=0\n' - 'max-tick-time=120000\n', encoding='utf-8') + 'max-tick-time=120000\n' + # The official Windows runtime falls back to Java NIO. Select it + # directly in disposable fixtures to avoid probing Linux/BSD libraries. + + ('use-native-transport=false\n' if platform.system()=='Windows' else ''), + encoding='utf-8') for name in ('ops', 'whitelist', 'banned-players', 'banned-ips'): (home / (name + '.json')).write_text('[]\n', encoding='utf-8') @@ -156,7 +160,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.1-52.1.20/' + arguments, '--nogui'] + *jvm_args, '@libraries/net/minecraftforge/forge/1.21.11-61.2.4/' + arguments, '--nogui'] validate_logging_inputs(command) def initial_diagnostics(process): # Short refusals and successful phases must retain real thread/heap @@ -206,14 +210,14 @@ def seed_flat_fixture(home, header, expected_sha256): version = data.value['DataVersion'] generator = data.value['WorldGenSettings'].value['dimensions'].value[ 'minecraft:overworld'].value['generator'].value['type'] - if version.kind != 3 or version.value != 3955 or generator.kind != 8 or generator.value != 'minecraft:flat': + if version.kind != 3 or version.value != 4671 or generator.kind != 8 or generator.value != 'minecraft:flat': raise ValueError('Wrong native version or generator') except (AttributeError, KeyError, ValueError) as error: - raise RuntimeError('Expected an explicit native3955 flat codec header') from error + raise RuntimeError('Expected an explicit native4671 flat codec header') from error world.mkdir() shutil.copy2(header, world/'level.dat') (home/'native-flat-header-input.json').write_text(json.dumps({ - 'sha256':expected_sha256, 'native_data_version':3955, 'generator':'minecraft:flat', + 'sha256':expected_sha256, 'native_data_version':4671, 'generator':'minecraft:flat', 'only_header_copied':True}, indent=2)+'\n', encoding='utf-8') @@ -249,7 +253,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.13.121011'}) + request = urllib.request.Request(address, headers={'User-Agent': 'LootableBodiesRuntimeTests/2.4.14.121111'}) with urllib.request.urlopen(request, timeout=60) as response: content = response.read() if hashlib.sha1(content).hexdigest() != expected: @@ -281,7 +285,7 @@ def validate_client_paths(paths): import re for path in paths: value=str(path).replace('\\','/').lower();name=path.name.lower() - # Forge 52's exact descriptor requires this dist-marker API archive. + # Forge 61's exact descriptor requires this dist-marker API archive. if (value.endswith('/net/minecraftforge/mergetool-api/1.0/mergetool-api-1.0.jar') and not path.is_symlink() and path.is_file() and digest(path) == '100F0C4D658B9DD73600CE4BB9C75657E0651F0F0F5E0B2F977F85D8F495350B'): @@ -297,7 +301,7 @@ def client_inputs(runtime, prepared): def prepare_assets(runtime, cache=None): - raise RuntimeError('Use pinned native 1.21.1 assets; inherited cache-copy hydration is disabled') + raise RuntimeError('Use pinned native 1.21.11 assets; inherited cache-copy hydration is disabled') def prepare_client(home, candidate=None, probe=None): @@ -316,20 +320,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/native121-client-inputs' + prepared=Path(__file__).resolve().parents[1]/'.gradle/native12111-client-inputs' return command(java,home,runtime,assets,prepared,username=username,extra=extra) def install(java, home, client=False): - """Install Forge 52 locally; its installer owns the server downloads.""" + """Install Forge 61 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.1-52.1.20/'+installer.name,installer,INSTALLER_SHA256) + fetch('https://maven.minecraftforge.net/net/minecraftforge/forge/1.21.11-61.2.4/'+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.1.json'},base) + _download({'sha1':BASE_SHA1,'url':'https://piston-meta.mojang.com/v1/packages/'+BASE_SHA1+'/1.21.11.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 1963dac..1f8ed0d 100644 --- a/tools/qualification-policy.json +++ b/tools/qualification-policy.json @@ -1,7 +1,7 @@ { "schema": 1, - "target": "1.21.1", - "status": "Forge 1.21.1 stays local until qualification and manual testing are complete.", + "target": "1.21.11", + "status": "Forge 1.21.11 stays local until qualification and manual testing are complete.", "hosted": { "allowed": [ "policy", diff --git a/tools/test_historical_runtime_budget.py b/tools/test_historical_runtime_budget.py index 2b8aac6..7544ce8 100644 --- a/tools/test_historical_runtime_budget.py +++ b/tools/test_historical_runtime_budget.py @@ -71,7 +71,7 @@ def test_forged_flag_or_stale_source_binding_cannot_authorize_history(self): import json with tempfile.TemporaryDirectory() as directory: policy=Path(directory)/'policy.json' - base={'minecraft':'1.21.1','forge':'52.1.20','reloads':200,'jvms':4,'qualified':True,'jvm_args':[]} + base={'minecraft':'1.21.11','forge':'61.2.4','reloads':200,'jvms':4,'qualified':True,'jvm_args':[]} for fields in ({'jvm_args':['-Dmax.bg.threads=1']},{'controller_sha256':'0'*64}): policy.write_text(json.dumps(dict(base,**fields))) with patch.object(native_runtime,'WORKER_BUDGET_PATH',policy),patch.object(native_runtime,'run_monitored') as launch: diff --git a/tools/test_hosted_policy.py b/tools/test_hosted_policy.py index 3d0b06b..0c69b1a 100644 --- a/tools/test_hosted_policy.py +++ b/tools/test_hosted_policy.py @@ -29,17 +29,17 @@ def test_missing_fork_guard_or_pipe_failure_guard_is_rejected(self): with self.subTest(removed=removed),self.assertRaises(ValueError):validate_workflow(text.replace(removed,'')) def test_manifest_boolean_cannot_override_an_integrity_hold(self): - version='2.4.11.121011';base='LootableBodies-'+version + version='2.4.11.121111';base='LootableBodies-'+version names=['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom'] - good={'version':version,'minecraft':'1.21.1','forge':'52.1.20','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in names}} + 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}} 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.121011';base='LootableBodies-'+version - good={'version':version,'minecraft':'1.21.1','forge':'52.1.20','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{name:'A'*64 for name in ['CHANGELOG.txt','SHA256SUMS',base+'.jar',base+'-sources.jar',base+'-javadoc.jar',base+'.pom']}} + 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']}} 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 c4909dc..bfd3c7f 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.1','forge':'52.1.20','local_qualification_complete':True,'qualification_integrity_status':'verified','files':{p.name:digest(p) for p in bundle.iterdir()}} + record={'version':MOD_VERSION,'minecraft':'1.21.11','forge':'61.2.4','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 b74e321..3e59680 100644 --- a/tools/test_native120_client_contracts.py +++ b/tools/test_native120_client_contracts.py @@ -34,7 +34,7 @@ def test_forge_bootstrap_uses_installed_client_descriptor_and_exact_java21(self) index=root/'assets/indexes/16.json';index.parent.mkdir(parents=True);index.write_bytes(b'{}') base={'assetIndex':{'id':'16','sha1':hashlib.sha1(b'{}').hexdigest()},'arguments':{'jvm':['-cp','${classpath}'],'game':['--version','${version_name}','--accessToken','${auth_access_token}','--clientId','${clientid}','--xuid','${auth_xuid}']}} forge={'arguments':{'jvm':['-Djava.net.preferIPv6Addresses=system'],'game':['--launchTarget','forge_client']},'mainClass':'net.minecraftforge.bootstrap.ForgeBootstrap'} - record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'1.21.1.jar')],'natives':str(root/'natives'),'system':'windows','architecture':'amd64'} + record={'library_directory':str(root/'sdk/libraries'),'classpath':[str(root/'1.21.11.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,16 +42,18 @@ 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.1.jar'),result) + self.assertIn(str(root/'1.21.11.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.1-forge-52.1.20',result[result.index('--version')+1]) + self.assertEqual('1.21.11-forge-61.2.4',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) + self.assertNotIn('--offlineDeveloperMode',owned) self.assertIn('-Dminecraft.api.services.host=http://127.0.0.1:51927',owned) def test_unsealed_classpath_and_changed_native_bytes_are_rejected(self): diff --git a/tools/test_native120_client_logs.py b/tools/test_native120_client_logs.py index 6e04cee..49d13e7 100644 --- a/tools/test_native120_client_logs.py +++ b/tools/test_native120_client_logs.py @@ -38,9 +38,9 @@ def test_actual_proof_reader_rejects_changed_resource_proof_target_and_control(s with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as directory: root=Path(directory);definition=root/'sounds.json';definition.write_text('{}');critical=root/'control.fixture';critical.write_text('synthetic independent control input');rawlog=root/'native.log';rawlog.write_text('synthetic native fixture') sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest().upper() - proof={'minecraft':'1.21.1','forge':'52.1.20','platform':'Windows amd64','qualified':True,'no_candidate':True,'no_world':True,'native_client_exit':0,'events':EVENTS,'control_sha256':'C'*64,'sound_definition':str(definition),'critical_files':{str(definition):sha(definition),str(critical):sha(critical)},'raw_control_logs':{str(rawlog):sha(rawlog)},'original_strict_audit_passed':False} + proof={'minecraft':'1.21.11','forge':'61.2.4','platform':'Windows amd64','qualified':True,'no_candidate':True,'no_world':True,'native_client_exit':0,'events':EVENTS,'control_sha256':'C'*64,'sound_definition':str(definition),'critical_files':{str(definition):sha(definition),str(critical):sha(critical)},'raw_control_logs':{str(rawlog):sha(rawlog)},'original_strict_audit_passed':False} saved=root/'proof.json';saved.write_text(json.dumps(proof)) - policy={'minecraft':'1.21.1','forge':'52.1.20','platform':'Windows amd64','qualified':True,'proof_sha256':sha(saved),'events':EVENTS,'control_sha256':'C'*64,'sound_definition_sha256':sha(definition)} + policy={'minecraft':'1.21.11','forge':'61.2.4','platform':'Windows amd64','qualified':True,'proof_sha256':sha(saved),'events':EVENTS,'control_sha256':'C'*64,'sound_definition_sha256':sha(definition)} if mutation=='resource':definition.write_text('{"item.goat_horn.play":{}}') elif mutation=='proof':saved.write_text('{}') elif mutation=='missing':critical.unlink() diff --git a/tools/test_native120_flat_header.py b/tools/test_native120_flat_header.py index 12e3a25..2e2582d 100644 --- a/tools/test_native120_flat_header.py +++ b/tools/test_native120_flat_header.py @@ -9,7 +9,7 @@ class Native120FlatHeaderTests(unittest.TestCase): - def header(self,root,version=3955,generator='minecraft:flat'): + def header(self,root,version=4671,generator='minecraft:flat'): settings=Tag(10,{'dimensions':Tag(10,{'minecraft:overworld':Tag(10,{ 'generator':Tag(10,{'type':Tag(8,generator)})})})}) data=encode('',Tag(10,{'Data':Tag(10,{'DataVersion':Tag(3,version),'WorldGenSettings':settings})})) @@ -23,7 +23,7 @@ def test_only_the_explicit_sealed_header_is_copied(self): self.assertEqual(before,(home/'world/level.dat').read_bytes());self.assertEqual(before,header.read_bytes()) self.assertEqual(['level.dat'],[p.name for p in (home/'world').iterdir()]) def test_wrong_hash_target_or_generator_refuses_before_creating_world(self): - for version,generator,bad_hash in [(3955,'minecraft:flat',True),(3839,'minecraft:flat',False),(3465,'minecraft:flat',False),(2586,'minecraft:flat',False),(2730,'minecraft:flat',False),(2975,'minecraft:flat',False),(3955,'minecraft:noise',False)]: + 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)]: with self.subTest(version=version,generator=generator,bad_hash=bad_hash),tempfile.TemporaryDirectory() as directory: root=Path(directory);header,digest=self.header(root,version,generator);home=self.home(root) with self.assertRaises(RuntimeError):seed_flat_fixture(home,header,'0'*64 if bad_hash else digest) @@ -36,8 +36,8 @@ def test_existing_world_is_never_overwritten(self): self.assertEqual(b'existing world bytes',protected.read_bytes()) def test_actual_native20_codec_header_is_a_sealed_current_target_input(self): - header=Path(__file__).parent/'fixtures/native121-flat-level.dat' - expected='C85D57B04D1309304A2B28992E5B45B4E695039BE162682255A0CCCCB3EE30CD' + header=Path(__file__).parent/'fixtures/native12111-flat-level.dat' + expected='33DBE405659FB4EF57D23A07485EBC9AFA57694D7B54357C5B534A3E8963F86F' self.assertEqual(expected,hashlib.sha256(header.read_bytes()).hexdigest().upper()) with tempfile.TemporaryDirectory() as directory: home=self.home(Path(directory));seed_flat_fixture(home,header,expected) @@ -45,12 +45,12 @@ def test_actual_native20_codec_header_is_a_sealed_current_target_input(self): def test_forge_only_control_rejects_native_header_with_the_mod_installed(self): fixtures=Path(__file__).parent/'fixtures' - forge_only=fixtures/'native121-forge-only-flat-level.dat' - expected='62253943F2A307C158BC1CE29E2430BE253C3F727A4587FEDDBE279DD3C057D4' + forge_only=fixtures/'native12111-forge-only-flat-level.dat' + expected='4754C3F3D514B82E89FCF7F89F882A38611A456ABA9A9AB4C6564A13707F8026' 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/'native121-flat-level.dat') + require_forge_only_header(fixtures/'native12111-flat-level.dat') if __name__=='__main__':unittest.main() diff --git a/tools/test_native120_infrastructure.py b/tools/test_native120_infrastructure.py index 34cbe7e..c26a4d6 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.1-52.1.20',runtime.FORGE) - self.assertEqual('6B83521A70AAF5371AF8C4C9F97D930D51EC4266AC6EC3BC975182FBA2EF9F62',runtime.INSTALLER_SHA256) - self.assertEqual(93,len(runtime.SERVER_INPUTS)) - self.assertEqual('A19B7DC205719132139D03492BBFAE51DCF58C8F65E2044935CF9C4DA9C0E5A2', - runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.21.1-52.1.20/forge-1.21.1-52.1.20-server.jar']) - self.assertEqual('8E1A1872D63BA80249626637B70043C864053A41338E18D7F04163902FFFD53E', - runtime.SERVER_INPUTS['libraries/net/minecraftforge/forge/1.21.1-52.1.20/win_args.txt']) + self.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.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 = '17'",text) - self.assertIn("1.21.1.json",text) + self.assertIn("minecraftAssetIndex = '29'",text) + self.assertIn("1.21.11.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 f64c8fe..a952566 100644 --- a/tools/test_native120_launch.py +++ b/tools/test_native120_launch.py @@ -31,7 +31,7 @@ def test_packaged_server_uses_installer_module_arguments_and_java21_gc(self): command=run.call_args.args[0] self.assertNotIn('-jar',command) self.assertIn('-Xmx2G',command) - self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/1.21.1-52.1.20/') for arg in command)) + self.assertTrue(any(arg.startswith('@libraries/net/minecraftforge/forge/1.21.11-61.2.4/') for arg in command)) gc=next((arg for arg in command if arg.startswith('-Xlog:')),None) self.assertIsNotNone(gc) self.assertIn('gc*,safepoint',gc) @@ -73,16 +73,16 @@ def attach(command, **options): self.assertEqual(['jstack','jstat'],started) def test_server_inputs_pin_native50_instead_of_the_old_launcher(self): - self.assertEqual('forge-1.21.1-52.1.20',native_runtime.FORGE) - for path in ('libraries/net/minecraftforge/forge/1.21.1-52.1.20/win_args.txt', - 'libraries/net/minecraftforge/forge/1.21.1-52.1.20/unix_args.txt', - 'libraries/net/minecraftforge/forge/1.21.1-52.1.20/forge-1.21.1-52.1.20-server.jar'): + self.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.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.1','52.1.20',200,4,True),tuple(proof[key] for key in + self.assertEqual(('1.21.11','61.2.4',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}$') @@ -102,7 +102,7 @@ def test_owned_server_does_not_trigger_native_bare_json_flat_settings_fallback(s native_runtime.prepare_server(root/'home',sdk) self.assertEqual(shim.read_bytes(),(root/'home'/shim.name).read_bytes()) properties=(root/'home/server.properties').read_text() - self.assertIn('level-type=DEFAULT\n',properties) + self.assertIn('level-type=minecraft\\:normal\n',properties) self.assertNotIn('level-type=FLAT',properties) self.assertFalse((root/'home/world').exists(),'Historical/native-codec worlds are copied separately') diff --git a/tools/test_native120_persistence.py b/tools/test_native120_persistence.py index 9b9fc42..e64da18 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=12):return Tag(10,{'id':Tag(8,'lootablebodies:corpse'),'LootableBodiesDataVersion':Tag(3,marker),'UUID':Tag(11,struct.pack('>iiii',0,0,0,number))}) - def chunk(self,bodies,version=3955,position=(8,8)): + 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)): 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 [(11,3955,(8,8)),(12,3839,(8,8)),(12,3955,(9,8))]: + for marker,version,position in [(12,4671,(8,8)),(13,3955,(8,8)),(13,4671,(9,8))]: with self.subTest(marker=marker,version=version,position=position),tempfile.TemporaryDirectory() as directory: world=Path(directory);self.write(world,self.chunk([self.body(marker=marker)],version,position)) with self.assertRaises(ValueError):count_persisted(world,0) @@ -60,7 +60,11 @@ def test_valid_future_entity_fixture_preserves_original_and_rejects_wrong_source else: mutation=inject_future_entity_version(world) self.assertEqual(original,(home/'future-entity-original.mca').read_bytes()) - self.assertEqual(Tag(3,3956),read_region(path)[264]['root'].value['DataVersion']) + converted = read_region(path)[264]['root'].value + self.assertEqual(Tag(3, 4672), converted['DataVersion']) + self.assertEqual(4672, mutation['injected_version']) + self.assertEqual(self.chunk([self.body(marker=8)], version)['root'].value['Entities'], + converted['Entities']) self.assertEqual(8,mutation['corpse_marker']) if __name__=='__main__':unittest.main() diff --git a/tools/test_native_evidence.py b/tools/test_native_evidence.py index bef5029..ae1b030 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.1-52.1.20/'+('win_args.txt' if os.name=='nt' else 'unix_args.txt'),'--nogui'], + '@libraries/net/minecraftforge/forge/1.21.11-61.2.4/'+('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.1-52.1.20/win_args.txt') + record['command'].append('@libraries/net/minecraftforge/forge/1.21.11-61.2.4/win_args.txt') (folder/'result.json').write_text(json.dumps(record)) with self.assertRaises(ValueError):audit_phase(home,'upgrade-near',relocated=True) diff --git a/tools/test_native_server_preparation.py b/tools/test_native_server_preparation.py index f26106e..f84b296 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 52 server input'):runtime.install(Path('fixture-java'),home) + with self.assertRaisesRegex(RuntimeError,'Incorrect official Forge 61 server input'):runtime.install(Path('fixture-java'),home) prefetch.assert_not_called();self.assertEqual(1,installer.call_count) self.assertFalse((home/'runtime-inputs.json').exists()) def test_missing_installed_input_is_fatal(self): diff --git a/tools/verify_native_respawn_control.py b/tools/verify_native_respawn_control.py index c710d75..e75a89f 100644 --- a/tools/verify_native_respawn_control.py +++ b/tools/verify_native_respawn_control.py @@ -1,4 +1,4 @@ -"""Exercise Forge 52 delayed respawns and FIFO packets without Lootable Bodies.""" +"""Exercise Forge 61 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 native37.""" + """Retained native36 log-fixture contract; never used to qualify Forge 61.""" 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', '52.1.20'), ('minecraft', '1.21.1')]: + if kind != 10 or sorted(mods) != [('forge', '61.2.4'), ('minecraft', '1.21.11')]: raise ValueError('Expected a Forge-only saved mod list') @@ -93,10 +93,10 @@ def start(name): server=(root/'server.log').read_text(errors='replace') batches=[int(value) for value in re.findall(r'NATIVE_FIFO_DELAY_RELEASE packets=([0-9]+)',client)] if server.count('NATIVE_DEATH ')!=8 or len(batches)!=8 or sum(batches)<=0: - raise RuntimeError('Forge 52 control did not exercise all eight real delayed respawn/FIFO actions') - result={'lootable_bodies_installed':False,'respawns':8,'forge52_delayed_fifo_batches':batches, + raise RuntimeError('Forge 61 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,'forge52_complete_logs_audited':True,'no_unclassified_diagnostics':True, + 'legacy36_error_matcher_used':False,'forge61_complete_logs_audited':True,'no_unclassified_diagnostics':True, 'control_jar_sha256':digest(probe),'native_outbound_packets_delayed_fifo':True,'log_audits':reports} (root/'result.json').write_text(json.dumps(result,indent=2)+'\n') print('NATIVE_RESPAWN_CONTROL_PASS',json.dumps(result),flush=True) diff --git a/tools/verify_normal_server.py b/tools/verify_normal_server.py index d51b6e2..1250a57 100644 --- a/tools/verify_normal_server.py +++ b/tools/verify_normal_server.py @@ -16,7 +16,10 @@ def prepare_development_home(home): home.mkdir(exist_ok=True);(home/'eula.txt').write_text('eula=true\n') seed_loader_defaults(home) - (home/'server.properties').write_text('level-name=world\nlevel-type=DEFAULT\nserver-ip=127.0.0.1\nserver-port=0\nonline-mode=false\n') + 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' + (home/'server.properties').write_text(properties) for name in ['ops','whitelist','banned-players','banned-ips']: (home/(name+'.json')).write_text('[]\n') diff --git a/tools/verify_upgrade_abort.py b/tools/verify_upgrade_abort.py index 3731dc0..2f011b1 100644 --- a/tools/verify_upgrade_abort.py +++ b/tools/verify_upgrade_abort.py @@ -167,9 +167,11 @@ def inject_future_entity_version(world): before=digest(path);original=world.parent/'future-entity-original.mca' if original.exists():raise ValueError('Future fixture original already exists') shutil.copy2(path,original) - root['DataVersion']=Tag(3,3956);write_disposable_region(path,chunks) + future_version = 4672 # Immediately beyond Minecraft 1.21.11's 4671. + 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, - 'injected_version':3956,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), + 'injected_version':future_version,'nbt_type':'Int','original_sha256':before,'mutated_sha256':digest(path), 'original_copy':str(original),'corpse_marker':8} From 0e641561a08e198964f7321668aca8902355e251 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 9 Nov 2026 12:03:19 +0000 Subject: [PATCH 2/2] Keep Forge 1.21.11 release archives reproducible --- build.gradle | 3 +++ src/main/resources/coremods/.gitkeep | 1 + 2 files changed, 4 insertions(+) create mode 100644 src/main/resources/coremods/.gitkeep diff --git a/build.gradle b/build.gradle index a2f1fe4..9ac7b74 100644 --- a/build.gradle +++ b/build.gradle @@ -216,6 +216,8 @@ dependencies { } tasks.named('processResources', ProcessResources) { + // Keep the empty directory present in the qualified 1.21.11 archives. + exclude 'coremods/.gitkeep' filteringCharset = 'UTF-8' inputs.properties(version: project.version, mcversion: project.minecraft_version, mod_url: project.mod_url, mod_license: project.mod_license) @@ -238,6 +240,7 @@ tasks.named('jar', Jar) { ext.releaseJar = tasks.named('jar', Jar) ['sourcesJar', 'javadocJar'].each { name -> tasks.named(name, Jar) { + if (name == 'sourcesJar') exclude 'coremods/.gitkeep' from(['LICENSE', 'LICENSE-CC0', 'NOTICE']) { into 'META-INF' } manifest.attributes('Implementation-Title': project.mod_name, 'Implementation-Version': project.version, 'Implementation-Vendor': project.mod_vendor, diff --git a/src/main/resources/coremods/.gitkeep b/src/main/resources/coremods/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/coremods/.gitkeep @@ -0,0 +1 @@ +