100 lines
4.3 KiB
Python
100 lines
4.3 KiB
Python
# analyze_nullmap_trace.py - 把客户端采到的「可选 + 列表」位痕迹定位到具体字段
|
||
#
|
||
# 数据来源:镜像站 module_24635.js 的 Ht.t7h 补丁(可选读器)。它只对「内层读器是列表」的可选字段
|
||
# 记录 {hid, bit, inner, elem, elemKeys, stack},节流 POST 到 serve.js 的 /__dump__/nullmap_trace.json。
|
||
# 目的:确认 opt<list<…>> 在真实数据里到底占不占 nullmap 位(例如 ParticleEffectItem.colors)。
|
||
#
|
||
# 定位方法(不依赖 stack,因为镜像站用 eval 加载模块,栈里没有模块名):
|
||
# 1. 每个可选包装器实例在客户端被分配稳定 id(hid),同一字段的所有读取共享同一个 hid;
|
||
# 2. elemKeys = 内层列表元素 codec 的槽位名集合。槽位名全局唯一,
|
||
# 用它去 names/codec_fields.json 的 Slots 里反查,能唯一确定「元素是哪个 codec」;
|
||
# 3. 再找哪些字段的类型是 opt<list<该模型>>,若唯一即定位成功。
|
||
#
|
||
# 用法: 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<list<ModelName>> 的 (codec 键, 字段名)。"""
|
||
Out = []
|
||
Needle = 'opt<list<' + ModelName + '>>'
|
||
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())
|