feat(state): 生成 10032 动作/状态层 Inventory.kt(运行时类型优先)
- 新增 extract_action_inventory / infer_action_types / analyze_action_trace / gen_action_kt 四个工具 - extractor 支持 return ( 换行形式,类数 619 → 864(唯一名 816,字段 2129) - 生成器按「运行时 dispatch 打点 > 调用点字面量 > 协议字段表」定类型,认不出写 Any? 并留证据 - 修掉两处会生成非法 Kotlin 的问题:逗号落在注释内、运行时 JS 模型名被当成 Kotlin 类型 - 结果:306/2129 字段已定类型(runtime 162 / callsite 144),compileKotlinJs 通过
This commit is contained in:
1 parent
20cc0f9858
commit
e8f5cf6e24
8 files changed
+37510
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,525 @@
|
||||
{
|
||||
"generatedAt": "2026-10-21T12:13:34.495Z",
|
||||
"stats": {
|
||||
"Rows": 4304,
|
||||
"Parsed": 4129,
|
||||
"Skipped": 175,
|
||||
"Actions": 135,
|
||||
"Unresolved": 14
|
||||
},
|
||||
"Types": {
|
||||
"SetLocations": {
|
||||
"captchaLocations": "List<Any?>"
|
||||
},
|
||||
"SocialNetworkAvailability": {
|
||||
"socialNetworkAvailability": "Map<String, Boolean>"
|
||||
},
|
||||
"SocialNetworkClientIds": {
|
||||
"socialNetworkClientIds": "Map<String, Any>"
|
||||
},
|
||||
"SetInviteNeed": {
|
||||
"inviteNeed": "Boolean"
|
||||
},
|
||||
"Init": {
|
||||
"disableRegistrationMessage": "Any?",
|
||||
"disableShopMessage": "Any?",
|
||||
"homeScreenMessage": "Any?",
|
||||
"announcementItems": "List<String>",
|
||||
"battleId": "Long",
|
||||
"remainingTime": "Int",
|
||||
"battleName": "String",
|
||||
"matchBattle": "Boolean",
|
||||
"isParkourMode": "Boolean",
|
||||
"isTutorial": "Boolean",
|
||||
"valuableBattle": "Boolean",
|
||||
"scoreLimit": "Int",
|
||||
"fund": "Int",
|
||||
"options": "List<String>",
|
||||
"customBattleName": "Any?",
|
||||
"battleFormat": "String",
|
||||
"isMatchmakingMark": "Boolean",
|
||||
"mapNameWithoutMode": "String"
|
||||
},
|
||||
"SetPasswordRestrictions": {
|
||||
"minPasswordLength": "Int",
|
||||
"maxPasswordLength": "Int",
|
||||
"passwordNeedLower": "Boolean",
|
||||
"passwordNeedUpper": "Boolean",
|
||||
"passwordNeedDigit": "Boolean"
|
||||
},
|
||||
"GoTo": {
|
||||
"screen": "List<String>",
|
||||
"history": "Boolean"
|
||||
},
|
||||
"Configure": {
|
||||
"modes": "List<String>",
|
||||
"disabledModes": "List<String>",
|
||||
"holidayEnabled": "Boolean",
|
||||
"holidayTitle": "String",
|
||||
"holidayIcon": "List<String>",
|
||||
"eventBigTile": "List<String>",
|
||||
"holidayDescription": "String",
|
||||
"minRankForProBattle": "Int",
|
||||
"maxUsersInGroup": "Int",
|
||||
"groupsInEventMode": "Boolean",
|
||||
"eventEndDate": "List<String>",
|
||||
"borderColorForEventButton": "String",
|
||||
"config": "String"
|
||||
},
|
||||
"InitProBattleRewardLimit": {
|
||||
"proLimit": "String"
|
||||
},
|
||||
"LimitReached": {
|
||||
"isLimitReached": "Boolean"
|
||||
},
|
||||
"Loaded": {
|
||||
"users": "Map<String, String>"
|
||||
},
|
||||
"SetUserId": {
|
||||
"userId": "Long"
|
||||
},
|
||||
"SetPremiumRemainingSeconds": {
|
||||
"seconds": "Int"
|
||||
},
|
||||
"SetDisabledWithoutPremiumLocations": {
|
||||
"locations": "List<Any?>"
|
||||
},
|
||||
"SetRankNames": {
|
||||
"rankNames": "List<String>"
|
||||
},
|
||||
"UpdateHasClan": {
|
||||
"hasClan": "Boolean"
|
||||
},
|
||||
"UpdateTimeToJoinRestriction": {
|
||||
"restrictionTimeJoinClanInSec": "Int"
|
||||
},
|
||||
"AddIncomingClan": {
|
||||
"clanIds": "List<Any?>"
|
||||
},
|
||||
"AddNewIncomingClan": {
|
||||
"clanIds": "List<Any?>"
|
||||
},
|
||||
"AddUsersToNewAccepted": {
|
||||
"clanId": "Int",
|
||||
"userIds": "List<Any?>",
|
||||
"usersId": "List<Any?>"
|
||||
},
|
||||
"AddNewIncoming": {
|
||||
"newIncoming": "List<Any?>"
|
||||
},
|
||||
"UpdateClanLicense": {
|
||||
"hasLicense": "Boolean"
|
||||
},
|
||||
"AddUsersToAccepted": {
|
||||
"usersId": "Any?"
|
||||
},
|
||||
"SetLimits": {
|
||||
"acceptedLimit": "Int",
|
||||
"incomingLimit": "Int"
|
||||
},
|
||||
"AddUsersToIncoming": {
|
||||
"usersId": "List<Any?>"
|
||||
},
|
||||
"AddUsersToNewIncoming": {
|
||||
"usersId": "List<Any?>"
|
||||
},
|
||||
"UpdateOnlineStatus": {
|
||||
"status": "Map<String, String>"
|
||||
},
|
||||
"UpdatePremiumStatus": {
|
||||
"premiums": "Map<String, String>"
|
||||
},
|
||||
"UpdateRanks": {
|
||||
"ranks": "Map<String, Int>"
|
||||
},
|
||||
"UpdateUids": {
|
||||
"uids": "Any?"
|
||||
},
|
||||
"SetMountedItemsProperties": {
|
||||
"properties": "List<String>"
|
||||
},
|
||||
"SetNewsItemAction": {
|
||||
"news": "List<String>"
|
||||
},
|
||||
"SetUserPropertiesAction": {
|
||||
"user": "String"
|
||||
},
|
||||
"UpdateEventPass": {
|
||||
"eventPassEndTime": "Int"
|
||||
},
|
||||
"UpdateStars": {
|
||||
"stars": "Map<String, Int>"
|
||||
},
|
||||
"SetCoins": {
|
||||
"coins": "Int"
|
||||
},
|
||||
"SetRubies": {
|
||||
"rubies": "Int"
|
||||
},
|
||||
"SetUnViewedElements": {
|
||||
"unViewedElements": "List<String>"
|
||||
},
|
||||
"SetMissionToElement": {
|
||||
"missionToElement": "Map<String, String>"
|
||||
},
|
||||
"SetRubiesForRename": {
|
||||
"rubiesForRename": "Int"
|
||||
},
|
||||
"SetAttachedCreatorTag": {
|
||||
"creatorTag": "Any?"
|
||||
},
|
||||
"SetUse2FA": {
|
||||
"isUse2FA": "Boolean"
|
||||
},
|
||||
"SetTwitchEnabled": {
|
||||
"twitchEnabled": "Boolean"
|
||||
},
|
||||
"SetAccount": {
|
||||
"account": "Any?"
|
||||
},
|
||||
"SetDaysForNextLinking": {
|
||||
"days": "Int"
|
||||
},
|
||||
"EnableSetBattleInviteNotifications": {
|
||||
"isEnabled": "Boolean"
|
||||
},
|
||||
"SetProfileBlockStatus": {
|
||||
"isProfileBlocked": "Boolean"
|
||||
},
|
||||
"SetChallenge": {
|
||||
"challenge": "Challenge"
|
||||
},
|
||||
"SetBattlePassPackages": {
|
||||
"battlePassPackages": "Map<String, Long>"
|
||||
},
|
||||
"InitUserReports": {
|
||||
"isEnabled": "Boolean",
|
||||
"helpdeskTicketsEnabled": "Boolean",
|
||||
"availableReports": "Int",
|
||||
"availableGameReports": "Int"
|
||||
},
|
||||
"SetActiveContract": {
|
||||
"contractId": "Any?"
|
||||
},
|
||||
"SetContract": {
|
||||
"contract": "List<Any?>"
|
||||
},
|
||||
"RemoveItemNotification": {
|
||||
"section": "String",
|
||||
"itemId": "Long",
|
||||
"type": "String",
|
||||
"parentId": "Long",
|
||||
"excludeType": "Any?",
|
||||
"skipPeriodCheck": "Boolean"
|
||||
},
|
||||
"AddItemNotifications": {
|
||||
"data": "List<ItemNotificationData>"
|
||||
},
|
||||
"SetEventPassShopItemId": {
|
||||
"shopItemId": "Any?"
|
||||
},
|
||||
"SetKeys": {
|
||||
"keys": "Map<String, Int>"
|
||||
},
|
||||
"SetReferralEventData": {
|
||||
"eventData": "String"
|
||||
},
|
||||
"SetClanEditPrice": {
|
||||
"clanEditPrice": "String"
|
||||
},
|
||||
"SetMinRankForCreate": {
|
||||
"rankIndex": "Int"
|
||||
},
|
||||
"NewIncomingUserNotification": {
|
||||
"count": "Int"
|
||||
},
|
||||
"NewAcceptedUserNotification": {
|
||||
"count": "Int"
|
||||
},
|
||||
"NewClanInviteNotification": {
|
||||
"count": "Int"
|
||||
},
|
||||
"InitGarageResources": {
|
||||
"params": "String"
|
||||
},
|
||||
"InitNotificationSound": {
|
||||
"sound": "List<String>"
|
||||
},
|
||||
"SetRankIndexToMinGS": {
|
||||
"rankIndexToMinGS": "Map<String, Int>"
|
||||
},
|
||||
"SetIcons": {
|
||||
"icons": "List<String>"
|
||||
},
|
||||
"SetCoinPaymentProviderUrl": {
|
||||
"url": "String"
|
||||
},
|
||||
"ToggleShopActivity": {
|
||||
"enabled": "Boolean"
|
||||
},
|
||||
"SetQuestCategories": {
|
||||
"categories": "List<String>"
|
||||
},
|
||||
"SetContainerData": {
|
||||
"type": "String",
|
||||
"name": "String",
|
||||
"description": "String"
|
||||
},
|
||||
"SetCreatorTags": {
|
||||
"creatorTags": "List<Any?>"
|
||||
},
|
||||
"SetPaint": {
|
||||
"itemId": "Long",
|
||||
"paint": "Coloring"
|
||||
},
|
||||
"SetDrone": {
|
||||
"itemId": "Long",
|
||||
"resource": "List<String>",
|
||||
"emissionColors": "EmissionColors"
|
||||
},
|
||||
"SetQuestCategoriesStatistic": {
|
||||
"statistic": "List<String>"
|
||||
},
|
||||
"SetPromoCode": {
|
||||
"code": "String"
|
||||
},
|
||||
"AddMessages": {
|
||||
"messages": "List<String>"
|
||||
},
|
||||
"UpdateTypingSpeedAntifloodParams": {
|
||||
"symbolCost": "Int",
|
||||
"enterCost": "Int"
|
||||
},
|
||||
"AddMap": {
|
||||
"id": "Long",
|
||||
"info": "String"
|
||||
},
|
||||
"RoundStarted": {
|
||||
"battleId": "Long",
|
||||
"timeLeftInSec": "Int"
|
||||
},
|
||||
"UpdateFund": {
|
||||
"battleId": "Long",
|
||||
"fund": "Int"
|
||||
},
|
||||
"AddBattle": {
|
||||
"battleId": "Long",
|
||||
"creatorId": "Long",
|
||||
"params": "String",
|
||||
"suspicionLevel": "String"
|
||||
},
|
||||
"AddUsers": {
|
||||
"battleId": "Long",
|
||||
"users": "List<String>",
|
||||
"team": "String"
|
||||
},
|
||||
"UpdateTeamScore": {
|
||||
"battleId": "Long",
|
||||
"team": "String",
|
||||
"score": "Int"
|
||||
},
|
||||
"RemoveUser": {
|
||||
"battleId": "Long",
|
||||
"userId": "Long"
|
||||
},
|
||||
"UpdateUserScore": {
|
||||
"battleId": "Long",
|
||||
"userId": "Long",
|
||||
"score": "Int"
|
||||
},
|
||||
"UnsubscribeUpdateUserData": {
|
||||
"usersId": "List<Any?>"
|
||||
},
|
||||
"RemoveBattle": {
|
||||
"battleId": "Long"
|
||||
},
|
||||
"ChatLoaded": {
|
||||
"commonBattleChatEnabled": "Boolean",
|
||||
"teamBattleChatEnabled": "Boolean"
|
||||
},
|
||||
"SetBattleUpdatedQuests": {
|
||||
"updatedQuestsData": "List<Any?>"
|
||||
},
|
||||
"SetSpectator": {
|
||||
"spectatorMode": "Boolean"
|
||||
},
|
||||
"SetReArmor": {
|
||||
"enabled": "Boolean"
|
||||
},
|
||||
"SetRankRange": {
|
||||
"rankRange": "String"
|
||||
},
|
||||
"SetBattleMode": {
|
||||
"mode": "String"
|
||||
},
|
||||
"EnterToBattle": {
|
||||
"battleId": "Long"
|
||||
},
|
||||
"SetIdleKickPeriod": {
|
||||
"idleKickPeriodMsec": "Int"
|
||||
},
|
||||
"UpdateUsersScoreKillsAndDeaths": {
|
||||
"userScores": "List<String>"
|
||||
},
|
||||
"UpdateScore": {
|
||||
"team": "String",
|
||||
"score": "Int"
|
||||
},
|
||||
"InitSuspiciousState": {
|
||||
"suspiciousUserIds": "List<Any?>",
|
||||
"suspiciousByIpUserIds": "List<Any?>"
|
||||
},
|
||||
"AddRelatedBazooka": {
|
||||
"bazookaId": "Long",
|
||||
"supplyId": "Long"
|
||||
},
|
||||
"SetTankResistance": {
|
||||
"user": "Long",
|
||||
"tankResistance": "List<String>"
|
||||
},
|
||||
"SetTankInfo": {
|
||||
"user": "Long",
|
||||
"info": "String"
|
||||
},
|
||||
"SetGearScore": {
|
||||
"user": "Long",
|
||||
"score": "Int"
|
||||
},
|
||||
"OnUserJoinTheBattle": {
|
||||
"userId": "Long"
|
||||
},
|
||||
"SetLocalWeaponResistanceProperty": {
|
||||
"itemProperty": "String"
|
||||
},
|
||||
"PauseActivated": {
|
||||
"timeToAutoLeavePeriod": "Int",
|
||||
"timeToAutoPausePeriod": "Int"
|
||||
},
|
||||
"UpdateUserScoreKillsAndDeaths": {
|
||||
"userStat": "String"
|
||||
},
|
||||
"OnKilled": {
|
||||
"killer": "Long",
|
||||
"destroyed": "Long",
|
||||
"damageType": "String",
|
||||
"isCritical": "Boolean"
|
||||
},
|
||||
"AddMessage": {
|
||||
"message": "Any?"
|
||||
},
|
||||
"OnUserLeaveTheBattle": {
|
||||
"userId": "Long"
|
||||
},
|
||||
"RemoveTankInfo": {
|
||||
"user": "Long"
|
||||
},
|
||||
"OnSuicide": {
|
||||
"destroyed": "Long"
|
||||
},
|
||||
"SetRestrictionMount": {
|
||||
"delayMs": "Int"
|
||||
},
|
||||
"AddOverdriveDescription": {
|
||||
"itemId": "Long",
|
||||
"descriptionData": "OverdriveDescriptionData"
|
||||
},
|
||||
"AddRelatedResistanceModule": {
|
||||
"itemId": "Long",
|
||||
"resistanceModuleId": "Long"
|
||||
},
|
||||
"InitState": {
|
||||
"type": "String",
|
||||
"action": "String"
|
||||
},
|
||||
"SetGarageSkinsAction": {
|
||||
"skins": "List<GarageItemObject>"
|
||||
},
|
||||
"GarageDevicesLoadedAction": {
|
||||
"baseItemId": "Long",
|
||||
"devices": "Any?",
|
||||
"mountedDevice": "List<String>"
|
||||
},
|
||||
"UpdateGearScore": {
|
||||
"gearScore": "Int",
|
||||
"weaponGS": "Int",
|
||||
"hullGS": "Int",
|
||||
"droneGS": "Int",
|
||||
"bazookaGS": "Int",
|
||||
"resistanceGS": "Int"
|
||||
},
|
||||
"OnChangeEquipment": {
|
||||
"destroyed": "Long"
|
||||
},
|
||||
"SetReArmorTemporaryDisabled": {
|
||||
"disabled": "Boolean"
|
||||
},
|
||||
"SetFlagOrBallOwner": {
|
||||
"ownerUserId": "Long"
|
||||
},
|
||||
"GarageUpdateDepotItem": {
|
||||
"itemId": "Int",
|
||||
"count": "Int"
|
||||
},
|
||||
"SelectBattle": {
|
||||
"battleId": "Long"
|
||||
},
|
||||
"SelectBattleFormat": {
|
||||
"format": "BattleFormat"
|
||||
},
|
||||
"UserEnterBattle": {
|
||||
"battles": "Map<String, String>"
|
||||
},
|
||||
"SubscribeUpdateUserData": {
|
||||
"userId": "Long"
|
||||
},
|
||||
"RoundFinish": {
|
||||
"battleId": "Long"
|
||||
},
|
||||
"QuestsChangeCategory": {
|
||||
"previousScreen": "Any?",
|
||||
"previousQuests": "String",
|
||||
"currentQuests": "String"
|
||||
},
|
||||
"QuestsLoaded": {
|
||||
"quests": "List<String>"
|
||||
},
|
||||
"SetReferrals": {
|
||||
"referralsCount": "Int",
|
||||
"referralsData": "Map<String, Any?>"
|
||||
},
|
||||
"UpdateQuests": {
|
||||
"quests": "List<String>"
|
||||
},
|
||||
"AddUser": {
|
||||
"battleId": "Long",
|
||||
"infoUser": "String",
|
||||
"team": "String"
|
||||
},
|
||||
"SwapTeam": {
|
||||
"battleId": "Long"
|
||||
},
|
||||
"GarageUpgradeItemConfirm": {
|
||||
"itemId": "Long",
|
||||
"level": "Int"
|
||||
},
|
||||
"UpdateCrystals": {
|
||||
"crystal": "Int"
|
||||
}
|
||||
},
|
||||
"Unresolved": [
|
||||
"Init.disableRegistrationMessage = null",
|
||||
"Init.disableShopMessage = null",
|
||||
"Init.homeScreenMessage = null",
|
||||
"Init.customBattleName = null",
|
||||
"AddUsersToAccepted.usersId = List<Any?>|List<Long>",
|
||||
"UpdateUids.uids = Map<String, String>|Map<String, Any>|Map<String, Int>|Map<String, Long>",
|
||||
"SetAttachedCreatorTag.creatorTag = null",
|
||||
"SetAccount.account = null",
|
||||
"SetActiveContract.contractId = null",
|
||||
"RemoveItemNotification.excludeType = null",
|
||||
"SetEventPassShopItemId.shopItemId = null",
|
||||
"AddMessage.message = BattleChatUserMessage|BattleChatTeamMessage",
|
||||
"GarageDevicesLoadedAction.devices = List<GarageDeviceObject>|List<Any?>",
|
||||
"QuestsChangeCategory.previousScreen = List<String>|QuestCategory"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// analyze_action_trace.js —— 把运行时动作打点(toString 原文)解析成「字段类型覆盖表」
|
||||
//
|
||||
// 打点来源:module_10849 的 ReduxToModelGateway.dispatch(`yt.uar`),每行是动作类的 toString:
|
||||
// "SetInviteNeed(inviteNeed=false)"
|
||||
// "Init(disableRegistrationMessage=null, disableShopMessage=null, …)"
|
||||
// "SocialNetworkClientIds(socialNetworkClientIds={facebook=…, google=…})"
|
||||
// "AddMap(mapName=LostTemple)" (无引号文本 = 字符串/枚举名,按 String 记)
|
||||
//
|
||||
// 输出:names/action_types_10032.json
|
||||
// { "SetInviteNeed": { "inviteNeed": "Boolean" }, … }
|
||||
// 供 gen_action_kt.js 覆盖生成时的类型。
|
||||
//
|
||||
// 用法: node analyze_action_trace.js
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT = path.resolve(__dirname, '..');
|
||||
const Trace = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_trace.json'), 'utf8'));
|
||||
const Rows = Trace.trace || [];
|
||||
|
||||
// --- 值解析 -------------------------------------------------------------
|
||||
function SplitTop(Text, Sep) {
|
||||
const Out = [];
|
||||
let Depth = 0;
|
||||
let Cur = '';
|
||||
for (const C of Text) {
|
||||
if ('([{'.includes(C)) Depth++;
|
||||
else if (')]}'.includes(C)) Depth--;
|
||||
if (C === Sep && Depth === 0) { Out.push(Cur); Cur = ''; continue; }
|
||||
Cur += C;
|
||||
}
|
||||
if (Cur.trim()) Out.push(Cur);
|
||||
return Out;
|
||||
}
|
||||
function ParseValue(V) {
|
||||
const S = String(V).trim();
|
||||
if (S === 'true' || S === 'false') return 'Boolean';
|
||||
if (S === 'null') return 'null';
|
||||
if (/^-?\d+$/.test(S)) {
|
||||
const N = Number(S);
|
||||
return Math.abs(N) > 2147483647 ? 'Long' : 'Int';
|
||||
}
|
||||
if (/^-?\d+\.\d+(E-?\d+)?$/.test(S) || /^-?\d+E-?\d+$/i.test(S)) return 'Double';
|
||||
if (S.startsWith('[') && S.endsWith(']')) {
|
||||
const Inner = S.slice(1, -1).trim();
|
||||
if (!Inner) return 'List<Any?>';
|
||||
const Elems = SplitTop(Inner, ',').map(ParseValue);
|
||||
return 'List<' + UnifyAll(Elems).replace(/\?$/, '') + '>';
|
||||
}
|
||||
if (S.startsWith('{') && S.endsWith('}')) {
|
||||
const Inner = S.slice(1, -1).trim();
|
||||
if (!Inner) return 'Map<String, Any?>';
|
||||
const Pairs = SplitTop(Inner, ',').map((P) => {
|
||||
const Eq = P.indexOf('=');
|
||||
return Eq < 0 ? 'Any?' : ParseValue(P.slice(Eq + 1));
|
||||
});
|
||||
return 'Map<String, ' + UnifyAll(Pairs).replace(/\?$/, '') + '>';
|
||||
}
|
||||
const Model = /^([A-Z][A-Za-z0-9_$]*)\(/.exec(S);
|
||||
if (Model) return Model[1];
|
||||
return 'String';
|
||||
}
|
||||
function UnifyAll(List) {
|
||||
const Distinct = new global.Set(List.filter((T) => T && T !== 'null'));
|
||||
if (!Distinct.size) return 'Any?';
|
||||
if (Distinct.size === 1) return [...Distinct][0];
|
||||
// Int + Long → Long;其余冲突 → Any?
|
||||
if (Distinct.size === 2 && Distinct.has('Int') && Distinct.has('Long')) return 'Long';
|
||||
return 'Any?';
|
||||
}
|
||||
|
||||
// --- 解析每行 ------------------------------------------------------------
|
||||
const Types = new Map(); // 动作名 → Map<字段, Set<类型>>
|
||||
let Parsed = 0;
|
||||
let Skipped = 0;
|
||||
for (const R of Rows) {
|
||||
const S = String(R.s || '');
|
||||
const M = /^([A-Z][A-Za-z0-9_$]*)\((.*)\)$/s.exec(S);
|
||||
if (!M) { Skipped++; continue; }
|
||||
const [, Name, Body] = M;
|
||||
if (!Body.trim()) { Parsed++; continue; }
|
||||
const Fields = Types.get(Name) || new Map();
|
||||
for (const Part of SplitTop(Body, ',')) {
|
||||
const Eq = Part.indexOf('=');
|
||||
if (Eq < 0) continue;
|
||||
const Key = Part.slice(0, Eq).trim();
|
||||
const Val = ParseValue(Part.slice(Eq + 1));
|
||||
const Bucket = Fields.get(Key) || new global.Set();
|
||||
Bucket.add(Val);
|
||||
Fields.set(Key, Bucket);
|
||||
}
|
||||
Types.set(Name, Fields);
|
||||
Parsed++;
|
||||
}
|
||||
|
||||
// --- 汇总成最终类型 ------------------------------------------------------
|
||||
const Out = {};
|
||||
const Unresolved = [];
|
||||
for (const [Name, Fields] of Types) {
|
||||
const Row = {};
|
||||
for (const [Key, Bucket] of Fields) {
|
||||
const T = UnifyAll([...Bucket]);
|
||||
Row[Key] = T;
|
||||
if (T === 'Any?') Unresolved.push(Name + '.' + Key + ' = ' + [...Bucket].join('|'));
|
||||
}
|
||||
Out[Name] = Row;
|
||||
}
|
||||
fs.writeFileSync(path.join(PROJECT, 'names', 'action_types_10032.json'), JSON.stringify({
|
||||
generatedAt: new Date().toISOString(),
|
||||
stats: { Rows: Rows.length, Parsed, Skipped, Actions: Object.keys(Out).length, Unresolved: Unresolved.length },
|
||||
Types: Out,
|
||||
Unresolved,
|
||||
}, null, 1), 'utf8');
|
||||
|
||||
console.log('打点 ' + Rows.length + ' 行:可解析 ' + Parsed + ',跳过(无 toString)' + Skipped +
|
||||
';动作 ' + Object.keys(Out).length + ',字段类型冲突 ' + Unresolved.length);
|
||||
console.log(' 输出: 3DTank/names/action_types_10032.json');
|
||||
for (const N of Object.keys(Out).slice(0, 6)) {
|
||||
console.log(' ' + N + ': ' + JSON.stringify(Out[N]));
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// extract_action_inventory.js —— 把「动作/状态类」结构化成 JSON(S3 状态层的第一步)
|
||||
//
|
||||
// 为什么要它:
|
||||
// module_10032(1.5MB / 1661 导出)里没有 Redux 引擎,只有业务动作与状态数据类。
|
||||
// 这些类的可恢复信息有三份:
|
||||
// ① 构造参数(`function Cls(a, b, c) {`)
|
||||
// ② toString 里的真名与字段顺序(`return "SetFragsCount(a=" + … `)
|
||||
// ③ 构造体里的 `this.<属性> = <参数>` 绑定
|
||||
// 本工具把三者对齐成一张表,供后续生成 Kotlin data class / sealed interface。
|
||||
//
|
||||
// 用法: node extract_action_inventory.js [模块号=10032] [--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; };
|
||||
const Mod = Arg('module', '10032');
|
||||
const Out = path.resolve(Arg('out', path.join(PROJECT, 'names', 'action_inventory_' + Mod + '.json')));
|
||||
const Src = fs.readFileSync(path.join(PROJECT, 'src', 'module_' + Mod + '.js'), 'utf8');
|
||||
|
||||
// ① 构造参数
|
||||
const Ctors = new Map(); // 局部类名 → [参数名…]
|
||||
for (const M of Src.matchAll(/function ([A-Za-z0-9_$]+)\(([^)]*)\)\s*\{/g)) {
|
||||
if (Ctors.has(M[1])) continue;
|
||||
const Params = M[2].trim() ? M[2].split(',').map((S) => S.trim()) : [];
|
||||
Ctors.set(M[1], Params);
|
||||
}
|
||||
|
||||
// ② toString:`(helper(Cls).toString = function () { … return "<Name>(<字段>=" …`
|
||||
const Rows = [];
|
||||
for (const M of Src.matchAll(/\(\s*[A-Za-z_$][\w$]*\(([A-Za-z0-9_$]+)\)\.toString = function \(\) \{([\s\S]{0,4000}?)\n\s*\}\)/g)) {
|
||||
const Cls = M[1];
|
||||
const Body = M[2];
|
||||
// 真名既可能紧跟 return(`return "Foo(`),也可能被包裹(`return (\n "Foo(`)
|
||||
const NameM = /return\s*\(?\s*"([A-Z][A-Za-z0-9_]{1,60})\(/.exec(Body);
|
||||
if (!NameM) continue;
|
||||
const Name = NameM[1];
|
||||
// 字段顺序:`字段名=` 出现顺序(排除内部的 "…=" 之外的表达式影响,只用带标识符前缀的片段)
|
||||
const Fields = [];
|
||||
const FieldRe = /([A-Za-z_][A-Za-z0-9_]{0,40})=/g;
|
||||
let F;
|
||||
while ((F = FieldRe.exec(Body))) {
|
||||
if (F[1] === 'return') continue;
|
||||
Fields.push(F[1]);
|
||||
}
|
||||
Rows.push({ Local: Cls, Name, Params: Ctors.get(Cls) || [], ToStringFields: Fields });
|
||||
}
|
||||
|
||||
// ③ 构造体绑定:`this.<属性> = <参数>`
|
||||
const Binds = new Map(); // 局部类名 → [{Prop, Param}]
|
||||
for (const M of Src.matchAll(/function ([A-Za-z0-9_$]+)\(([^)]*)\)\s*\{([\s\S]{0,3000}?)\n\s{4}\}/g)) {
|
||||
const Cls = M[1];
|
||||
const Body = M[3];
|
||||
const List = [];
|
||||
for (const B of Body.matchAll(/this\.([A-Za-z0-9_$]+) = ([A-Za-z0-9_$]+)/g)) List.push({ Prop: B[1], Param: B[2] });
|
||||
if (List.length) Binds.set(Cls, List);
|
||||
}
|
||||
for (const R of Rows) R.Binds = Binds.get(R.Local) || [];
|
||||
|
||||
const Names = new Map();
|
||||
for (const R of Rows) if (!Names.has(R.Name)) Names.set(R.Name, R);
|
||||
const Out_obj = {
|
||||
module: Mod,
|
||||
generatedAt: new Date().toISOString(),
|
||||
stats: { Rows: Rows.length, UniqueNames: Names.size, WithParams: Rows.filter((R) => R.Params.length).length },
|
||||
Rows,
|
||||
};
|
||||
fs.writeFileSync(Out, JSON.stringify(Out_obj, null, 1), 'utf8');
|
||||
console.log('动作/状态类 ' + Rows.length + '(唯一名 ' + Names.size + ',有构造参数 ' +
|
||||
Out_obj.stats.WithParams + ')');
|
||||
console.log(' 输出: ' + Out);
|
||||
for (const R of Rows.slice(0, 8)) {
|
||||
console.log(' ' + R.Name + '(' + R.Params.join(', ') + ') 字段=' + R.ToStringFields.slice(0, 6).join(','));
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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; };
|
||||
// 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 = {};
|
||||
try {
|
||||
RuntimeTypes = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_types_10032.json'), 'utf8')).Types || {};
|
||||
} catch (E) { RuntimeTypes = {}; }
|
||||
|
||||
// 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));
|
||||
|
||||
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];
|
||||
// 明显是布尔表达式:!x / x.equals(...) / x instanceof Y / 比较
|
||||
if (/^!/.test(E) || /\.equals\(|instanceof|===|!==|[<>]=?/.test(E)) 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 };
|
||||
}
|
||||
if (Source === 'callsite' && Names.has(S) && S !== Self) return { Ok: true, Type: T };
|
||||
return { Ok: false, Bad: T };
|
||||
}
|
||||
|
||||
const Emitted = [];
|
||||
const Seen = new Set();
|
||||
let TypedFields = 0;
|
||||
let TotalFields = 0;
|
||||
for (const R of Inventory.Rows) {
|
||||
if (Seen.has(R.Name)) continue;
|
||||
Seen.add(R.Name);
|
||||
const Sites = Calls[R.Name] || [];
|
||||
const Fields = R.ToStringFields.length ? R.ToStringFields : R.Params.map((P, I) => 'field' + (I + 1));
|
||||
const Lines = [];
|
||||
Fields.forEach((F, I) => {
|
||||
TotalFields++;
|
||||
// ① 运行时打点(真值)→ ② 协议字段表/字面量反推 → ③ Any?
|
||||
const Rt = RuntimeTypes[R.Name] && RuntimeTypes[R.Name][F];
|
||||
let Type = Rt || null;
|
||||
let Source = Rt ? 'runtime' : '';
|
||||
const Notes = [];
|
||||
if (!Type) {
|
||||
for (const S of Sites) {
|
||||
const A = S.Args[I];
|
||||
if (A === undefined) continue;
|
||||
const T = InferType(A);
|
||||
if (T) { if (!Type) { Type = T; Source = 'callsite'; } }
|
||||
else if (Notes.length < 2) Notes.push(A.slice(0, 40));
|
||||
}
|
||||
}
|
||||
let Note = '';
|
||||
if (Type) {
|
||||
const C = CheckType(Type, Source, R.Name);
|
||||
if (C.Ok) Type = C.Type;
|
||||
else { Note = '// 未收录类型: ' + C.Bad + '(' + Source + ')'; Type = null; }
|
||||
}
|
||||
if (Type) TypedFields++;
|
||||
if (!Type && !Note) Note = Notes.length ? '// 调用点: ' + Notes.join(' | ') : '// 无调用点';
|
||||
if (!Note) Note = '// ' + Source;
|
||||
// 注释必须压成单行,且逗号要放在注释之前(否则逗号被 // 吞掉)
|
||||
Note = Note.replace(/\s+/g, ' ').slice(0, 120);
|
||||
Lines.push(' val ' + F + ': ' + (Type || 'Any?') + ', ' + Note);
|
||||
});
|
||||
Emitted.push('data class ' + R.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', 'utf8');
|
||||
console.log('生成 ' + Emitted.length + ' 个 data class,字段 ' + TotalFields + ',已定类型 ' + TypedFields +
|
||||
'(' + ((TypedFields * 100 / Math.max(1, TotalFields)).toFixed(1)) + '%)');
|
||||
console.log(' 输出: ' + OUT);
|
||||
@@ -0,0 +1,112 @@
|
||||
// infer_action_types.js —— 从调用点反推「动作/状态类」的字段类型(S3 状态层第二步)
|
||||
//
|
||||
// 思路:
|
||||
// 1. module_10032 的导出表:导出键 → 局部类名;配合 action_inventory 得到 类名 ↔ 导出键;
|
||||
// 2. 其它模块用 `<别名> = i(10032).$_$.<键>` 引入,再 `<别名>(...)` 构造(Kotlin data class 的 new 被编译成函数调用);
|
||||
// 3. 采集每个构造点的实参表达式,按下面规则定类型(认不出就记 unknown,不猜):
|
||||
// · 数字字面量 → Int/Double;true/false → Boolean;"…" → String;null → null
|
||||
// · `<x>.f20_1/g20_1/h20_1`(向量分量) → Float
|
||||
// · `new <Cls>()` / `<Cls>()` → 该 Cls 的局部名
|
||||
// · `<x>.<prop>` → 记录 prop 名,供后续按 codec_fields 反查
|
||||
//
|
||||
// 用法: node infer_action_types.js [--limit 30]
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT = path.resolve(__dirname, '..');
|
||||
const SRC = path.join(PROJECT, 'src');
|
||||
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; };
|
||||
const Limit = Number(Arg('limit', '30')) || 30;
|
||||
|
||||
const Inventory = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_inventory_10032.json'), 'utf8'));
|
||||
|
||||
function SplitArgs(Text) {
|
||||
const Out = [];
|
||||
let Depth = 0;
|
||||
let Cur = '';
|
||||
for (let I = 0; I < Text.length; I++) {
|
||||
const C = Text[I];
|
||||
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;
|
||||
}
|
||||
function FindDeps(Src) {
|
||||
const Head = /\(\s*function\s*\(([^)]*)\)\s*\{/.exec(Src);
|
||||
let Tail = null;
|
||||
const Re = /\}\)\(([^;]*)\);/g;
|
||||
let M;
|
||||
while ((M = Re.exec(Src))) Tail = M;
|
||||
if (!Head || !Tail) return {};
|
||||
const P = SplitArgs(Head[1]);
|
||||
const A = SplitArgs(Tail[1]);
|
||||
const Out = {};
|
||||
for (let I = 0; I < P.length && I < A.length; I++) {
|
||||
const X = /\(\s*(\d+)\s*\)/.exec(A[I]);
|
||||
if (X) Out[P[I]] = X[1];
|
||||
}
|
||||
return Out;
|
||||
}
|
||||
|
||||
// 10032 导出表:键 → 局部类名
|
||||
const Src10032 = fs.readFileSync(path.join(SRC, 'module_10032.js'), 'utf8');
|
||||
const ExportToLocal = new Map();
|
||||
for (const M of Src10032.matchAll(/t\.\$_\$\.([A-Za-z0-9_$]+) = ([A-Za-z0-9_$]+)/g)) ExportToLocal.set(M[1], M[2]);
|
||||
const LocalToName = new Map();
|
||||
for (const R of Inventory.Rows) if (!LocalToName.has(R.Local)) LocalToName.set(R.Local, R.Name);
|
||||
const KeyToName = new Map();
|
||||
for (const [K, L] of ExportToLocal) if (LocalToName.has(L)) KeyToName.set(K, LocalToName.get(L));
|
||||
|
||||
// 扫全包调用点
|
||||
const Calls = new Map(); // 动作名 → [{Module, Args}]
|
||||
for (const F of fs.readdirSync(SRC).filter((N) => /^module_\d+\.js$/.test(N))) {
|
||||
if (F === 'module_10032.js') continue;
|
||||
const Text = fs.readFileSync(path.join(SRC, F), 'utf8');
|
||||
const Deps = FindDeps(Text);
|
||||
const Alias = new Map(); // 局部别名 → 动作名
|
||||
for (const M of Text.matchAll(/(?<![A-Za-z0-9_$.])([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\.\$_\$\.([A-Za-z0-9_$]+)[,;)]/g)) {
|
||||
if (Deps[M[2]] !== '10032') continue;
|
||||
const Name = KeyToName.get(M[3]);
|
||||
if (Name && !Alias.has(M[1])) Alias.set(M[1], Name);
|
||||
}
|
||||
if (!Alias.size) continue;
|
||||
for (const [Local, Name] of Alias) {
|
||||
const Re = new RegExp('(?<![\\w$.])new ' + Local.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
|
||||
let M;
|
||||
while ((M = Re.exec(Text))) {
|
||||
const Open = Text.indexOf('(', M.index);
|
||||
let Depth = 0;
|
||||
let End = -1;
|
||||
for (let I = Open; I < Text.length; I++) {
|
||||
if (Text[I] === '(') Depth++;
|
||||
else if (Text[I] === ')') { Depth--; if (Depth === 0) { End = I; break; } }
|
||||
}
|
||||
if (End < 0) continue;
|
||||
const Args = SplitArgs(Text.slice(Open + 1, End));
|
||||
const List = Calls.get(Name) || [];
|
||||
List.push({ Module: F.replace('.js', ''), Args });
|
||||
Calls.set(Name, List);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Named = Inventory.Rows.filter((R) => Calls.has(R.Name));
|
||||
console.log('有调用点的动作/状态类 ' + Named.length + ' / ' + Inventory.Rows.length);
|
||||
console.log('总调用点 ' + [...Calls.values()].reduce((A, B) => A + B.length, 0));
|
||||
for (const R of Named.slice(0, Limit)) {
|
||||
const List = Calls.get(R.Name);
|
||||
const Sample = List.slice(0, 3).map((C) => C.Module + ': ' + C.Args.join(' , '));
|
||||
console.log(' ' + R.Name + '(' + R.Params.join(',') + ') ×' + List.length);
|
||||
for (const S of Sample) console.log(' ' + S.slice(0, 140));
|
||||
}
|
||||
fs.writeFileSync(path.join(PROJECT, 'names', 'action_calls_10032.json'), JSON.stringify({
|
||||
generatedAt: new Date().toISOString(),
|
||||
stats: { Classes: Inventory.Rows.length, WithCalls: Named.length },
|
||||
Calls: Object.fromEntries([...Calls.entries()]),
|
||||
}, null, 1), 'utf8');
|
||||
console.log(' 输出: 3DTank/names/action_calls_10032.json');
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user