From b8651b4a8a0e180ef87f259dc1c6530000b2f52a Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 21 Oct 2026 04:03:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(protocol):=20=E5=85=A8=E9=87=8F=20opt=20?= =?UTF-8?q?=E6=89=93=E7=82=B9=E6=8C=89=E5=B8=A7=E5=88=86=E7=BB=84=EF=BC=8C?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E6=AF=8F=E4=B8=AA=E6=A8=A1=E5=9E=8B=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E7=9A=84=E4=BD=8D=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/tools/analyze_opt_trace.py | 86 +++++++++++++++++++ .../play/static/js/unpacked/module_24635.js | 61 +++++++------ 2 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 3DTank/tools/analyze_opt_trace.py diff --git a/3DTank/tools/analyze_opt_trace.py b/3DTank/tools/analyze_opt_trace.py new file mode 100644 index 0000000..f58058a --- /dev/null +++ b/3DTank/tools/analyze_opt_trace.py @@ -0,0 +1,86 @@ +# analyze_opt_trace.py - 分析客户端「全量 opt 读取」打点,按帧还原每个模型消费了几位 +# +# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁。每条记录: +# rid = 位读取器实例 id(同一帧共享 → 同 rid 即同帧) +# pos = 读位前的绝对位索引;bit = 读到的位 +# slots = 内层读器槽位名(模型字段);elemKeys = 列表元素读器槽位名 +# 槽位名全局唯一,所以能用 names/codec_fields.json 的 Slots 反查出具体模型。 +# +# 用法: python tools/analyze_opt_trace.py [--names names] [--only bs,Tu,tu] +import json +import os +import sys +from collections import defaultdict + +Root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def Arg(Name, Def): + if '--' + Name in sys.argv: + I = sys.argv.index('--' + Name) + if I + 1 < len(sys.argv): + return sys.argv[I + 1] + return Def + + +NamesDir = os.path.abspath(Arg('names', os.path.join(Root, 'names'))) +TracePath = os.path.join(NamesDir, 'opt_trace.json') +FieldsPath = os.path.join(NamesDir, 'codec_fields.json') + + +def LoadSlotIndex(): + with open(FieldsPath, 'r', encoding='utf-8') as F: + Codecs = json.load(F).get('Codecs') or {} + Index = {} + for Key, C in Codecs.items(): + Slots = tuple(sorted(S.get('Slot') for S in (C.get('Slots') or []))) + if Slots and Slots not in Index: + Index[Slots] = Key + return Index + + +def Main(): + if not os.path.exists(TracePath): + print('缺少 ' + TracePath) + print('请先用镜像站进一局战斗(Ht.t7h 已打全量 opt 打点)。') + return 1 + with open(TracePath, 'r', encoding='utf-8') as F: + Data = json.load(F) + Rows = Data.get('trace') or [] + print('opt 打点:%d 条(采集于 %s)' % (len(Rows), Data.get('at', '?'))) + + Index = LoadSlotIndex() + Only = [S for S in (Arg('only', '') or '').split(',') if S] + + Frames = defaultdict(list) + for R in Rows: + Frames[R.get('rid')].append(R) + + print(' 帧数:%d' % len(Frames)) + Shown = 0 + for Rid in sorted(Frames, key=lambda X: (X is None, X)): + Seq = Frames[Rid] + Items = [] + Names = [] + for R in Seq: + Slots = tuple(sorted(R.get('slots') or [])) + Key = Index.get(Slots) + if not Key: + ElemKeys = tuple(sorted(R.get('elemKeys') or [])) + Key = ('list<' + (Index.get(ElemKeys) or '?') + '>') if ElemKeys else ('?' + str(Slots)) + Short = Key.split('::')[-1] + Names.append(Short) + Items.append('%s@%s=%s' % (Short, R.get('pos'), R.get('bit'))) + if Only and not any(N in Only for N in Names): + continue + Shown += 1 + if Shown > 12: + continue + print(' rid=%s(%d 位):%s' % (Rid, len(Seq), ' '.join(Items))) + if Shown > 12: + print(' …还有 %d 帧匹配' % (Shown - 12)) + return 0 + + +if __name__ == '__main__': + sys.exit(Main()) diff --git a/3dtank.com/play/static/js/unpacked/module_24635.js b/3dtank.com/play/static/js/unpacked/module_24635.js index ef9cb69..8407e29 100644 --- a/3dtank.com/play/static/js/unpacked/module_24635.js +++ b/3dtank.com/play/static/js/unpacked/module_24635.js @@ -413,26 +413,29 @@ null == n ? t.f7h_1.b7h(!0) : (t.f7h_1.b7h(!1), this.r7h_1.s7h(t, n)); }), (z(Ht).t7h = function (t) { - // [Hornet 调试] 可选读器的位痕迹采样:只对「可选 + 列表」这类罕见的组合记录 - // (判据:内层读器带列表槽位 z7h_1),把位值、内层读器类名、调用栈节流 POST 到 - // /__dump__/nullmap_trace.json,用于确认 opt> 在真实数据里到底占不占位。 - // 位只读一次:采样失败也只是少一条日志,绝不改变解码结果。 + // [Hornet 调试] 可选读器打点(全量 opt,按帧分组):记录 + // rid = 位读取器实例 id(一帧内所有记录共享同一个位读取器 → 同一 rid 即同一帧) + // pos = 读位前的绝对位索引;bit = 读到的位 + // slots = 内层读器的槽位名(模型字段 → 槽位名全局唯一,可离线反查是哪个模型) + // 目的:直接看客户端对「某个模型」到底消费了几个 opt 位,从而定位我们少算的那一位。 + // 位只读一次;打点异常不影响解码。 var Inner = this.r7h_1; - if (Inner && Inner.z7h_1 !== undefined) { - var B0 = t.f7h_1.xy(); + var BR = t && t.f7h_1; + if (BR && typeof BR.xy === "function" && BR.y7g_1 !== undefined) { + var B0 = BR.xy(); try { if (typeof window !== "undefined") { - var T = (window.__HornetBitTrace__ = window.__HornetBitTrace__ || { - rows: [], sent: 0, timer: 0, + var T0 = (window.__HornetOptTrace__ = window.__HornetOptTrace__ || { + rows: [], timer: 0, nextRid: 1, push: function (Row) { - if (this.rows.length >= 4000) return; + if (this.rows.length >= 8000) return; this.rows.push(Row); if (!this.timer) { var Me = this; this.timer = setTimeout(function () { Me.timer = 0; try { - fetch("/__dump__/nullmap_trace.json", { + fetch("/__dump__/opt_trace.json", { method: "POST", body: JSON.stringify({ at: new Date().toISOString(), count: Me.rows.length, trace: Me.rows }), }); @@ -441,30 +444,26 @@ } }, }); - var St = ""; - try { - St = String(new Error().stack || "").split("\n").slice(1, 4).join(" | "); - } catch (E3) {} - // hid:给每个可选包装器实例分配稳定 id(同一字段的多次读取共享 id); - // elem/elemKeys:内层列表的元素 codec 类名与槽位名——槽位名全局唯一,可离线唯一定位字段。 - var Elem = Inner.z7h_1; - if (!Inner.__hornetHid) { - Inner.__hornetHid = T.nextId || 1; - T.nextId = Inner.__hornetHid + 1; - } - var ElemName = ''; + var Slots = []; var ElemKeys = []; try { - ElemName = (Elem && Elem.constructor && Elem.constructor.name) || ''; - ElemKeys = Elem ? Object.keys(Elem) : []; + Slots = Inner ? Object.keys(Inner) : []; + ElemKeys = Inner && Inner.z7h_1 ? Object.keys(Inner.z7h_1) : []; } catch (E5) {} - T.push({ - hid: Inner.__hornetHid, bit: B0, inner: Inner.constructor && Inner.constructor.name, - elem: ElemName, elemKeys: ElemKeys, - // pos = 客户端位读取器的绝对游标(Et.y7g_1,读之前)。用来和我们离线解的位游标对比 - pos: (t.f7h_1 && typeof t.f7h_1.y7g_1 === "number") ? t.f7h_1.y7g_1 : -1, - stack: St, - }); + // 只记「能唯一定位」的:模型槽位 或 列表元素槽位;基础类型 opt 太碎且无指纹,跳过 + if (Slots.length > 0 || ElemKeys.length > 0) { + if (!BR.__hornetRid) { + BR.__hornetRid = T0.nextRid || 1; + T0.nextRid = BR.__hornetRid + 1; + } + T0.push({ + rid: BR.__hornetRid, + pos: BR.y7g_1 - 1, + bit: B0, + slots: Slots, + elemKeys: ElemKeys, + }); + } } } catch (E4) {} return B0 ? null : Inner.t7h(t);