feat(protocol): 采集运行时 modelId→codec 注册并用源码反查模块归属
This commit is contained in:
1 parent
76e00f7aa2
commit
fcd071485d
3 files changed
+286
No files matched your search
@@ -0,0 +1,186 @@
|
||||
# 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 (...)
|
||||
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
|
||||
Resolved.append({
|
||||
'ModelId': ModelId,
|
||||
'Module': Module,
|
||||
'Codec': Codec,
|
||||
'Key': Key,
|
||||
'InCodecFields': Key in Codecs,
|
||||
'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']]
|
||||
Out = {
|
||||
'Note': '运行时 modelId → codec 的权威绑定:u7h/t7h 源码文本反查模块 + 类名;'
|
||||
'InCodecFields=false 表示绑定已确定但 codec_fields 还没抽出该 codec 的字段表。',
|
||||
'Source': 'names/regdump_model.json(镜像站 module_61860.n7o 采集)',
|
||||
'Count': len(Entries),
|
||||
'Resolved': len(Resolved),
|
||||
'MissingCodec': len(MissingCodec),
|
||||
'AmbiguousCount': len(Ambiguous),
|
||||
'UnresolvedCount': len(Unresolved),
|
||||
'Entries': Resolved,
|
||||
'MissingCodecList': MissingCodec,
|
||||
'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' %
|
||||
(len(Resolved), len(Entries), len(MissingCodec), len(Ambiguous), len(Unresolved)))
|
||||
if MissingCodec:
|
||||
print(' 字段表缺失的 codec(前 15):')
|
||||
for R in MissingCodec[:15]:
|
||||
print(' %s %s ctor=%s' % (R['ModelId'], R['Key'], R['Ctor']))
|
||||
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())
|
||||
@@ -1663,6 +1663,81 @@
|
||||
}),
|
||||
(J(mr).n7o = function (t, n) {
|
||||
this.a7p_1.g3(t, n);
|
||||
// [Hornet 调试] modelId → codec 注册采样:把 codec 的类名/槽位/读写函数源码攒起来,
|
||||
// 节流后 POST 到本地镜像服务器的 /__dump__/regdump_model.json(serve.js 已实现落盘)。
|
||||
// 用途:离线用 u7h/t7h 源码文本反查 codec 所在模块与类,解决「短类名全局复用」的归属歧义。
|
||||
// 全程 try/catch,不影响注册本身的语义。
|
||||
try {
|
||||
var H = typeof window !== "undefined" ? window : globalThis;
|
||||
var D = H.__HornetModelRegDump__;
|
||||
if (!D) {
|
||||
D = H.__HornetModelRegDump__ = {
|
||||
entries: {},
|
||||
order: [],
|
||||
sent: 0,
|
||||
timer: 0,
|
||||
push: function (Id, Codec) {
|
||||
if (null == Id || null == Codec) return;
|
||||
var K = String(Id);
|
||||
if (this.entries[K]) return;
|
||||
var Src = function (F) {
|
||||
try {
|
||||
return F ? String(F) : null;
|
||||
} catch (E) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
var Own = [];
|
||||
var Proto = [];
|
||||
var Ctor = "";
|
||||
try {
|
||||
Own = Object.keys(Codec).slice(0, 64);
|
||||
} catch (E) {}
|
||||
try {
|
||||
Proto = Object.keys(Codec.prototype || {}).slice(0, 64);
|
||||
} catch (E) {}
|
||||
try {
|
||||
Ctor = String(Codec.constructor || "");
|
||||
} catch (E) {}
|
||||
this.entries[K] = {
|
||||
modelId: K,
|
||||
ctor: (Codec.constructor && Codec.constructor.name) || typeof Codec,
|
||||
ctorSrc: Ctor,
|
||||
keys: Own,
|
||||
protoKeys: Proto,
|
||||
u7h: Src(Codec.u7h),
|
||||
t7h: Src(Codec.t7h),
|
||||
s7h: Src(Codec.s7h),
|
||||
};
|
||||
this.order.push(K);
|
||||
if (!this.timer) {
|
||||
var Me = this;
|
||||
this.timer = setTimeout(function () {
|
||||
Me.timer = 0;
|
||||
Me.flush();
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
flush: function () {
|
||||
if (typeof fetch !== "function") return;
|
||||
var Self = this;
|
||||
var Body = JSON.stringify({
|
||||
at: new Date().toISOString(),
|
||||
href: typeof location !== "undefined" ? location.href : "",
|
||||
count: this.order.length,
|
||||
entries: this.order.map(function (K) {
|
||||
return Self.entries[K];
|
||||
}),
|
||||
});
|
||||
this.sent++;
|
||||
try {
|
||||
fetch("/__dump__/regdump_model.json", { method: "POST", body: Body });
|
||||
} catch (E) {}
|
||||
},
|
||||
};
|
||||
}
|
||||
D.push(t, n);
|
||||
} catch (E) {}
|
||||
}),
|
||||
(J(mr).o7o = function (t) {
|
||||
var n = this.a7p_1.z2(t);
|
||||
|
||||
@@ -228,6 +228,31 @@ def Main():
|
||||
|
||||
Codecs = CodecFields.get("Codecs", {})
|
||||
|
||||
# ---------- 0. 运行时权威绑定(若已采集)----------
|
||||
# codec_fields 里的 ModelId/ModelIds 是「短类名 + codec 槽位名」指纹 + 源码登记点推出来的;
|
||||
# 短类名在 126 个模块里大量复用,误绑会让 pt 项静默串位(最后读出 modelId=8 这种不存在值)。
|
||||
# names/model_binding.json 来自客户端运行时 ModelReg.n7o 注册采样 + u7h 源码反查模块,
|
||||
# 一旦存在就以它为准:只保留它给出的 modelId(其余 codec 在运行时没注册,ByModelId 里也不该有)。
|
||||
BindingPath = NAMES / "model_binding.json"
|
||||
if BindingPath.exists():
|
||||
Binding = json.loads(BindingPath.read_text("utf-8")).get("Entries") or []
|
||||
BindingOverride = {}
|
||||
for Row in Binding:
|
||||
BindingOverride.setdefault(Row["Key"], []).append(str(Row["ModelId"]))
|
||||
Dropped = 0
|
||||
for Key, C in Codecs.items():
|
||||
Ids = BindingOverride.get(Key)
|
||||
if Ids:
|
||||
C["ModelId"] = Ids[0]
|
||||
C["ModelIds"] = Ids
|
||||
else:
|
||||
if C.get("ModelId") or C.get("ModelIds"):
|
||||
Dropped += 1
|
||||
C["ModelId"] = None
|
||||
C["ModelIds"] = []
|
||||
print(f"[绑定] 采用运行时权威绑定:{len(Binding)} 条 modelId / {len(BindingOverride)} 个 codec,"
|
||||
f"丢弃 {Dropped} 个无运行时注册的旧绑定", file=sys.stderr)
|
||||
|
||||
# ---------- 1. 给每个 codec 分配唯一类名 ----------
|
||||
CodecKeyToName = {}
|
||||
NameOwner = {}
|
||||
|
||||
Reference in new issue
Block a user