Implement playable Mac client and rendering validation
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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.
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
@@ -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-hoc;CODESIGN_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"
|
||||
|
||||
@@ -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_server;skill_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`:2301–2307 对应 ent_trent/ent_guru/ent_hu/ent_red/ent_black/ent_huge/ent_elder;2311–2315 对应前五种。共享模型仍保留独立 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,每秒一次,记录单位 KiB;Godot 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 可并行推进。工期在首次真实闭环和树怪截图产生后根据实际缺陷估算。
|
||||
@@ -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 对旧基线 PASS,hair_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=0;GPU 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=6(4 实为 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 使用 RotationSpeed,CCW 使用负值,无额外随机减速;`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.5,RGB 饱和后混合,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、IndependentFlag;native `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 release,ad-hoc,codesign 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.342ms;GPU 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` PASS,24.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.051ms,GPU = 16.704/16.965/17.133ms;CPU 八角色组仍有 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.218ms;CPU 八角色动作切换耗时最大 23.262ms,仍是明确长帧。各组 Godot static memory 增量约 0.07–0.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-256:engine `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 scissor(0.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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user