Implement playable Mac client and rendering validation

This commit is contained in:
shen
2026-09-11 14:58:22 +08:00
parent 1ab68bd06e
commit 374d4165d8
233 changed files with 6546 additions and 284 deletions
+27
View File
@@ -0,0 +1,27 @@
# Native tree geometry
These 87 GLBs are offline SpeedTreeRT extractions of this workspace's existing
SPT assets, not procedural replacements. Their embedded bark/composite textures
and meshes are game artwork: the same internal-use/licensing restrictions as
`assets/Tree` apply. Do not treat them as freely redistributable open-source art.
`manifest.json` maps original SPT SHA-256 to the GLB filename and SHA-256.
Both hashes are checked by `script/stage_tree_geometry.mjs`; the runtime also
verifies both before loading. The source conversion index lives in the sibling
`metin2-godot-main/generated/content-v1/40250-full-unpacked-final` workspace.
No proprietary Windows DLL is shipped or loaded by the Mac client.
Coordinates are already Godot Y-up metres. Keep original height, origin, UVs,
branches, fronds and leaf cards; do not apply the former proxy's 12m scale.
Leaf materials use double-sided alpha scissor with cutoff 0.3.
This is real **static** geometry: full SpeedTree wind, camera-facing leaf
clusters and native LOD transitions are not reconstructed by these GLBs.
Validation: all 87 species import; all 81 species referenced by current static
AreaData have a conversion; six principal maps render 2,092 native trees with
zero proxies. The 31 legacy SPTs under `Zone/ymir work/zone/b/tree` have different
source hashes and are not converted or aliased to these meshes. They are not
referenced by current tree property records. One unused historical property
references missing `christmastree.spt`; the actual `christmastree1/2/3.spt`
variants are converted. Seasonal event spawning is separate from static maps.
+963
View File
@@ -0,0 +1,963 @@
{
"schema_version": 1,
"coordinates": "godot_y_up_meters",
"trees": {
"1d20b68d50ce007b4814da28936b1992ccb399f1c264df3a5dcaa8b92de5be5e": {
"source": "Tree/ymir work/tree/b1_baobab_rt.spt",
"source_sha256": "1d20b68d50ce007b4814da28936b1992ccb399f1c264df3a5dcaa8b92de5be5e",
"glb": "1d20b68d50ce007b4814da28936b1992ccb399f1c264df3a5dcaa8b92de5be5e.glb",
"glb_sha256": "642c31b558023112fc6c5fba9955c235d39d97a7c05eb56613bd76a4cf33e83e",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"9e5b696ab020cd18232e18c866454c5d52deb1076998db0a1fbf8703d1599741": {
"source": "Tree/ymir work/tree/b1_baobab_rt2.spt",
"source_sha256": "9e5b696ab020cd18232e18c866454c5d52deb1076998db0a1fbf8703d1599741",
"glb": "9e5b696ab020cd18232e18c866454c5d52deb1076998db0a1fbf8703d1599741.glb",
"glb_sha256": "4a4d6013668a084dc3b00d44e9cc7474457ddd1f06e64229a348ff8f0a445326",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"de17c0c13f956cf75d92997063a12c5dbca1a26234ad14a5f868a03362a11b48": {
"source": "Tree/ymir work/tree/b1_beech_rt.spt",
"source_sha256": "de17c0c13f956cf75d92997063a12c5dbca1a26234ad14a5f868a03362a11b48",
"glb": "de17c0c13f956cf75d92997063a12c5dbca1a26234ad14a5f868a03362a11b48.glb",
"glb_sha256": "18f49b44e9b435f91da8b75958aab7aef4b0d368301da7b3789f51e0570481ac",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"cdb81537efcda4c26781463a6018d59c42edcb12ca3d5b724c029810bf434de9": {
"source": "Tree/ymir work/tree/b1_beech_rt2.spt",
"source_sha256": "cdb81537efcda4c26781463a6018d59c42edcb12ca3d5b724c029810bf434de9",
"glb": "cdb81537efcda4c26781463a6018d59c42edcb12ca3d5b724c029810bf434de9.glb",
"glb_sha256": "9da1a6ab558c70dda232c36a2bd85bb710d24031149dac0198af3532b0bd4145",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"9d7ee35f949b4764090b1a916ade1005b622b31fea49b2d4b8586076711c8fc7": {
"source": "Tree/ymir work/tree/b1_beech_rt3.spt",
"source_sha256": "9d7ee35f949b4764090b1a916ade1005b622b31fea49b2d4b8586076711c8fc7",
"glb": "9d7ee35f949b4764090b1a916ade1005b622b31fea49b2d4b8586076711c8fc7.glb",
"glb_sha256": "edf0fbac525cfc48f87527aa25d3f49df6f010ee51f56ae78e0ac69d00aa4ba5",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"25d677d269f8cfc38ca1cd3f692810cb293ab13db46b576b278cc695f7cf3f16": {
"source": "Tree/ymir work/tree/b1_beech_rt4.spt",
"source_sha256": "25d677d269f8cfc38ca1cd3f692810cb293ab13db46b576b278cc695f7cf3f16",
"glb": "25d677d269f8cfc38ca1cd3f692810cb293ab13db46b576b278cc695f7cf3f16.glb",
"glb_sha256": "edc5c99aab45da766baa28ee402086b2b107326ec52d81b3324c06ad38def6ee",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"66392538f04a55d484f9c92cdc9d19d21341f499f30162d288e75812669f1f04": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt.spt",
"source_sha256": "66392538f04a55d484f9c92cdc9d19d21341f499f30162d288e75812669f1f04",
"glb": "66392538f04a55d484f9c92cdc9d19d21341f499f30162d288e75812669f1f04.glb",
"glb_sha256": "9abdfc7f413b5aa4117f36106720dc979ebfa4c647c28a833904aa4d9e713466",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0b90a99d6480452b12b5f78a9b61e453180ec4a83f4246bdf1b91c02b949f4e8": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt2.spt",
"source_sha256": "0b90a99d6480452b12b5f78a9b61e453180ec4a83f4246bdf1b91c02b949f4e8",
"glb": "0b90a99d6480452b12b5f78a9b61e453180ec4a83f4246bdf1b91c02b949f4e8.glb",
"glb_sha256": "acd4675f30e805d5543c17f0a7a52fe37f31f6e67874b196b7395997e1ea8e83",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"f6691c8b816eea9bc8c49189d7f780f531ed9ec82d432fbba3a847d0776c27db": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt3.spt",
"source_sha256": "f6691c8b816eea9bc8c49189d7f780f531ed9ec82d432fbba3a847d0776c27db",
"glb": "f6691c8b816eea9bc8c49189d7f780f531ed9ec82d432fbba3a847d0776c27db.glb",
"glb_sha256": "56effe5240c0cf44f74c033412fa2bf25a8329be74c66a62da3d2288ae24ab10",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d7ca320772595a77c28856c45a36674e18918b9e7bc5101cd5cb8cdf12de27e1": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt4.spt",
"source_sha256": "d7ca320772595a77c28856c45a36674e18918b9e7bc5101cd5cb8cdf12de27e1",
"glb": "d7ca320772595a77c28856c45a36674e18918b9e7bc5101cd5cb8cdf12de27e1.glb",
"glb_sha256": "d371eba315defbfb6956cc8cfbd26dde7ff9240d21f832c37a6fc63559bfcca2",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"cb9102770aca71a6e269e806c6e6e74e5d4cb8096abbf078180ab27c8e1367c3": {
"source": "Tree/ymir work/tree/b1_montereycypress_rt5.spt",
"source_sha256": "cb9102770aca71a6e269e806c6e6e74e5d4cb8096abbf078180ab27c8e1367c3",
"glb": "cb9102770aca71a6e269e806c6e6e74e5d4cb8096abbf078180ab27c8e1367c3.glb",
"glb_sha256": "28c7a3e30c21d3874247e10a0bf6107de1775d29b0effa664edeb743c036f953",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2851666e8f3556aa6606642bcd529672ab45b0f8db1304d6fbb4466900a2ca5a": {
"source": "Tree/ymir work/tree/b1_pagodatree_rt.spt",
"source_sha256": "2851666e8f3556aa6606642bcd529672ab45b0f8db1304d6fbb4466900a2ca5a",
"glb": "2851666e8f3556aa6606642bcd529672ab45b0f8db1304d6fbb4466900a2ca5a.glb",
"glb_sha256": "0af3909bf5fe30ae77f918e7641e12791b8c2d3f40ec576aa375b3005bb8f528",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d35194db82fe4ca4a7668f95d5d95f116ab6b08c326b7baf0c0ab074276db953": {
"source": "Tree/ymir work/tree/b1_pagodatree_rt2.spt",
"source_sha256": "d35194db82fe4ca4a7668f95d5d95f116ab6b08c326b7baf0c0ab074276db953",
"glb": "d35194db82fe4ca4a7668f95d5d95f116ab6b08c326b7baf0c0ab074276db953.glb",
"glb_sha256": "a6546ab05a6722bf2eaefc0b23ae399d97050a280631488bb82d651ed131ea52",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"a566eff0304a67971167e4e7d3bf254ab0c7a33478fcd657cec613c16058e900": {
"source": "Tree/ymir work/tree/b1_pagodatree_rt3.spt",
"source_sha256": "a566eff0304a67971167e4e7d3bf254ab0c7a33478fcd657cec613c16058e900",
"glb": "a566eff0304a67971167e4e7d3bf254ab0c7a33478fcd657cec613c16058e900.glb",
"glb_sha256": "fd60281f1384d80b51f838ee512e5d7506fef17d8ad9c28c962a4970e5e02be6",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"1bc5e65c0e4efcb60f7ea930f683b80001b8dd97762b6d25281946a295685149": {
"source": "Tree/ymir work/tree/b1_sassafras_rt_fall.spt",
"source_sha256": "1bc5e65c0e4efcb60f7ea930f683b80001b8dd97762b6d25281946a295685149",
"glb": "1bc5e65c0e4efcb60f7ea930f683b80001b8dd97762b6d25281946a295685149.glb",
"glb_sha256": "69ba5cd343cb193e12d5fa9203356e08e5536e6a0083650ca4be88f87472fcf8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"5c6dabb419b86756136240a32b532746d6f86759c8e744f1701c24bcb840262a": {
"source": "Tree/ymir work/tree/b1_sassafras_rt_fall2.spt",
"source_sha256": "5c6dabb419b86756136240a32b532746d6f86759c8e744f1701c24bcb840262a",
"glb": "5c6dabb419b86756136240a32b532746d6f86759c8e744f1701c24bcb840262a.glb",
"glb_sha256": "14cb7ad036580d3387112e2098d52ee6815d158c533ed52f3db0604a00c33b09",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d76530cfba5e75a94a9634fd76f86a02126c22bd51cccfc3d995eff8974742f2": {
"source": "Tree/ymir work/tree/b2_cedaroflebanon_rt.spt",
"source_sha256": "d76530cfba5e75a94a9634fd76f86a02126c22bd51cccfc3d995eff8974742f2",
"glb": "d76530cfba5e75a94a9634fd76f86a02126c22bd51cccfc3d995eff8974742f2.glb",
"glb_sha256": "d8df7faf73265c6a5b2a725cfacab421b609cd9a4bd2bad41997e349e9ee4fa7",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"73d757a146ca27a9459ccc3027cd9464ce415f1a6731771cbff37827a3ee1caa": {
"source": "Tree/ymir work/tree/b2_cedaroflebanon_rt2.spt",
"source_sha256": "73d757a146ca27a9459ccc3027cd9464ce415f1a6731771cbff37827a3ee1caa",
"glb": "73d757a146ca27a9459ccc3027cd9464ce415f1a6731771cbff37827a3ee1caa.glb",
"glb_sha256": "c6c15c69d0b60f232293e74f8da191b3ae62f4a69fba61d0e7fe94008c51176e",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"fd2e5bb94c5223e1eaca97544e757e135252edb05f0ddfa991e3abad3fb78b3f": {
"source": "Tree/ymir work/tree/b2_cedaroflebanon_rt3.spt",
"source_sha256": "fd2e5bb94c5223e1eaca97544e757e135252edb05f0ddfa991e3abad3fb78b3f",
"glb": "fd2e5bb94c5223e1eaca97544e757e135252edb05f0ddfa991e3abad3fb78b3f.glb",
"glb_sha256": "81e2b1c1cd753ba17e8d875bc5ad0104c0f2a8cc1831d4decbc64d7a82fb1202",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"50557c6da89615d82f9d81799315549bed25e9ae09dd128a6f161f83a17f04f5": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt.spt",
"source_sha256": "50557c6da89615d82f9d81799315549bed25e9ae09dd128a6f161f83a17f04f5",
"glb": "50557c6da89615d82f9d81799315549bed25e9ae09dd128a6f161f83a17f04f5.glb",
"glb_sha256": "d632d027f5e228e6d85a986068ada05996461692623dc082569b9078965af7dd",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d012fcd397e8dac1d1778b3ad700ee36f08d282bd53ca6466a7232824e453f5a": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt2.spt",
"source_sha256": "d012fcd397e8dac1d1778b3ad700ee36f08d282bd53ca6466a7232824e453f5a",
"glb": "d012fcd397e8dac1d1778b3ad700ee36f08d282bd53ca6466a7232824e453f5a.glb",
"glb_sha256": "ce88fcfb1f04048912665592e0ad4da69679163c23b8b2f541641bad7e319f2d",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"065babe8c785f54d23b99e91e9d505b91eadaae1dff492bcfe236b657acf9272": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt3.spt",
"source_sha256": "065babe8c785f54d23b99e91e9d505b91eadaae1dff492bcfe236b657acf9272",
"glb": "065babe8c785f54d23b99e91e9d505b91eadaae1dff492bcfe236b657acf9272.glb",
"glb_sha256": "aea108dfb8fcaf7021b54df8c27a194ac5486fac08fb32dbc15c00b7834d2596",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"61f67291ff5d7c49014991dddb3d03550ae18e157f1acf62daf249ca40206bf3": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt4.spt",
"source_sha256": "61f67291ff5d7c49014991dddb3d03550ae18e157f1acf62daf249ca40206bf3",
"glb": "61f67291ff5d7c49014991dddb3d03550ae18e157f1acf62daf249ca40206bf3.glb",
"glb_sha256": "fcd95e7569a5a6d8af5e987f3764d520d941fa518827ae247514b003c2de5a87",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"f40054b4e07fdc6071f8b5bd6ad85782d5207c51fa116bfb8e0aca4d8675badd": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt5.spt",
"source_sha256": "f40054b4e07fdc6071f8b5bd6ad85782d5207c51fa116bfb8e0aca4d8675badd",
"glb": "f40054b4e07fdc6071f8b5bd6ad85782d5207c51fa116bfb8e0aca4d8675badd.glb",
"glb_sha256": "0824f3fc789c420d6eb543cf932f617eabb8f01437e5bf6ef9aacf116e0eca83",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"80422882e2d1ec63f6c5080c9c308634b8e8acc1342ebe1ebc74039fe2384631": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt_winter.spt",
"source_sha256": "80422882e2d1ec63f6c5080c9c308634b8e8acc1342ebe1ebc74039fe2384631",
"glb": "80422882e2d1ec63f6c5080c9c308634b8e8acc1342ebe1ebc74039fe2384631.glb",
"glb_sha256": "f2a4fa095e0d7ca96a49afc371b9f9189a5b370ca4543827fc3098971b779a31",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"8c9d519de4a1652c2b923e11927be3818942235052cc67e6825d9705a09feaeb": {
"source": "Tree/ymir work/tree/b2_ivyspy_rt_winter2.spt",
"source_sha256": "8c9d519de4a1652c2b923e11927be3818942235052cc67e6825d9705a09feaeb",
"glb": "8c9d519de4a1652c2b923e11927be3818942235052cc67e6825d9705a09feaeb.glb",
"glb_sha256": "97a8e81c46cf1f5dbe8084535e17286c7df07e7b71d61ae7c591adff1dd62fda",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"fa2b9ac46f0f3be36f386fb8630f034393bde37d9cc5b2ec8b38470bdbb14c59": {
"source": "Tree/ymir work/tree/b2_japanesemaple_rt_fall.spt",
"source_sha256": "fa2b9ac46f0f3be36f386fb8630f034393bde37d9cc5b2ec8b38470bdbb14c59",
"glb": "fa2b9ac46f0f3be36f386fb8630f034393bde37d9cc5b2ec8b38470bdbb14c59.glb",
"glb_sha256": "34831b921865e35889704938333c80d69daaba09762e4fe8351c673811053dea",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"86b5d9065583eabca29ad89b7ccb67ac640bada57af8504c6448027fa05bd655": {
"source": "Tree/ymir work/tree/b2_japanesemaple_rt_fall2.spt",
"source_sha256": "86b5d9065583eabca29ad89b7ccb67ac640bada57af8504c6448027fa05bd655",
"glb": "86b5d9065583eabca29ad89b7ccb67ac640bada57af8504c6448027fa05bd655.glb",
"glb_sha256": "f38ffdbebb6f60e4e92d3b8993cc3001c502a8223c72bfb3688812d9217a67b2",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"13292b7af3472178d07e40005acd73b09d61f1d9f9570606cc74f115a42a1637": {
"source": "Tree/ymir work/tree/b2_redoak_rt.spt",
"source_sha256": "13292b7af3472178d07e40005acd73b09d61f1d9f9570606cc74f115a42a1637",
"glb": "13292b7af3472178d07e40005acd73b09d61f1d9f9570606cc74f115a42a1637.glb",
"glb_sha256": "6fc61c59dc838a768f10b749a9b7ce1ea659e4a9d6f9fc6ee6c887c0532adc6c",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"55ee1c716113b30ebc1e4166698765f041f1c094bf28d4ec86f8ca61202e5116": {
"source": "Tree/ymir work/tree/b2_redoak_rt2.spt",
"source_sha256": "55ee1c716113b30ebc1e4166698765f041f1c094bf28d4ec86f8ca61202e5116",
"glb": "55ee1c716113b30ebc1e4166698765f041f1c094bf28d4ec86f8ca61202e5116.glb",
"glb_sha256": "3200e730f4551792a504f20e4046e7bcf466e1e51d90229c9328b3ea63bc47cb",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"c50b043a6ceed03fbdac17cd0a861b1de1d06f4c5933a9078d073f42867e1a22": {
"source": "Tree/ymir work/tree/b2_redoak_rt3.spt",
"source_sha256": "c50b043a6ceed03fbdac17cd0a861b1de1d06f4c5933a9078d073f42867e1a22",
"glb": "c50b043a6ceed03fbdac17cd0a861b1de1d06f4c5933a9078d073f42867e1a22.glb",
"glb_sha256": "6781839f9fcdcdbae7b156b5708436e2580781a11bb784ff11f5f088a841471a",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0d62629ce2423bdfce51595038cab213dec628acb4cf063b0b271279a28e8832": {
"source": "Tree/ymir work/tree/b2_redoak_rt4.spt",
"source_sha256": "0d62629ce2423bdfce51595038cab213dec628acb4cf063b0b271279a28e8832",
"glb": "0d62629ce2423bdfce51595038cab213dec628acb4cf063b0b271279a28e8832.glb",
"glb_sha256": "febe8dcbe1880cec8f91dc5844a55749c8d183e86c1b581d07440f6bf38ab794",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"eace2fb1af759da813559a33d38690596a066397983994cfea75232441ca3086": {
"source": "Tree/ymir work/tree/b3_beech_rt.spt",
"source_sha256": "eace2fb1af759da813559a33d38690596a066397983994cfea75232441ca3086",
"glb": "eace2fb1af759da813559a33d38690596a066397983994cfea75232441ca3086.glb",
"glb_sha256": "1d29e58f3e04b4dbfe4cbfb91403bddcfed2af0f282636f506d358b5f15cccb1",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"65846f0ea0ac8ace155a11cc6a41631d3330e6db25ba9a9c8b8171b96fdfde67": {
"source": "Tree/ymir work/tree/b3_beech_rt2.spt",
"source_sha256": "65846f0ea0ac8ace155a11cc6a41631d3330e6db25ba9a9c8b8171b96fdfde67",
"glb": "65846f0ea0ac8ace155a11cc6a41631d3330e6db25ba9a9c8b8171b96fdfde67.glb",
"glb_sha256": "2bdbe584e694ec4516803ac0966f741cf39920f20c4692b6f5f4496e5fff0b06",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"09c12d2bf8ef2c5ada7d54323b7b11816bebc56cc3fddd7373aba4eed7d3527e": {
"source": "Tree/ymir work/tree/b3_beech_rt3.spt",
"source_sha256": "09c12d2bf8ef2c5ada7d54323b7b11816bebc56cc3fddd7373aba4eed7d3527e",
"glb": "09c12d2bf8ef2c5ada7d54323b7b11816bebc56cc3fddd7373aba4eed7d3527e.glb",
"glb_sha256": "cae93d33e0849b769cb9070bb094239a2d68c33e22942df24836190757c3cd98",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e622c7689d6525c09b22244a9a112ce9448c1c189cbe7f0468da9de82b8d9c36": {
"source": "Tree/ymir work/tree/b3_beech_rt_fall.spt",
"source_sha256": "e622c7689d6525c09b22244a9a112ce9448c1c189cbe7f0468da9de82b8d9c36",
"glb": "e622c7689d6525c09b22244a9a112ce9448c1c189cbe7f0468da9de82b8d9c36.glb",
"glb_sha256": "59e6e7c3378af28c44a3293b1bd026023cbcc9c5447be1684e63a88d9b3691f8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"8aaf7a77ff61d8dd3828057df396ae27a6471faa7db94a6f2c93c0522ce22aa0": {
"source": "Tree/ymir work/tree/b3_beech_rt_fall2.spt",
"source_sha256": "8aaf7a77ff61d8dd3828057df396ae27a6471faa7db94a6f2c93c0522ce22aa0",
"glb": "8aaf7a77ff61d8dd3828057df396ae27a6471faa7db94a6f2c93c0522ce22aa0.glb",
"glb_sha256": "3e684dc00afd7451b28ed3f89a00baa7ea6c9d45c442caad332b9596ed0b872c",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"6600b5cae4c4f473b5cea7e9d90614ffeb3559807d4605d6d729ab7b3182eb77": {
"source": "Tree/ymir work/tree/b3_pagodatree_rt_winter.spt",
"source_sha256": "6600b5cae4c4f473b5cea7e9d90614ffeb3559807d4605d6d729ab7b3182eb77",
"glb": "6600b5cae4c4f473b5cea7e9d90614ffeb3559807d4605d6d729ab7b3182eb77.glb",
"glb_sha256": "bec163b2c1fa99b26d97bad86155a401ab19c067b2acb160a747fceb5b168226",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"ea34cd93e320c38e79837ab348022ea6cab4a62b295fd20a54c7a4a9688129f5": {
"source": "Tree/ymir work/tree/b3_pagodatree_rt_winter2.spt",
"source_sha256": "ea34cd93e320c38e79837ab348022ea6cab4a62b295fd20a54c7a4a9688129f5",
"glb": "ea34cd93e320c38e79837ab348022ea6cab4a62b295fd20a54c7a4a9688129f5.glb",
"glb_sha256": "63aa6ef7fe8181bc1877e86a6b3f1f3bd617a2af303208006289c8e68a76b859",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e03e7c40dd56befae1804550d90f114a1496f7a5c0fa18ae111de5a19372639b": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt.spt",
"source_sha256": "e03e7c40dd56befae1804550d90f114a1496f7a5c0fa18ae111de5a19372639b",
"glb": "e03e7c40dd56befae1804550d90f114a1496f7a5c0fa18ae111de5a19372639b.glb",
"glb_sha256": "54bb8cf813b9c6f05f14ea7dbb39689a3a26f9737e7526969c2bea070b8bf78f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d59d8a429a63272bcd21b403cd581556b57a140f5cdc65c7af329d30b6770d26": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt2.spt",
"source_sha256": "d59d8a429a63272bcd21b403cd581556b57a140f5cdc65c7af329d30b6770d26",
"glb": "d59d8a429a63272bcd21b403cd581556b57a140f5cdc65c7af329d30b6770d26.glb",
"glb_sha256": "6b85b0a649472c7268791a0adce367099b4fbb783d07ad3a508e0aa88d77c331",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2bec6d9317e3947386c56c4183db454fe629af9fa87de759324928e681eb5742": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt3.spt",
"source_sha256": "2bec6d9317e3947386c56c4183db454fe629af9fa87de759324928e681eb5742",
"glb": "2bec6d9317e3947386c56c4183db454fe629af9fa87de759324928e681eb5742.glb",
"glb_sha256": "3a3fc8e8afa93b2a65806fd5d6eb625a05426c6f8a99c50a77ac1c197bf9a6f5",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0a15ca2a0809cc74e6d72ed48b137906f02f0e44994228fa9f840d76a6ca52cc": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt4.spt",
"source_sha256": "0a15ca2a0809cc74e6d72ed48b137906f02f0e44994228fa9f840d76a6ca52cc",
"glb": "0a15ca2a0809cc74e6d72ed48b137906f02f0e44994228fa9f840d76a6ca52cc.glb",
"glb_sha256": "39dc26f0713574b98e8bba0a26ff448bd6fabd97c5b66880f155516f536d35a4",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"dbc300acb7356f4f51e897a2e248a2883241ea570d5d8cded218b31401206534": {
"source": "Tree/ymir work/tree/b3_shingleoak_rt5.spt",
"source_sha256": "dbc300acb7356f4f51e897a2e248a2883241ea570d5d8cded218b31401206534",
"glb": "dbc300acb7356f4f51e897a2e248a2883241ea570d5d8cded218b31401206534.glb",
"glb_sha256": "08dc609a9aa5b16ce943dfd6bc63d2db45daf3bc7252fd6d1ee58a7ce6449685",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0e8ab0197ee0db8e3a40e4b8d6c5c8f0b291284d2ccb7e13cee41d0f0792cc6f": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers.spt",
"source_sha256": "0e8ab0197ee0db8e3a40e4b8d6c5c8f0b291284d2ccb7e13cee41d0f0792cc6f",
"glb": "0e8ab0197ee0db8e3a40e4b8d6c5c8f0b291284d2ccb7e13cee41d0f0792cc6f.glb",
"glb_sha256": "580a3ff0582a7723250650e8b0828912000faf494afb25ace42578e0d1397bb8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"3c5c75535819e405b61cd21c89d514a8d2b0e6afdd75428cc0687745a3314cfe": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers2.spt",
"source_sha256": "3c5c75535819e405b61cd21c89d514a8d2b0e6afdd75428cc0687745a3314cfe",
"glb": "3c5c75535819e405b61cd21c89d514a8d2b0e6afdd75428cc0687745a3314cfe.glb",
"glb_sha256": "2483acf297ce1f35e3ef297ff14e84c33b16bfae885fb04f373a601ad41cfab6",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"a6592779c9460b004be7956123a13f439bef0669dbfdad4bd330b78c44b100d2": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers3.spt",
"source_sha256": "a6592779c9460b004be7956123a13f439bef0669dbfdad4bd330b78c44b100d2",
"glb": "a6592779c9460b004be7956123a13f439bef0669dbfdad4bd330b78c44b100d2.glb",
"glb_sha256": "0d5fe2e83f4621f6cefbbf8e645306af026c540364d05d91e2757d8d4775ceff",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"44ec99e53b3158a6619ec7a5cb4110bfb67ff9943224b068a8bc99fa1d92c0d0": {
"source": "Tree/ymir work/tree/b3_umbrellathorn_rt_flowers4.spt",
"source_sha256": "44ec99e53b3158a6619ec7a5cb4110bfb67ff9943224b068a8bc99fa1d92c0d0",
"glb": "44ec99e53b3158a6619ec7a5cb4110bfb67ff9943224b068a8bc99fa1d92c0d0.glb",
"glb_sha256": "1efd17b58f6dd39b3df3e7de84d20a1d4740e8d53c05a08080a3e335619e6ac8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d85676762825b8324ebeba85bc9a0e2242da8d225c5699c042f469abd17d01d7": {
"source": "Tree/ymir work/tree/christmastree1.spt",
"source_sha256": "d85676762825b8324ebeba85bc9a0e2242da8d225c5699c042f469abd17d01d7",
"glb": "d85676762825b8324ebeba85bc9a0e2242da8d225c5699c042f469abd17d01d7.glb",
"glb_sha256": "6836fef2133fbc14a4ac17f3762cfe64b596e2cc1dbae818c80fe4eb421a510f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"6277b6d6dd747b0a6aa067fc2a27e8e7dad3bd8c9c755d6b124bc474b4107a1d": {
"source": "Tree/ymir work/tree/christmastree2.spt",
"source_sha256": "6277b6d6dd747b0a6aa067fc2a27e8e7dad3bd8c9c755d6b124bc474b4107a1d",
"glb": "6277b6d6dd747b0a6aa067fc2a27e8e7dad3bd8c9c755d6b124bc474b4107a1d.glb",
"glb_sha256": "451bd2edca71c9aa5c658ce8bc1353b7315530220f1d1458c037d5ea007b2794",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"074755f44edc3d836c20b8e254c61af805c3e7f3cbe1e65f76bb35bd3d66ca8f": {
"source": "Tree/ymir work/tree/christmastree3.spt",
"source_sha256": "074755f44edc3d836c20b8e254c61af805c3e7f3cbe1e65f76bb35bd3d66ca8f",
"glb": "074755f44edc3d836c20b8e254c61af805c3e7f3cbe1e65f76bb35bd3d66ca8f.glb",
"glb_sha256": "6445e98dd9760e3371dbcd13673b539b2c6be6450dc883605c50b3d5fac3f825",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0ce9fe068b0a227cb4ce1c36dbeff5792ae6488f012c028b54ab89d7ef2b2f3b": {
"source": "Tree/ymir work/tree/n1_baobab_rt_winter.spt",
"source_sha256": "0ce9fe068b0a227cb4ce1c36dbeff5792ae6488f012c028b54ab89d7ef2b2f3b",
"glb": "0ce9fe068b0a227cb4ce1c36dbeff5792ae6488f012c028b54ab89d7ef2b2f3b.glb",
"glb_sha256": "d49a2346357486cab5368ecb1a32a8e728bd45318b301b86755c41d481f3712f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0d766295707ebd0e6af56bfc49ed1bca8bc11f7b15f79be44e221bbe9e18d53e": {
"source": "Tree/ymir work/tree/n1_beech_rt_winter.spt",
"source_sha256": "0d766295707ebd0e6af56bfc49ed1bca8bc11f7b15f79be44e221bbe9e18d53e",
"glb": "0d766295707ebd0e6af56bfc49ed1bca8bc11f7b15f79be44e221bbe9e18d53e.glb",
"glb_sha256": "031cde20e4c46d5f75cda94fd783834614d31584f7af36e9be2218dabc6a0cbb",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e5497e163459d6ad06745e4b8a5ab95422fb59143e7aa76cf40ac65d242872b3": {
"source": "Tree/ymir work/tree/n1_beech_rt_winter02.spt",
"source_sha256": "e5497e163459d6ad06745e4b8a5ab95422fb59143e7aa76cf40ac65d242872b3",
"glb": "e5497e163459d6ad06745e4b8a5ab95422fb59143e7aa76cf40ac65d242872b3.glb",
"glb_sha256": "f542b8c33350569b80f2a3c88fc1db22be93ed51fbcfe46654d2b29fccaa413a",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"41997e16e6110e21096ffa2d85f0d2135e7b8a4ec82a869f899a6325776a2225": {
"source": "Tree/ymir work/tree/n1_beech_rt_winter03.spt",
"source_sha256": "41997e16e6110e21096ffa2d85f0d2135e7b8a4ec82a869f899a6325776a2225",
"glb": "41997e16e6110e21096ffa2d85f0d2135e7b8a4ec82a869f899a6325776a2225.glb",
"glb_sha256": "fd2fd865a012ba6f53b0bdd0eea8971c7858826cc4cbb08d3a6db1becf027fc0",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"b60fa3d6329d76d4b85da226e88c36241bf8fb4e3a3fc5e1316f0a64d7e2c6ad": {
"source": "Tree/ymir work/tree/n1_coloradobluespruce_rt_01.spt",
"source_sha256": "b60fa3d6329d76d4b85da226e88c36241bf8fb4e3a3fc5e1316f0a64d7e2c6ad",
"glb": "b60fa3d6329d76d4b85da226e88c36241bf8fb4e3a3fc5e1316f0a64d7e2c6ad.glb",
"glb_sha256": "82d906bb624b0041c3d687e3af51cff8ed524ba75e6432a5f293554d5d4ff137",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"265aef90b037111a8d08bd4f258501872be9fc0b4bacf6ad17820241c5f959a1": {
"source": "Tree/ymir work/tree/n1_coloradobluespruce_rt_02.spt",
"source_sha256": "265aef90b037111a8d08bd4f258501872be9fc0b4bacf6ad17820241c5f959a1",
"glb": "265aef90b037111a8d08bd4f258501872be9fc0b4bacf6ad17820241c5f959a1.glb",
"glb_sha256": "6265bd35d1da9f9daa34a15b34dd9cadc5af9d6346c0b9104b2c3d582c510225",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2e3fe98b0ca1aae0ad512741f9465e42653b4c4df7345bc915e6b6b3bf80d27d": {
"source": "Tree/ymir work/tree/n1_coloradobluespruce_rt_03.spt",
"source_sha256": "2e3fe98b0ca1aae0ad512741f9465e42653b4c4df7345bc915e6b6b3bf80d27d",
"glb": "2e3fe98b0ca1aae0ad512741f9465e42653b4c4df7345bc915e6b6b3bf80d27d.glb",
"glb_sha256": "cbeb5930c57e2dbefbe9c50987275e7abc2b1ac9aa9cf190f60de71dae75f52b",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"4c3b7f1bc32ca54a581a22eb99a5229c066a2e3db84d73c4a260b41ecb4151d6": {
"source": "Tree/ymir work/tree/n1_commonolive_rt_winter.spt",
"source_sha256": "4c3b7f1bc32ca54a581a22eb99a5229c066a2e3db84d73c4a260b41ecb4151d6",
"glb": "4c3b7f1bc32ca54a581a22eb99a5229c066a2e3db84d73c4a260b41ecb4151d6.glb",
"glb_sha256": "a2fb958788a84301f9008e4efe1ea9994314d28b4b3954649c64b3033bf912c0",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e63e6fd8aec38f5a4dea10edce507870f834cf1da5fbd8b5e789dc75f773957c": {
"source": "Tree/ymir work/tree/n1_montereycypress_rt_winter_01.spt",
"source_sha256": "e63e6fd8aec38f5a4dea10edce507870f834cf1da5fbd8b5e789dc75f773957c",
"glb": "e63e6fd8aec38f5a4dea10edce507870f834cf1da5fbd8b5e789dc75f773957c.glb",
"glb_sha256": "9dc165cb8f6417f9e27486db714809722b5e85ff4ed827b21a0fe1d52feee89f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"bd813274fcc6fc3c8bf26a9bc85eb6b3c18d0a4a68dd0a51e13183f4634680da": {
"source": "Tree/ymir work/tree/n1_montereycypress_rt_winter_02.spt",
"source_sha256": "bd813274fcc6fc3c8bf26a9bc85eb6b3c18d0a4a68dd0a51e13183f4634680da",
"glb": "bd813274fcc6fc3c8bf26a9bc85eb6b3c18d0a4a68dd0a51e13183f4634680da.glb",
"glb_sha256": "f64d701e3ec752f566eef179ed62d613fd20d7dcc0bd86dcb95c1e3a0ef56c86",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"1af978f7efd1be5ba7af90056c4b933f74154c60a47a20fe88f4872aaf7bc862": {
"source": "Tree/ymir work/tree/n1_montereycypress_rt_winter_03.spt",
"source_sha256": "1af978f7efd1be5ba7af90056c4b933f74154c60a47a20fe88f4872aaf7bc862",
"glb": "1af978f7efd1be5ba7af90056c4b933f74154c60a47a20fe88f4872aaf7bc862.glb",
"glb_sha256": "f80e080f78042b751116ef056edf98501922de0bb46c6e0efb950e17115afbbe",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"36389501b6156b934f960d826ac2e3ecae86430cf53dc88dd6c30f5a8415aba9": {
"source": "Tree/ymir work/tree/n1_tulip_rt_winter_01.spt",
"source_sha256": "36389501b6156b934f960d826ac2e3ecae86430cf53dc88dd6c30f5a8415aba9",
"glb": "36389501b6156b934f960d826ac2e3ecae86430cf53dc88dd6c30f5a8415aba9.glb",
"glb_sha256": "ce0f23b6d5931ebeea06862a4ffef37b2274dc0b8643ec2f5768f1b0df3ae858",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"0df26ab085d443bccc61d8ec878b61117f64db3b3ffa31e70fa0fce6d5f59361": {
"source": "Tree/ymir work/tree/n1_whitepine_rt_low_01.spt",
"source_sha256": "0df26ab085d443bccc61d8ec878b61117f64db3b3ffa31e70fa0fce6d5f59361",
"glb": "0df26ab085d443bccc61d8ec878b61117f64db3b3ffa31e70fa0fce6d5f59361.glb",
"glb_sha256": "48bd12ddffdc0aee165a29bfc6fd0dcbfe1366aec5542bcee102bf8d5d82c3e2",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"ac5bb9b19999fe6d20793898edc7b709b1ba792e6cac2cc7ed5fcd64abdfb03f": {
"source": "Tree/ymir work/tree/n1_whitepine_rt_low_02.spt",
"source_sha256": "ac5bb9b19999fe6d20793898edc7b709b1ba792e6cac2cc7ed5fcd64abdfb03f",
"glb": "ac5bb9b19999fe6d20793898edc7b709b1ba792e6cac2cc7ed5fcd64abdfb03f.glb",
"glb_sha256": "fa1fe6e118c91b6d1276b17ed05297eed6349e4c29726e11a51de48252dbf787",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"34314fd26754a94e9215723fd970a09b360eef0f8fa7e93c55c44b6dadd4b4c8": {
"source": "Tree/ymir work/tree/n2_aloevera_rt_flowers_01.spt",
"source_sha256": "34314fd26754a94e9215723fd970a09b360eef0f8fa7e93c55c44b6dadd4b4c8",
"glb": "34314fd26754a94e9215723fd970a09b360eef0f8fa7e93c55c44b6dadd4b4c8.glb",
"glb_sha256": "96d7b310fa8d13e910b11e5524a1a30886e21c9e715290c64d1df2e9b4298d17",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e0c3f6cfe9200acf3fe66fcc33fa52e2bbab55a48a35815178e74ee80e717c36": {
"source": "Tree/ymir work/tree/n2_aloevera_rt_flowers_02.spt",
"source_sha256": "e0c3f6cfe9200acf3fe66fcc33fa52e2bbab55a48a35815178e74ee80e717c36",
"glb": "e0c3f6cfe9200acf3fe66fcc33fa52e2bbab55a48a35815178e74ee80e717c36.glb",
"glb_sha256": "968300bd31c235557926343e46d7335ae75644aff6d0ca58f3a3e4a1963c5e51",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"f7c94a390c712edae5557d03849373c713d4ce28fba6080d87fc761216c4a26d": {
"source": "Tree/ymir work/tree/n2_bananatree_rt_01.spt",
"source_sha256": "f7c94a390c712edae5557d03849373c713d4ce28fba6080d87fc761216c4a26d",
"glb": "f7c94a390c712edae5557d03849373c713d4ce28fba6080d87fc761216c4a26d.glb",
"glb_sha256": "0b43c6087d26a19e6863a1d488eb6e3e524240793d850b27f844e4c194818afa",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"23b662d2b6d87d268460e4ae100b618ecfac6dd5262f59a2912e6dfe74530436": {
"source": "Tree/ymir work/tree/n2_bananatree_rt_02.spt",
"source_sha256": "23b662d2b6d87d268460e4ae100b618ecfac6dd5262f59a2912e6dfe74530436",
"glb": "23b662d2b6d87d268460e4ae100b618ecfac6dd5262f59a2912e6dfe74530436.glb",
"glb_sha256": "c5bd49676be213f69141b644b109a890f682fcc31ae8bf4fb0d078b72b28d8b8",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"93ded82028c0f2e97d0403ec045b89bbc1ac859ad8b84a2e1930e0503907b354": {
"source": "Tree/ymir work/tree/n2_cinnamonfern_rt_01.spt",
"source_sha256": "93ded82028c0f2e97d0403ec045b89bbc1ac859ad8b84a2e1930e0503907b354",
"glb": "93ded82028c0f2e97d0403ec045b89bbc1ac859ad8b84a2e1930e0503907b354.glb",
"glb_sha256": "027fe6300b18a9608ffe465d1e1c77783a8c05c17e99c2999e058095ed62de05",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"dc389cbfc25b6e3226e961e7e392ed96c7a789c0dfc1d392b0a1950e81f59f9e": {
"source": "Tree/ymir work/tree/n2_cinnamonfern_rt_02.spt",
"source_sha256": "dc389cbfc25b6e3226e961e7e392ed96c7a789c0dfc1d392b0a1950e81f59f9e",
"glb": "dc389cbfc25b6e3226e961e7e392ed96c7a789c0dfc1d392b0a1950e81f59f9e.glb",
"glb_sha256": "0c537775ba7e98e11b6d5b74a6665dfd5eb74c94f70ad39c80c4f0979b5246ac",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"82566982c7decbccbbb9e50f0b1d399a86bed94a968e0a78e2ce8b25989d2462": {
"source": "Tree/ymir work/tree/n2_cinnamonfern_rt_03.spt",
"source_sha256": "82566982c7decbccbbb9e50f0b1d399a86bed94a968e0a78e2ce8b25989d2462",
"glb": "82566982c7decbccbbb9e50f0b1d399a86bed94a968e0a78e2ce8b25989d2462.glb",
"glb_sha256": "1d38479f01b5dac599ff8dbc80dab1657671950047da6bae43e7a201562da106",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 1,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"af5abcd685bf92c9597740c9c85168f6104e62c12e30117ca5759bf555f2c6ff": {
"source": "Tree/ymir work/tree/n2_coconutpalm_rt_01.spt",
"source_sha256": "af5abcd685bf92c9597740c9c85168f6104e62c12e30117ca5759bf555f2c6ff",
"glb": "af5abcd685bf92c9597740c9c85168f6104e62c12e30117ca5759bf555f2c6ff.glb",
"glb_sha256": "3344943038999faf012f8d1520b2feaaab367c06a6b24748685a233b89f7ec70",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e45bdc521daaeb53191a995485513b046b0602b60d55185e465ded20a8242828": {
"source": "Tree/ymir work/tree/n2_coconutpalm_rt_02.spt",
"source_sha256": "e45bdc521daaeb53191a995485513b046b0602b60d55185e465ded20a8242828",
"glb": "e45bdc521daaeb53191a995485513b046b0602b60d55185e465ded20a8242828.glb",
"glb_sha256": "3f46282c3c8d094206d516475cf1e086e87af584c4537c3d48da1e2ee544a248",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"75b89b54281bedd408be91863cfbf1131a7c9fce5c0e153e601db3c7195c8dd6": {
"source": "Tree/ymir work/tree/n2_coconutpalm_rt_03.spt",
"source_sha256": "75b89b54281bedd408be91863cfbf1131a7c9fce5c0e153e601db3c7195c8dd6",
"glb": "75b89b54281bedd408be91863cfbf1131a7c9fce5c0e153e601db3c7195c8dd6.glb",
"glb_sha256": "1e19c9fe65ef02d0277206f4737eeafec5541ba6bc2619dcb015e31bf8d76800",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"8a6d6468b7f97040b08ba70718650abbd0d0c4684fa6e092b0a603717ed0ec28": {
"source": "Tree/ymir work/tree/n2_curlypalm_rt_01.spt",
"source_sha256": "8a6d6468b7f97040b08ba70718650abbd0d0c4684fa6e092b0a603717ed0ec28",
"glb": "8a6d6468b7f97040b08ba70718650abbd0d0c4684fa6e092b0a603717ed0ec28.glb",
"glb_sha256": "a0859095e4b20c71d33e3cde76696584880df357d3642c07d79355cb0d732809",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"de5e22bb1610c61bcb00e3835698b6a9b50b67263a438b9ba7400157dce42b5e": {
"source": "Tree/ymir work/tree/n2_curlypalm_rt_02.spt",
"source_sha256": "de5e22bb1610c61bcb00e3835698b6a9b50b67263a438b9ba7400157dce42b5e",
"glb": "de5e22bb1610c61bcb00e3835698b6a9b50b67263a438b9ba7400157dce42b5e.glb",
"glb_sha256": "c8b1d7ad7faf78e14b0c50f7c33dd9712a2c79dbec54a574945e2b20f7e22d4a",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"97ce0c94956eeafad580424081ae4ab767f01e948ab9461a9b48e5cf8bd824af": {
"source": "Tree/ymir work/tree/n2_curlypalm_rt_03.spt",
"source_sha256": "97ce0c94956eeafad580424081ae4ab767f01e948ab9461a9b48e5cf8bd824af",
"glb": "97ce0c94956eeafad580424081ae4ab767f01e948ab9461a9b48e5cf8bd824af.glb",
"glb_sha256": "700aa9caba46ed1b7141d58a653d77ea9eb96911172d1a4dacb414328c8f18ee",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"6e24a4896134057a1bc495f0f8af32ca74a7b51e0d54baa612f6dcd36a99b54e": {
"source": "Tree/ymir work/tree/n2_datepalm_rt_01.spt",
"source_sha256": "6e24a4896134057a1bc495f0f8af32ca74a7b51e0d54baa612f6dcd36a99b54e",
"glb": "6e24a4896134057a1bc495f0f8af32ca74a7b51e0d54baa612f6dcd36a99b54e.glb",
"glb_sha256": "7c2a38b68b0b33d46db13830c12302a1962584e51e628386d654509998e1795f",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"55de8a592ad3d9294583bcf27c80fba3fe71afdc669c5657ee7cc77a4002d255": {
"source": "Tree/ymir work/tree/n2_datepalm_rt_02.spt",
"source_sha256": "55de8a592ad3d9294583bcf27c80fba3fe71afdc669c5657ee7cc77a4002d255",
"glb": "55de8a592ad3d9294583bcf27c80fba3fe71afdc669c5657ee7cc77a4002d255.glb",
"glb_sha256": "e7391ec2d87aa7df6f87943e4faaf91de5951dccb045e6935844c997f7de1179",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2007d4ba90f12b8a00d2a0a539954e420c37f2151f438c5ad06eb63a78fe853d": {
"source": "Tree/ymir work/tree/n2_datepalm_rt_03.spt",
"source_sha256": "2007d4ba90f12b8a00d2a0a539954e420c37f2151f438c5ad06eb63a78fe853d",
"glb": "2007d4ba90f12b8a00d2a0a539954e420c37f2151f438c5ad06eb63a78fe853d.glb",
"glb_sha256": "ec41d7cb6c58864cc32bccca5e364d8fa4746501d4bf9a37d3f64c611834e0c9",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 2,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"2bc2d707e302c58d099e64615ae127371c93700f91d52d31e8ec6f63010e2c0f": {
"source": "Tree/ymir work/tree/n2_joshuatree_rt_01.spt",
"source_sha256": "2bc2d707e302c58d099e64615ae127371c93700f91d52d31e8ec6f63010e2c0f",
"glb": "2bc2d707e302c58d099e64615ae127371c93700f91d52d31e8ec6f63010e2c0f.glb",
"glb_sha256": "27937a1b7cb30186a7041b4c8e405e531a3152004d5365082d9e0908e3998670",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 4,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"111cb80abecb5c5a35aaa8315ad6ec4c59772af927729780e0add02c9ba262b5": {
"source": "Tree/ymir work/tree/n2_joshuatree_rt_02.spt",
"source_sha256": "111cb80abecb5c5a35aaa8315ad6ec4c59772af927729780e0add02c9ba262b5",
"glb": "111cb80abecb5c5a35aaa8315ad6ec4c59772af927729780e0add02c9ba262b5.glb",
"glb_sha256": "6082d34315db4cd58e606b4b35cdbd07ab374d7587580512e07b0e27cfdedb11",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 4,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"a89a40ac942ee7a3d701a6835450a513a336e9835d20750036374e27c259b2d5": {
"source": "Tree/ymir work/tree/n2_joshuatree_rt_03.spt",
"source_sha256": "a89a40ac942ee7a3d701a6835450a513a336e9835d20750036374e27c259b2d5",
"glb": "a89a40ac942ee7a3d701a6835450a513a336e9835d20750036374e27c259b2d5.glb",
"glb_sha256": "5964a02431360700dabd0f88cddde3419141f9f2626b48068b80a6da4110ddf6",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 6,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"19685a989e47f21d14fb9c2f2e71fd86930f8841a3dafae42b608988c3317119": {
"source": "Tree/ymir work/tree/n2_palmetto_rt_01.spt",
"source_sha256": "19685a989e47f21d14fb9c2f2e71fd86930f8841a3dafae42b608988c3317119",
"glb": "19685a989e47f21d14fb9c2f2e71fd86930f8841a3dafae42b608988c3317119.glb",
"glb_sha256": "6fc27607202b42a36b0cea01228e05e3ffd36fb21800ed5ba171831c8a4b7ae3",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"e167a7cb1d19c9d118be00d5dff39221695a582ec7caeabfae1f7826aff0d823": {
"source": "Tree/ymir work/tree/n2_palmetto_rt_02.spt",
"source_sha256": "e167a7cb1d19c9d118be00d5dff39221695a582ec7caeabfae1f7826aff0d823",
"glb": "e167a7cb1d19c9d118be00d5dff39221695a582ec7caeabfae1f7826aff0d823.glb",
"glb_sha256": "1597e961990b4bae697f92238b93978910c39d839f8585baaa44cda135197cde",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
},
"d096c9a78ede5fb7db97685b6dab4967f9dad9a24dbf147b762faffdf1281fc4": {
"source": "Tree/ymir work/tree/n2_palmetto_rt_03.spt",
"source_sha256": "d096c9a78ede5fb7db97685b6dab4967f9dad9a24dbf147b762faffdf1281fc4",
"glb": "d096c9a78ede5fb7db97685b6dab4967f9dad9a24dbf147b762faffdf1281fc4.glb",
"glb_sha256": "cb67edad8c58da648f2e7b19e5cce0d1ec665bb7738f0fa91ac0fd7dab1176ec",
"native_extractor": "spt_extract.exe",
"extractor_runs": 2,
"primitives": 5,
"leaves": "static_extracted_cards",
"wind": "not_baked"
}
}
}
+15 -1
View File
@@ -10,10 +10,21 @@ set -euo pipefail
cd "$(dirname "$0")"
REPO="$(pwd)"
CFG="${1:-debug}"
OUT="$REPO/build/export"
OUT="${MT_MAC_OUTPUT_DIR:-$REPO/build/export}"
APP="$OUT/mtgodot-poc.app"
GODOT="${GODOT:-godot}"
MAC_ARCHES="${MT_MAC_ARCHES:-universal}"
ENGINE_OVERRIDE="${MT_MAC_ENGINE:-}"
if [ -n "$ENGINE_OVERRIDE" ]; then
test -x "$ENGINE_OVERRIDE"
ENGINE_INFO="$(lipo -info "$ENGINE_OVERRIDE")"
case "$MAC_ARCHES:$ENGINE_INFO" in
universal:*x86_64*arm64*|universal:*arm64*x86_64*|arm64:*arm64*|x86_64:*x86_64*) ;;
*) echo "错误:自定义引擎架构不匹配:$ENGINE_INFO" >&2; exit 1 ;;
esac
echo "自定义引擎:$ENGINE_OVERRIDE"
"$ENGINE_OVERRIDE" --version
fi
BUILD_TYPE="Debug"
if [ "$CFG" = release ]; then
BUILD_TYPE="Release"
@@ -55,6 +66,9 @@ ditto "$REPO/bgm" "$APP/Contents/Resources/bgm"
test -f "$APP/Contents/Resources/assets/asset_index.txt"
echo "== 4. 签名(默认 ad-hocCODESIGN_IDENTITY 可切正式证书)=="
if [ -n "$ENGINE_OVERRIDE" ]; then
cp "$ENGINE_OVERRIDE" "$APP/Contents/MacOS/mtgodot-poc"
fi
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:--}"
ENTITLEMENTS="$REPO/macos.entitlements"
test -f "$ENTITLEMENTS"
+275
View File
@@ -0,0 +1,275 @@
# 首个 Mac 联网内测版:开发实施文档
日期:2026-09-11。范围:Apple Silicon / arm64 Release。
本文将联网闭环、基础战斗、首测地图和稳定性四项需求拆为可开发任务。本文新增的文件、API、环境变量均为**待实现设计**,不是已存在功能;列为“现有”的入口已按当前工作树核对。本轮只编写文档,不执行联网测试或修改运行时代码。
## 1. 基线与交付边界
现有候选包:`build/export-native-trees-20260911/mtgodot-poc.app`。已通过离线包内加载及干净退出;树木专项覆盖 87 种资源、六图 2092 棵树,不能代替联网或树怪验收。
首版放行不是“渲染全部完成”。必须交付:固定哈希的候选包、实际联网闭环证据、基础战斗矩阵、首测地图截图、稳定性报告和已知问题。风动、逐叶朝向、SpeedTree 原生 LOD、40250 全环境像素一致性可延期。
首测地图拟为出生城镇及练级区、鬼木林、赤鬼木林。开发前通过资源和实际服务器确定 map key、进入坐标、合法路线、角色槽位与怪物刷新;不得凭中文名硬编码地图路径。测试角色等级、装备、技能、掉落条件需由测试环境负责人准备;没有条件时报告 BLOCKED,禁止伪造事件或偷偷修改服务器。
## 2. 已有实现与明确缺口
| 现有文件 / API | 可复用部分 | 本轮必须补齐 |
| --- | --- | --- |
| `script/live_smoke_test.sh` | 指定 MT_SMOKE_APP、凭据输入、报告与进程码 | 地址预检写死;无完整退出错误门禁;缺墙钟超时和战斗流程 |
| `project/live_smoke_test.gd` | setup、状态机、世界读取、移动、聊天、reconnect | 发包成功不是服务端结果;无完整打怪拾取链 |
| `project/client_main.gd` | 包内 MT_TEST_MODE=smoke/render 分流 | 增加 playable、地图视觉入口;普通启动不启用测试 |
| `project/app_flow.gd` | start、state、登录/选角/进场和断线生命周期 | 提供只读场景快照,避免测试遍历私有节点 |
| `project/net_play.gd` | set_attack_key、pick_ground_item、skill_context | 提取公共选目标入口,沿用真实战斗逻辑 |
| `project/ui/quickbar.gd` | activate、assign、restore_from_serverskill_cast_started、skill_rejected | 输入、服务端结果与视觉证据关联 |
| `project/player_skill.gd` | use_skill、可用性与目标检查 | 新增负向/生命周期覆盖,不另造施法实现 |
| `project/ui/mob_view.gd` | build、set_anim_state、motlist 映射 | 树怪矩阵及禁止静默动作回退的验收 |
| `project/rendering_scenario_test.gd` | 离线 CPU/GPU 多角色、帧时间、内存计数 | P99、事件关联、RSS、长期联网负载;现有假客户端不是实网证据 |
| `script/package_render_test.sh` | 进程退出后扫描完整日志 | 抽取通用门禁供联网/稳定性使用 |
当前 `mob_winding_test.gd` 只覆盖 101、110、20001,不覆盖树怪。`native_tree_test.gd` 测的是静态场景树,不能复用其 PASS 证明怪物蒙皮正确。
## 3. 开发拆分与依赖
每个任务需单独可审查 diff,先添加失败用例再修复;工作树已有修改必须保留,不把无关修改混进任务结论。
| ID | 开发内容 | 依赖 | 完成产物 |
| --- | --- | --- | --- |
| INF-01 | 场景清单、报告协议、事件采集、配置校验 | 无 | 配置负测及报告协议测试 |
| INF-02 | 通用包运行器、墙钟超时、退出门禁 | INF-01 | 假进程门禁测试 |
| NET-01 | 包内真实联网状态机、只读诊断接口 | INF-01/02 | 3 次真实闭环 |
| CBT-01 | 技能/普攻输入路径与证据关联 | NET-01 | 四职业矩阵 |
| MAP-01 | 地图/树怪资源审计与离线 Metal 截图 | INF-01,可与 NET 并行 | 资源清单、动作截图 |
| MAP-02 | 实际地图定位、战斗、落地检查 | NET-01、MAP-01 | 首测地图验收 |
| STB-01 | 采样、循环场景、故障与窗口测试 | NET-01、CBT-01、MAP-02 | 2 小时报告 |
| REL-01 | 重打包、聚合门禁、已知问题 | 全部 | 可追溯内测包 |
实际运行缺陷的修复范围由证据决定;不能预先将未知 bug 标成已定位。缺陷修复卡必须写出:失败用例 ID、资源/角色/地图、原因、修改文件、修复前后证据、回归集合。
## 4. INF-01:测试基础设施的数据契约
### 4.1 新增文件
- `test/playable/scenario.example.json`:无账号密码的模板,未解析的地图/技能字段留空并拒绝运行。
- `project/testing/playable_config.gd`:验证配置、场景与资源前置条件。
- `project/testing/playable_report.gd`:统一检查、事件记录与最终报告。
- `project/testing/playable_probe.gd`:只读订阅生产对象,不发包、不写实体数据。
- `project/playable_harness_test.gd`:假客户端验证状态机和证据判定,不作为实网验收。
配置采用 schema_version=1;包含 scenario_id、protocol、serverinfo 中选定地址、character_slot、map_key、waypoints_cm、allowed_mob_vnums、allowed_drop_vnums、skill_cases、resolution、loops、timeout_seconds。所有坐标字段显式带单位,网络到世界换算沿用生产代码;不把 Vector2/Vector3 强转用于凑通过。路线必须预先人工确认安全可达。
账号密码仅通过现有 MT_ACCOUNT/MT_PASSWORD 或安全交互输入提供;禁止写入 JSON、命令行参数、截图或报告。报告不复制进程环境。启动脚本不能使用 set -x。日志在落盘前脱敏,测试账号名也不写入最终公开报告。
### 4.2 报告契约
`report.json` 必须具有如下字段:
```json
{
"schema_version": 1,
"run_id": "unique-run-id",
"suite": "playable",
"status": "BLOCKED",
"build": {"engine_sha256": "...", "extension_sha256": "...", "pck_sha256": "...", "arch": "arm64"},
"environment": {"os": "...", "renderer": "...", "resolution": [1280, 720]},
"cases": [{"id": "NET-ATTACK-01", "status": "BLOCKED", "duration_ms": 0, "reason": "fixture_not_ready", "evidence": []}],
"failures": [],
"blocked": ["fixture_not_ready"],
"coverage": {"required": 1, "passed": 0},
"exit_gate": {"checked": false, "process_code": null, "errors": []}
}
```
状态限定 PASS/FAIL/BLOCKED/SKIP;必测项 BLOCKED、SKIP 均不得放行。未知 schema、空 cases、缺哈希、证据文件不存在、required 与清单不一致均失败。客户端只能写 `client-report.json`,父运行器在真实退出后生成最终 `report.json`,防止退出前 PASS 掩盖泄漏。
事件写 `events.jsonl`,每条含 monotonic_us、run_id、case_id、connection_epoch、stage、kind、actor_vid、target_vid、payload(白名单字段)。重连后 VID 可变化,按角色身份与新 epoch 重新绑定,禁止等待旧 VID。检测“来自服务端”的依据必须是接收端分发,而非本地预测信号。
### 4.3 必须先有的负向测试
无凭据/坏配置、空覆盖、旧报告、错 run_id、报告 PASS 但进程失败、PASS 后输出 RID 警告、只有本地动画无服务端事件、重连后旧事件、超时不退出,均应返回非零。任何缺测都不能降级成 PASS。
## 5. INF-02:统一运行与退出门禁
新增 `script/run_client_gate.sh``script/validate_playable_report.mjs`,由 `script/playable_test.sh` 调用;复用 `package_render_test.sh` 的错误匹配规则,不弱化现有门禁。
1. 每次创建唯一输出目录,记录本次 run_id 和包哈希;不复用历史报告。
2. 校验包可执行、架构和签名;清除 MT_ASSETS,保证验收用包内资源。
3. 预检地址与 `project/net/serverinfo.gd` 的实际选中配置一致。新增测试配置覆盖只能在测试模式生效;删除冒烟脚本预检地址与客户端连接地址分叉的风险。
4. 只管理本运行器启动的子进程 PID;超时先 TERM,宽限 10 秒后 KILL,再 wait 收集状态。不得 pkill 所有客户端或修改全机网络。
5. 读取完整标准输出/错误,检查 SCRIPT ERROR、ERROR、leaked at exit、shaders never freed。无过滤 RID 警告的允许名单。
6. 同时满足进程 0、报告协议正确、全部必测 PASS、完整日志门禁通过,才写最终 PASS。
建议退出码:0=PASS,1=断言或退出门禁失败,2=配置/前置条件阻塞,124=墙钟超时。进程原始退出码另存报告,不丢失。
门禁单测使用脚本生成的假子进程输出,不需游戏服务器。确认超时分支、信号退出和 stdout 管道不会绕过真实进程码。
## 6. NET-01:真实联网闭环状态机
### 6.1 修改点与接口
- 新增 `project/playable_live_test.gd`,实现 `setup(flow: Node, client: Node, config: Dictionary)`
- `client_main.gd` 增加 `MT_TEST_MODE=playable` 分支,在 AppFlow 建立后挂载该 Node;不得导出后用 --script 替代包内入口。
- `app_flow.gd` 新增只读 `get_playable_snapshot() -> Dictionary`,返回 stage、角色身份摘要、scene_ready 和当前地图。测试对象引用通过明确的测试 adapter 获取,不把 Node 放入 JSON。
- `game_scene.gd` 新增只读 `get_playable_context() -> Dictionary`,在测试模式提供现有 net_play、quickbar、net_world、world、player 的引用;场景销毁时失效。
- `net_play.gd` 新增公共 `select_target(vid: int) -> bool`,复用现有 `_change_target_to_picked_instance``_on_pick` 的共同目标选择逻辑;保留距离、死亡和可攻击检查,不直接改私有字段。
先核实 M2Client 信号签名与 classic 收包源;缺少来源可辨识证据时,向现有接收分发处增加测试诊断通知,不添加假业务事件,不改变协议格式。
### 6.2 状态及判定
| 状态 | 动作 | 成功证据 | 默认超时 |
| --- | --- | --- | --- |
| LOGIN | 复用 AppFlow 自动登录 | char_list 接收且指定槽位存在 | 30s |
| SELECT | 现有选角流程 | entered_game + 主实体有效 | 45s |
| WORLD_READY | 等待场景加载 | map key 符合、地图与角色就绪、HUD 可用 | 60s |
| MOVE | 通过玩家控制器走配置路线 | 本地到达 + 无服务端纠正;重登/服务端观察验证落点 | 20s/点 |
| TARGET | select_target 选择允许范围怪物 | 当前 epoch 中存活实体、race 合法 | 30s |
| ATTACK | set_attack_key(true),结束/失败必释放 | 指定目标接收血量/伤害变化与死亡结果 | 90s |
| DROP | 观察真实掉落 | 新地面物品、允许 vnum、可拾取归属和邻近位置 | 30s |
| PICKUP | pick_ground_item(iid) | 地面消失 + 背包对应 vnum 总量增加 | 15s |
| EXIT | 正常退出入口 | 父运行器检查真实退出和完整日志 | 20s |
MOVE 不要求服务器必然回显主角每一个移动包;classic 若无回显,使用重登位置或服务器/第二观察客户端确认,不把本地 position 变化叫做服务端确认。若没有可用证据,标记该断言 BLOCKED。
拾取比较所有背包格的 vnum 总量(支持堆叠),并关联时间窗和地面 iid;只看到地面删除不能证明由自己拾取。金钱掉落另测点数,不套背包断言。无掉落的随机战斗不能无限重试:测试环境准备确定可拾取掉落,最多 3 次允许怪物击杀,无条件则 BLOCKED。
每个状态入口记录快照,失败保存截图;禁止跨状态复用旧计数。断线立即取消输入、释放目标和待处理操作。统一 teardown 断开监听,场景 free 后 probe 不再访问节点。
### 6.3 用例与完成条件
- NET-01:完整闭环 3 次,每次独立进程与报告。
- NET-02:无效账号和不存在槽位,明确错误且有界退出(限制尝试,避免账号锁定)。
- NET-03:进场期间断线,不能留下半加载操作对象。
- NET-04:目标在攻击前消失,取消操作而非继续攻击旧 VID。
- NET-05:掉落被他人拾取、背包已满,不能误报成功;需专用夹具或离线负测,实网缺条件须标出。
自动调用业务入口不等于鼠标命中正确;另做一次真实鼠标/键盘闭环,记录输入设备与截图,作为手动必测项。
## 7. CBT-01:基础战斗和技能
新增 `project/playable_combat_test.gd`(离线事件与输入断言)、`test/playable/skill-cases.json`(实际角色技能矩阵)。联网用例由 playable_live_test 调度,不再创建第二条发包路径。
### 7.1 实现与定位
1. 通过 Quickbar.assign/restore_from_server 构造测试账号实际槽位;需要持久化变更时仅对专用账号执行,记录原值并在可行时恢复。无服务端技能等级不得本地伪造。
2. 调用 activate 复用 PlayerSkill.use_skill,监听现有 skill_cast_started(skill_id,target_vid) 与 skill_rejected(skill_id,code)。cast_started 只是本地施法开始,不是服务端命中证明。
3. case_id 绑定施法时目标;施法中换目标后,定点效果仍使用原目标快照,跟随效果按技能定义更新。
4. probe 关联接收端资源/状态变化、目标血量或伤害事件;不同技能配置 required_evidence,不强制所有技能都有伤害或资源消耗。
5.`game_scene.gd``fx/skill_fx.gd``fx/effect_player.gd` 的实际生成/结束边界记录测试事件。不可只用节点数量增加判定特效正确。
6. 自动截图覆盖施法前、触发帧、峰值、结束;短命特效按事件时间采样,不统一等一秒。
诊断路由:图标/点击区域→quickbar;拒绝条件/目标→player_skill;动作触发→net_play 与玩家视图;挂点/生命周期→game_scene、skill_fx、effect_player;骨骼坐标→metin2_model/metin2_anim。只在复现后修改对应层。
### 7.2 测试矩阵
每职业从真实已学技能中选择适用类型:单体、范围、自身增益、飞行效果;不存在的类型标注不适用并经清单确认,不擅自补技能 ID。每技能成功施放 10 次;另覆盖无目标、超距、冷却、资源不足、目标死亡、自身死亡、切图/断线、目标切换。
断言包括:拒绝不发有效施法请求;正常请求数无重复;服务端效果可观察;角色持续可见且包围盒有限;特效在定义寿命+1秒宽限内清理;挂点相对预期骨骼/目标坐标误差默认不超过 0.1m(有资源偏移需记录校准值)。
`skill_test.gd``player_skill_test.gd``combat_fx_test.gd``target_effect_test.gd` 扩展相应失败用例,并纳入 rendering_batch_test。截图必须人工复核一次,像素非空不能证明技能外观正确。
完成条件:必测职业/技能覆盖完整、功能阻断清零、首次施法与重复施法均通过;高级特效差异写入已知问题,不改成假 PASS。
## 8. MAP-01/02:鬼木林、赤鬼木林与树怪
### 8.1 资源审计
新增 `script/audit_playable_maps.mjs`,输出 `map-assets.json`。读取实际 map 配置、AreaData/Property、npclist、MSM、motlist 和引用文件,分别列出静态 SPT、动态 GR2、贴图、动作及缺失项。没有服务端刷新数据时只报告候选树怪,不声称已确定地图所有怪物。
已核实 `assets/root/npclist.txt`23012307 对应 ent_trent/ent_guru/ent_hu/ent_red/ent_black/ent_huge/ent_elder23112315 对应前五种。共享模型仍保留独立 race 用例,避免漏掉 proto/外观配置差异。实际 map key 与刷新关联由前置审计填入配置。
### 8.2 单模型 GPU 验收
新增 `project/forest_mob_render_test.gd`,沿用 MobView.build、set_anim_state 和现有 Metin2Model/Metin2AnimPlayer。读取 motlist 的具体文件,报告 requested_state、resolved_motion、fallback_reason;“调用 dead 却播放 wait”不能通过。
对 12 个候选 race × CPU/GPU × wait/run/attack/damage/dead 生成基准集合;资源确无动作时据旧客户端回退规则单独判定,不能静默跳过。动作每个采样起点、25%、50%、75%、末段;固定随机种子或明确选择变体。死亡动作不能为了截图强行循环。
自动断言:构建成功;贴图实际加载;顶点/包围盒有限;无非法材质;同一姿态 CPU/GPU 包围盒一致(初始容差 1cm 或高度0.5%,取较大值);截图非空。容差变更需记录理由,不通过放宽到无限解决失败。
输出每 race 的正面、侧面、动作 contact sheet 和 JSON。检查材质双面/裁剪应依资源,不对所有树怪强制双面;蒙皮异常先检查骨骼索引、权重、bind pose、坐标,再修改 C++ 公共路径。`mob_winding_test.gd` 增加这些 race,若影响全怪物必须回归原 101/110/20001。
### 8.3 地图内验收
新增 `project/forest_map_render_test.gd` 和包内 `MT_TEST_MODE=forest_render` 分支,加载配置指定的生产地图。新测试模式不存在于旧包,必须重新构建后执行。
在经确认的平地、坡地、密林和传送点保存固定机位截图,记录 world.sample_height、怪物世界位置、ground offset、材质和实际动作路径。平地不允许持续悬空/埋地超过校准阈值;坡地需人工检查脚/根部接触,AABB 底部不是精确足部,不能用它宣布足部 IK 完成。
实网追加逐地图进入、选中树怪、攻击、死亡、拾取、离开。静态地图截图与动态服务器刷新分别记录证据,不混为一次测试。
完成条件:首测区域关键资源无未解释缺失、主要动作和目标交互正常、Metal 截图人工签核;其余地图不在本轮覆盖中。
## 9. STB-01:稳定性与长帧
新增 `project/testing/playable_metrics.gd``script/playable_soak.sh`,扩展 rendering_scenario_test 的 P99 与事件时间点,但保留“离线合成”和“真实联网”两种 suite 标签。
### 9.1 指标实现
- 每帧记录 monotonic 间隔;按 map/phase/actor_count/gpu_skin/动作切换标记。加载与游玩分段统计。
- 固定大小环形缓冲,按时间窗口输出分位数和 >50ms/>100ms 样本;不得无限数组增长制造测试自身内存泄漏。
- 默认关闭逐帧打印,只保留长帧事件;采样开启/关闭各跑短基线,量化探针开销。
- 外部采样父运行器所持 PID 的 RSS,每秒一次,记录单位 KiBGodot MEMORY_STATIC 单独记录。GPU 内存拿不到写 null/unavailable,不填写 0 或据此宣称无显存泄漏。
- 缓存预热后比较重复路线每轮结束并静置30秒的低水位;记录前5轮与后5轮中位数。连续5轮增长且后5轮中位数高出 max(50MiB,5%) 时门禁失败并分析;阈值以下也不能证明长期无泄漏。
### 9.2 执行场景与边界
2小时实网循环移动/战斗/拾取;20次经合法传送入口切图;10次重连;3种受支持逻辑分辨率各切10次;10次正常退出。窗口物理像素与逻辑尺寸都记入报告,不能把 Retina 缩放当成分辨率错误。
主动 reconnect() 与真实传输故障分别计数。真实故障至少覆盖“连接被关闭”和“短时不可达”;仅测试专用连接,可使用经确认的局部代理/测试网络条件,不改全机路由、防火墙、不关生产服务。缺少该条件标为 BLOCKED,不用主动重连替代断网验收。
时间采用墙钟期限,不用固定帧数冒充2小时。重连后重新获取主 VID、场景和 UI 对象,检测重复信号、重复实体、旧目标/技能残留。跨服切图需核实测试服务器支持,失败不能通过直接传送客户端节点掩盖。
### 9.3 长帧处理流程
先标记同步模型 build、动作 reload、GR2解析、材质初次使用和实体批量生成;记录缓存命中。确认瓶颈后才做路径级修改:动画缓存→metin2_anim;实例建立→net_world/game_scene;材质预热→实际渲染建立路径。后台工作只做线程安全的解析,不把 SceneTree/RenderingServer 操作随意搬入工作线程。
相同机器/路线对比修改前后3轮,保留最大帧而非只报平均。正常游玩目标60FPS;同一交互3次重复中至少2次出现 >100ms 视为阻断,任何 >50ms 样本需归因。首次加载独立预算在基线采集后固定,不临时扩大预算让门禁通过。
完成条件:零崩溃/永久卡死、循环后功能正常、无达到上述阈值的未解释内存增长、无重复严重长帧、每次退出干净。冷启动报告注明进程冷启动/系统缓存状态未知,不执行系统缓存清空来影响用户其他程序。
## 10. 执行命令与验证顺序
以下命令在仓库根目录执行。账号通过安全交互提供,禁止放入命令历史。新脚本落地前不得宣称这些未来命令已可运行。
现有可执行回归:
```bash
bash script/rendering_batch_test.sh
MT_RENDER_APP="$PWD/build/export-native-trees-20260911/mtgodot-poc.app" bash script/package_render_test.sh
```
待实现 CLI 契约(各脚本必须有 --help、配置校验、非零失败码):
```bash
# 离线资源/蒙皮/报告门禁,不连接服务器
node script/audit_playable_maps.mjs --config test/playable/scenario.local.json --output build/playable/map-assets.json
godot --headless --path project --script playable_harness_test.gd
godot --path project --script forest_mob_render_test.gd
# 重新构建带新包内测试入口的候选包,不能用旧包冒充
MT_MAC_ARCHES=arm64 MT_MAC_ENGINE="$PWD/build/godot-particle-diagnostic/bin/godot.macos.template_release.arm64" MT_MAC_OUTPUT_DIR="$PWD/build/export-playable-rc1" bash build-macos-client.sh release
# 明确显式允许专用账号上的游戏操作;默认仅校验配置,不发移动/战斗请求
bash script/playable_test.sh --app "$PWD/build/export-playable-rc1/mtgodot-poc.app" --config test/playable/scenario.local.json --suite full --allow-gameplay --repeat 3
bash script/playable_soak.sh --app "$PWD/build/export-playable-rc1/mtgodot-poc.app" --config test/playable/scenario.local.json --allow-gameplay --duration-seconds 7200
node script/validate_playable_report.mjs --release-dir build/playable/rc1
```
新脚本参数还需支持 --output;聚合器只接受显式列出的本次 run_id 与相同包哈希,不扫描历史 PASS 凑覆盖。测试模式、测试账号均禁止自动聊天、交易、出售、丢弃物品或创建/删除角色;不沿用旧 smoke 的聊天副作用。
配置 local.json、凭据文件、运行产物加入适当忽略规则;提交无敏感信息的 example/schema。最终修复 C++ 后必须重建同步扩展、重新签名和打包;脚本变更也必须重新导出 PCK。
## 11. REL-01:放行检查表
- [ ] INF-01/02 负向用例通过,必测项缺失不能 PASS。
- [ ] 同一候选包3轮联网闭环通过,另有鼠标键盘流程证据。
- [ ] 四职业基础战斗、技能及异常路径清单完整。
- [ ] 首测地图和树怪资源/动作/Metal画面验收通过。
- [ ] 2小时、切图、故障恢复、窗口缩放、退出门禁达到配置次数。
- [ ] P0(崩溃/数据异常/无法进场)与 P1(核心操作失效/主要模型丢失/严重错位/重复严重卡顿)清零。
- [ ] 所有 BLOCKED 明确处理;首测必测项不得以“已知问题”绕过。
- [ ] 包哈希、签名、引擎来源、设备信息与报告一致。
- [ ] 已知 P2、未覆盖地图/装备/技能及未验收平台明确列出。
官方 Release 模板仍有粒子退出警告;必须显式使用验证过的自编译 arm64 引擎,并对最终包重新跑退出门禁。不将当前方案表述为已修复所有官方模板或 Intel 平台。
完成文档不等于完成上述开发。开发起点是 INF-01/02;它们完成后 NET-01 与 MAP-01 可并行推进。工期在首次真实闭环和树怪截图产生后根据实际缺陷估算。
+300 -1
View File
@@ -1,6 +1,8 @@
# 新客户端渲染修复实施路线
更新日期:2026-09-08。目标客户端:本仓库 `metin2-client`,第一交付平台为 macOS。
更新日期:2026-09-09。目标客户端:本仓库 `metin2-client`,第一交付平台为 macOS。
最新集中交付见文末「集中修复、整套回归与 mac 实包验收」:已生成新 universal mac 包,独立资源加载与只读真实登录通过;剩余视觉保真和性能项未宣称完成。前文表格及旧执行记录保留历史状态,以最新记录为准。
本文是实施规格,2026-09-08 已开始第一批 R0/R1 修改;阶段结果见文末执行记录。未标明通过的工作仍待执行,不代表整体渲染或最终包完成。
@@ -369,3 +371,300 @@ godot --path project --script model_render_test.gd # PASS
不在这次修复范围内。
- R2-01 的 GLB 对照、R0 完整视觉基线与区域阈值、R4 选角/快捷栏、R5 最终包验收、R6 性能,均未开始。
### 2026-09-08 后续复核:种族与动画资源配套
- 修复 `PlayerView.build()` 用性别选择 pc/pc2 的错误。资源中的 `playersettingmodule.py` 明确规定 race 0..3 使用 pc、4..7 使用 pc2;女性刺客/萨满属于 pc。原实现使 race 1/3/5/7 初始模型性别错误,EquipModel 从 MSM 恢复正确身体后仍保留另一性别动作。现在按 race 范围选择同一资源族;对应资源缺失时返回构建失败,避免跨性别回退。
- 新增 `race_motion_assembly_test.gd`:以真实 MSM 换装后的身体目录为独立依据,检查身体、发型与动作资源配套,覆盖八种族 × 六动作(wait/walk/run/attack/damage/dead)× 三个时间点。重新构建实际加载的扩展后运行结果 `failures=0``race_spec_test.gd` 通过。有限包围盒断言不等于完整动作画面或骨骼精度验收。
- 通过 `bash script/rendering_test.sh` 重建并同步 debug 扩展;CPU 绕序与选角装配通过。旧本地库复现已修复的约一米悬空,重建后 `model_render_test.gd` 的八种族足底、剪影下限和材质负对照绝对断言通过。
- **像素回归仍失败,未更新基线**:`model_render_test.gd` 的 head_cover、silhouette_px、hair_mean_lum 与保存基线不一致。种族 1/3/5/7 原基线含错误性别资源;其余种族亦存在亮度或剪影差异,须继续排查合并后材质/绕序和基线生成条件。当前扩展 SHA-256:`b682582b2c96d7b9bd005d75ac781dde8d35fbe309ca01792d5ae3c5f35c5409`。不得沿用旧“八职业 PASS”作为当前代码像素验收结论。
- 剩余:像素基线差异定位、装备/发型组合与完整动作视觉验收、GLB 对照、坡地/台阶、R3 世界与光照、R4 UI 联机恢复、R5 最终包、R6 性能。上述新修复尚未在最终导出包或真实登录中复验。
### 2026-09-08 像素基线差异闭环
- 确认基线来自合并前 `e0c18539`:其 CPU 蒙皮没有应用 flip_winding。诊断参数 `--legacy-winding` 恢复旧索引顺序后,race 0 对旧基线 PASShair_mean_lum=0.060864(基线 0.060796),head_cover=0.324646(基线 0.324674)。这解释了合并后双面头发的光照/遮挡变化;race 1/3/5/7 还受已修正的性别资源映射影响。
- 人工检查八种族正面和侧面近景:模型性别、脸和发型正确,无先前浅色三角斑。保留当前正确绕序与资源映射,用生产路径重建 `model_baseline.json`,未放宽任何像素阈值。近景为固定局部采样,部分头顶出框,不能据此宣称完整头部多角度验收。
- `model_render_test.gd` 冻结采样时关闭动作过渡,报告/基线增加身体和发型 SHA-256;回归检查资源哈希。仅在全部绝对断言通过、八种族生产模式运行时允许写基线,禁止单种族或旧绕序诊断覆盖它。
- 重建报告:`build/rendering/modeltest-1788861134/report.json`。三次独立复测:`modeltest-1788861136`、`modeltest-1788861139`、`modeltest-1788861142`,均 PASS。旧绕序对新基线负对照失败,证明测试仍可检测旧行为。以上仅为当前 CPU 基础模型单角色场景;GPU、换装和最终包仍待独立验收。
### 2026-09-08 GPU 发型切换与选角朝向
- 修复 `_load_hair()` 后 GPU 合并网格未刷新的问题:换发型/颜色与移除发型现在重建网格、重新分配身体/头发材质,并更新当前姿势包围盒。CPU 移除发型也即时刷新,不依赖下一动画帧。GPU 发型采用与 CPU 一致的双面材质。
- `character_winding_test.gd` 增加暂停动画期间移除/替换头发、纹理像素比对、双面材质与身体不误分类检查。CPU/GPU 均 failures=0。重新构建扩展后八种族 CPU 像素回归 PASS,基线未改。
- 实景发现 InitialPlacement 修正后,选角仍保留旧 90 度补偿,导致人物侧向镜头;现移除该补偿。男战士 5th 护甲 + HairIndex 1001 GPU 截图 `build/rendering/selection-1788861936-4253/selection.png` 人工复核正面与完整入框;CPU 装配截图报告 `selection-1788861960-4277` PASS。
- 本轮不代表完整 GPU 动作/LOD、多职业高级装备或最终导出包验收通过;这些仍需继续覆盖。
### 2026-09-08 GPU LOD 附件丢失修复
- 确认 `_set_lod()` 在 GPU 路径仅调用身体 `build_mesh()`,切远景时丢掉合并头发,回到 LOD0 仍不恢复,且保留旧身体/发型材质分界。现在 GPU LOD 使用 `_build_gpu_mesh()`,重建身体与头发后刷新材质和当前姿势包围盒。
- 新增 `gpu_lod_attachment_test.gd`,使用实际相机距离驱动八种族 LOD 0→1→2→3→2→1→0。检查实际 LOD、头发表面/顶点、alpha-test 和骨矩阵纹理。修改前 144 项失败;修改后 headless 与 Metal 实际渲染后端均 failures=0。这是网格/材质状态验收,不是动画全周期像素或剔除边界验收。
- `bash script/rendering_test.sh` 构建成功;GPU `character_winding_test.gd` 通过;八种族 CPU `model_render_test.gd` PASS,未调整基线。实际加载库 SHA-256:`306265206b3a5cf5c8066d908613a1df3cf17074c2bc44ebee71ac2f7cd450bd`。
- 仍需:GPU 完整动作的动态剔除包围盒、LOD 淡入淡出视觉检查、多职业高级装备、坡地与最终包复验。
### 2026-09-08 GPU 动态剔除范围修复
- 原 GPU 路径只在首帧/LOD/换发型时更新剔除盒;后续跑步、攻击、死亡沿用旧姿势范围。现在每次 `gpu_skin()` 采样同步当前身体和发型包围盒。首次就绪信号仍只触发一次,避免选角镜头随待机呼吸不断重新取景;取景查询直接读取已更新的结果。
- 新增 `gpu_pose_bounds_test.gd`,八种族 × 六动作 × 五个时间点(含非循环末帧),将 GPU 实际 `custom_aabb` 与同姿势 CPU 网格包围盒比较,不调用会刷新范围的取景接口。修改前 195/240 采样失败,修改后 headless 和 Metal 均 failures=0GPU LOD 附件回归亦通过。
- 构建入口 `bash script/rendering_test.sh` 通过;库 SHA-256`8e7d635dbb9b407d8b41b61c9e73330e12ecece6033ab1b3a8885f796e24a32d`。包围盒计算增加逐采样 CPU 顶点遍历成本,尚未测量多角色帧时间,GPU 仍属实验模式;后续可在保留此正确性对照的前提下优化。
- 覆盖限于基础人物和默认发型的上述离散采样;骨骼变形像素精度、混合动作、LOD 过渡残影、坡地、高级装备与最终包仍待验收。
### 2026-09-08 LOD 淡出残影快照修复
- `_set_lod()` 的 GPU 淡出模型此前与主模型共享骨矩阵纹理,不能保持切换时姿势,且未复制动态剔除盒。现保存独立纹理快照与旧姿势 custom AABB,后续动画采样不再改变淡出模型。
- 旧模型材质改为逐表面复制实际 active material,避免 CPU 头发单独设置的材质不在 surf_mats 缓存中而丢失。
- 扩展 `gpu_lod_attachment_test.gd`:验证纹理隔离、后续采样不修改快照、剔除盒及逐表面材质。在旧库上 48 次淡出检查共 96 项失败;重建后 headless 与 Metal 均 failures=0、ghosts=48。构建、CPU 绕序/发型切换与选角装配亦通过。
- 尚未完成逐帧淡入淡出视觉差异验收;CPU 淡出材质修复尚缺专门快照回归。坡地、高级装备与最终包保持待验。
### 2026-09-08 技能特效透明混合与静止粒子生命周期
- 对照 40250 `ClientVS22/extern/include/d3d8types.h`D3DBLEND_SRCALPHA=5、INVSRCALPHA=64 实为 INVSRCCOLOR)。`EffectPlayer._blend()` 原先将 5/6 回落为 ADD,现映射 MIX,供粒子与网格特效共用。真实萨满 `7budongbakbu_throw.mse` 使用 5/6;此修复避免普通透明特效被误当作叠加发光。其他混合对仍保留原有近似,不代表全部 D3D 混合已还原。
- 去除 `_advance_particle()` 在位置轨道为空时的提前返回:静止粒子同样累计时钟、遵守 StartTime、非循环与有限循环结束条件;无位置轨道不移动节点,无限循环保持原行为。
- 新增 `effect_render_regression_test.gd`:修前 5 项失败,修后 failures=0;覆盖材质构建、无位置轨道的延迟/有限循环/非循环/无限循环,以及真实萨满 MSE 解析后的透明材质。headless 和 Metal 均通过状态断言;本测试不采集画面,不等于技能像素验收。真实资源加载测试退出时有 1 个 ObjectDB 泄漏警告,来源待定位,不能宣称资源释放验收通过。
- `skill_fx_test.gd`、`player_skill_test.gd`、`damage_effect_test.gd` 全部 PASS`git diff --check` 通过。本轮仅修改 GDScript,未重建扩展、导出 mac 包或联机施法验收。
#### 当前剩余修复 / 验收清单(历史记录中的“未开始”以本节为准)
- 技能:停止/重播时序、RGBA 寿命曲线与重复颜色乘法已修复并补回归(见后续记录);其余混合因子、粒子发射/速度/尺寸近似、贴图动画、骨骼挂点与动作事件同步,尚需对照 40250。缺四职业多等级技能的实机逐帧视觉验收。
- R0:40250 同资源、同机位、同动作的配对截图及区域差异验收;当前本地 CPU 回归基线不是 40250 等价证明。
- R1/R2:切角色/重连/切图边界、高级装备与武器/发型组合、混合动作、坡地/台阶、NPC/怪物、GPU 像素精度和 GLB 同资产对照。LOD 残影已有快照修复,但仍缺逐帧视觉与 CPU 专项回归。
- R3:角色偏暗、环境光/雾/阴影、地形混合与接缝、建筑和树木材质。
- R4:完整选角界面对齐,技能栏实际施法/冷却/拖放/翻页/重连恢复与多分辨率验收;不应将已修复的快捷栏遮挡记为完全未做。
- R5:包含这些新修改的最终 mac 客户端打包、独立启动与真实登录验收。
- R6:多角色性能、缓存和资源释放;GPU 每姿势包围盒的 CPU 遍历成本尚未量化,保持实验开关。
### 2026-09-08 技能停止与重播时序修复
- 原延迟 SceneTreeTimer 在 `stop()` 后仍启动粒子,原一次性回收计时器会删除后续重播实例;原 `_process()` 还会把 stop 隐藏的灯光重新显示。改为播放进度统一驱动粒子启动和一次性回收,移除独立定时器;停止后不再推进 CPU 状态。
- 构建粒子时关闭默认发射,`play()` 即时遵守 StartTime。重播重置粒子启动标记、位置与源循环模式,并重置网格/灯光时钟和网格帧/贴图帧缓存,重新采样零时刻。此前 `play(true)` 的 one_shot 标志不再污染后续 `play(false)`。
- `stop()` 关闭发射并隐藏网格/灯光,已生成粒子允许自然结束;取消自动回收,实例由拥有者释放或再次播放。这不是暂停/恢复接口。正常一次性播放仍按播放时钟回收;场景暂停或禁用处理不会消耗 CPU 播放时长。
- 新增 `effect_playback_test.gd`:修前 13 项失败,修后 headless 与 Metal 均 failures=0。覆盖停止后延迟回调、旧回收与新重播隔离、网格首帧与灯光复位、循环模式恢复、当前一次性回收,以及实际 SceneTree 帧驱动下延迟启动/停止稳定性。网格使用合成双帧夹具;不代表真实 MDE/IFL 逐帧画面验收。
- `fx_test.gd` 灯光测试补显式 `play()`(与生产调用顺序一致)。`fx_test`、`combat_fx_test`、`skill_fx_test`、`player_skill_test`、`damage_effect_test` 及上一轮 `effect_render_regression_test` 均通过。现有真实资源/战斗测试仍分别出现 ObjectDB 退出泄漏警告,留待 R6 定位;新播放测试无此警告。`git diff --check` 通过。
- 本轮未修改 C++、未提交、未打包或联机施法验收。上节 R0–R6 剩余项保持待办,下一步优先检查技能颜色曲线、混合与挂点同步,并建立四职业真实施法画面对照。
### 2026-09-08 技能 RGBA 寿命曲线修复
- 对照 40250 `Source/EffectLib/ParticleSystemData.cpp`:参考实现合并 R/G/B/A 四通道关键帧时间,在并集处逐通道线性求值;`ParticleInstance.cpp` 使用粒子已消耗寿命比例。原 Godot 实现将 RGB 固定为各轨道末值,仅 Alpha 随寿命变化,丢失中间变色。
- Godot 4.7 `scene/resources/particle_process_material.cpp` 的生成着色器使用 `parameters.color *= texture(color_ramp, vec2(lifetime))`。此前同一 RGB 写入基础颜色和渐变,导致通道平方(如 0.5→0.25)。现在基础颜色设白色,仅在完整 RGBA 渐变中存储颜色。
- `_color_ramp()` 合并所有通道关键帧并保留线性插值;无轨道默认 1,首尾延续最近值。对无序/重复/非有限数据做归一化,重复时间保留最后值;有效资源的寿命归一化时间语义不变。未改变混合模式或放宽现有像素基线阈值。
- 新增 `effect_color_test.gd`:旧实现 8 项失败,修复后通过。覆盖不同通道独立拐点、常量非白色、无 Alpha 时的 RGB 动画、缺省及异常数据。另解析 warrior `palbang_4_sword`、assassin `beam_blow`、sura `pabeopsul_4_blow`、shaman `noejeon_making_1` 四份真实 MSE,把颜色轨道经过材质构建后与独立标量插值对照,共 21 组粒子 × 41 时间点 = 861 采样,headless / Metal 均 failures=0。
- 真实资源测试有意只装配颜色轨道,未加载原贴图或发射几何;测试检查 Gradient 与材质配置,不是最终 GPU 粒子像素、色彩空间、纹理采样精度或 40250 8-bit 颜色量化等价验收。
- `effect_playback_test`、`effect_render_regression_test`、`fx_test`、`skill_fx_test`、`player_skill_test`、`damage_effect_test`、`combat_fx_test` 均通过;部分既有资源/战斗测试的 ObjectDB 泄漏警告仍在,新颜色测试无泄漏警告。`git diff --check` 通过。
- 剩余优先项:其他 D3D 混合/ColorOperationType、发射方向/速度/尺寸/重力等时间曲线、贴图逐粒子动画、技能挂点与动作时序,以及四职业多等级真实施法对比。R0 配对基线、R1/R2 高级装备/坡地/混合动作/GPU/NPC、R3 世界光照材质、R4 UI、R5 最终 mac 包、R6 性能与泄漏仍未完成。本轮未提交、未打包、未联机验收。
### 2026-09-08 粒子宽高与寿命缩放修复
- 对照 `ParticleSystemInstance.cpp` / `ParticleInstance.cpp`TimeEventSizeX/Y 是出生时的独立半宽/半高,顶点沿两个方向分别加减 half-size × ScaleX/Y。旧实现用 SizeX 单值和 1×1 Quad,忽略 SizeY,还加入 0.7–1 的随机缩放与强制最小尺寸。
- 现 Quad 完整尺寸为各轴 half-size × 2 × 0.01 米,统一随机缩放设 1,零尺寸不再强制放大。使用 Godot 支持的 CurveXYZTexture 保留独立 ScaleX/Y,Z 为 1;线性切线替换默认平滑插值,曲线范围随数据确定,不再固定到 0–2。缺失缩放轨道采用常量 1,而不是自动缩到零。
- **范围限制**:SizeX/Y 的发射器时钟采样仍取末值,不是每个粒子出生时采样;本轮修复独立轴与静态尺寸,不声称出生尺寸时间曲线已还原。
- 真实 `palbang_4_sword` 含同时间不同缩放值。`Type.h` 的 GetTimeEventBlendValue 在命中重复时间时取第一值,之后从最后值插值。现保留两侧,用最多 0.0001 归一化寿命的窄过渡近似跳变(纹理无法表达零宽跳变);GPU LUT 还会有采样平滑。跳变像素仍待验。此前颜色轨道的重复时间去重策略也应进一步核对,不能视为完整突变语义一致。
- 新增 `effect_scale_test.gd`:修前 5 项失败,修后 headless / Metal 均 failures=0。合成用例覆盖独立宽高、去随机缩放、线性插值、超过 2 的缩放、零尺寸与缺省;四职业真实 MSE 共 21 组粒子、130 个关键帧/中点检查通过。仅跳变后的中点允许 <0.001 缩放值误差以容纳上述窄过渡,其余仍使用近似相等;未修改既有人物像素阈值。真实测试仅装配尺寸/缩放,不加载原贴图,也不是施法截图验收。
- 颜色、播放时序、透明混合、fx/skill/player_skill/damage/combat 回归通过。既有资源与战斗测试退出泄漏警告仍存在。未修改 C++、未提交、未打包、未联机施法验收。
- **当前未完成**:技能出生尺寸与方向/速度/重力等时变参数、其他混合/颜色操作、重复关键帧精确行为、逐粒子贴图动画、挂点与动作同步;R0 同机位 40250 对比;R1/R2 高级装备/混合动作/坡地/NPC/GPU 精度;R3 光照偏暗与世界材质;R4 选角和技能栏完整交互;R5 最新 mac 包和登录验收;R6 多角色性能与资源泄漏。
### 2026-09-08 粒子恒速旋转与 billboard 修复
- 核对 EffectLib `ParticleSystemData.cpp`CW 使用 RotationSpeedCCW 使用负值,无额外随机减速;`ParticleSystemInstance.cpp` 读取 RotationRandomStartingBegin/End。Godot 4.7 ParticleProcessMaterial 恒定角速度属性同样使用度/秒,着色器内部再转弧度。去除提前 deg_to_rad 和恒速模式的 0.3–1 随机倍率,恢复初始角度范围;NONE 不自动旋转,但仍保留初始角度。
- 相机朝向的粒子(BillboardType=1)改为 BILLBOARD_PARTICLES,使绘制着色器读取 INSTANCE_CUSTOM.x 的角度;普通 mesh 保留 BILLBOARD_ENABLED,不读取不存在的粒子 custom data。
- 核对 `EffectLib/Type.h`:固定 Y 轴为 2,旧映射错误地使用 4,现修正粒子/网格共用映射。粒子 3=LIE、4=2FACE、5=3FACE 仍近似为普通相机朝向,未实现原几何与方向;Y 轴及无 billboard 模式下的粒子自旋也仍需独立实现/视觉验证。
- 新增 `effect_rotation_test.gd`:初版负对照修前 8 项失败,修后通过;补测正确 Y 轴枚举、mesh 不受粒子路径影响,以及真实萨满 `7budongbakbu_throw` 中 2 组恒速粒子,headless / Metal 均 failures=0。真实测试只检查材质/旋转参数,不加载原贴图,不是最终旋转方向或逐帧像素验收。
- 颜色、尺寸、播放、透明混合、fx/skill/player_skill/damage/combat 回归均通过。已有资源/战斗测试仍有退出泄漏警告,新旋转测试无该警告。`git diff --check` 通过。
- **仍未完成**TIME_EVENT 转速仍取末值,RANDOM_DIRECTION 仍未按原实现逐粒子选择等幅正/反转;铺地/交叉面/拉伸 billboard、出生尺寸与运动时间曲线、其他混合/颜色操作、贴图动画、挂点同步与实机技能对比。R0 配对基线、R1/R2 高级装备/坡地/GPU/NPC、R3 世界光照材质、R4 UI、R5 最新 mac 包、R6 性能和泄漏继续待办。本轮未提交、未打包、未联机验收。
### 2026-09-09 TIME_EVENT 粒子转速曲线
- 将 RotationType=1 从取 TimeEventRotation 末值改为 CurveTexture 寿命采样,保留正负速度、过零与末帧停止;缺失轨道默认零,不使用 RotationSpeed 回退。共用线性曲线构造新增 fallback 参数,尺寸曲线仍默认 1,不改变上一轮尺寸行为。
- 对照 EffectLib `CRotationDecorator` 的 speed × elapsed 积分,以及 Godot 4.7 ParticleProcessMaterial 生成着色器:带 angular_velocity_curve 时,纹理值 × 基础速度 × DELTA 直接累积至弧度 CUSTOM.x;无曲线恒速分支另做 degree_to_rad。因此曲线保留资源的度/秒值、基础乘数固定 PI/180;恒速模式继续使用度/秒,不混用两条路径。此适配与 Godot 4.7 实现绑定,升级引擎应重新核对。
- 扩展 `effect_rotation_test.gd`,修前新增两项曲线存在性断言失败,修后通过。合成 180→-180→0 曲线验证过零、负速度、末帧归零,以及两秒寿命加初始 30° 后积分为 -60°;空轨道为零。真实萨满 jeungsok_blow、8chojeonbu、jeungryeok_hand 覆盖 3 组 TIME_EVENT / 3 个关键帧(这些真实样本各为单关键帧,变化曲线由合成用例覆盖)。headless 与 Metal 均 failures=0。
- 测试验证配置、曲线采样与 CPU 数值积分,不直接读取 GPU 粒子角度;不代表实际逐帧积分误差、GPU 曲线纹理采样精度或 40250 画面方向已验收。原始多变速真实技能与长时间/多帧率视觉回归仍待补。
- 尺寸、颜色、透明混合、播放时序及 fx/skill/player_skill/damage/combat 回归通过,`git diff --check` 通过;既有资源/战斗测试退出泄漏警告仍在。未提交、未打包、未联机验收。
- **当前剩余**:随机等幅正反转、铺地/交叉面/拉伸及非相机朝向粒子自旋;出生尺寸与方向/速度/重力时间曲线;其他混合/颜色操作、重复关键帧精确行为、逐粒子贴图动画与挂点同步;R0 40250 配对基线,R1/R2 高级装备/混合动作/坡地/NPC/GPU 像素,R3 光照偏暗与世界材质,R4 选角/技能栏交互,R5 最新 mac 包和真实登录,R6 性能与泄漏。
### 2026-09-09 特效双面显示与深度状态
- 核对 `EffectLib/EffectInstance.cpp::OnRender()`:粒子与网格效果共同使用 D3DCULL_NONE、ZWRITEENABLE=FALSE。现 `_build_particle()` 和 `_mesh_material()` 显式设置 CULL_DISABLED、DEPTH_DRAW_DISABLED;保留默认深度测试,不影响角色、地形、建筑材质。
- 新增 `effect_surface_test.gd`:用实际构造的粒子/网格材质绘制白色静态 Quad,Metal 分别从正面和背面读取中心像素,再放入不透明黑色物体确认遮挡。旧代码 4 项材质状态断言和 2 项背面像素断言失败,共 6 项;修后 failures=0,两个材质的双面可见及双向世界遮挡共 8 项像素检查通过。headless 仅运行状态断言,显式跳过像素检查。
- 测试使用白色替代纹理、静态 Quad 和两个机位,隔离材质剔除行为;未覆盖真实 MDE 几何、动态粒子、透明物体排序、最终游戏画面。深度写入由属性断言验证,未把原默认透明材质描述为必然写入深度。
- 旋转、缩放、颜色、透明混合、播放时序及 fx/skill/player_skill/damage/combat 回归均通过;已有资源/战斗退出泄漏警告仍在,新表面测试无该警告。`git diff --check` 通过。本轮未提交、未打包、未联机验收。
- 材质调查记录:assets 中 ColorOperationType 计数为 2:5、3:12、4:5596、5:354、6:6、8:1(字段出现次数,非去重技能数)。当前 StandardMaterial 主要对应 4 的调制逻辑,其他颜色操作仍未实现;特别是 5/6 的加倍调制不能仅通过修改混合模式替代。本轮未改变这些颜色操作,留待独立着色器及像素回归。
- **仍未完成**:上述其他颜色操作/混合、随机正反转、铺地/交叉面/拉伸 billboard、运动与出生尺寸时间曲线、贴图动画/重复关键帧精确行为、技能挂点同步与真实施法对比;高级装备/混合动作/坡地/NPC/GPU 画面;角色偏暗及世界光照/地形/建筑植被材质;选角与技能栏完整交互;40250 配对基线、最终 mac 包与登录验收、多角色性能和资源泄漏。
### 2026-09-09 粒子双倍/四倍颜色调制
- 实现 D3DTOP_MODULATE2X(5) / MODULATE4X(6)。新增 `fx/particle_color_material.gd`RGB 为 clamp(texture.rgb × particle.rgb × 2/4, 0, 1),之后交给 framebuffer 混合;Alpha 仍为 texture.a × particle.a,不随 RGB 倍率放大。操作 4 保留既有 StandardMaterial 路径,未改变其他颜色操作的近似。
- 新材质复用现有混合模式选择(mix/add),保留粒子旋转、相机 billboard、固定 Y 和独立轴缩放;双面绘制、不写深度但保留深度测试。铺地/交叉面等原有近似未因此还原。着色器按混合模式缓存,仅粒子操作 5/6 进入新路径,网格特效材质不受影响。
- 新增 `effect_color_operation_test.gd`:临时恢复原材质选择作负对照,5 项配置/真实资源断言失败;恢复修复后通过。真实 `noejeon_4_blow` 中一组 operation=5、Y billboard 粒子正确选择新材质(真实检查不加载原纹理)。
- Metal 像素检查:操作 4/5/6 × mix/add 共 6 组,用白纹理、已知顶点色及 Alpha=0.5、黑背景绘制。检查 RGB 饱和发生在透明混合之前且 Alpha 不放大;将输出 sRGB 转线性后,RGB 向量误差 <0.025,全部通过。白纹理隔离颜色运算,不覆盖真实非白贴图色彩空间、彩色背景或 40250 非线性色彩管线等价性。
- 扩展 `effect_surface_test.gd`,加入两种新材质的正反面可见和世界遮挡;四种材质共 16 项 Metal 像素断言通过。headless 仅验证颜色操作配置,明确跳过像素。动态 GPUParticles 的旋转/缩放与复杂透明排序仍需场景级画面验收。
- 颜色、尺寸、旋转、透明混合、播放时序与 fx/skill/player_skill/damage/combat 回归均通过,`git diff --check` 通过。已有资源/战斗退出泄漏警告仍在,新颜色操作/表面测试无该警告。未提交、未打包、未联机验收。
- **当前未完成**SELECTARG1/2、ADDSIGNED 等其他颜色操作及更多混合对;随机正反转、铺地/交叉面/拉伸、出生尺寸和运动时间曲线、贴图逐粒子动画、关键帧突变精度与挂点同步;R0 40250 同机位基线;R1/R2 高级装备/混合动作/坡地/NPC/GPU 像素;R3 角色偏暗与世界材质光照;R4 选角/技能栏完整交互;R5 最新 mac 包/登录;R6 多角色性能和泄漏。
## 2026-09-09 集中修复、整套回归与 mac 实包验收
### 本轮已完成的代码修复
1. **补齐现有资源中的颜色操作**SELECTARG1(2) 取粒子色、SELECTARG2(3) 取纹理色、ADDSIGNED(8) 为两色相加减 0.5RGB 饱和后混合,Alpha 仍单独相乘。与已有 4/5/6 共覆盖此前资产调查发现的六类 ColorOperationType;不等于所有 D3D 混合因子已支持。扩展白/彩色纹理 × 六操作 × mix/add 共 24 项 Metal 像素检查,保留原误差阈值。
2. **发射生命周期**EffectPlayer 的 one_shot 仅控制特效实例回收,GPUParticles3D 不再用一次粒子寿命终止发射。短寿命粒子在 MSE 发射窗口内持续补充,窗口结束仍停止。新增实时后端测试:粒子寿命 0.05s、发射窗口 0.8s,在 0.3s 仍发射,0.9s 后停止。
3. **附着粒子**:读取原字段 AttachEnable,映射 local_coords,区分粒子随发射器移动和留在世界空间。该配置修复不代表完整技能骨骼挂点/坐标轴与动作事件同步已验收。
4. **技能栏冷却**:按技能 ID 保存会话内冷却,服务端槽位刷新、同技能多格、换格/重新放入不再清空冷却遮罩;冷却结束包清理共享状态。技能 UI 回归覆盖重复技能格、服务端恢复、提前解锁及原有翻页/移动端行为;未实现跨断线恢复的服务端剩余冷却同步。
5. **DDS 真正缺图根因**:真实 `shamanblackcore.dds` 为 ARGB1555,旧解码器无法读取。现按 DDS 位掩码和行 pitch 解码非压缩 16/24/32 位 RGB,包括 A1R5G5B5、R5G6B5、A4R4G4B4、24 位和不同 32 位通道顺序;无有效 alpha 标志按不透明处理。拒绝重叠/非连续掩码、无效 pitch、截断输入;DXT1/3/5 保留。新增 C++ 用例经 AddressSanitizer/UndefinedBehaviorSanitizer 通过。此为 level-0 解码,不表示全 DDS 格式/mipmap 支持。
6. **资源生命周期**UiAssets 的 Metin2World 解码辅助节点现同步释放,返回 Image 独立存活;真实 DDS 连续 20 次解码节点计数不增加。combat_fx_test 的两个孤立假节点改为正确归属树(这是测试夹具修复,不误记为生产泄漏修复)。
### 综合回归证据
- 新增入口 `bash script/rendering_batch_test.sh`,保留每项完整日志,检查退出码、成功标记和 GDScript 错误;遇到失败继续收集其余结果。最初运行发现测试夹具干扰和 macOS Bash 空数组兼容性问题,修复后重跑。
- 最终:**21 项 headless + 7 项 Metal = 28 项全部通过**,日志目录 `build/rendering/batch-20260909-072138-35505/`。覆盖特效、技能栏、施法校验、战斗、场景与登录生命周期、换装规则、八种族动作装配、CPU 像素基线、GPU LOD 附件与动态包围盒;本次最终批次未出现退出泄漏警告。
- `bash script/rendering_test.sh` 重建/同步 debug 扩展与选角装配通过,人物 CPU 像素基线未调整。native 测试:`clang++ -std=c++17 -Wall -Wextra -fsanitize=address,undefined -Iextension/src extension/tests/dxt_decode_test.cpp extension/src/dxt.cpp -o build/dxt_decode_test && build/dxt_decode_test`。
- `git diff --check` 通过。全部原有未提交修改已保留,本轮未提交。
### 新 mac 包与实际进场
- 独立产物:`build/export-rendering-20260909/mtgodot-poc.app`,保留 `build/export/` 原包。`build-macos-client.sh` 新增 MT_MAC_OUTPUT_DIR,便于输出到独立目录;这次是 release、arm64+x86_64 universal、ad-hoc 签名,codesign deep/strict 检查通过。实际运行验证平台为 Apple M4,Intel 平台未实测。
- release 扩展 SHA-256`683aad5a97a0e6b0c959b082bb3774950d12565b4a6e0c216a1f53eb9a1d2120`PCK SHA-256`9cd602618e76debad02aa6a5544ac61df498ded1e3cebe5351d962800d0be023`。
- **包内离线 PASS**:清除 MT_ASSETS,资源实际来自 Contents/Resources/assets;真实 A1 地图、角色、DDS 和专用技能材质均加载成功。报告/截图 `build/rendering/package-20260909/report.json` / `game.png`,脚底 gap=0.001373m,20 块地形加载、0 块失败。额外技能检查为资源/材质装配,不是施法画面。
- **真实只读登录 PASS**:用新包连接现有 40250 服务,9.633 秒完成登录和主角/世界/属性/背包/技能/快捷栏接口检查。`build/rendering/live-20260909/live-smoke-report.json`mutations=false、reconnect=false;未发送测试移动/聊天/施法/换装。测试脚本支持独立 APP/输出路径,遇到已有客户端会退出提示,不再杀掉用户运行中的客户端。
- **实际游戏截图**`build/rendering/live-20260909/game.png`。第二次普通自动登录后只观察,C1 主角就绪(main_attempts=1),后续多次采样位置稳定;人工复核画面包含主角、地表、NPC/建筑、小地图与底部技能图标。没有用离线截图替代此实机证据。
- **未解决警告**:包内离线整场景测试退出时仍出现 3 个 shader RID 释放警告,尽管测试报告 PASS;不能宣称全场景资源释放已完成。源码已有世界 shader 清理函数,具体退出时序/引用来源仍待定位,未用屏蔽日志处理。
### 仍需继续的工作(不能记为本轮完成)
- **技能完整运行时**:随机等幅正反转、铺地/交叉面/拉伸 billboard、非相机朝向自旋、运动与出生参数时间曲线、逐粒子贴图动画、重复关键帧突变精度、骨骼挂点/动作事件与真实四职业多等级施法验收;更多 D3D blend 对仍为近似。
- **人物/环境保真**:全高级装备与武器发型组合、混合动作、坡地/台阶、NPC/怪物/GPU 画面;角色与环境光照、雾/阴影、地形/水/建筑/植被和完整选角/UI 与 40250 的受控对照。SpeedTree 仍为 proxy,不能以本轮 DDS 支持替代完整树模型还原。
- **交互与稳定性**:多分辨率/完整技能栏输入拖放、跨重连状态与切图/换角色实机压力测试;本轮只读登录没有覆盖这些外部状态变更。
- **性能/发布**:多角色帧时间和 GPU 包围盒 CPU 成本、完整场景 shader RID 警告;Intel Mac、正式签名/公证、全量游戏流程验收。
- **验收所缺证据**:光照与材质精调仍需要 40250 同资源/角色/坐标/相机/动作/环境的参考帧,不能仅凭现有不同机位截图任意调亮后宣称一致。R5 的“有新包/独立加载/只读登录”本轮已完成,R0–R6 的全部保真门禁尚未完成。
## 2026-09-09 后续修复:动作挂点、逐粒子贴图、NPC 绕序及水面
本节为以上历史批次之后的最新进展。**五组问题尚未全部关闭**;配置/数值测试、Metal 像素测试、实包退出和 40250 配对验收分别记账。
### 已修复
1. **MSA 动作特效挂点数据链**:两条 MSA 解析入口均保留 AttachingBoneName、AttachingEnable、FollowingEnable、IndependentFlagnative `get_events()` 和新增 `motion_event_detailed` 携带完整信息,旧四参数信号保留。GameScene 优先订阅详细信号,声音/射箭仍只分派一次。普通 EFFECT(type=1) 保留完整资源路径,不再取 basename 猜文件;使用实际动画混合后的全仿射骨骼矩阵区分跟随、世界快照、独立效果和根节点附着。偏移保持源码 `bone * translation * actor` 的行向量组合顺序,再转换厘米/Z-up;缺失骨骼不再误放到角色根。跟随节点在动画之后更新,角色销毁时随之回收。**尚不覆盖** EFFECT_TO_TARGET(type=10)、远端角色全部动作事件、所有 MSE 内部坐标/旋转与真实施法同步。
2. **逐粒子贴图动画**:移除粒子共用 AnimatedTexture 时钟,原纹理装成 Texture2DArray;使用粒子自身 CUSTOM.y 年龄、CUSTOM.z 出生随机种子,在绘制 shader 中选择帧。支持 NONE/CW/CCW/RANDOM_FRAME/RANDOM_DIRECTION、随机起始帧及延时阈值;倒放从第 0 帧开始,随机方向的倒放从末帧开始,依据 ParticleSystemInstance.cpp。随机序列不承诺与旧客户端 RNG 逐帧相同;寿命目前仍是既有单发射器近似,不等于出生参数时间曲线已还原。
3. **NPC/怪物外表面被剔除**:截图复核发现 MobView 未应用 PC 已使用的 flip_winding。真实野狗(101)、熊(110)、炼金师(20001) 的 CPU/GPU 六组几何法线负对照修前均失败(面积加权绕序/法线比值约 +0.93~+0.97),修后约 -0.93~-0.97、全部通过。不以三个样本代表所有 NPC/怪物资源。
4. **水面明确的源码差异**MapOutdoorWater.cpp 使用纹理 RGB 与顶点 diffuse alpha,不含原实现附加的 tint/Fresnel/PBR 光照。现恢复直接取纹理色与顶点透明度;AreaTerrain.cpp / MapOutdoor.cpp 的 raw 水深除以 400、上限 0.8 替代原 60cm/最低 0.12 的公式,正确考虑 HeightScale。按 2m 水格保留内部岸线采样,避免跨整行合并丢失岸线;四角透明度均为零时不生成水格。保留 30 帧/70ms 序列。**仍有近似**:水面高度浮动、远雾处混合状态及整体色彩空间尚未与受控参考帧对齐。
### 回归与证据
- 原整套回归扩为 **25 headless + 8 Metal = 33 项 PASS**,日志 `build/rendering/batch-20260909-163241-59569/`;本批未出现退出泄漏警告。新增 motion_effect_anchor / mob_winding / water_reference / effect_texture_animation 测试。
- `motion_effect_anchor_test.gd` 使用真实武士骨架与 gigongcham.msa,验证骨骼位置、角色转向下的厘米偏移、跟随/快照/独立/缺骨行为及文件解析元数据;不是完整施法截图验收。
- `effect_texture_animation_test.gd` 用同一材质、两个不同年龄的实例在 Metal 读取像素,覆盖五种动画类型和三个年龄点;新生实例保持起始帧,老实例按自身年龄选帧。合成纹理用于隔离帧选择,不冒充真实四职业技能画面;随机起始帧与随机分布尚无统计验收。
- 水深 native 单测经 ASan/UBSan 通过;最终水格剔除优化后另跑 `water_reference_test.gd`**15 块水面、212587 个顶点对照、11898 个浅岸样本通过**。RGBA8 顶点色容差为 1/255;最外地图边界 1149 个顶点不调用跨区块 `sample_height` 对照(其邻块不存在),明确计入 skipped。它验证几何/材质,不是水面配对像素。
- `formats_msa_test` 重编通过;`script/rendering_test.sh` 重建并同步 debug 扩展、选角检查通过;`git diff --check` 通过。未提交,既有未提交内容保留。
- `rendering_scenario_test.gd` 为独立、离线、真实资源场景报告:A1、CPU/GPU 两路径额外 1/8/16 个角色、run→attack→wait、野狗/熊/炼金师,1280×720 / 1920×1080 / 2560×1440 技能栏边界与截图,以及 world_reset + warp 信号驱动的 A1→C1。输出 `build/rendering/scenarios-followup/report.json` 与三张 `scene-*.png`。技能栏没有来自服务器的技能数据,不能将空槽截图称为完整技能交互验收。每组 90 个窗口帧间隔受垂直同步影响,不是 GPU timestamp、无帧率上限基准或大量角色性能保证。
### shader RID 警告:已定位范围,未修复
- 原包复跑 `exit-export-repeat.log` 重现 **3 个 ParticlesShaderRD / Shader RID** 警告。源码编辑器相同离线进场没有该警告。
- 实包诊断中仅跳过粒子预检,地图/角色仍完整加载:`exit-no-effects.log` 不再出现警告。由此定位到粒子 process material 路径,不是凭“正好三个”推测为地形/树/水 shader。
- 等待实际渲染帧、提前解除 emitter 的 process_material 引用、提前清理世界缓存均未消除实包警告;无效的生产清理改动已撤回,也没有屏蔽日志或将跳过粒子作为正式通过方式。保留正常进树/渲染/释放的包内预检。
- 引用计数诊断显示释放 emitter 后仅剩测试数组/循环局部引用;仍需进一步检查 release 模板的粒子 shader 缓存/销毁路径,不能据此断言已经修复或一定属于某个上游缺陷。
### 当前剩余清单
1. **技能**:运动/出生参数时间曲线、径向发射、随机等幅自旋、铺地/交叉面/拉伸等特殊 billboard、内部坐标/旋转一致性、目标特效与远端事件、更多 D3D blend 对、四职业多等级真实施法/挂点画面验收。
2. **人物**:全部高级护甲/武器/发型组合、混合动作逐帧像素、坡地/台阶足部贴合、更多 NPC/怪物及 GPU 最终画面;本轮 crowd 的有限数值/稳定性覆盖不等于全部通过。
3. **环境保真**:40250 同资源/相机/动作/天气参考帧下的角色亮度、光照/阴影/雾、地形、水面、建筑植被;SpeedTree 仍为近似树。完整树几何需要可在目标平台使用、授权明确的导出/替代资源链,不能拿现有代理模型宣称还原。
4. **交互与性能**:真实重连/切图/换角色压力、跨重连冷却状态、多分辨率鼠标/键盘拖放与技能释放全流程、更大人数与长期帧时间/内存测量。离线 warp 测试不是联网重连验收。
5. **释放与发布**:实包三 shader RID 警告尚在;Intel Mac、正式签名/公证和全流程发布验收未完成。
### 本轮最终实包与发布门禁
- 新产物 `build/export-rendering-followup-20260909/mtgodot-poc.app`universal releasead-hoccodesign deep/strict 通过);保留之前的 `build/export-rendering-20260909/` 和 `build/export/`。本轮未重新联网登录,不能沿用上一批次登录结果宣称本包已完成联网验收。
- 包内离线加载报告 `build/rendering/package-followup-final/report.json` 为 PASS:20 块地形、18 块非全透明水面、2 个专用技能材质,脚底 gap=0.001373m。完整日志 `build/rendering/package-followup-final.log` 在报告之后仍出现 3 shader RID 警告,**因此干净退出门禁未通过**。
- release dylib SHA-256`63c5c0a639d5c531c0f28d64f9283c536a491156a1d29fa1b6ae931fdc826303`PCK`bcae262f6e91d82e79a7c0618750f1bed1325c5e7f7a043bb25d751d32918816`。
- 新增 `script/package_render_test.sh`:需同时满足进场成功标记、进程正常返回、完整日志无 script/renderer 错误或泄漏,才返回成功。可用 `MT_RENDER_APP` 指定包、`MT_RENDER_OUTPUT` 指定报告目录。本包检查日志保存在 `build/rendering/package-followup-gate/client.log`,已知泄漏必须让检查失败,而不是过滤日志或只读 JSON 的 passed 字段。
- 最终离线场景报告 failures=[],截图复核的 2560×1440 帧中野狗、熊、炼金师表面可见;早期小分辨率帧仍会记录模型刚创建时的 LOD 渐变,不是固定姿态参考帧。16 个额外角色样本:CPU median=16.666ms / P95=16.872ms / max=47.342msGPU median=16.705ms / P95=17.862ms / max=51.997ms。动作切换仍有长帧,不能宣称“稳定 60fps”或“GPU 更快”。
## 2026-09-10 续修检查点(以此节更新上述历史剩余清单)
### 已实现与专项验证
- **目标特效**MSA type10 携带 FishingEffectFlag;快捷栏成功施法时捕获目标,不随后续选中对象改变。跟随效果用目标变换,快照效果使用目标位置加世界偏移,目标消失后释放跟随节点。钓鱼落点尚未接入,明确不在施法者脚下误生成;快照中心仍为可见网格包围盒近似,不是完整旧版包围球还原。
- **坐标与水平面特效**:MSE 粒子/网格/灯光移动、Bezier 控制向量、发射方向与 MDE 顶点统一为厘米 Z-up → 米 Y-up。LIE 使用水平面 shader,Metal 俯视可见/侧视边缘负控通过。
- **双面/三面粒子**:新增 `particle_faces.gd`,同一粒子模拟驱动两个/三个面,面角通过 UV2 传入 shader;对应旧版 TwoSideRenderer 的 ±30°、ThreeSideRenderer 的 0°/±60°,不创建随机状态不同的重复发射器。`effect_faces_test.gd` 验证面数、角度、材质,以及两个相机方向下的 Metal 投影宽度。拉伸、复杂挂接与各技能实景仍需验收,不能把有限投影测试当作全技能像素一致。
- **动作缓存**:不可变 GR2 解码缓存上限 32 条/64MiB section 数据;MSA 元数据缓存上限 128 条,按路径/修改时间失效。各角色时间、事件游标与姿态独立,清空缓存不破坏活动动作;失败不缓存。删除 reload 中同一 MSA 的第二次解析。专项测试两角色三种动作为 GR2 17 hits/3 misses、MSA 17 hits/3 misses。GR2 上限不含解析对象开销及活动角色仍持有的淘汰条目;MSA 为条数上限,不是总内存硬上限。
- **水面移动**:删除按顶点空间位置变化的正弦波,改为旧版整片共享垂直位移;初始 300ms 静止,随后 1000–3000ms 线性过渡,目标在随机 0..−15cm 与零之间切换。`water_motion_test.cpp` 在 ASan/UBSan 下验证端点、时长及长帧不越界;随机时序未与 Windows 同步,不宣称逐帧一致。远处水面关混合/雾过渡仍未实现。
- **真实重连历史证据**`build/rendering/reconnect-20260909/live-smoke-report.json` PASS24.009 秒、mutations=false、reconnect=true,两次进入游戏并重建主角。该实包早于最新坐标/多面/水面修改,不可当作当前最终包的联网验收;不是断网故障或长期重连压力测试。
### 回归证据与边界
- 扩展重建、debug dylib 同步签名、选角测试通过;新增测试纳入 `script/rendering_batch_test.sh`。
- 最新完整回归 **39 项通过**29 headless + 10 Metal):`build/rendering/batch-20260910-213214-9037/`,汇总 `followup-final-batch-20260910.log`。包含目标、坐标、缓存、多面、LIE 以及既有模型/材质/技能回归。
- 离线场景报告 `build/rendering/scenarios-20260910/report.json` failures=[],覆盖真实 A1→C1、1/8/16 个额外角色 CPU/GPU、三分辨率布局;仅是短窗口帧间隔,不是冷启动/长期性能证明。本轮进程诊断可能与此轮场景采样时间相邻,性能结论以随后隔离复测为准。
- **释放仍未关闭**`particle-exit-primed.log` 证明提前调用 get_rid 再配置仍有三个泄漏,未作为生产修复。进一步按 gravity/scale/shape、保留若干帧后释放、恢复默认形状等情况拆分诊断;只认可成功标记与退出后的完整日志,外部 --script 探针没有执行标记,不能计为测试通过。
### 仍未完成
1. 实包粒子 shader RID 泄漏的真正修复及干净退出门禁。
2. 拉伸 billboard、径向与出生参数时间曲线、随机等幅正反转、钓鱼落点、准确目标包围球中心、远端完整事件、更多混合模式,以及四职业多等级真实施法画面对照。
3. 光照/阴影/雾/地形/建筑/植被和远处水面,与 40250 同资源/坐标/相机/动作/环境的受控像素对照;缺少这些成对帧,不能靠任意调亮验收。SpeedTree 仍是近似模型,需要授权明确且可用的几何转换或替代资源。
4. 冷加载、首次材质/动画编译、全高级装备/混合动作/坡地/更多 NPC 验收,长期多角色帧时间及完整交互/断网恢复/重复切图压力。
5. Intel Mac、正式签名/公证及全流程发布验收。当前代码及旧批次改动均未提交。
### 本次最终实包与隔离复测
- 新包 `build/export-rendering-20260910/mtgodot-poc.app`,保留所有旧包;universal release、ad-hoc`codesign --verify --deep --strict` 通过。dylib SHA-256 `b4d8fc1f0d4aba0183531769c409545e8639842fc734563cacb6233e4c728fdb`PCK `d88fb74b9db9d8ae9a0a4caa47de40c69d350864dd97046c454b82079e39aa7d`。
- `build/rendering/package-20260910/report.json` / `game.png` 完成包内离线进场,`client.log` 有 PKGRENDER PASS**退出后仍有三个 shader RID 泄漏,故 package_render_test.sh 正确返回失败**。这不是发布验收通过。
- 进一步最小实验 `particle-exit-20260910-{gravity,scale,shape,held,restored}.log`:仅改重力、仅改缩放退出干净;设置发射形状后泄漏;持有五帧后释放、恢复默认形状后释放均仍泄漏。范围已缩小到形状配置触发的粒子 shader 路径,具体引擎内部根因未确认。无效清理方案未加入生产。
- 所有构建、实包诊断结束后独立重跑 `scenarios-20260910-isolated/report.json`failures=[]。16 个额外角色 CPU median/P95/max = 16.669/16.932/17.051msGPU = 16.704/16.965/17.133msCPU 八角色组仍有 23.809ms 最大帧。每组仅 90 个受 VSync 影响的窗口帧间隔,包含两次动作切换;不能据此保证全部冷加载或长期无卡顿,也不能将所有改善归因于单一缓存改动。
## 2026-09-11:干净退出构建路径、随机旋转与目标中心
### 粒子退出警告:新 arm64 Release 包通过,官方模板仍复现
- 独立工程 `test/rendering/particle-leak/` 不包含客户端扩展、autoload、地图、网络或特效播放器。八个粒子材质切换三种发射形状,WeakRef 确认存活数为零,最后执行 RenderingServer.force_sync。官方 4.7.1 Release 模板仍报告三个 Shader RID 泄漏;官方 Debug 模板退出干净。日志 `particle-isolated-release.log` / `particle-isolated-debug.log`。
- 从上游固定提交 `a13da4feb8d8aefc283c3763d33a2f170a18d541` 构建 arm64 Release`debug_symbols=yes optimize=speed lto=none vulkan=no`(Metal 可用)。加入计数日志的诊断构建干净退出,**移除全部诊断修改、恢复原版源码后也干净退出**,日志 `particle-engine-baseline.log`。不能据此断言某一个编译选项或某行上游源码就是根因;已确认的是当前安装模板与这条构建路径的行为差异。
- 新增 `script/build_macos_release_engine.sh` 固定源码提交并拒绝带修改的引擎工作树,不替换系统模板。`build-macos-client.sh` 支持显式 `MT_MAC_ENGINE` 并验证架构,替换包内引擎后重新签名。示例:先运行 `bash script/build_macos_release_engine.sh arm64`,再以 `MT_MAC_ARCHES=arm64`、`MT_MAC_ENGINE="$PWD/build/godot-particle-diagnostic/bin/godot.macos.template_release.arm64"` 和独立 `MT_MAC_OUTPUT_DIR` 调用 `bash build-macos-client.sh release`。
- 新产物 `build/export-rendering-clean-exit-20260911/mtgodot-poc.app`**Apple Silicon/arm64,实际 release=true、debug=false、editor=false**;不是以 Debug 模板替代 Release。ad-hoc 签名 deep/strict 通过,保留所有旧包。
- `script/package_render_test.sh` 连续两次通过:`build/rendering/clean-exit-20260911-1/client.log`、`clean-exit-20260911-2/client.log`。包内地图、模型、DDS、三种专用特效材质正常装配,退出完整日志无 shader/RID 错误。没有跳过粒子或过滤警告。
- **关闭范围**:这条新 arm64 构建路径的整场景退出门禁已通过;安装的官方 Release 模板仍未修复。默认不指定 MT_MAC_ENGINE 仍使用官方模板,不应发布未经退出门禁检查的包。Intel/universal 自编译模板、其他图形后端及正式签名/公证尚未验证。
### 特效修复与回归
- 随机等幅旋转:依据 ParticleSystemData.cpp 的 RANDOM_DIRECTION,在出生时选择正/反方向且保持完整角速度;去掉仅正向随机减速。绘制 shader 使用粒子年龄与稳定种子,保留初始角度,不在 process material 重复积分。覆盖普通/Y/水平/多面 billboard 路由;`effect_random_rotation_test.gd` 包含两个符号种子的 Metal 镜像画面负控。
- 目标中心:GR2 解析保留 BoneBindings 的 OBBMin/OBBMax(验证 float3 与有限范围),通过主体骨骼 world pose 变换八个角点得到包围盒中心,首次查询后缓存,模型 reload 时失效;不合并独立发型、武器、场景特效或 LOD ghost。`fly_target_bounds_test.gd` 覆盖八种角色的 CPU/GPU 一致性、远处外接网格不改变中心、后续动作不移动缓存中心、角色整体位移正确传播。缺少有效 OBB/pose 的资源仍走显式旧近似回退;首次建球时机、所有装备/NPC 和 Windows 实际目标帧还需进一步对照。
- **43 项回归通过**`build/rendering/batch-20260911-062535-40433/`31 headless + 12 Metal),汇总 `clean-exit-final-batch.log`。原有目标特效、坐标、动画缓存、多面、LIE、水面及模型检查保留。未提交 Git,原有工作树修改保留。
### 性能与仍未完成的内容
- `rendering_scenario_test.gd` 新增 `MT_RENDER_SAMPLE_FRAMES`90..36000),每 30 帧切换动作,记录同步模型构建耗时、群体动作切换耗时、帧间隔、Godot static memory 与动画缓存。模型构建记录不是清空 OS/驱动缓存后的“真正冷启动”;static memory 也不是进程 RSS 或显存。
- 拉伸 billboard、径向/出生参数运动曲线、钓鱼水面落点及其完整动作事件、更多混合方式、远端完整事件和四职业真实施法仍未完成。
- 光照/阴影/雾/地形/建筑/植被、远处水面与 40250 同条件配对画面仍未完成。树木仍是代理几何,缺少授权明确的完整转换资源链;未用任意调色代替受控验收。
- 冷启动/首次 shader 编译、全装备/坡地/NPC、长期联网重连切图和多角色压力、Intel 及发布验收仍待完成;不能把本次退出通过等同于渲染全部完成。
### 持续采样与最终包补充
- `build/rendering/soak-20260911/report.json`309.124 秒,CPU/GPU × 1/8/16 个额外角色,每组 3000 帧、99 次群体动作切换,共 18000 帧/594 次,功能 failures=[]。各组帧间隔最大值依次为 18.458、27.558、17.846、18.025、18.093、18.218msCPU 八角色动作切换耗时最大 23.262ms,仍是明确长帧。各组 Godot static memory 增量约 0.070.14MB,包含测试数组和缓存,不是 RSS/显存或长期无泄漏证明。同步模型 build 最大 7.36ms,OS/资源缓存未清空。
- 在这次采样结束后,进一步把每次 reload 的骨骼匹配诊断扫描和三类普通日志改为 `MTGODOT_ANIM_DIAGNOSTICS=1` 才执行;解析失败、错误与警告完全保留。旧五分钟样本是**优化前**的数据,不用于冒称优化后的长期性能。`animation-diagnostics-enabled.log` 验证显式开启仍输出诊断并通过缓存测试。
- 最终重新构建并签名同名 arm64 测试包;43 项回归再次通过,日志目录 `build/rendering/batch-20260911-063306-41283/`。`build/rendering/clean-exit-20260911-final/client.log` 再次通过完整包内进场与干净退出门禁。
- 最终包 SHA-256engine `b930ded944fd51ce911e1958289f9bc46c51cad17aa62ef30ca61c69330fb50a`dylib `6f60298bdd9e45c5047e9327de24546506a0b1ba90feb8db40b82e1ff4416b3d`PCK `e3e38b68b12b670ce44687870785ae519c90ff283e56112190a597b594a2b779`。
- 日志优化后的短场景复查 `build/rendering/scenarios-quiet-20260911/report.json` failures=[],仍覆盖 CPU/GPU、多角色、三分辨率和 A1→C1;只有每组 90 帧,不能替代优化后重新进行的持续或冷启动性能验收。
## 首个联网内测版开发入口
联网闭环、基础战斗、首测地图与稳定性门禁的文件级任务、接口设计、状态机、测试证据和执行命令见 [首个 Mac 联网内测版:开发实施文档](FIRST-MAC-PLAYABLE-IMPLEMENTATION.md)。该文档中的新增接口与脚本为待实现设计,不代表已经完成验收;树木及其他渲染完成状态仍以本路线的实际测试记录为准。
## 2026-09-11:真实静态树木模型接入
### 已完成(更新“树木仍为代理”的历史状态)
- 在同工作区 `metin2-godot-main/generated/content-v1/40250-full-unpacked-final` 找到原始 SpeedTreeRT 提取产物及来源清单。87 个 SPT 原文件 SHA-256、87 个 GLB SHA-256 全部匹配当前资源;不是依名称套用其他树种,也不是新生成的近似几何。
- `script/stage_tree_geometry.mjs` 校验来源、转换坐标约定、节点结构、内嵌贴图和双向哈希后,放入 `assets/TreeGeometry`(约 59MB)。Mac 不运行或分发 SpeedTree DLL。
- `get_tree_mesh` 优先读取经校验的 GLB,保留真实枝干/叶状枝/叶片、UV、原始树高和树皮/composite 贴图。顶点已经为米/Y-up,不再套代理的统一 12m 高度或二次坐标转换。继续按 treefile 共享 ArrayMesh/MultiMesh,保留原 AreaData 位置和原烘焙阴影策略。
- 叶片保留双面 alpha scissor0.3);观察 `build/rendering/native-trees-metal/beech-0.png` / `beech-90.png` 的两个角度,树冠与枝干可见,没有把整张 atlas 画成不透明方块。
- 原代理代码仅为缺失/损坏资源的诊断回退:打印警告、mesh/instance 标记 `tree_geometry=proxy`,地图报告新增 `native_trees_placed` 和 `proxy_trees_placed`。包内验收要求真实树数大于零且代理数为零,回退不能被当成完成。
### 验证与覆盖
- `native_tree_test.gd` 对87树种逐项核对哈希、GLB装载、surface数量、纹理、透明裁剪、双面材质及有限包围盒;另对榉树做两个机位的 Metal 轮廓检查并保存截图。
- 六图实际加载:A1=368、A3=389、B1=317、B3=431、C1=296、C3=291,共 **2,092 棵真实树,代理回退 0**。各图已有少量建筑模型缺失不是树木缺失,本轮未改变它们。
- 全量回归 **45 项通过**32 headless + 13 Metal),`build/rendering/batch-20260911-134729-54151/`,汇总 `native-tree-batch.log`。原角色、技能、水面、缓存和 GPU 检查保留。
- `script/audit_native_trees.mjs` 扫描当前资源:118 个 SPT 中87个有原生导出;PRT引用84种,其中实际静态 AreaData 放置81种,**missing_placed=[]**。报告 `build/rendering/native-tree-coverage.json`。
- 新包 `build/export-native-trees-20260911/mtgodot-poc.app`Apple Silicon/arm64 Release,使用上一节验证过的自编译引擎。codesign deep/strict 通过;`build/rendering/native-trees-package/client.log` 的完整进场/退出门禁通过,包内 A1 native=368、proxy=0,没有新增 shader RID 警告。旧包保留,未提交 Git。
### 树木相关剩余边界
- 本轮完成的是**真实静态模型**;原版 SpeedTree 的逐叶相机朝向、风场/枝叶动画和原生 LOD 切换仍未恢复,不能宣称完整 SpeedTree 视觉行为已一致。
- `Zone/ymir work/zone/b/tree` 中31个旧 SPT 未转换,当前 PRT 未引用这些路径,不借相似名字冒充匹配。一个未被静态 AreaData 使用的旧 PRT 指向不存在的 `christmastree.spt`;真实季节变体 `christmastree1/2/3.spt` 已转换,活动树生成/等级特效属于另一个流程。
- 六图以外的逐地图 GPU 画面、与40250同条件像素对照,以及更大规模树木长期性能仍待验收。
+33 -11
View File
@@ -67,7 +67,19 @@ void decode_dxt5_alpha(const uint8_t* blk, uint8_t out[16][4]) {
}
}
enum Fmt { F_NONE, F_DXT1, F_DXT3, F_DXT5, F_BGRA8 };
enum Fmt { F_NONE, F_DXT1, F_DXT3, F_DXT5, F_RGB };
bool valid_mask(uint32_t mask, uint32_t bits) {
if (!mask || (bits < 32 && (mask >> bits))) return false;
while (!(mask & 1)) mask >>= 1;
return (mask & (mask + 1)) == 0; // contiguous channel bits
}
uint8_t channel(uint32_t pixel, uint32_t mask) {
if (!mask) return 255;
while (!(mask & 1)) { mask >>= 1; pixel >>= 1; }
return uint8_t((uint64_t(pixel & mask) * 255 + mask / 2) / mask);
}
Image decode(const uint8_t* d, size_t len) {
Image img;
@@ -85,8 +97,8 @@ Image decode(const uint8_t* d, size_t len) {
if (!std::memcmp(fourcc, "DXT1", 4)) fmt = F_DXT1;
else if (!std::memcmp(fourcc, "DXT3", 4)) fmt = F_DXT3;
else if (!std::memcmp(fourcc, "DXT5", 4)) fmt = F_DXT5;
} else if ((pf_flags & 0x40) && rgb_bitcount == 32) { // DDPF_RGB, 32bpp
fmt = F_BGRA8; // Metin2 的非压缩 DDS 一般是 B8G8R8A8
} else if ((pf_flags & 0x40) && (rgb_bitcount == 16 || rgb_bitcount == 24 || rgb_bitcount == 32)) {
fmt = F_RGB;
}
if (fmt == F_NONE || w == 0 || h == 0 || w > 8192 || h > 8192) return img;
@@ -96,15 +108,25 @@ Image decode(const uint8_t* d, size_t len) {
img.h = uint16_t(h);
img.rgba.assign(size_t(w) * h * 4, 0);
if (fmt == F_BGRA8) {
if (avail < size_t(w) * h * 4) { img = Image{}; return img; }
for (size_t i = 0; i < size_t(w) * h; ++i) {
img.rgba[i * 4 + 0] = src[i * 4 + 2];
img.rgba[i * 4 + 1] = src[i * 4 + 1];
img.rgba[i * 4 + 2] = src[i * 4 + 0];
img.rgba[i * 4 + 3] = src[i * 4 + 3];
if (fmt == F_RGB) {
const uint32_t r = rd_le32(d + 92), g = rd_le32(d + 96), b = rd_le32(d + 100);
const uint32_t a = (pf_flags & 1) ? rd_le32(d + 104) : 0;
if (!valid_mask(r, rgb_bitcount) || !valid_mask(g, rgb_bitcount) || !valid_mask(b, rgb_bitcount) ||
(a && !valid_mask(a, rgb_bitcount)) || ((pf_flags & 1) && !a) ||
(r & g) || (r & b) || (g & b) || (a & (r | g | b))) return {};
const size_t bytes = rgb_bitcount / 8, row_bytes = size_t(w) * bytes;
const size_t pitch = (rd_le32(d + 8) & 8) ? rd_le32(d + 20) : row_bytes;
if (pitch < row_bytes || avail < row_bytes || pitch > avail || (h - 1) > (avail - row_bytes) / pitch) return {};
for (size_t y = 0; y < h; ++y) {
for (size_t x = 0; x < w; ++x) {
uint32_t pixel = 0;
for (size_t k = 0; k < bytes; ++k) pixel |= uint32_t(src[y * pitch + x * bytes + k]) << (8 * k);
auto* out = &img.rgba[(y * w + x) * 4];
out[0] = channel(pixel, r); out[1] = channel(pixel, g);
out[2] = channel(pixel, b); out[3] = channel(pixel, a);
}
img.format = "BGRA8";
}
img.format = rgb_bitcount == 32 && r == 0xff0000 && b == 0xff ? "BGRA8" : "RGB_MASKED";
return img;
}
+1 -1
View File
@@ -1,5 +1,5 @@
// dxt.{h,cpp} — ported verbatim from xrender-poc/engine/dxt.{h,cpp}
// (namespace engine -> mtgodot). DDS DXT1/3/5 + BGRA8 -> RGBA8, level 0.
// (namespace engine -> mtgodot). DDS DXT1/3/5 + masked 16/24/32-bit RGB -> RGBA8, level 0.
// engine/dxt —— DDS(DXT1/3/5) → RGBA8 软解。见 docs/steps/M1-static-render.md T3
// 自己写 ~150 行绕开 reuse/EterImageLib 的 windows.h 依赖(M1 风险表允许)。
// 对拍时两侧都喂软解 RGBA,不被 GPU S3TC 的 bit 级差异污染。
+141 -25
View File
@@ -10,6 +10,7 @@
#include <cmath>
#include <string>
#include <list>
#include <godot_cpp/classes/mesh_instance3d.hpp>
#include <godot_cpp/classes/os.hpp>
@@ -25,10 +26,102 @@ using namespace godot;
namespace mtgodot {
namespace {
struct ClipEntry {
String path;
uint64_t modified;
size_t bytes;
std::shared_ptr<const gr2::File> file;
};
// Immutable decoded clips are shared, not playback time/pose/event cursors.
// Main-thread animation loading only; no Godot objects survive in this cache.
std::list<ClipEntry> clip_cache;
size_t clip_bytes = 0;
uint64_t clip_hits = 0, clip_misses = 0;
constexpr size_t CLIP_LIMIT = 32, CLIP_BYTE_LIMIT = 64 * 1024 * 1024;
struct MsaEntry {
String path;
uint64_t modified;
fmt::Msa metadata;
};
std::list<MsaEntry> msa_cache;
uint64_t msa_hits = 0, msa_misses = 0;
// Keep events immutable in the cache; reload copies them into each actor.
bool cached_msa(const String &path, fmt::Msa &metadata, std::string *error) {
const uint64_t modified = FileAccess::get_modified_time(path);
for (auto it = msa_cache.begin(); it != msa_cache.end(); ++it) {
if (it->path != path) continue;
if (it->modified == modified) {
++msa_hits;
metadata = it->metadata;
msa_cache.splice(msa_cache.begin(), msa_cache, it);
return true;
}
msa_cache.erase(it);
break;
}
++msa_misses;
if (!fmt::parse_msa_file(std::string(path.utf8().get_data()), metadata, error)) return false;
if (msa_cache.size() >= 128) msa_cache.pop_back();
msa_cache.push_front({path, modified, metadata});
return true;
}
std::shared_ptr<const gr2::File> cached_clip(const String &path, gr2::LoadError *err) {
uint64_t modified = FileAccess::get_modified_time(path);
for (auto it = clip_cache.begin(); it != clip_cache.end(); ++it) {
if (it->path != path) continue;
if (it->modified == modified) {
++clip_hits;
clip_cache.splice(clip_cache.begin(), clip_cache, it);
return clip_cache.front().file;
}
clip_bytes -= it->bytes;
clip_cache.erase(it);
break;
}
++clip_misses;
auto loaded = gr2_from_file(path, err);
if (!loaded) return {};
size_t bytes = 0;
for (size_t i = 0; i < loaded->sections().size(); ++i) bytes += loaded->section_bytes(i).size();
auto file = std::make_shared<const gr2::File>(std::move(*loaded));
if (bytes <= CLIP_BYTE_LIMIT) {
while (!clip_cache.empty() && (clip_cache.size() >= CLIP_LIMIT || clip_bytes + bytes > CLIP_BYTE_LIMIT)) {
clip_bytes -= clip_cache.back().bytes;
clip_cache.pop_back();
}
clip_cache.push_front({path, modified, bytes, file});
clip_bytes += bytes;
}
return file;
}
}
Dictionary Metin2AnimPlayer::get_clip_cache_stats() {
Dictionary d;
d["hits"] = clip_hits;
d["misses"] = clip_misses;
d["entries"] = static_cast<int64_t>(clip_cache.size());
d["section_bytes"] = static_cast<int64_t>(clip_bytes);
d["msa_hits"] = msa_hits;
d["msa_misses"] = msa_misses;
d["msa_entries"] = static_cast<int64_t>(msa_cache.size());
return d;
}
void Metin2AnimPlayer::clear_clip_cache() {
clip_cache.clear();
clip_bytes = 0;
clip_hits = clip_misses = 0;
msa_cache.clear();
msa_hits = msa_misses = 0;
}
Metin2AnimPlayer::Metin2AnimPlayer() {}
Metin2AnimPlayer::~Metin2AnimPlayer() = default;
void Metin2AnimPlayer::_bind_methods() {
ClassDB::bind_static_method("Metin2AnimPlayer", D_METHOD("get_clip_cache_stats"), &Metin2AnimPlayer::get_clip_cache_stats);
ClassDB::bind_static_method("Metin2AnimPlayer", D_METHOD("clear_clip_cache"), &Metin2AnimPlayer::clear_clip_cache);
ClassDB::bind_method(D_METHOD("set_anim_path", "path"), &Metin2AnimPlayer::set_anim_path);
ClassDB::bind_method(D_METHOD("get_anim_path"), &Metin2AnimPlayer::get_anim_path);
ClassDB::bind_method(D_METHOD("set_model_path", "path"), &Metin2AnimPlayer::set_model_path);
@@ -49,6 +142,7 @@ void Metin2AnimPlayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("selfcheck", "samples"), &Metin2AnimPlayer::selfcheck, DEFVAL(24));
ClassDB::bind_method(D_METHOD("get_accumulation"), &Metin2AnimPlayer::get_accumulation);
ClassDB::bind_method(D_METHOD("get_events"), &Metin2AnimPlayer::get_events);
ClassDB::bind_method(D_METHOD("get_effect_bone_pose", "bone"), &Metin2AnimPlayer::get_effect_bone_pose);
ClassDB::bind_method(D_METHOD("get_loop_data"), &Metin2AnimPlayer::get_loop_data);
ClassDB::bind_method(D_METHOD("get_motion_data"), &Metin2AnimPlayer::get_motion_data);
ClassDB::bind_static_method("Metin2AnimPlayer",
@@ -63,6 +157,7 @@ void Metin2AnimPlayer::_bind_methods() {
PropertyInfo(Variant::STRING, "sound"),
PropertyInfo(Variant::VECTOR3, "pos")));
ADD_SIGNAL(MethodInfo("playback_finished"));
ADD_SIGNAL(MethodInfo("motion_event_detailed", PropertyInfo(Variant::DICTIONARY, "event")));
ADD_PROPERTY(PropertyInfo(Variant::STRING, "anim_path", PROPERTY_HINT_GLOBAL_FILE, "*.gr2,*.msa"),
"set_anim_path", "get_anim_path");
@@ -132,17 +227,11 @@ static String strip_dpath(const String &raw) {
return k >= 0 ? p.substr(k) : p; // "ymir work/pc/.../x.gr2"
}
String Metin2AnimPlayer::resolve_anim_gr2(const String &spec) {
String Metin2AnimPlayer::resolve_anim_gr2(const String &spec, const fmt::Msa &m) {
String low = spec.to_lower();
if (!low.ends_with(".msa")) {
return spec; // already a .gr2 path
}
fmt::Msa m;
std::string e;
if (!fmt::parse_msa_file(std::string(spec.utf8().get_data()), m, &e)) {
UtilityFunctions::push_error(String("[Metin2AnimPlayer] .msa parse: ") + String(e.c_str()));
return spec;
}
const String motion = String(m.motion_gr2.c_str());
const String base = motion.replace("\\", "/").get_file();
const String msa_dir = spec.get_base_dir();
@@ -171,16 +260,45 @@ String Metin2AnimPlayer::resolve_anim_gr2(const String &spec) {
return cand;
}
Array Metin2AnimPlayer::get_events() const {
Array a;
for (const fmt::MotionEvent &ev : events) {
static Dictionary motion_event_dict(const fmt::MotionEvent &ev) {
Dictionary d;
d["type"] = ev.type;
d["start_time"] = ev.start_time;
d["effect"] = String(ev.effect_file.c_str());
d["sound"] = String(ev.sound_file.c_str());
d["pos"] = Vector3(ev.position[0], ev.position[1], ev.position[2]);
a.push_back(d);
d["bone"] = String(ev.attaching_bone.c_str());
d["attaching"] = ev.attaching;
d["following"] = ev.following;
d["independent"] = ev.independent;
d["fishing_effect"] = ev.fishing_effect;
return d;
}
Dictionary Metin2AnimPlayer::get_effect_bone_pose(const String &bone) {
Dictionary result;
Metin2Model *model = resolve_model();
const gr2::Skeleton *sk = model ? model->gr2_skeleton() : nullptr;
if (!sk || !anim_file) return result;
for (size_t i = 0; i < sk->bones.size() && i < world_buf.size(); ++i) {
if (String(sk->bones[i].name.c_str()) == bone) {
result["transform"] = gr2_to_godot(world_buf[i]);
break;
}
}
return result;
}
void Metin2AnimPlayer::emit_motion_event(const fmt::MotionEvent &ev) {
emit_signal("motion_event", ev.type, String(ev.effect_file.c_str()),
String(ev.sound_file.c_str()), Vector3(ev.position[0], ev.position[1], ev.position[2]));
emit_signal("motion_event_detailed", motion_event_dict(ev));
}
Array Metin2AnimPlayer::get_events() const {
Array a;
for (const fmt::MotionEvent &ev : events) {
a.push_back(motion_event_dict(ev));
}
return a;
}
@@ -280,7 +398,7 @@ void Metin2AnimPlayer::_ready() {
}
void Metin2AnimPlayer::reload() {
anim_file = std::nullopt;
anim_file.reset();
duration = 0.0;
time = 0.0;
prev_time = 0.0;
@@ -294,16 +412,18 @@ void Metin2AnimPlayer::reload() {
}
// .msa -> resolve the real motion .gr2 + pull accumulation / events.
const bool diagnostics = OS::get_singleton()->get_environment("MTGODOT_ANIM_DIAGNOSTICS") == "1";
String gr2_spec = anim_path;
if (anim_path.to_lower().ends_with(".msa")) {
fmt::Msa m;
std::string e;
if (fmt::parse_msa_file(std::string(anim_path.utf8().get_data()), m, &e)) {
if (cached_msa(anim_path, m, &e)) {
msa_metadata = m;
accumulation = Vector3(m.accumulation[0], m.accumulation[1], m.accumulation[2]);
events = m.events;
gr2_spec = resolve_anim_gr2(anim_path);
UtilityFunctions::print(vformat("[Metin2AnimPlayer] .msa -> %s accum=(%.2f %.2f %.2f) events=%d loopdata=%s",
// Reuse the metadata just parsed; path resolution must not parse it again.
gr2_spec = resolve_anim_gr2(anim_path, m);
if (diagnostics) UtilityFunctions::print(vformat("[Metin2AnimPlayer] .msa -> %s accum=(%.2f %.2f %.2f) events=%d loopdata=%s",
gr2_spec, accumulation.x, accumulation.y, accumulation.z, (int)events.size(),
m.has_loop_data ? vformat("%d x [%.3f,%.3f]", m.motion_loop_count,
m.loop_start_time, m.loop_end_time) : String("none")));
@@ -314,7 +434,7 @@ void Metin2AnimPlayer::reload() {
}
gr2::LoadError err;
auto f = mtgodot::gr2_from_file(gr2_spec, &err);
auto f = cached_clip(gr2_spec, &err);
if (!f) {
UtilityFunctions::push_error(String("[Metin2AnimPlayer] anim load failed [") +
String(err.stage.c_str()) + "]: " + String(err.message.c_str()));
@@ -330,7 +450,7 @@ void Metin2AnimPlayer::reload() {
duration = an.duration;
// Diagnostic: how many anim tracks map to a model-skeleton bone by name?
if (Metin2Model *model = resolve_model()) {
if (Metin2Model *model = diagnostics ? resolve_model() : nullptr) {
if (const gr2::Skeleton *sk = model->gr2_skeleton()) {
int matched = 0;
godot::String unmatched;
@@ -356,7 +476,7 @@ void Metin2AnimPlayer::reload() {
last_info = vformat("anim '%s' dur=%.3f tracks=%d anims=%d",
String(an.name.c_str()), duration, (int)an.tracks.size(),
(int)anim_file->file_info().animations.size());
UtilityFunctions::print("[Metin2AnimPlayer] ", last_info);
if (diagnostics) UtilityFunctions::print("[Metin2AnimPlayer] ", last_info);
apply_pose(0.0);
}
@@ -480,7 +600,7 @@ void Metin2AnimPlayer::_process(double delta) {
if (prev_anim_file) {
blend_elapsed += delta; // real seconds, independent of time_scale
if (blend_elapsed >= blend_time) {
prev_anim_file = std::nullopt;
prev_anim_file.reset();
}
}
double next = time + delta * time_scale;
@@ -508,9 +628,7 @@ void Metin2AnimPlayer::dispatch_events(double from, double to) {
double b = std::fmax(0.0, std::fmin(to, duration));
for (const fmt::MotionEvent &ev : events) {
if (a < ev.start_time && ev.start_time <= b) {
emit_signal("motion_event", ev.type, String(ev.effect_file.c_str()),
String(ev.sound_file.c_str()),
Vector3(ev.position[0], ev.position[1], ev.position[2]));
emit_motion_event(ev);
}
}
return;
@@ -522,9 +640,7 @@ void Metin2AnimPlayer::dispatch_events(double from, double to) {
double t = ev.start_time;
bool hit = (a < t && t <= b) || (b > duration && t <= b - duration);
if (hit) {
emit_signal("motion_event", ev.type, String(ev.effect_file.c_str()),
String(ev.sound_file.c_str()),
Vector3(ev.position[0], ev.position[1], ev.position[2]));
emit_motion_event(ev);
}
}
}
+9 -3
View File
@@ -10,6 +10,7 @@
#include <msa.h>
#include <optional>
#include <memory>
#include <vector>
namespace mtgodot {
@@ -67,8 +68,12 @@ public:
// .msa metadata (empty / zero when anim_path is a raw .gr2).
godot::Vector3 get_accumulation() const { return accumulation; }
godot::Array get_events() const; // [{type,start_time,effect,sound,pos}, ...]
// Full affine pose in model GR2 coordinates, shared by CPU/GPU and blending.
godot::Dictionary get_effect_bone_pose(const godot::String &bone);
godot::Dictionary get_loop_data() const;
godot::Dictionary get_motion_data() const; // {duration,next_combo,attack_start_time,...}
static godot::Dictionary get_clip_cache_stats();
static void clear_clip_cache();
// CLIENT-GAP §3.5 — parse the PC combo tables out of playersettingmodule.py
// (chrmgr.ReserveComboAttackNew / RegisterComboAttackNew → CRaceData). Returns
@@ -86,6 +91,7 @@ protected:
static void _bind_methods();
private:
void emit_motion_event(const fmt::MotionEvent &ev);
godot::String anim_path;
godot::NodePath model_path;
bool playing = true;
@@ -95,14 +101,14 @@ private:
double duration = 0.0;
godot::String last_info;
std::optional<gr2::File> anim_file;
std::shared_ptr<const gr2::File> anim_file;
std::vector<gr2::Mat4> world_buf;
std::vector<gr2::Mat4> skin_buf;
// Crossfade state: the outgoing clip is kept and sampled at a frozen time,
// its pose blended into the incoming clip for `blend_time` seconds.
double blend_time = 0.15;
std::optional<gr2::File> prev_anim_file;
std::shared_ptr<const gr2::File> prev_anim_file;
double prev_anim_time = 0.0;
double prev_anim_duration = 0.0;
bool prev_anim_loop = true;
@@ -119,7 +125,7 @@ private:
void apply_pose(double t);
void dispatch_events(double from, double to);
// .msa path -> real motion .gr2 path; passes plain .gr2 paths through.
static godot::String resolve_anim_gr2(const godot::String &spec);
static godot::String resolve_anim_gr2(const godot::String &spec, const fmt::Msa &metadata);
};
} // namespace mtgodot
+81 -16
View File
@@ -69,6 +69,7 @@ void Metin2Model::_bind_methods() {
ClassDB::bind_method(D_METHOD("reload"), &Metin2Model::reload);
ClassDB::bind_method(D_METHOD("get_info"), &Metin2Model::get_info);
ClassDB::bind_method(D_METHOD("get_visual_aabb"), &Metin2Model::get_visual_aabb);
ClassDB::bind_method(D_METHOD("get_fly_target_bounds"), &Metin2Model::get_fly_target_bounds);
ClassDB::bind_method(D_METHOD("get_ground_offset"), &Metin2Model::get_ground_offset);
ClassDB::bind_method(D_METHOD("get_hair_options"), &Metin2Model::get_hair_options);
ClassDB::bind_method(D_METHOD("set_hair_gr2", "p"), &Metin2Model::set_hair_gr2);
@@ -274,14 +275,26 @@ void Metin2Model::_set_lod(int n) {
lod_prev_mi->set_name("LodGhost");
lod_prev_mi->set_layer_mask(1u << 1);
lod_prev_mi->set_mesh(mi->get_mesh());
lod_prev_mi->set_custom_aabb(mi->get_custom_aabb());
add_child(lod_prev_mi);
lod_prev_mi->set_transform(mi->get_transform());
std::vector<Ref<Material>> ghost_mats;
for (int s = 0; s < (int)surf_mats.size(); ++s) {
Ref<Material> src = surf_mats[s];
// A frozen GPU ghost must not share the live, updated bone texture.
Ref<ImageTexture> ghost_bones;
if (gpu_skin_active && bones_img.is_valid()) {
Ref<godot::Image> snapshot = bones_img->duplicate();
ghost_bones = ImageTexture::create_from_image(snapshot);
}
for (int s = 0; s < mi->get_mesh()->get_surface_count(); ++s) {
// CPU hair overrides are not necessarily present in surf_mats.
Ref<Material> src = mi->get_active_material(s);
Ref<Material> dup;
if (src.is_valid()) {
dup = src->duplicate();
Ref<ShaderMaterial> shader = dup;
if (shader.is_valid() && ghost_bones.is_valid()) {
shader->set_shader_parameter("bones_tex", ghost_bones);
}
}
lod_prev_mi->set_surface_override_material(s, dup);
ghost_mats.push_back(dup);
@@ -298,7 +311,11 @@ void Metin2Model::_set_lod(int n) {
_end_lod_fade();
return;
}
if (cpu_mesh.is_valid()) {
if (gpu_skin_active) {
// Keep attachments and the body/hair material boundary across LODs.
_build_gpu_mesh();
gpu_visual_bounds_ready = false;
} else if (cpu_mesh.is_valid()) {
AABB b;
cpu_mesh = build_mesh(*afi, parts, flip_winding, b);
mi->set_mesh(cpu_mesh);
@@ -310,6 +327,9 @@ void Metin2Model::_set_lod(int n) {
}
}
_apply_materials();
if (gpu_skin_active && !current_skin.empty()) {
gpu_skin(current_skin);
}
if (cpu_mesh.is_valid() && !current_skin.empty()) {
cpu_skin(current_skin);
}
@@ -372,6 +392,8 @@ void Metin2Model::_clear_children() {
}
void Metin2Model::reload() {
current_world_pose.clear();
fly_target_bounds_valid = false;
_clear_children();
*file = std::nullopt;
last_info = "";
@@ -595,24 +617,37 @@ void Metin2Model::_load_hair() {
UtilityFunctions::print(vformat("[Metin2Model] hair: %s %d/%d bones matched, %d parts",
hair_gr2.get_file(), matched, (int)hbones.size(), (int)hair_parts.size()));
if (has_cpu_skin_mesh() && !current_skin.empty())
}
void Metin2Model::_refresh_hair_mesh() {
if (gpu_skin_active) {
_build_gpu_mesh();
_apply_materials();
gpu_visual_bounds_ready = false;
if (!current_skin.empty()) gpu_skin(current_skin);
} else if (has_cpu_skin_mesh() && !current_skin.empty()) {
cpu_skin(current_skin);
}
}
void Metin2Model::set_hair_gr2(const String &p) {
if (p == hair_gr2)
return;
hair_gr2 = p;
if (file && *file)
if (file && *file) {
_load_hair();
_refresh_hair_mesh();
}
}
void Metin2Model::set_hair_skin(const String &p) {
if (p == hair_skin)
return;
hair_skin = p;
if (file && *file && !hair_gr2.is_empty())
if (file && *file && !hair_gr2.is_empty()) {
_load_hair();
_refresh_hair_mesh();
}
}
void Metin2Model::set_weapon_gr2(const String &p) {
@@ -793,6 +828,7 @@ void Metin2Model::_load_attach(const String &gr2_rel, const String &bone_name,
}
void Metin2Model::update_weapon_pose(const std::vector<gr2::Mat4> &world_pose) {
current_world_pose = world_pose;
// weapon local = gripPre · handWorld (client: weaponBone[0] composed onto the
// parent attach-bone matrix). gripPre is identity for near-origin weapons, so
// this is backward-compatible with the ones that already looked right.
@@ -804,6 +840,38 @@ void Metin2Model::update_weapon_pose(const std::vector<gr2::Mat4> &world_pose) {
mtgodot::mul4x3(shield_pre, world_pose[shield_bone_idx])));
}
namespace { inline void xform_pt(const gr2::Mat4 &M, const float *v, float *o); }
Dictionary Metin2Model::get_fly_target_bounds() {
// Legacy caches its sphere until the model is reset. Do not let particles,
// hair, weapons, LOD ghosts or subsequent animation frames shift the centre.
if (!fly_target_bounds_valid) {
const gr2::FileInfo *fi = active_fi();
bool found = false;
AABB bounds;
if (fi) for (const auto &mesh : fi->meshes) {
for (size_t slot = 0; slot < mesh.bone_bounds.size(); ++slot) {
const auto &obb = mesh.bone_bounds[slot];
const int bone = slot < mesh.bone_bindings.size() ? mesh.bone_bindings[slot] : -1;
if (!obb.valid || bone < 0 || bone >= (int)current_world_pose.size()) continue;
for (int corner = 0; corner < 8; ++corner) {
float point[3], transformed[3];
for (int axis = 0; axis < 3; ++axis) point[axis] = (corner & (1 << axis)) ? obb.max[axis] : obb.min[axis];
xform_pt(current_world_pose[bone], point, transformed);
const Vector3 p(transformed[0], transformed[1], transformed[2]);
bounds = found ? bounds.expand(p) : AABB(p, Vector3());
found = true;
}
}
}
if (found) { fly_target_bounds = bounds; fly_target_bounds_valid = true; }
}
Dictionary result;
result["valid"] = fly_target_bounds_valid;
result["bounds"] = fly_target_bounds;
return result;
}
String Metin2Model::_guess_texture_dir() const {
if (!texture_dir.is_empty()) {
return texture_dir;
@@ -1137,6 +1205,7 @@ void Metin2Model::_apply_materials() {
md.albedo = hair_tex;
md.blend = mtgodot::BlendMode::AlphaTest;
md.alpha_scissor = 0.5f;
md.two_sided = true; // Match CPU hair cards when viewed from behind.
md.skinned = true;
md.bones_tex = bones_tex;
Ref<Material> hm = mtgodot::make_material(md);
@@ -1591,10 +1660,11 @@ void Metin2Model::gpu_skin(const std::vector<gr2::Mat4> &skin) {
return;
}
current_skin = skin;
// The shader owns deformation after this point. Sample the posed bounds
// once for culling and initial camera framing; doing the full vertex walk on
// every GPU frame would erase the CPU saving this path is intended to make.
if (!gpu_visual_bounds_ready && _update_skinned_bounds(skin)) {
// Culling must follow every pose, including offscreen actors and one-shots.
// Only the initial ready notification reframes selection cameras; emitting
// it on every pose would make the camera follow the breathing animation.
const bool bounds_updated = _update_skinned_bounds(skin);
if (!gpu_visual_bounds_ready && bounds_updated) {
gpu_visual_bounds_ready = true;
emit_signal("visual_bounds_changed");
}
@@ -1620,12 +1690,7 @@ String Metin2Model::get_info() const {
}
AABB Metin2Model::get_visual_aabb() {
// Consumers such as CharSelect ask only while framing. Refresh on demand
// so their final camera fit reflects the current GPU pose without adding a
// per-frame CPU vertex walk to every world actor.
if (gpu_skin_active && !current_skin.empty()) {
_update_skinned_bounds(current_skin);
}
// Both animation paths publish bounds when sampling, before camera queries.
if (have_visual_bounds) {
return visual_bounds;
}
+5
View File
@@ -130,6 +130,7 @@ public:
// Deformed mesh bounds in this node's local GR2 coordinate frame. Unlike
// MeshInstance3D::get_aabb(), this follows the GPU shader pose as well.
godot::AABB get_visual_aabb();
godot::Dictionary get_fly_target_bounds();
// Parent-space Y adjustment that places the bind-pose model's lowest point
// on the actor origin. GR2 is Z-up/cm; this is converted to Godot Y/metres.
double get_ground_offset() const { return ground_offset; }
@@ -187,6 +188,7 @@ private:
godot::Ref<godot::ImageTexture> hair_tex; // resolved hair albedo (for GPU path)
int gpu_base_surf = 0; // base surfaces before appended GPU hair
void _load_hair();
void _refresh_hair_mesh();
void _build_gpu_mesh(); // base + remapped hair surfaces -> mi->mesh (GPU-skin path, §2.2)
godot::String weapon_gr2; // attached weapon .gr2 (or "")
@@ -244,6 +246,9 @@ private:
bool gpu_visual_bounds_ready = false;
bool have_visual_bounds = false;
godot::AABB visual_bounds;
std::vector<gr2::Mat4> current_world_pose;
godot::AABB fly_target_bounds;
bool fly_target_bounds_valid = false;
std::vector<gr2::Mat4> current_skin;
godot::Ref<godot::Image> bones_img; // RGBAF 3 x bone_count
godot::Ref<godot::ImageTexture> bones_tex;
+9 -3
View File
@@ -611,12 +611,13 @@ void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj,
}
}
// Tree proxy:每 treefile 一个共享 mesh + MultiMeshInstance3D。
// 当前从 .spt 嗅探真实 bark/composite atlas;几何仍等待离线 SpeedTree exporter。
// Native extracted tree geometry, shared once per treefile across instances.
for (auto &kv : tree_groups) {
if (kv.second.xforms.empty())
continue;
Ref<godot::ArrayMesh> tmesh = get_tree_proxy_mesh(kv.first, *resolver, 12.0f);
Ref<godot::ArrayMesh> tmesh = get_tree_mesh(kv.first, *resolver, 12.0f);
const bool native = String(tmesh->get_meta("tree_geometry", "proxy")) == "native_spt";
(native ? native_trees_placed : proxy_trees_placed) += (int)kv.second.xforms.size();
Ref<MultiMesh> mm;
mm.instantiate();
mm->set_transform_format(MultiMesh::TRANSFORM_3D);
@@ -628,6 +629,7 @@ void Metin2World::place_chunk_objects(int tx, int ty, Node3D *root, int &n_obj,
mmi->set_name(String("Trees_") + String(kv.first.c_str()).get_file().get_basename());
mmi->set_layer_mask(1u << 0);
mmi->set_multimesh(mm);
mmi->set_meta("tree_geometry", native ? "native_spt" : "proxy");
// §3.3: 树影已烘焙进 shadowmap.dds;proxy 几何的实时投影形状也不对 -> 默认不投。
mmi->set_cast_shadows_setting(tree_shadows ? GeometryInstance3D::SHADOW_CASTING_SETTING_ON
: GeometryInstance3D::SHADOW_CASTING_SETTING_OFF);
@@ -827,6 +829,7 @@ void Metin2World::set_focus_position(double gx_m, double gz_m) {
}
void Metin2World::_process(double) {
update_water_animation();
for (int n = 0; n < stream_budget && !stream_queue.empty(); ++n) {
auto [tx, ty] = stream_queue.front();
stream_queue.erase(stream_queue.begin());
@@ -854,6 +857,7 @@ void Metin2World::unload_map() {
chunks_built = chunks_failed = chunks_splatted = 0;
objects_placed = objects_skipped = objects_missing_model = 0;
trees_placed = tree_species = 0;
native_trees_placed = proxy_trees_placed = 0;
objects_mdatr_built = 0;
build_ms = 0;
splat_ready = registry_ok = false;
@@ -997,6 +1001,8 @@ Dictionary Metin2World::get_load_report() const {
d["ambience_sources"] = (int)ambience_sources.size();
d["trees_placed"] = trees_placed;
d["tree_species"] = tree_species;
d["native_trees_placed"] = native_trees_placed;
d["proxy_trees_placed"] = proxy_trees_placed;
d["objects_mdatr_built"] = objects_mdatr_built; // 使用 .mdatr 精确碰撞的对象数
d["static_meshes"] = static_cache.loaded;
d["build_ms"] = build_ms;
+1
View File
@@ -143,6 +143,7 @@ private:
int chunks_built = 0, chunks_failed = 0, chunks_splatted = 0;
int objects_placed = 0, objects_skipped = 0, objects_missing_model = 0;
int trees_placed = 0, tree_species = 0;
int native_trees_placed = 0, proxy_trees_placed = 0;
int objects_mdatr_built = 0;
godot::Node3D *objects_root = nullptr;
double build_ms = 0;

Some files were not shown because too many files have changed in this diff Show More