diff --git a/3dtank.com/play/static/js/unpacked/hornet_bundle.js b/3dtank.com/play/static/js/unpacked/hornet_bundle.js index 26df6b8..21a50a3 100644 --- a/3dtank.com/play/static/js/unpacked/hornet_bundle.js +++ b/3dtank.com/play/static/js/unpacked/hornet_bundle.js @@ -1983,8 +1983,13 @@ } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== - function CollectMeshNodes(bpNode, nodes, depth, group, stats) { - if (!bpNode || depth > 5) return; + // Seen:跨「车身根 / 部件表 / 炮塔 / 扫描」多路径去重,避免同一节点被画多次 + function CollectMeshNodes(bpNode, nodes, depth, group, stats, Seen) { + if (!bpNode || depth > 12) return; + if (Seen) { + if (Seen.has(bpNode)) return; + Seen.add(bpNode); + } if (stats) stats.seen++; var name = bpNode.g35_1 || ''; if (bpNode.v3m_1) { @@ -2023,7 +2028,7 @@ ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { - if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats); + if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats, Seen); }); } } @@ -2037,30 +2042,38 @@ V.r35_1 && typeof V.r35_1 === 'object'); } + // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 + function IsPlainObj(V) { + if (!V || typeof V !== 'object') return false; + if (Array.isArray(V) || ArrayBuffer.isView(V)) return false; + if (typeof Map !== 'undefined' && V instanceof Map) return false; + if (typeof Set !== 'undefined' && V instanceof Set) return false; + return true; + } + function ScanMeshNodes(tank, nodes, stats, Seen) { Seen = Seen || new WeakSet(); function Try(V) { if (!IsSceneNode(V) || Seen.has(V)) return; - Seen.add(V); - CollectMeshNodes(V, nodes, 0, 'scan', stats); + CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); } try { tank.qz1_1.s2a_1.forEach(function (VL) { var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; - if (!comp) return; + if (!IsPlainObj(comp)) return; for (var K in comp) { var V = comp[K]; - if (!V || typeof V !== 'object') continue; + if (!IsPlainObj(V)) continue; if (IsSceneNode(V)) { Try(V); continue; } // 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1) try { for (var K2 in V) { var V2 = V[K2]; - if (!V2 || typeof V2 !== 'object') continue; + if (!IsPlainObj(V2)) continue; if (IsSceneNode(V2)) { Try(V2); continue; } for (var K3 in V2) { var V3 = V2[K3]; - if (V3 && typeof V3 === 'object') Try(V3); + if (IsPlainObj(V3)) Try(V3); } } } catch (e) {} @@ -2072,21 +2085,30 @@ // ===== 提取坦克的mesh数据 ===== function GetTankMeshData(tank, stats) { var nodes = []; - var skin = GetHullSkin(tank, stats); + var Seen = new WeakSet(); + var skinComp = GetSkinComponent(tank); + var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null; + if (stats) { + stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); + stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; + } + // ① 车身根节点:皮肤组件 h159_1 是「所有部件节点」的总根(比逐个部件更完整) + if (skinComp && skinComp.h159_1) { + try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} + } + // ② 部件表:某些构建下根节点不完整,仍逐个部件补一遍(Seen 自动去重) if (skin) { try { - if (stats) stats.hullParts = (typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; ForEachInList(skin.g17r_1, function (part) { if (!part) return; - // 新版:part.j17r_1 是单个 BP 节点;旧版是节点列表 - var j = part.j17r_1; + var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { - if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null); + if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null, Seen); }); } else { - CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null); + CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } }); } catch(e) {} @@ -2095,15 +2117,11 @@ if (stats) stats.turretComp = !!turretNode; if (turretNode) { if (stats) stats.turretRoot = true; - try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {} - } - // 结构路径没拿到任何 mesh(皮肤/炮塔节点缺失或结构不符)时,直接扫实体所有组件字段找 3D 节点 - if (!skin || !turretNode || nodes.length === 0) { - var Seen = new WeakSet(); - for (var ni = 0; ni < nodes.length; ni++) { if (nodes[ni] && nodes[ni].node) Seen.add(nodes[ni].node); } - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - if (stats) stats.hullSource = (stats.hullSource || '扫描'); + try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {} } + // ③ 无条件结构扫描兜底:把组件字段里能看到的 3D 节点全部补进来 + // (解决「有的坦克只有点」「有的只有头没有身子」——结构字段不完整时漏采) + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} return nodes; } diff --git a/3dtank_bundle.js b/3dtank_bundle.js index 26df6b8..21a50a3 100644 --- a/3dtank_bundle.js +++ b/3dtank_bundle.js @@ -1983,8 +1983,13 @@ } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== - function CollectMeshNodes(bpNode, nodes, depth, group, stats) { - if (!bpNode || depth > 5) return; + // Seen:跨「车身根 / 部件表 / 炮塔 / 扫描」多路径去重,避免同一节点被画多次 + function CollectMeshNodes(bpNode, nodes, depth, group, stats, Seen) { + if (!bpNode || depth > 12) return; + if (Seen) { + if (Seen.has(bpNode)) return; + Seen.add(bpNode); + } if (stats) stats.seen++; var name = bpNode.g35_1 || ''; if (bpNode.v3m_1) { @@ -2023,7 +2028,7 @@ ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { - if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats); + if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats, Seen); }); } } @@ -2037,30 +2042,38 @@ V.r35_1 && typeof V.r35_1 === 'object'); } + // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 + function IsPlainObj(V) { + if (!V || typeof V !== 'object') return false; + if (Array.isArray(V) || ArrayBuffer.isView(V)) return false; + if (typeof Map !== 'undefined' && V instanceof Map) return false; + if (typeof Set !== 'undefined' && V instanceof Set) return false; + return true; + } + function ScanMeshNodes(tank, nodes, stats, Seen) { Seen = Seen || new WeakSet(); function Try(V) { if (!IsSceneNode(V) || Seen.has(V)) return; - Seen.add(V); - CollectMeshNodes(V, nodes, 0, 'scan', stats); + CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); } try { tank.qz1_1.s2a_1.forEach(function (VL) { var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; - if (!comp) return; + if (!IsPlainObj(comp)) return; for (var K in comp) { var V = comp[K]; - if (!V || typeof V !== 'object') continue; + if (!IsPlainObj(V)) continue; if (IsSceneNode(V)) { Try(V); continue; } // 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1) try { for (var K2 in V) { var V2 = V[K2]; - if (!V2 || typeof V2 !== 'object') continue; + if (!IsPlainObj(V2)) continue; if (IsSceneNode(V2)) { Try(V2); continue; } for (var K3 in V2) { var V3 = V2[K3]; - if (V3 && typeof V3 === 'object') Try(V3); + if (IsPlainObj(V3)) Try(V3); } } } catch (e) {} @@ -2072,21 +2085,30 @@ // ===== 提取坦克的mesh数据 ===== function GetTankMeshData(tank, stats) { var nodes = []; - var skin = GetHullSkin(tank, stats); + var Seen = new WeakSet(); + var skinComp = GetSkinComponent(tank); + var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null; + if (stats) { + stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); + stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; + } + // ① 车身根节点:皮肤组件 h159_1 是「所有部件节点」的总根(比逐个部件更完整) + if (skinComp && skinComp.h159_1) { + try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} + } + // ② 部件表:某些构建下根节点不完整,仍逐个部件补一遍(Seen 自动去重) if (skin) { try { - if (stats) stats.hullParts = (typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; ForEachInList(skin.g17r_1, function (part) { if (!part) return; - // 新版:part.j17r_1 是单个 BP 节点;旧版是节点列表 - var j = part.j17r_1; + var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { - if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null); + if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null, Seen); }); } else { - CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null); + CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } }); } catch(e) {} @@ -2095,15 +2117,11 @@ if (stats) stats.turretComp = !!turretNode; if (turretNode) { if (stats) stats.turretRoot = true; - try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {} - } - // 结构路径没拿到任何 mesh(皮肤/炮塔节点缺失或结构不符)时,直接扫实体所有组件字段找 3D 节点 - if (!skin || !turretNode || nodes.length === 0) { - var Seen = new WeakSet(); - for (var ni = 0; ni < nodes.length; ni++) { if (nodes[ni] && nodes[ni].node) Seen.add(nodes[ni].node); } - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - if (stats) stats.hullSource = (stats.hullSource || '扫描'); + try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {} } + // ③ 无条件结构扫描兜底:把组件字段里能看到的 3D 节点全部补进来 + // (解决「有的坦克只有点」「有的只有头没有身子」——结构字段不完整时漏采) + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} return nodes; } diff --git a/tools/hornet_3d_wireframe.js b/tools/hornet_3d_wireframe.js index 944a986..f4b54ee 100644 --- a/tools/hornet_3d_wireframe.js +++ b/tools/hornet_3d_wireframe.js @@ -458,8 +458,13 @@ } // ===== 递归收集mesh节点(每帧重新获取 Bp 节点引用) ===== - function CollectMeshNodes(bpNode, nodes, depth, group, stats) { - if (!bpNode || depth > 5) return; + // Seen:跨「车身根 / 部件表 / 炮塔 / 扫描」多路径去重,避免同一节点被画多次 + function CollectMeshNodes(bpNode, nodes, depth, group, stats, Seen) { + if (!bpNode || depth > 12) return; + if (Seen) { + if (Seen.has(bpNode)) return; + Seen.add(bpNode); + } if (stats) stats.seen++; var name = bpNode.g35_1 || ''; if (bpNode.v3m_1) { @@ -498,7 +503,7 @@ ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { - if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats); + if (child) CollectMeshNodes(child, nodes, depth + 1, group, stats, Seen); }); } } @@ -512,30 +517,38 @@ V.r35_1 && typeof V.r35_1 === 'object'); } + // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 + function IsPlainObj(V) { + if (!V || typeof V !== 'object') return false; + if (Array.isArray(V) || ArrayBuffer.isView(V)) return false; + if (typeof Map !== 'undefined' && V instanceof Map) return false; + if (typeof Set !== 'undefined' && V instanceof Set) return false; + return true; + } + function ScanMeshNodes(tank, nodes, stats, Seen) { Seen = Seen || new WeakSet(); function Try(V) { if (!IsSceneNode(V) || Seen.has(V)) return; - Seen.add(V); - CollectMeshNodes(V, nodes, 0, 'scan', stats); + CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); } try { tank.qz1_1.s2a_1.forEach(function (VL) { var comp = (VL && typeof VL.g1 === 'function') ? VL.g1(0) : VL; - if (!comp) return; + if (!IsPlainObj(comp)) return; for (var K in comp) { var V = comp[K]; - if (!V || typeof V !== 'object') continue; + if (!IsPlainObj(V)) continue; if (IsSceneNode(V)) { Try(V); continue; } // 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1) try { for (var K2 in V) { var V2 = V[K2]; - if (!V2 || typeof V2 !== 'object') continue; + if (!IsPlainObj(V2)) continue; if (IsSceneNode(V2)) { Try(V2); continue; } for (var K3 in V2) { var V3 = V2[K3]; - if (V3 && typeof V3 === 'object') Try(V3); + if (IsPlainObj(V3)) Try(V3); } } } catch (e) {} @@ -547,21 +560,30 @@ // ===== 提取坦克的mesh数据 ===== function GetTankMeshData(tank, stats) { var nodes = []; - var skin = GetHullSkin(tank, stats); + var Seen = new WeakSet(); + var skinComp = GetSkinComponent(tank); + var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null; + if (stats) { + stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); + stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; + } + // ① 车身根节点:皮肤组件 h159_1 是「所有部件节点」的总根(比逐个部件更完整) + if (skinComp && skinComp.h159_1) { + try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} + } + // ② 部件表:某些构建下根节点不完整,仍逐个部件补一遍(Seen 自动去重) if (skin) { try { - if (stats) stats.hullParts = (typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; ForEachInList(skin.g17r_1, function (part) { if (!part) return; - // 新版:part.j17r_1 是单个 BP 节点;旧版是节点列表 - var j = part.j17r_1; + var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { - if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null); + if (bp) CollectMeshNodes(bp, nodes, 0, 'hull', stats ? stats.hull : null, Seen); }); } else { - CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null); + CollectMeshNodes(j, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } }); } catch(e) {} @@ -570,15 +592,11 @@ if (stats) stats.turretComp = !!turretNode; if (turretNode) { if (stats) stats.turretRoot = true; - try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null); } catch(e) {} - } - // 结构路径没拿到任何 mesh(皮肤/炮塔节点缺失或结构不符)时,直接扫实体所有组件字段找 3D 节点 - if (!skin || !turretNode || nodes.length === 0) { - var Seen = new WeakSet(); - for (var ni = 0; ni < nodes.length; ni++) { if (nodes[ni] && nodes[ni].node) Seen.add(nodes[ni].node); } - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - if (stats) stats.hullSource = (stats.hullSource || '扫描'); + try { CollectMeshNodes(turretNode, nodes, 0, 'turret', stats ? stats.turret : null, Seen); } catch(e) {} } + // ③ 无条件结构扫描兜底:把组件字段里能看到的 3D 节点全部补进来 + // (解决「有的坦克只有点」「有的只有头没有身子」——结构字段不完整时漏采) + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} return nodes; }