refactor(schema): 新增字段抽象层,透视滤除非本车节点并重做敌我判定

新增 hornet_schema.js 集中维护匿名引擎字段映射与导出键,支持 SelfCheck 自检失配;透视兜底扫描增加车体距离闸门,只保留跟着本车动的节点;敌我改为按队伍编号(k3_1)每帧比对,中立时沿用上次结果,取消一次判定永久锁定;穿坦克改用抽象字段名。
This commit is contained in:
TermiNexus committed 2026-10-20 20:18:14 +08:00
1 parent 6e79057856
commit 972928e848
7 files changed
+1341 -269

No files matched your search

@@ -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;
+444 -86
View File
@@ -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;
+12 -11
View File
@@ -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 = """// ============================================================
+86 -26
View File
@@ -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); // 兜底:至少可见
+78 -50
View File
@@ -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,
+28 -10
View File
@@ -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;
+249
View File
@@ -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 + ' 项)');
})();