From e0542b2681a453037439d75b6dcf2c13879776ee Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 21 Oct 2026 02:55:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(protocol):=20=E4=BD=8D=E7=97=95=E8=BF=B9?= =?UTF-8?q?=E6=8C=89=20codec=20=E6=A7=BD=E4=BD=8D=E6=8C=87=E7=BA=B9?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/tools/analyze_nullmap_trace.py | 128 +++++++----------- .../play/static/js/unpacked/module_24635.js | 18 ++- 2 files changed, 69 insertions(+), 77 deletions(-) diff --git a/3DTank/tools/analyze_nullmap_trace.py b/3DTank/tools/analyze_nullmap_trace.py index e2e4214..329e51a 100644 --- a/3DTank/tools/analyze_nullmap_trace.py +++ b/3DTank/tools/analyze_nullmap_trace.py @@ -1,18 +1,20 @@ -# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹映射回 模块::类.方法 +# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹定位到具体字段 # -# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的 -# 可选字段记录一条 {bit, inner, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。 +# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的可选字段 +# 记录 {hid, bit, inner, elem, elemKeys, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。 # 目的:确认 opt> 在真实数据里到底占不占 nullmap 位(例如 ParticleEffectItem.colors)。 # -# 映射方法:Node 的 stack 里带 `module_XXXX.js:行:列`;用 src 里 `(<类>).<方法> = function` -# 的起始行建「行号 → 类.方法」索引,取不晚于该行的最近一条。 +# 定位方法(不依赖 stack,因为镜像站用 eval 加载模块,栈里没有模块名): +# 1. 每个可选包装器实例在客户端被分配稳定 id(hid),同一字段的所有读取共享同一个 hid; +# 2. elemKeys = 内层列表元素 codec 的槽位名集合。槽位名全局唯一, +# 用它去 names/codec_fields.json 的 Slots 里反查,能唯一确定「元素是哪个 codec」; +# 3. 再找哪些字段的类型是 opt>,若唯一即定位成功。 # -# 用法: python tools/analyze_nullmap_trace.py [--names names] [--src src] +# 用法: python tools/analyze_nullmap_trace.py [--names names] import json import os -import re import sys -from collections import Counter +from collections import Counter, defaultdict Root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -26,42 +28,33 @@ def Arg(Name, Def): NamesDir = os.path.abspath(Arg('names', os.path.join(Root, 'names'))) -SrcDir = os.path.abspath(Arg('src', os.path.join(Root, 'src'))) TracePath = os.path.join(NamesDir, 'nullmap_trace.json') - -FrameRe = re.compile(r'module_(\d+)\.js:(\d+):(\d+)') -AssignRe = re.compile(r'\(([\w$]+)\)\.(t7h|s7h|u7h)\s*=\s*function') +FieldsPath = os.path.join(NamesDir, 'codec_fields.json') -def LoadLineMap(): - """module 号 → [(起始行, '类.方法')],按行号升序。""" - Out = {} - for Name in os.listdir(SrcDir): - M = re.match(r'module_(\d+)\.js$', Name) - if not M: - continue - Mod = str(int(M.group(1))) - Path = os.path.join(SrcDir, Name) - Rows = [] - with open(Path, 'r', encoding='utf-8', errors='replace') as F: - for LineNo, Line in enumerate(F, 1): - for A in AssignRe.finditer(Line): - Rows.append((LineNo, A.group(1) + '.' + A.group(2))) - Rows.sort() - Out[Mod] = Rows +def LoadSlots(): + """槽位集合 → [(codec 键, 模型名)],用于反查元素 codec。""" + with open(FieldsPath, 'r', encoding='utf-8') as F: + Codecs = json.load(F).get('Codecs') or {} + Out = defaultdict(list) + for Key, C in Codecs.items(): + Slots = tuple(sorted(S.get('Slot') for S in (C.get('Slots') or []))) + if Slots: + Out[Slots].append((Key, C.get('ModelName') or C.get('ModelClassLocal') or '?')) + return Out, Codecs + + +def FindOwners(Codecs, ModelName): + """类型为 opt> 的 (codec 键, 字段名)。""" + Out = [] + Needle = 'opt>' + for Key, C in Codecs.items(): + for F in C.get('Fields') or []: + if F.get('Type') == Needle: + Out.append((Key, F.get('Name'))) return Out -def Locate(LineMap, Mod, Line): - Rows = LineMap.get(Mod) or [] - Best = None - for Start, Name in Rows: - if Start > Line: - break - Best = Name - return Best or '?' - - def Main(): if not os.path.exists(TracePath): print('缺少 ' + TracePath) @@ -72,46 +65,29 @@ def Main(): Rows = Data.get('trace') or [] print('位痕迹:%d 条(采集于 %s)' % (len(Rows), Data.get('at', '?'))) - LineMap = LoadLineMap() - ByCaller = Counter() - ByBit = Counter() - Samples = {} - Ordered = [] + SlotIndex, Codecs = LoadSlots() + ByHid = defaultdict(list) for R in Rows: - Bit = R.get('bit') - Stack = R.get('stack') or '' - Caller = '?' - CallerMod = '?' - # 取第一个能定位的栈帧(跳过 Ht.t7h 自己) - for M in FrameRe.finditer(Stack): - Mod, Line = M.group(1), int(M.group(2)) - Name = Locate(LineMap, str(int(Mod)), Line) - if Name.startswith('Ht.'): - continue - Caller = Name - CallerMod = 'module_' + str(int(Mod)) - break - Key = CallerMod + '::' + Caller - ByCaller[(Key, R.get('inner'))] += 1 - ByBit[(Key, R.get('inner'), Bit)] += 1 - if Key not in Samples: - Samples[Key] = Stack - Ordered.append((Key, R.get('inner'), Bit)) + ByHid[R.get('hid')].append(R) - print(' 调用者分布(按次数):') - for (Key, Inner), N in ByCaller.most_common(20): - Ones = ByBit.get((Key, Inner, 1), 0) - Zeros = ByBit.get((Key, Inner, 0), 0) - print(' %6d %-40s inner=%-6s bit=1:%d bit=0:%d' % (N, Key, Inner, Ones, Zeros)) - - # 关注 ParticleEffectItem(85183::Ll):把它的位序列按出现顺序打出来 - Focus = [(K, B) for K, I, B in Ordered if K == 'module_85183::Ll.t7h'] - if Focus: - print(' ParticleEffectItem(module_85183::Ll)位序列,前 60 条:') - print(' ' + ''.join(str(B) for _, B in Focus[:60])) - print(' 样例栈:') - for Key, S in list(Samples.items())[:3]: - print(' [' + Key + '] ' + S[:160]) + print(' 可选列表字段 %d 个(按 hid 分组):' % len(ByHid)) + for Hid in sorted(ByHid, key=lambda H: (H is None, H)): + Group = ByHid[Hid] + ElemKeys = tuple(sorted(Group[0].get('elemKeys') or [])) + Elems = SlotIndex.get(ElemKeys) or [] + ElemName = Elems[0][1] if len(Elems) == 1 else Group[0].get('elem') + Owners = FindOwners(Codecs, ElemName) if ElemName else [] + Bits = Counter(R.get('bit') for R in Group) + Seq = ''.join(str(R.get('bit')) for R in Group[:40]) + Where = (Owners[0][0] + ' . ' + str(Owners[0][1])) if len(Owners) == 1 else '?' + print(' hid=%-4s 次数=%-4d bit0=%-4d bit1=%-4d 元素=%-22s 字段=%s' + % (Hid, len(Group), Bits.get(0, 0), Bits.get(1, 0), ElemName or '?', Where)) + if len(Seq) >= 4: + print(' 位序列:' + Seq) + if len(Elems) > 1: + print(' 元素多解:' + ', '.join(K + '(' + N + ')' for K, N in Elems[:5])) + if len(Owners) > 1: + print(' 字段多解:' + ', '.join(K + '.' + str(N) for K, N in Owners[:5])) return 0 diff --git a/3dtank.com/play/static/js/unpacked/module_24635.js b/3dtank.com/play/static/js/unpacked/module_24635.js index 727dfa2..9119bc9 100644 --- a/3dtank.com/play/static/js/unpacked/module_24635.js +++ b/3dtank.com/play/static/js/unpacked/module_24635.js @@ -445,7 +445,23 @@ try { St = String(new Error().stack || "").split("\n").slice(1, 4).join(" | "); } catch (E3) {} - T.push({ bit: B0, inner: Inner.constructor && Inner.constructor.name, stack: St }); + // 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 ElemKeys = []; + try { + ElemName = (Elem && Elem.constructor && Elem.constructor.name) || ''; + ElemKeys = Elem ? Object.keys(Elem) : []; + } catch (E5) {} + T.push({ + hid: Inner.__hornetHid, bit: B0, inner: Inner.constructor && Inner.constructor.name, + elem: ElemName, elemKeys: ElemKeys, stack: St, + }); } } catch (E4) {} return B0 ? null : Inner.t7h(t);