Files
TankIC/3DTank/tools/resolve_model_binding.py
T

202 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# resolve_model_binding.py - 用「运行时 modelId → codec 实例」dump 反查 codec 的模块归属
#
# 为什么需要这个工具:
# regdump.json 的 ModelReg.w7n_1 只给出「类短名 + codec 槽位名」。类短名在 126 个模块里
# 大量复用(Fs / hs / It 这类两字母名到处都是),而 regdump 不含模块信息,
# 所以靠「短名 + 槽位」做指纹经常多解或误绑(例:658797348514407988 被绑到 ItemModelCC,
# 真实 codec 是另一个模块里的 Fs)→ pt 项静默串位,最后读出 modelId=8 这种不存在的值。
#
# 数据来源(由镜像站自动采集,不需要手工粘控制台):
# 3dtank.com/play/static/js/unpacked/module_61860.js 的 [Hornet 调试] 补丁
# 在 ModelReg.n7o(modelId, codec) 注册时记录 codec 的 u7h/t7h/s7h 源码文本,
# 节流后 POST 到 serve.js 的 /__dump__/regdump_model.json
# → 落盘到 3DTank/names/regdump_model.json
#
# 反查方法:
# u7h(codec 的「建槽」函数)文本在每个模块里都是唯一的:把它的空白全部去掉,
# 在同样去掉空白的模块源码里搜;命中后回看最近的一个 `X(类名).u7h = function`,
# 该赋值左侧的类名就是 codec 类,文件名给出模块 id。
# u7h 缺失(继承父类)时退回 t7h。
#
# 用法: python tools/resolve_model_binding.py [--names names] [--src src]
# 输出: names/model_binding.json
import json
import os
import re
import sys
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')))
SrcDir = os.path.abspath(Arg('src', os.path.join(Root, 'src')))
DumpPath = os.path.join(NamesDir, 'regdump_model.json')
OutPath = os.path.join(NamesDir, 'model_binding.json')
# u7h/t7h 的赋值点:Se(ds).u7h = function (...) / x(lt).t7h = function (...)
# 注意类名里允许 `$`:Kotlin/JS 的局部类常被命名为 `$s`、`j_` 这类;
# 写成 \w+ 会漏掉 `$s`,从而把「最近的前一个 u7h」误当成本类(曾把 $s 误判成 bs)。
AssignRe = re.compile(r'\(([\w$]+)\)\.(u7h|t7h)\s*=\s*function')
ModuleRe = re.compile(r'module_(\d+)\.js$')
def Normalize(Text):
"""去掉全部空白,同时保留「归一化下标 → 原文下标」的映射。"""
Chars = []
Idx = []
for I, C in enumerate(Text):
if C.isspace():
continue
Chars.append(C)
Idx.append(I)
return ''.join(Chars), Idx
def LoadModules():
Out = []
for Name in sorted(os.listdir(SrcDir)):
if not ModuleRe.search(Name):
continue
Path = os.path.join(SrcDir, Name)
with open(Path, 'r', encoding='utf-8', errors='replace') as F:
Text = F.read()
Norm, Idx = Normalize(Text)
Assigned = [(M.start(), M.group(1), M.group(2)) for M in AssignRe.finditer(Text)]
Out.append({'Module': ModuleRe.search(Name).group(0)[:-3], 'Text': Text,
'Norm': Norm, 'Idx': Idx, 'Assigned': Assigned})
return Out
def OwnerAt(Mod, Offset, Want):
"""在原文 Offset 处,找出包含它的那一次 `<类>.u7h/t7h = function` 赋值。"""
Best = None
for Start, Cls, Kind in Mod['Assigned']:
if Start <= Offset and (Best is None or Start > Best[0]):
Best = (Start, Cls, Kind)
if Best is None:
return None
# 命中的函数体必须就是这次赋值(同一 class 可能先后有 u7h 和 t7h 两个赋值)
return Best[1] if Best[2] == Want else None
def Main():
if not os.path.exists(DumpPath):
print('缺少运行期 dump:' + DumpPath)
print('请先用镜像站进一次游戏(镜像 module_61860.js 已打采集补丁,会自动 POST)。')
return 1
with open(DumpPath, 'r', encoding='utf-8') as F:
Dump = json.load(F)
Entries = Dump.get('entries') or []
print('运行期 dump:%d 条 modelId 注册(采集于 %s)' % (len(Entries), Dump.get('at', '?')))
Modules = LoadModules()
print('已载入 %d 个模块源码' % len(Modules))
# codec_fields 里已有的 codec 键(module_XXXX::类名),用于判断「绑定成功但字段表缺失」
with open(os.path.join(NamesDir, 'codec_fields.json'), 'r', encoding='utf-8') as F:
Codecs = json.load(F).get('Codecs') or {}
Resolved = []
Unresolved = []
Ambiguous = []
for E in Entries:
ModelId = str(E.get('modelId'))
Hit = None
HitCount = 0
for Kind in ('u7h', 't7h'):
Sig = E.get(Kind)
if not Sig:
continue
NormSig = ''.join(Sig.split())
if len(NormSig) < 40:
continue
Hits = []
for Mod in Modules:
Pos = Mod['Norm'].find(NormSig)
if Pos < 0:
continue
Owner = OwnerAt(Mod, Mod['Idx'][Pos], Kind)
if Owner:
Hits.append((Mod['Module'], Owner))
if len(Hits) == 1:
Hit = Hits[0]
HitCount = 1
break
if len(Hits) > 1:
Ambiguous.append({'ModelId': ModelId, 'How': Kind, 'Candidates': Hits})
HitCount = len(Hits)
break
if Hit:
Module, Codec = Hit
Key = Module + '::' + Codec
# 自洽性校验:运行期 codec 实例的槽位名(keys)应与 codec_fields 里该 codec 的 Slots 一致。
# 不一致说明「源码反查落到了同名的另一个类」或字段表抽错,必须显式标出来,不能静默使用。
Slots = [S.get('Slot') for S in ((Codecs.get(Key) or {}).get('Slots') or [])]
RuntimeKeys = [str(K) for K in (E.get('keys') or [])]
SlotsOk = sorted(Slots) == sorted(RuntimeKeys)
Resolved.append({
'ModelId': ModelId,
'Module': Module,
'Codec': Codec,
'Key': Key,
'InCodecFields': Key in Codecs,
'SlotsOk': SlotsOk,
'Slots': Slots,
'RuntimeKeys': RuntimeKeys,
'Ctor': E.get('ctor'),
'Keys': E.get('keys'),
})
elif HitCount == 0:
Unresolved.append({'ModelId': ModelId, 'Ctor': E.get('ctor'), 'Keys': E.get('keys')})
MissingCodec = [R for R in Resolved if not R['InCodecFields']]
SlotMismatch = [R for R in Resolved if not R['SlotsOk']]
Out = {
'Note': '运行时 modelId → codec 的权威绑定:u7h/t7h 源码文本反查模块 + 类名;'
'InCodecFields=false 表示绑定已确定但 codec_fields 还没抽出该 codec 的字段表;'
'SlotsOk=false 表示反查到的类与运行期 codec 槽位不一致(疑似落在同名类上)。',
'Source': 'names/regdump_model.json(镜像站 module_61860.n7o 采集)',
'Count': len(Entries),
'Resolved': len(Resolved),
'MissingCodec': len(MissingCodec),
'SlotMismatch': len(SlotMismatch),
'AmbiguousCount': len(Ambiguous),
'UnresolvedCount': len(Unresolved),
'Entries': Resolved,
'MissingCodecList': MissingCodec,
'SlotMismatchList': SlotMismatch,
'Ambiguous': Ambiguous,
'Unresolved': Unresolved,
}
with open(OutPath, 'w', encoding='utf-8') as F:
json.dump(Out, F, ensure_ascii=False, indent=1)
print('已写入 ' + OutPath)
print(' 反查成功 %d / %d;其中字段表缺失 %d;槽位不一致 %d;多解 %d;未解 %d' %
(len(Resolved), len(Entries), len(MissingCodec), len(SlotMismatch), len(Ambiguous), len(Unresolved)))
if SlotMismatch:
print(' 槽位不一致(前 15):')
for R in SlotMismatch[:15]:
print(' %s %s 运行期=%s 字段表=%s' %
(R['ModelId'], R['Key'], R['RuntimeKeys'], R['Slots']))
if Ambiguous:
print(' 多解(前 5):')
for A in Ambiguous[:5]:
print(' %s ' % A['ModelId'] + ' / '.join(M + '::' + C for M, C in A['Candidates']))
if Unresolved:
print(' 未解(前 10):')
for U in Unresolved[:10]:
print(' %s ctor=%s keys=%s' % (U['ModelId'], U.get('Ctor'), U.get('Keys')))
return 0
if __name__ == '__main__':
sys.exit(Main())