Files
TankIC/3DTank/tools/gen_action_kt.js
T

877 lines
38 KiB
JavaScript
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.
// gen_action_kt.js —— 生成 S3 状态层的 Kotlin 数据类(动作 + 状态)
//
// 输入:
// names/action_inventory_10032.json 类名 / 构造参数 / toString 字段顺序
// names/action_calls_10032.json 每个类的调用点实参表达式
// names/codec_fields.json 协议字段的 Prop → 线上类型(类型反查的权威来源)
// 输出:
// TankiOffline/app/src/jsMain/kotlin/App/State/Inventory.kt
//
// 类型判定(认不出就写 Any? 并附调用点原文,不静默猜):
// !0 / !1 → Boolean
// 纯数字字面量 → Int(带小数点 → Double)
// "…" → String
// <x>.<prop> → 查 codec_fields 的 Prop 得到协议类型再映射 Kotlin
// new <Cls>(…) / <Cls>() → 该 Cls(同表内的类)或 Any?
// 其它 → Any?(注释里留原文)
//
// 用法: node gen_action_kt.js [--out <文件>]
'use strict';
const fs = require('fs');
const path = require('path');
const PROJECT = path.resolve(__dirname, '..');
const Argv = process.argv.slice(2);
const Arg = (N, D) => { const I = Argv.indexOf('--' + N); return I >= 0 && Argv[I + 1] ? Argv[I + 1] : D; };
// --audit:对「已有运行时真值」的字段也跑一遍推断,输出冲突,用来校验推断规则是否可靠
const Audit = Argv.indexOf('--audit') >= 0;
const Conflicts = [];
// --stats:统计仍然认不出的实参形态(决定下一条推断规则该打哪里)
const Stats = Argv.indexOf('--stats') >= 0;
// --why <类名.字段>:打印该字段每个调用点的推断结果,用于排查错型
const Why = Arg('why', '');
const ShapeHist = new Map();
const CallHist = new Map();
function Shape(Expr) {
const E = String(Expr || '').trim();
if (!E) return 'empty';
if (E === 'null') return 'null';
if (/^[A-Za-z_$][\w$]*$/.test(E)) return 'ident';
if (/^this\.[A-Za-z0-9_$]+$/.test(E)) return 'this.field';
if (/^(?:[A-Za-z_$][\w$]*\.)+[A-Za-z0-9_$]+$/.test(E)) return 'a.b.path';
if (/^[A-Za-z_$][\w$]*\(/.test(E) || /^(?:[A-Za-z_$][\w$]*\.)+[A-Za-z0-9_$]+\(/.test(E)) return 'call';
if (/^new\s+/.test(E)) return 'new';
if (/^function|^\(function/.test(E)) return 'func';
if (/^\[/.test(E)) return 'array';
if (/^\{/.test(E)) return 'object';
if (/^!/.test(E)) return 'not';
return 'other';
}
// TankIC/3DTank/tools → TankIC/TankiOffline/...
const OUT = path.resolve(Arg('out', path.join(PROJECT, '..', 'TankiOffline', 'app', 'src', 'jsMain', 'kotlin', 'App', 'State', 'Inventory.kt')));
const Inventory = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_inventory_10032.json'), 'utf8'));
const Calls = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_calls_10032.json'), 'utf8')).Calls || {};
const Codec = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'codec_fields.json'), 'utf8'));
// 运行时真值(客户端 dispatch 打点解析出来的字段类型)优先级最高
let RuntimeTypes = {};
let RuntimeModels = {};
let RuntimeSamples = {};
try {
const J = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_types_10032.json'), 'utf8'));
RuntimeTypes = J.Types || {};
RuntimeModels = J.Models || {};
RuntimeSamples = J.Samples || {};
} catch (E) { RuntimeTypes = {}; RuntimeModels = {}; RuntimeSamples = {}; }
// Prop → 协议类型(重复项若类型不一致则不采用)
const PropType = new Map();
const PropBad = new Set();
for (const C of Object.values(Codec.Codecs)) {
for (const F of C.Fields || []) {
if (!F.Prop) continue;
if (PropBad.has(F.Prop)) continue;
const Prev = PropType.get(F.Prop);
if (Prev && Prev !== F.Type) { PropType.delete(F.Prop); PropBad.add(F.Prop); continue; }
if (!Prev) PropType.set(F.Prop, F.Type);
}
}
// 协议类型 → Kotlin
function KotlinOf(ProtoType) {
let T = String(ProtoType || '').trim();
let Suffix = '';
if (T.startsWith('opt<')) { T = T.slice(4, -1).trim(); Suffix = '?'; }
const L = /^list<([\s\S]+)>$/.exec(T);
if (L) return 'List<' + KotlinOf(L[1]).replace(/\?$/, '') + '>' + Suffix;
const M = /^map<([\s\S]+)>$/.exec(T);
if (M) {
const P = M[1].split(',').map((X) => KotlinOf(X).replace(/\?$/, ''));
return 'Map<' + P[0] + ', ' + P[1] + '>' + Suffix;
}
if (T.startsWith('enum<')) return 'Int' + Suffix;
const Base = {
bool: 'Boolean', i8: 'Int', i16: 'Int', i32: 'Int', i64: 'Long',
f32: 'Float', f64: 'Double', string: 'String', bytes: 'ByteArray',
vector3: 'Vector3',
}[T];
return (Base || 'Any?') + (Base ? Suffix : '');
}
const Names = new Set(Inventory.Rows.map((R) => R.Name));
// 运行时 toString 里出现的嵌套对象类(EmissionColors / Coloring …)。
// 有三种情况:
// ① 已有同名数据类且字段集覆盖运行时字段 → 同一个类,直接用(并把它选为该名字的代表行)
// ② 有同名数据类但字段集对不上 → 不是同一个类,放弃,避免张冠李戴
// ③ 没有同名数据类 → 按运行时字段现生成一个 data class
const RowsByName = new Map();
for (const R of Inventory.Rows) {
const List = RowsByName.get(R.Name) || [];
List.push(R);
RowsByName.set(R.Name, List);
}
const ModelDefs = {};
const KnownModels = new Set();
const EmitModels = new Set();
const RowPick = new Map(); // 名字 → 与运行时证据吻合的那一行
for (const [Name, Fields] of Object.entries(RuntimeModels)) {
const Rows0 = RowsByName.get(Name);
if (Rows0) {
const Keys = Object.keys(Fields);
const Match = Rows0.find((R) => {
const Fs = new Set(R.ToStringFields.length ? R.ToStringFields : R.Params);
return Keys.every((K) => Fs.has(K));
});
if (!Match) continue;
KnownModels.add(Name);
RowPick.set(Name, Match);
continue;
}
ModelDefs[Name] = Fields;
KnownModels.add(Name);
EmitModels.add(Name);
}
// 同名多行时,除了上面的对象类证据,动作的运行时字段集同样能指出「哪一行才是真的」。
// 优先用「样例那一行」的字段集(同名不同类的类型会互相污染,合并后的 RuntimeTypes 可能多出不属于该类的字段)。
function SampleFieldNames(Raw) {
const Open = String(Raw).indexOf('(');
const Close = String(Raw).lastIndexOf(')');
if (Open < 0 || Close <= Open) return [];
const Body = String(Raw).slice(Open + 1, Close);
if (!Body.trim()) return [];
const Parts = [];
let Depth = 0;
let Cur = '';
for (const C of Body) {
if (C === '(' || C === '[' || C === '{') Depth++;
else if (C === ')' || C === ']' || C === '}') Depth--;
if (C === ',' && Depth === 0) { Parts.push(Cur); Cur = ''; continue; }
Cur += C;
}
if (Cur.trim()) Parts.push(Cur);
const Out = [];
for (const P of Parts) {
const Eq = P.indexOf('=');
if (Eq > 0) Out.push(P.slice(0, Eq).trim());
}
return Out;
}
for (const [Name, Raw] of Object.entries(RuntimeSamples)) {
const Rows0 = RowsByName.get(Name);
if (!Rows0) continue;
const Keys = SampleFieldNames(Raw);
if (!Keys.length) continue;
const Match = Rows0.find((R) => {
const Fs = new Set(R.ToStringFields.length ? R.ToStringFields : R.Params);
return Keys.every((K) => Fs.has(K));
});
if (Match) RowPick.set(Name, Match);
}
for (const [Name, Fields] of Object.entries(RuntimeTypes)) {
if (RowPick.has(Name)) continue;
const Rows0 = RowsByName.get(Name);
if (!Rows0) continue;
const Keys = Object.keys(Fields);
const Match = Rows0.find((R) => {
const Fs = new Set(R.ToStringFields.length ? R.ToStringFields : R.Params);
return Keys.every((K) => Fs.has(K));
});
if (Match) RowPick.set(Name, Match);
}
// Kotlin 关键字做属性名时要加反引号
const KotlinKeyword = new Set(['as', 'break', 'class', 'continue', 'do', 'else', 'false', 'for', 'fun', 'if', 'in',
'interface', 'is', 'null', 'object', 'package', 'return', 'super', 'this', 'throw', 'true', 'try', 'typealias',
'typeof', 'val', 'var', 'when', 'while', 'by', 'catch', 'constructor', 'delegate', 'dynamic', 'field', 'file',
'finally', 'get', 'import', 'init', 'param', 'property', 'receiver', 'set', 'setparam', 'where', 'actual',
'abstract', 'annotation', 'companion', 'const', 'crossinline', 'data', 'enum', 'expect', 'external', 'final',
'infix', 'inline', 'inner', 'internal', 'lateinit', 'noinline', 'open', 'operator', 'out', 'override', 'private',
'protected', 'public', 'reified', 'sealed', 'suspend', 'tailrec', 'vararg']);
function EscapeName(N) {
return KotlinKeyword.has(N) ? '`' + N + '`' : N;
}
const PrimitiveKinds = new Set(['Int', 'Long', 'Double', 'Boolean', 'String', 'Any?']);
function InferType(Expr) {
const E = String(Expr || '').trim();
if (!E) return null;
if (E === '!0' || E === '!1') return 'Boolean';
if (/^-?\d+$/.test(E)) return 'Int';
if (/^-?\d+\.\d+$/.test(E)) return 'Double';
if (/^".*"$/.test(E)) return 'String';
const Prop = /^[A-Za-z_$][\w$]*\.([A-Za-z0-9_$]+)$/.exec(E);
if (Prop) {
const T = PropType.get(Prop[1]);
if (T) return KotlinOf(T);
return null;
}
const New = /^new\s+([A-Za-z0-9_$]+)\s*\(/.exec(E);
if (New && Names.has(New[1])) return New[1];
// 布尔表达式要判在顶层:`(i = i === cd ? null : i)` 这种「赋值 + 三元」不是 Boolean
const B = Booleanish(E);
if (B) return B;
return null;
}
function SkipStr(S, I) {
const Q = S[I];
I++;
while (I < S.length) {
if (S[I] === '\\') { I += 2; continue; }
if (S[I] === Q) return I + 1;
I++;
}
return I;
}
function HasTopLevel(S, Needle) {
let Depth = 0;
for (let I = 0; I < S.length; I++) {
const C = S[I];
if (C === '"' || C === "'" || C === '`') { I = SkipStr(S, I) - 1; continue; }
if (C === '(' || C === '[' || C === '{') { Depth++; continue; }
if (C === ')' || C === ']' || C === '}') { Depth--; continue; }
if (Depth === 0 && S.startsWith(Needle, I)) return true;
}
return false;
}
function StripOuterParens(S) {
let T = S;
while (T.startsWith('(') && T.endsWith(')')) {
let Depth = 0;
let ClosedEarly = false;
for (let I = 0; I < T.length; I++) {
const C = T[I];
if (C === '"' || C === "'" || C === '`') { I = SkipStr(T, I) - 1; continue; }
if (C === '(') Depth++;
else if (C === ')') { Depth--; if (Depth === 0 && I < T.length - 1) { ClosedEarly = true; break; } }
}
if (ClosedEarly) break;
T = T.slice(1, -1).trim();
}
return T;
}
// 顶层赋值 `X = expr` → 返回 `=` 的下标(排除 ==/!=/<=/>=)
function TopLevelAssign(S) {
let Depth = 0;
for (let I = 0; I < S.length; I++) {
const C = S[I];
if (C === '"' || C === "'" || C === '`') { I = SkipStr(S, I) - 1; continue; }
if (C === '(' || C === '[' || C === '{') { Depth++; continue; }
if (C === ')' || C === ']' || C === '}') { Depth--; continue; }
if (Depth === 0 && C === '=' && S[I + 1] !== '=' && '=!<>'.indexOf(S[I - 1] || '') < 0) return I;
}
return -1;
}
function Booleanish(E) {
const S = StripOuterParens(String(E || '').trim());
if (!S) return null;
if (S[0] === '!') return 'Boolean';
const Asg = TopLevelAssign(S);
if (Asg >= 0) return Booleanish(S.slice(Asg + 1));
if (HasTopLevel(S, '?')) return null; // 三元表达式类型不定
if (/\.equals\(/.test(S) || /\binstanceof\b/.test(S)) return 'Boolean';
for (const Op of ['===', '!==', '==', '!=', '>=', '<=', '>', '<']) {
if (HasTopLevel(S, Op)) return 'Boolean';
}
return null;
}
// 只接受「当前工程里确实存在」的 Kotlin 类型,否则退回 Any?(避免生成引用不到的名字而编译失败)
// · 标量 / Vector3 / 它们的容器 → 接受
// · 运行时 toString 里的模型类名 → 拒绝(那是 JS 模型名,本工程没有对应 Kotlin 类)
// · 调用点 new <动作类>(...) → 接受(同文件内确有该 data class,且不能是自身)
const Primitive = new Set(['Boolean', 'Int', 'Long', 'Float', 'Double', 'String', 'ByteArray', 'Vector3']);
function CheckType(T, Source, Self) {
const S = String(T).trim().replace(/\?$/, '');
if (Primitive.has(S)) return { Ok: true, Type: T };
const L = /^List<([\s\S]+)>$/.exec(S);
if (L) {
const I = CheckType(L[1], Source, Self);
return I.Ok ? { Ok: true, Type: 'List<' + I.Type.replace(/\?$/, '') + '>' + (/\?$/.test(T) ? '?' : '') } : { Ok: false, Bad: T };
}
const M = /^Map<([\s\S]+)>$/.exec(S);
if (M) {
const P = M[1].split(',').map((X) => CheckType(X.trim(), Source, Self));
return P.every((X) => X.Ok)
? { Ok: true, Type: 'Map<' + P[0].Type.replace(/\?$/, '') + ', ' + P[1].Type.replace(/\?$/, '') + '>' + (/\?$/.test(T) ? '?' : '') }
: { Ok: false, Bad: T };
}
// 运行时 toString 里出现过的对象类(已确认与同名数据类一致,或已现生成),可直接当类型用
if (Source === 'runtime' && KnownModels.has(S) && S !== Self) return { Ok: true, Type: T };
if ((Source === 'callsite' || Source === 'dataflow') && Names.has(S) && S !== Self) return { Ok: true, Type: T };
return { Ok: false, Bad: T };
}
// ---- 纯静态局部数据流:实参是变量时,回溯形参/局部赋值 ----
// 只做两种最可靠的形状,且深度受限、冲突即放弃(不猜):
// ① 标识符是某个函数的形参 → 找该函数的调用点,推断对应位置的实参类型
// ② 标识符最近一次被赋值 → 推断赋值右侧表达式
const SrcDir = path.join(PROJECT, 'src');
const SrcCache = new Map();
function SrcOf(Mod) {
if (!SrcCache.has(Mod)) {
let Text = '';
try { Text = fs.readFileSync(path.join(SrcDir, 'module_' + String(Mod).replace(/^module_/, '') + '.js'), 'utf8'); } catch (E) { Text = ''; }
SrcCache.set(Mod, Text);
}
return SrcCache.get(Mod);
}
// 模块内所有 `function Name(params) {`(带花括号配对得到的函数体范围)
const FnIndexCache = new Map();
const BraceCache = new Map();
function BracePairs(Text) {
if (BraceCache.has(Text)) return BraceCache.get(Text);
const Pairs = new Map();
const Stack = [];
let I = 0;
while (I < Text.length) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') { I = SkipString(Text, I); continue; }
if (C === '/' && Text[I + 1] === '/') { const N = Text.indexOf('\n', I); if (N < 0) break; I = N; continue; }
if (C === '/' && Text[I + 1] === '*') { const N = Text.indexOf('*/', I + 2); if (N < 0) break; I = N + 2; continue; }
if (C === '{') Stack.push(I);
else if (C === '}') { const O = Stack.pop(); if (O !== undefined) Pairs.set(O, I); }
I++;
}
BraceCache.set(Text, Pairs);
return Pairs;
}
function SkipString(Text, I) {
const Q = Text[I];
I++;
while (I < Text.length) {
if (Text[I] === '\\') { I += 2; continue; }
if (Text[I] === Q) return I + 1;
I++;
}
return I;
}
function FnIndex(Mod) {
if (!FnIndexCache.has(Mod)) {
const Text = SrcOf(Mod);
const Pairs = BracePairs(Text);
const List = [];
for (const M of Text.matchAll(/(?<![\w$.])function\s+([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End === undefined) continue;
List.push({
Name: M[1],
Params: M[2].trim() ? M[2].split(',').map((S) => S.trim()) : [],
Start: M.index,
End,
});
}
FnIndexCache.set(Mod, List);
}
return FnIndexCache.get(Mod);
}
// 包住 Off、且形参表里含 Param 的所有函数,由内向外(具名 / 赋值到方法 / IIFE 三种)
function ScopesFor(Mod, Off, Param) {
const Out = [];
for (const F of FnIndex(Mod)) {
if (F.Start <= Off && F.End >= Off && F.Params.indexOf(Param) >= 0) Out.push({ ...F, Kind: 'named' });
}
for (const F of AnonIndex(Mod)) {
if (F.Start > Off || F.End < Off || F.Params.indexOf(Param) < 0) continue;
if (F.Method) Out.push({ ...F, Kind: 'method', Name: F.Method });
if (F.Iife) Out.push({ ...F, Kind: 'iife' });
}
Out.sort((A, B) => B.Start - A.Start);
return Out;
}
// 匿名函数表达式:赋值到方法(`x.m = function (…)`)或立即调用(`(function (…) {…})(a, b)`)
const AnonCache = new Map();
function AnonIndex(Mod) {
if (AnonCache.has(Mod)) return AnonCache.get(Mod);
const Text = SrcOf(Mod);
const Pairs = BracePairs(Text);
const List = [];
for (const M of Text.matchAll(/(?<![\w$.])function\s*\(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End === undefined) continue;
const Asg = /\.([A-Za-z0-9_$]+)\s*=\s*$/.exec(Text.slice(Math.max(0, M.index - 90), M.index));
const Params = M[1].trim() ? M[1].split(',').map((S) => S.trim()) : [];
// 立即调用:} (…) 或 } ) (…)
let I = End + 1;
while (I < Text.length && /\s/.test(Text[I])) I++;
if (Text[I] === ')') { I++; while (I < Text.length && /\s/.test(Text[I])) I++; }
let Iife = null;
let IifeAt = -1;
if (Text[I] === '(') {
const Close = MatchParen(Text, I);
if (Close > 0) { Iife = SplitArgs(Text.slice(I + 1, Close)); IifeAt = I; }
}
List.push({ Params, Start: M.index, End, Method: Asg ? Asg[1] : null, Iife, IifeAt });
}
AnonCache.set(Mod, List);
return List;
}
// `x.<Name>(…)` 调用点(用于 `x.m = function (…)` 这种原型方法)
const MethodCallsCache = new Map();
function MethodCalls(Mod, Name) {
const Key = Mod + '|.' + Name;
if (MethodCallsCache.has(Key)) return MethodCallsCache.get(Key);
const Text = SrcOf(Mod);
const Out = [];
const Re = new RegExp('\\.' + Name.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
let M;
while ((M = Re.exec(Text)) && Out.length < 40) {
const Open = Text.indexOf('(', M.index);
if (/=\s*function\s*$/.test(Text.slice(Math.max(0, M.index - 20), M.index))) continue;
const End = MatchParen(Text, Open);
if (End < 0) continue;
Out.push({ Args: SplitArgs(Text.slice(Open + 1, End)), At: M.index });
}
MethodCallsCache.set(Key, Out);
return Out;
}
// 包住 Off 的最内层函数(不管形参是什么)
function InnermostScope(Mod, Off) {
let Best = null;
for (const F of FnIndex(Mod)) if (F.Start <= Off && F.End >= Off && (!Best || F.Start > Best.Start)) Best = { ...F, Kind: 'named' };
for (const F of AnonIndex(Mod)) {
if (F.Start > Off || F.End < Off) continue;
if (!Best || F.Start > Best.Start) Best = { ...F, Kind: F.Method ? 'method' : (F.Iife ? 'iife' : 'anon'), Name: F.Method };
}
return Best;
}
// 原样转发:实参就是所在函数的形参表,且该函数挂在原型上(`ld(Cls).m = function (…) { return new Cls(…) }`)。
// 这是 Kotlin 为 data class 生成的 copy/with,类型信息就是类自身的字段,属于循环,不能当证据。
// 自由函数里的 `make(a, b) { return new X(a, b) }` 不在此列(那条链是有信息的)。
const ForwardCache = new WeakSet();
const ForwardJudged = new WeakSet();
function IsForwarding(Mod, Site) {
if (ForwardJudged.has(Site)) return ForwardCache.has(Site);
ForwardJudged.add(Site);
const Sc = InnermostScope(Mod, Site.At);
if (!Sc || Sc.Kind !== 'method' || !Sc.Params.length) return false;
if (Sc.Params.length !== Site.Args.length) return false;
for (let I = 0; I < Sc.Params.length; I++) if (Site.Args[I].trim() !== Sc.Params[I]) return false;
ForwardCache.add(Site);
return true;
}
const FnCallsCache = new Map();
function FnCalls(Mod, Name) {
const Key = Mod + '|' + Name;
if (FnCallsCache.has(Key)) return FnCallsCache.get(Key);
const Text = SrcOf(Mod);
const Out = [];
const Re = new RegExp('(?<![\\w$.])' + Name.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
let M;
while ((M = Re.exec(Text)) && Out.length < 40) {
if (/function\s+$/.test(Text.slice(Math.max(0, M.index - 10), M.index))) continue;
const End = MatchParen(Text, Text.indexOf('(', M.index));
if (End < 0) continue;
Out.push({ Args: SplitArgs(Text.slice(Text.indexOf('(', M.index) + 1, End)), At: M.index });
}
FnCallsCache.set(Key, Out);
return Out;
}
function MatchParen(Text, Open) {
let Depth = 0;
for (let I = Open; I < Text.length; I++) {
const C = Text[I];
if (C === '(') Depth++;
else if (C === ')') { Depth--; if (Depth === 0) return I; }
}
return -1;
}
function SplitArgs(Text) {
const Out = [];
let Depth = 0;
let Cur = '';
for (const C of Text) {
if (C === '(' || C === '[' || C === '{') Depth++;
else if (C === ')' || C === ']' || C === '}') Depth--;
if (C === ',' && Depth === 0) { Out.push(Cur.trim()); Cur = ''; continue; }
Cur += C;
}
if (Cur.trim()) Out.push(Cur.trim());
return Out;
}
// [From, To) 范围内最近一次 `X = <expr>`
function LastAssign(Text, Name, From, To) {
const Re = new RegExp('(?<![\\w$.])' + Name.replace(/[$]/g, '\\$') + '\\s*=\\s*([^;\\n]{1,80})', 'g');
let Best = null;
let M;
while ((M = Re.exec(Text)) && M.index < To) {
if (M.index >= From) Best = M[1];
}
if (!Best) return null;
const E = Best.trim().replace(/[,)]+$/, '').trim();
return /^[A-Za-z0-9_$"]/.test(E) || /^!/.test(E) ? E : null;
}
// 多来源类型合一:全同 → 该类型;Int/Long 混合 → Long;否则放弃
function Unify(List) {
const S = new Set(List.filter(Boolean));
if (!S.size) return null;
if (S.size === 1) return [...S][0];
if (S.size === 2 && S.has('Int') && S.has('Long')) return 'Long';
return null;
}
const FlowSeen = new Set();
function InferDeep(Expr, Mod, Off, Depth) {
const E = String(Expr || '').trim();
if (!E) return null;
const Base = InferType(E);
if (Base) return Base;
if (Depth <= 0 || !Mod) return null;
const Text = SrcOf(Mod);
if (!Text) return null;
// a.b.c 形式:用最后一段查协议字段表
const Chain = /^(?:[A-Za-z_$][\w$]*\.)+([A-Za-z0-9_$]+)$/.exec(E);
if (Chain) {
const P = PropType.get(Chain[1]);
return P ? KotlinOf(P) : null;
}
if (!/^[A-Za-z_$][\w$]*$/.test(E)) return null;
const Guard = Mod + '|' + Off + '|' + E + '|' + Depth;
if (FlowSeen.has(Guard)) return null;
FlowSeen.add(Guard);
try {
// ① 形参 → 调用点 / IIFE 实参(由内向外逐层试,按调用点自身的作用域继续回溯)
const Scopes = ScopesFor(Mod, Off, E);
for (const Scope of Scopes) {
const Idx = Scope.Params.indexOf(E);
const Types = [];
if (Scope.Kind === 'iife') {
if (Scope.Iife.length > Idx) Types.push(InferDeep(Scope.Iife[Idx], Mod, Scope.IifeAt, Depth - 1));
} else {
const CallSites = Scope.Kind === 'method' ? MethodCalls(Mod, Scope.Name) : FnCalls(Mod, Scope.Name);
for (const Site of CallSites) {
if (Site.Args.length <= Idx) continue;
Types.push(InferDeep(Site.Args[Idx], Mod, Site.At, Depth - 1));
}
}
const U = Unify(Types);
if (U) return U;
}
// ② 所在函数体内的最近一次赋值
const Assign = LastAssign(Text, E, Scopes.length ? Scopes[0].Start : 0, Off);
if (Assign && Assign !== E) return InferDeep(Assign, Mod, Off, Depth - 1);
} finally {
FlowSeen.delete(Guard);
}
return null;
}
// 个别字段的调用点/运行时证据只能是 Any?(例如 GoTo 样例里的 screen=[object Object]),
// 但切片 reducer 已经能给出强类型时在这里显式指定。键:`类名.字段`。
// GoTo.screen:QK.grm_1 在 screen 切片里只按 ScreenId 使用(instanceof LX/RX + m()/equals),
// 类型定义见 TankiOffline/.../App/State/ScreenId.kt。
const FieldTypeOverrides = new Map([
['GoTo.screen', 'ScreenId'],
// lootBoxes 切片:lH/qH/yH 的字段在别处的调用点只能给出 Boolean / Any?,但 reducer 里分别是
// ContainerType(默认 Ly()=STANDARD)、可空 ContainerType、及补给箱奖励列表。
['SetCurrentContainer.type', 'ContainerType'],
['SelectCategory.containerCategory', 'ContainerType?'],
['ShownRewards.rewards', 'List<ContainerGivenItem>'],
// garage 切片:生成器从调用点只能给 Any?,但 reducer 里这些字段参与 Map 键/算术/子状态类型。
// · 装备/物品 id 在运行时派发里都是 Long;count 是 Int。
// · itemsByBase 的值是 iB(无 toString):Kotlin 侧自建 GarageBaseItems(Items + 派生字段)。
// · baseItemIdToDevices 的值是 List<VF(GarageDevice)>。
['GarageItem.id', 'Long'],
['GarageItem.count', 'Int'],
['GarageDevice.id', 'Long'],
['GarageDevice.baseItemId', 'Long'],
['GarageItem.modification', 'GarageItemModification?'],
['GarageItem.upgradeableParams', 'UpgradableItemParams?'],
['GarageItemsState.itemsByBase', 'Map<Long, GarageBaseItems>'],
['Devices.baseItemIdToDevices', 'Map<Long, List<GarageDevice>>'],
['GarageItemObject.id', 'Long'],
['GarageItemObject.count', 'Int'],
['GarageItemObject.itemCategory', 'Int'],
['GarageItemObject.itemViewCategory', 'Int'],
['GarageItemObject.isPremium', 'Boolean'],
['GarageItemObject.buyable', 'Boolean'],
['GarageItemObject.availableForAll', 'Boolean'],
['GarageItemObject.modification', 'GarageItemModification?'],
['GarageItemObject.upgradeableParams', 'UpgradableItemParams?'],
['GarageItemObject.visible', 'Boolean'],
['GarageDeviceObject.id', 'Long'],
['GarageDeviceObject.baseItemId', 'Long'],
['GarageDeviceObject.itemCategory', 'Int'],
['GarageDeviceObject.itemViewCategory', 'Int'],
['GarageDeviceObject.isPremium', 'Boolean'],
['GarageDeviceObject.buyable', 'Boolean'],
['GarageDeviceObject.visible', 'Boolean'],
['GarageDevicesLoadedAction.mountedDevice', 'GarageDevice?'],
['GarageSelectItem.itemId', 'Long'],
['GarageAddItemToDepot.itemId', 'Long'],
['GarageAddItemToDepot.count', 'Int'],
['GarageRemoveItemFromDepot.item', 'GarageItemObject'],
['GarageRemoveItemFromStore.itemId', 'Long'],
['GarageUpdateItemCount.item', 'GarageItem'],
['GarageUpdateItemCount.count', 'Int'],
['GarageUpgradeItemAction.itemId', 'Long'],
['GarageUpgradeItemCoolDownStarted.itemId', 'Long'],
['GarageUpgradeItemCoolDownStarted.endTime', 'Long'],
['GarageUpgradeItemCoolDownFinished.itemId', 'Long'],
['GarageUpgradeItemCoolDownFinished.endTime', 'Long'],
['GarageRemoveDevice.device', 'GarageDevice'],
['GarageRemoveDevice.item', 'GarageItem'],
['GarageInsertDeviceClientAndServer.device', 'GarageDevice'],
['GarageInsertDeviceClientAndServer.item', 'GarageItem'],
['GarageBuyDevice.device', 'GarageDevice'],
['GarageBuyDevice.item', 'GarageItem'],
['GarageDeviceTrySetInfinityLifetime.deviceId', 'Long'],
['GarageApplyResistanceMount.resistance', 'GarageItem'],
['GarageApplyResistanceMount.index', 'Int'],
['GarageResistanceUnMount.resistance', 'Long'],
['GarageSelectAlteration.alterationId', 'Long'],
['GarageSelectSkin.skinId', 'Long'],
['GarageSelectShotSkin.shotSkinId', 'Long'],
['GarageRemoveDiscountForDevice.item', 'GarageItem'],
['UpdateDiscount.item', 'GarageItem'],
['RemoveFromSale.removeItems', 'Collection<Long>'],
['AddToFavoriteItem.itemId', 'Long'],
['RemoveFromFavoriteItem.itemId', 'Long'],
['BuyProtectionSlot.slotToUnlock', 'Int'],
]);
const Emitted = [];
// 动作目录:类名 + 字段(名, 类型),用于生成 ActionCatalog.kt(运行时按名字查类)
const Catalog = [];
const Seen = new Set();
let TypedFields = 0;
let TotalFields = 0;
for (const R of Inventory.Rows) {
if (Seen.has(R.Name)) continue;
// 同名多行时,优先用与运行时证据吻合的那一行
const Pick = RowPick.get(R.Name);
if (Pick && Pick !== R) continue;
Seen.add(R.Name);
// 调用点按「动作名|局部类名」索引:同名不同类(如 Init 的 1 字段版/18 字段版)不能混用
const Sites = Calls[R.Name + '|' + R.Local] || [];
const Fields = R.ToStringFields.length ? R.ToStringFields : R.Params.map((P, I) => 'field' + (I + 1));
const Lines = [];
const FieldPairs = [];
Fields.forEach((F, I) => {
TotalFields++;
// ① 运行时打点(真值)→ ② 调用点/数据流反推 → ③ Any?
// 运行时也可能「什么也没说」(值全是空列表 / [object Object]),这时不能挡住调用点推断
const RtRaw = RuntimeTypes[R.Name] && RuntimeTypes[R.Name][F];
// Opaque = 运行时看到对象但说不出类型,此时不能用调用点猜;其余「没信息」类型才让位
const Rt = !RtRaw ? null
: (RtRaw === 'Opaque' ? 'Any?'
: (/^(Any\?|List<Any\?>|Map<String, Any\?>|Map<String, Any>)$/.test(RtRaw) ? null : RtRaw));
let Inferred = null;
let InferredSource = '';
const Notes = [];
let NullSeen = false;
let SiteSeen = 0;
let SiteSkipped = 0;
{
const Deep = [];
for (const S of Sites) {
const A = S.Args[I];
if (A === undefined) continue;
SiteSeen++;
if (Why === R.Name + '.' + F) {
const Direct = InferType(A);
const DeepOne = Direct || InferDeep(A, S.Module, S.At, 2);
console.log(' [why] ' + S.Module + '@' + S.At + ' 实参=' + String(A).replace(/\s+/g, ' ').slice(0, 70) +
' 直接=' + (Direct || '-') + ' 深推=' + (DeepOne || '-'));
}
if (IsForwarding(S.Module, S)) { SiteSkipped++; continue; }
if (A.trim() === 'null') NullSeen = true;
const T = InferType(A);
if (T) { if (!Inferred) { Inferred = T; InferredSource = 'callsite'; } }
else {
if (Notes.length < 2) Notes.push(A.slice(0, 40));
const D = InferDeep(A, S.Module, S.At, 2);
if (D) Deep.push(D);
}
}
if (!Inferred) {
const U = Unify(Deep);
if (U) { Inferred = U; InferredSource = 'dataflow'; }
}
}
// 协议字段表的 opt<> 只说明「线上可选」,动作字段本身没观察到 null 就当非空。
// 但运行时若见过 null(RtRaw 为 Any? 或带 ?),保留可空性。
if (Inferred && !NullSeen) {
const RuntimeNullish = RtRaw === 'Any?' || (RtRaw && RtRaw.endsWith('?'));
if (!RuntimeNullish) Inferred = Inferred.replace(/\?$/, '');
}
if (Audit && Rt && Inferred && Rt.replace(/\?$/, '') !== Inferred.replace(/\?$/, '')) {
Conflicts.push(R.Name + '.' + F + ' 运行时=' + Rt + ' 推断=' + Inferred + '(' + InferredSource + ')');
}
let Type = Rt || Inferred;
let Source = Rt ? 'runtime' : InferredSource;
let Note = '';
if (Type) {
const C = CheckType(Type, Source, R.Name);
if (C.Ok) Type = C.Type;
else { Note = '// 未收录类型: ' + C.Bad + '(' + Source + ')'; Type = null; }
}
const Override = FieldTypeOverrides.get(R.Name + '.' + F);
if (Override) { Type = Override; Source = 'override'; Note = ''; }
if (Type) TypedFields++;
if (Stats && !Type) {
for (const S of Sites) {
const A = S.Args[I];
if (A === undefined) continue;
const K = Shape(A);
ShapeHist.set(K, (ShapeHist.get(K) || 0) + 1);
if (K === 'call') {
const All = String(A).trim().match(/[A-Za-z0-9_$]+(?=\s*\()/g) || [];
const Name = All.length ? All[All.length - 1] : '?';
CallHist.set(Name, (CallHist.get(Name) || 0) + 1);
}
}
}
if (!Type && !Note) {
if (SiteSeen && SiteSkipped === SiteSeen) Note = '// 仅 copy 转发(无类型信息)';
else Note = Notes.length ? '// 调用点: ' + Notes.join(' | ') : '// 无调用点';
}
if (!Note) Note = '// ' + Source;
// 注释必须压成单行,且逗号要放在注释之前(否则逗号被 // 吞掉)
Note = Note.replace(/\s+/g, ' ').slice(0, 120);
Lines.push(' val ' + F + ': ' + (Type || 'Any?') + ', ' + Note);
FieldPairs.push([F, Type || 'Any?']);
});
Emitted.push('data class ' + R.Name + '(\n' + Lines.join('\n') + '\n)');
Catalog.push({ Name: R.Name, Fields: FieldPairs });
}
// 嵌套模型类(字段名来自 runtime toString,类型来自样例值)
function ModelFieldType(T) {
const S = String(T || '').trim();
// 可空性直接透传(Any? 本身已经是可空)
if (S.endsWith('?') && S !== 'Any?') {
const Inner = ModelFieldType(S.slice(0, -1));
return Inner === 'Any?' ? 'Any?' : Inner + '?';
}
if (PrimitiveKinds.has(S)) return S;
const L = /^List<([\s\S]+)>$/.exec(S);
if (L) {
const I = ModelFieldType(L[1]);
return 'List<' + (I === 'Any?' ? 'Any?' : I) + '>';
}
const M = /^Map<([\s\S]+),\s*([\s\S]+)>$/.exec(S);
if (M) return 'Map<String, ' + ModelFieldType(M[2]) + '>';
return KnownModels.has(S) ? S : 'Any?';
}
const ModelEmitted = [];
let ModelTyped = 0;
let ModelTotal = 0;
for (const Name of [...EmitModels].sort()) {
const Fields = ModelDefs[Name];
const Lines = [];
for (const [F, T] of Object.entries(Fields)) {
ModelTotal++;
const K = ModelFieldType(T);
if (K !== 'Any?') ModelTyped++;
Lines.push(' val ' + EscapeName(F) + ': ' + K + ', // ' + (K === 'Any?' ? '未收录: ' + T : 'runtime'));
}
if (!Lines.length) continue;
ModelEmitted.push('data class ' + Name + '(\n' + Lines.join('\n') + '\n)');
}
const Head = [
'// 本文件由 3DTank/tools/gen_action_kt.js 生成。',
'// 来源:module_10032 的动作/状态类(类名与字段顺序取自 toString 证据,类型由调用点 + 协议字段表反推)。',
'// 标注 Any? 的字段表示调用点认不出类型,行尾注释给出原始实参,便于人工补。',
'package App.State',
'',
'import Core.Vector3',
'',
'// 动作/状态类共 ' + Emitted.length + ' 个,字段 ' + TotalFields + ' 个(已定类型 ' + TypedFields + ' 个)',
'',
];
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, Head.join('\n') + Emitted.join('\n\n') + '\n\n' +
'// ---- 嵌套对象类(来自运行时 toString 样例:字段名是证据,类型由样例值推断)----\n' +
'// 共 ' + ModelEmitted.length + ' 个,字段 ' + ModelTotal + ' 个(已定类型 ' + ModelTyped + ' 个)\n\n' +
ModelEmitted.join('\n\n') + '\n', 'utf8');
console.log('生成 ' + Emitted.length + ' 个 data class,字段 ' + TotalFields + ',已定类型 ' + TypedFields +
'(' + ((TypedFields * 100 / Math.max(1, TotalFields)).toFixed(1)) + '%)');
console.log('生成 ' + ModelEmitted.length + ' 个嵌套对象类,字段 ' + ModelTotal + ',已定类型 ' + ModelTyped);
// ---- 动作目录:名字 → Kotlin 类 + 字段类型(运行时按 dispatch 打点里的名字反查)----
const CatalogPath = path.join(path.dirname(OUT), 'ActionCatalog.kt');
const TracedNames = Object.keys(RuntimeTypes).sort();
// 不在 module_10032 里、但有 toString 证据的动作。
// InitState:module_10849 的 ct,toString 为 `InitState(type=…, action=…)`,已在 State 模块实现
const ExternalActions = [
{ Name: 'InitState', Type: 'InitState', Fields: [['type', 'Any?'], ['action', 'Any?']] },
];
const CatalogLines = [];
for (const Row of Catalog) {
const Fields = Row.Fields.map(([N, T]) => 'ActionFieldMeta("' + N + '", "' + T + '")').join(', ');
CatalogLines.push(' ActionMeta("' + Row.Name + '", ' + Row.Name + '::class, listOf(' + Fields + ')),');
}
for (const Row of ExternalActions) {
const Fields = Row.Fields.map(([N, T]) => 'ActionFieldMeta("' + N + '", "' + T + '")').join(', ');
CatalogLines.push(' ActionMeta("' + Row.Name + '", ' + Row.Type + '::class, listOf(' + Fields + ')),');
}
const CatalogText = [
'// 本文件由 3DTank/tools/gen_action_kt.js 生成。',
'// 动作/状态类目录:把 dispatch 打点里的字符串类名映射到 Kotlin 类(字段类型一并带上)。',
'package App.State',
'',
'import kotlin.reflect.KClass',
'import State.InitState',
'',
'data class ActionFieldMeta(val Name: String, val Type: String)',
'',
'data class ActionMeta(val Name: String, val Type: KClass<*>, val Fields: List<ActionFieldMeta>)',
'',
'object ActionCatalog {',
' val All: List<ActionMeta> = listOf(',
CatalogLines.join('\n'),
' )',
'',
' val ByName: Map<String, ActionMeta> = All.associateBy { it.Name }',
'',
' /** 运行时 dispatch 打点里真实出现过的动作名(真值证据)。 */',
' val TracedNames: Set<String> = setOf(' + TracedNames.map((N) => '"' + N + '"').join(', ') + ')',
'',
' /** 打点里出现但目录里没有的名字(正常应为空)。 */',
' fun MissingTraced(): List<String> = TracedNames.filter { !ByName.containsKey(it) }.sorted()',
'}',
'',
].join('\n');
fs.writeFileSync(CatalogPath, CatalogText, 'utf8');
console.log('生成动作目录 ' + (Catalog.length + ExternalActions.length) + ' 条(打点出现过 ' + TracedNames.length + ' 个名字)');
// ---- 回放样例:每个动作在运行时打点里的第一行原文(供 Kotlin 侧字段/类型对照)----
const SamplesPath = path.join(path.dirname(OUT), 'ActionSamples.kt');
const EscapeKt = (S) => String(S).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/\r?\n/g, '\\n');
const SampleNames = Object.keys(RuntimeSamples).sort();
const SampleLines = SampleNames.map((N) => ' "' + N + '" to "' + EscapeKt(RuntimeSamples[N]) + '",');
const SamplesText = [
'// 本文件由 3DTank/tools/gen_action_kt.js 生成。',
'// 每个动作在运行时 dispatch 打点里的第一行原文(真值样例),用于回放对照。',
'package App.State',
'',
'object ActionSamples {',
' val Raw: Map<String, String> = mapOf(',
SampleLines.join('\n'),
' )',
'}',
'',
].join('\n');
fs.writeFileSync(SamplesPath, SamplesText, 'utf8');
console.log('生成回放样例 ' + SampleNames.length + ' 条');
if (Audit) {
console.log('推断 vs 运行时真值冲突 ' + Conflicts.length + ' 处');
for (const C of Conflicts.slice(0, 25)) console.log(' ' + C);
}
if (Stats) {
console.log('认不出的实参形态:');
for (const [K, V] of [...ShapeHist.entries()].sort((A, B) => B[1] - A[1])) console.log(' ' + K + ' = ' + V);
console.log('其中调用表达式的被调名 Top 15:');
for (const [K, V] of [...CallHist.entries()].sort((A, B) => B[1] - A[1]).slice(0, 15)) console.log(' ' + K + ' = ' + V);
}
console.log(' 输出: ' + OUT);