Compare commits
14
Commits
c3a6fb973e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d217b313 | ||
|
|
1db9e9a129 | ||
|
|
400e3e8ea5 | ||
|
|
85ce75e707 | ||
|
|
474a709f69 | ||
|
|
1933a5ceda | ||
|
|
96c29d2bb7 | ||
|
|
cc5573ddc1 | ||
|
|
f39a55fdd5 | ||
|
|
374d4165d8 | ||
|
|
1ab68bd06e | ||
|
|
e0c1853909 | ||
|
|
fb4d2d222b | ||
|
|
cea79fe17c |
+12
@@ -5,6 +5,8 @@
|
||||
|
||||
# build
|
||||
/build/
|
||||
# 渲染测试的落图输出(test/rendering 用例自建)
|
||||
/project/build/
|
||||
/build-ios/
|
||||
/build-android/
|
||||
/build-classic/
|
||||
@@ -35,6 +37,16 @@ compile_commands.json
|
||||
*.a
|
||||
.env.live-smoke.local
|
||||
|
||||
# playable network tests: local scenario/fixture configs, credential files and
|
||||
# run outputs never enter git. Only scenario.example.json and the static gate
|
||||
# fixtures in test/playable are committed. Run outputs live under /build/playable.
|
||||
/test/playable/*.local.json
|
||||
/test/playable/*.local.txt
|
||||
/test/playable/runs/
|
||||
*.credentials
|
||||
*.credentials.json
|
||||
.env.*.local
|
||||
|
||||
# clangd
|
||||
/.cache/
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
@@ -6,11 +6,11 @@ window = {
|
||||
"name" : "SystemDialog",
|
||||
"style" : ("float",),
|
||||
|
||||
"x" : (SCREEN_WIDTH - 200) /2,
|
||||
"y" : (SCREEN_HEIGHT - 288) /2,
|
||||
"x" : SCREEN_WIDTH/2 - 100,
|
||||
"y" : SCREEN_HEIGHT/2 - 114,
|
||||
|
||||
"width" : 200,
|
||||
"height" : 288,
|
||||
"height" : 258,
|
||||
|
||||
"children" :
|
||||
(
|
||||
@@ -22,7 +22,7 @@ window = {
|
||||
"y" : 0,
|
||||
|
||||
"width" : 200,
|
||||
"height" : 288,
|
||||
"height" : 258,
|
||||
|
||||
"children" :
|
||||
(
|
||||
@@ -39,27 +39,13 @@ window = {
|
||||
"over_image" : ROOT + "XLarge_Button_02.sub",
|
||||
"down_image" : ROOT + "XLarge_Button_03.sub",
|
||||
},
|
||||
{
|
||||
"name" : "mall_button",
|
||||
"type" : "button",
|
||||
|
||||
"x" : 10,
|
||||
"y" : 57,
|
||||
|
||||
"text" : uiScriptLocale.SYSTEM_MALL,
|
||||
"text_color" : 0xffF8BF24,
|
||||
|
||||
"default_image" : ROOT + "XLarge_Button_02.sub",
|
||||
"over_image" : ROOT + "XLarge_Button_02.sub",
|
||||
"down_image" : ROOT + "XLarge_Button_02.sub",
|
||||
},
|
||||
|
||||
{
|
||||
"name" : "system_option_button",
|
||||
"type" : "button",
|
||||
|
||||
"x" : 10,
|
||||
"y" : 87,
|
||||
"y" : 57,
|
||||
|
||||
"text" : uiScriptLocale.SYSTEMOPTION_TITLE,
|
||||
|
||||
@@ -72,7 +58,7 @@ window = {
|
||||
"type" : "button",
|
||||
|
||||
"x" : 10,
|
||||
"y" : 117,
|
||||
"y" : 87,
|
||||
|
||||
"text" : uiScriptLocale.GAMEOPTION_TITLE,
|
||||
|
||||
@@ -85,7 +71,7 @@ window = {
|
||||
"type" : "button",
|
||||
|
||||
"x" : 10,
|
||||
"y" : 147,
|
||||
"y" : 117,
|
||||
|
||||
"text" : uiScriptLocale.SYSTEM_CHANGE,
|
||||
|
||||
@@ -98,7 +84,7 @@ window = {
|
||||
"type" : "button",
|
||||
|
||||
"x" : 10,
|
||||
"y" : 177,
|
||||
"y" : 147,
|
||||
|
||||
"text" : uiScriptLocale.SYSTEM_LOGOUT,
|
||||
|
||||
@@ -111,7 +97,7 @@ window = {
|
||||
"type" : "button",
|
||||
|
||||
"x" : 10,
|
||||
"y" : 217,
|
||||
"y" : 177,
|
||||
|
||||
"text" : uiScriptLocale.SYSTEM_EXIT,
|
||||
|
||||
@@ -124,7 +110,7 @@ window = {
|
||||
"type" : "button",
|
||||
|
||||
"x" : 10,
|
||||
"y" : 247,
|
||||
"y" : 217,
|
||||
|
||||
"text" : uiScriptLocale.CANCEL,
|
||||
|
||||
|
||||
+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"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- generated-by: gsd-doc-writer -->
|
||||
# Android 真机测试指南
|
||||
|
||||
本文说明如何在 Android 真机上安装 `mtgodot-poc`、生成并导入资源包、验证资源挂载,以及进行 40250 客户端测试。
|
||||
本文说明如何在 Android 真机上安装本客户端(APK 产物名仍是 `mtgodot-poc.apk`,包名 `org.internal.mtgodotpoc`)、生成并导入资源包、验证资源挂载,以及进行 40250 客户端测试。
|
||||
|
||||
## 1. 当前测试范围与已知前置条件
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
当前仓库的资源部署存在一个需要特别注意的限制:
|
||||
|
||||
- `export-android.sh --install` 会把资源推到 `/sdcard/Android/data/org.internal.mtgodotpoc/files/assets.zip`。
|
||||
- `project/asset_pack.gd` 首先查找 `user://assets.zip`,当前没有显式把上述外部目录加入候选路径。
|
||||
- `project/asset_pack.gd` 的候选顺序是 `MT_ASSETS_ZIP` 环境变量 → `user://assets.zip` → `OS.get_user_data_dir()/assets.zip` → 可执行文件目录 → `res://../assets.zip`(见 `_find_local_zip()`);上述外部目录不在候选路径里。
|
||||
- 因此,Debug 真机测试应把外部目录中的压缩包再复制到应用私有 `files/` 目录,确保客户端的 `user://assets.zip` 能命中。
|
||||
|
||||
另外,40250 classic 网络栈当前由进程环境变量 `MT_PROTOCOL=classic` 选择。Android 通过 Activity 启动 APK 时不能直接继承 Mac 终端的环境变量;如果 APK 尚未增加 Android 侧的协议配置入口或将 classic 设为移动端默认值,则只能完成 APK、资源和 UI 验证,不能据此宣称已经完成真实 40250 登录验收。
|
||||
@@ -34,7 +34,7 @@
|
||||
进入仓库并设置环境变量。路径按本机安装位置调整:
|
||||
|
||||
```bash
|
||||
cd /Users/shenlei/Work/mt/mtgodot-poc
|
||||
cd /Users/shenlei/Work/mt/metin2-client
|
||||
|
||||
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
|
||||
export ANDROID_HOME="$ANDROID_SDK_ROOT"
|
||||
@@ -369,7 +369,7 @@ shasum -a 256 build/export/mtgodot-poc.apk \
|
||||
首次测试按以下顺序执行:
|
||||
|
||||
```bash
|
||||
cd /Users/shenlei/Work/mt/mtgodot-poc
|
||||
cd /Users/shenlei/Work/mt/metin2-client
|
||||
|
||||
./pack-assets.sh
|
||||
./export-android.sh Debug
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
# mtgodot-poc — 未完成工作清单
|
||||
# metin2-client — 未完成工作清单
|
||||
|
||||
> 更新:2026-08-29 · 配套 [`GODOT-POC-PLAN.md`](./GODOT-POC-PLAN.md)(计划)、[`MIDREVIEW.md`](./MIDREVIEW.md)(中期评审)、
|
||||
> [`SHINSOO-WORLD-RENDERING.md`](./SHINSOO-WORLD-RENDERING.md)(世界渲染实施)、[`PARITY-GAP.md`](./PARITY-GAP.md)(距参考图的保真化差距)。
|
||||
@@ -55,8 +55,8 @@
|
||||
| B6 | 贴图采样设置(filter / wrap / mip / 各向异性) | ⬜ | S | 现在 shader 里写死 `filter_linear_mipmap, repeat_enable`。 |
|
||||
| B7 | 顶点色 | ✅ 排查=不适用 | — | 全资产 `.gr2`(PC/monster2/effect)无 `DiffuseColor`/`Color0` 顶点成员(grep assets = 0)。Metin2 逐顶点色 = 地形雾(`MapOutdoorRenderSTP` 运行时动态 VB,非资产,Godot `Environment` 雾已覆盖)+ `.mse` `colorfactor`/粒子色(属 E7/§1.2)。libgr2 / `build_mesh` 无需动。详见 PARITY §2.6。 |
|
||||
| B8 | 切线帧(若确有法线贴图资产) | ⬜ | M | PNT332 无切线。xrender 时期结论:Metin2 资产**基本不用**法线贴图 —— 先排查有没有,再决定是否做(`SurfaceTool::generate_tangents()` 或从 UV 算)。 |
|
||||
| B9 | 镜面 / 自发光 | 🟡 | M | Sphere-map 高光管线已端口(`SRC_MIX`/`SRC_SKIN` 加 `spec_map`/`spec_power`/`spec_enable`,复现 `Material.cpp:305 __ApplySpecularRenderState`:不透明面、相机空间 `reflect().xy` 查 `spheremap.jpg`、`EMISSION += sphere*tex.a*power`)。`Metin2Model.specular_power` 属性,默认 0 休眠 → 需装备层用 `item_proto bSpecular/100` 逐部位驱动。`SRC_ADD` / `SRC_ADD_SKIN` 已覆盖 additive 发光的一种,且 additive 动画面不再漏蒙皮。 |
|
||||
| B10 | 背面剔除 / 双面 | 🟡 | S | 世界静态对象(W3/B1)已按 `two_sided` + 贴图名切 `CULL_DISABLED`。角色 `m2_material` shader 仍写死 `cull_back`(`MaterialDesc::two_sided` 死字段)。需给角色也按材质切。 |
|
||||
| B9 | 镜面 / 自发光 | 🟡 | M | Sphere-map 高光管线已端口(`SRC_MIX`/`SRC_SKIN` 加 `spec_map`/`spec_power`/`spec_enable`,复现 `Material.cpp:305 __ApplySpecularRenderState`:不透明面、相机空间 `reflect().xy` 查 `spheremap.jpg`、`EMISSION += sphere*tex.a*power`)。`Metin2Model.specular_power` 属性,默认 0 休眠。✅ 装备层已接:`project/ui/equip_model.gd:283-288` `_apply_body()` 读 `item_proto` 的 `specular` 列并 `set("specular_power", sp / 100.0)`。⬜ 只驱动身体/护甲一个部位(武器 / 发型 / 时装未接),且 power 未按受控参考帧标定(PARITY §0 门禁)。`SRC_ADD` / `SRC_ADD_SKIN` 已覆盖 additive 发光的一种,且 additive 动画面不再漏蒙皮。 |
|
||||
| B10 | 背面剔除 / 双面 | ✅ 首版 | S | 世界静态对象(W3/B1)已按 `two_sided` + 贴图名切 `CULL_DISABLED`。✅ 角色也已按材质切:`m2_material.cpp:188-211` `shader_for()` 对 `two_sided` 把 `cull_back` replace 成 `cull_disabled`(opaque/blend × 普通/蒙皮 四个变体各自缓存),`metin2_model.cpp:1182`(身体)/ `:769`(武器)经 `decide_two_sided()` 驱动,`_load_hair()` 直接置 true。⬜ 判据仍是启发式 —— `gr2::MaterialInfo.two_sided` 按材质名(`2side`/`twoside`/`double`/`leaf`/`tree`/`ivy`/`fence`)+ 表面名兜底(`hair`/`cloak`/`cape`/`skirt`/`ribbon`/`leaf`),不是真 `ExtendedData` 的 "Two-sided"。 |
|
||||
| B11 | winding 逐模型验 | ⬜ | S | `flip_winding` 默认 false(4 个骨架验过,未全量)。 |
|
||||
|
||||
---
|
||||
@@ -117,14 +117,14 @@
|
||||
|
||||
| ID | 项 | 状态 | 规模 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| F0 | vendored 原生依赖 | ✅ | S | libsodium / libzstd / miniLZO 从 Homebrew 改为**源码内建**(`extension/third_party/`,pinned submodule + vendored miniLZO)—— 同一棵树可交叉编 NDK / iOS SDK。别名 `mt3p::sodium` / `mt3p::zstd` / `mt3p::minilzo`,无源码改动,macOS 9/9 CTest + 真实 item/mob_proto 解码逐字一致。详见 `docs/THIRD-PARTY.md`。miniLZO 是 GPL —— 自用可接受(同 oodle1.c),发布前须换。 |
|
||||
| F0 | vendored 原生依赖 | ✅ | S | libsodium / libzstd / miniLZO 从 Homebrew 改为**源码内建**(`extension/third_party/`,pinned submodule + vendored miniLZO)—— 同一棵树可交叉编 NDK / iOS SDK。2026-09-02 起第四个:**Crypto++ 8.4.0**(vendored 源码,`MT_PROTOCOL=classic` 的 DH2/CTR cipher 用,公有领域许可)。别名 `mt3p::sodium` / `mt3p::zstd` / `mt3p::minilzo` / `mt3p::cryptopp`,无源码改动,macOS 9/9 CTest + 真实 item/mob_proto 解码逐字一致。详见 `docs/THIRD-PARTY.md`。miniLZO 是 GPL —— 自用可接受(同 oodle1.c),发布前须换。 |
|
||||
| F1 | iOS(iPhone 16 / Metal) | 🟡 | M | ✅ 交叉编译打通:`./build-ios.sh` 出 `libmtgodot.ios.template_{debug,release}.a`(arm64 / minos 15.0),全依赖(libsodium/zstd/miniLZO/libgr2/formats/mtnet/godot-cpp)随之交叉编过;iOS 强制 extension 为 STATIC(不能 dlopen),`.gdextension` 加了 `[dependencies] ios.*` 归档清单。⬜ 剩 Godot 4.7 iOS 导出模板 + 导出 preset 链 `project/bin/ios/*.a` + 免费 provisioning 签名 + iPhone 16 真机跑 + Release/strip。预留 2–3 天。详见 `docs/PLATFORMS.md`。 |
|
||||
| F2 | Android(一加 13 / Vulkan) | 🟡 | M | ✅ CMake 已接 `CMAKE_SYSTEM_NAME==Android`(SHARED `.so` + `arm64` 名字后缀);`./build-android.sh` 走 NDK 自带 `android.toolchain.cmake`(`arm64-v8a` / `android-24`)。⬜ 本机无 NDK —— 装 NDK 后 `./build-android.sh` 出 `.so`,再 Godot Android 导出 preset(Gradle)+ 一加 13 真机跑。**只验 Vulkan**,不做 Compatibility(GLES3) / 老机。详见 `docs/PLATFORMS.md`。 |
|
||||
| F3 | 三端一致性 | ⬜ | S | 同工程 / 同 t / 同机位截图,iPhone 16 · 一加 13 · Mac 对齐。 |
|
||||
| F4 | 移动端性能 / 帧预算 | 🟡 | M | ✅ **GPU 纹理压缩**:`extension/src/texture_util.{h,cpp}` `make_color_texture()` —— 移动端(`OS.has_feature("mobile")`)或 `MTGODOT_TEXCOMP=1` 时对颜色贴图运行时 ASTC 8x8(~2bpp vs RGBA8 32bpp),编码失败回退 RGBA8;桌面默认关,逐调用点保留原 mipmap 标志 → OFF 时与旧路径逐字节一致。接入:角色皮肤(`_load_dds`)、建筑 albedo(`static_object`)、树皮/树冠(`tree_placeholder`);splat 控制图 / 阴影图 / 水 / HUD 保持 RGBA8。macOS+iOS 编过,ctest 9/9,桌面渲染无变化,`MTGODOT_TEXCOMP=1` 角色渲染正常。⬜ 剩:一加 13 / iPhone 16 单 + N 角色 bench(`MTGODOT_GPUSKIN=1` 人群)、构建成本(~170ms/角色,A5/G2)、移动端画质核对。 |
|
||||
| F5 | App 生命周期 | 🟡 | S–M | ✅ `project/app_lifecycle.gd`(AppLifecycle 节点):把 `NOTIFICATION_APPLICATION_PAUSED/RESUMED` / `FOCUS_IN/OUT` / `OS_MEMORY_WARNING` / `WM_GO_BACK_REQUEST` 收成信号,默认动作 = 后台暂停树 + 停 BGM + 降 `max_fps`,前台还原;`bind(m2client, audio)` 一键接。`M2Client`(C++):`_notification` 收 PAUSED/RESUMED → `suspend()/resume()`(挂起时不 pump socket),`reconnect()` 用存好的凭据重登;新信号 `suspended`/`resumed`。已接 `login.gd`(回前台若在游戏中自动重连)+ `world_demo.gd`。headless smoke(suspend/resume/reconnect 绑定 + 幂等 + 信号)通过,ctest 9/9,iOS 编过。渲染上下文丢失由 Godot Vulkan 自恢复,GPU 资源全 RenderingServer 托管,无需处理。⬜ 剩真机验证后台 30s→掐连接→回前台重连、iOS 内存告警实际触发。 |
|
||||
| F6 | 打包 | ⬜ | S–M | macOS / iOS / Android export preset、资产打包策略、iOS 签名。自用,无商店上架。 |
|
||||
| F8 | 实体点选精度 | ✅ 首版 | S | `player_controller._ray_pick_t()`:射线 vs 每个 pickable 下所有 `MeshInstance3D` 的**世界 AABB 合并 + slab 相交**(逐实体精确,大怪 / 小怪不再被固定 1.4 m 半径误判;`grow(0.15)` 宽容度)。无网格回退旧胶囊近似。对齐 `EterGrnLib/ModelInstanceCollisionDetection::Intersect` 的**结果**,不复刻逐三角。`eterngrn_polish_test.gd` 覆盖(命中 t / 未命中 -1 / 擦边 / 无网格回退)。⬜ 逐三角 / 骨骼包围体、`GetHeight`。 |
|
||||
| F9 | 实体点选精度 | ✅ 首版 | S | `player_controller._ray_pick_t()`:射线 vs 每个 pickable 下所有 `MeshInstance3D` 的**世界 AABB 合并 + slab 相交**(逐实体精确,大怪 / 小怪不再被固定 1.4 m 半径误判;`grow(0.15)` 宽容度)。无网格回退旧胶囊近似。对齐 `EterGrnLib/ModelInstanceCollisionDetection::Intersect` 的**结果**,不复刻逐三角。`eterngrn_polish_test.gd` 覆盖(命中 t / 未命中 -1 / 擦边 / 无网格回退)。⬜ 逐三角 / 骨骼包围体、`GetHeight`。 |
|
||||
| F7 | 触屏输入 / 手势相机 | ✅ | S | `game_camera.gd`:单指拖拽环绕(8px deadzone,低于视作点击)/ 双指捏合缩放;`player_controller.gd`:单指轻点(≤12px 位移 & ≤0.35s)= 点地 / 点选,拖拽或双指手势时不触发。`project.godot` 关 `emulate_mouse_from_touch`(避免与轻点重复触发),桌面鼠标路径不变。两脚本 `--check-only` + 项目导入无错。 |
|
||||
| F8 | `godot-cpp` 钉稳定版 | ✅ | S | gitlink 精确锁定 `101ae38034304346a46ea9ea84ae156d3e860496`,`.gitmodules` 已移除移动的 `master` branch 配置;目标 API 固定为 Godot 4.7。 |
|
||||
|
||||
@@ -183,7 +183,7 @@ UI / Python 脚本层、多角色 AI、网络、玩法逻辑、eterpack(Phase
|
||||
|
||||
### Sprint 1 —— 小项收尾(~1 周)
|
||||
|
||||
已完成:`A3` · `A6` · `F8` · `I1`,并补完 `A9`/`A10` 正确性问题。
|
||||
已完成:`A3` · `A6` · `F8`(`godot-cpp` 钉版)· `I1`,并补完 `A9`/`A10` 正确性问题。
|
||||
剩余:`A5`(CPU 路径原地更新)· `A7`(前台 Metal stress 基线)· `A8`+`H1`(桥接层畸形用例与全语料 draw 冒烟)。
|
||||
|
||||
### Sprint 2 —— 组合基础(~2 周)
|
||||
|
||||
+154
-11
@@ -10,7 +10,7 @@
|
||||
> **[待读参考]** 并给出应读的参考文件,而不是臆造。
|
||||
>
|
||||
> 路径约定:`REF/` = `/Users/shenlei/Work/mt/40250/ClientVS22/source/`,
|
||||
> `POC/` = `/Users/shenlei/Work/mt/mtgodot-poc/`。
|
||||
> `POC/` = `/Users/shenlei/Work/mt/metin2-client/`。
|
||||
>
|
||||
> 创建 / 修订日期:2026-09-04
|
||||
|
||||
@@ -355,8 +355,10 @@
|
||||
普攻 `_swing_skill=0`),被击退目标记 `_victim_flush`。`_emit_swing()` 改为**只有拿不到
|
||||
命中窗**(headless / 缺资源)才在挥击时立即发 `CG_ATTACK`。`_flush_victim_list()`(对齐
|
||||
`PythonPlayerEventHandler.cpp:194`)帧末把 `_victim_flush`(≤16)打包 `client.sync_positions()`。
|
||||
几何判定为近似:`reach = max(WeaponLength, 窗内采样最大水平偏移)`,命中 = 目标落在正面 `±60°`、
|
||||
`(reach+20+45) cm` 弧内;真实动态圆柱扫掠需骨骼矩阵 + defending sphere(随 §3.1 留桩)。
|
||||
~~几何判定为近似:`reach = max(WeaponLength, 窗内采样最大水平偏移)`,命中 = 目标落在正面 `±60°`、
|
||||
`(reach+20+45) cm` 弧内;真实动态圆柱扫掠需骨骼矩阵 + defending sphere(随 §3.1 留桩)。~~
|
||||
**增量 130 已替换**为 `.msa` 刀尖扫掠 Z 圆柱 vs `.msm` 防御球的 `detect_z_cylinder` 判定(见 §3.5 修改 4 /
|
||||
C.5 增量 130)。
|
||||
`combat_fx_test.gd` 加 `FakeNetWorld` + 6 段断言(`CG_ATTACK` 延后 / 窗内命中 / `CG_SYNC_POSITION` /
|
||||
COMBO 单次 / 正面弧 / 越窗收起)。C++ 21/21 + 7 个 GDScript 回归全绿。留给增量 84:武器种类 →
|
||||
`combo_motion_mode` 映射。
|
||||
@@ -700,7 +702,7 @@
|
||||
新增 `_attack_ctx()` 从 `client.get_duel()` / `get_pvp_relations()` / `get_guild_wars()` /
|
||||
`get_party()` 装配 ctx(observer / main_vid / pk_mode / duel_mode / duel_opponents /
|
||||
pvp_pairs / gvg_pairs / party_vids)。5 处调用点(含 `__ReserveProcess_ClickActor` 第 3 步、
|
||||
`_can_shot`、`_hit_geometry`)无签名变化。
|
||||
`_can_shot`、`_hit_geometry`;增量 130 起后者并入 `_normal_attack_process`)无签名变化。
|
||||
④ 自检 `project/entity_rules_test.gd`(第 15 个 canonical,25 组断言);`combat_fx_test` /
|
||||
`netplay_test` / `fly_test` 的怪物夹具补 `ch_type: 2`(此前无 `ch_type` → `_entity_kind` 归 PC,
|
||||
桩按 PC 也「可打」;1:1 规则下和平同帝国 PC 不可打 → 夹具需明确怪类别,与真实服务器一致)。
|
||||
@@ -2352,9 +2354,23 @@ OnHit(uSkill, victimActor, isSendPacket):
|
||||
**只有拿不到命中窗数据**(headless / 缺资源)才在挥击时立即发 `CG_ATTACK`,有窗时交给
|
||||
`_attack_process`。`_flush_victim_list()`(对齐 `PythonPlayerEventHandler.cpp:194`)在
|
||||
`_process` 帧末把 `_victim_flush`(≤ `SYNC_POSITION_LIMIT` 16)打包 `client.sync_positions()`。
|
||||
几何判定是 **近似**:`reach = max(WeaponLength, 窗内采样最大水平偏移)`,命中 = 目标落在正面
|
||||
~~几何判定是 **近似**:`reach = max(WeaponLength, 窗内采样最大水平偏移)`,命中 = 目标落在正面
|
||||
`±60°`、`(reach + 20 + 45) cm` 弧内;真实的动态圆柱-圆柱扫掠需要骨骼矩阵 + defending
|
||||
sphere 数据(同 §3.1 actor 碰撞一起留桩)。`combat_fx_test.gd` 加 `FakeNetWorld` +
|
||||
sphere 数据(同 §3.1 actor 碰撞一起留桩)。~~
|
||||
**增量 130 升级为逐行判定**:删除 `_hit_deduped` / `_hit_geometry`,新增 `_normal_attack_process()`
|
||||
对齐 `__NormalAttackProcess`:
|
||||
- 距离门 `v3Distance = (dX, dZ, dZ)`:出货代码平面 Y 不参与、高度差算两次,原样保留;上限 300 cm,
|
||||
`IS_HUGE_RACE` 为 500 cm。
|
||||
- 命中窗 `[start,end]` 与 `[t-dt, t]` 相交后查 `m_HitDataMap`:COMBO 同窗不再判,其余等 `fInvisibleTime`。
|
||||
- 窗内每个采样的刀尖 `last_pos → pos` 按攻击者朝向转到世界 cm,作为半径 20 的动态 Z 圆柱,
|
||||
对受击方每个防御球做 `HitCollision.detect_z_cylinder()`(逐行移植
|
||||
`DetectCollisionDynamicZCylinderVSDynamicZCylinder` + `IntersectLineSegments`)。
|
||||
- 碰上后 `_register_hit()` → `_process_attack_success()`。
|
||||
- `_register_hit` 修正为 `map::insert` 语义:已有目标不覆盖冷却,插入后超上限才返回 FALSE。
|
||||
- 防御球取 `.msm` 的 `CollisionType 3`(`HitCollision.parse_msm_defending`):PC 读
|
||||
`root/msm/<class>_<m|w>.msm`,怪物读模型目录 `.msm`。上一帧球心缓存在 `_def_last`。
|
||||
- 判定时间改为 `_attacking_elapsed() = (_local_time - _motion_start_t) * _atk_speed_factor`
|
||||
(`GetAttackingElapsedTime`);`_local_time` 由 `_advance_local_time()` 先扣 `m_fDelay` 再推进。`combat_fx_test.gd` 加 `FakeNetWorld` +
|
||||
命中窗几何判定 / `CG_ATTACK` 延后 / `CG_SYNC_POSITION` / COMBO 单次 / 正面弧 / 越窗收起 6 段断言。
|
||||
5. `OnAttack` 事件(动作真正起手时)发 `CG_CHARACTER_MOVE{FUNC_COMBO, wMotionIndex}`,
|
||||
与 `CG_ATTACK` 是**两个不同时刻**的两个包,不能合并。**[已在增量 78 `_do_attack_swing` 落地]**
|
||||
@@ -2395,8 +2411,10 @@ OnHit(uSkill, victimActor, isSendPacket):
|
||||
修改 6 完成(增量 84:`motion_mode_for` + `_refresh_motion_mode` 按装备武器 subtype / 骑乘 / 变身
|
||||
切 `combo_motion_mode`,对齐 `RefreshState`);修改 7 完成(增量 85:`_clickable_distance()` 弓箭
|
||||
`__GetBowRange()` 分支 + `_bow_range_cm()` + `POINT_BOW_DISTANCE` 加成)。C++ 21/21 + 7 GDScript
|
||||
全绿。仍待:几何判定升级为真实动态圆柱扫掠(需骨骼矩阵 + defending sphere,随 §3.1 actor 碰撞)、
|
||||
服务端合法性校准。§3.6 箭矢 `MOTION_EVENT_TYPE_FLY` 事件已在增量 86 落地(见 §3.6)。
|
||||
全绿。~~仍待:几何判定升级为真实动态圆柱扫掠(需骨骼矩阵 + defending sphere,随 §3.1 actor 碰撞)、
|
||||
服务端合法性校准。~~ —— 增量 130 已落地:Z 圆柱 vs `.msm` 防御球,以及 `__ProcessDataAttackSuccess`
|
||||
的完整效果(见修改 4 / §3.7 / C.5 增量 130)。仍待:服务端合法性校准;防御球当前是模型本地近似,
|
||||
未跟骨骼矩阵走。§3.6 箭矢 `MOTION_EVENT_TYPE_FLY` 事件已在增量 86 落地(见 §3.6)。
|
||||
|
||||
---
|
||||
|
||||
@@ -2512,6 +2530,35 @@ handler 由 `__OnPressActor` 通过
|
||||
`netplay_test` / `netbridge_test` / `gamescene_test` / `player_motion_test`
|
||||
全绿。
|
||||
|
||||
**增量 130(受击方反应 1:1,2026-09-12)**
|
||||
|
||||
- 新增 `project/ui/hit_reaction.gd`,由 PlayerView / MobView 共用。`good()` / `greate()` / `stone()`
|
||||
逐分支对齐 `__HitGood` / `__HitGreate` / `__HitStone`:
|
||||
- 击倒中不再换受击动作,Greate 在起身中也不换。
|
||||
- 晕眩时:Good → `Die`(死亡动作由服务端驱动,seam);Greate 只播 `DAMAGE_FLYING(_BACK)`,
|
||||
并置 `m_isRealDead`,不起身。
|
||||
- `isLock`(普攻 / 连击 / 技能 / 表情 / 钓鱼等)只抖动不换动作;Greate 在 `IsUsingSkill` 时也只抖。
|
||||
- `scalar = dot(攻击方朝向, 受击方朝向)`:小于 0 走正面 `DAMAGE` / `DAMAGE_FLYING → STAND_UP`;
|
||||
否则先试 `_BACK` 版,缺资源退回正面版。
|
||||
- 动作链 = `InterceptOnceMotion` + `PushOnceMotion` + 链尾 `PushLoopMotion`,回到受击前的
|
||||
wait / walk / run。
|
||||
- `__Shake(100)`:100 ms 内模型位置每帧偏移 ±`rand()%10` cm,结束复位。
|
||||
- `InsertDelay(fStiffenTime)`:`time_scale = 0` 冻结,到期恢复该动作的 `fSpeedRatio`;冻结期间新绑定的
|
||||
动作同样保持冻结。
|
||||
- PC 受击文件按 `playersettingmodule` 注册表取,同名多份等权随机:
|
||||
- `damage` / `damage_1`;BACK 版 `damage_2` / `damage_3`
|
||||
- `damage_flying` / `back_damage_flying`
|
||||
- `falling_stand` / `back_falling_stand`
|
||||
- 怪物在 `STATE_MOTIONS` 新增 `BACK_DAMAGE` / `FRONT_KNOCKDOWN` / `BACK_KNOCKDOWN` / `FRONT_STANDUP` /
|
||||
`BACK_STANDUP`。
|
||||
- `is_in_hit_reaction()` 改为返回 `_hit.active`:整条链播完才清 `net_play._knock_down`。原来的
|
||||
`_state == "damage"` 分支并入链尾处理。
|
||||
- 击退:`net_world.push_victim()` 用 `physics_push.gd`(逐行移植 `CPhysicsObject` +
|
||||
`CEaseOutInterpolation`),每帧叠加位移。
|
||||
- `blending_position()` = `GetBlendingPosition`,供 `OnHit` 的 `CG_SYNC_POSITION` 使用。
|
||||
- 推动期间服务端位置变化:以新位置为基准继续叠加。
|
||||
- 推动结束后服务端位置一变,即丢弃偏移。
|
||||
|
||||
---
|
||||
|
||||
### 3.8 技能缺少统一合法性检查
|
||||
@@ -7880,7 +7927,7 @@ dx² + dy² > s_fLimitDistance → 不播
|
||||
project/inventory_ui_test.gd —— FakeProto.item() 补 sub_type=0 / anti_flags=0 /
|
||||
wear_flags=1<<4 / limits=[] 让 iv.use(5)(vnum 19 武器)仍过新前置门
|
||||
|
||||
测试:M2_ASSETS=/Users/shenlei/Work/mt/mtgodot-poc/assets ctest --test-dir build → 21/21
|
||||
测试:M2_ASSETS=/Users/shenlei/Work/mt/metin2-client/assets ctest --test-dir build → 21/21
|
||||
(无 C++ 改动);56 个 GDScript 回归全 PASS(新增 equip_rules_test;inventory_test /
|
||||
inventory_ui_test 保持绿)。
|
||||
|
||||
@@ -7961,7 +8008,7 @@ dx² + dy² > s_fLimitDistance → 不播
|
||||
weapon_gr2 / shield_gr2 / hair_gr2、脱时装武器重解析、婚纱(桩返回 201)遮武器保头发、
|
||||
婚纱换捧花(vnum 50201)_last_weapon_vnum 恢复为 50201
|
||||
|
||||
测试:M2_ASSETS=/Users/shenlei/Work/mt/mtgodot-poc/assets ctest --test-dir build → 21/21
|
||||
测试:M2_ASSETS=/Users/shenlei/Work/mt/metin2-client/assets ctest --test-dir build → 21/21
|
||||
(无 C++ 改动);57 个 GDScript 回归全 PASS(新增 part_hiding_test;equip_model_test /
|
||||
equip_rules_test / inventory_ui_test 保持绿)。
|
||||
|
||||
@@ -9083,7 +9130,7 @@ dx² + dy² > s_fLimitDistance → 不播
|
||||
🔎 真机目视:鼠标悬停某 NPC / 怪 → 其头顶名字立即强显(即便未开「总显名字」且超 3500 距)、
|
||||
移开 → 复原;鼠标压 UI / 悬停地面物品时不强显任何角色名。
|
||||
参考(sole baseline = /Users/shenlei/Work/mt/40250/ClientVS22/source/;运行时 python =
|
||||
mtgodot-poc/assets/root/game.py):
|
||||
metin2-client/assets/root/game.py):
|
||||
root/game.py:1519 GameWindow.OnRender()
|
||||
root/game.py:1531 textTail.UpdateAllTextTail()
|
||||
root/game.py:1533 if True == wndMgr.IsPickedWindow(self.hWnd) —— 鼠标在 3D game 窗口上才拾取
|
||||
@@ -10145,3 +10192,99 @@ dx² + dy² > s_fLimitDistance → 不播
|
||||
seam ⑨ 名字与气泡合并为单尾标链表;完整 Quest / fishing / dungeon / mount / observer 玩法与
|
||||
复杂 UI 分支;真实服务器、真机、纯 pack、双架构和正式签名验收。
|
||||
```
|
||||
|
||||
### C.5 批次记录 · 增量 130(W2 / §3.5 · §3.7 战斗与 40250 对齐:命中判定、命中效果、受击反应、攻速、连击类型、技能包)
|
||||
|
||||
```text
|
||||
批次:增量 130 —— 逐项修正与 40250 参考端战斗实现的差异(2026-09-12)。每项先写失败断言再改实现。
|
||||
|
||||
1. 连击类型:
|
||||
net_play.gd 监听 client.combo_changed,由 _on_combo_changed() 处理,对齐
|
||||
CPythonPlayer::SetComboSkillFlag:
|
||||
· 查连击技能 122 的等级;缺槽或等级 <= 0 直接返回,不改 combo type。
|
||||
· 否则 _combo_type = enabled ? MIN(level, 2) : 0。
|
||||
· 不在本地伪造技能等级。
|
||||
|
||||
2. 命中判定:
|
||||
删除 _hit_deduped / _hit_geometry(reach + 正面 ±60° 近似)。
|
||||
· 新增 project/hit_collision.gd,逐行移植:
|
||||
IntersectLineSegments / FindNearestPointOnLineSegment / FindNearestPointOfParallelLineSegments /
|
||||
AdjustNearestPoints / DetectCollisionDynamicZCylinderVSDynamicZCylinder;
|
||||
另含 .msm CollisionType 3 防御球解析与 IS_HUGE_RACE。
|
||||
· net_play._normal_attack_process() 流程:
|
||||
(dX, dZ, dZ) 距离门 300 / 500 cm → 命中窗相交 → m_HitDataMap →
|
||||
刀尖采样半径 20 的 Z 圆柱 vs 受击方防御球(上一帧球心存 _def_last)。
|
||||
· PlayerView 取 root/msm/<class>_<m|w>.msm,MobView 取模型目录 .msm,都经 get_defending_spheres() 提供。
|
||||
· 没有 .msm 的占位节点退化为单球 HIT_FALLBACK_SPHERE(seam)。
|
||||
|
||||
3. _register_hit:
|
||||
改为 map::insert 语义:已登记的目标不刷新冷却,插入后超上限才返回 FALSE。
|
||||
|
||||
4. 攻速:
|
||||
_atk_speed_factor(bAttackSpeed/100,>1100 → 0)同时用于两处:
|
||||
· _attacking_elapsed() = (_local_time - _motion_start_t) * fSpeedRatio
|
||||
· PlayerView.play_attack_motion(mode, index, speed_ratio) 的 time_scale
|
||||
play_attack_motion 按 MOTION_MODE 目录绑定 attack(_1) / combo_0N,模式目录缺该段时回退 general。
|
||||
|
||||
5. 连击清零:
|
||||
删除本地连击超时清空。PlayerView / MobView 新增 motion_bound(state) 信号(__SetMotion 尾),
|
||||
由 net_play._on_motion_bound() 处理:
|
||||
· 新动作带 AttackingData → 清 m_HitDataMap。
|
||||
· 连击中换到无 ComboInputData 的动作 → _combo_index = 0。
|
||||
|
||||
6. __ProcessDataAttackSuccess(_process_attack_success):
|
||||
· InsertDelay(fStiffenTime):攻击者写 _delay(_advance_local_time 先扣),视图 time_scale 冻结;
|
||||
受击方同样 insert_delay,并记 m_fInvisibleTime(_victim_invisible_until,期间 AttackingProcess 跳过它)。
|
||||
· 击退门 __CanPushDestActor(_can_push):
|
||||
建筑 / 门 / 石头 / NPC / 巨型不推;晕眩必推;否则要求 owner 为主角且 owner 时间 <= 3 s。
|
||||
通过后 net_world.push_victim() 推动(physics_push.gd 逐行移植 CPhysicsObject + CEaseOutInterpolation)。
|
||||
OnHit 时目标仍在推动中 → GetBlendingPosition 进 CG_SYNC_POSITION 列表。
|
||||
· 命中特效 blow_1_low.mse:
|
||||
建筑 / 门放在攻击者身前 30 cm、不转向;巨型放在命中点;其余放在受击方。
|
||||
game_scene 装配 net_play.fx = fx、fx_parent = Entities。
|
||||
· 受击反应:石头 / 门 → hit_stone;HIT_TYPE_GOOD → hit_good;HIT_TYPE_GREAT → hit_greate;
|
||||
scalar = cos(攻击方 yaw − 受击方 yaw)。状态机 ui/hit_reaction.gd 见 §3.7 增量 130。
|
||||
· 最后 _on_hit:SetTarget + CG_ATTACK。
|
||||
|
||||
7. 技能包:
|
||||
ClassicSession::send_use_skill 删除夹带的 CG_FLY_TARGETING,只发 TPacketCGUseSkill(对齐
|
||||
SendUseSkillPacket)。飞行目标仍由施法方的 CG_ADD_FLY_TARGETING / OnSetFlyTarget 发送。
|
||||
net_classic_session_test 新增 "use skill wire: no extra fly targeting" 断言。
|
||||
|
||||
8. 既有测试随行为调整:
|
||||
受击 damage 按 playersettingmodule 注册为 damage / damage_1 等权随机。
|
||||
· gpu_pose_bounds_test:CPU / GPU 两个视图绑定前用同一 seed,保证选中同一段。
|
||||
· race_motion_assembly_test:damage 允许任一注册变体。
|
||||
这两个测试在本批次改动后曾失败(CPU / GPU 选到不同段、断言写死 damage.msa),调整后通过。
|
||||
|
||||
回归(godot --headless --path project --script <test>.gd):
|
||||
新增 PASS:hit_collision_test、physics_push_test、hit_view_test、net_world_push_test。
|
||||
PASS:combat_fx_test、gamescene_test、netplay_test、skill_test、player_move_test、input_key_test、
|
||||
weapon_attach_test、mob_view_test、player_motion_test、fly_test、remote_player_test、
|
||||
net_world_vis_test、forest_mob_render_test、p2b_test、playable_harness_test、playable_combat_test、
|
||||
gpu_pose_bounds_test、race_motion_assembly_test。
|
||||
已知失败(与本批次无关):equip_model_test "armor 11000 -> values[3]=0 回退 =vnum"(夹具期望过期)。
|
||||
forest_map_render_test 是 extends Node 的 MT_TEST_MODE 入口,不能用 --script 直跑,本批次未运行。
|
||||
ctest --test-dir build:22/23 PASS。net.classic_session 仍是既有 25 条失败
|
||||
(quest / duel / messenger / exchange / guild / safebox / mall / sync / ground item / despawn),
|
||||
都与 use_skill 无关;新增的 use skill 断言通过。
|
||||
|
||||
验收边界:
|
||||
已验证(离线):Z 圆柱几何、.msm 防御球解析、命中表 / 上限、攻速缩放、连击类型与清零、
|
||||
硬直 / 击退 / 同步位置 / 特效挂载、受击动作链 / 抖动 / 冻结、技能包线格式。
|
||||
未验证:真实服务器下的命中与击退手感、真机视觉。
|
||||
seam:
|
||||
· 击退没有地形 / 角色碰撞,也无 AdjustCollisionWithOtherObjects、溅射、IsResistFallen。
|
||||
· owner 时间取客户端观测时刻。
|
||||
· 防御球按模型本地坐标、不跟骨骼矩阵;占位节点退化为单球。
|
||||
· 远端角色不跑 AttackProcess;晕眩 → Die 不在本地播。
|
||||
· 攻击 / 受击动作变体等权随机;受击动作固定取 general 目录。
|
||||
· 抖动用模型位置偏移,InsertDelay 用 time_scale 冻结。
|
||||
· 受击链期间的循环动作请求延后到链尾。
|
||||
· 推动结束后服务端位置一变即丢弃偏移。
|
||||
· 本地 GC 伤害包仍直接走 set_anim_state("damage")。
|
||||
改动 C++ 后已重建 mtgodot 扩展;脚本改动需要重新导出 PCK 才进包。
|
||||
|
||||
下一批次:
|
||||
击退世界碰撞与 IsResistFallen;防御球跟骨骼矩阵;远端 AttackProcess;真实服务器战斗验收。
|
||||
```
|
||||
|
||||
+72
-54
File diff suppressed because one or more lines are too long
@@ -0,0 +1,89 @@
|
||||
# 40250 客户端对拍缺陷发现与修复工程指南 (CLIENT-PARITY-AUDIT-AND-FIX-GUIDE)
|
||||
|
||||
> **目标**:建立系统化的排查、探针探测与修复流程,解决 Godot 客户端从 40250 移植过程中由于“管线实现不完整”、“只实现裸数据漏掉复合层”导致的隐性细节缺陷(典型案例如:桥梁可行走但高度采样遗漏 `.mdatr` 导致掉落穿透)。
|
||||
|
||||
---
|
||||
|
||||
## 一、问题本质剖析:为何会出现此类缺陷?
|
||||
|
||||
在 Metin2 40250 原版中,几乎所有物理、移动、战斗与视觉表现都是**“多层复合叠加”**的。在向 Godot 移植的早期原型阶段,很多系统往往只实现了最底层的第 1 层,缺少了第 2 层和第 3 层的复合逻辑:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph 原版 40250 表现
|
||||
L1["基础数据层 (如: PRTerrain height.raw 裸高度图)"]
|
||||
L2["静态实体层 (如: CGraphicThingInstance / .mdatr 高度多边形)"]
|
||||
L3["动态状态层 (如: 骨骼动作高度、骑乘偏置、水体/浮力判定)"]
|
||||
L1 --> L2 --> L3 --> R1["最终真实表现 (如: 角色稳稳立于桥面)"]
|
||||
end
|
||||
|
||||
subgraph 缺陷实现模式
|
||||
D1["基础数据层 (fmt::terrain_height_at)"]
|
||||
D1 --> R2["直接作为最终结果 (角色跌落到桥底裸地形)"]
|
||||
end
|
||||
```
|
||||
|
||||
如果仅依赖玩家跑图偶然触发,发现成本极高且容易重复反弹。必须转为**白盒走查、探针自动化与协议日志审计**。
|
||||
|
||||
---
|
||||
|
||||
## 二、高危缺陷领域清单与 40250 真实机制对照
|
||||
|
||||
| 领域 | 常见隐性缺陷表现 | 40250 原版真实机制(唯一真相源) | 关键代码位置 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **1. 空间与地貌 (Terrain & Objects)** | • 桥梁、楼梯、门廊台阶悬空或掉落穿透<br>• 水陆交界处判断错误(干地游泳或踩深水不减速/不可钓鱼)<br>• 地下城/山洞进出门廊穿透掉落 | • `CMapOutdoor::GetHeight` 查询 `.mdatr` 中的 `heights` 网格多边形,与裸高度图取 `fMAX`<br>• `attr.atr` 中的 `ATTRIBUTE_WATER` 与水面多边形绝对高度差<br>• `CDungeonBlock::GetObjectHeight` | `GameLib/MapOutdoor.cpp`<br>`EterLib/AttributeInstance.cpp`<br>`GameLib/AreaTerrain.cpp` |
|
||||
| **2. 物理与移动 (Physics & Locomotion)** | • 遇到陡坡时直接穿山或以相同速度瞬移上山<br>• 被怪物或技能击退(Knockback)时穿透墙体飞出地图<br>• 自动寻路走直线卡死在栅栏/岩石缝隙 | • `ActorInstanceSync.cpp` 坡度倾角与重力滑落向量计算<br>• `ActorInstanceCollisionDetection.cpp` 击退时沿障碍物法线切向滑动<br>• `CMapOutdoor::is_blocked` 的栅格阻挡光栅化与逐轴滑动降级 | `GameLib/ActorInstanceSync.cpp`<br>`GameLib/ActorInstanceCollisionDetection.cpp` |
|
||||
| **3. 动作与挂点 (Animation & Attachment)** | • 武器/盾牌朝向偏转或拿反、脱离手部<br>• 骑马时双手未握缰绳或双腿穿透马鞍<br>• 更换头盔/发型时头部穿模或发色异常 | • `CItemData::GetItemScale` 与骨骼挂点矩阵 `m_matBone` 变换<br>• `InstanceBaseMotion.cpp` 的骑乘姿势 override (`COMBO_TYPE_HORSE`)<br>• `CRaceData::SetHair` 的 MSM 顶点隐藏机制与 Alpha Blending 管道 | `GameLib/ActorInstanceAttach.cpp`<br>`UserInterface/InstanceBaseMotion.cpp`<br>`GameLib/RaceData.cpp` |
|
||||
| **4. 战斗与射弹 (Combat & Flying)** | • 弓箭/法球穿透墙体或直接打入地底<br>• 普攻挥刀动作还没劈下来伤害数值已提前跳出<br>• 怪物死后不立刻倒地或死亡姿势嵌入地面 | • `CFlyingInstance::Update` 中的 `BackgroundHit` 碰撞(与地面/建筑交点)<br>• `.msa` 中的 `MotionEvent`(命中帧 `HIT` 严格帧事件驱动)<br>• `ActorInstanceBlend.cpp` 死亡动作的地面投影与骨骼终帧冻结 | `GameLib/FlyingInstance.cpp`<br>`GameLib/ActorInstanceBattle.cpp`<br>`GameLib/ActorInstanceBlend.cpp` |
|
||||
| **5. 交互与拾取 (Picking & Decal)** | • 鼠标点击桥面、屋顶或 NPC 拾取点偏移到桥下<br>• 地面范围技能光圈(Decal)漏到桥底或悬空 | • `CMapOutdoor::GetPickingPointWithRay` 射线先求交物体再求交地形<br>• `CTerrainDecal` 的多边形表面投影裁切 | `GameLib/MapOutdoor.cpp`<br>`GameLib/TerrainDecal.cpp` |
|
||||
|
||||
---
|
||||
|
||||
## 三、四大系统化排查手段(主动发现)
|
||||
|
||||
### 手段 1:全图几何与阻挡坐标探针(Headless Coordinate Probes)
|
||||
由于地图静态放置数据由 `areadata.txt` 和 `attr.atr` 完全定义,无需人工跑图,可由自动化脚本扫盘检测:
|
||||
1. **特殊静态模型巡检**:
|
||||
- 提取地图上全部引用了含有 `.mdatr`(带有 `heights` 网格)的物体(桥梁、台阶、悬崖栈道等)。
|
||||
- 在该物体几何中心上方向下打垂直射线或采样 `sample_height(x, z)`。
|
||||
- **断言**:采样高度必须落在物体表面标高区间内,与裸地形高度应有明显的阶跃高差;若 `sample_height == terrain_height`,直接报警并定位对应模型路径与坐标。
|
||||
2. **地貌高度一阶导数(梯度突变)扫描**:
|
||||
- 按 0.5m 步长对可通行路网进行网格采样。
|
||||
- 检测相邻点高度差:若连续两点高度差 $> 2.5\text{m}$ 且未标记 `ATTRIBUTE_BLOCK`,判定为地貌穿透或缝隙掉落点。
|
||||
|
||||
### 手段 2:40250 核心链路白盒对拍(Static Call-Graph Audit)
|
||||
以 40250 源码的关键函数为索引,走查当前客户端对应的实现:
|
||||
- **查阅 40250 源码**:`40250/Server Client TMP4/ClientVS22/source/`
|
||||
- **重点审查模式**:
|
||||
- 40250 中包含 `fMAX(...)`、`min(...)`、多层分支判断的函数,当前客户端是否只实现了其中单条简单路径?
|
||||
- 40250 中从资源读取参数的逻辑(如 `.msa` 帧事件、`.msm` 挂点、`.mdatr` 碰撞体),当前客户端是否被硬编码(hardcoded)常量替代?
|
||||
|
||||
### 手段 3:未决协议与资源缺失日志审计(Log & Protocol Auditing)
|
||||
在测试与实机运行期间,关注并收集控制台日志中的警告:
|
||||
- 过滤关键 Warning:`unhandled packet`、`missing model`、`cannot find mdatr`、`mesh has no height`。
|
||||
- 检查服务端发送的所有 GC 封包(如 `GC_MOVE`, `GC_SYNC_POSITION`, `GC_ATTACK`, `GC_FLY_TARGETING`),确认不仅有网络层的解析反序列化,更在表现层驱动了正确的物理与动画状态。
|
||||
|
||||
### 手段 4:自动化无头路网巡逻测试(Headless Patrol Suite)
|
||||
利用 Godot `--headless` 启动无头客户端:
|
||||
- 让角色沿关键地貌路网(广场 $\to$ 跨河桥梁 $\to$ 仓库台阶 $\to$ 城门通道 $\to$ 郊外野怪区)自动移动。
|
||||
- 监控指标:
|
||||
- 角色 Y 坐标是否发生非预期的下坠(掉水、穿地);
|
||||
- `_blocked` 是否产生虚假阻挡导致角色提前停步卡死;
|
||||
- 状态机是否在 `walk` / `run` / `wait` 之间异常震荡。
|
||||
|
||||
---
|
||||
|
||||
## 四、工程化修改与防回退治理原则
|
||||
|
||||
每次发现并修复类似细节问题时,必须严格执行以下三条工程原则:
|
||||
|
||||
### 原则 1:拒绝打补丁式硬编码(Treat the Root, Never Patch Coordinates)
|
||||
- **禁止**针对个别坐标做特殊条件判断(如 `if x > 750 and x < 770: pos.y = 206.0`)。
|
||||
- **必须**在底层管线实现通用机制(如在 C++ 引擎层解析 `.mdatr` 的 `heights` 并求交平面),确保全服数十张地图、数百座桥梁与所有建筑台阶自动获得修复。
|
||||
|
||||
### 原则 2:路线 A(严格 40250 原版实现)
|
||||
- 不凭空发明“更好”的经验算法,遇到模糊细节直接翻阅 40250 对应 C++ 模块源码,将原版的数学公式、坐标系旋转(Z-up 转 Y-up、米制转换)与浮点容差 1:1 映射。
|
||||
|
||||
### 原则 3:修一处问题,必补一个 Parity 自动化回归测试
|
||||
- 修复缺陷后,必须在 `project/` 下建立对应的 `test_*_parity.gd` 单元/集成测试(例如针对桥梁建立的 `test_bridge_height_parity.gd`)。
|
||||
- 将测试纳入持续集成验证集(134+ Parity 测试套件),任何一次打包或构建前运行,确保旧缺陷绝不回退。
|
||||
@@ -1,243 +0,0 @@
|
||||
# 客户端移植 —— 从素材渲染器到能联机的客户端
|
||||
|
||||
> 起点:`mtgodot-poc` 的渲染纵切「差不多能用」(见 [`PARITY-GAP.md`](./PARITY-GAP.md))。
|
||||
> 本文是把它变成**能连服务端玩**的客户端的分层计划。分水岭是**网络层**——别的玩家、
|
||||
> 物品、战斗、聊天都以封包为数据源。
|
||||
>
|
||||
> 玩法 / 客户端系统层面「原始客户端有、这里还没有」的完整差距清单见
|
||||
> [`CLIENT-GAP.md`](./CLIENT-GAP.md)。
|
||||
|
||||
## 0. 目标服务端
|
||||
|
||||
- LAN:`192.168.21.203`(这台 Mac 能路由到,但握手端口目前 **connection refused** ——
|
||||
服务端进程没起 / 端口和默认不同,需确认)。
|
||||
- 默认端口(`assets/root/serverinfo.py`):auth `11000`,频道/游戏 `11011/11021/11031/11041`。
|
||||
- 测试账号:`admin` / `123456789`。
|
||||
|
||||
## 1. 协议(本 m2dev fork,**不是老 Metin2**)
|
||||
|
||||
### 1.1 帧格式
|
||||
|
||||
每个包 = `[uint16 header LE][uint16 length LE][payload...]`,`length` = 含 4 字节前缀的整包长度
|
||||
(≥ 4)。包间的 0 字节是加密对齐 padding,跳过。header 是 `CG::`/`GC::` 命名空间常量
|
||||
(`UserInterface/Packet.h` + `EterLib/ControlPackets.h`)。
|
||||
|
||||
### 1.2 加密握手(libsodium)
|
||||
|
||||
`EterBase/SecureCipher` —— X25519 密钥交换(`crypto_kx`)+ 每方向固定 nonce 的 XChaCha20
|
||||
流加密(带运行字节计数,**顺序敏感**)+ `crypto_auth` HMAC 挑战应答 + XChaCha20-Poly1305
|
||||
AEAD 加密 session token。
|
||||
|
||||
握手序列(连上 auth server 后):
|
||||
|
||||
| 方向 | 包 | header | 动作 |
|
||||
|---|---|---|---|
|
||||
| S→C | `GCPhase{phase=PHASE_HANDSHAKE}` | `GC_PHASE` 0x0008 | 进握手态 |
|
||||
| S→C | `GCKeyChallenge{server_pk[32], challenge[32], server_time}` | `GC_KEY_CHALLENGE` 0x000B | `Initialize()`;`compute_client_keys(server_pk)` |
|
||||
| C→S | `CGKeyResponse{client_pk[32], challenge_response[32]}` | `CG_KEY_RESPONSE` 0x000A | `get_public_key()`;`compute_challenge_response(challenge)` |
|
||||
| S→C | `GCKeyComplete{encrypted_token[48], nonce[24]}` | `GC_KEY_COMPLETE` 0x000C | `decrypt_token()`→token;`set_activated(true)`;解密已缓冲的剩余字节 |
|
||||
| S→C | `GCPhase{phase=PHASE_AUTH}` | `GC_PHASE` | 进认证态 |
|
||||
| C→S | `CGLogin3{name[31], pwd[17]}` | `CG_LOGIN3` 0x0102 | 明文账号密码(**此时流已加密**)|
|
||||
| S→C | `GCAuthSuccess{login_key, result}` | `GC_AUTH_SUCCESS` 0x0108 / 失败 `GC_LOGIN_FAILURE` 0x0106 | `result` 真→拿 `login_key` |
|
||||
|
||||
拿到 `login_key` 后断开 auth,连**游戏服**(`:11011`),同样握手,然后 `CGLogin2{name, login_key}`
|
||||
(`CG_LOGIN2` 0x0101)→ 选人 → 加载 → 进游戏相位。
|
||||
|
||||
> S→C 流 nonce `[0]=0x01`,C→S `[0]=0x02`。`ping`(`GC_PING` 0x0007)→`pong`(`CG_PONG` 0x0006) 全程要回。
|
||||
|
||||
## 2. 进度
|
||||
|
||||
| 模块 | 状态 | 位置 |
|
||||
|---|---|---|
|
||||
| `SecureCipher`(KX + 流 + AEAD token) | ✅ 端口完成,`net.cipher_roundtrip` CTest(KX / 挑战应答 + 篡改拒绝 / token 往返 / 跨块流加解密 / 字节计数) | `extension/src/net/secure_cipher.{h,cpp}` |
|
||||
| `wire.h`(帧 + 控制 + KX + auth 结构体) | ✅ `#pragma pack(1)` 尺寸断言 | `extension/src/net/wire.h` |
|
||||
| `ByteBuffer`(收发暂存) | ✅ header-only | `extension/src/net/byte_buffer.h` |
|
||||
| libsodium 接入 | ✅ 从源码 vendor(`extension/third_party/libsodium-cmake` + zstd + miniLZO),macOS/iOS 都编过,不再依赖 Homebrew。详见 `docs/THIRD-PARTY.md` | `extension/third_party/CMakeLists.txt` |
|
||||
| `NetStream`(非阻塞 POSIX socket + `process()` 循环 + 帧解析 + 内建控制面 handler:`GC_PHASE` / `GC_PING`→`CG_PONG` / KX 握手 / 激活后解密已缓冲字节) | ✅ 编译通过 | `extension/src/net/net_stream.{h,cpp}` |
|
||||
| `AuthClient`(auth 相位:`PHASE_AUTH`→发 `CGLogin3`→收 `GCAuthSuccess`/`GCLoginFailure`) | ✅ 编译通过,等可达服务端端到端测 | `extension/src/net/auth_client.h` |
|
||||
| `GameClient`(游戏服:`PHASE_LOGIN`→发 `CGLogin2{name, login_key}`→收 `GCLoginSuccess3` 角色列表→`select_character()`→`PHASE_SELECT/LOADING/GAME`) | ✅ 编译通过。角色列表按 `TSimplePlayerInformation`(pack(1),103B)解析成 `{index,id,name,job,level,x,y}`;游戏相位包暂只回 PING、记未知 header | `extension/src/net/game_client.h` |
|
||||
| `net_probe` 诊断工具 | ✅ `net_probe <host> <port> <id> <pw>`,打印 state / 握手 / 登录结果 / login_key | `extension/tools/net_probe.cpp` |
|
||||
| **`M2Client` GDExtension 节点** | ✅ 已注册可用。`connect_to_server(...)` 编排 Auth→(`auth_ok`)→GameConnect→游戏相位;`select_character(index)`。信号 `stage_changed` / `phase_changed` / `auth_ok` / `login_failed` / `char_list` / `entered_game` / `disconnected`。生命周期(F5):`suspend()`/`resume()`/`reconnect()` + `suspended`/`resumed` 信号,`NOTIFICATION_APPLICATION_PAUSED/RESUMED` 自动触发。烟测:正确报 `auth_connect → login_failed(Connection refused) → failed` | `extension/src/net/m2_client.{h,cpp}` |
|
||||
| **`M2Client` 游戏内 API** | ✅ 出站 `move(func,arg,rot_deg,x,y)`(rot `/5`、`time` 取帧钟)/ `attack(motion,vid)` / `set_target(vid)` / `say(type,text)`,未入局返回 false。快照 `get_entities()` / `get_entity(vid)`(含 hp/sp/level/dead/stunned,坐标转 Godot 米)/ `get_main_vid()` / `get_points()`(HP/SP/level/exp/gold)/ `get_target()`(vid + hp%)。`pump_game()` 每帧 `EntityStore::set_now/tick` 并抽信号:`entity_spawned/despawned/moved/main_set`、`chat`、`vitals_changed(vid)`、`entity_dead(vid)`(首次去重)、`damage(vid,amount,flag)`、`motion(vid,victim,motion)`、`points_changed(Dict)`、`target_info(vid,hp%)`。`net.loopback_flow` 扩展:出站 `CG_MOVE/ATTACK/TARGET/CHAT` 经加密信道字节级校验 | `extension/src/net/m2_client.{h,cpp}` |
|
||||
| **游戏相位封包结构** | ✅ `wire.h`:`GC_MAIN_CHARACTER` / `GC_CHARACTER_ADD[2]` / `GC_CHARACTER_DEL` / `GC_CHARACTER_UPDATE` / `GC_MOVE` / `GC_CHAT` + `CG_MOVE` / `CG_ATTACK` / `CG_TARGET` / `CG_CHAT`。**战斗/状态**(对 `Packet.h`):`GC_PLAYER_POINTS`(0x0214, int32[255] 1024B) / `GC_PLAYER_POINT_CHANGE`(0x0215) / `GC_STUN`(0x0216) / `GC_DEAD`(0x0217) / `GC_MOTION`(0x0307) / `GC_DAMAGE_INFO`(0x0410) / `GC_TARGET_INFO`(0x0A10) + `EPointTypes`(HP/MAX_HP/SP/MAX_SP/LEVEL/EXP/GOLD…)+ `EDamageFlag`(NORMAL/POISON/DODGE/BLOCK/PENETRATE/CRITICAL)。全部 `static_assert` 尺寸 | `extension/src/net/wire.h` |
|
||||
| **`EntityStore`(封包→世界模型)** | ✅ headless。`apply()` 消化上述包 → `vid → Entity{race,name,x,y,z,angle,parts,func,moving, hp,max_hp,sp,max_sp,level,dead,stunned}`;`GC_MOVE` FUNC_MOVE 插值/其他 func 吸附;`tick()` 按 `now_ms` 插值。战斗:`GC_PLAYER_POINTS`→`points()` 全量属性 + 镜像到主角实体;`GC_PLAYER_POINT_CHANGE`→按 `EPointTypes` 更新单实体 vital;`GC_DEAD`/`GC_STUN`→标记;拉取口 `drain_changes` / `drain_chat` / `drain_vitals` / `drain_damage` / `drain_motions` / `take_points_dirty` / `take_target_dirty`。单测 `net.entity_store` 扩展覆盖全部战斗包 | `extension/src/net/entity_store.{h,cpp}` |
|
||||
| **`CG_*` 出站构造** | ✅ `GameClient::send_move/send_attack/send_target/send_chat`(`CG_CHAT` 变长) | `extension/src/net/game_client.h` |
|
||||
| **环回集成测试** | ✅ `net.loopback_flow` CTest:进程内 `MockServer`(真协议:帧 + libsodium KX + 脚本相位包)驱动 `AuthClient`→`GameClient` 走完 auth→角色列表→选人→`PHASE_GAME`→游戏包进 `EntityStore`→NPC 移动插值。服务端一通改个 IP 即可 | `extension/tests/net_loopback_test.cpp` |
|
||||
| **`EntityStore` 单测** | ✅ `net.entity_store` CTest:合成包 → spawn / 移动插值(半程/到达/func 复位)/ 吸附 / 变长 chat / despawn | `extension/tests/net_entity_test.cpp` |
|
||||
| **`NetWorld` 场景桥(GDScript)** | ✅ `net_world.gd`:消费 M2Client 实体信号,每 vid 一个子节点,位置向 `get_entity(vid).pos` 平滑逼近(超 `snap_dist` 瞬移)、`rotation.y` 取自 `angle_deg`、动画状态取自 `func`。默认「占位胶囊 + 名字牌 + HP 条」;`set_model_factory(cb)` 换真模型。**战斗**:`vitals_changed`→缩放 HP 条 + emit `vitals(vid,hp,max_hp,dead)`;`damage`→实体头顶飘字(crit 放大变金 / dodge=MISS,tween 上浮淡出)+ emit `damage_number`;`entity_dead`→倒地 + `set_anim_state("dead")`。`main_entity_ready` / `chat_line` 信号。headless 自检 `netbridge_test.gd`(spawn/main/远移吸附/近移插值/chat/伤害数字/HP 条/暴击/死亡/despawn)通过 | `project/net_world.gd` |
|
||||
| **P0 闭环胶水 + 场景组装** | ✅ `net_play.gd`:`player_controller` ↔ `M2Client` —— 点地→`move(FUNC_MOVE)`(节流)/ 停→`move(FUNC_WAIT)` / 点实体→`set_target` + 进 2.5m 自动 `attack` / 收自己 `GC_MOVE`→位置校正 / `points_changed`+`vitals_changed`+`target_info`→HUD。`hud.gd` 加 `set_vitals/set_exp/set_level/set_target/clear_target`。`game_scene.gd`(简版)把 world+player+camera+pc+net_world+hud+net_play+audio+lifecycle 拼起来,`login.gd` 的 `entered_game`(`--auto` 自动选槽)实例化。`netplay_test.gd` + `gamescene_test.gd` headless 全过 | `project/{net_play,game_scene,hud,login}.gd` |
|
||||
| **P1 UI 工具层** | ✅ `ui/uiscript.gd` 解析原始 `uiscript/*.py`(子集解释器,**80/80 真实文件通过**)+ `ui/ui_build.gd` dict→Control 树(~20 种 type)+ `ui/ui_assets.gd`(`.sub`/tga/png)+ `ui/ui_manager.gd`(窗口栈 / ESC / modal / 拖动)+ `ui/dialogs.gd`。`ui_test.gd` 全过 | `project/ui/*.gd` |
|
||||
| **P2 物品数据层** | 🟡 `wire.h`:`GC_ITEM_SET/DEL/UPDATE/USE/GROUND_ADD/GROUND_DEL/GET` + `CG_ITEM_MOVE/USE/DROP/PICKUP`(全 static_assert)。`EntityStore`:`inventory[90]` / `equipment[11]` / ground map + `drain_inv/ground/item_events`。`M2Client`:`get_inventory/equipment/item/ground_items` + `move_item/use_item/drop_item/pickup_item` + 5 信号。新 `Metin2Proto` 节点(`item(vnum)`/`mob(vnum)`,载 5748 真实 item)。⬜ 背包 / 装备窗 UI + 穿脱换模型 | `extension/src/{net/wire.h,net/entity_store.*,net/game_client.h,net/m2_client.*,proto/proto_node.*}` |
|
||||
| **P3 聊天** | ✅ `wire.h` `GC_WHISPER`/`CG_WHISPER` + `EntityStore` whisper 解析(`ChatMsg` 加 from/sub)+ `M2Client.whisper()` + `whisper_received` 信号。`ui/chat_ui.gd`:4 标签 + 前缀 `/w`/`/g`/`/p`/`/s` + 系统/战斗日志。`net_world._bubble` 头顶气泡 + `main_bubble`。`chat_test.gd` 全过 | `extension/src/net/{wire.h,entity_store.*,game_client.h,m2_client.*}` · `project/ui/chat_ui.gd` |
|
||||
| **P4 战斗表现** | 🟡 `wire.h` `GC_AFFECT_ADD/REMOVE` + `EntityStore` affect map + `M2Client.get_affects()` / `affect_added`·`affect_removed`。受击硬直(`net_play` + `pc.frozen`)· combo(motion 递增)· 相机抖动(`game_camera.shake`)· 死亡窗(`ui/death_ui.gd` → `/restart_here`·`/restart_town`)· `hud.set_affects` 图标条。`combat_fx_test.gd` 全过。⬜ 武器拖尾 / 相机锁定 | `extension/src/net/*` · `project/{net_play,game_camera,hud}.gd` · `project/ui/death_ui.gd` |
|
||||
| **P5 特效引擎(首版)** | 🟡 `fx/mse.gd`(`.mse` 花括号树解析,真文件 6 particle)+ `fx/effect_player.gd`(Particle→`GPUParticles3D`:发射形状/速率/寿命/曲线/颜色渐变/billboard/加法混合)+ `fx/effect_registry.gd`(名字→`.mse` + 缓存 + spawn)。`wire.h` `GC_SPECIAL/SPECIFIC_EFFECT` + `M2Client.effect_cue` 信号 → `game_scene` 在实体上播。`fx_test.gd` 全过。⬜ `.dds` 纹理 / `.mde` mesh / 绑骨骼 | `extension/src/net/*` · `project/fx/*.gd` |
|
||||
| **P6 技能系统(首版)** | 🟢 `wire.h` `GC_SKILL_LEVEL/COOLTIME_END/CREATE_FLY` + `EntityStore` skill 表 + `M2Client.get_skills`/`cast_skill`/`skill_up` + `skills_changed`/`skill_cooldown_end`/`fly_cue` 信号。`ui/skill_table.gd`(skilldesc.txt)+ `ui/skill_ui.gd`(K 键,加点)+ `ui/quickbar.gd`(1-8 施放 + 冷却)+ `net_world` 飞行球。`skill_test.gd` 全过 | `extension/src/net/*` · `project/ui/{skill_table,skill_ui,quickbar}.gd` |
|
||||
| **P7 任务 / NPC(首版)** | 🟡 `wire.h` `GC_SCRIPT`/`GC_QUEST_CONFIRM`/`GC_QUEST_INFO` + `CG_ON_CLICK`/`SCRIPT_ANSWER`/`SCRIPT_BUTTON`/`QUEST_INPUT_STRING`/`QUEST_CONFIRM`(全 static_assert)。`EntityStore`:`ScriptCue`/`ConfirmCue`/`QuestInfo` map + `drain_scripts/confirms/quest_changes` + `quest(idx)`。`Entity` 加 `ch_type`。`M2Client`:`click_npc`/`script_answer`/`script_button`/`quest_input`/`quest_confirm`/`get_quests` + `script_dialog`/`quest_confirm_ask`/`quest_info` 信号。`ui/quest_dialog.gd`(EventManager 脚本渲染:`[ENTER]`/`[QUESTION]`/`[DONE]` → 文本+选项+确认)+ `ui/quest_log.gd`(J 键)。`net_play._on_pick` NPC 分流。quest 逻辑纯服务器驱动(选型②)。`quest_test.gd` 全过。⬜ `[INPUT]` 输入 UI / 选道具 / 立绘 / 世界任务箭头 | `extension/src/net/*` · `project/ui/{quest_dialog,quest_log}.gd` · `project/net_play.gd` |
|
||||
| charselect 串场 | ⬜ P10 | — |
|
||||
| **P8 社交 / 商店 / 仓库(首版)** | 🟡 `wire.h` `GC_PARTY_*`(0x0710-16) / `GC_MESSENGER`(0x0741) / `GC_SHOP`(0x0810) / `GC_EXCHANGE`(0x051C) / `GC_SAFEBOX_*`(0x0830-34) + 对应 CG(全 static_assert;`ShopItem`/`GCExchange`/`GCSubHeader` 等)。`EntityStore`:`PartyMember` map / `Friend` map / `ShopEntry` 表 / `ExchangeState` / `m_safebox[135]` + dirty 标志 + `drain_party_invites`/`drain_shop_errors`。`M2Client`:`party_invite/answer/leave` · `add/remove_friend` · `shop_buy/sell/close` · `exchange_start/add_item/add_gold/accept/cancel` · `safebox_checkin/checkout/move` + `get_party/friends/shop_items/exchange/safebox_items` + 8 信号。`ui/{party_ui,friend_ui,shop_ui,exchange_ui,safebox_ui}.gd`(队员面板常驻 / O 键好友 / 商店·交易·仓库随信号自动开)。`inventory_ui` 右键分流(商店卖 / 交易放 / 仓库存)。`p8_test.gd` + `net.entity_store` / `net.loopback_flow` 扩测全过。⬜ 公会(`PythonGuild` 40 法 + Mark 下载)、精炼 / 强化 / 龙魂、私人商店 / 拍卖、交易道具图标网格 | `extension/src/net/*` · `project/ui/{party,friend,shop,exchange,safebox}_ui.gd` · `project/game_scene.gd` |
|
||||
| 公会 / 精炼 / 龙魂精炼 / 会徽下载(首版) | 🟡 `GC_GUILD`(0x0730) INFO/LIST/GRADE + `ui/guild_ui.gd`(G,真机 `[GM-TEAM]`);`GC_REFINE_INFO`(0x051D) + `ui/refine_ui.gd`(`refine_ask` → `refine(pos,type)`);`GC_DRAGON_SOUL_REFINE`(0x051F)/`CG`(0x050D `grid[15]`) + `DS_SUB_*` + `EntityStore.DragonSoulCue`/`m_dragon_soul[180]` + `M2Client.ds_refine(mode,cells)`/`ds_window_open`/`ds_refine_result` + `ui/dragon_soul_ui.gd`(L / 自动弹,3 模式 + 15 格背包右键填)。**会徽**:`MARK_*` 独立连接(`GC_MARK_IDXLIST`/`BLOCK` 0x0C1x、`GC_MARK_UPDATE` 0x0B15、`CG_MARK_LOGIN/IDXLIST/CRCLIST` 0x0C0x);`NetStream` 加 `on_cipher_active()`/`on_raw()`(u32 `buf_size` 分帧);`mark_image.{h,cpp}` `MarkImageSet`(LZO1X → 512×512 → `mark_pixels`)+ `mark_client.h` `MarkClient`;`M2Client.download_guild_marks`/`get_guild_mark_image`→`Image`;`serverinfo` `mark_port` 第 8 列;`app_flow` 进游戏连 + `guild_mark_updated` 冷却重连。**公会战 + 技能页**:`GUILD_GC_SKILL_INFO`(12) `GuildSkillInfoBody`(17)、`WAR`(15) `GCGuildWar`(10)、`GUILD_WAR_LIST`/`END_LIST`(17/18) `GuildWarPair`(8)、`WAR_POINT`(19) `GuildWarPoint`(12)、`NAME`(16) `GuildNameEntry`(16);`EntityStore` `GuildSkillState`/`GuildWarStatus`/`m_guild_wars`/`m_guild_names` + `drain_guild_war_events`/`_scores`;`GameClient.send_guild_use_skill`;`M2Client` `get_guild_skill`/`get_guild_wars`/`get_guild_war`/`use_guild_skill`/`declare_guild_war`(`/war` 命令) + `guild_skill_changed`/`guild_war_changed`/`guild_war_event`/`guild_war_point` 信号;`ui/guild_ui.gd` 改三页 Tab(成员/技能/公会战)。**会徽上传**:`CG_MARK_UPLOAD`(0x0C03) `CGMarkUpload`(776)、`CG_GUILD_SYMBOL_UPLOAD`(0x0722) `CGSymbolUpload`(8)+文件字节;`NetStream.send_pending()`;`MarkClient` `Mode`(Download/UploadMark/UploadSymbol) + `upload_done()`;`M2Client.upload_guild_mark`(转 RGBA8/缩放)/`upload_guild_symbol`/`get_mark_server` + `guild_mark_uploaded`;`guild_ui` 成员页「上传会徽」按钮。`net.loopback_flow` 加 `MockServer::Mode::Mark` 真 socket 端到端校验(KX → CG_MARK_LOGIN → CG_MARK_UPLOAD 768 字节校验和 → upload_done)。`guild_refine_test.gd` + `dragon_soul_test.gd` + `guild_mark_test.gd` + `guild_war_skill_test.gd` + `net.entity_store` + `net.guild_mark` + `net.loopback_flow`(CTest) 全过。**真机验证(2026-08-31,`net_e2e`)**:`GUILD_GC_SKILL_INFO`(sub 12) 服务器进游戏自动推送,17B body 解析正确(skill_point=19 / guild_point 0/2300 / 12 等级);`GUILD_GC_WAR`(sub 15) 无战状态解析正确;trace 显示 `GC_GUILD`(0x0730) 多个子包 len=171/40/83/22/21/9/5 均无 unknown / parse 错误。会徽独立连接 + 会徽上传仍未接真机(联调服未开会徽服端口)。⬜ 龙魂属性表、公会等级/日志页 | `extension/src/net/*` · `project/ui/{guild,refine,dragon_soul}_ui.gd` · `project/{net/serverinfo,app_flow}.gd` |
|
||||
| **私人商店 / 道具商城 / Cube(首版)** | 🟡 `wire.h`:`CG_MYSHOP`(0x0802) `CGMyShopHead`(38,`char sign[33]+u8 count`) + `MyShopItem`(13,`u32 vnum,u8 count,ItemPos,u32 price,u8 display_pos`)×N;`CG_MALL_CHECKOUT`(0x0840) `CGMallCheckout`(8);`GC_MALL_OPEN`(0x0841) `GCMallOpen`(5)、`GC_MALL_SET`(0x0842)/`GC_MALL_DEL`(0x0843) 复用 `GCItemSet`/`GCItemDel`,全 static_assert。`GameClient`:`send_open_private_shop(sign, vector<MyShopItem>)`(打包头+N项)/ `send_close_private_shop`(=`SHOP_CG_END`) / `send_mall_checkout` / `send_cube_make|material_info|result_list`(`/cube make|mInfo|rList` 走 `send_chat(CHAT_TYPE_COMMAND,…)`)。`EntityStore`:`m_mall[135]` + `mall_open/size/slot/dirty`;`GC_CHAT` 若 `type==CHAT_TYPE_COMMAND` 不进聊天,转 `apply_server_command()` 解析 `cube open/close/info/success/fail/r_list/m_info`(`v,c` 结果列表 + `@`配方 `&`组 `|`槽 `/`金币)→ `CubeState{open,npc,recipes[],results[],need_*}` + `drain_cube_events()`。`M2Client`:`open/close_private_shop`、`get_mall_items`/`mall_checkout`、`get_cube`/`cube_make`/`cube_request_result_list`/`cube_request_materials` + `mall_opened`/`mall_changed`/`cube_opened`/`cube_closed`/`cube_changed`/`cube_result` 信号。`ui/private_shop_ui.gd`(招牌 + 背包挑件 + 定价 + 개설/철수)、`ui/mall_ui.gd`(列表 + 取出到背包空格)、`ui/cube_ui.gd`(配方列表 + 材料/金币 + 제작);`game_scene` 装配三窗。`net.entity_store`(合成 GC_MALL_* + `GC_CHAT/COMMAND` cube 全家桶)+ `net.loopback_flow`(真 socket:server 推 mall+cube,client 发 `CG_MYSHOP`/`CG_MALL_CHECKOUT`/`/cube` 字节校验)+ `shop_cube_mall_test.gd`(三窗 FakeClient)全过。**真机验证(2026-08-31,`net_e2e MT_E2E_MYSHOP=1`)**:`CG_MYSHOP`(0x0802) 38B head + 13B item×1(vnum 149,price 99999999)被真服接受,无断连 / 无 shop error;`SHOP_CG_END` 关店正常。Cube `/cube rList` 与 `CG_MALL_CHECKOUT` 送出路径不断连,但服务器只在玩家站到对应 NPC 前才回应,`mall_open=0` / `cube open=0` —— 需 NPC 交互才能完整验。⬜ `GC_SHOP_SIGN`/`SYMBOL_DATA` 头顶招牌、他人开店 `START_EX` 多标签、39 格拖放网格 / 鼠标物品态、能量条、NPC 前完整验 cube/mall。**拍卖行**:m2dev-client 无此系统(`0x08xx` 只有 shop/myshop/safebox/mall),非移植项,需自定义协议 + 服务端 | `extension/src/net/{wire.h,game_client.h,entity_store.{h,cpp},m2_client.{h,cpp}}` · `project/ui/{private_shop,mall,cube}_ui.gd` |
|
||||
| **P9 世界系统(首版)** | 🟡 `wire.h` `GCWarp`(18) / `GCTime`(12) / `GCChannel`(5) / `GCNPCPosition`(6+78×n) / `GCTargetCreate/Update/Delete`(46/16/8) / `GCMount`(21),全 static_assert。`EntityStore`:`WarpCue` + `drain_warps`、`server_time`/`channel` + dirty、`m_npc_marks` / `m_markers` map、`Entity.mount_vnum` + `drain_mount_changes`。`M2Client`:`get_channel/get_server_time/get_npc_marks/get_world_markers` + `warp`/`time_changed`/`channel_changed`/`npc_marks_changed`/`world_markers_changed`/`mount_changed` 信号。`ui/minimap.gd`(右上圆形,北朝上,实体/NPC/任务标记 + 玩家箭头 + CH 角标)+ `world/world_time.gd`(epoch → 太阳角度/光色/ambient 昼夜)+ `fx/weather.gd`(雪/雨 GPUParticles 跟相机)。`game_scene._on_warp` 同服挪人。`p9_test.gd` + `net.entity_store` 扩测全过 | `extension/src/net/*` · `project/ui/minimap.gd` · `project/world/world_time.gd` · `project/fx/weather.gd` · `project/game_scene.gd` |
|
||||
| **P10 网络补全(首版)** | 🟢 `wire.h` `CGClientVersion`(0x000D,70B)。`GameClient` 收 `GC_MAIN_CHARACTER` → 自动发 `CG_CLIENT_VERSION` = **打通 LOADING→PHASE_GAME**(真机验证进游戏 + 121 实体)。`net/serverinfo.gd`(`class_name ServerInfo`,TSV 服务器表 + 频道端口偏移)+ `ui/loading_screen.gd`(相位驱动读取遮罩)+ `ui/reconnect_ui.gd`(断线倒计时 + 自动/手动重连)+ `app_flow.gd`(LOGIN→SELECT→GAME 串场状态机,单 `M2Client` 贯穿)。attack CRC 全 0 实测服务器接受。`net_e2e` 加 move+attack poke;`net.loopback_flow` 断言自动版本报文;`p10_test.gd` 全过 | `extension/src/net/{wire.h,game_client.h}` · `extension/tools/net_e2e.cpp` · `project/{app_flow,net/serverinfo}.gd` · `project/ui/{loading_screen,reconnect_ui}.gd` |
|
||||
| 大地图窗 / 地形贴花 / 钓鱼 / 副本 / 坐骑换模型 / 跨服换图 / 频道人数轮询 / 真机字节校准 | ⬜ 见 [`CLIENT-ROADMAP.md`](./CLIENT-ROADMAP.md) P9·P10 剩余 | — |
|
||||
| 物品 / 装备(`item_proto` + 背包封包 + 装备挂模型 → 解锁 §2.7 高光 / §2.3 时装) | ⬜ | — |
|
||||
|
||||
## 3. 连通性(2026-08-30:真机端到端打通到 PHASE_GAME)
|
||||
|
||||
服务端 = **m2dev-fork 协议**。`net_e2e`(`extension/tools/net_e2e.cpp`,`build/extension/net_e2e`)
|
||||
驱动 auth→game→选人→进游戏全程;`MT_NET_TRACE=1` 打印每包 header/length/consumed。
|
||||
|
||||
**真机结果 —— 全程打通**:
|
||||
- ✅ auth 握手(libsodium KX)+ `CG_LOGIN3` → `GC_AUTH_SUCCESS`
|
||||
- ✅ game `CG_LOGIN2` → **`GC_LOGIN_SUCCESS4`(0x0105,4 槽)** 角色列表(`[SA]Admin` lv105)
|
||||
- ✅ `CG_CHARACTER_SELECT` → `GC_PHASE(LOADING)` → 整包洪流全部按正确字节解析
|
||||
- ✅ **`GC_MAIN_CHARACTER` 后自动发 `CG_CLIENT_VERSION`(0x000D)** → `GC_PHASE(GAME)` / **IN GAME**
|
||||
- ✅ 进游戏后:121 个实体、HP 14990/15178、level 105、20 格背包、好友列表 1 人
|
||||
- ✅ `CG_MOVE` + `CG_ATTACK`(CRC 全 0)保持连接不被踢 → **不需要占位 CRC**
|
||||
|
||||
**修的 3 个真 bug(对任何真服务器都必需)**:
|
||||
1. `GC_LOGIN_SUCCESS4`(0x0105) 没处理(只认 3 槽的)→ 加 `GCLoginSuccess4`(492B) + handler。
|
||||
2. 帧分发器 "did not consume" 误判:连续同 header 的包(一串 `GC_PLAYER_POINT_CHANGE`)会误杀连接
|
||||
→ 改成按**实际消费字节数**判定(`net_stream.cpp` dispatch())。
|
||||
3. **缺 `CG_CLIENT_VERSION`**:服务器收到 `GC_MAIN_CHARACTER` 后等客户端回版本报文,缺它就在
|
||||
phase 4 断线 → `GameClient` 收到 `GC_MAIN_CHARACTER` 时自动发(fork 固定 timestamp `1215955205`)。
|
||||
|
||||
**真机字节校准(2026-08-30,已做)** —— 用 `MT_NET_DUMP=1 net_e2e` 抓真实封包逐字节对:
|
||||
- **实体名 / 等级 / 公会 / 骑乘** ✅ 走 `GC_CHAR_ADD_INFO`(0x0207,`TPacketGCCharacterAdditionalInfo`
|
||||
97B),之前根本没处理。加 `GCCharAddInfo` struct + handler,NPC「Old Man」(race 20009) 名字正确出。
|
||||
- **`GC_CHARACTER_UPDATE`**(0x0209,38B) ✅ 之前只粗解 parts;现按 `TPacketGCCharacterUpdate` 全解
|
||||
(parts / guild / alignment / pk_mode / mount,`GCCharacterUpdate` struct)。
|
||||
- **`ch_type`**:`GC_CHARACTER_ADD.bType` 对 NPC/怪都是 0(服务器不填)。改在 GDScript 按 `race` 查
|
||||
`mob_proto.type` 分类(`net_play._entity_kind`:0 怪 / 1 NPC / 2 石头 / 3 warp),`_entity_name`
|
||||
怪名回退 `mob_proto.locale_name`。`game_scene` 现在也载 `mob_proto`。
|
||||
- **`GC_PLAYER_POINTS`**(0x0214,1024B) —— **本来就对的**。真机 gold=13.5 亿(GM 号)、exp=0、
|
||||
next_exp=int32 溢出为负都是服务器真实值。`M2Client.get_points` 把 exp/next_exp 按 unsigned 输出。
|
||||
|
||||
**第二轮校准(2026-08-30,已做)** —— 又抓了 in-game 洪流里没对过的包:
|
||||
- **`GC_SKILL_LEVEL_NEW`**(0x021B,1534B) ✅ 这才是本服发的技能包(**没发老的 0x021A**),之前
|
||||
`get_skills()` 永远空、K 键技能窗没内容、快捷栏没法施放。加 `PlayerSkill{u8 master, u8 level,
|
||||
u32 next_read}`(6B/条,fork 的 `time_t` 是 32 位)+ `GCSkillLevelNew` + handler。真机实测:33 个
|
||||
技能全 lvl 40 master 3(GM 号)。`get_skills()` 加 `master`(0 普/1 M/2 G/3 P)。
|
||||
- **`GC_QUICKSLOT_ADD/DEL/SWAP`**(0x0519/1A/1B) ✅ 之前没处理 → 快捷栏进游戏是空的。加
|
||||
`QuickSlot{u8 type,u8 position}` + 3 个 handler + `M2Client.get_quickslots()` + `quickslots_changed`。
|
||||
`ui/quickbar.gd.restore_from_server()` 按服务器内容填 1-8 号格(type 1 道具 / 2 技能)。真机 10 格。
|
||||
- **`GC_NPC_POSITION`**(0x0A50) —— struct **本来就对**(6 + 78×n,name[65])。真机 49 条 NPC,名字对
|
||||
(服务器不 zero-pad name buffer,`strnlen` 已兜住)。
|
||||
- **`GC_ITEM_UPDATE`**(0x0514,41B) / **`GC_CHAT`**(0x0603,10B 头) / **`GC_AFFECT_ADD`**(0x0A20,25B)
|
||||
/ **`GC_SPECIAL_EFFECT`**(0x0A30,9B) / **`GC_QUEST_INFO`**(0x0912,7B) —— 逐字节核对,**全部已对**。
|
||||
- **`GC_GUILD`**(0x0730) sub-header 包(grade/info/list)已解析(见 M4 行);会徽走 `MARK_*` 独立连接。
|
||||
|
||||
**校准后续(2026-08-30,已做)**:
|
||||
- **3D 名字标签刷新** ✅ `EntityStore` 加 `ChangeKind::Info`;`GC_CHAR_ADD_INFO` 到达时对已存在实体
|
||||
推 `Info` 变更 → `M2Client.entity_info(vid, dict)` 信号 → `net_world._on_info` 刷 Label3D。
|
||||
`net_world.name_resolver`(`game_scene` 设为 `net_play._entity_name`)让怪名走 `mob_proto`。
|
||||
- **跨服换图 `GC_WARP` addr≠0** ✅ `M2Client`:`WarpCue.addr` 是网络序 IPv4,转点分十进制后
|
||||
`warp_to_game_server(host, port)` —— 拆掉当前 game 连接、复用存下的 `login_key` 直连新 game
|
||||
server(跳过 auth)。同服 warp 仍只挪玩家。
|
||||
- **断点快连** ✅ `M2Client.reconnect()` 现在优先用 `last_login_key` 直连 game server(省一次
|
||||
auth 往返),失败再走完整 auth→game。服务器重发 spawn = 断点续连。
|
||||
- **频道负载查询** ✅ `net/channel_status.gd`(对齐 `CServerStateChecker`):明文 TCP 连任一频道口
|
||||
→ 发 `CG_STATE_CHECKER`(0x000F,len 4) → 跳过杂包到 `GC_RESPOND_CHANNELSTATUS`(0x0010) →
|
||||
读 `int32 count` + count×`{i16 port, u8 status}`。status:0 关 / 1 正常 / 2 拥挤 / 3 爆满
|
||||
(Metin2 协议里就是 4 态,不是在线人数)。`app_flow._probe_channels` 先查状态、没答上的频道回退
|
||||
`_probe_tcp`,下拉项标 ●/◐/✕。**真机实测**:`192.168.21.203:11011` 回 `{11011:1, 11012:1,
|
||||
11013:1, 11991:1}` —— 也据此把 `serverinfo` 的频道端口步长从 +10 改成 **+1**(可在 serverlist.txt
|
||||
第 7 列 `port_step` 按服覆盖)。`channel_status_test`(进程内 mock 服务器:请求字节 + 跳杂包 +
|
||||
解析 + 不可达返回空)。
|
||||
|
||||
- **怪 / NPC 真 3D 模型** ✅ `ui/mob_view.gd`(对齐 `player_view.gd`):`race` = `mob_proto` vnum →
|
||||
**`root/npclist.txt`**(`<vnum>\t<代号>\t…`,如 `113→bear_brown`、`185→tiger_big`、`102→wolf`;
|
||||
翻译版 `mob_proto` 把 `szName` 也本地化成 "Brown Bear" 了,解不出目录,必须靠这张表,`mob_proto`
|
||||
名仅作兜底)→ 目录按代号逐段回退(`bear_brown → bear`),网格 / 贴图用完整代号
|
||||
(换肤网格都在基础目录里:`bear/bear_brown.gr2` + `bear/bear_brown.dds`)→ 进 `Metin2Model`
|
||||
(texture_dir = 目录)+ 解 `motlist.txt` → `Metin2AnimPlayer`。扫
|
||||
`<assets>/*/ymir work/{monster,monster2,npc,npc2}/<folder>/`。npclist 解析一次进 `static` 缓存。
|
||||
`set_anim_state("wait/run/attack/damage/dead/skill")` 按 motlist 动作名映射。
|
||||
`game_scene._make_entity_model` 设为 `net_world` 的默认工厂(非主角 + 有 mob_proto 时);race→是否有模型
|
||||
缓存,失败回退占位胶囊。`net_world._attach_nameplate` 抽出来,真模型也挂头顶名字 + HP 条。
|
||||
`mob_view_test`:race 102 Wolf → `wolf/` 目录、Metin2Model(40 骨 762 顶点)、motlist ≥3 动作、状态切换。
|
||||
实测进游戏「Alpha Grey Wolf」「Hungry Black Bear」等出真模型 + 贴图 + 动作 + 名字牌 / 血条。
|
||||
|
||||
- **重连 / 已在局内时补拉实体** ✅ `game_scene.setup()` 现在是协程(分帧避免阻塞漏 PONG),
|
||||
`net_world.setup()` 接 `entity_spawned` 已晚于 `M2Client` 的初始 spawn 洪流 → `_on_spawn` 0 次触发。
|
||||
修:`net_world.catch_up()` 遍历 `client.get_entities()` 补 spawn 未镜像的;`game_scene` 已在局内分支里
|
||||
**先** `net_world.set_local_vid(get_main_vid())` + `net_play._on_main_set(get_main_vid())` 再
|
||||
`catch_up()`,否则本地玩家会被画成一个压在身上的大蓝胶囊。
|
||||
|
||||
**剩的都不是代码问题**:字面「在线人数」(数字)Metin2 协议里没有 —— `bStatus` 就是 4 态负载指示,
|
||||
要真数字得改服务器;本地 `assets/` 只有 ~85 个 monster 目录(不是全量),没目录的 race
|
||||
仍是占位胶囊 —— 换全量资产包即可全部出真模型。
|
||||
|
||||
**联调路上修的两个真 bug**(对任何真实服务器都必需):
|
||||
1. **`GC_LOGIN_SUCCESS4`(0x0105) 没处理** —— 只认 3 槽的 `GC_LOGIN_SUCCESS3`。已加 `GCLoginSuccess4`
|
||||
结构(492B) + handler。
|
||||
2. **帧分发器误杀连接** —— 连续同 header 的封包(如一串 `GC_PLAYER_POINT_CHANGE`)会触发
|
||||
"on_packet did not consume" 误判并断线。改成按**实际消费字节数**判定
|
||||
(`net_stream.cpp` dispatch())。此 bug 之前会让 P0–P8 对任何真服务器的端到端全挂。
|
||||
|
||||
**当前状态**:P0–P8 的封包层仍以合成封包 CTest + FakeClient GDScript 测试离线验证(14 套 GDScript +
|
||||
9 CTest 全绿)。真机端到端到 PHASE_GAME 的最后一跳留给 P10。
|
||||
|
||||
## 3b. B 轨(不依赖服务端的客户端系统)
|
||||
|
||||
| 模块 | 状态 | 位置 |
|
||||
|---|---|---|
|
||||
| **eterpack 读写器**(G1)| ✅ 本 fork = `PackLib` 单文件包(header + XChaCha20 加密索引 + zstd 数据 + 可选逐文件加密,硬编码 `PACK_KEY`)。`EterPack`(读,name-field 从 header 反推兼容真实包)+ `write_pack` + `packtool` CLI。`pack.roundtrip` CTest + 真实 `textureset/` 45 文件明文/加密往返 `diff` 一致 | `extension/src/pack/eterpack.{h,cpp}`、`extension/tools/packtool.cpp` |
|
||||
| **登录 / 选人界面**(首版)| ✅ `charselect.gd`:3 旋转台 × 真实职业模型(`<class>_novice.gr2` + `wait.msa`)+ 真实 `board` 九宫格面板(槽位按钮 + 进入游戏)。数据来自 `M2Client.char_list`(联机)或 `--mock`(离线开发)。`login.gd`:服务器/账号/密码 + 连接,接全部 `M2Client` 信号(`--auto` 测试)。`ui_kit.gd`:从 `hud.gd` 抽出的九宫格助手。⬜ 创建/删除角色、旋转台微调、login→charselect→world 串场 | `project/{login,charselect,ui_kit}.gd` |
|
||||
| **正式选人页(Metin2 风)** | ✅ `ui/char_select_screen.gd`:`app_flow._build_char_list` 用它替掉原来的裸按钮列表。全屏背景 `d:/ymir work/ui/intro/select/select.sub`(=select.jpg)+ 右侧 `SubViewport`(`own_world_3d`)里 `PlayerView` 真模型(正面朝相机 + `wait` + 缓摆)+ 左上职业名(locale `name_<class>.sub` → `select.dds`,DDS 走 `Metin2World.load_dds`;解不出退中文描边字 猛将/刺客/术士/巫女)+ 左侧 `board` 信息板(帮会徽 `get_guild_mark_image` / **国家名 `get_empire()`→`EMPIRE_A/B/C` locale + 国旗 `empireflag_<a/b/c>.sub`** / 帮会名 / 名称 / 等级 / 游戏时间 / 体力·智力·力量·敏捷 条)+ 开始 / 创建 / 删除 / 退出 + 多角色 ◀ ▶ 槽位切换(`_pad_slots` 按 `get_slot_count()` 补空槽,◀▶ 能走到空槽建号)。**建号 / 删号(1:1 对齐 `PythonNetworkStreamPhaseSelect.cpp`)**:`CG_CHARACTER_CREATE`(0x0201) `CGCreateCharacter`(77,job 是 **u16**)、`CG_CHARACTER_DELETE`(0x0202) `CGDeleteCharacter`(13,`private_code[8]`)、`GC_PLAYER_CREATE_SUCCESS`(0x020C) `GCPlayerCreateSuccess`(108=`{u8 slot, SimplePlayerInfo}`)、`GC_PLAYER_CREATE_FAILURE`(0x020D) `{u8 type}`、`GC_PLAYER_DELETE_SUCCESS`(0x020E) `{u8 slot}`、`GC_PLAYER_DELETE_WRONG_SOCIAL_ID`(0x020F)。`GameClient`:`create_character`/`delete_character` + `drain_char_events()`(`CharEvent{CreateOk/CreateFail/DeleteOk/DeleteFail}`)+ `replace_slot`/`clear_slot`(服务器只补丁槽位不重发列表)+ `slot_count()`。`M2Client`:`create_character(slot,name,job,shape,con,int,str,dex)`/`delete_character(slot,code)`/`get_empire`/`get_slot_count` + `char_created`/`char_create_failed`/`char_deleted`/`char_delete_failed` 信号(建 / 删成功后重发 `char_list`)。选人页建号弹窗(职业下拉 + 名称 + 各职业起始四维只读)、删号弹窗(secret 删除码);`app_flow._on_char_list` 在 SELECT 态只 `set_chars()` 不整屏重建。会徽:`app_flow._goto_select` 也发 `download_guild_marks`,`guild_marks_ready`(one-shot) → `refresh_crest()`。相机改按**包围球** `r/sin(fovV/2)` 自适应 + 沿 -X 推 `r*0.5`(横向视锥更宽,不切远侧手),蒙皮定型后多 fit 4 帧。C++:`CharSlot` + `char_list` dict 补 `play_minutes/st/ht/dx/iq/main_part/hair_part`。`ui_assets.gd` 加 `load_dds_image()`。`net.loopback_flow` MockServer 加 `CG_CHARACTER_CREATE/DELETE` 真 socket 端到端(名称字节校验 + 成功补丁槽位 + 名称冲突 fail + 错误删除码拒绝);`char_create_delete_test.gd`(弹窗 → 假 client 收 `create_character(slot1,"Newbie",job1,con3/int3/str4/dex6)` + 短名拒绝 + `char_created` 关窗 + 删号码 + 失败清框 + `set_chars` 保持选中);`p10_test` 选人断言改点「开始」 | `project/ui/char_select_screen.gd` · `project/ui/ui_assets.gd` · `project/app_flow.gd` · `extension/src/net/{wire.h,game_client.h,m2_client.{h,cpp}}` · `extension/tests/net_loopback_test.cpp` |
|
||||
| **角色状态窗(1:1 窗口迁移样板)** | ✅ `project/ui/char_status_ui.gd`:`UiScript`+`UiBuild` 直接装载 `assets/uiscript/uiscript/characterwindow.py` 真布局(4 页 tab:STATUS/SKILL/EMOTICON/QUEST,`SetState` 切页;技能 / 表情 / 任务的数据仍走各自专用窗)。STATUS 页数值逐字对照 `assets/root/uicharacter.py.RefreshStatus`:`Level_Value`=`points[LEVEL]`、`Exp_Value`=`u32(exp)`、`RestExp_Value`=`u32(next_exp)-u32(exp)`、`HP/SP_Value`=`cur/max`、`STR/DEX/HTH/INT_Value`=`points[ST/DX/HT/IQ]`、`ATT_Value`=`(MIN_ATK|MAX_ATK)+ATT_GRADE_BONUS+PARTY_ATT_GRADE`(min==max 取单值,全 0 退 `ATT_POWER`)、`DEF_Value`=`DEF_GRADE(+DEF_GRADE_BONUS 若非 0)`、`MATT_Value`=`MAGIC_ATT_GRADE+(MIN|MAX)_MAGIC_WEP`、`MDEF_Value`=`MAGIC_DEF_GRADE`、`ASPD/MSPD/CSPD/ER_Value`=`points[ATT_SPEED/MOV_SPEED/CASTING_SPEED/EVADE_RATE]`(EPointTypes 索引取自 m2dev `src/UserInterface/Packet.h`)。加点:`HTH/INT/STR/DEX` 的 `_Plus`/`_Minus` 按钮 → `client.say(0, "/stat ht"|"/stat- ht" …)`,与原 `statusPlusCommandDict`/`statusMinusCommandDict` 完全一致;`Status_Plus_Value`=`points[STAT]`,`>0` 才显示 `_Plus` 按钮和 `Status_Plus_Label`。名称 / 帮会 / 职业头像取 `get_entity(get_main_vid())`(`race%4` → `face_warrior/assassin/sura/shaman.sub`)。`points_changed` 信号刷新。`game_scene`:`char_status_ui` 节点 + V / C 热键。`char_status_ui_test.gd`:假 client 灌 points → 断言全部 Value label 文本 + `/stat` 命令 + `STAT==0` 隐藏加点按钮 + tab 切换页可见性。后续选道具网格 / 商店 START_EX / 私人商店 39 格照抄这套(uiscript 装载 + 逐字数值绑定 + 原聊天命令 / 封包)| `project/ui/char_status_ui.gd` · `project/game_scene.gd` · `project/char_status_ui_test.gd` |
|
||||
| **组队成员信息板(`uiparty.PartyMemberInfoBoard`)** | ✅ `wire.h`:`CGPartySetState{u16 hdr,u16 len,u32 pid,u8 role,u8 on}`(static_assert 10)+ `EPartyRole` 枚举(NORMAL 0 / LEADER 1 / ATTACKER 2 / TANKER 3 / BUFFER 4 / SKILL_MASTER 5 / BERSERKER 6 / DEFENDER 7,PythonPlayer.h)。`game_client.send_party_set_state(pid,role,on)`(`CG_PARTY_SET_STATE` 0x0704,注意结构字段名 `dwVID` 实际传 PID —— 对齐 `uiparty.py OnSelectState` 的 `net.SendPartySetStatePacket(self.pid,...)`)。`M2Client.party_set_state(pid,role,on)` + bind;`get_party()` dict 补 `state`(完整角色字节,bit0=leader)+ `affects[7]`(int×7)。`project/ui/party_ui.gd` 从「名字+一条 HP」重做成 `partymemberinfoboard.py` 布局(每员 106×36 条:StateButton 22px 角色图标 + NameSlot + Gauge + 附加图标行):`_strip(m)` → 角色状态按钮(`_local_is_leader()` 才可点 → `_open_role_popup` 弹 普通/攻击/坦克/狂战/辅助/宗师/防御 + 踢出;普通=`party_set_state(pid, cur_role, false)` 清角色;踢出=`party_leave(pid)`=`SendPartyRemovePacket`)+ 名字(★队长) + HP `ProgressBar` + `affects[i]!=0` → chip(`AFFECT_LABEL` 暂定 经验/攻击/防御/辅助/宗师/时间/回复,tooltip 带值)。点名字 → `set_target(vid)`(`OnMouseLeftButtonDown` 选中)。header:EXP 分配开关 `_toggle_distribute` → `party_set_distribute`(`EPartyExpDistributionType` 0 NON_PARITY / 1 PARITY)+ 组队治疗 `_party_heal` → `party_use_skill(PARTY_SKILL_HEAL=1, 0)`。`_local_is_leader()` = `get_party()` 里 `vid == get_main_vid()` 那条的 `leader`。角色菜单按党技能等级门控(`uiparty.__ShowStateButton` Tanker≥10…Defender≥40)暂略。`p8_test` 组队段扩:affect chip 渲染 + 队长点 StateButton 弹菜单 + 「攻击」→ `party_set_state(pid, 2, true)` + 踢出 → `party_leave` + 分配切换 → `party_set_distribute(1)` + 治疗 → `party_use_skill(1,0)` + 点名字 → `set_target` + 非队长 StateButton disabled。**顺带修 `p8_test._init` 未 `await _run()` 的隐患**(第一个 `await` 之后所有断言从未生效,含增量 34 `shop_ex`)→ 改 `await` 后暴露 `shop_ui.refresh()` `queue_free` 延迟 bug(旧 row 当帧未消失),改 `remove_child`+`queue_free` | `extension/src/net/{wire.h,game_client.h,m2_client.{h,cpp}}` · `project/ui/party_ui.gd` · `project/ui/shop_ui.gd` · `project/p8_test.gd` |
|
||||
| **ESC 系统菜单 + 游戏设置窗(`uisystem.SystemDialog` + `uigameoption.OptionDialog`)** | ✅ `project/ui/system_menu_ui.gd`(`systemdialog.py` 真 uiscript,thinboard 8 键)= `uisystem.py __LoadSystemMenu_Default` 1:1:`system_option_button`→`system_option_ui.open()`,`game_option_button`→`game_option_ui.open()`,`change_button`→`client.say(0,"/phase_select")`(`net.ExitGame()` 在有 ping 时发 `SendChatPacket("/phase_select")`),`logout_button`→`"/logout"`(`net.LogOutGame()`),`mall_button`→`"/in_game_mall"`,`exit_button`→`get_tree().quit()`,`help_button`→占位 `toast`,`cancel_button`/标题栏→关。modal 打开。`game_scene`:ESC 从直接 `system_option_ui.toggle()` 改成 `system_menu_ui.toggle()`。<br>`project/ui/game_option_ui.gd`(`gameoptiondialog.py` 真 uiscript)= `uigameoption.py`:`BLOCK_BITS` = `block_{exchange,party,guild,whisper,friend,party_request}_button`(toggle_button) → `_toggle_block(bit)`:`_block_mode ^= bit` + `client.say(0, "/setblockmode %d" % _block_mode)`(`Packet.h EBlockAction` 位 `1<<0..1<<5`,逐字对照 `__OnClickBlock*` 的 `net.SendChatPacket("/setblockmode " + str(blockMode ^ player.BLOCK_*))`)+ 持久化 `[gameopt] block_mode`(服务器回包同步待补)。`PK_CMD` = `pvp_{peace,revenge,free,guild}`(radio) → `client.say(0,"/pkmode %d")` 值 `{0,1,2,4}`(`__OnClickPvPMode*`;guild 是 `/pkmode 4`,`PK_MODE_PROTECT`=3 跳过)。`DISPLAY_RADIOS`:`name_color`/`target_board`/`view_chat`/`always_show_name`/`show_damage`/`salestext` 的 on/off/normal/empire radio → `_display[key]` + 持久化 `user://system_option.cfg [gameopt]`(等价 `systemSetting.Set*Flag`;渲染侧钩子——头顶名字配色 / 伤害数字 / 聊天显隐——待补)。`_bind_radio` 处理 radio 배타성,`_sync`/`_sync_block` 开窗回填,`_relabel` 中文兜底(uiScriptLocale 未接全局 Locale)。`system_menu_ui_test.gd`:两窗装载 + `/setblockmode 1`→`3`→`2`(位 XOR)+ `_block_mode` 跟踪 + `/pkmode 1`/`4` + radio 배타 + 显示开关落 cfg + 二次实例读 `block_mode` 回填按钮态 + menu 各键 `/in_game_mall`/`/phase_select`/`/logout` + `system_option_button`/`game_option_button` 开子窗关菜单 + `cancel` 关。C++ 无改动 | `project/ui/system_menu_ui.gd` · `project/ui/game_option_ui.gd` · `project/game_scene.gd` · `project/system_menu_ui_test.gd` |
|
||||
| **系统设置窗(`uisystemoption.OptionDialog`)** | ✅ `project/ui/system_option_ui.gd`:`UiScript`+`UiBuild` 装 `assets/uiscript/uiscript/systemoptiondialog.py`(board + titlebar + `music/sound_volume_controller` sliderbar + `camera_short/long` + `fog_level0/1/2` + `tiling_cpu/gpu` + `tiling_apply` + `bgm_button`)。控件绑定逐字对照 `assets/root/uisystemoption.py`:`OnChangeMusicVolume`→`snd.SetMusicVolume`+`systemSetting.SetMusicVolume`(接 `Audio.master_bgm` + 即时改在播 `_bgm[_bgm_cur].volume_db`),`OnChangeSoundVolume`→`Audio.master_sfx`(`play_ui`/`play_at` 每次读),`camera_short/long`→`game_camera.max_dist`(`CAMERA_MAX=[11,20]`,并 `dist=min(dist,max)`),`fog_level0/1/2`(浓/중/淡)→`__SetFogLevel` = `Environment.fog_enabled=true`+`fog_density=[0.055,0.018,0.004][i]`,`tiling_*`/`tiling_apply`(原 CPU/GPU 分块渲染 + `net.ExitGame` 重启)Godot 渲染器无对应 → 保留占位。`_bind_radio` 处理 radio 배타성。持久化 `user://system_option.cfg`(section `audio`/`video`,= `systemSetting` 配置文件);`setup()` 读回并 `_apply_all()` 即时生效;`game_scene._on_main_set` 在 `.msenv` 换 `Environment` 后再 `_apply_all()`。`game_scene`:ESC 呼出(`ui_manager._unhandled_input` 先 `close_top()` 吃事件,无窗时才落到 `game_scene`)。uiScriptLocale 未接全局 Locale → `LABELS` 中文兜底。`system_option_ui_test.gd`:装载 + `music/sound` slider `value_changed` 已连 + `_on_*_volume` 应用到 `master_bgm/sfx` + camera radio→`max_dist`+dist 夹 + fog radio→`fog_density` + cfg 落盘 + 二次实例读 cfg 即时应用。C++ 无改动 | `project/ui/system_option_ui.gd` · `project/game_scene.gd` · `project/system_option_ui_test.gd` |
|
||||
| **私人商店开设窗(`PrivateShopBuilder` 模型)** | ✅ `project/ui/private_shop_ui.gd` 从「前 5 行 checkbox」重做成 1:1 复刻 `assets/root/uiprivateshopbuilder.py`(**增量 36:布局改走真 uiscript `privateshopbuilder.py` —— board + TitleBar + `NameLine`(盖 LineEdit) + `ItemSlot` 5×8=40 grid + Ok/Close;背包候选面板挂窗右侧**):`_stock` = `itemStock`(`{格号 -> {cell,vnum,count,price}}`)。左侧 `_inv_list` 背包候选(排除已上货的 cell),点一件 → `_pick` 设 `_picked`(= `mouseModule` attached item);点右侧 40 格(`GRID_SLOTS=40` = `shop.SHOP_SLOT_COUNT`,5×8)空格 → `_ask_price`(`uiCommon.MoneyInputDialog`,`SpinBox`)→ `_place(slot, price)` 写 `_stock`(= `AddPrivateShopItemStock` + `itemStock[targetSlotPos]=(src)`);点已占用格 → `_stock.erase` (= `OnSelectItemSlot` / `DelPrivateShopItemStock`)。`_ok()`(OkButton → `BuildPrivateShop`):`_stock` 按格号 `sort()`、`display_pos = 格号`、`items.size() >= PRIVATE_SHOP_ITEM_MAX(39)` 截断 → `client.open_private_shop(sign, [{vnum,count,inv_cell,price,display_pos}])`(`MyShopItem`/`TShopItemTable` 字段 1:1:`{u32 vnum, u8 count, ItemPos pos, u32 price, u8 display_pos}`)。`_close_shop()` = `close_private_shop()` = `SHOP_CG_END`。测试拆成独立 `private_shop_ui_test.gd`(真 uiscript,缺资产跳过):`_cells` 40 格 / `NameLine` 盖 LineEdit / 2 候选 / 空 stock 不发 / 拿起→价格弹窗→`_place` / 개설按格号排序(格 0 在格 3 前)+ `display_pos` / 点占用格撤下 / 철수;`shop_cube_mall_test` 私人商店段移除。C++ 无改动(`open_private_shop` / `MyShopItem` 早已就位)| `project/ui/private_shop_ui.gd` · `project/game_scene.gd` · `project/private_shop_ui_test.gd` · `project/shop_cube_mall_test.gd` |
|
||||
| **`SHOP_GC_START_EX` 多货架商店** | ✅ `wire.h`:`ShopTabHead{char name[SHOP_TAB_NAME_MAX=32]; u8 coin_type}`(static_assert 33)+ `SHOP_TAB_COUNT_MAX=3`。`entity_store`:`SHOP_GC_START_EX`(10) 解析 `TPacketGCShopStartEx{u32 owner_vid; u8 tab_count}` + `tab_count` × `{ShopTabHead, ShopItem[SHOP_HOST_ITEM_MAX_NUM=40]}`(对齐 `PythonNetworkStreamPhaseGame.cpp RecvShopSub_StartEx`)。`ShopEntry` 加 `u8 pos`(货架内槽位);`ShopTab{name, coin_type, items}` + `m_shop_tabs`;`SHOP_GC_START` 也改为按固定 40-槽数组填 `pos`(空槽计入,与参考 `SetItemData(j,...)` 一致);`SHOP_GC_END` 清 `m_shop_tabs`。`shop_items()` 保持是 `m_shop_tabs[0].items` 的镜像,旧 `get_shop_items()` 不动。`M2Client.get_shop()` → `{vid, open, tabs:[{name, coin_type, items:[{pos,vnum,price,count}]}]}`。`shop_ui.gd`:`tabs.size()>1` 时显示货架 `Button` 行,切货架重建列表;买位置 = `_active_tab * SHOP_SLOT_COUNT(40) + it.pos`(1:1 对齐 `uishop.py GetIndexFromSlotPos` = `tabIdx * shop.SHOP_SLOT_COUNT + slotPos`,`shop_buy` 的 `u8 pos` 容得下 3×40)。`net_entity_test`:双货架合成包 → 断言名称 / coin_type / 槽位保留 / tab0 镜像 / END 清空;`p8_test`:多货架 UI + 切页 + `buy pos 5`(tab0 slot5)/ `buy pos 43`(tab1 slot3) | `extension/src/net/{wire.h,entity_store.{h,cpp},m2_client.{h,cpp}}` · `project/ui/shop_ui.gd` · `extension/tests/net_entity_test.cpp` · `project/p8_test.gd` |
|
||||
| **选魔石窗(样板第 2 例)** | ✅ `project/ui/select_item_ui.gd`:`UiScript`+`UiBuild` 装 `assets/uiscript/uiscript/selectitemwindow.py`(`ItemSlot` = 5×8 `grid_table`,`ui_build._slot_cells` 按 `x_count/start_index/x_step/y_step` 生成 `slot_0..39`)。填格逐字照 `assets/root/uiselectitem.py.RefreshSlot`:遍历背包 `range(INVENTORY_PAGE_SIZE*2=90)` 格,`_is_metin`(`proto.item(vnum).type == 10 = ITEM_TYPE_METIN`)+ `_item_grade(vnum) <= 2`(= `proto.item(vnum).name` **内部名**最后一位数字,非数字按 0,复刻 `PythonPlayerModule.GetItemGrade`),`slotPos > 54` 截断,按背包格号升序映射到选择窗格 `_slot_to_inv[slotPos]=invCell`。点格 → `client.script_select_item(invCell)`(`send_script_select_item` → `CGScriptSelectItem{CG_SCRIPT_SELECT_ITEM 0x0903, len, selection}`,= `net.SendSelectItemPacket`)+ 关窗;`ExitButton` / 标题栏关闭 → `script_select_item(0)`(`uiselectitem.Close`),`_sent` 标记避免选完再补发 0。**触发链 1:1**:`quest_dialog.gd.parse_script` 新识别 EventManager `[SELECT_ITEM]` token → `has_select_item` → `_on_script` 发新 `select_item_requested` 信号(对应 `PythonEventManager EVENT_TYPE_SELECT_ITEM` → `interfacemodule.BINARY_OpenSelectItemWindow` → `wndItemSelect.Open()`);`game_scene` 在 `proto`/`item_list` 就绪后建 `select_item_ui` 并把该信号接到 `.open`。`select_item_ui_test.gd`:`[SELECT_ITEM]` token 解析 + 非 metin / grade>2 过滤 + 升序映射 + 点格发 `script_select_item(7)` + 关窗不补发 + 空手关窗发 `script_select_item(0)` | `project/ui/select_item_ui.gd` · `project/ui/quest_dialog.gd` · `project/game_scene.gd` · `project/select_item_ui_test.gd` |
|
||||
| **`item_proto` / `mob_proto` 读取器** | ✅ `mtproto::load_proto` —— 外层 `MIPX`(item:`[fourcc][ver=1][stride][elements][datasize]`) / `MMPT`(mob:`[fourcc][elements][datasize]`);内层 `MCOZ` CLZO 容器 = **XChaCha20**(本 fork 把 TEA 换成 libsodium:key=BLAKE2b(key16,"M2DevPackEncrypt"),nonce=BLAKE2b(key16,"M2DevNonce")[:24])解密 + **LZO1X** 解压。两把硬编码 4-DWORD key(`ItemManager.cpp` / `PythonNonPlayer.cpp`)。`ItemRecord`/`MobRecord` 解前导字段(`bSpecular`@234 → §2.7;**`alValues[6]`@191** —— aLimits[2]×5 @166 + aApplies[3]×5 @176 后,`bSpecular`@234 锚定整链)。armor 的 `alValues[3]` = race `.msm` 的 body shape index(对齐客户端 `__ArmorVnumToShape` `SHAPE_VALUE_SLOT_INDEX=3`)—— 实测 "Monk Plate Armour" 11200-09 全 = shape 3,specular 随强化 0→100 递增。`proto.item_mob` CTest 断言 11209 `values[3]==3` + `specular==100`。5748 items(stride 236)+ 1338 mobs(stride 335) | `extension/src/proto/proto.{h,cpp}` |
|
||||
| **`AssetResolver` 接包**(首版)| ✅ `mtpack::PackMount` —— 挂载多个 `.epk`,按虚拟路径(盘符 + 反斜杠 + 大小写不敏感 + `ymir work/` 后缀)resolve/read,`metin2_patch_*` 包后挂覆盖。`mtpack::AssetSource` —— 散文件 + 包统一:`read()` 取字节;`to_path()` 把包内文件解到缓存目录一次(路径型调用方不用改)。散文件优先于包(dev overlay,对齐客户端 `pack/` 覆盖)。`pack.roundtrip` CTest 扩:2 包挂载 + 覆盖 + 虚拟路径 + 非 ymir 路径。⬜ 把 `Metin2World`/`Metin2Model` 等 ~6 处 `resolver->resolve()` 换成 `AssetSource::to_path()`(机械改动)| `extension/src/pack/{pack_mount,asset_source}.{h,cpp}` |
|
||||
| **输入 & 游戏相机**(首版)| ✅ `game_camera.gd` = `GameCamera`:右键拖拽环绕(yaw/pitch clamp)+ 滚轮缩放(3.5–20m)+ 目标平滑跟随 + 地形防穿。`player_controller.gd` = `PlayerController`:**点地移动**(射线求地表交点 → 走过去,沿途 `is_blocked`(`attr.atr` bit0)判可行走,贴墙磨蹭按实际位移切回 wait —— 客户端预测,无服务端纠正)+ WASD 覆盖 + **选目标**(射线取最近 pickable → `target_selected` 信号)+ `anim_state`/`moved` 信号。已换进 `world_demo.gd`(去掉 ~70 行临时相机/输入)。⬜ 相机撞建筑/树淡出(§8.2)、格子 A* 寻路、NPC 交互/拾取 | `project/{game_camera,player_controller}.gd` |
|
||||
| **本地化**(首版)| ✅ `locale.gd` = `Locale`:加载 `locale/locale/<lang>/{locale_interface,locale_game,itemdesc,skilldesc}.txt`(`KEY\tVALUE`,UTF-8)。`t(key, args)` —— `%` 格式化 + 缺失回 `<KEY>`(开发期可见);`set_lang()`;`apply_font()` 挂 CJK ttf。已接 `charselect`(`JOB_*` 职业名 / `SELECT_LEVEL` / `SELECT_SELECT`=Start / `SELECT_CREATE`)+ `login`(`LOGIN_ID`/`LOGIN_PASSWORD`/`LOGIN_CONNECT`)。18 语言(拉丁/西里尔/希腊/阿拉伯,Godot 原生,无需打包字体;CJK 需外挂 ttf)| `project/locale.gd` |
|
||||
| **声音**(首版)| ✅ `audio.gd` = `Audio` 节点:**BGM 交叉淡**(2× `AudioStreamPlayer`,`bgm/*.mp3`)+ **UI 音效池**(8×,`**/sound/ui/*.wav`)+ **3D 定位音效**(`AudioStreamPlayer3D`,`**/sound/<rel>`)。路径解析走 11 个声音根 + `bgm/`(`.mss` 不用,Godot 原生解 mp3/wav/ogg)。已接:`charselect`→`characterselect` BGM + `<class>_select` 选人音;`login`→`loginok`/`loginfail`;`world_demo`→`Metin2AnimPlayer.motion_event` 的 `sound` 字段 → `play_at(model_pos)`(脚步/挥击)。⬜ 按地图 BGM(服务端 `GCMainCharacter.szBGMName`)、`.msenv` 环境音循环、战斗/怪物/NPC 语音 | `project/audio.gd` |
|
||||
| 登录 / 选人界面 | 🟡 首版 | `project/{login,charselect,ui_kit}.gd` |
|
||||
| 声音(`AudioLib`→Godot audio)| 🟡 首版 | `project/audio.gd` |
|
||||
| `item_proto` / `mob_proto` 读取器 | ✅ | `extension/src/proto/proto.{h,cpp}` |
|
||||
| 本地化(`EterLocale`)| ✅ 首版 | `project/locale.gd` |
|
||||
| 输入 & 游戏相机 | 🟡 首版 | `project/{game_camera,player_controller}.gd` |
|
||||
| `AssetResolver` 从包读取(接 eterpack)| 🟡 | `extension/src/pack/{pack_mount,asset_source}.{h,cpp}` |
|
||||
|
||||
## 3c. C 轨(渲染打磨,PARITY-GAP 不卡 §0 门禁的项)
|
||||
|
||||
| 项 | 状态 | 位置 |
|
||||
|---|---|---|
|
||||
| §8.2 相机遮挡淡出 + §8.3 相机撞建筑防穿 | ✅ 首版 | `extension/src/metin2_world.cpp`(层 2 盒碰撞)+ `project/game_camera.gd` |
|
||||
| §2.2 GPU 路径发型 | ✅ | `extension/src/metin2_model.{h,cpp}` |
|
||||
| §2.10 角色 LOD 距离切换 | ✅ 首版 | `extension/src/metin2_model.{h,cpp}` |
|
||||
| §3.5 地形 patch 裁剪 | ✅ 首版(拆 4×4 patch + 逐 patch 视锥剔除 + 远景剔除)| `extension/src/metin2_world.{h,cpp}` |
|
||||
| §2.6 顶点色 | ✅ 排查=不适用(全资产 gr2 无顶点色成员;逐顶点色是地形雾运行时 VB + `.mse` colorfactor)| — |
|
||||
| §1.2 `.mse` EffectLib 端口 | ⬜(大工程)| — |
|
||||
|
||||
## 4. 下一步
|
||||
|
||||
1. 打通到 `.203` 的连通性(见上)。可达后立即能测:
|
||||
- `./build/extension/net_probe 192.168.21.203 11000 admin 123456789` → 完整握手 + `CGLogin3` → `login_key`
|
||||
- GDScript 里 `M2Client.connect_to_server(...)` → 一路到 `char_list` / `entered_game`
|
||||
2. **`GCLoginSuccess3` 布局对真实字节校准**:`net_probe` / `M2Client` 收到后 dump 原始字节,
|
||||
核对 `TSimplePlayerInformation`(现假设 pack(1)、字段顺序照 `Packet.h:1161`)。size 不符会走
|
||||
`login_failed` 兜底。
|
||||
3. 游戏相位包解码:`GC_CHARACTER_ADD/DEL` / `GC_MOVE` / `GC_CHARACTER_UPDATE` →
|
||||
`NetworkActorManager` 端口 → 每实体一个 `Metin2Model`+`Metin2AnimPlayer`,用现有
|
||||
`Metin2World` 当场景。里程碑「角色连上、站在地图里、别的实体在动」。
|
||||
4. 输入(点地移动 → `CG_MOVE`)、聊天(`CG_CHAT`/`GC_CHAT` 接 HUD)。
|
||||
|
||||
## 4. 风险
|
||||
|
||||
- **协议版本漂移**:`Packet.h` 结构体要和服务端 build 一致。冻结一对 client/server commit。
|
||||
- **端到端测试阻塞**:目前握手端口 refused,走不到 `CGLogin3` 之前的验证。
|
||||
- **移动端 libsodium**:Android/iOS 要静态 vendor(本身可移植,非阻塞)。
|
||||
- 自用范围可跳过:商城 / GM 工具 / 部分社交系统。**反作弊(`CG_HACK` / `ProcessCRC` /
|
||||
`ProcessScanner`)已在 `CLIENT-GAP.md`「不做清单」标注 ❌ 不做**,除非服务端强制校验。
|
||||
|
||||
## 5. 移植面(客户端源码)
|
||||
|
||||
`EterBase/SecureCipher` · `EterLib/{NetStream,ControlPackets,NetPacketHeaderMap}` ·
|
||||
`UserInterface/{Packet.h, AccountConnector, PythonNetworkStream, PythonNetworkStreamPhase*}` ·
|
||||
`UserInterface/NetworkActorManager`(实体) · `GameLib`(玩法)。
|
||||
@@ -0,0 +1,263 @@
|
||||
# 客户端重构与 40250 逻辑对齐实施方案
|
||||
|
||||
## 1. 现状诊断与核心痛点
|
||||
|
||||
### 1.1 现状与优势
|
||||
- **渲染层(已基本稳定)**:
|
||||
- `extension/src/metin2_model.{h,cpp}`:基于开源 libgr2 的模型网格解析、材质贴图加载、GPU/CPU 骨骼蒙皮。
|
||||
- `extension/src/metin2_anim.{h,cpp}`:骨骼动画采样、双动画平滑融合(Crossfade)、挂点武器同步。
|
||||
- `extension/src/metin2_world.{h,cpp}`:地形高度场采样、贴图混合、水体、静态建筑碰撞。
|
||||
- `project/game_camera.gd`:轨道摄像机、防穿模逻辑。
|
||||
- **结论**:渲染层已具备优秀的跨平台能力(macOS/iOS/Android/Linux/Web),**无需推倒,纯粹作为“哑终端”画面渲染器(View)予以保留**。
|
||||
|
||||
### 1.2 逻辑层的混乱根源
|
||||
当前各种 Bug(“WASD翻跟头”、“连击不流畅/手感怪异”、“NPC交互后商店空白”、“任务无法打开”)的共同根源在于:
|
||||
1. **庞大的 GDScript 意大利面条代码**:
|
||||
- `net_play.gd` (~2200 行)、`game_scene.gd` (~1800 行)、`net_world.gd` (~1760 行) 试图用 GDScript 凭空重写 40250 的几万行 C++ 客户端逻辑。
|
||||
2. **凭感觉实现的“伪逻辑”**:
|
||||
- 没有严格对照 40250 C++ 源码的状态机(如按键优先级、阻尼公式、连击输入窗、NPC点击距离分流、封包处理链),而是用自制计时器和启发式逻辑拼接,导致产生大量边界 Bug 和时序脱节。
|
||||
3. **数据没有单一真实来源(Single Source of Truth)**:
|
||||
- 实体数据同时散落在 C++ `EntityStore`、`net_play.gd`、`net_world.gd`、`client.ents` 和 `node.meta` 中,互相通过数十个信号来回同步,极易丢状态或时序错乱。
|
||||
|
||||
---
|
||||
|
||||
## 2. 1:1 复刻的边界与原则(复刻哪些,不复刻哪些)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **核心原则**:**“形式上”不要 1:1 照搬类文件,但“内核上”必须 1:1 严格对齐封包、状态机与数学公式。**
|
||||
|
||||
### 2.1 坚决不要 1:1 复刻的部分(直接使用 Godot 原生)
|
||||
40250 源码中超过 60% 的代码是在重复造 2004 年 DirectX 8 / Windows 的轮子,这些在现代跨平台环境中是沉重的技术包袱:
|
||||
|
||||
| 40250 原版模块 | 为什么不要 1:1 复刻 | Godot 现成替代方案 |
|
||||
| :--- | :--- | :--- |
|
||||
| **`EterLib` 图形底层**<br>(`CGraphicDevice`, `CStateManager`, `CRenderTarget`) | 深度强绑定 DirectX 8/9,引入会导致无法在 macOS (Metal) 及移动端 (Vulkan) 编译。 | **Godot 渲染引擎**(原生支持 Metal / Vulkan / WebGL) |
|
||||
| **`EterPythonLib` 2D UI 系统**<br>(`CWindowManager`, `CWindow`, `CButton`, `CScrollBar`) | 20 年前手工编写的固定坐标 2D 贴图拼接系统,不支持高分屏自适应、矢量字体、触屏手势。 | **Godot Control 体系**(完善的锚点自适应、触屏支持) |
|
||||
| **`milesLib` 音频系统** | 2004 年的 Miles 闭源声卡驱动库。 | **Godot AudioServer / AudioStreamPlayer** |
|
||||
| **`CCamera` 摄像机类** | 自行维护的 D3D 视锥矩阵,死板且不灵活。 | **Godot `Camera3D`**(现有的 `game_camera.gd` 表现良好) |
|
||||
| **`EterGrnLib` 模型封装** | 依赖 Granny 2.8 闭源 Windows DLL。 | **现有 `metin2_model` + `metin2_anim`**(基于开源 libgr2,功能已完备) |
|
||||
| **内嵌 Python 2.7 解释器** | Python 2.7 早已停止维护,在 iOS / Android 上打包极其臃肿且受 App Store 审核限制。 | **C++ 纯逻辑导出给 GDScript 调用** |
|
||||
|
||||
### 2.2 必须 1:1 严格复刻的部分(下沉至 C++ GDExtension 核心逻辑层)
|
||||
下面这些是 Metin2 的“灵魂与运行规则”,必须 100% 对齐 40250 源码,漏掉一个判断分支就会导致手感完全走样:
|
||||
|
||||
1. **网络协议与封包结构(1:1 严丝合缝)**:
|
||||
- 对应源码:`Packet.h`
|
||||
- 要求:每个 `TPacketGC*`(如 `TPacketGCShop`、`TPacketGCCharacterAdd`、`TPacketGCDamageInfo`)的结构体字段、字节对齐、Subheader 常量 100% 照搬。
|
||||
2. **角色移动与物理状态机(1:1 算法复刻)**:
|
||||
- 对应源码:`PythonPlayerInput.cpp`、`InstanceBaseMovement.cpp`、`ActorInstanceMovement.cpp`
|
||||
- 要求:按键优先级判定(`NEW_GetMultiKeyDirRotation`)、转向角 45° 阻尼与防过冲单向贴齐(`GetRotatingDirection`、`SetAdvancingRotation`)、碰撞切线滑移(`AdjustDynamicCollisionMovement`)、位移累加(`__AccumulationMovement`)。
|
||||
3. **战斗与打击判定状态机(1:1 状态机复刻)**:
|
||||
- 对应源码:`ActorInstanceBattle.cpp`、`InstanceBaseBattle.cpp`
|
||||
- 要求:连击段位状态转移(Combo 1→2→3→4)与按键输入时间窗(`ComboInputData`)、`.msa` 多命中时间窗(`HitDataContainer`)的毫秒级触发、普通受击方位判定与击倒击飞硬直。
|
||||
4. **业务数据模型与操作上下文(1:1 逻辑复刻)**:
|
||||
- 对应源码:`PythonShop.cpp`(商店货架数据)、`PythonQuest.cpp`(任务状态树)、`PythonPlayerSkill.cpp`(技能冷却与释放条件三门)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 架构重构结论与策略
|
||||
|
||||
### 3.1 架构重构的必要性
|
||||
现有项目积累了大量技术债务,表面上看是“连击不连贯”、“WASD翻跟头”、“NPC打不开商店”等散发 Bug,深层原因是架构失衡:
|
||||
1. **“上帝文件(God Class)”过载**:`net_play.gd` (2200+行) 与 `game_scene.gd` (1800+行) 把网络封包解码、角色状态、背包/商店业务、3D渲染节点管理、点击拾取、UI开关全部混在一起,牵一发而动全身。
|
||||
2. **缺乏单一真实来源(No Single Source of Truth)**:角色坐标、朝向、连击段位在 `player_controller`、`net_play`、`net_world` 和 C++ `EntityStore` 中各自维护,靠数十个信号来回同步,极易时序混乱或丢状态。
|
||||
3. **启发式“伪逻辑”**:没有遵循 40250 的严密数学公式与状态机,而是用散落的计时器和直觉代码修补。
|
||||
|
||||
### 3.2 重构策略:拒绝推倒重来,采用“绞杀者式渐进解耦(Strangler Fig Pattern)”
|
||||
- **保留成果(绝不推倒)**:已稳定的 C++ GR2 渲染层(`metin2_model`、`metin2_anim`、`metin2_world`)和摄像机防穿模逻辑完全保留。
|
||||
- **业务逐块下沉**:按“战斗 -> 商店/NPC -> 任务 -> 背包”的顺序,把上帝文件中的业务逐个抽成独立的 Manager/状态机,先解耦跑通,再下沉对齐 40250。
|
||||
|
||||
---
|
||||
|
||||
## 4. 推荐的目标系统架构(`extension/src/m2_core/`)
|
||||
|
||||
在 C++ GDExtension 中建立清晰、模块化、无 DirectX 依赖的逻辑层:
|
||||
|
||||
```
|
||||
extension/src/m2_core/
|
||||
├── protocol/ # 1:1 严格对齐 40250 的网络层
|
||||
│ ├── packets.h # 1:1 复制 40250 Packet.h 的所有包结构体
|
||||
│ └── packet_dispatcher.h # 1:1 复制 PythonNetworkStreamPhaseGame 的封包解析分发
|
||||
│
|
||||
├── player/ # 1:1 复制 CPythonPlayer 的核心逻辑
|
||||
│ ├── player_input.cpp # 1:1 移植 PythonPlayerInput.cpp (按键优先级、朝向合成、预约动作)
|
||||
│ ├── player_state.cpp # 1:1 移植 玩家属性、金币、背包、技能CD数据
|
||||
│ └── player_combat.cpp # 1:1 移植 连击判定、攻击输入窗
|
||||
│
|
||||
├── world/ # 1:1 复制 CPythonCharacterManager & CInstanceBase
|
||||
│ ├── instance_entity.cpp # 1:1 移植 实体状态机、45°阻尼移动、切线碰撞滑移
|
||||
│ └── combat_resolver.cpp # 1:1 移植 命中范围、受击硬直计算
|
||||
│
|
||||
└── gameplay/ # 1:1 复制业务逻辑数据源
|
||||
├── shop_system.cpp # 1:1 移植 PythonShop.cpp (解决商店商品空白)
|
||||
└── quest_system.cpp # 1:1 移植 PythonQuest.cpp (解决NPC任务交互)
|
||||
```
|
||||
|
||||
### 运行时数据与渲染流向:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph View ["Godot 4 表现与输入层 (View & Input)"]
|
||||
InputHandler["Input / Touch / UI 事件捕获"]
|
||||
GodotNodes["PlayerView / MobView (Metin2Model + Metin2AnimPlayer)"]
|
||||
UI_Screens["ShopUI / QuestUI / InventoryUI / CharacterUI"]
|
||||
end
|
||||
|
||||
subgraph LogicCore ["GDExtension 40250 纯逻辑核心 (Core Logic)"]
|
||||
CPP_Input["PlayerInput (按键优先级/朝向合成/输入队列)"]
|
||||
CPP_Move["InstanceEntity (45°阻尼/切线滑动/累积位移)"]
|
||||
CPP_Combat["Combat & Combo (命中窗/连击状态机)"]
|
||||
CPP_Net["NetworkStream & Dispatcher (收发严丝合缝对齐 40250)"]
|
||||
CPP_Data["Shop & Quest Data (货架缓存/任务对话树)"]
|
||||
end
|
||||
|
||||
subgraph Server ["40250 游戏服务端"]
|
||||
GameServer["TCP Server"]
|
||||
end
|
||||
|
||||
InputHandler -->|按键/点击事件| CPP_Input
|
||||
CPP_Input --> CPP_Move
|
||||
CPP_Move -->|状态包| CPP_Net
|
||||
CPP_Combat <--> CPP_Net
|
||||
CPP_Net <-->|TCP 封包| GameServer
|
||||
|
||||
CPP_Move -.->|输出权威坐标 (x,y,z) 与朝向 yaw| GodotNodes
|
||||
CPP_Combat -.->|输出当前动作名、时间与打击帧| GodotNodes
|
||||
CPP_Data -.->|响应数据变动通知| UI_Screens
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 移动端(iOS / Android)操作界面与跨端支持设计
|
||||
|
||||
> [!NOTE]
|
||||
> **结论**:**当前规划的解耦架构天然、完美支持后续无缝接入移动端专属操作界面与触控系统。**
|
||||
|
||||
### 5.1 核心解耦机制:输入意图抽象层(Input Intent Abstraction)
|
||||
重构后的架构将**“硬件输入采集”**与**“游戏角色行为”**彻底解耦。底层逻辑不再监听 `KEY_W`、`KEY_SPACE` 或鼠标点击,而是消费高阶**输入意图(Input Intents)**:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph InputSources ["输入源采集(多端隔离)"]
|
||||
PC_Input["PC 桌面端输入<br>(WASD / 鼠标左右键 / 快捷键 1~4 / 空格)"]
|
||||
Mobile_Input["移动端触控输入<br>(虚拟摇杆 / 技能轮盘 / 普攻大按钮 / 目标切换)"]
|
||||
end
|
||||
|
||||
subgraph IntentLayer ["输入意图抽象接口 (PlayerInputIntent)"]
|
||||
I_Move["move_intent(dir: Vector2, is_run: bool)"]
|
||||
I_Attack["attack_intent()"]
|
||||
I_Skill["skill_intent(slot_idx: int)"]
|
||||
I_Target["target_intent(vid: int)"]
|
||||
I_Interact["interact_intent(vid: int)"]
|
||||
end
|
||||
|
||||
subgraph CoreLogic ["40250 核心状态机 (跨平台纯逻辑)"]
|
||||
M2_Move["45° 转向阻尼 / 切线滑移碰撞"]
|
||||
M2_Combat["连击输入时间窗 / 多段命中判定"]
|
||||
M2_Target["500cm 距离门限 / 预约走向交互"]
|
||||
end
|
||||
|
||||
PC_Input -->|转换映射| IntentLayer
|
||||
Mobile_Input -->|转换映射| IntentLayer
|
||||
IntentLayer --> CoreLogic
|
||||
```
|
||||
|
||||
1. **移动控制无缝适配**:
|
||||
- PC 端:WASD 键位经过 40250 `NEW_GetMultiKeyDirRotation` 算成方向向量。
|
||||
- 移动端:屏幕左侧的**虚拟摇杆(Virtual Joystick)**直接输出方向向量给 `move_intent`,底层 45° 转向阻尼与碰撞滑移算法无需修改一行代码。
|
||||
2. **战斗与技能轮盘**:
|
||||
- 移动端右侧可布局经典“大按键普攻 + 环形技能轮盘”,普攻按钮每次点击触发 `attack_intent()`,底层直接进入 40250 的 `ComboInputData` 输入缓冲队列。
|
||||
|
||||
### 5.2 双端自适应 UI 表现层(Dual-Mode Adaptive UI)
|
||||
由于业务数据(`ShopManager`、`InventoryManager`、`QuestManager`)已与界面彻底解耦:
|
||||
- **数据核心统一**:所有数据模型只负责维护商品列表、道具属性、背包格子、对话选项,通过信号/事件对外广播。
|
||||
- **UI 视图按端加载**:
|
||||
- PC 端:加载 `DesktopHUD.tscn`(支持鼠标 Hover 查看装备 Tooltip、右键穿戴、拖拽格子)。
|
||||
- 移动端:根据 `OS.has_feature("mobile")` 或屏幕分辨率动态加载 `MobileHUD.tscn`(更大的触控热区、点击弹出操作抽屉 Drawer、全屏大卡片商店展示)。
|
||||
- **完全零冗余**:两套 UI 共享 100% 的底层数据逻辑、网络协议与状态机。
|
||||
|
||||
---
|
||||
|
||||
## 6. 分阶段实施路线图
|
||||
|
||||
按照“**先主要解决战斗系统不一致,再解决 NPC/商店/任务交互混乱**”的优先级分步推进:
|
||||
|
||||
### 阶段一:战斗与连击系统精准对齐(优先级:最高)
|
||||
> 对应 40250 源码:`ActorInstanceBattle.cpp`、`InstanceBaseBattle.cpp`、`formats/msa.h`
|
||||
|
||||
1. **普通攻击与连击链(Combo Chain)1:1 复刻**:
|
||||
- 移植 40250 `ActorInstanceBattle.cpp:157-308` 状态机:
|
||||
- `m_dwcurComboIndex`(连击段数 1→2→3→4)
|
||||
- `ComboInputData`:严格按照 `.msa` 的 `InputStartTime`、`InputEndTime`、`NextComboTime` 判定是否允许按下一次普攻。
|
||||
### 阶段一:移动与战斗核心状态机 40250 对齐(优先级:最高)✅ [COMPLETED]
|
||||
> 对应 40250 源码:`PythonPlayerInput.cpp`、`ActorInstanceBattle.cpp`、`InstanceBaseBattle.cpp`
|
||||
|
||||
1. **废弃原有人为的外层普攻 CD 拦截**:
|
||||
- 彻底废除 `_attack_cd` 吞键逻辑,完全对齐 40250 普攻/连击内禀门控机制。
|
||||
2. **严格 1:1 接入 `.msa` 的连击输入数据窗**:
|
||||
- 读取并严格应用 `duration`、`InputStartTime`、`NextComboTime`、`InputEndTime`。
|
||||
- 完美支持长按空格连续 4 击(1→2→3→4 段无缝切换与回 Wait 重置)与快速连点。
|
||||
3. **受击与击倒对齐**:
|
||||
- 动作播放时长内动态释放受击锁,触发屏幕震颤与击退同步。
|
||||
|
||||
### 阶段二:NPC 交互与商店系统修复(优先级:高)✅ [COMPLETED]
|
||||
> 对应 40250 源码:`PythonPlayerInputMouse.cpp`、`PythonNetworkStreamPhaseGame.cpp`、`PythonShop.cpp`、`uishop.py`
|
||||
|
||||
1. **NPC 点击与预约走向交互(__OnClickActor)**:
|
||||
- 点击 NPC:距离 `<= 500 cm`(`CLICK_DIST_NPC`)立即停步、面朝 NPC 发送 `HEADER_CG_ON_CLICK` (26)。
|
||||
- 距离 `> 500 cm`:进入 `m_eReservedMode = CLICK_ACTOR` 预约模式,驱动角色寻路靠近,进入 500cm 瞬间停步交互。
|
||||
- 加入 40250 `CPythonPlayer::__SendClickActorPacket` 的 1000ms 狂点节流保护。
|
||||
2. **修复商店数据流与属性丢失(彻底解决白板商品 Bug)**:
|
||||
- C++ `mtnet::ShopEntry` 补齐 `sockets[3]` 与 `attrs[7]`,解析 `START` / `START_EX` / `UPDATE_ITEM` 全量属性。
|
||||
- `m2_client` 修复切页被冲掉问题,仅在初次打开触发 `shop_opened`,格位更新触发 `shop_updated(pos)`。
|
||||
- UI 接入 40-slot 货架、多 Tab 切换(`tabIdx * 40 + slot`)、右键直接秒买(`UnselectItemSlot`)、超出 10m 离开自动关闭窗口。
|
||||
|
||||
### 阶段三:任务系统与对话流对齐(优先级:中)✅ [COMPLETED]
|
||||
> 对应 40250 源码:`PythonEventManager.h/cpp`、`PythonQuest.cpp`、`PythonNetworkStreamPhaseGame.cpp`(`RecvScriptPacket`)、`uiquest.py`
|
||||
|
||||
1. **40250 任务与 NPC 交互协议全套对齐**:
|
||||
- 封包层精准对齐:`HEADER_GC_SCRIPT` (45)、`HEADER_CG_SCRIPT_ANSWER` (29)、`HEADER_CG_SCRIPT_BUTTON` (66)、`HEADER_CG_QUEST_INPUT_STRING` (30)、`HEADER_GC_QUEST_CONFIRM` (46)、`HEADER_CG_QUEST_CONFIRM` (31)。
|
||||
- 在 C++ 网络会话层 (`ClassicSession`) 与 `M2Client` 实现全套安全发包接口:`send_script_answer`、`send_script_button`、`send_quest_input`、`send_quest_confirm`、`send_quest_cancel`。
|
||||
2. **40250 `CPythonEventManager` 脚本语法与表现层 1:1 解析**:
|
||||
- 支持标准 40250 分号/管道符问题格式 `[QUESTION 1;选项一|2;选项二|...]`,0-indexed 答案映射,保留向后兼容旧格式。
|
||||
- 实现 40250 `uiquest.py` 8 选项多页分页机制(`MAX_CHOICES_PER_PAGE = 8`),自动挂载 `[上一页]` / `[下一页]` 导航行,精确计算全局索引 `page * 8 + slot`。
|
||||
- 皮肤与立绘对齐:`SKIN_NOWINDOW` (1) 与 `SKIN_CINEMA` (5) 隐藏背景板;`LEFTIMAGE` 自动偏移文本与按钮内容区宽度。
|
||||
- 支持 `[INPUT]` 文本输入框即时提交、`[NEXT]` 剧情推进与 `[DONE]` 流程收尾(`script_answer(254)`),以及 ESC 键安全取消(`OnCancel`)。
|
||||
- 编写 `test_quest_dialog_parity.gd` 覆盖 9 大测试场景 45 项断言全部 100% 通过。
|
||||
|
||||
### 阶段四:角色属性面板、背包与技能系统对齐(优先级:中)✅ [COMPLETED]
|
||||
> 对应 40250 源码:`PythonPlayerSkill.cpp`、`PythonSkill.h`、`PythonItem.cpp`、`GameType.h`、`Packet.h`、`uicharacter.py`
|
||||
|
||||
1. **主动技能释放链与三层完整门控(40250 CheckSkillUsable 对齐)**:
|
||||
- 严格 1:1 实现死亡状态(`CANNOT_ACT`)、安全区攻击拦截(`IN_SAFE`,增益 Buff 技能放行)、武器类型匹配(`NOT_MATCHABLE_WEAPON`)、弓箭数量检验(`EMPTY_ARROW`)、SP/HP 消耗门检(`NOT_ENOUGH_SP`)、冷却时间阻断(`WAIT_COOLTIME`)以及施法距离/目标判定(超出距离进入 `RESERVED` 预约模式,进入范围后放行)。
|
||||
2. **装备与背包双端线协议对齐(40250 ItemPos 对齐)**:
|
||||
- `_to_wire` 严格映射 40250 `TItemPos`:背包槽位 `0..89` 映射 `WINDOW_INVENTORY (1)`;装备槽位 `90..100` 映射 `WINDOW_EQUIPMENT (2)` 且内部 cell 严格从 `0` 开始偏移;时装槽位 `109..113` 映射 `WINDOW_INVENTORY (1)`。
|
||||
- 右键穿脱发送 `HEADER_CG_ITEM_USE` (11),拖拽移动发送 `HEADER_CG_ITEM_MOVE` (12),并严格走 `EquipRules` 职业、等级(`LIMIT_LEVEL`)与性别门控。
|
||||
3. **角色属性面板实时换算与指令对齐(uicharacter.py:RefreshStatus 对齐)**:
|
||||
- 物攻格式严格呈现 `min-max`(或相等时单值显示)、物防 `DEF_GRADE + DEF_BONUS`、魔攻 `MAG_ATT + MAGIC_WEP` 纯正公式。
|
||||
- 加点与洗点协议通过安全网络通道发送原生 `/stat ht`、`/stat iq`、`/stat st`、`/stat dx` 与 `/stat-` 指令。
|
||||
4. **快捷栏(Quickbar)持久化与全量同步(40250 TQuickSlot 协议)**:
|
||||
- 快捷栏配置新增发送 `HEADER_CG_QUICKSLOT_ADD` (16)(支持技能、物品、动作表情)、交换发送 `HEADER_CG_QUICKSLOT_SWAP` (18)、清空发送 `HEADER_CG_QUICKSLOT_DEL` (17),并支持全量 36 格状态本地与服务端双向恢复。
|
||||
- 编写 `test_skill_inventory_parity.gd` 覆盖 4 大领域 42 项测试断言全部 100% 通过。
|
||||
|
||||
---
|
||||
|
||||
## 7. 技术实施规范
|
||||
|
||||
1. **严禁“自造算法”**:
|
||||
- 任何涉及移动、旋转、连击、受击、技能、网络包逻辑的代码,必须明确注明对应的 40250 源码文件和行号(例如 `// 对齐 40250 ActorInstanceBattle.cpp:215`),1:1 复刻其状态转移条件。
|
||||
2. **职责边界分明**:
|
||||
- **C++ (GDExtension)**:拥有所有数据、状态机、网络封包、物理数学计算。
|
||||
- **GDScript / Godot Node**:只做两件事:① 把 C++ 的位置和动作画在屏幕上;② 把屏幕上的按键和点击事件传给 C++。
|
||||
3. **严格单向数据流**:
|
||||
- 废除 GDScript 层各节点之间混乱的信号交叉监听,所有状态统一归拢到核心状态机。
|
||||
|
||||
---
|
||||
|
||||
## 8. 验证计划
|
||||
|
||||
1. **单元测试与 Headless 自动化测试**:
|
||||
- 为连击状态机编写类似 `test_wasd_steering_parity.gd` 的无头自动化回归测试。
|
||||
- 模拟封包注入测试商店数据解析与 NPC 交互流程。
|
||||
2. **实服联机验证**:
|
||||
- 配合真实 40250 服务端,联机点击杂货商/铁匠,验证商品列表 100% 正常展示并成功购买。
|
||||
- 联机打怪,验证普通攻击连击节奏、打击僵直与伤害数字飘字时机与原版客户端完全吻合。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user