diff --git a/3dtank.com/play/static/js/unpacked/hornet_bundle.js b/3dtank.com/play/static/js/unpacked/hornet_bundle.js index 19900e9..fe749cf 100644 --- a/3dtank.com/play/static/js/unpacked/hornet_bundle.js +++ b/3dtank.com/play/static/js/unpacked/hornet_bundle.js @@ -212,6 +212,258 @@ })(); +// ===== hornet_schema.js ===== +// hornet_schema.js — 引擎匿名字段抽象层(字段映射总表) +// +// 为什么需要它: +// 游戏每个构建版本都会把类的字段名整体平移(如 ejt 的 zzq_1 / bzr_1), +// 工具里到处硬编码 `comp.zzq_1`、`tank.qz1_1.s2a_1` 这类匿名名, +// 版本一换就要全局搜替换,还容易改错(同名复用极严重)。 +// 这里把「语义名 → 匿名字段路径」集中成一张表: +// · 工具只用语义名(Schema.Get(tank, 'Tank.StateValue')) +// · 换版本时只改这张表,并跑一次 SelfCheck() 看哪几项失配 +// +// 路径语法: +// 'a.b.c' 属性链 +// 'lz4()' 方法调用(末尾带括号即调用,中间也可混用,如 'm10b_1.j11y()') +// +// 用法: +// var S = window.__hornetSchema__; +// S.Get(gm, 'GameMode.TankList') +// S.Get(tank, 'Tank.StateValue') +// S.Check(gm, 'GameMode') // → { Ok: [...], Missing: [...] } +// S.SelfCheck() // 用当前战局对象自检并打印 +// +// 版本:main.7cfa4058(字段核对自 module_87072 / 61378 / 92980 / 55088) + +(function () { + 'use strict'; + + // ===== 字段映射总表 ===== + // 命名规则:<对象类型>.<语义名> + var Fields = { + // ---- 战斗实体基类(fe / BattleEntity)---- + 'Entity.Id': 'iz1_1', // 实体 ID(坦克/战场唯一) + 'Entity.Components': 'qz1_1.s2a_1', // Map<组件标识, 组件列表>,取值要 valueList.g1(0) + + // ---- GameMode(ght,window.__gameMode__)---- + 'GameMode.Scene': 'lz4()', // 场景实体 + 'GameMode.TankList': 'm10b_1.j11y()', // 坦克列表(e1() 数量 / g1(i) 取) + 'GameMode.OwnedTank': 'u10b_1', // 本地玩家拥有的坦克(onTankPossessed) + 'GameMode.ObservedTank': 't10b_1', // 当前观察目标(死亡/观战时可能指向敌人) + 'GameMode.LocalTeam': 'v10b_1', // 本地队伍(与 u10b_1 同生命周期) + + // ---- 坦克组件(lxt,含状态机)---- + 'Tank.Fsm': 'p10c_1', // 状态机字段(组件识别用) + 'Tank.State': 'k10c_1', // 状态枚举对象 + 'Tank.StateValue': 'k10c_1.k3_1', // 状态数值:0=DEAD 1=DEAD_PHANTOM 2=SEMI_ACTIVE 3=ACTIVE 4=DISABLED 5=TELEPORT + 'Tank.IsAlive': 's14j()', // 引擎可见性判定(2/3/5) + 'Tank.TeamSource': 'l10c_1', // 状态机里下发的队伍对象 + + // ---- 队伍组件(GIt)与队伍对象 ---- + 'Team.Field': 'b135_1', // 队伍组件上保存队伍对象的字段 + 'Team.Id': 'k3_1', // 队伍编号:0 / 1 / 2(2 = 中立) + 'Team.NeutralId': 2, + + // ---- 坦克物理组件(ejt,组件名 tankPhysics)---- + 'Physics.Position': 'bzr_1', // 世界位置向量(f20_1/g20_1/h20_1) + 'Physics.Size': 'xzq_1', // 车体半尺寸(**不是位置**) + 'Physics.Hull': 'zzq_1', // 车体主碰撞形状(x9j_1 === 'hull-main') + 'Physics.Body': 'pbk()', // 物理刚体(jM) + 'Physics.AddMask': 'n155()', // z9j_1 |= t + 'Physics.RemoveMask': 'm155()', // z9j_1 &= ~t + 'Physics.BodyShapes': 'pbk().y9i_1', // 刚体上的形状列表(e1()/g1(i)) + + // ---- 碰撞形状(yi)---- + 'Shape.Name': 'x9j_1', + 'Shape.Category': 'y9j_1', // bit0=坦克(1) bit3=地雷(8) bit4=地形(16) + 'Shape.Mask': 'z9j_1', // 愿意碰撞的分类位 + 'Shape.Ghost': 'g9k_1', // true = 只探测不产生碰撞响应 + + // ---- 血量组件(Vkt)---- + 'Health.Max': 'p12o_1', + 'Health.Current': 'q12o_1', + + // ---- 用户信息组件(yot)---- + 'User.Name': 'c12n_1', + 'User.Clan': 'e12n_1', + + // ---- 装备分组件(BattleGearScoreCC)---- + 'Gear.Score': 'cjf_1', + + // ---- 皮肤组件(eUt)---- + 'Skin.HullSkin': 'g159_1', // 皮肤对象 + 'Skin.HullRoot': 'h159_1', // 车身渲染树根(BP 节点) + 'Skin.Parts': 'g159_1.g17r_1', // 部件表 + 'Skin.PartNode': 'j17r_1', // 部件 → BP 节点 + + // ---- 炮塔皮肤组件(LUt)---- + 'Turret.Wrapper': 'a11p_1', // 组件首字段(包装对象) + 'Turret.Root': 'e11p_1.a17q_1', // 炮塔 BP 根节点 + 'Turret.GetRoot': 'u11p()', + + // ---- BP 场景节点(zl)---- + 'Node.Name': 'g35_1', + 'Node.Matrix': 'p35_1', // 世界矩阵(列主序,[12][13][14] = 平移) + 'Node.Children': 'r35_1', + 'Node.Mesh': 'v3m_1', + 'Node.Lod': 'u4x_1', + + // ---- 相机(场景实体 .jz1_1.iyx_1)---- + 'Camera.SceneRef': 'jz1_1', + 'Camera.Main': 'jz1_1.iyx_1', + 'Camera.Position': 'jz1_1.iyx_1.t35_1', + 'Camera.ViewMatrix': 'jz1_1.iyx_1.d37_1', + 'Camera.ClipMatrix': 'jz1_1.iyx_1.g37_1', + 'Camera.Fov': 'jz1_1.iyx_1.b37_1' + }; + + // ===== module_87072 的导出键($_.xx),每版都可能平移 ===== + // 队伍:vd = WIt(t) 取实体队伍;dl = R7(a,b) 同队且非中立 + // ⚠ 旧版是 kd / rk;新版 kd 已指向无关构造器 + var Exports = { + 'Export.TeamOf': ['vd', 'kd'], + 'Export.TeamSame': ['dl', 'rk'] + }; + + // ===== 路径解析 ===== + function IsCall(Token) { + var N = Token.length; + return N > 2 && Token.charAt(N - 1) === ')' && Token.charAt(N - 2) === '('; + } + + function ResolvePath(Target, Path) { + if (Target === null || Target === undefined || !Path) return undefined; + var Tokens = String(Path).split('.'); + var Cur = Target; + for (var i = 0; i < Tokens.length; i++) { + if (Cur === null || Cur === undefined) return undefined; + var Tok = Tokens[i]; + if (IsCall(Tok)) { + var Name = Tok.slice(0, -2); + if (typeof Cur[Name] !== 'function') return undefined; + try { Cur = Cur[Name](); } catch (e) { return undefined; } + } else { + Cur = Cur[Tok]; + } + } + return Cur; + } + + // 取字段值:Key 是总表键(如 'Tank.StateValue'),未命中则把 Key 当字面路径 + function Get(Target, Key) { + var Path = Fields[Key]; + return ResolvePath(Target, Path !== undefined ? Path : Key); + } + + // 取字段路径(字符串),用于打印/自检 + function PathOf(Key) { + return Fields[Key] !== undefined ? Fields[Key] : String(Key); + } + + // 取单段属性名:'Node.Matrix' → 'p35_1';带方法或多段时返回 null + // 工具里写成 `Node[FieldName('Node.Matrix') || 'p35_1']`,即「优先用总表,缺表时用字面名」 + function FieldName(Key) { + var P = Fields[Key]; + if (!P || P.indexOf('.') >= 0 || IsCall(P)) return null; + return P; + } + + // 取单段方法名:'Physics.RemoveMask' → 'm155' + function MethodName(Key) { + var P = Fields[Key]; + if (!P || !IsCall(P) || P.indexOf('.') >= 0) return null; + return P.slice(0, -2); + } + + // 自检:按前缀检查某对象上哪些语义字段能解析 + function Check(Target, Prefix) { + var Out = { Ok: [], Missing: [] }; + Object.keys(Fields).forEach(function (Key) { + if (Prefix && Key.indexOf(Prefix + '.') !== 0) return; + var V = Get(Target, Key); + if (V === undefined) Out.Missing.push(Key + ' → ' + Fields[Key]); + else Out.Ok.push(Key + ' → ' + Fields[Key]); + }); + return Out; + } + + // 用当前战局对象做一次全量自检(控制台调用 __hornetSchema__.SelfCheck()) + function SelfCheck() { + var Gm = window.__gameMode__; + var Report = { HasGameMode: !!Gm, Groups: {}, Missing: [] }; + if (!Gm) return Report; + + var Scene = Get(Gm, 'GameMode.Scene'); + var TankList = Get(Gm, 'GameMode.TankList'); + var Tank = null; + try { + Tank = Get(Gm, 'GameMode.OwnedTank'); + if (!Tank && TankList && typeof TankList.e1 === 'function' && TankList.e1() > 0) Tank = TankList.g1(0); + } catch (e) {} + + var Targets = { + Entity: Tank || Scene, + GameMode: Gm, + Tank: Tank, + Physics: Tank ? FindPhysics(Tank) : null, + Shape: null, + Skin: Tank, + Turret: Tank, + Node: null, + Camera: Scene + }; + + Object.keys(Targets).forEach(function (Prefix) { + if (!Targets[Prefix]) { Report.Groups[Prefix] = '无样本对象'; return; } + var R = Check(Targets[Prefix], Prefix); + Report.Groups[Prefix] = R; + R.Missing.forEach(function (M) { Report.Missing.push(Prefix + '.' + M); }); + }); + + if (Report.Missing.length) { + console.warn('[字段抽象] 失配 ' + Report.Missing.length + ' 项:', Report.Missing); + } else { + console.log('[字段抽象] 全部语义字段解析正常'); + } + return Report; + } + + // 找坦克物理组件(与工具内实现同源,放这里方便自检) + function FindPhysics(Tank) { + if (!Tank) return null; + var Map = Get(Tank, 'Entity.Components'); + if (!Map || typeof Map.forEach !== 'function') return null; + var Found = null; + Map.forEach(function (ValueList) { + if (Found) return; + var Comp = (ValueList && typeof ValueList.g1 === 'function') ? ValueList.g1(0) : ValueList; + if (!Comp) return; + var Hull = Get(Comp, 'Physics.Hull'); + if (Hull && Get(Hull, 'Shape.Name') === 'hull-main') Found = Comp; + }); + return Found; + } + + // ===== 导出 ===== + window.__hornetSchema__ = { + Version: 'main.7cfa4058', + Fields: Fields, + Exports: Exports, + Get: Get, + PathOf: PathOf, + FieldName: FieldName, + MethodName: MethodName, + Check: Check, + SelfCheck: SelfCheck, + ResolvePath: ResolvePath, + FindPhysics: FindPhysics + }; + + console.log('[3DTank] 字段抽象层已加载(版本 main.7cfa4058,语义字段 ' + Object.keys(Fields).length + ' 项)'); +})(); + + // ===== hornet_lib.js ===== // ===== 3DTank公共库 - 工具文件共享辅助函数 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) @@ -228,6 +480,18 @@ // ===== 缓存队伍判断函数(ZCt/m2Fn) ===== var ZCt = null, m2Fn = null; + // ===== 字段抽象层引用(字段名统一在 hornet_schema.js 里维护) ===== + // 取不到抽象层时退回本地常量,保证单独加载本文件也能工作 + var Schema = window.__hornetSchema__ || null; + function FieldOf(Key, Fallback) { + if (!Schema) return Fallback; + var P = Schema.PathOf(Key); + return (P && P.indexOf('.') < 0 && P.indexOf('(') < 0) ? P : Fallback; + } + var TEAM_FIELD = FieldOf('Team.Field', 'b135_1'); // 队伍组件上的队伍对象字段 + var TEAM_ID_FIELD = FieldOf('Team.Id', 'k3_1'); // 队伍编号:0/1/2(2=中立) + var NEUTRAL_TEAM_ID = 2; + // 加载队伍判断函数(从 module_87072 导出) // 新旧差异(main.7cfa4058): // 取队伍 $_$.vd = WIt(t) → g$(HIt(), t).b135_1(坦克无队伍时返回中立 q$()) @@ -471,80 +735,92 @@ // 只在「本地坦克实体变了」(换局/换车)时才重新缓存。 var LocalTeam = null; var LocalTeamTankId = null; + + // 读队伍对象:优先引擎导出函数 WIt($_$.vd),失败则直接读队伍组件字段 b135_1 + // WIt(t) 的实现就是 g$(HIt(), t).b135_1 —— 所以直接读组件字段与引擎同源, + // 导出键漂移时这条路仍然有效 + function ReadTeamRaw(tank) { + if (!tank) return null; + if (!ZCt) LoadTeamFunctions(); + if (ZCt) { + try { var T = ZCt(tank); if (T) return T; } catch (e) {} + } + var Comp = FindComponentByField(tank, TEAM_FIELD); + return Comp ? Comp[TEAM_FIELD] : null; + } + + // 读队伍编号:0 / 1 = 两个阵营,2 = 中立(q$());读不到返回 -1 + // ⚠ 用数值编号比较,而不是引擎 equals/R7:编号是稳定语义 + // (module_87072 里 P7()/$ct() 都按 k3_1 分派) + function GetTeamId(Team) { + if (!Team) return -1; + var Id = Team[TEAM_ID_FIELD]; + return (typeof Id === 'number') ? Id : -1; + } + + function GetLocalTeamId() { return GetTeamId(LocalTeam); } + function GetTankTeamId(tank) { return GetTeamId(ReadTeamRaw(tank)); } + function RememberLocalTeam(gm) { - if (!ZCt || !m2Fn) LoadTeamFunctions(); - if (!ZCt) return; var t = null; try { t = gm.u10b_1; } catch (e) { t = null; } if (t) { var Id = t.iz1_1; if (LocalTeam && LocalTeamTankId === Id) return; - try { - var Team = ZCt(t); - if (Team && (!m2Fn || m2Fn(Team, Team))) { LocalTeam = Team; LocalTeamTankId = Id; } - } catch (e) {} + var Team = ReadTeamRaw(t); + var TeamId = GetTeamId(Team); + if (TeamId >= 0 && TeamId !== NEUTRAL_TEAM_ID) { LocalTeam = Team; LocalTeamTankId = Id; } return; } // u10b_1 为空(死亡/观战):保持已有缓存;若还没缓存过,再试 GameMode 上的本地队伍字段 if (LocalTeam) return; try { var G = gm.v10b_1; - if (G && (!m2Fn || m2Fn(G, G))) LocalTeam = G; + var GId = GetTeamId(G); + if (GId >= 0 && GId !== NEUTRAL_TEAM_ID) LocalTeam = G; } catch (e) {} } // ===== 判断是否友方 ===== - // 队伍同步有延迟:坦克刚出现时队伍是「中立」,直接比较会先判成敌人、同步后再变队友(颜色跳变)。 - // 处理:① 每辆坦克缓存最近一次**非中立**队伍;② 无法判定时沿用上一次判定结果。 - var TankTeamCache = new WeakMap(); // tank -> 非中立队伍对象 + // 规则:队伍编号相同且非中立 → 友方;中立/未下发 → 沿用该坦克上次判定(默认敌人)。 + // ⚠ 不再「一次判定永久锁定」:锁定会在基准短暂错误时把整局颜色固化错, + // 这里改成每帧重算 + 中立抖动时沿用上次结果,既不会闪烁也不会锁死。 var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定 - var TeamLockLogged = new WeakSet(); - function ResolveTankTeam(tank) { - if (!ZCt) return null; - var Team = null; - try { Team = ZCt(tank); } catch (e) { Team = null; } - if (Team && (!m2Fn || m2Fn(Team, Team))) { // 同队且非中立 = 有效队伍 - TankTeamCache.set(tank, Team); - return Team; - } - return TankTeamCache.get(tank) || null; - } + var TeamJudgeLogged = new WeakSet(); function IsFriendly(tank, playerTank, gm) { if (!tank) return false; - // 队伍函数可能因为工具早于游戏模块加载而没取到,这里按需补加载 - if (!ZCt || !m2Fn) LoadTeamFunctions(); if (gm) RememberLocalTeam(gm); - // 一旦在「基准队伍可靠」时判过,就锁定本局结果,战中不再改变 - var Locked = TankFriendCache.get(tank); - if (Locked !== undefined) return Locked; - if (ZCt && m2Fn) { - try { - var tankTeam = ResolveTankTeam(tank); - // 可靠基准(用于锁定):本地拥有坦克的队伍 / GameMode 上的本地队伍字段 - var LockBase = LocalTeam; - if (!LockBase && gm) { try { LockBase = gm.v10b_1; } catch (e) {} } - // 观战目标只作临时参考,绝不用它锁定(它可能是敌人) - var BaseTeam = LockBase || (playerTank ? ResolveTankTeam(playerTank) : null); - if (tankTeam && BaseTeam) { - var Friendly = m2Fn(tankTeam, BaseTeam); - if (LockBase) { - TankFriendCache.set(tank, Friendly); - if (Friendly && !TeamLockLogged.has(tank)) { - TeamLockLogged.add(tank); - try { - console.log('[敌我] 锁定为友方 id=' + tank.iz1_1 + ' name=' + (GetTankName(tank) || '?')); - } catch (e) {} - } - } - return Friendly; - } - } catch(e) {} + + var TankId = GetTeamId(ReadTeamRaw(tank)); + if (TankId < 0 || TankId === NEUTRAL_TEAM_ID) { + var Last = TankFriendCache.get(tank); // 队伍还没同步:保持上一帧颜色 + return Last === undefined ? false : Last; } - // 基准还没就绪:先按敌人显示,但**不锁定**,等基准可用后再定 - return false; + + // 可靠基准:本地拥有坦克的队伍(缓存)→ GameMode 上的本地队伍字段 + // 观战目标(t10b_1)可能是敌人,绝不作为基准 + var BaseTeam = LocalTeam; + if (!BaseTeam && gm) { try { BaseTeam = gm.v10b_1; } catch (e) {} } + var BaseId = GetTeamId(BaseTeam); + if (BaseId < 0 || BaseId === NEUTRAL_TEAM_ID) { + var Last2 = TankFriendCache.get(tank); // 基准不可靠(极早期):沿用上次 + return Last2 === undefined ? false : Last2; + } + + var Friendly = (TankId === BaseId); + TankFriendCache.set(tank, Friendly); + if (!TeamJudgeLogged.has(tank)) { + TeamJudgeLogged.add(tank); + try { + console.log('[敌我] id=' + tank.iz1_1 + ' 队伍=' + TankId + ' 本地=' + BaseId + + ' → ' + (Friendly ? '友方' : '敌方') + ' name=' + (GetTankName(tank) || '?')); + } catch (e) {} + } + return Friendly; } + // ===== 获取相机信息(场景主相机 scene.iyx_1) ===== // 新旧差异:旧版是找挂在实体上的相机组件(kym_1/lym_1),新版这些字段已被别的类占用; // 新版直接暴露主相机对象:gm.lz4().jz1_1(场景).iyx_1(相机,类 Jf/lc) @@ -831,6 +1107,10 @@ IsBot: IsBot, GetPlayerTank: GetPlayerTank, IsFriendly: IsFriendly, + ReadTeamRaw: ReadTeamRaw, + GetTeamId: GetTeamId, + GetTankTeamId: GetTankTeamId, + GetLocalTeamId: GetLocalTeamId, GetCameraInfo: GetCameraInfo, GetUMtComponent: GetUMtComponent, MkVec: MkVec, @@ -1616,6 +1896,23 @@ var gm = window.__gameMode__ || null; + // ===== 字段抽象层(匿名引擎字段 → 语义名,总表在 hornet_schema.js)===== + // 换版本时只改 schema;Fld 返回当前版本的字面名,schema 缺失时退回 fallback + var Schema = window.__hornetSchema__ || null; + function Fld(Key, Fallback) { + var N = Schema ? Schema.FieldName(Key) : null; + return N || Fallback; + } + var FNodeName = Fld('Node.Name', 'g35_1'); // 节点名 + var FNodeMatrix = Fld('Node.Matrix', 'p35_1'); // 世界矩阵 + var FNodeChildren = Fld('Node.Children', 'r35_1'); // 子节点列表 + var FNodeMesh = Fld('Node.Mesh', 'v3m_1'); // mesh 资源(Tp) + var FNodeLod = Fld('Node.Lod', 'u4x_1'); // LOD 列表 + var FSkinHullSkin = Fld('Skin.HullSkin', 'g159_1'); // 皮肤对象 + var FSkinHullRoot = Fld('Skin.HullRoot', 'h159_1'); // 车身渲染树根 + var FSkinParts = Fld('Skin.Parts', 'g17r_1'); // 部件表 + var FSkinPartNode = Fld('Skin.PartNode', 'j17r_1'); // 部件 → BP 节点 + // ===== 当前模式 ===== var Mode = 'outline'; // 'wireframe' | 'outline' try { @@ -1826,7 +2123,7 @@ if (!entity) return null; var Cached = SkinCompCache.get(entity); if (Cached) return Cached; - var comp = FindComponentByField(entity, 'g159_1'); + var comp = FindComponentByField(entity, FSkinHullSkin); if (!comp) comp = FindComponentByField(entity, 'a17q_1'); // 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表) if (!comp) { @@ -1837,7 +2134,7 @@ if (!C) return; for (var K in C) { var V = C[K]; - if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; } + if (V && typeof V === 'object' && V[FSkinParts] && typeof V[FSkinParts].e1 === 'function') { comp = C; return; } } }); } catch (e) {} @@ -1850,7 +2147,7 @@ function GetHullSkin(tank, stats) { var comp = GetSkinComponent(tank); if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; } - if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; } + if (comp[FSkinHullSkin] && comp[FSkinHullSkin][FSkinParts]) { if (stats) stats.hullSource = 'g159_1'; return comp[FSkinHullSkin]; } if (stats) stats.hullSource = '有组件但无皮肤'; return null; } @@ -2061,25 +2358,26 @@ Seen.add(bpNode); } if (stats) stats.seen++; - var name = bpNode.g35_1 || ''; - if (bpNode.v3m_1) { + var name = bpNode[FNodeName] || ''; + if (bpNode[FNodeMesh]) { if (stats) stats.hasMesh++; - var gpuData = GetGpuData(bpNode.v3m_1); + var gpuData = GetGpuData(bpNode[FNodeMesh]); if (gpuData) { if (stats) stats.gpuOk++; nodes.push({ - name: name, node: bpNode, tp: bpNode.v3m_1, + name: name, node: bpNode, tp: bpNode[FNodeMesh], positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } else if (stats) stats.gpuFail++; } - if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') { + var LodList = bpNode[FNodeLod]; + if (LodList && typeof LodList.e1 === 'function') { try { - var lodCount = bpNode.u4x_1.e1(); + var lodCount = LodList.e1(); for (var lod = 0; lod < lodCount && lod < 1; lod++) { - var lodLevel = bpNode.u4x_1.g1(lod); + var lodLevel = LodList.g1(lod); if (!lodLevel) continue; - var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1; + var lodTp = lodLevel[FNodeMesh] || lodLevel.v3m_1; if (!lodTp) continue; var lodGpuData = GetGpuData(lodTp); if (lodGpuData) { @@ -2094,7 +2392,7 @@ } // 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点) // 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表 - var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_1 : + var Children = (bpNode[FNodeChildren] && typeof bpNode[FNodeChildren].e1 === 'function') ? bpNode[FNodeChildren] : ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { @@ -2107,9 +2405,9 @@ // 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表 function IsSceneNode(V) { return !!(V && typeof V === 'object' && - typeof V.g35_1 === 'string' && - V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 && - V.r35_1 && typeof V.r35_1 === 'object'); + typeof V[FNodeName] === 'string' && + V[FNodeMatrix] && typeof V[FNodeMatrix].length === 'number' && V[FNodeMatrix].length >= 16 && + V[FNodeChildren] && typeof V[FNodeChildren] === 'object'); } // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 @@ -2121,6 +2419,43 @@ return true; } + // ===== 节点归属校验:只认「跟着这辆坦克」的节点 ===== + // 兜底扫描是盲扫组件字段,可能翻到共享的场景根/周围物件(表现:非坦克也被画线框)。 + // 坦克自身所有部件都在车体附近(整车 < 10m),用世界矩阵平移做距离闸门即可把它们挡掉。 + // 只对 group === 'scan' 的节点生效:车身根/炮塔根是可信路径,绝不因距离丢模型。 + var NODE_SCAN_RADIUS = 2500; // 25m(单位与引擎世界坐标一致) + + function NodeTranslation(Node) { + if (!Node) return null; + var M = Node[FNodeMatrix]; + if (!M || typeof M.length !== 'number' || M.length < 16) { + M = Node.n35_1; // 旧版矩阵字段 + if (!M || typeof M.length !== 'number' || M.length < 16) return null; + } + return { x: M[12], y: M[13], z: M[14] }; + } + + // 剔除「非本车」的兜底扫描节点(车身根/炮塔根是可信路径,不参与该过滤) + function FilterOwnedNodes(nodes, Pos) { + if (!nodes || !Pos || !nodes.length) return nodes; + var Out = []; + for (var i = 0; i < nodes.length; i++) { + var N = nodes[i]; + if (N.group === 'scan' && !IsNodeNearPoint(N.node, Pos)) continue; + Out.push(N); + } + return Out; + } + + function IsNodeNearPoint(Node, Pos, Radius) { + var T = NodeTranslation(Node); + if (!T || !Pos) return true; // 读不到矩阵时不拦(交给后续渲染阶段处理) + var dx = T.x - Pos.x, dy = T.y - Pos.y, dz = T.z - Pos.z; + var R = Radius || NODE_SCAN_RADIUS; + return (dx * dx + dy * dy + dz * dz) <= (R * R); + } + + // 扫描结果按「组件字段路径」缓存(不缓存节点对象:引擎会重建节点树, // 但字段名与组件实例稳定,路径每次都能解析到当前节点) var NodeSrcCache = new WeakMap(); // entity -> [{ Comp, Path: [K1, K2, K3] }] @@ -2135,7 +2470,7 @@ return V; } - function ScanMeshNodes(tank, nodes, stats, Seen) { + function ScanMeshNodes(tank, nodes, stats, Seen, TankPos) { Seen = Seen || new WeakSet(); // ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件 ScanTick++; @@ -2146,6 +2481,7 @@ for (var c = 0; c < Cached.length; c++) { var V0 = ResolveSrc(Cached[c]); if (!IsSceneNode(V0)) { AllOk = false; break; } + if (!IsNodeNearPoint(V0, TankPos)) continue; // 缓存根跑远了(不是这辆车的)→ 不画 if (!Seen.has(V0)) CollectMeshNodes(V0, nodes, 0, 'scan', stats, Seen); } if (AllOk) return; @@ -2154,6 +2490,8 @@ var Srcs = []; function Try(V, Comp, Path) { if (!IsSceneNode(V) || Seen.has(V)) return; + // 归属闸门:盲扫可能翻到共享场景根/周围物件,按「是否在车体附近」剔除 + if (!IsNodeNearPoint(V, TankPos)) { if (stats) stats.farRejected = (stats.farRejected || 0) + 1; return; } CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); Srcs.push({ Comp: Comp, Path: Path }); } @@ -2208,25 +2546,25 @@ // ===== 提取坦克的mesh数据 ===== // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底) - function GetTankMeshData(tank, stats) { + function GetTankMeshData(tank, stats, TankPos) { var nodes = []; var Seen = new WeakSet(); var skinComp = GetSkinComponent(tank); - var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null; + var skin = (skinComp && skinComp[FSkinHullSkin] && skinComp[FSkinHullSkin][FSkinParts]) ? skinComp[FSkinHullSkin] : null; if (stats) { stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); - stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; + stats.hullParts = (skin && typeof skin[FSkinParts].e1 === 'function') ? skin[FSkinParts].e1() : -1; } // ① 主路径:车身根节点(包含全部部件节点) - if (skinComp && skinComp.h159_1) { - try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} + if (skinComp && skinComp[FSkinHullRoot]) { + try { CollectMeshNodes(skinComp[FSkinHullRoot], nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } // ② 根节点采不到才退回部件表 if (nodes.length === 0 && skin) { try { - ForEachInList(skin.g17r_1, function (part) { + ForEachInList(skin[FSkinParts], function (part) { if (!part) return; - var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表 + var j = part[FSkinPartNode]; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { @@ -2240,7 +2578,7 @@ } // ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分) // 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体" - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen, TankPos); } catch (e) {} // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; @@ -2843,9 +3181,11 @@ var MeshStats = { hullSource: '', hullParts: 0, turretComp: false, turretRoot: false, hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, - turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 } + turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, + scan: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0, farRejected: 0 } }; - var meshNodes = GetTankMeshData(tank, MeshStats); + var meshNodes = GetTankMeshData(tank, MeshStats, pos); + meshNodes = FilterOwnedNodes(meshNodes, pos); // 只保留跟着这辆车动的节点(挡掉非坦克物件) if (!meshNodes || meshNodes.length === 0) { LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见 @@ -5839,6 +6179,24 @@ // 坦克 category 位(bit0) var TANK_CATEGORY_BIT = 1; + // ===== 字段抽象层(语义名 → 匿名字段,总表在 hornet_schema.js)===== + var Schema = window.__hornetSchema__ || null; + function Fld(Key, Fallback) { + var N = Schema ? Schema.FieldName(Key) : null; + return N || Fallback; + } + function Mth(Key, Fallback) { + var N = Schema ? Schema.MethodName(Key) : null; + return N || Fallback; + } + var FPhysicsHull = Fld('Physics.Hull', 'zzq_1'); // 车体主碰撞形状 + var FShapeName = Fld('Shape.Name', 'x9j_1'); // 形状名('hull-main') + var FShapeMask = Fld('Shape.Mask', 'z9j_1'); // 碰撞掩码 + var FBodyShapes = 'y9i_1'; // 刚体上的形状列表(pbk().y9i_1) + var MAddMask = Mth('Physics.AddMask', 'n155'); + var MRemoveMask = Mth('Physics.RemoveMask', 'm155'); + var MGetBody = Mth('Physics.Body', 'pbk'); + var Stat = { running: false, shapes: 0, // 本帧处理的碰撞形状数 @@ -5853,10 +6211,10 @@ // 特征:zzq_1 是车体主碰撞形状(x9j_1 === "hull-main"),且带 n155 / m155 / pbk function IsTankPhysics(Comp) { if (!Comp) return false; - if (typeof Comp.n155 !== 'function' || typeof Comp.m155 !== 'function') return false; - if (typeof Comp.pbk !== 'function') return false; - var Hull = Comp.zzq_1; - return !!(Hull && Hull.x9j_1 === 'hull-main'); + if (typeof Comp[MAddMask] !== 'function' || typeof Comp[MRemoveMask] !== 'function') return false; + if (typeof Comp[MGetBody] !== 'function') return false; + var Hull = Comp[FPhysicsHull]; + return !!(Hull && Hull[FShapeName] === 'hull-main'); } // 在实体上找坦克物理组件(不走 FindComponentByField:zzq_1 字段名在其它类里也有复用) @@ -5888,7 +6246,7 @@ function ReleaseSnapshot() { MaskSnapshot.forEach(function (Mask, Shape) { try { - if (Shape && typeof Mask === 'number') Shape.z9j_1 = Mask; + if (Shape && typeof Mask === 'number') Shape[FShapeMask] = Mask; } catch (e) {} }); MaskSnapshot.clear(); @@ -5911,8 +6269,8 @@ } var Body = null; - try { Body = Phys.pbk(); } catch (e) { Body = null; } - var Shapes = Body && Body.y9i_1; + try { Body = Phys[MGetBody](); } catch (e) { Body = null; } + var Shapes = Body && Body[FBodyShapes]; if (!Shapes || typeof Shapes.e1 !== 'function' || typeof Shapes.g1 !== 'function') return; var Count = Shapes.e1() | 0; @@ -5920,10 +6278,10 @@ for (var i = 0; i < Count; i++) { var Shape = null; try { Shape = Shapes.g1(i); } catch (e) { Shape = null; } - if (!Shape || typeof Shape.z9j_1 !== 'number') continue; + if (!Shape || typeof Shape[FShapeMask] !== 'number') continue; // 首次见到该形状时记录原始 mask,保证 Stop 能精确还原 - if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape.z9j_1); - Shape.z9j_1 = Shape.z9j_1 & ~TANK_CATEGORY_BIT; + if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape[FShapeMask]); + Shape[FShapeMask] = Shape[FShapeMask] & ~TANK_CATEGORY_BIT; Touched++; } Stat.shapes = Touched; diff --git a/3dtank_bundle.js b/3dtank_bundle.js index 19900e9..fe749cf 100644 --- a/3dtank_bundle.js +++ b/3dtank_bundle.js @@ -212,6 +212,258 @@ })(); +// ===== hornet_schema.js ===== +// hornet_schema.js — 引擎匿名字段抽象层(字段映射总表) +// +// 为什么需要它: +// 游戏每个构建版本都会把类的字段名整体平移(如 ejt 的 zzq_1 / bzr_1), +// 工具里到处硬编码 `comp.zzq_1`、`tank.qz1_1.s2a_1` 这类匿名名, +// 版本一换就要全局搜替换,还容易改错(同名复用极严重)。 +// 这里把「语义名 → 匿名字段路径」集中成一张表: +// · 工具只用语义名(Schema.Get(tank, 'Tank.StateValue')) +// · 换版本时只改这张表,并跑一次 SelfCheck() 看哪几项失配 +// +// 路径语法: +// 'a.b.c' 属性链 +// 'lz4()' 方法调用(末尾带括号即调用,中间也可混用,如 'm10b_1.j11y()') +// +// 用法: +// var S = window.__hornetSchema__; +// S.Get(gm, 'GameMode.TankList') +// S.Get(tank, 'Tank.StateValue') +// S.Check(gm, 'GameMode') // → { Ok: [...], Missing: [...] } +// S.SelfCheck() // 用当前战局对象自检并打印 +// +// 版本:main.7cfa4058(字段核对自 module_87072 / 61378 / 92980 / 55088) + +(function () { + 'use strict'; + + // ===== 字段映射总表 ===== + // 命名规则:<对象类型>.<语义名> + var Fields = { + // ---- 战斗实体基类(fe / BattleEntity)---- + 'Entity.Id': 'iz1_1', // 实体 ID(坦克/战场唯一) + 'Entity.Components': 'qz1_1.s2a_1', // Map<组件标识, 组件列表>,取值要 valueList.g1(0) + + // ---- GameMode(ght,window.__gameMode__)---- + 'GameMode.Scene': 'lz4()', // 场景实体 + 'GameMode.TankList': 'm10b_1.j11y()', // 坦克列表(e1() 数量 / g1(i) 取) + 'GameMode.OwnedTank': 'u10b_1', // 本地玩家拥有的坦克(onTankPossessed) + 'GameMode.ObservedTank': 't10b_1', // 当前观察目标(死亡/观战时可能指向敌人) + 'GameMode.LocalTeam': 'v10b_1', // 本地队伍(与 u10b_1 同生命周期) + + // ---- 坦克组件(lxt,含状态机)---- + 'Tank.Fsm': 'p10c_1', // 状态机字段(组件识别用) + 'Tank.State': 'k10c_1', // 状态枚举对象 + 'Tank.StateValue': 'k10c_1.k3_1', // 状态数值:0=DEAD 1=DEAD_PHANTOM 2=SEMI_ACTIVE 3=ACTIVE 4=DISABLED 5=TELEPORT + 'Tank.IsAlive': 's14j()', // 引擎可见性判定(2/3/5) + 'Tank.TeamSource': 'l10c_1', // 状态机里下发的队伍对象 + + // ---- 队伍组件(GIt)与队伍对象 ---- + 'Team.Field': 'b135_1', // 队伍组件上保存队伍对象的字段 + 'Team.Id': 'k3_1', // 队伍编号:0 / 1 / 2(2 = 中立) + 'Team.NeutralId': 2, + + // ---- 坦克物理组件(ejt,组件名 tankPhysics)---- + 'Physics.Position': 'bzr_1', // 世界位置向量(f20_1/g20_1/h20_1) + 'Physics.Size': 'xzq_1', // 车体半尺寸(**不是位置**) + 'Physics.Hull': 'zzq_1', // 车体主碰撞形状(x9j_1 === 'hull-main') + 'Physics.Body': 'pbk()', // 物理刚体(jM) + 'Physics.AddMask': 'n155()', // z9j_1 |= t + 'Physics.RemoveMask': 'm155()', // z9j_1 &= ~t + 'Physics.BodyShapes': 'pbk().y9i_1', // 刚体上的形状列表(e1()/g1(i)) + + // ---- 碰撞形状(yi)---- + 'Shape.Name': 'x9j_1', + 'Shape.Category': 'y9j_1', // bit0=坦克(1) bit3=地雷(8) bit4=地形(16) + 'Shape.Mask': 'z9j_1', // 愿意碰撞的分类位 + 'Shape.Ghost': 'g9k_1', // true = 只探测不产生碰撞响应 + + // ---- 血量组件(Vkt)---- + 'Health.Max': 'p12o_1', + 'Health.Current': 'q12o_1', + + // ---- 用户信息组件(yot)---- + 'User.Name': 'c12n_1', + 'User.Clan': 'e12n_1', + + // ---- 装备分组件(BattleGearScoreCC)---- + 'Gear.Score': 'cjf_1', + + // ---- 皮肤组件(eUt)---- + 'Skin.HullSkin': 'g159_1', // 皮肤对象 + 'Skin.HullRoot': 'h159_1', // 车身渲染树根(BP 节点) + 'Skin.Parts': 'g159_1.g17r_1', // 部件表 + 'Skin.PartNode': 'j17r_1', // 部件 → BP 节点 + + // ---- 炮塔皮肤组件(LUt)---- + 'Turret.Wrapper': 'a11p_1', // 组件首字段(包装对象) + 'Turret.Root': 'e11p_1.a17q_1', // 炮塔 BP 根节点 + 'Turret.GetRoot': 'u11p()', + + // ---- BP 场景节点(zl)---- + 'Node.Name': 'g35_1', + 'Node.Matrix': 'p35_1', // 世界矩阵(列主序,[12][13][14] = 平移) + 'Node.Children': 'r35_1', + 'Node.Mesh': 'v3m_1', + 'Node.Lod': 'u4x_1', + + // ---- 相机(场景实体 .jz1_1.iyx_1)---- + 'Camera.SceneRef': 'jz1_1', + 'Camera.Main': 'jz1_1.iyx_1', + 'Camera.Position': 'jz1_1.iyx_1.t35_1', + 'Camera.ViewMatrix': 'jz1_1.iyx_1.d37_1', + 'Camera.ClipMatrix': 'jz1_1.iyx_1.g37_1', + 'Camera.Fov': 'jz1_1.iyx_1.b37_1' + }; + + // ===== module_87072 的导出键($_.xx),每版都可能平移 ===== + // 队伍:vd = WIt(t) 取实体队伍;dl = R7(a,b) 同队且非中立 + // ⚠ 旧版是 kd / rk;新版 kd 已指向无关构造器 + var Exports = { + 'Export.TeamOf': ['vd', 'kd'], + 'Export.TeamSame': ['dl', 'rk'] + }; + + // ===== 路径解析 ===== + function IsCall(Token) { + var N = Token.length; + return N > 2 && Token.charAt(N - 1) === ')' && Token.charAt(N - 2) === '('; + } + + function ResolvePath(Target, Path) { + if (Target === null || Target === undefined || !Path) return undefined; + var Tokens = String(Path).split('.'); + var Cur = Target; + for (var i = 0; i < Tokens.length; i++) { + if (Cur === null || Cur === undefined) return undefined; + var Tok = Tokens[i]; + if (IsCall(Tok)) { + var Name = Tok.slice(0, -2); + if (typeof Cur[Name] !== 'function') return undefined; + try { Cur = Cur[Name](); } catch (e) { return undefined; } + } else { + Cur = Cur[Tok]; + } + } + return Cur; + } + + // 取字段值:Key 是总表键(如 'Tank.StateValue'),未命中则把 Key 当字面路径 + function Get(Target, Key) { + var Path = Fields[Key]; + return ResolvePath(Target, Path !== undefined ? Path : Key); + } + + // 取字段路径(字符串),用于打印/自检 + function PathOf(Key) { + return Fields[Key] !== undefined ? Fields[Key] : String(Key); + } + + // 取单段属性名:'Node.Matrix' → 'p35_1';带方法或多段时返回 null + // 工具里写成 `Node[FieldName('Node.Matrix') || 'p35_1']`,即「优先用总表,缺表时用字面名」 + function FieldName(Key) { + var P = Fields[Key]; + if (!P || P.indexOf('.') >= 0 || IsCall(P)) return null; + return P; + } + + // 取单段方法名:'Physics.RemoveMask' → 'm155' + function MethodName(Key) { + var P = Fields[Key]; + if (!P || !IsCall(P) || P.indexOf('.') >= 0) return null; + return P.slice(0, -2); + } + + // 自检:按前缀检查某对象上哪些语义字段能解析 + function Check(Target, Prefix) { + var Out = { Ok: [], Missing: [] }; + Object.keys(Fields).forEach(function (Key) { + if (Prefix && Key.indexOf(Prefix + '.') !== 0) return; + var V = Get(Target, Key); + if (V === undefined) Out.Missing.push(Key + ' → ' + Fields[Key]); + else Out.Ok.push(Key + ' → ' + Fields[Key]); + }); + return Out; + } + + // 用当前战局对象做一次全量自检(控制台调用 __hornetSchema__.SelfCheck()) + function SelfCheck() { + var Gm = window.__gameMode__; + var Report = { HasGameMode: !!Gm, Groups: {}, Missing: [] }; + if (!Gm) return Report; + + var Scene = Get(Gm, 'GameMode.Scene'); + var TankList = Get(Gm, 'GameMode.TankList'); + var Tank = null; + try { + Tank = Get(Gm, 'GameMode.OwnedTank'); + if (!Tank && TankList && typeof TankList.e1 === 'function' && TankList.e1() > 0) Tank = TankList.g1(0); + } catch (e) {} + + var Targets = { + Entity: Tank || Scene, + GameMode: Gm, + Tank: Tank, + Physics: Tank ? FindPhysics(Tank) : null, + Shape: null, + Skin: Tank, + Turret: Tank, + Node: null, + Camera: Scene + }; + + Object.keys(Targets).forEach(function (Prefix) { + if (!Targets[Prefix]) { Report.Groups[Prefix] = '无样本对象'; return; } + var R = Check(Targets[Prefix], Prefix); + Report.Groups[Prefix] = R; + R.Missing.forEach(function (M) { Report.Missing.push(Prefix + '.' + M); }); + }); + + if (Report.Missing.length) { + console.warn('[字段抽象] 失配 ' + Report.Missing.length + ' 项:', Report.Missing); + } else { + console.log('[字段抽象] 全部语义字段解析正常'); + } + return Report; + } + + // 找坦克物理组件(与工具内实现同源,放这里方便自检) + function FindPhysics(Tank) { + if (!Tank) return null; + var Map = Get(Tank, 'Entity.Components'); + if (!Map || typeof Map.forEach !== 'function') return null; + var Found = null; + Map.forEach(function (ValueList) { + if (Found) return; + var Comp = (ValueList && typeof ValueList.g1 === 'function') ? ValueList.g1(0) : ValueList; + if (!Comp) return; + var Hull = Get(Comp, 'Physics.Hull'); + if (Hull && Get(Hull, 'Shape.Name') === 'hull-main') Found = Comp; + }); + return Found; + } + + // ===== 导出 ===== + window.__hornetSchema__ = { + Version: 'main.7cfa4058', + Fields: Fields, + Exports: Exports, + Get: Get, + PathOf: PathOf, + FieldName: FieldName, + MethodName: MethodName, + Check: Check, + SelfCheck: SelfCheck, + ResolvePath: ResolvePath, + FindPhysics: FindPhysics + }; + + console.log('[3DTank] 字段抽象层已加载(版本 main.7cfa4058,语义字段 ' + Object.keys(Fields).length + ' 项)'); +})(); + + // ===== hornet_lib.js ===== // ===== 3DTank公共库 - 工具文件共享辅助函数 ===== // 在浏览器控制台运行(需先运行 hook_gamemode.js) @@ -228,6 +480,18 @@ // ===== 缓存队伍判断函数(ZCt/m2Fn) ===== var ZCt = null, m2Fn = null; + // ===== 字段抽象层引用(字段名统一在 hornet_schema.js 里维护) ===== + // 取不到抽象层时退回本地常量,保证单独加载本文件也能工作 + var Schema = window.__hornetSchema__ || null; + function FieldOf(Key, Fallback) { + if (!Schema) return Fallback; + var P = Schema.PathOf(Key); + return (P && P.indexOf('.') < 0 && P.indexOf('(') < 0) ? P : Fallback; + } + var TEAM_FIELD = FieldOf('Team.Field', 'b135_1'); // 队伍组件上的队伍对象字段 + var TEAM_ID_FIELD = FieldOf('Team.Id', 'k3_1'); // 队伍编号:0/1/2(2=中立) + var NEUTRAL_TEAM_ID = 2; + // 加载队伍判断函数(从 module_87072 导出) // 新旧差异(main.7cfa4058): // 取队伍 $_$.vd = WIt(t) → g$(HIt(), t).b135_1(坦克无队伍时返回中立 q$()) @@ -471,80 +735,92 @@ // 只在「本地坦克实体变了」(换局/换车)时才重新缓存。 var LocalTeam = null; var LocalTeamTankId = null; + + // 读队伍对象:优先引擎导出函数 WIt($_$.vd),失败则直接读队伍组件字段 b135_1 + // WIt(t) 的实现就是 g$(HIt(), t).b135_1 —— 所以直接读组件字段与引擎同源, + // 导出键漂移时这条路仍然有效 + function ReadTeamRaw(tank) { + if (!tank) return null; + if (!ZCt) LoadTeamFunctions(); + if (ZCt) { + try { var T = ZCt(tank); if (T) return T; } catch (e) {} + } + var Comp = FindComponentByField(tank, TEAM_FIELD); + return Comp ? Comp[TEAM_FIELD] : null; + } + + // 读队伍编号:0 / 1 = 两个阵营,2 = 中立(q$());读不到返回 -1 + // ⚠ 用数值编号比较,而不是引擎 equals/R7:编号是稳定语义 + // (module_87072 里 P7()/$ct() 都按 k3_1 分派) + function GetTeamId(Team) { + if (!Team) return -1; + var Id = Team[TEAM_ID_FIELD]; + return (typeof Id === 'number') ? Id : -1; + } + + function GetLocalTeamId() { return GetTeamId(LocalTeam); } + function GetTankTeamId(tank) { return GetTeamId(ReadTeamRaw(tank)); } + function RememberLocalTeam(gm) { - if (!ZCt || !m2Fn) LoadTeamFunctions(); - if (!ZCt) return; var t = null; try { t = gm.u10b_1; } catch (e) { t = null; } if (t) { var Id = t.iz1_1; if (LocalTeam && LocalTeamTankId === Id) return; - try { - var Team = ZCt(t); - if (Team && (!m2Fn || m2Fn(Team, Team))) { LocalTeam = Team; LocalTeamTankId = Id; } - } catch (e) {} + var Team = ReadTeamRaw(t); + var TeamId = GetTeamId(Team); + if (TeamId >= 0 && TeamId !== NEUTRAL_TEAM_ID) { LocalTeam = Team; LocalTeamTankId = Id; } return; } // u10b_1 为空(死亡/观战):保持已有缓存;若还没缓存过,再试 GameMode 上的本地队伍字段 if (LocalTeam) return; try { var G = gm.v10b_1; - if (G && (!m2Fn || m2Fn(G, G))) LocalTeam = G; + var GId = GetTeamId(G); + if (GId >= 0 && GId !== NEUTRAL_TEAM_ID) LocalTeam = G; } catch (e) {} } // ===== 判断是否友方 ===== - // 队伍同步有延迟:坦克刚出现时队伍是「中立」,直接比较会先判成敌人、同步后再变队友(颜色跳变)。 - // 处理:① 每辆坦克缓存最近一次**非中立**队伍;② 无法判定时沿用上一次判定结果。 - var TankTeamCache = new WeakMap(); // tank -> 非中立队伍对象 + // 规则:队伍编号相同且非中立 → 友方;中立/未下发 → 沿用该坦克上次判定(默认敌人)。 + // ⚠ 不再「一次判定永久锁定」:锁定会在基准短暂错误时把整局颜色固化错, + // 这里改成每帧重算 + 中立抖动时沿用上次结果,既不会闪烁也不会锁死。 var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定 - var TeamLockLogged = new WeakSet(); - function ResolveTankTeam(tank) { - if (!ZCt) return null; - var Team = null; - try { Team = ZCt(tank); } catch (e) { Team = null; } - if (Team && (!m2Fn || m2Fn(Team, Team))) { // 同队且非中立 = 有效队伍 - TankTeamCache.set(tank, Team); - return Team; - } - return TankTeamCache.get(tank) || null; - } + var TeamJudgeLogged = new WeakSet(); function IsFriendly(tank, playerTank, gm) { if (!tank) return false; - // 队伍函数可能因为工具早于游戏模块加载而没取到,这里按需补加载 - if (!ZCt || !m2Fn) LoadTeamFunctions(); if (gm) RememberLocalTeam(gm); - // 一旦在「基准队伍可靠」时判过,就锁定本局结果,战中不再改变 - var Locked = TankFriendCache.get(tank); - if (Locked !== undefined) return Locked; - if (ZCt && m2Fn) { - try { - var tankTeam = ResolveTankTeam(tank); - // 可靠基准(用于锁定):本地拥有坦克的队伍 / GameMode 上的本地队伍字段 - var LockBase = LocalTeam; - if (!LockBase && gm) { try { LockBase = gm.v10b_1; } catch (e) {} } - // 观战目标只作临时参考,绝不用它锁定(它可能是敌人) - var BaseTeam = LockBase || (playerTank ? ResolveTankTeam(playerTank) : null); - if (tankTeam && BaseTeam) { - var Friendly = m2Fn(tankTeam, BaseTeam); - if (LockBase) { - TankFriendCache.set(tank, Friendly); - if (Friendly && !TeamLockLogged.has(tank)) { - TeamLockLogged.add(tank); - try { - console.log('[敌我] 锁定为友方 id=' + tank.iz1_1 + ' name=' + (GetTankName(tank) || '?')); - } catch (e) {} - } - } - return Friendly; - } - } catch(e) {} + + var TankId = GetTeamId(ReadTeamRaw(tank)); + if (TankId < 0 || TankId === NEUTRAL_TEAM_ID) { + var Last = TankFriendCache.get(tank); // 队伍还没同步:保持上一帧颜色 + return Last === undefined ? false : Last; } - // 基准还没就绪:先按敌人显示,但**不锁定**,等基准可用后再定 - return false; + + // 可靠基准:本地拥有坦克的队伍(缓存)→ GameMode 上的本地队伍字段 + // 观战目标(t10b_1)可能是敌人,绝不作为基准 + var BaseTeam = LocalTeam; + if (!BaseTeam && gm) { try { BaseTeam = gm.v10b_1; } catch (e) {} } + var BaseId = GetTeamId(BaseTeam); + if (BaseId < 0 || BaseId === NEUTRAL_TEAM_ID) { + var Last2 = TankFriendCache.get(tank); // 基准不可靠(极早期):沿用上次 + return Last2 === undefined ? false : Last2; + } + + var Friendly = (TankId === BaseId); + TankFriendCache.set(tank, Friendly); + if (!TeamJudgeLogged.has(tank)) { + TeamJudgeLogged.add(tank); + try { + console.log('[敌我] id=' + tank.iz1_1 + ' 队伍=' + TankId + ' 本地=' + BaseId + + ' → ' + (Friendly ? '友方' : '敌方') + ' name=' + (GetTankName(tank) || '?')); + } catch (e) {} + } + return Friendly; } + // ===== 获取相机信息(场景主相机 scene.iyx_1) ===== // 新旧差异:旧版是找挂在实体上的相机组件(kym_1/lym_1),新版这些字段已被别的类占用; // 新版直接暴露主相机对象:gm.lz4().jz1_1(场景).iyx_1(相机,类 Jf/lc) @@ -831,6 +1107,10 @@ IsBot: IsBot, GetPlayerTank: GetPlayerTank, IsFriendly: IsFriendly, + ReadTeamRaw: ReadTeamRaw, + GetTeamId: GetTeamId, + GetTankTeamId: GetTankTeamId, + GetLocalTeamId: GetLocalTeamId, GetCameraInfo: GetCameraInfo, GetUMtComponent: GetUMtComponent, MkVec: MkVec, @@ -1616,6 +1896,23 @@ var gm = window.__gameMode__ || null; + // ===== 字段抽象层(匿名引擎字段 → 语义名,总表在 hornet_schema.js)===== + // 换版本时只改 schema;Fld 返回当前版本的字面名,schema 缺失时退回 fallback + var Schema = window.__hornetSchema__ || null; + function Fld(Key, Fallback) { + var N = Schema ? Schema.FieldName(Key) : null; + return N || Fallback; + } + var FNodeName = Fld('Node.Name', 'g35_1'); // 节点名 + var FNodeMatrix = Fld('Node.Matrix', 'p35_1'); // 世界矩阵 + var FNodeChildren = Fld('Node.Children', 'r35_1'); // 子节点列表 + var FNodeMesh = Fld('Node.Mesh', 'v3m_1'); // mesh 资源(Tp) + var FNodeLod = Fld('Node.Lod', 'u4x_1'); // LOD 列表 + var FSkinHullSkin = Fld('Skin.HullSkin', 'g159_1'); // 皮肤对象 + var FSkinHullRoot = Fld('Skin.HullRoot', 'h159_1'); // 车身渲染树根 + var FSkinParts = Fld('Skin.Parts', 'g17r_1'); // 部件表 + var FSkinPartNode = Fld('Skin.PartNode', 'j17r_1'); // 部件 → BP 节点 + // ===== 当前模式 ===== var Mode = 'outline'; // 'wireframe' | 'outline' try { @@ -1826,7 +2123,7 @@ if (!entity) return null; var Cached = SkinCompCache.get(entity); if (Cached) return Cached; - var comp = FindComponentByField(entity, 'g159_1'); + var comp = FindComponentByField(entity, FSkinHullSkin); if (!comp) comp = FindComponentByField(entity, 'a17q_1'); // 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表) if (!comp) { @@ -1837,7 +2134,7 @@ if (!C) return; for (var K in C) { var V = C[K]; - if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; } + if (V && typeof V === 'object' && V[FSkinParts] && typeof V[FSkinParts].e1 === 'function') { comp = C; return; } } }); } catch (e) {} @@ -1850,7 +2147,7 @@ function GetHullSkin(tank, stats) { var comp = GetSkinComponent(tank); if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; } - if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; } + if (comp[FSkinHullSkin] && comp[FSkinHullSkin][FSkinParts]) { if (stats) stats.hullSource = 'g159_1'; return comp[FSkinHullSkin]; } if (stats) stats.hullSource = '有组件但无皮肤'; return null; } @@ -2061,25 +2358,26 @@ Seen.add(bpNode); } if (stats) stats.seen++; - var name = bpNode.g35_1 || ''; - if (bpNode.v3m_1) { + var name = bpNode[FNodeName] || ''; + if (bpNode[FNodeMesh]) { if (stats) stats.hasMesh++; - var gpuData = GetGpuData(bpNode.v3m_1); + var gpuData = GetGpuData(bpNode[FNodeMesh]); if (gpuData) { if (stats) stats.gpuOk++; nodes.push({ - name: name, node: bpNode, tp: bpNode.v3m_1, + name: name, node: bpNode, tp: bpNode[FNodeMesh], positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } else if (stats) stats.gpuFail++; } - if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') { + var LodList = bpNode[FNodeLod]; + if (LodList && typeof LodList.e1 === 'function') { try { - var lodCount = bpNode.u4x_1.e1(); + var lodCount = LodList.e1(); for (var lod = 0; lod < lodCount && lod < 1; lod++) { - var lodLevel = bpNode.u4x_1.g1(lod); + var lodLevel = LodList.g1(lod); if (!lodLevel) continue; - var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1; + var lodTp = lodLevel[FNodeMesh] || lodLevel.v3m_1; if (!lodTp) continue; var lodGpuData = GetGpuData(lodTp); if (lodGpuData) { @@ -2094,7 +2392,7 @@ } // 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点) // 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表 - var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_1 : + var Children = (bpNode[FNodeChildren] && typeof bpNode[FNodeChildren].e1 === 'function') ? bpNode[FNodeChildren] : ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { @@ -2107,9 +2405,9 @@ // 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表 function IsSceneNode(V) { return !!(V && typeof V === 'object' && - typeof V.g35_1 === 'string' && - V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 && - V.r35_1 && typeof V.r35_1 === 'object'); + typeof V[FNodeName] === 'string' && + V[FNodeMatrix] && typeof V[FNodeMatrix].length === 'number' && V[FNodeMatrix].length >= 16 && + V[FNodeChildren] && typeof V[FNodeChildren] === 'object'); } // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 @@ -2121,6 +2419,43 @@ return true; } + // ===== 节点归属校验:只认「跟着这辆坦克」的节点 ===== + // 兜底扫描是盲扫组件字段,可能翻到共享的场景根/周围物件(表现:非坦克也被画线框)。 + // 坦克自身所有部件都在车体附近(整车 < 10m),用世界矩阵平移做距离闸门即可把它们挡掉。 + // 只对 group === 'scan' 的节点生效:车身根/炮塔根是可信路径,绝不因距离丢模型。 + var NODE_SCAN_RADIUS = 2500; // 25m(单位与引擎世界坐标一致) + + function NodeTranslation(Node) { + if (!Node) return null; + var M = Node[FNodeMatrix]; + if (!M || typeof M.length !== 'number' || M.length < 16) { + M = Node.n35_1; // 旧版矩阵字段 + if (!M || typeof M.length !== 'number' || M.length < 16) return null; + } + return { x: M[12], y: M[13], z: M[14] }; + } + + // 剔除「非本车」的兜底扫描节点(车身根/炮塔根是可信路径,不参与该过滤) + function FilterOwnedNodes(nodes, Pos) { + if (!nodes || !Pos || !nodes.length) return nodes; + var Out = []; + for (var i = 0; i < nodes.length; i++) { + var N = nodes[i]; + if (N.group === 'scan' && !IsNodeNearPoint(N.node, Pos)) continue; + Out.push(N); + } + return Out; + } + + function IsNodeNearPoint(Node, Pos, Radius) { + var T = NodeTranslation(Node); + if (!T || !Pos) return true; // 读不到矩阵时不拦(交给后续渲染阶段处理) + var dx = T.x - Pos.x, dy = T.y - Pos.y, dz = T.z - Pos.z; + var R = Radius || NODE_SCAN_RADIUS; + return (dx * dx + dy * dy + dz * dz) <= (R * R); + } + + // 扫描结果按「组件字段路径」缓存(不缓存节点对象:引擎会重建节点树, // 但字段名与组件实例稳定,路径每次都能解析到当前节点) var NodeSrcCache = new WeakMap(); // entity -> [{ Comp, Path: [K1, K2, K3] }] @@ -2135,7 +2470,7 @@ return V; } - function ScanMeshNodes(tank, nodes, stats, Seen) { + function ScanMeshNodes(tank, nodes, stats, Seen, TankPos) { Seen = Seen || new WeakSet(); // ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件 ScanTick++; @@ -2146,6 +2481,7 @@ for (var c = 0; c < Cached.length; c++) { var V0 = ResolveSrc(Cached[c]); if (!IsSceneNode(V0)) { AllOk = false; break; } + if (!IsNodeNearPoint(V0, TankPos)) continue; // 缓存根跑远了(不是这辆车的)→ 不画 if (!Seen.has(V0)) CollectMeshNodes(V0, nodes, 0, 'scan', stats, Seen); } if (AllOk) return; @@ -2154,6 +2490,8 @@ var Srcs = []; function Try(V, Comp, Path) { if (!IsSceneNode(V) || Seen.has(V)) return; + // 归属闸门:盲扫可能翻到共享场景根/周围物件,按「是否在车体附近」剔除 + if (!IsNodeNearPoint(V, TankPos)) { if (stats) stats.farRejected = (stats.farRejected || 0) + 1; return; } CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); Srcs.push({ Comp: Comp, Path: Path }); } @@ -2208,25 +2546,25 @@ // ===== 提取坦克的mesh数据 ===== // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底) - function GetTankMeshData(tank, stats) { + function GetTankMeshData(tank, stats, TankPos) { var nodes = []; var Seen = new WeakSet(); var skinComp = GetSkinComponent(tank); - var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null; + var skin = (skinComp && skinComp[FSkinHullSkin] && skinComp[FSkinHullSkin][FSkinParts]) ? skinComp[FSkinHullSkin] : null; if (stats) { stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); - stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; + stats.hullParts = (skin && typeof skin[FSkinParts].e1 === 'function') ? skin[FSkinParts].e1() : -1; } // ① 主路径:车身根节点(包含全部部件节点) - if (skinComp && skinComp.h159_1) { - try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} + if (skinComp && skinComp[FSkinHullRoot]) { + try { CollectMeshNodes(skinComp[FSkinHullRoot], nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } // ② 根节点采不到才退回部件表 if (nodes.length === 0 && skin) { try { - ForEachInList(skin.g17r_1, function (part) { + ForEachInList(skin[FSkinParts], function (part) { if (!part) return; - var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表 + var j = part[FSkinPartNode]; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { @@ -2240,7 +2578,7 @@ } // ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分) // 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体" - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen, TankPos); } catch (e) {} // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; @@ -2843,9 +3181,11 @@ var MeshStats = { hullSource: '', hullParts: 0, turretComp: false, turretRoot: false, hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, - turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 } + turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, + scan: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0, farRejected: 0 } }; - var meshNodes = GetTankMeshData(tank, MeshStats); + var meshNodes = GetTankMeshData(tank, MeshStats, pos); + meshNodes = FilterOwnedNodes(meshNodes, pos); // 只保留跟着这辆车动的节点(挡掉非坦克物件) if (!meshNodes || meshNodes.length === 0) { LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见 @@ -5839,6 +6179,24 @@ // 坦克 category 位(bit0) var TANK_CATEGORY_BIT = 1; + // ===== 字段抽象层(语义名 → 匿名字段,总表在 hornet_schema.js)===== + var Schema = window.__hornetSchema__ || null; + function Fld(Key, Fallback) { + var N = Schema ? Schema.FieldName(Key) : null; + return N || Fallback; + } + function Mth(Key, Fallback) { + var N = Schema ? Schema.MethodName(Key) : null; + return N || Fallback; + } + var FPhysicsHull = Fld('Physics.Hull', 'zzq_1'); // 车体主碰撞形状 + var FShapeName = Fld('Shape.Name', 'x9j_1'); // 形状名('hull-main') + var FShapeMask = Fld('Shape.Mask', 'z9j_1'); // 碰撞掩码 + var FBodyShapes = 'y9i_1'; // 刚体上的形状列表(pbk().y9i_1) + var MAddMask = Mth('Physics.AddMask', 'n155'); + var MRemoveMask = Mth('Physics.RemoveMask', 'm155'); + var MGetBody = Mth('Physics.Body', 'pbk'); + var Stat = { running: false, shapes: 0, // 本帧处理的碰撞形状数 @@ -5853,10 +6211,10 @@ // 特征:zzq_1 是车体主碰撞形状(x9j_1 === "hull-main"),且带 n155 / m155 / pbk function IsTankPhysics(Comp) { if (!Comp) return false; - if (typeof Comp.n155 !== 'function' || typeof Comp.m155 !== 'function') return false; - if (typeof Comp.pbk !== 'function') return false; - var Hull = Comp.zzq_1; - return !!(Hull && Hull.x9j_1 === 'hull-main'); + if (typeof Comp[MAddMask] !== 'function' || typeof Comp[MRemoveMask] !== 'function') return false; + if (typeof Comp[MGetBody] !== 'function') return false; + var Hull = Comp[FPhysicsHull]; + return !!(Hull && Hull[FShapeName] === 'hull-main'); } // 在实体上找坦克物理组件(不走 FindComponentByField:zzq_1 字段名在其它类里也有复用) @@ -5888,7 +6246,7 @@ function ReleaseSnapshot() { MaskSnapshot.forEach(function (Mask, Shape) { try { - if (Shape && typeof Mask === 'number') Shape.z9j_1 = Mask; + if (Shape && typeof Mask === 'number') Shape[FShapeMask] = Mask; } catch (e) {} }); MaskSnapshot.clear(); @@ -5911,8 +6269,8 @@ } var Body = null; - try { Body = Phys.pbk(); } catch (e) { Body = null; } - var Shapes = Body && Body.y9i_1; + try { Body = Phys[MGetBody](); } catch (e) { Body = null; } + var Shapes = Body && Body[FBodyShapes]; if (!Shapes || typeof Shapes.e1 !== 'function' || typeof Shapes.g1 !== 'function') return; var Count = Shapes.e1() | 0; @@ -5920,10 +6278,10 @@ for (var i = 0; i < Count; i++) { var Shape = null; try { Shape = Shapes.g1(i); } catch (e) { Shape = null; } - if (!Shape || typeof Shape.z9j_1 !== 'number') continue; + if (!Shape || typeof Shape[FShapeMask] !== 'number') continue; // 首次见到该形状时记录原始 mask,保证 Stop 能精确还原 - if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape.z9j_1); - Shape.z9j_1 = Shape.z9j_1 & ~TANK_CATEGORY_BIT; + if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape[FShapeMask]); + Shape[FShapeMask] = Shape[FShapeMask] & ~TANK_CATEGORY_BIT; Touched++; } Stat.shapes = Touched; diff --git a/build_bundle.py b/build_bundle.py index 08a29c0..b17a468 100644 --- a/build_bundle.py +++ b/build_bundle.py @@ -16,17 +16,18 @@ UNPACKED_OUTPUT = os.path.join(UNPACKED_DIR, "hornet_bundle.js") # 按依赖顺序加载 FILES = [ "hook_gamemode.js", # 1. Hook 捕获 GameMode(异步) - "hornet_lib.js", # 2. 公共库(软警告,不阻塞加载) - "hornet_radar.js", # 3. 雷达模块 -> __hornetTools__.Radar - "hornet_3d_wireframe.js",# 4. 3D透视模块 -> __hornetTools__.Wireframe - "hornet_rearview.js", # 5. 后视镜模块 -> __hornetTools__.Rearview - "hornet_aim_warning.js", # 6. 瞄准警告模块 -> __hornetTools__.AimWarning - "hornet_freelook.js", # 7. 自由视角模块 -> __hornetTools__.FreeLook - "hornet_warsandbox.js", # 8. 战争沙盘模块 -> __hornetTools__.WarSandbox - "hornet_mouse.js", # 9. 鼠标输入稳定器 -> __hornetTools__.Mouse - "hornet_noclip.js", # 10. 穿坦克(忽略玩家碰撞)-> __hornetTools__.NoClip - "hornet_panel.js", # 11. 控制面板(Alt+T + HUD) - "hornet_prefetch.js", # 12. 车库资源预热(战局空闲时后台加载车库所需资源) + "hornet_schema.js", # 2. 字段抽象层(匿名引擎字段 → 语义名,换版本只改这里) + "hornet_lib.js", # 3. 公共库(软警告,不阻塞加载) + "hornet_radar.js", # 4. 雷达模块 -> __hornetTools__.Radar + "hornet_3d_wireframe.js",# 5. 3D透视模块 -> __hornetTools__.Wireframe + "hornet_rearview.js", # 6. 后视镜模块 -> __hornetTools__.Rearview + "hornet_aim_warning.js", # 7. 瞄准警告模块 -> __hornetTools__.AimWarning + "hornet_freelook.js", # 8. 自由视角模块 -> __hornetTools__.FreeLook + "hornet_warsandbox.js", # 9. 战争沙盘模块 -> __hornetTools__.WarSandbox + "hornet_mouse.js", # 10. 鼠标输入稳定器 -> __hornetTools__.Mouse + "hornet_noclip.js", # 11. 穿坦克(忽略玩家碰撞)-> __hornetTools__.NoClip + "hornet_panel.js", # 12. 控制面板(Alt+T + HUD) + "hornet_prefetch.js", # 13. 车库资源预热(战局空闲时后台加载车库所需资源) ] HEADER = """// ============================================================ diff --git a/tools/hornet_3d_wireframe.js b/tools/hornet_3d_wireframe.js index 296cd40..e09a472 100644 --- a/tools/hornet_3d_wireframe.js +++ b/tools/hornet_3d_wireframe.js @@ -22,6 +22,23 @@ var gm = window.__gameMode__ || null; + // ===== 字段抽象层(匿名引擎字段 → 语义名,总表在 hornet_schema.js)===== + // 换版本时只改 schema;Fld 返回当前版本的字面名,schema 缺失时退回 fallback + var Schema = window.__hornetSchema__ || null; + function Fld(Key, Fallback) { + var N = Schema ? Schema.FieldName(Key) : null; + return N || Fallback; + } + var FNodeName = Fld('Node.Name', 'g35_1'); // 节点名 + var FNodeMatrix = Fld('Node.Matrix', 'p35_1'); // 世界矩阵 + var FNodeChildren = Fld('Node.Children', 'r35_1'); // 子节点列表 + var FNodeMesh = Fld('Node.Mesh', 'v3m_1'); // mesh 资源(Tp) + var FNodeLod = Fld('Node.Lod', 'u4x_1'); // LOD 列表 + var FSkinHullSkin = Fld('Skin.HullSkin', 'g159_1'); // 皮肤对象 + var FSkinHullRoot = Fld('Skin.HullRoot', 'h159_1'); // 车身渲染树根 + var FSkinParts = Fld('Skin.Parts', 'g17r_1'); // 部件表 + var FSkinPartNode = Fld('Skin.PartNode', 'j17r_1'); // 部件 → BP 节点 + // ===== 当前模式 ===== var Mode = 'outline'; // 'wireframe' | 'outline' try { @@ -232,7 +249,7 @@ if (!entity) return null; var Cached = SkinCompCache.get(entity); if (Cached) return Cached; - var comp = FindComponentByField(entity, 'g159_1'); + var comp = FindComponentByField(entity, FSkinHullSkin); if (!comp) comp = FindComponentByField(entity, 'a17q_1'); // 兜底:按结构找 —— 组件字段值是"皮肤对象"(含 g17r_1 部件表) if (!comp) { @@ -243,7 +260,7 @@ if (!C) return; for (var K in C) { var V = C[K]; - if (V && typeof V === 'object' && V.g17r_1 && typeof V.g17r_1.e1 === 'function') { comp = C; return; } + if (V && typeof V === 'object' && V[FSkinParts] && typeof V[FSkinParts].e1 === 'function') { comp = C; return; } } }); } catch (e) {} @@ -256,7 +273,7 @@ function GetHullSkin(tank, stats) { var comp = GetSkinComponent(tank); if (!comp) { if (stats) stats.hullSource = '无皮肤组件'; return null; } - if (comp.g159_1 && comp.g159_1.g17r_1) { if (stats) stats.hullSource = 'g159_1'; return comp.g159_1; } + if (comp[FSkinHullSkin] && comp[FSkinHullSkin][FSkinParts]) { if (stats) stats.hullSource = 'g159_1'; return comp[FSkinHullSkin]; } if (stats) stats.hullSource = '有组件但无皮肤'; return null; } @@ -467,25 +484,26 @@ Seen.add(bpNode); } if (stats) stats.seen++; - var name = bpNode.g35_1 || ''; - if (bpNode.v3m_1) { + var name = bpNode[FNodeName] || ''; + if (bpNode[FNodeMesh]) { if (stats) stats.hasMesh++; - var gpuData = GetGpuData(bpNode.v3m_1); + var gpuData = GetGpuData(bpNode[FNodeMesh]); if (gpuData) { if (stats) stats.gpuOk++; nodes.push({ - name: name, node: bpNode, tp: bpNode.v3m_1, + name: name, node: bpNode, tp: bpNode[FNodeMesh], positions: gpuData.positions, indices: gpuData.indices, stride: gpuData.stride, group: group }); } else if (stats) stats.gpuFail++; } - if (bpNode.u4x_1 && typeof bpNode.u4x_1.e1 === 'function') { + var LodList = bpNode[FNodeLod]; + if (LodList && typeof LodList.e1 === 'function') { try { - var lodCount = bpNode.u4x_1.e1(); + var lodCount = LodList.e1(); for (var lod = 0; lod < lodCount && lod < 1; lod++) { - var lodLevel = bpNode.u4x_1.g1(lod); + var lodLevel = LodList.g1(lod); if (!lodLevel) continue; - var lodTp = lodLevel.v3m_1 || lodLevel.v3m_1; + var lodTp = lodLevel[FNodeMesh] || lodLevel.v3m_1; if (!lodTp) continue; var lodGpuData = GetGpuData(lodTp); if (lodGpuData) { @@ -500,7 +518,7 @@ } // 子节点:新版是 r35_1(zl 基类:this.r35_1 = new lo(),r3k 里递归子节点) // 旧版是 p35_1;新版 p35_1 已变成"世界矩阵",绝不能当子节点列表 - var Children = (bpNode.r35_1 && typeof bpNode.r35_1.e1 === 'function') ? bpNode.r35_1 : + var Children = (bpNode[FNodeChildren] && typeof bpNode[FNodeChildren].e1 === 'function') ? bpNode[FNodeChildren] : ((bpNode.p35_1 && typeof bpNode.p35_1.e1 === 'function') ? bpNode.p35_1 : null); if (Children) { ForEachInList(Children, function (child) { @@ -513,9 +531,9 @@ // 新版节点(zl 基类)特征:g35_1 = 名字字符串,p35_1 = 世界矩阵(长度≥16),r35_1 = 子节点列表 function IsSceneNode(V) { return !!(V && typeof V === 'object' && - typeof V.g35_1 === 'string' && - V.p35_1 && typeof V.p35_1.length === 'number' && V.p35_1.length >= 16 && - V.r35_1 && typeof V.r35_1 === 'object'); + typeof V[FNodeName] === 'string' && + V[FNodeMatrix] && typeof V[FNodeMatrix].length === 'number' && V[FNodeMatrix].length >= 16 && + V[FNodeChildren] && typeof V[FNodeChildren] === 'object'); } // 只遍历普通对象:TypedArray / 数组 / Map / Set 的 for-in 会产出成千上万个键,必须跳过 @@ -527,6 +545,43 @@ return true; } + // ===== 节点归属校验:只认「跟着这辆坦克」的节点 ===== + // 兜底扫描是盲扫组件字段,可能翻到共享的场景根/周围物件(表现:非坦克也被画线框)。 + // 坦克自身所有部件都在车体附近(整车 < 10m),用世界矩阵平移做距离闸门即可把它们挡掉。 + // 只对 group === 'scan' 的节点生效:车身根/炮塔根是可信路径,绝不因距离丢模型。 + var NODE_SCAN_RADIUS = 2500; // 25m(单位与引擎世界坐标一致) + + function NodeTranslation(Node) { + if (!Node) return null; + var M = Node[FNodeMatrix]; + if (!M || typeof M.length !== 'number' || M.length < 16) { + M = Node.n35_1; // 旧版矩阵字段 + if (!M || typeof M.length !== 'number' || M.length < 16) return null; + } + return { x: M[12], y: M[13], z: M[14] }; + } + + // 剔除「非本车」的兜底扫描节点(车身根/炮塔根是可信路径,不参与该过滤) + function FilterOwnedNodes(nodes, Pos) { + if (!nodes || !Pos || !nodes.length) return nodes; + var Out = []; + for (var i = 0; i < nodes.length; i++) { + var N = nodes[i]; + if (N.group === 'scan' && !IsNodeNearPoint(N.node, Pos)) continue; + Out.push(N); + } + return Out; + } + + function IsNodeNearPoint(Node, Pos, Radius) { + var T = NodeTranslation(Node); + if (!T || !Pos) return true; // 读不到矩阵时不拦(交给后续渲染阶段处理) + var dx = T.x - Pos.x, dy = T.y - Pos.y, dz = T.z - Pos.z; + var R = Radius || NODE_SCAN_RADIUS; + return (dx * dx + dy * dy + dz * dz) <= (R * R); + } + + // 扫描结果按「组件字段路径」缓存(不缓存节点对象:引擎会重建节点树, // 但字段名与组件实例稳定,路径每次都能解析到当前节点) var NodeSrcCache = new WeakMap(); // entity -> [{ Comp, Path: [K1, K2, K3] }] @@ -541,7 +596,7 @@ return V; } - function ScanMeshNodes(tank, nodes, stats, Seen) { + function ScanMeshNodes(tank, nodes, stats, Seen, TankPos) { Seen = Seen || new WeakSet(); // ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件 ScanTick++; @@ -552,6 +607,7 @@ for (var c = 0; c < Cached.length; c++) { var V0 = ResolveSrc(Cached[c]); if (!IsSceneNode(V0)) { AllOk = false; break; } + if (!IsNodeNearPoint(V0, TankPos)) continue; // 缓存根跑远了(不是这辆车的)→ 不画 if (!Seen.has(V0)) CollectMeshNodes(V0, nodes, 0, 'scan', stats, Seen); } if (AllOk) return; @@ -560,6 +616,8 @@ var Srcs = []; function Try(V, Comp, Path) { if (!IsSceneNode(V) || Seen.has(V)) return; + // 归属闸门:盲扫可能翻到共享场景根/周围物件,按「是否在车体附近」剔除 + if (!IsNodeNearPoint(V, TankPos)) { if (stats) stats.farRejected = (stats.farRejected || 0) + 1; return; } CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); Srcs.push({ Comp: Comp, Path: Path }); } @@ -614,25 +672,25 @@ // ===== 提取坦克的mesh数据 ===== // 采集策略:**单一路径优先**,绝不把两条路径的结果混在一起(混在一起就是"复制身体") // ① 皮肤组件 h159_1(实际渲染树根)→ ② 部件表 → ③ 组件字段扫描(兜底) - function GetTankMeshData(tank, stats) { + function GetTankMeshData(tank, stats, TankPos) { var nodes = []; var Seen = new WeakSet(); var skinComp = GetSkinComponent(tank); - var skin = (skinComp && skinComp.g159_1 && skinComp.g159_1.g17r_1) ? skinComp.g159_1 : null; + var skin = (skinComp && skinComp[FSkinHullSkin] && skinComp[FSkinHullSkin][FSkinParts]) ? skinComp[FSkinHullSkin] : null; if (stats) { stats.hullSource = skin ? 'g159_1' : (skinComp ? '有组件但无皮肤' : '无皮肤组件'); - stats.hullParts = (skin && typeof skin.g17r_1.e1 === 'function') ? skin.g17r_1.e1() : -1; + stats.hullParts = (skin && typeof skin[FSkinParts].e1 === 'function') ? skin[FSkinParts].e1() : -1; } // ① 主路径:车身根节点(包含全部部件节点) - if (skinComp && skinComp.h159_1) { - try { CollectMeshNodes(skinComp.h159_1, nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} + if (skinComp && skinComp[FSkinHullRoot]) { + try { CollectMeshNodes(skinComp[FSkinHullRoot], nodes, 0, 'hull', stats ? stats.hull : null, Seen); } catch (e) {} } // ② 根节点采不到才退回部件表 if (nodes.length === 0 && skin) { try { - ForEachInList(skin.g17r_1, function (part) { + ForEachInList(skin[FSkinParts], function (part) { if (!part) return; - var j = part.j17r_1; // 新版:单个 BP 节点;旧版:节点列表 + var j = part[FSkinPartNode]; // 新版:单个 BP 节点;旧版:节点列表 if (!j) return; if (typeof j === 'object' && typeof j.e1 === 'function' && typeof j.g1 === 'function') { ForEachInList(j, function (bp) { @@ -646,7 +704,7 @@ } // ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分) // 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体" - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen, TankPos); } catch (e) {} // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; @@ -1249,9 +1307,11 @@ var MeshStats = { hullSource: '', hullParts: 0, turretComp: false, turretRoot: false, hull: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, - turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 } + turret: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0 }, + scan: { seen: 0, hasMesh: 0, gpuOk: 0, gpuFail: 0, farRejected: 0 } }; - var meshNodes = GetTankMeshData(tank, MeshStats); + var meshNodes = GetTankMeshData(tank, MeshStats, pos); + meshNodes = FilterOwnedNodes(meshNodes, pos); // 只保留跟着这辆车动的节点(挡掉非坦克物件) if (!meshNodes || meshNodes.length === 0) { LogTankDiag(tankName, tankState, MeshStats, 0, 0, 0); DrawTankMarker(ctx, pos, camInfo, screenW, screenH, color); // 兜底:至少可见 diff --git a/tools/hornet_lib.js b/tools/hornet_lib.js index ade4e08..43206e4 100644 --- a/tools/hornet_lib.js +++ b/tools/hornet_lib.js @@ -13,6 +13,18 @@ // ===== 缓存队伍判断函数(ZCt/m2Fn) ===== var ZCt = null, m2Fn = null; + // ===== 字段抽象层引用(字段名统一在 hornet_schema.js 里维护) ===== + // 取不到抽象层时退回本地常量,保证单独加载本文件也能工作 + var Schema = window.__hornetSchema__ || null; + function FieldOf(Key, Fallback) { + if (!Schema) return Fallback; + var P = Schema.PathOf(Key); + return (P && P.indexOf('.') < 0 && P.indexOf('(') < 0) ? P : Fallback; + } + var TEAM_FIELD = FieldOf('Team.Field', 'b135_1'); // 队伍组件上的队伍对象字段 + var TEAM_ID_FIELD = FieldOf('Team.Id', 'k3_1'); // 队伍编号:0/1/2(2=中立) + var NEUTRAL_TEAM_ID = 2; + // 加载队伍判断函数(从 module_87072 导出) // 新旧差异(main.7cfa4058): // 取队伍 $_$.vd = WIt(t) → g$(HIt(), t).b135_1(坦克无队伍时返回中立 q$()) @@ -256,80 +268,92 @@ // 只在「本地坦克实体变了」(换局/换车)时才重新缓存。 var LocalTeam = null; var LocalTeamTankId = null; + + // 读队伍对象:优先引擎导出函数 WIt($_$.vd),失败则直接读队伍组件字段 b135_1 + // WIt(t) 的实现就是 g$(HIt(), t).b135_1 —— 所以直接读组件字段与引擎同源, + // 导出键漂移时这条路仍然有效 + function ReadTeamRaw(tank) { + if (!tank) return null; + if (!ZCt) LoadTeamFunctions(); + if (ZCt) { + try { var T = ZCt(tank); if (T) return T; } catch (e) {} + } + var Comp = FindComponentByField(tank, TEAM_FIELD); + return Comp ? Comp[TEAM_FIELD] : null; + } + + // 读队伍编号:0 / 1 = 两个阵营,2 = 中立(q$());读不到返回 -1 + // ⚠ 用数值编号比较,而不是引擎 equals/R7:编号是稳定语义 + // (module_87072 里 P7()/$ct() 都按 k3_1 分派) + function GetTeamId(Team) { + if (!Team) return -1; + var Id = Team[TEAM_ID_FIELD]; + return (typeof Id === 'number') ? Id : -1; + } + + function GetLocalTeamId() { return GetTeamId(LocalTeam); } + function GetTankTeamId(tank) { return GetTeamId(ReadTeamRaw(tank)); } + function RememberLocalTeam(gm) { - if (!ZCt || !m2Fn) LoadTeamFunctions(); - if (!ZCt) return; var t = null; try { t = gm.u10b_1; } catch (e) { t = null; } if (t) { var Id = t.iz1_1; if (LocalTeam && LocalTeamTankId === Id) return; - try { - var Team = ZCt(t); - if (Team && (!m2Fn || m2Fn(Team, Team))) { LocalTeam = Team; LocalTeamTankId = Id; } - } catch (e) {} + var Team = ReadTeamRaw(t); + var TeamId = GetTeamId(Team); + if (TeamId >= 0 && TeamId !== NEUTRAL_TEAM_ID) { LocalTeam = Team; LocalTeamTankId = Id; } return; } // u10b_1 为空(死亡/观战):保持已有缓存;若还没缓存过,再试 GameMode 上的本地队伍字段 if (LocalTeam) return; try { var G = gm.v10b_1; - if (G && (!m2Fn || m2Fn(G, G))) LocalTeam = G; + var GId = GetTeamId(G); + if (GId >= 0 && GId !== NEUTRAL_TEAM_ID) LocalTeam = G; } catch (e) {} } // ===== 判断是否友方 ===== - // 队伍同步有延迟:坦克刚出现时队伍是「中立」,直接比较会先判成敌人、同步后再变队友(颜色跳变)。 - // 处理:① 每辆坦克缓存最近一次**非中立**队伍;② 无法判定时沿用上一次判定结果。 - var TankTeamCache = new WeakMap(); // tank -> 非中立队伍对象 + // 规则:队伍编号相同且非中立 → 友方;中立/未下发 → 沿用该坦克上次判定(默认敌人)。 + // ⚠ 不再「一次判定永久锁定」:锁定会在基准短暂错误时把整局颜色固化错, + // 这里改成每帧重算 + 中立抖动时沿用上次结果,既不会闪烁也不会锁死。 var TankFriendCache = new WeakMap(); // tank -> 上一次的友方判定 - var TeamLockLogged = new WeakSet(); - function ResolveTankTeam(tank) { - if (!ZCt) return null; - var Team = null; - try { Team = ZCt(tank); } catch (e) { Team = null; } - if (Team && (!m2Fn || m2Fn(Team, Team))) { // 同队且非中立 = 有效队伍 - TankTeamCache.set(tank, Team); - return Team; - } - return TankTeamCache.get(tank) || null; - } + var TeamJudgeLogged = new WeakSet(); function IsFriendly(tank, playerTank, gm) { if (!tank) return false; - // 队伍函数可能因为工具早于游戏模块加载而没取到,这里按需补加载 - if (!ZCt || !m2Fn) LoadTeamFunctions(); if (gm) RememberLocalTeam(gm); - // 一旦在「基准队伍可靠」时判过,就锁定本局结果,战中不再改变 - var Locked = TankFriendCache.get(tank); - if (Locked !== undefined) return Locked; - if (ZCt && m2Fn) { - try { - var tankTeam = ResolveTankTeam(tank); - // 可靠基准(用于锁定):本地拥有坦克的队伍 / GameMode 上的本地队伍字段 - var LockBase = LocalTeam; - if (!LockBase && gm) { try { LockBase = gm.v10b_1; } catch (e) {} } - // 观战目标只作临时参考,绝不用它锁定(它可能是敌人) - var BaseTeam = LockBase || (playerTank ? ResolveTankTeam(playerTank) : null); - if (tankTeam && BaseTeam) { - var Friendly = m2Fn(tankTeam, BaseTeam); - if (LockBase) { - TankFriendCache.set(tank, Friendly); - if (Friendly && !TeamLockLogged.has(tank)) { - TeamLockLogged.add(tank); - try { - console.log('[敌我] 锁定为友方 id=' + tank.iz1_1 + ' name=' + (GetTankName(tank) || '?')); - } catch (e) {} - } - } - return Friendly; - } - } catch(e) {} + + var TankId = GetTeamId(ReadTeamRaw(tank)); + if (TankId < 0 || TankId === NEUTRAL_TEAM_ID) { + var Last = TankFriendCache.get(tank); // 队伍还没同步:保持上一帧颜色 + return Last === undefined ? false : Last; } - // 基准还没就绪:先按敌人显示,但**不锁定**,等基准可用后再定 - return false; + + // 可靠基准:本地拥有坦克的队伍(缓存)→ GameMode 上的本地队伍字段 + // 观战目标(t10b_1)可能是敌人,绝不作为基准 + var BaseTeam = LocalTeam; + if (!BaseTeam && gm) { try { BaseTeam = gm.v10b_1; } catch (e) {} } + var BaseId = GetTeamId(BaseTeam); + if (BaseId < 0 || BaseId === NEUTRAL_TEAM_ID) { + var Last2 = TankFriendCache.get(tank); // 基准不可靠(极早期):沿用上次 + return Last2 === undefined ? false : Last2; + } + + var Friendly = (TankId === BaseId); + TankFriendCache.set(tank, Friendly); + if (!TeamJudgeLogged.has(tank)) { + TeamJudgeLogged.add(tank); + try { + console.log('[敌我] id=' + tank.iz1_1 + ' 队伍=' + TankId + ' 本地=' + BaseId + + ' → ' + (Friendly ? '友方' : '敌方') + ' name=' + (GetTankName(tank) || '?')); + } catch (e) {} + } + return Friendly; } + // ===== 获取相机信息(场景主相机 scene.iyx_1) ===== // 新旧差异:旧版是找挂在实体上的相机组件(kym_1/lym_1),新版这些字段已被别的类占用; // 新版直接暴露主相机对象:gm.lz4().jz1_1(场景).iyx_1(相机,类 Jf/lc) @@ -616,6 +640,10 @@ IsBot: IsBot, GetPlayerTank: GetPlayerTank, IsFriendly: IsFriendly, + ReadTeamRaw: ReadTeamRaw, + GetTeamId: GetTeamId, + GetTankTeamId: GetTankTeamId, + GetLocalTeamId: GetLocalTeamId, GetCameraInfo: GetCameraInfo, GetUMtComponent: GetUMtComponent, MkVec: MkVec, diff --git a/tools/hornet_noclip.js b/tools/hornet_noclip.js index 89843aa..cf31843 100644 --- a/tools/hornet_noclip.js +++ b/tools/hornet_noclip.js @@ -28,6 +28,24 @@ // 坦克 category 位(bit0) var TANK_CATEGORY_BIT = 1; + // ===== 字段抽象层(语义名 → 匿名字段,总表在 hornet_schema.js)===== + var Schema = window.__hornetSchema__ || null; + function Fld(Key, Fallback) { + var N = Schema ? Schema.FieldName(Key) : null; + return N || Fallback; + } + function Mth(Key, Fallback) { + var N = Schema ? Schema.MethodName(Key) : null; + return N || Fallback; + } + var FPhysicsHull = Fld('Physics.Hull', 'zzq_1'); // 车体主碰撞形状 + var FShapeName = Fld('Shape.Name', 'x9j_1'); // 形状名('hull-main') + var FShapeMask = Fld('Shape.Mask', 'z9j_1'); // 碰撞掩码 + var FBodyShapes = 'y9i_1'; // 刚体上的形状列表(pbk().y9i_1) + var MAddMask = Mth('Physics.AddMask', 'n155'); + var MRemoveMask = Mth('Physics.RemoveMask', 'm155'); + var MGetBody = Mth('Physics.Body', 'pbk'); + var Stat = { running: false, shapes: 0, // 本帧处理的碰撞形状数 @@ -42,10 +60,10 @@ // 特征:zzq_1 是车体主碰撞形状(x9j_1 === "hull-main"),且带 n155 / m155 / pbk function IsTankPhysics(Comp) { if (!Comp) return false; - if (typeof Comp.n155 !== 'function' || typeof Comp.m155 !== 'function') return false; - if (typeof Comp.pbk !== 'function') return false; - var Hull = Comp.zzq_1; - return !!(Hull && Hull.x9j_1 === 'hull-main'); + if (typeof Comp[MAddMask] !== 'function' || typeof Comp[MRemoveMask] !== 'function') return false; + if (typeof Comp[MGetBody] !== 'function') return false; + var Hull = Comp[FPhysicsHull]; + return !!(Hull && Hull[FShapeName] === 'hull-main'); } // 在实体上找坦克物理组件(不走 FindComponentByField:zzq_1 字段名在其它类里也有复用) @@ -77,7 +95,7 @@ function ReleaseSnapshot() { MaskSnapshot.forEach(function (Mask, Shape) { try { - if (Shape && typeof Mask === 'number') Shape.z9j_1 = Mask; + if (Shape && typeof Mask === 'number') Shape[FShapeMask] = Mask; } catch (e) {} }); MaskSnapshot.clear(); @@ -100,8 +118,8 @@ } var Body = null; - try { Body = Phys.pbk(); } catch (e) { Body = null; } - var Shapes = Body && Body.y9i_1; + try { Body = Phys[MGetBody](); } catch (e) { Body = null; } + var Shapes = Body && Body[FBodyShapes]; if (!Shapes || typeof Shapes.e1 !== 'function' || typeof Shapes.g1 !== 'function') return; var Count = Shapes.e1() | 0; @@ -109,10 +127,10 @@ for (var i = 0; i < Count; i++) { var Shape = null; try { Shape = Shapes.g1(i); } catch (e) { Shape = null; } - if (!Shape || typeof Shape.z9j_1 !== 'number') continue; + if (!Shape || typeof Shape[FShapeMask] !== 'number') continue; // 首次见到该形状时记录原始 mask,保证 Stop 能精确还原 - if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape.z9j_1); - Shape.z9j_1 = Shape.z9j_1 & ~TANK_CATEGORY_BIT; + if (!MaskSnapshot.has(Shape)) MaskSnapshot.set(Shape, Shape[FShapeMask]); + Shape[FShapeMask] = Shape[FShapeMask] & ~TANK_CATEGORY_BIT; Touched++; } Stat.shapes = Touched; diff --git a/tools/hornet_schema.js b/tools/hornet_schema.js new file mode 100644 index 0000000..9c2257e --- /dev/null +++ b/tools/hornet_schema.js @@ -0,0 +1,249 @@ +// hornet_schema.js — 引擎匿名字段抽象层(字段映射总表) +// +// 为什么需要它: +// 游戏每个构建版本都会把类的字段名整体平移(如 ejt 的 zzq_1 / bzr_1), +// 工具里到处硬编码 `comp.zzq_1`、`tank.qz1_1.s2a_1` 这类匿名名, +// 版本一换就要全局搜替换,还容易改错(同名复用极严重)。 +// 这里把「语义名 → 匿名字段路径」集中成一张表: +// · 工具只用语义名(Schema.Get(tank, 'Tank.StateValue')) +// · 换版本时只改这张表,并跑一次 SelfCheck() 看哪几项失配 +// +// 路径语法: +// 'a.b.c' 属性链 +// 'lz4()' 方法调用(末尾带括号即调用,中间也可混用,如 'm10b_1.j11y()') +// +// 用法: +// var S = window.__hornetSchema__; +// S.Get(gm, 'GameMode.TankList') +// S.Get(tank, 'Tank.StateValue') +// S.Check(gm, 'GameMode') // → { Ok: [...], Missing: [...] } +// S.SelfCheck() // 用当前战局对象自检并打印 +// +// 版本:main.7cfa4058(字段核对自 module_87072 / 61378 / 92980 / 55088) + +(function () { + 'use strict'; + + // ===== 字段映射总表 ===== + // 命名规则:<对象类型>.<语义名> + var Fields = { + // ---- 战斗实体基类(fe / BattleEntity)---- + 'Entity.Id': 'iz1_1', // 实体 ID(坦克/战场唯一) + 'Entity.Components': 'qz1_1.s2a_1', // Map<组件标识, 组件列表>,取值要 valueList.g1(0) + + // ---- GameMode(ght,window.__gameMode__)---- + 'GameMode.Scene': 'lz4()', // 场景实体 + 'GameMode.TankList': 'm10b_1.j11y()', // 坦克列表(e1() 数量 / g1(i) 取) + 'GameMode.OwnedTank': 'u10b_1', // 本地玩家拥有的坦克(onTankPossessed) + 'GameMode.ObservedTank': 't10b_1', // 当前观察目标(死亡/观战时可能指向敌人) + 'GameMode.LocalTeam': 'v10b_1', // 本地队伍(与 u10b_1 同生命周期) + + // ---- 坦克组件(lxt,含状态机)---- + 'Tank.Fsm': 'p10c_1', // 状态机字段(组件识别用) + 'Tank.State': 'k10c_1', // 状态枚举对象 + 'Tank.StateValue': 'k10c_1.k3_1', // 状态数值:0=DEAD 1=DEAD_PHANTOM 2=SEMI_ACTIVE 3=ACTIVE 4=DISABLED 5=TELEPORT + 'Tank.IsAlive': 's14j()', // 引擎可见性判定(2/3/5) + 'Tank.TeamSource': 'l10c_1', // 状态机里下发的队伍对象 + + // ---- 队伍组件(GIt)与队伍对象 ---- + 'Team.Field': 'b135_1', // 队伍组件上保存队伍对象的字段 + 'Team.Id': 'k3_1', // 队伍编号:0 / 1 / 2(2 = 中立) + 'Team.NeutralId': 2, + + // ---- 坦克物理组件(ejt,组件名 tankPhysics)---- + 'Physics.Position': 'bzr_1', // 世界位置向量(f20_1/g20_1/h20_1) + 'Physics.Size': 'xzq_1', // 车体半尺寸(**不是位置**) + 'Physics.Hull': 'zzq_1', // 车体主碰撞形状(x9j_1 === 'hull-main') + 'Physics.Body': 'pbk()', // 物理刚体(jM) + 'Physics.AddMask': 'n155()', // z9j_1 |= t + 'Physics.RemoveMask': 'm155()', // z9j_1 &= ~t + 'Physics.BodyShapes': 'pbk().y9i_1', // 刚体上的形状列表(e1()/g1(i)) + + // ---- 碰撞形状(yi)---- + 'Shape.Name': 'x9j_1', + 'Shape.Category': 'y9j_1', // bit0=坦克(1) bit3=地雷(8) bit4=地形(16) + 'Shape.Mask': 'z9j_1', // 愿意碰撞的分类位 + 'Shape.Ghost': 'g9k_1', // true = 只探测不产生碰撞响应 + + // ---- 血量组件(Vkt)---- + 'Health.Max': 'p12o_1', + 'Health.Current': 'q12o_1', + + // ---- 用户信息组件(yot)---- + 'User.Name': 'c12n_1', + 'User.Clan': 'e12n_1', + + // ---- 装备分组件(BattleGearScoreCC)---- + 'Gear.Score': 'cjf_1', + + // ---- 皮肤组件(eUt)---- + 'Skin.HullSkin': 'g159_1', // 皮肤对象 + 'Skin.HullRoot': 'h159_1', // 车身渲染树根(BP 节点) + 'Skin.Parts': 'g159_1.g17r_1', // 部件表 + 'Skin.PartNode': 'j17r_1', // 部件 → BP 节点 + + // ---- 炮塔皮肤组件(LUt)---- + 'Turret.Wrapper': 'a11p_1', // 组件首字段(包装对象) + 'Turret.Root': 'e11p_1.a17q_1', // 炮塔 BP 根节点 + 'Turret.GetRoot': 'u11p()', + + // ---- BP 场景节点(zl)---- + 'Node.Name': 'g35_1', + 'Node.Matrix': 'p35_1', // 世界矩阵(列主序,[12][13][14] = 平移) + 'Node.Children': 'r35_1', + 'Node.Mesh': 'v3m_1', + 'Node.Lod': 'u4x_1', + + // ---- 相机(场景实体 .jz1_1.iyx_1)---- + 'Camera.SceneRef': 'jz1_1', + 'Camera.Main': 'jz1_1.iyx_1', + 'Camera.Position': 'jz1_1.iyx_1.t35_1', + 'Camera.ViewMatrix': 'jz1_1.iyx_1.d37_1', + 'Camera.ClipMatrix': 'jz1_1.iyx_1.g37_1', + 'Camera.Fov': 'jz1_1.iyx_1.b37_1' + }; + + // ===== module_87072 的导出键($_.xx),每版都可能平移 ===== + // 队伍:vd = WIt(t) 取实体队伍;dl = R7(a,b) 同队且非中立 + // ⚠ 旧版是 kd / rk;新版 kd 已指向无关构造器 + var Exports = { + 'Export.TeamOf': ['vd', 'kd'], + 'Export.TeamSame': ['dl', 'rk'] + }; + + // ===== 路径解析 ===== + function IsCall(Token) { + var N = Token.length; + return N > 2 && Token.charAt(N - 1) === ')' && Token.charAt(N - 2) === '('; + } + + function ResolvePath(Target, Path) { + if (Target === null || Target === undefined || !Path) return undefined; + var Tokens = String(Path).split('.'); + var Cur = Target; + for (var i = 0; i < Tokens.length; i++) { + if (Cur === null || Cur === undefined) return undefined; + var Tok = Tokens[i]; + if (IsCall(Tok)) { + var Name = Tok.slice(0, -2); + if (typeof Cur[Name] !== 'function') return undefined; + try { Cur = Cur[Name](); } catch (e) { return undefined; } + } else { + Cur = Cur[Tok]; + } + } + return Cur; + } + + // 取字段值:Key 是总表键(如 'Tank.StateValue'),未命中则把 Key 当字面路径 + function Get(Target, Key) { + var Path = Fields[Key]; + return ResolvePath(Target, Path !== undefined ? Path : Key); + } + + // 取字段路径(字符串),用于打印/自检 + function PathOf(Key) { + return Fields[Key] !== undefined ? Fields[Key] : String(Key); + } + + // 取单段属性名:'Node.Matrix' → 'p35_1';带方法或多段时返回 null + // 工具里写成 `Node[FieldName('Node.Matrix') || 'p35_1']`,即「优先用总表,缺表时用字面名」 + function FieldName(Key) { + var P = Fields[Key]; + if (!P || P.indexOf('.') >= 0 || IsCall(P)) return null; + return P; + } + + // 取单段方法名:'Physics.RemoveMask' → 'm155' + function MethodName(Key) { + var P = Fields[Key]; + if (!P || !IsCall(P) || P.indexOf('.') >= 0) return null; + return P.slice(0, -2); + } + + // 自检:按前缀检查某对象上哪些语义字段能解析 + function Check(Target, Prefix) { + var Out = { Ok: [], Missing: [] }; + Object.keys(Fields).forEach(function (Key) { + if (Prefix && Key.indexOf(Prefix + '.') !== 0) return; + var V = Get(Target, Key); + if (V === undefined) Out.Missing.push(Key + ' → ' + Fields[Key]); + else Out.Ok.push(Key + ' → ' + Fields[Key]); + }); + return Out; + } + + // 用当前战局对象做一次全量自检(控制台调用 __hornetSchema__.SelfCheck()) + function SelfCheck() { + var Gm = window.__gameMode__; + var Report = { HasGameMode: !!Gm, Groups: {}, Missing: [] }; + if (!Gm) return Report; + + var Scene = Get(Gm, 'GameMode.Scene'); + var TankList = Get(Gm, 'GameMode.TankList'); + var Tank = null; + try { + Tank = Get(Gm, 'GameMode.OwnedTank'); + if (!Tank && TankList && typeof TankList.e1 === 'function' && TankList.e1() > 0) Tank = TankList.g1(0); + } catch (e) {} + + var Targets = { + Entity: Tank || Scene, + GameMode: Gm, + Tank: Tank, + Physics: Tank ? FindPhysics(Tank) : null, + Shape: null, + Skin: Tank, + Turret: Tank, + Node: null, + Camera: Scene + }; + + Object.keys(Targets).forEach(function (Prefix) { + if (!Targets[Prefix]) { Report.Groups[Prefix] = '无样本对象'; return; } + var R = Check(Targets[Prefix], Prefix); + Report.Groups[Prefix] = R; + R.Missing.forEach(function (M) { Report.Missing.push(Prefix + '.' + M); }); + }); + + if (Report.Missing.length) { + console.warn('[字段抽象] 失配 ' + Report.Missing.length + ' 项:', Report.Missing); + } else { + console.log('[字段抽象] 全部语义字段解析正常'); + } + return Report; + } + + // 找坦克物理组件(与工具内实现同源,放这里方便自检) + function FindPhysics(Tank) { + if (!Tank) return null; + var Map = Get(Tank, 'Entity.Components'); + if (!Map || typeof Map.forEach !== 'function') return null; + var Found = null; + Map.forEach(function (ValueList) { + if (Found) return; + var Comp = (ValueList && typeof ValueList.g1 === 'function') ? ValueList.g1(0) : ValueList; + if (!Comp) return; + var Hull = Get(Comp, 'Physics.Hull'); + if (Hull && Get(Hull, 'Shape.Name') === 'hull-main') Found = Comp; + }); + return Found; + } + + // ===== 导出 ===== + window.__hornetSchema__ = { + Version: 'main.7cfa4058', + Fields: Fields, + Exports: Exports, + Get: Get, + PathOf: PathOf, + FieldName: FieldName, + MethodName: MethodName, + Check: Check, + SelfCheck: SelfCheck, + ResolvePath: ResolvePath, + FindPhysics: FindPhysics + }; + + console.log('[3DTank] 字段抽象层已加载(版本 main.7cfa4058,语义字段 ' + Object.keys(Fields).length + ' 项)'); +})();