# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹定位到具体字段 # # 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的可选字段 # 记录 {hid, bit, inner, elem, elemKeys, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。 # 目的:确认 opt> 在真实数据里到底占不占 nullmap 位(例如 ParticleEffectItem.colors)。 # # 定位方法(不依赖 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] import json import os import sys from collections import Counter, 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, 'nullmap_trace.json') FieldsPath = os.path.join(NamesDir, 'codec_fields.json') 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 Main(): if not os.path.exists(TracePath): print('缺少 ' + TracePath) print('请先用镜像站进一局战斗(module_24635.js 已打位痕迹补丁,会自动 POST)。') return 1 with open(TracePath, 'r', encoding='utf-8') as F: Data = json.load(F) Rows = Data.get('trace') or [] print('位痕迹:%d 条(采集于 %s)' % (len(Rows), Data.get('at', '?'))) SlotIndex, Codecs = LoadSlots() ByHid = defaultdict(list) for R in Rows: ByHid[R.get('hid')].append(R) 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) Pos = [R.get('pos') for R in Group if isinstance(R.get('pos'), int) and R.get('pos') >= 0] Seq = ''.join('1' if R.get('bit') else '0' 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 Pos: print(' 客户端位游标 pos:首个=%d 最后=%d 前 12 个=%s' % (Pos[0], Pos[-1], ','.join(str(P) for P in Pos[:12]))) 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 if __name__ == '__main__': sys.exit(Main())