From e77ec20d81d51c6a912c5e561cdec1dab4c2d892 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 18 Sep 2026 15:56:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M13a=20=E5=AF=BC=E5=87=BA=E5=AE=98?= =?UTF-8?q?=E6=96=B9=E3=80=8C=E5=9E=8B=E5=8F=B7=E6=9E=9A=E4=B8=BE=E2=86=92?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E3=80=8D=E8=A1=A8=20+=20=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E4=B8=AD=E7=BB=A7=20+=20=E7=89=A9=E5=93=81=E5=90=8D=E7=A7=B0/?= =?UTF-8?q?=E7=B1=BB=E5=88=AB=E4=B8=AD=E6=96=87=E5=8F=AF=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 图标表:从 module_36659.js:5135 的 eA.upf(底盘)与 module_57128.js:840 的 Dn.duk(炮塔)解出 case→形参→实参→i(模块id)→resources.json 的映射(严格走 case→形参名→形参声明行→实参行,不假设 case 号等于实参位置,因为该模块实参表约 1000 项而工厂形参只有 714 个、多出的被丢弃)。自验:用同文件 :907 的 Dn.euk 抗性表 21 case 全命中且本地镜像均存在(去重 20 个文件)。规模:底盘 416 case(未解 1)、炮塔 20、抗性 21、7 格道具 7。B 物品信息:名称来自服务端模型 3935107155251354407(DescriptionModelCC)的 name 字段,类别中文用 lobby.garage.<枚举名>(ARMOR=底盘/WEAPON=炮塔/PAINT=迷彩/DRONE=辅助无人机…),15 件仓库物品名称 9/9、类别中文 9/9 命中;但图标 0/9 —— 承载底盘/炮塔型号的 TankInfoCC(5543162332469864498)在全部 68 个 s2c 帧与模板里 0 次出现,属数据缺失而非算法问题,已如实标注。C 素材中继 client/tools/AssetRelay.mjs(8130):/ 服务客户端产物、/play/static/** 映射 3DTank/web/play/static 本地镜像、正确 MIME 与宽 CORS、防目录穿越(%2e%2e 返回 400)。回归:CompareContainerDecode 5 帧/26950/0、selftest 1108/9/90、semantic --demo 全部断言通过、Kotlin/JS 构建 SUCCESSFUL。 --- client/src/jsMain/kotlin/Main.kt | 6 + .../src/jsMain/kotlin/Protocol/ClientState.kt | 113 ++++++ .../src/jsMain/kotlin/Protocol/ItemIcons.kt | 101 +++++ .../jsMain/kotlin/Protocol/ItemIconsData.kt | 6 + .../jsMain/kotlin/Protocol/ItemInfoDriver.kt | 144 ++++++++ client/src/jsMain/resources/ItemIcons.json | 1 + client/tools/AssetRelay.mjs | 189 ++++++++++ client/tools/ExportItemIcons.mjs | 347 ++++++++++++++++++ 8 files changed, 907 insertions(+) create mode 100644 client/src/jsMain/kotlin/Protocol/ItemIcons.kt create mode 100644 client/src/jsMain/kotlin/Protocol/ItemIconsData.kt create mode 100644 client/src/jsMain/kotlin/Protocol/ItemInfoDriver.kt create mode 100644 client/src/jsMain/resources/ItemIcons.json create mode 100644 client/tools/AssetRelay.mjs create mode 100644 client/tools/ExportItemIcons.mjs diff --git a/client/src/jsMain/kotlin/Main.kt b/client/src/jsMain/kotlin/Main.kt index 5ec6fa5..7f0533b 100644 --- a/client/src/jsMain/kotlin/Main.kt +++ b/client/src/jsMain/kotlin/Main.kt @@ -30,6 +30,7 @@ import Protocol.ContainerDump import Protocol.GarageDriver import Protocol.GarageSession import Protocol.IsNodeRuntime +import Protocol.ItemInfoDriver import Protocol.SelfTest import Protocol.SpaceDumpDriver @@ -157,6 +158,11 @@ fun main() { SpaceDumpDriver.Run() return } + // M13a 仓库物品可展示信息离线导出(TANKIC_ITEMINFO=1) + if (ItemInfoDriver.Wanted()) { + ItemInfoDriver.Run() + return + } // M5 车库因果闭环驱动(TANKIC_GARAGE=1) if (GarageDriver.Wanted()) { GarageDriver.Run() diff --git a/client/src/jsMain/kotlin/Protocol/ClientState.kt b/client/src/jsMain/kotlin/Protocol/ClientState.kt index 93bd679..7423266 100644 --- a/client/src/jsMain/kotlin/Protocol/ClientState.kt +++ b/client/src/jsMain/kotlin/Protocol/ClientState.kt @@ -85,6 +85,20 @@ internal class DevicesLoadedEntry( // 一条 UnloadObjects(lt) 记录 internal class UnloadEntry(val FrameNo: Int, val Ids: List) +// 仓库物品的可展示信息(M13a):名称 / 类别 / 稀有度 / 型号 / 图标。 +// 型号与图标推不出来时为 null(数据里没有 TankInfoCC,不猜、不拼文件名)。 +internal class ItemInfo( + val ItemId: String, + val ModelValue: Long?, + val IconFile: String?, + val Name: String?, + val CategoryRaw: String?, + val CategoryMember: String?, + val CategoryChinese: String, + val RarityRaw: String?, + val RarityMember: String?, +) + // 一个对象是否「挂载位」的两枚标记 modelId(依据见文件头 (2)) private const val SLOT_MARK_MODIFICATION = "2494628067134376039" private const val SLOT_MARK_DEVICES_AVAILABLE = "7391892068905080676" @@ -93,6 +107,30 @@ private const val SLOT_MARK_MODIFICATION_COUNT = 7 // 物品「装备槽类别」所在的模型项 modelId(module_85196::Lo)—— 依据见 ItemCategory 的头注 private const val CATEGORY_MODEL = "6117306895090150422" +// ---- 可展示物品信息(M13a)用到的模型 id / 字段名(全部来自 names/codec_fields.json,逐条可复核) +// +// 物品中文名:modelId=3935107155251354407(module_99527::N DescriptionModelCC),字段 name / description。 +// 稀有度: modelId=658797348514407988(module_85196::Fs ItemRarityCC),字段 rarity(线型 enum)。 +// 型号(底盘/炮塔):modelId=5543162332469864498(module_85183::Nb TankInfoCC,ModelName=TankInfoCC)。 +// · 该 codec 的字段(codec_fields.json:51248-51350)按序是: +// 1 (匿名) prop=okz_1 i64 ← 底盘型号 +// 2 hullDeviceId opt +// 3 (匿名) prop=qkz_1 i64 +// 4 (匿名) prop=rkz_1 i64 ← 炮塔型号 +// 5 weaponDeviceId opt +// 6 (匿名) prop=tkz_1 i64 +// 两个「型号」字段是匿名的,导出表里按 `(匿名#序)` 命名,故运行时键是 "(匿名#1)" / "(匿名#4)"。 +// · 客户端取图标入口(source-of-truth):车库 UI module_21300.js:7862-7908(rkz_1)/ +// 7950-7982(okz_1)逐值比对后调 hn.upf(u),即走 ItemIcons 的 Turret/Hull 表。 +// · ★如实标注:本账号抓包(space-100073070 各 s2c 帧)与 space-init-*.json 里**都没有**这个模型项, +// 所以仓库里这 15 件物品的「型号值」取不到(返回 null),图标也就推不出来 —— 不是算错,是数据里没有。 +private const val NAME_MODEL = "3935107155251354407" +private const val RARITY_MODEL = "658797348514407988" +private const val TANKINFO_MODEL = "5543162332469864498" +private const val TANKINFO_HULL_FIELD = "(匿名#1)" // 源码 prop okz_1(底盘型号) +private const val TANKINFO_WEAPON_FIELD = "(匿名#4)" // 源码 prop rkz_1(炮塔型号) +private const val RARITY_ENUM_SHORT = "Ma" // module_85196::Fs 的 rarity 线型 enum + // 类别枚举的查名上下文(**仅展示层**):codec module_85196::Lo 的 category 线型是 `enum`, // 而 ja 的成员表定义在 module_85667(TypeCodec=module_85667::pu)。导出 EnumMembers 时已按 // 「字段所在模块 → TypeCodec 模块」回退解析并把结果挂在字段所在模块(85196)键下,故这里传 85196。 @@ -164,6 +202,81 @@ internal class ClientState { return EnumNames.Label(CATEGORY_ENUM_MODULE, CATEGORY_ENUM_SHORT, Raw) } + // ---------------------------------------------------------------- 可展示物品信息(M13a) + + // 取某物品在 pt 里的某个模型项的字段表(Value);没有该模型项返回 null。 + private fun ModelFields(ItemId: String, ModelId: String): Map<*, *>? { + val O = PtByObjectId[ItemId] ?: return null + for (M in O.Models) if (M.ModelId == ModelId) return M.Value as? Map<*, *> + return null + } + + // 物品中文名:模型 3935107155251354407(DescriptionModelCC)的 name;空串/无此项返回 null。 + fun ItemName(ItemId: String): String? { + val V = ModelFields(ItemId, NAME_MODEL)?.get("name")?.toString() ?: return null + return V.ifEmpty { null } + } + + // 类别的**成员名**(如 ARMOR/WEAPON/PAINT/INVENTORY);查不到返回 null。 + fun ItemCategoryMember(ItemId: String): String? { + val Raw = ItemCategory(ItemId) ?: return null + return EnumNames.OfRaw(CATEGORY_ENUM_MODULE, CATEGORY_ENUM_SHORT, Raw) + } + + // 类别中文:先取成员名,再查 zh-CN 的 lobby.garage.<成员名>;缺键兜底成成员名;未解兜底成原始 enum#N。 + fun ItemCategoryChinese(ItemId: String): String { + val Raw = ItemCategory(ItemId) ?: return "?" + val Member = EnumNames.OfRaw(CATEGORY_ENUM_MODULE, CATEGORY_ENUM_SHORT, Raw) ?: return Raw + return ItemIcons.GarageLabel(Member) ?: Member + } + + // 稀有度(原始 enum#N 与成员名,成员表 module_85196::Fs 的 rarity 线型 enum);没有该模型项返回 null + fun ItemRarityRaw(ItemId: String): String? = ModelFields(ItemId, RARITY_MODEL)?.get("rarity")?.toString() + + fun ItemRarityMember(ItemId: String): String? { + val Raw = ItemRarityRaw(ItemId) ?: return null + return EnumNames.OfRaw(CATEGORY_ENUM_MODULE, RARITY_ENUM_SHORT, Raw) + } + + // 型号值:TankInfoCC(5543162332469864498)的 okz_1(底盘)/ rkz_1(炮塔),按类别选字段。 + // 没有该模型项 / 类别不是底盘炮塔 / 字段不是数字 ⇒ null(**不猜**)。 + fun ItemModelValue(ItemId: String): Long? { + val Member = ItemCategoryMember(ItemId) ?: return null + val Field = when (Member) { + "ARMOR" -> TANKINFO_HULL_FIELD + "WEAPON" -> TANKINFO_WEAPON_FIELD + else -> return null + } + val V = ModelFields(ItemId, TANKINFO_MODEL)?.get(Field) ?: return null + return (V as? Number)?.toLong() + } + + // 图标文件:型号值 + A 表(ItemIcons)推出;型号取不到就返回 null(不拼文件名、不按名字猜)。 + fun ItemIconFile(ItemId: String): String? { + val Mv = ItemModelValue(ItemId) ?: return null + return when (ItemCategoryMember(ItemId)) { + "ARMOR" -> ItemIcons.Hull(Mv) + "WEAPON" -> ItemIcons.Turret(Mv) + else -> null + } + } + + // 单件物品的可展示信息 + fun ItemInfo(ItemId: String): ItemInfo = ItemInfo( + ItemId = ItemId, + ModelValue = ItemModelValue(ItemId), + IconFile = ItemIconFile(ItemId), + Name = ItemName(ItemId), + CategoryRaw = ItemCategory(ItemId), + CategoryMember = ItemCategoryMember(ItemId), + CategoryChinese = ItemCategoryChinese(ItemId), + RarityRaw = ItemRarityRaw(ItemId), + RarityMember = ItemRarityMember(ItemId), + ) + + // 仓库物品(itemsOnDepot)逐件信息 + fun ItemInfos(): List = ItemsOnDepot.map { ItemInfo(it) } + // ---- 挂载位 / 设备 // devicesLoaded 在服务端是**覆盖写设备行**(见 3DTank/tools/server/responder.js 头注), // 所以按挂载位 id 收敛、后到者覆盖;同时保留收到过的记录条数以便追溯。 diff --git a/client/src/jsMain/kotlin/Protocol/ItemIcons.kt b/client/src/jsMain/kotlin/Protocol/ItemIcons.kt new file mode 100644 index 0000000..886b0c6 --- /dev/null +++ b/client/src/jsMain/kotlin/Protocol/ItemIcons.kt @@ -0,0 +1,101 @@ +// ItemIcons.kt —— 「型号枚举 → 官方图标文件」查询对象(M13a) +// +// 数据来源:client/src/jsMain/resources/ItemIcons.json(由 client/tools/ExportItemIcons.mjs 从官方 +// 解包源码 module_36659.js / module_57128.js / module_82347.js 解出,同一份 JSON 内嵌在 ItemIconsData.kt)。 +// +// 口径(为什么不在这里拼文件名):官方客户端从不拼文件名,而是 switch(型号枚举) → 一个**图标资源模块**; +// 表里存的就是那一层映射(case → "static/images/xxx.svg")。查不到就返回 null(不猜)。 +// +// 图标路径是相对 web/play 根的("static/images/..."), 客户端配素材中继(AssetRelay 的 /play 前缀)取用。 +package Protocol + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +// 导出的统计信息(自验/规模,给人看) +@Serializable +internal class ItemIconSelection( + val HullCases: Int = 0, + val HullMisses: Int = 0, + val TurretCases: Int = 0, + val TurretMisses: Int = 0, + val ResistanceCases: Int = 0, + val ResistanceMisses: Int = 0, + val SupplyCount: Int = 0, + val OtherTableCount: Int = 0, + val GarageLabelCount: Int = 0, +) + +@Serializable +internal class ItemIconSelfCheck( + val ResistanceCases: Int = 0, + val ResistanceResolved: Int = 0, + val ResistanceLocalFilesPresent: Int = 0, + val ResistanceDistinctFiles: Int = 0, + val Lines: List = emptyList(), + val HullMissLines: List = emptyList(), + val TurretMissLines: List = emptyList(), +) + +// 7 格道具图标表的一项:Key = 源码里产出「道具类型枚举」的函数名(局部名,语义未解,照抄不猜) +@Serializable +internal class SupplyIcon( + val Key: String = "", + val Icon: String? = null, + val ModuleId: String? = null, + val Line: Int = 0, +) + +// 自动扫描到的其它同类表(case → 图标) +@Serializable +internal class OtherIconTable( + val Module: String = "", + val Line: Int = 0, + val Cases: Int = 0, + val Icons: Int = 0, + val Sample: List = emptyList(), + val All: Map = emptyMap(), +) + +@Serializable +internal class ItemIconsData( + val Note: String = "", + val GeneratedAt: String = "", + val Source: String = "", + val Selection: ItemIconSelection = ItemIconSelection(), + val SelfCheck: ItemIconSelfCheck = ItemIconSelfCheck(), + // 型号(switch 的 case 值,十进制字符串)→ 图标文件 + val Hull: Map = emptyMap(), + val Turret: Map = emptyMap(), + val Resistance: Map = emptyMap(), + // 7 格道具图标表(按表内顺序) + val Supply: List = emptyList(), + val OtherTables: List = emptyList(), + // 显示层中文:lobby.garage.<成员名> → 中文 + val GarageLabels: Map = emptyMap(), +) + +internal object ItemIcons { + + private val JsonFormat = Json { ignoreUnknownKeys = true } + + // 懒加载:第一次用到时才解析那段 JSON + val Data: ItemIconsData by lazy { + JsonFormat.decodeFromString(ItemIconsData.serializer(), ItemIconsJsonText) + } + + // 底盘型号 → 图标文件;查不到返回 null + fun Hull(ModelValue: Long): String? = Data.Hull[ModelValue.toString()] + + // 炮塔型号 → 图标文件;查不到返回 null + fun Turret(ModelValue: Long): String? = Data.Turret[ModelValue.toString()] + + // 抗性型号 → 图标文件(自验用,也供防御模块显示);查不到返回 null + fun Resistance(ModelValue: Long): String? = Data.Resistance[ModelValue.toString()] + + // 7 格道具图标表的第 Index 项(0 起);越界/无图标返回 null + fun Supply(Index: Int): String? = Data.Supply.getOrNull(Index)?.Icon + + // 类别/稀有度成员名 → 中文(取 zh-CN 的 lobby.garage.<成员名>);缺键返回 null + fun GarageLabel(Member: String): String? = Data.GarageLabels[Member] +} diff --git a/client/src/jsMain/kotlin/Protocol/ItemIconsData.kt b/client/src/jsMain/kotlin/Protocol/ItemIconsData.kt new file mode 100644 index 0000000..32ff827 --- /dev/null +++ b/client/src/jsMain/kotlin/Protocol/ItemIconsData.kt @@ -0,0 +1,6 @@ +// 本文件由 client/tools/ExportItemIcons.mjs 生成,请勿手改。 +// 内容 = client/src/jsMain/resources/ItemIcons.json(同一份 JSON)。 +package Protocol + +internal const val ItemIconsJsonText: String = + "{\"Note\":\"物品图标表,由 client/tools/ExportItemIcons.mjs 从官方解包源码生成,请勿手改。Hull = module_36659.js 的 Mb(eA).upf switch:case(底盘型号)→ i(<模块id>) → resources.json 文件名;Turret = module_57128.js 的 ct(Dn).duk switch(炮塔型号);Resistance = 同文件 ct(Dn).euk switch(抗性,用于自验解析算法)。解析口径:case → 形参名 → 形参声明行 → 同序实参行 → i(<模块id>),不能假设「case 号 = 实参位置」。Supply = module_82347.js 的 7 格道具图标表(new ZMi 数组,非 switch)。OtherTables = 自动扫描到的其它同类 case→图标 表(供手榴弹/无人机/防御模块查用;没有就是空数组)。GarageLabels = zh-CN 的 lobby.garage.*(只取字符串叶子),显示层类别中文用。图标路径统一为 \\\"static/images/xxx.svg\\\"(相对 web/play 根,客户端配 AssetRelay 的 /play 前缀取用)。\",\"GeneratedAt\":\"2026-09-18T07:47:43.890Z\",\"Source\":\"3dtank.com/play/static/js/unpacked/{resources.json,module_36659.js,module_57128.js,module_82347.js} + 3DTank/web/play/static/translations/zh-CN.a2b303af.json\",\"Selection\":{\"HullCases\":416,\"HullMisses\":1,\"TurretCases\":20,\"TurretMisses\":0,\"ResistanceCases\":21,\"ResistanceMisses\":0,\"SupplyCount\":7,\"OtherTableCount\":3,\"GarageLabelCount\":108},\"SelfCheck\":{\"ResistanceCases\":21,\"ResistanceResolved\":21,\"ResistanceLocalFilesPresent\":21,\"ResistanceDistinctFiles\":20,\"Lines\":[\"case 23 -> static/images/firebird_resistance.785a9d6b.svg ✓本地存在\",\"case 25 -> static/images/smoky_resistance.845afc14.svg ✓本地存在\",\"case 26 -> static/images/tsunami_resistance.6200aad9.svg ✓本地存在\",\"case 27 -> static/images/scorpio_resistance.e8f1787f.svg ✓本地存在\",\"case 28 -> static/images/twins_resistance.ad189f61.svg ✓本地存在\",\"case 29 -> static/images/railgun_resistance.636a554f.svg ✓本地存在\",\"case 30 -> static/images/isis_resistance.30a69ffc.svg ✓本地存在\",\"case 31 -> static/images/mine_resistance.dd581c90.svg ✓本地存在\",\"case 32 -> static/images/thunder_resistance.6d7f4531.svg ✓本地存在\",\"case 33 -> static/images/freeze_resistance.33bdf642.svg ✓本地存在\",\"case 34 -> static/images/ricochet_resistance.8247beaa.svg ✓本地存在\",\"case 35 -> static/images/shaft_resistance.0778fd3e.svg ✓本地存在\",\"case 36 -> static/images/vulcan_resistance.824f6f0e.svg ✓本地存在\",\"case 37 -> static/images/hammer_resistance.6c549d29.svg ✓本地存在\",\"case 38 -> static/images/rocket_launcher_resistance.b7dfd64f.svg ✓本地存在\",\"case 39 -> static/images/artillery_resistance.9b4cbc34.svg ✓本地存在\",\"case 40 -> static/images/all_resistance.b45eae39.svg ✓本地存在\",\"case 41 -> static/images/gauss_resistance.bb8f409c.svg ✓本地存在\",\"case 42 -> static/images/tesla_resistance.3e686c8e.svg ✓本地存在\",\"case 43 -> static/images/crit_resistance.94e32312.svg ✓本地存在\",\"case 44 -> static/images/all_resistance.b45eae39.svg ✓本地存在\"]},\"Hull\":{\"0\":\"static/images/dot.69f2e150.svg\",\"1\":\"static/images/hudSupplyBackground.19f6b76c.svg\",\"2\":\"static/images/supplyKeyLabelBackground.fe3c4a5e.svg\",\"3\":\"static/images/background_flash.5c9bb879.svg\",\"4\":\"static/images/no_device.d6a5ed38.svg\",\"5\":\"static/images/jamming.27ca209f.svg\",\"6\":\"static/images/pierce.53f9cfc2.svg\",\"7\":\"static/images/freezing.ecb7c14a.svg\",\"8\":\"static/images/firebird_high_pressure_pump.a8244e4a.svg\",\"9\":\"static/images/compact_tanks.f7e14a7d.svg\",\"10\":\"static/images/thermite_mix.48b12b96.svg\",\"11\":\"static/images/self_heating.b24239b8.svg\",\"12\":\"static/images/firebird_magnetic.422b1b1c.svg\",\"13\":\"static/images/firebird_paralyzing.ce9a0315.svg\",\"14\":\"static/images/firebird_toxic.53f9cfc2.svg\",\"15\":\"static/images/crit_mix.bc4eeef4.svg\",\"16\":\"static/images/freeze_high_pressure_pump.a8244e4a.svg\",\"17\":\"static/images/corrosive_mix.48b12b96.svg\",\"18\":\"static/images/deep_cooling.9dee7d51.svg\",\"19\":\"static/images/self_cooling.7f72e2b9.svg\",\"20\":\"static/images/freeze_emp.999d4c2f.svg\",\"21\":\"static/images/freeze_stun.ce9a0315.svg\",\"22\":\"static/images/stable_mix.854e4ede.svg\",\"23\":\"static/images/vampire.369dbf7a.svg\",\"24\":\"static/images/damage_nanobots.88a331a6.svg\",\"25\":\"static/images/support_nanobots.58f8a26a.svg\",\"26\":\"static/images/broadband_emitters.9113bd6f.svg\",\"27\":\"static/images/isida_vampiric_nanobots.fd08d293.svg\",\"28\":\"static/images/isida_enduring_nanobots.c22db7a0.svg\",\"29\":\"static/images/incendiary_belt.d80e9a8e.svg\",\"30\":\"static/images/incendiary_bombs.50e991df.svg\",\"31\":\"static/images/faster_horizontal_tracking_vulcan.843faf29.svg\",\"32\":\"static/images/modified_firing_rate.f7c9fbd9.svg\",\"33\":\"static/images/machine_gun_rubber_bullets.b991ff76.svg\",\"34\":\"static/images/vulcan_shredder.62384918.svg\",\"35\":\"static/images/magnum_lafet.ffcd092b.svg\",\"36\":\"static/images/crio_bombs.70e1cab1.svg\",\"37\":\"static/images/modified_carriage.9b8f19d3.svg\",\"38\":\"static/images/minelayer.23123790.svg\",\"39\":\"static/images/incendiary_shot.6fa2b96d.svg\",\"40\":\"static/images/accelerated_loading_mechanism.4e9d4492.svg\",\"41\":\"static/images/magnum_carronade.2174f9ae.svg\",\"42\":\"static/images/hyperspace_projectiles.b4d4c1bb.svg\",\"43\":\"static/images/destabilized_shells.fe1994b2.svg\",\"44\":\"static/images/capacitor_power_reduction.36d059ee.svg\",\"45\":\"static/images/adaptive_recharge_railgun.ca70ee86.svg\",\"46\":\"static/images/faster_horizontal_tracking_railgun.fef446a1.svg\",\"47\":\"static/images/high_caliber_ammo.7d27e891.svg\",\"48\":\"static/images/shell_stabilization.1721fbfe.svg\",\"49\":\"static/images/railgun_incendiary projectiles.f37f8a30.svg\",\"50\":\"static/images/railgun_emp_projectiles.a38fb7ed.svg\",\"51\":\"static/images/railgun_freezing_projectiles.d1dda9d2.svg\",\"52\":\"static/images/railgun_stun_projectiles.ce9a0315.svg\",\"53\":\"static/images/railgun_hyperspeed_shells.97017b02.svg\",\"54\":\"static/images/railgun_shotgun.40a0fb07.svg\",\"55\":\"static/images/excalibur.04bf0648.svg\",\"56\":\"static/images/railgun_detonator.ab8cc577.svg\",\"57\":\"static/images/adapted_charging.57598ece.svg\",\"58\":\"static/images/plasma_torch.b06f598e.svg\",\"59\":\"static/images/unstable_plazma.9ab051b7.svg\",\"60\":\"static/images/minus_field_stabilization.5a5754eb.svg\",\"61\":\"static/images/incineratingRicochet.d5e1474a.svg\",\"62\":\"static/images/crioFieldRicochet.d1dda9d2.svg\",\"63\":\"static/images/maghetronRicochet.13b5e353.svg\",\"64\":\"static/images/tectonicRicochet.ce9a0315.svg\",\"65\":\"static/images/pulsarRicochet.af282303.svg\",\"66\":\"static/images/heliosRicochet.2d0f9e6c.svg\",\"67\":\"static/images/radio_explosion.e80117ad.svg\",\"68\":\"static/images/stun_striker.ce9a0315.svg\",\"69\":\"static/images/bfg.9a60aeb4.svg\",\"70\":\"static/images/rocket_launcher_hunter.517e6c9c.svg\",\"71\":\"static/images/rocket_launcher_cyclone.deb25fdb.svg\",\"72\":\"static/images/striker_tandem_missile_launcher.9859b400.svg\",\"73\":\"static/images/striker_hydra_missile_launcher.0728e898.svg\",\"74\":\"static/images/striker_faust_missile_launcher.07f4364a.svg\",\"75\":\"static/images/railgun_freezing_projectiles.d1dda9d2.svg\",\"76\":\"static/images/striker_meteor.cf9e3d55.svg\",\"77\":\"static/images/jammer.a43c0424.svg\",\"78\":\"static/images/lightweight_capacitors.89165c09.svg\",\"79\":\"static/images/robo_crosshair.2ef965e2.svg\",\"80\":\"static/images/triple_shot.53afa709.svg\",\"81\":\"static/images/assault_emitters.7d27e891.svg\",\"82\":\"static/images/heavy_capacitors.6a7b069b.svg\",\"83\":\"static/images/shaft_healing_emitters.94038ef9.svg\",\"84\":\"static/images/adaptive_recharge_hammer.f9c879de.svg\",\"85\":\"static/images/double_barrel.85637e6b.svg\",\"86\":\"static/images/dragons_breath.fe8b0feb.svg\",\"87\":\"static/images/high_capacity_drum.2ed109c1.svg\",\"88\":\"static/images/slugger.0ff79d90.svg\",\"89\":\"static/images/hammer_blunderbuss.1ef20536.svg\",\"90\":\"static/images/hammerWyversBreath.d1dda9d2.svg\",\"91\":\"static/images/hammerStunningPellets.ce9a0315.svg\",\"92\":\"static/images/hammerMagneticPellets.b50a317e.svg\",\"93\":\"static/images/hammer_heavy_slugger.7a0d279d.svg\",\"94\":\"static/images/hammer_hunter_duplet.b6718440.svg\",\"95\":\"static/images/freezing_shot.d1dda9d2.svg\",\"96\":\"static/images/super_cumulative.7d27e891.svg\",\"97\":\"static/images/auto_turret.856b1a96.svg\",\"98\":\"static/images/precision_targeting_system.1721fbfe.svg\",\"99\":\"static/images/assault_ammunition.53afa709.svg\",\"100\":\"static/images/smoky_stun.ce9a0315.svg\",\"101\":\"static/images/smoky_emp_shell.e95b61b7.svg\",\"102\":\"static/images/smoky_rubber.b991ff76.svg\",\"103\":\"static/images/smoky_explosive.fe1994b2.svg\",\"104\":\"static/images/smoky_sorted_ammo.531b9ad7.svg\",\"105\":\"static/images/hyperspeed_shells.97017b02.svg\",\"106\":\"static/images/hammer_shells.7a262076.svg\",\"107\":\"static/images/lightweight_ordnance_autoloader.db3c43fc.svg\",\"108\":\"static/images/armor_piercing_ammo.3a9177d1.svg\",\"109\":\"static/images/adaptive_reload.321ac9df.svg\",\"110\":\"static/images/anvil_shells.b4d4c1bb.svg\",\"111\":\"static/images/corrosion_shells.53f9cfc2.svg\",\"112\":\"static/images/hyperspeed_shells.97017b02.svg\",\"113\":\"static/images/magneeto_shells.05362150.svg\",\"114\":\"static/images/nanotech_shells.94038ef9.svg\",\"115\":\"static/images/noise_shells.27ca209f.svg\",\"116\":\"static/images/peacemaker_shells.ce9a0315.svg\",\"117\":\"static/images/salamandra_shells.c59e2245.svg\",\"118\":\"static/images/strict_loading_ammunition.531b9ad7.svg\",\"119\":\"static/images/vivern_shells.d1dda9d2.svg\",\"120\":\"static/images/gauss_emp.04e86cc8.svg\",\"121\":\"static/images/super_solenoids.136bdfd5.svg\",\"122\":\"static/images/faster_horizontal_tracking.610c4011.svg\",\"123\":\"static/images/hacked_processor.c787fca4.svg\",\"124\":\"static/images/hyperspeed_shells.97017b02.svg\",\"125\":\"static/images/jammer.a43c0424.svg\",\"126\":\"static/images/large_caliber.246743df.svg\",\"127\":\"static/images/solenoid_cooling.a308d483.svg\",\"128\":\"static/images/nemesis.84239c03.svg\",\"129\":\"static/images/modulators_of_plasma.d4d50811.svg\",\"130\":\"static/images/bfg_twins.ddef2ac9.svg\",\"131\":\"static/images/stable_plazma.5046b47c.svg\",\"132\":\"static/images/plazma_core_accelerators.76f38ac6.svg\",\"133\":\"static/images/TwinsVaporizer.ac45b820.svg\",\"134\":\"static/images/TwinsCryotron.d1dda9d2.svg\",\"135\":\"static/images/TwinsMagnetron.2b2616a5.svg\",\"136\":\"static/images/TwinsTectonicPlasma.ce9a0315.svg\",\"137\":\"static/images/TwinsPlasmaInjector.53f9cfc2.svg\",\"138\":\"static/images/TwinsPlasmaDisruptor.27ca209f.svg\",\"139\":\"static/images/TwinsPlasmaTurboAccelerators.9a195099.svg\",\"140\":\"static/images/twins_plasmatron.4294d6dd.svg\",\"141\":\"static/images/accelerated_lightning.4b384476.svg\",\"142\":\"static/images/penetration_effect.21deab47.svg\",\"143\":\"static/images/stabilized_lightning.5b5469d1.svg\",\"144\":\"static/images/electroturret.5503cb45.svg\",\"145\":\"static/images/freeze_effect.b9cc4b3f.svg\",\"146\":\"static/images/minus_field.0c494806.svg\",\"147\":\"static/images/stun_effect.d3e40fe1.svg\",\"148\":\"static/images/emi_effect.c537df83.svg\",\"149\":\"static/images/shock_therapy.c11c390d.svg\",\"150\":\"static/images/large_caliber.fe4da415.svg\",\"151\":\"static/images/assault_magazine.77d2e1cd.svg\",\"152\":\"static/images/armour_piercing.53f9cfc2.svg\",\"153\":\"static/images/cryo.d1dda9d2.svg\",\"154\":\"static/images/emp.4e760200.svg\",\"155\":\"static/images/explosive_shells.fe1994b2.svg\",\"156\":\"static/images/explosive_warheads.d05207d0.svg\",\"157\":\"static/images/incendiary.8867f776.svg\",\"158\":\"static/images/swarm.1c109353.svg\",\"159\":\"static/images/tornado.deb25fdb.svg\",\"160\":\"static/images/wolfpack.517e6c9c.svg\",\"161\":\"static/images/stun.ce9a0315.svg\",\"162\":\"static/images/uran_shells.b991ff76.svg\",\"163\":\"static/images/javelin.1721fbfe.svg\",\"164\":\"static/images/adaptive_reload.0e12f909.svg\",\"165\":\"static/images/rapier_mode.a254fcd0.svg\",\"166\":\"static/images/manual_mode.246743df.svg\",\"167\":\"static/images/explosive_shells.c68f37c5.svg\",\"168\":\"static/images/tungstenshells.f19a2433.svg\",\"169\":\"static/images/duplet.8da7d9f3.svg\",\"170\":\"static/images/megatsunami.a308d483.svg\",\"171\":\"static/images/triplet.7da6b70e.svg\",\"172\":\"static/images/autocannon.47db9e92.svg\",\"173\":\"static/images/sorted_ammunition.6a7811e3.svg\",\"174\":\"static/images/hyperspeed_shells.97017b02.svg\",\"175\":\"static/images/north_star.4eb10738.svg\",\"176\":\"static/images/uranium_shells.ca6f15fc.svg\",\"177\":\"static/images/adaptive_autocannon.c0f4334a.svg\",\"178\":\"static/images/phoenix.d1b99809.svg\",\"179\":\"static/images/demoman.d52cfed6.svg\",\"180\":\"static/images/driver.0c6e1a01.svg\",\"181\":\"static/images/engineer.81e7a551.svg\",\"182\":\"static/images/miner.9e793eb7.svg\",\"183\":\"static/images/rescuer.dab2966b.svg\",\"184\":\"static/images/stun.ce9a0315.svg\",\"185\":\"static/images/bombard.a58f8bea.svg\",\"186\":\"static/images/emp.04e86cc8.svg\",\"187\":\"static/images/parkour.02e3455b.svg\",\"188\":\"static/images/adrenalin.6c46d7e2.svg\",\"189\":\"static/images/grenadier.d3753e03.svg\",\"190\":\"static/images/destroyer.d849105e.svg\",\"191\":\"static/images/quasar.ca409d7b.svg\",\"192\":\"static/images/bolter.e897e58a.svg\",\"193\":\"static/images/hull_pierce_immunity.53f9cfc2.svg\",\"194\":\"static/images/hull_overheat_immunity.ceb4cd29.svg\",\"195\":\"static/images/hull_overheat_protection.6ab44f29.svg\",\"196\":\"static/images/hull_freezing_immunity.d1dda9d2.svg\",\"197\":\"static/images/hull_freezing_protection.1e959682.svg\",\"198\":\"static/images/hull_emp_immunity.7d4854f7.svg\",\"199\":\"static/images/hull_default.56ad9071.svg\",\"200\":\"static/images/hull_less_mass.8a135999.svg\",\"201\":\"static/images/hull_more_mass.474ae82a.svg\",\"202\":\"static/images/hull_stun_immunity.ce9a0315.svg\",\"203\":\"static/images/button.22e4056c.svg\",\"204\":\"static/images/buttonReady.53dd51e7.svg\",\"205\":\"static/images/dictator.48e6b050.svg\",\"206\":\"static/images/hornet.e403abbc.svg\",\"207\":\"static/images/hunter.faef601d.svg\",\"208\":\"static/images/juggernaut.77e55855.svg\",\"209\":\"static/images/mammonth.24f55e8c.svg\",\"210\":\"static/images/titan.885b04ed.svg\",\"211\":\"static/images/viking.23c652d9.svg\",\"212\":\"static/images/wasp.72c29025.svg\",\"213\":\"static/images/paladin.d9a9fa6e.svg\",\"214\":\"static/images/ares.3e860aa7.svg\",\"215\":\"static/images/hopper.7de9d626.svg\",\"216\":\"static/images/crusader.8d6be837.svg\",\"217\":\"static/images/ctf_blue_base.13dd239f.svg\",\"218\":\"static/images/ctf_blue_base_no_flag.94a2bf33.svg\",\"219\":\"static/images/ctf_red_base.725359a2.svg\",\"220\":\"static/images/ctf_red_base_no_flag.8053d3fa.svg\",\"221\":\"static/images/ctf_blue_delivering.51894cf2.svg\",\"222\":\"static/images/ctf_blue_delivering_down.f9808b5c.svg\",\"223\":\"static/images/ctf_red_delivering.05f7555f.svg\",\"224\":\"static/images/ctf_red_delivering_down.ee038857.svg\",\"225\":\"static/images/rgb_free.0ba25488.svg\",\"226\":\"static/images/rgb_blue_delivering.6c8a284b.svg\",\"227\":\"static/images/rgb_red_delivering.8ebc011a.svg\",\"228\":\"static/images/rgb_blue_base.48f0209e.svg\",\"229\":\"static/images/rgb_red_base.f7d852a9.svg\",\"230\":\"static/images/asl_blue_base.48f0209e.svg\",\"231\":\"static/images/asl_red_base.f7d852a9.svg\",\"232\":\"static/images/jgr_red.8e25466e.svg\",\"233\":\"static/images/jgr_blue.c3fe3a20.svg\",\"234\":\"static/images/jgr_red_ally.ad048a31.svg\",\"235\":\"static/images/jgr_blue_ally.051eb141.svg\",\"236\":\"static/images/jgr_red_hostile.8e25466e.svg\",\"237\":\"static/images/jgr_blue_hostile.c3fe3a20.svg\",\"238\":\"static/images/blue-base-indicator.adc00d07.svg\",\"239\":\"static/images/red-base-indicator.f2de75f8.svg\",\"240\":\"static/images/blue-attack-indicator.9e8adfa6.svg\",\"241\":\"static/images/red-attack-indicator.dc2612ad.svg\",\"242\":\"static/images/blue-a.c46da4bf.svg\",\"243\":\"static/images/blue-b.fad1cee0.svg\",\"244\":\"static/images/blue-c.c21fd0b1.svg\",\"245\":\"static/images/blue-d.a8b80bd8.svg\",\"246\":\"static/images/blue-e.6a26c991.svg\",\"247\":\"static/images/blue-f.36e513e4.svg\",\"248\":\"static/images/blue-g.691ccdc7.svg\",\"249\":\"static/images/blue-h.84e8d693.svg\",\"250\":\"static/images/indicator-blue.23ed1adc.svg\",\"251\":\"static/images/red-a.e82caf68.svg\",\"252\":\"static/images/red-b.b3fd135b.svg\",\"253\":\"static/images/red-c.49c46faa.svg\",\"254\":\"static/images/red-d.3fd7d7c2.svg\",\"255\":\"static/images/red-e.5d14c195.svg\",\"256\":\"static/images/red-f.f4e866c2.svg\",\"257\":\"static/images/red-g.f33ddb69.svg\",\"258\":\"static/images/red-h.c8a10d9a.svg\",\"259\":\"static/images/neutral-a.24abb664.svg\",\"260\":\"static/images/neutral-b.6371af26.svg\",\"261\":\"static/images/neutral-c.a18f381d.svg\",\"262\":\"static/images/neutral-d.4c0f6412.svg\",\"263\":\"static/images/neutral-e.43e9f5ee.svg\",\"264\":\"static/images/neutral-f.8d5ff476.svg\",\"265\":\"static/images/neutral-g.1560f6ee.svg\",\"266\":\"static/images/neutral-h.44771d90.svg\",\"267\":\"static/images/blue-a.3c4e9fd3.svg\",\"268\":\"static/images/blue-b.626263b9.svg\",\"269\":\"static/images/blue-c.0f460fde.svg\",\"270\":\"static/images/blue-d.4bc1fb70.svg\",\"271\":\"static/images/blue-e.540e34eb.svg\",\"272\":\"static/images/blue-f.ed409afd.svg\",\"273\":\"static/images/blue-g.d42ae2fe.svg\",\"274\":\"static/images/blue-h.64d9bd9a.svg\",\"275\":\"static/images/red-a.4bb56d31.svg\",\"276\":\"static/images/red-b.f8a6ca02.svg\",\"277\":\"static/images/red-c.0da087f5.svg\",\"278\":\"static/images/red-d.0984fa7a.svg\",\"279\":\"static/images/red-e.8f24ab84.svg\",\"280\":\"static/images/red-f.786788ab.svg\",\"281\":\"static/images/red-g.710c2485.svg\",\"282\":\"static/images/red-h.3dec069d.svg\",\"283\":\"static/images/neutral-a.42ac9dd1.svg\",\"284\":\"static/images/neutral-b.678833d3.svg\",\"285\":\"static/images/neutral-c.0c9c83e3.svg\",\"286\":\"static/images/neutral-d.a9f4ddf1.svg\",\"287\":\"static/images/neutral-e.d525937c.svg\",\"288\":\"static/images/neutral-f.128b0328.svg\",\"289\":\"static/images/neutral-g.38b68a23.svg\",\"290\":\"static/images/neutral-h.3276e30e.svg\",\"291\":\"static/images/indicator-red-sge.09547444.svg\",\"292\":\"static/images/indicator-blue-sge.45d60052.svg\",\"293\":\"static/images/red-indicator.285d02f6.svg\",\"294\":\"static/images/blue-indicator.82508438.svg\",\"295\":\"static/images/red-indicator.6d32aa0e.svg\",\"296\":\"static/images/white-indicator-left.890affb7.svg\",\"297\":\"static/images/white-indicator-right.d2c948fb.svg\",\"298\":\"static/images/blue-indicator.1208edf8.svg\",\"299\":\"static/images/red-indicator.ffb20aae.svg\",\"300\":\"static/images/blue-indicator.2704912b.svg\",\"301\":\"static/images/red-indicator.cd1c9bd3.svg\",\"302\":\"static/images/white-indicator-left.fdae109e.svg\",\"303\":\"static/images/white-indicator-right.1ce33d9b.svg\",\"304\":\"static/images/blue-indicator.1208edf8.svg\",\"305\":\"static/images/red-indicator.ffb20aae.svg\",\"306\":\"static/images/progress-background.9f69a68c.svg\",\"307\":\"static/images/score.f4ee69d2.svg\",\"308\":\"static/images/tjr-score-blue.c7565943.svg\",\"309\":\"static/images/tjr-score-red.f5aee776.svg\",\"310\":\"static/images/ar-indicator-red.59875923.svg\",\"311\":\"static/images/ar-indicator-blue.c2910b77.svg\",\"312\":\"static/images/blue-health-indicator.acf2ce82.svg\",\"313\":\"static/images/green-health-indicator.d0d7151e.svg\",\"314\":\"static/images/red-health-indicator.3c0e5d61.svg\",\"315\":\"static/images/weapon-reload-indicator.eaa91ece.svg\",\"316\":\"static/images/armorWithBackground.3a887dad.svg\",\"317\":\"static/images/damageWithBackground.4a80d6f3.svg\",\"318\":\"static/images/nitroWithBackground.9d0ffef7.svg\",\"319\":\"static/images/mineWithBackground.eced36f5.svg\",\"320\":\"static/images/repairWithBackground.58991a46.svg\",\"321\":\"static/images/ultimateWithBackground.d18cf95d.svg\",\"322\":\"static/images/booster_armor.3dfb0e1e.svg\",\"323\":\"static/images/booster_damage.c3e7e6b3.svg\",\"324\":\"static/images/booster_speed.6b64eec3.svg\",\"325\":\"static/images/emp.65915d57.svg\",\"326\":\"static/images/freezing.bff72869.svg\",\"327\":\"static/images/immortality.6f56f3ad.svg\",\"328\":\"static/images/polarization.f4c89bee.svg\",\"329\":\"static/images/rescue.3fb29ae9.svg\",\"330\":\"static/images/overheat.c5da6580.svg\",\"331\":\"static/images/stun.afaad6e7.svg\",\"332\":\"static/images/jamming.42ebef54.svg\",\"333\":\"static/images/supercharge.df85d848.svg\",\"335\":\"static/images/pierce.349b551d.svg\",\"336\":\"static/images/overdrive_ready.b56250e7.svg\",\"337\":\"static/images/overdrive_active.cfe36fdd.svg\",\"338\":\"static/images/armorWithoutBackground.7c123edf.svg\",\"339\":\"static/images/damageWithoutBackground.4fb8a2b2.svg\",\"340\":\"static/images/nitroWithoutBackground.bf86a6bd.svg\",\"341\":\"static/images/repairWithoutBackground.b9e452b0.svg\",\"342\":\"static/images/ultimateWithoutBackground.18426912.svg\",\"343\":\"static/images/overdrive_active.f6ac659d.svg\",\"344\":\"static/images/booster_armor.e4f3bf2e.svg\",\"345\":\"static/images/booster_damage.eea41239.svg\",\"346\":\"static/images/booster_speed.323be23b.svg\",\"347\":\"static/images/emp.f3975b48.svg\",\"348\":\"static/images/freezing.7f014edf.svg\",\"349\":\"static/images/rescue.8306a7b4.svg\",\"350\":\"static/images/immortality.c6cb0f55.svg\",\"351\":\"static/images/polarization.8633cb05.svg\",\"352\":\"static/images/overheat.89c0e772.svg\",\"353\":\"static/images/stun.074235c8.svg\",\"354\":\"static/images/jamming.22e2534d.svg\",\"355\":\"static/images/supercharge.d24b4ed3.svg\",\"356\":\"static/images/suicide.fa6ce4b0.svg\",\"357\":\"static/images/pierce.b182be03.svg\",\"358\":\"static/images/background.6252e553.svg\",\"359\":\"static/images/tank.ea532f21.svg\",\"360\":\"static/images/tank-above.ac5b8ccc.svg\",\"361\":\"static/images/tank-below.4ec90bd7.svg\",\"362\":\"static/images/tank-dead.4e8ccc19.svg\",\"363\":\"static/images/change_equipment.66cf72d8.svg\",\"364\":\"static/images/self_destroy.88e26ae8.svg\",\"365\":\"static/images/join_battle.b0903eb3.svg\",\"366\":\"static/images/leave_battle.b562acdc.svg\",\"367\":\"static/images/crystalSmall.242e6c15.svg\",\"368\":\"static/images/fund.7496d0c5.svg\",\"369\":\"static/images/tsunami_resistance.6200aad9.svg\",\"370\":\"static/images/firebird_resistance.785a9d6b.svg\",\"371\":\"static/images/freeze_resistance.33bdf642.svg\",\"372\":\"static/images/isis_resistance.30a69ffc.svg\",\"373\":\"static/images/vulcan_resistance.824f6f0e.svg\",\"374\":\"static/images/artillery_resistance.9b4cbc34.svg\",\"375\":\"static/images/railgun_resistance.636a554f.svg\",\"376\":\"static/images/ricochet_resistance.8247beaa.svg\",\"377\":\"static/images/rocket_launcher_resistance.b7dfd64f.svg\",\"378\":\"static/images/shaft_resistance.0778fd3e.svg\",\"379\":\"static/images/hammer_resistance.6c549d29.svg\",\"380\":\"static/images/smoky_resistance.845afc14.svg\",\"381\":\"static/images/thunder_resistance.6d7f4531.svg\",\"382\":\"static/images/gauss_resistance.bb8f409c.svg\",\"383\":\"static/images/twins_resistance.ad189f61.svg\",\"384\":\"static/images/tesla_resistance.3e686c8e.svg\",\"385\":\"static/images/scorpio_resistance.e8f1787f.svg\",\"386\":\"static/images/Jugger.b9550587.svg\",\"387\":\"static/images/bonk.37ae6633.svg\",\"388\":\"static/images/defaultDevice.47b22a7c.svg\",\"389\":\"static/images/mine_resistance.dd581c90.svg\",\"390\":\"static/images/all_resistance.b45eae39.svg\",\"391\":\"static/images/crit_resistance.94e32312.svg\",\"392\":\"static/images/Dictator.770a5277.svg\",\"393\":\"static/images/Hornet.aa9c33f2.svg\",\"394\":\"static/images/Mammoth.1c266702.svg\",\"395\":\"static/images/Titan.d2bf1806.svg\",\"396\":\"static/images/Viking.45549e00.svg\",\"397\":\"static/images/Wasp.63098bb6.svg\",\"398\":\"static/images/Hunter.82b7d58f.svg\",\"399\":\"static/images/Ares.e0b4f363.svg\",\"400\":\"static/images/Hopper.17d5fd98.svg\",\"401\":\"static/images/Crusader.f14927dc.svg\",\"402\":\"static/images/Jugger.b9550587.svg\",\"403\":\"static/images/Paladin.7b656d82.svg\",\"404\":\"static/images/defaultDevice.47b22a7c.svg\",\"405\":\"static/images/burn_effect.7ac3028a.svg\",\"406\":\"static/images/revolver.3f7f2a0e.svg\",\"407\":\"static/images/camper.1d12b50f.svg\",\"408\":\"static/images/excelsior.4ea675f7.svg\",\"409\":\"static/images/knuckles.1b4b2cd3.svg\",\"410\":\"static/images/boxer.de1f9ba8.svg\",\"411\":\"static/images/stellarator.b5b7ca7a.svg\",\"412\":\"static/images/explosive_shell.77ac7b70.svg\",\"413\":\"static/images/vacuum_shell.b1432566.svg\",\"414\":\"static/images/yamato.e9264376.svg\",\"415\":\"static/images/high_voltage.7a7ef903.svg\",\"416\":\"static/images/needler.91bf89e9.svg\"},\"Turret\":{\"5\":\"static/images/UltraResist.fc42ccb2.svg\",\"6\":\"static/images/Firebird.f17e7721.svg\",\"7\":\"static/images/Smoky.a2623c59.svg\",\"8\":\"static/images/Scorpion.17f0ca64.svg\",\"9\":\"static/images/Twins.0809b017.svg\",\"10\":\"static/images/Railgun.e2aea740.svg\",\"11\":\"static/images/Isida.e0a284b9.svg\",\"12\":\"static/images/Mine.de6d4b99.svg\",\"13\":\"static/images/Thunder.5e4fe37f.svg\",\"14\":\"static/images/Freeze.51cbde00.svg\",\"15\":\"static/images/Ricochet.126321e9.svg\",\"16\":\"static/images/new_icon_shaft.4a28c47a.svg\",\"17\":\"static/images/Hammer.d7ddf1ba.svg\",\"18\":\"static/images/Vulcan.eb748cd8.svg\",\"19\":\"static/images/Striker.4d0c2fad.svg\",\"20\":\"static/images/Magnum.54718a7f.svg\",\"21\":\"static/images/Gauss.da869a90.svg\",\"22\":\"static/images/Tesla.da7544a4.svg\",\"23\":\"static/images/Critical.41589075.svg\",\"24\":\"static/images/Tsunami.1dff89b7.svg\"},\"Resistance\":{\"23\":\"static/images/firebird_resistance.785a9d6b.svg\",\"25\":\"static/images/smoky_resistance.845afc14.svg\",\"26\":\"static/images/tsunami_resistance.6200aad9.svg\",\"27\":\"static/images/scorpio_resistance.e8f1787f.svg\",\"28\":\"static/images/twins_resistance.ad189f61.svg\",\"29\":\"static/images/railgun_resistance.636a554f.svg\",\"30\":\"static/images/isis_resistance.30a69ffc.svg\",\"31\":\"static/images/mine_resistance.dd581c90.svg\",\"32\":\"static/images/thunder_resistance.6d7f4531.svg\",\"33\":\"static/images/freeze_resistance.33bdf642.svg\",\"34\":\"static/images/ricochet_resistance.8247beaa.svg\",\"35\":\"static/images/shaft_resistance.0778fd3e.svg\",\"36\":\"static/images/vulcan_resistance.824f6f0e.svg\",\"37\":\"static/images/hammer_resistance.6c549d29.svg\",\"38\":\"static/images/rocket_launcher_resistance.b7dfd64f.svg\",\"39\":\"static/images/artillery_resistance.9b4cbc34.svg\",\"40\":\"static/images/all_resistance.b45eae39.svg\",\"41\":\"static/images/gauss_resistance.bb8f409c.svg\",\"42\":\"static/images/tesla_resistance.3e686c8e.svg\",\"43\":\"static/images/crit_resistance.94e32312.svg\",\"44\":\"static/images/all_resistance.b45eae39.svg\"},\"Supply\":[{\"Key\":\"n1()\",\"Icon\":\"static/images/Ultimate.94f7d547.svg\",\"ModuleId\":\"5012\",\"Line\":91126},{\"Key\":\"nS()\",\"Icon\":\"static/images/Repair.b9f61bf0.svg\",\"ModuleId\":\"64414\",\"Line\":91127},{\"Key\":\"iS()\",\"Icon\":\"static/images/Double armor.064daed8.svg\",\"ModuleId\":\"64379\",\"Line\":91128},{\"Key\":\"rS()\",\"Icon\":\"static/images/Double damage.868dc39c.svg\",\"ModuleId\":\"53749\",\"Line\":91129},{\"Key\":\"eS()\",\"Icon\":\"static/images/Double speed.b917b469.svg\",\"ModuleId\":\"55271\",\"Line\":91130},{\"Key\":\"uS()\",\"Icon\":\"static/images/Mine.cf7c3a30.svg\",\"ModuleId\":\"22512\",\"Line\":91131},{\"Key\":\"i1()\",\"Icon\":\"static/images/Gold box.6efd6936.svg\",\"ModuleId\":\"69604\",\"Line\":91132}],\"OtherTables\":[{\"Module\":\"module_82347.js\",\"Line\":37290,\"Cases\":6,\"Icons\":6,\"Sample\":[\"0=static/images/iconCasualGray.3eea12e7.svg\",\"1=static/images/uncommon.ca77d7da.svg\",\"2=static/images/iconRareBlue.4e3c7303.svg\",\"3=static/images/iconEpicFiolet.d91b1151.svg\"],\"All\":{\"0\":\"static/images/iconCasualGray.3eea12e7.svg\",\"1\":\"static/images/uncommon.ca77d7da.svg\",\"2\":\"static/images/iconRareBlue.4e3c7303.svg\",\"3\":\"static/images/iconEpicFiolet.d91b1151.svg\",\"4\":\"static/images/iconLegendaryGold.7c76cb29.svg\",\"5\":\"static/images/iconCustomiseRed.2b5c8828.svg\"}},{\"Module\":\"module_82347.js\",\"Line\":62117,\"Cases\":7,\"Icons\":7,\"Sample\":[\"0=static/images/iconCustomiseRed.2b5c8828.svg\",\"1=static/images/iconLegendaryGold.7c76cb29.svg\",\"2=static/images/iconEpicFiolet.d91b1151.svg\",\"3=static/images/iconRareBlue.4e3c7303.svg\"],\"All\":{\"0\":\"static/images/iconCustomiseRed.2b5c8828.svg\",\"1\":\"static/images/iconLegendaryGold.7c76cb29.svg\",\"2\":\"static/images/iconEpicFiolet.d91b1151.svg\",\"3\":\"static/images/iconRareBlue.4e3c7303.svg\",\"4\":\"static/images/uncommon.ca77d7da.svg\",\"5\":\"static/images/iconCasualGray.3eea12e7.svg\",\"6\":\"static/images/favoriteStar.1ce58570.svg\"}},{\"Module\":\"module_82347.js\",\"Line\":118947,\"Cases\":17,\"Icons\":17,\"Sample\":[\"0=static/images/privateIcon.4427d3a1.svg\",\"1=static/images/BonusBoxes.59bbac9e.svg\",\"2=static/images/Upgrades.fe5718ef.svg\",\"3=static/images/Devices.ae5dfc92.svg\"],\"All\":{\"0\":\"static/images/privateIcon.4427d3a1.svg\",\"1\":\"static/images/BonusBoxes.59bbac9e.svg\",\"2\":\"static/images/Upgrades.fe5718ef.svg\",\"3\":\"static/images/Devices.ae5dfc92.svg\",\"4\":\"static/images/Autobalance.e011e48f.svg\",\"5\":\"static/images/ClanWar.18ddff9a.svg\",\"6\":\"static/images/GoldBoxes.eea52483.svg\",\"7\":\"static/images/FriendFire.cde8d0d5.svg\",\"8\":\"static/images/WeaponChange.89d8adcc.svg\",\"9\":\"static/images/Overdrives.ac06ab56.svg\",\"10\":\"static/images/Supplies.57cf4b22.svg\",\"11\":\"static/images/Nuclear.6c595d3c.svg\",\"12\":\"static/images/SmartSupplies.d7f811ef.svg\",\"13\":\"static/images/Drones.07e10f28.svg\",\"14\":\"static/images/ProtectionModules.41103d70.svg\",\"15\":\"static/images/Valuable.b03ff5d2.svg\",\"16\":\"static/images/Grenades.25dcf096.svg\"}}],\"GarageLabels\":{\"GRENADE\":\"手榴弹\",\"TITLE\":\"车库\",\"GEAR_SCORE\":\"装备分数\",\"TURRET\":\"炮塔\",\"DAMAGE\":\"伤害\",\"IMPACT_FORCE\":\"冲击力\",\"RANGE\":\"范围\",\"HULL\":\"底盘\",\"PROTECTION\":\"防护\",\"MAXIMUM_SPEED\":\"最快速度\",\"POWER\":\"启动加速度\",\"SKINS\":\"皮肤\",\"SUPPLIES\":\"道具\",\"BATTERIES\":\"电池\",\"ALTERATION\":\"装备改造\",\"WEAPON\":\"炮塔\",\"ARMOR\":\"底盘\",\"DRONE\":\"辅助无人机\",\"RESISTANCE\":\"防御模块\",\"PAINT\":\"迷彩\",\"INVENTORY\":\"道具\",\"KIT\":\"套件\",\"GIVEN_PRESENTS\":\"收到的礼物\",\"SPECIAL\":\"特殊\",\"TOP_TIP_WEAPONS\":\"您总的装备分数是坦克所有参数的总和,包括无人机,迷彩,道具等。\",\"UPGRADES\":\"升级\",\"OVERDRIVE\":\"过速装置\",\"PROTECTION_LABEL\":\"防护\",\"UNEQUIP_ALL_BUTTON\":\"取消所有装备\",\"STATS_LABEL\":\"统计\",\"DAMAGE_REDUCTION\":\"减少伤害\",\"PROPERTIES\":\"统计\",\"BUY_BUTTON\":\"购买\",\"PAINTS\":\"迷彩\",\"EQUIPPED\":\"已装备\",\"EQUIP_BUTTON\":\"装备\",\"UNEQUIP_BUTTON\":\"取消装备\",\"ALL_PAINTS\":\"显示所有\",\"PAINTS_SPECIAL\":\"特殊\",\"PAINTS_LEGENDARY\":\"传奇\",\"PAINTS_EPIC\":\"史诗\",\"PAINTS_RARE\":\"稀有\",\"PAINTS_UNCOMMON\":\"罕见\",\"PAINTS_COMMON\":\"普通\",\"PAINTS_FAVORITE\":\"收藏\",\"BACK_BUTTON\":\"返回\",\"UPGRADE_LABEL\":\"升级\",\"UPGRADE_BUTTON\":\"升级\",\"STANDARD_SETTINGS_NAME\":\"标准设置\",\"STANDARD_SETTINGS_DESCRIPTION\":\"标准出厂设置无特殊效果\",\"STANDARD_SKIN_NAME\":\"标准设置\",\"STANDARD_SKIN_DESCRIPTION\":\"标准出厂设置无特殊效果\",\"LABEL_VIDEO_PLAYER\":\"它是如何工作的\",\"FULLSCREEN_BUTTON\":\"全屏观看\",\"HEADER_DIALOG_BUY_ITEM\":\"购买物品\",\"TEXT_DIALOG_BUY_ITEM\":\"您要购买 %ITEM%\",\"DIALOG_BUY_ITEM_CANCEL_BUTTON\":\"取消\",\"DIALOG_BUY_ITEM_BUY_BUTTON\":\"购买\",\"HEADER_DIALOG_UPGRADE_ITEM\":\"升级\",\"TEXT_DIALOG_UPGRADE_ITEM\":\"升级 %ITEM%\",\"DIALOG_BUY_ITEM_UPGRADE_BUTTON\":\"升级\",\"DIALOG_NOT_ENOUGH_CRYSTAL_HEADER\":\"没有足够的水晶\",\"DIALOG_NOT_ENOUGH_CRYSTAL\":\"您没有足够的水晶 (%CRYSTAL%)\",\"DIALOG_NOT_ENOUGH_CRYSTAL_FOR_BUY_ITEM\":\"您没有足够的水晶 (%CRYSTAL%) 来购买这个 %ITEM%\",\"DIALOG__NOT_ENOUGH_CRYSTAL_BUY_CRYSTAL_BUTTON\":\"购买水晶\",\"DIALOG_NOT_ENOUGH_COIN_HEADER\":\"没有足够的坦克币\",\"DIALOG_NOT_ENOUGH_COIN\":\"您没有足够的坦克币 (%COIN%)\",\"DIALOG_NOT_ENOUGH_COIN_FOR_ITEM\":\"您没有足够的坦克币 (%COIN%) 购买这 %ITEM%\",\"DIALOG_NOT_ENOUGH_COIN_FOR_UPGRADE_ITEM\":\"您没有足够的坦克币 (%COIN%) 来升级 %ITEM%\",\"DIALOG__NOT_ENOUGH_COIN_BUY_COIN_BUTTON\":\"购买坦克币\",\"DIALOG_NOT_ENOUGH_RUBY_HEADER\":\"没有足够红宝石\",\"DIALOG_NOT_ENOUGH_RUBY_FOR_ITEM\":\"您没有足够红宝石 (%RUBY%) 来获得 %ITEM%\",\"DIALOG_NOT_ENOUGH_RUBY_FOR_UPGRADE_ITEM\":\"您没有足够红宝石 (%RUBY%) 来升级 %ITEM%\",\"DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPGRADE_TITLE\":\"升级\",\"DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPGRADE_MESSAGE\":\"没有足够的水晶 (%PRICE%) 来升级 %ITEM%\",\"DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPGRADE_BUTTON\":\"升级需要 %PRICE%\",\"SPEED_UP_BUTTON\":\"立即完成\",\"NOT_AVAILABLE\":\"无法使用\",\"LEVEL\":\"等级\",\"NO_DRONE_EQUIPPED\":\"未装备无人机\",\"RANK_IS_NEEDED\":\"需要等级 %RANK_NAME%\",\"MAX_LEVEL\":\"最高等级\",\"READ_MORE\":\"阅读更多\",\"PREMIUM_NEEDED\":\"需要VIP\",\"RANK_AND_PREMIUM_NEEDED\":\"需要Vip和%RANK_NAME%等级\",\"NO_RESISTANCE\":\"没有防护\",\"INSTALL_HERE\":\"在这里安装\",\"REMOVE_RESISTANCE\":\"移除防护\",\"EMPTY\":\"空\",\"LOCKED\":\"已锁定\",\"BATTERY_PROPERTY\":\"一节电池为无人机供电一分钟。\",\"SLOT_LOCKED\":\"已锁定\",\"SLOT_UNLOCK\":\"解锁\",\"BUY_PAINT_DESCRIPTION\":\"购买迷彩\",\"LOCK_PAINT_DESCRIPTION\":\"迷彩不可用\",\"EQUIP_PAINT_DESCRIPTION\":\"装备迷彩\",\"ITEM_LOCKED\":\"更改将应用于\",\"SELECT_SLOT_PROTECTION\":\"选择装备防护的插槽\",\"ALL\":\"全部\",\"GO_STORE\":\"去商店\",\"BUY_CONTAINER_BUTTON\":\"购买幸运礼盒\",\"PROTECTION_MODULE\":\"防御模块\",\"SHOT_COLOR\":\"弹道颜色​\",\"NEEDED_RANK\":\"解锁等级​\",\"COMPLETED\":\"完成\",\"OPEN_THE_SHOP\":\"进入商店\",\"DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPDATE_ITEM\":\"您没有足够的水晶 (%CRYSTAL%) 来升级 %ITEM%\",\"BOUGHT\":\"已购买\"}}" diff --git a/client/src/jsMain/kotlin/Protocol/ItemInfoDriver.kt b/client/src/jsMain/kotlin/Protocol/ItemInfoDriver.kt new file mode 100644 index 0000000..464f430 --- /dev/null +++ b/client/src/jsMain/kotlin/Protocol/ItemInfoDriver.kt @@ -0,0 +1,144 @@ +// ItemInfoDriver.kt —— M13a:仓库物品「可展示信息」离线导出(nodejs 目标,读本地抓包帧,不连服务器) +// +// 为什么离线读抓包:itemsOnDepot / pt 都在**已保存的 s2c 帧**里(space-100073070/s2c_0001.bin 有 +// pt + itemsOnDepot,s2c_0004.bin 有 pt + devicesLoaded)。直接解本地帧即可把 ClientState 填满, +// 不依赖 ws://localhost:9091 是否在跑,结果也可复现。 +// +// 用法:TANKIC_ITEMINFO=1 node +// 可选 TANKIC_PACKET_DIR 指定 3DTank/packets/plain +// 可选 TANKIC_ITEMINFO_FILES 覆盖帧清单(分号分隔的相对路径) +package Protocol + +internal object ItemInfoDriver { + + private const val EnterLabel = "读本地抓包帧(离线)" + + fun Wanted(): Boolean = Env("TANKIC_ITEMINFO") == "1" + + fun Run() { + Log("=== 仓库物品可展示信息导出(M13a,离线)===") + val Files = Env("TANKIC_ITEMINFO_FILES").ifEmpty { + "space-100073070/s2c_0001.bin;space-100073070/s2c_0004.bin" + }.split(";").filter { it.isNotEmpty() } + + val State = ClientState() + var FrameNo = 0 + for (Rel in Files) { + val Found = ReadPacket(Rel) + if (Found == null) { + Log("✗ 找不到抓包帧 $Rel(可用 TANKIC_PACKET_DIR 指定 3DTank/packets/plain)") + ExitProcess(1) + return + } + val (Path, Raw) = Found + Log("← 读入 $Path ${Raw.size} 字节") + var Off = 0 + while (Off < Raw.size) { + val Info = Frame.PeekFrame(Raw, Off) ?: break + val Body = try { + Frame.DecodeFrame(WireFrame(Raw.copyOfRange(Off + Info.HeaderSize, Off + Info.TotalSize), Info.Compressed)) + } catch (E: ProtocolException) { + Log("✗ 帧解析失败:${E.message}") + break + } + FrameNo++ + val NmSize = Nullmap.Size(Body, 0) + val MethodAt8 = if (Body.size >= NmSize + 16) Body.ReadI64(NmSize + 8).toString() else "" + val IsSpace = ProtocolTables.Downlink(MethodAt8) != null || ProtocolTables.CodecByMethod(MethodAt8) != null + if (IsSpace) State.OnSpaceFrame(SpaceFrame.Parse(Body), FrameNo, Info.TotalSize) + Off += Info.TotalSize + } + } + + Log("") + Log("itemsOnDepot ${State.ItemsOnDepot.size} 件 / mountedItems ${State.MountedItems.size} 件 / pt 对象 ${State.PtObjectCount} 个") + Log("") + Log("--- 仓库物品逐件(物品id | 型号值 | 图标文件 | 名称 | 类别中文 | 类别成员 | 稀有度成员 | 稀有度原始)---") + val Infos = State.ItemInfos() + for (I in Infos) { + Log( + " " + I.ItemId + " | " + (I.ModelValue?.toString() ?: "—") + " | " + (I.IconFile ?: "—") + + " | " + (I.Name ?: "—") + " | " + I.CategoryChinese + + " | " + (I.CategoryMember ?: I.CategoryRaw ?: "—") + + " | " + (I.RarityMember ?: "—") + " | " + (I.RarityRaw ?: "—") + ) + } + + // 与「已知答案」逐件比对(已知答案 = 官方客户端显示,见任务书) + val Known = linkedMapOf( + "920009630955" to ("Wasp" to "黄蜂轻甲"), + "920009631018" to ("Titan" to "泰坦"), + "920009631039" to ("Hunter" to "猎人中甲"), + "921009707333" to ("Crusader" to "十字军"), + "920009630969" to ("Smoky" to "轰天炮"), + "920009630983" to ("Firebird" to "火焰炮"), + "920009630997" to ("Twins" to "离子炮"), + "920009631011" to ("Railgun" to "激光炮"), + "10007268" to ("Double armor" to "护甲提升"), + ) + Log("") + Log("--- 与已知答案比对(9 件:4 底盘 + 4 炮塔 + 1 道具)---") + var IconHit = 0 + var NameHit = 0 + for ((Id, Expect) in Known) { + val I = Infos.firstOrNull { it.ItemId == Id } + val IconStem = I?.IconFile?.substringAfterLast('/')?.substringBeforeLast('.') + val IconOk = IconStem == Expect.first + val NameOk = I?.Name == Expect.second + if (IconOk) IconHit++ + if (NameOk) NameHit++ + Log( + " " + Id + " 期望图标=" + Expect.first + "/期望名称=" + Expect.second + + " 实际图标=" + (IconStem ?: "—") + "(" + (if (IconOk) "✓" else "✗") + ")" + + " 实际名称=" + (I?.Name ?: "—") + "(" + (if (NameOk) "✓" else "✗") + ")" + ) + } + Log(" 图标命中 $IconHit / 9,名称命中 $NameHit / 9") + if (IconHit < 9) { + Log(" 原因:仓库物品的 pt 模型表里**没有** TankInfoCC(modelId=5543162332469864498),") + Log(" 型号值(okz_1/rkz_1)取不到 ⇒ 图标推不出来。这不是算法问题,是抓包数据里没有该模型项。") + } + Log("") + Log("--- ItemIcons 表规模(A 产物)---") + Log( + " 底盘 " + ItemIcons.Data.Selection.HullCases + " case / 炮塔 " + ItemIcons.Data.Selection.TurretCases + + " case / 抗性 " + ItemIcons.Data.Selection.ResistanceCases + " case / 7 格道具 " + ItemIcons.Data.Selection.SupplyCount + + " 项 / 其它表 " + ItemIcons.Data.Selection.OtherTableCount + " 张" + ) + Log(" 抽样:底盘 397 → " + ItemIcons.Hull(397) + ",炮塔 7 → " + ItemIcons.Turret(7) + ",道具[2] → " + ItemIcons.Supply(2)) + Log("") + Log("✓ M13a 物品信息导出结束") + ExitProcess(0) + } + + // ---------------------------------------------------------------- node 侧小工具 + + private fun Env(Name: String): String = js("process.env[Name] || ''") as String + + // 抓包目录解析(与 SelfTest 同口径:环境变量优先,其次从 CWD 逐级往上找) + private fun PacketDirs(): List { + val Out = ArrayList() + val EnvDir = Env("TANKIC_PACKET_DIR") + if (EnvDir.isNotEmpty()) Out.add(EnvDir) + val Cwd = js("process.cwd()") as String + for (Up in 0..6) Out.add(Cwd + "/" + "../".repeat(Up) + "3DTank/packets/plain") + return Out + } + + private fun ReadPacket(Rel: String): Pair? { + val Fs = NodeRequire("fs") + for (Dir in PacketDirs()) { + val Path = Dir + "/" + Rel + try { + return Path to BufferToByteArray(Fs.readFileSync(Path)) + } catch (E: dynamic) { + // 该候选目录下没有这个文件,试下一个 + } + } + return null + } + + private fun Log(Msg: String) { + println(Msg) + } +} diff --git a/client/src/jsMain/resources/ItemIcons.json b/client/src/jsMain/resources/ItemIcons.json new file mode 100644 index 0000000..0a782bb --- /dev/null +++ b/client/src/jsMain/resources/ItemIcons.json @@ -0,0 +1 @@ +{"Note":"物品图标表,由 client/tools/ExportItemIcons.mjs 从官方解包源码生成,请勿手改。Hull = module_36659.js 的 Mb(eA).upf switch:case(底盘型号)→ i(<模块id>) → resources.json 文件名;Turret = module_57128.js 的 ct(Dn).duk switch(炮塔型号);Resistance = 同文件 ct(Dn).euk switch(抗性,用于自验解析算法)。解析口径:case → 形参名 → 形参声明行 → 同序实参行 → i(<模块id>),不能假设「case 号 = 实参位置」。Supply = module_82347.js 的 7 格道具图标表(new ZMi 数组,非 switch)。OtherTables = 自动扫描到的其它同类 case→图标 表(供手榴弹/无人机/防御模块查用;没有就是空数组)。GarageLabels = zh-CN 的 lobby.garage.*(只取字符串叶子),显示层类别中文用。图标路径统一为 \"static/images/xxx.svg\"(相对 web/play 根,客户端配 AssetRelay 的 /play 前缀取用)。","GeneratedAt":"2026-09-18T07:47:43.890Z","Source":"3dtank.com/play/static/js/unpacked/{resources.json,module_36659.js,module_57128.js,module_82347.js} + 3DTank/web/play/static/translations/zh-CN.a2b303af.json","Selection":{"HullCases":416,"HullMisses":1,"TurretCases":20,"TurretMisses":0,"ResistanceCases":21,"ResistanceMisses":0,"SupplyCount":7,"OtherTableCount":3,"GarageLabelCount":108},"SelfCheck":{"ResistanceCases":21,"ResistanceResolved":21,"ResistanceLocalFilesPresent":21,"ResistanceDistinctFiles":20,"Lines":["case 23 -> static/images/firebird_resistance.785a9d6b.svg ✓本地存在","case 25 -> static/images/smoky_resistance.845afc14.svg ✓本地存在","case 26 -> static/images/tsunami_resistance.6200aad9.svg ✓本地存在","case 27 -> static/images/scorpio_resistance.e8f1787f.svg ✓本地存在","case 28 -> static/images/twins_resistance.ad189f61.svg ✓本地存在","case 29 -> static/images/railgun_resistance.636a554f.svg ✓本地存在","case 30 -> static/images/isis_resistance.30a69ffc.svg ✓本地存在","case 31 -> static/images/mine_resistance.dd581c90.svg ✓本地存在","case 32 -> static/images/thunder_resistance.6d7f4531.svg ✓本地存在","case 33 -> static/images/freeze_resistance.33bdf642.svg ✓本地存在","case 34 -> static/images/ricochet_resistance.8247beaa.svg ✓本地存在","case 35 -> static/images/shaft_resistance.0778fd3e.svg ✓本地存在","case 36 -> static/images/vulcan_resistance.824f6f0e.svg ✓本地存在","case 37 -> static/images/hammer_resistance.6c549d29.svg ✓本地存在","case 38 -> static/images/rocket_launcher_resistance.b7dfd64f.svg ✓本地存在","case 39 -> static/images/artillery_resistance.9b4cbc34.svg ✓本地存在","case 40 -> static/images/all_resistance.b45eae39.svg ✓本地存在","case 41 -> static/images/gauss_resistance.bb8f409c.svg ✓本地存在","case 42 -> static/images/tesla_resistance.3e686c8e.svg ✓本地存在","case 43 -> static/images/crit_resistance.94e32312.svg ✓本地存在","case 44 -> static/images/all_resistance.b45eae39.svg ✓本地存在"]},"Hull":{"0":"static/images/dot.69f2e150.svg","1":"static/images/hudSupplyBackground.19f6b76c.svg","2":"static/images/supplyKeyLabelBackground.fe3c4a5e.svg","3":"static/images/background_flash.5c9bb879.svg","4":"static/images/no_device.d6a5ed38.svg","5":"static/images/jamming.27ca209f.svg","6":"static/images/pierce.53f9cfc2.svg","7":"static/images/freezing.ecb7c14a.svg","8":"static/images/firebird_high_pressure_pump.a8244e4a.svg","9":"static/images/compact_tanks.f7e14a7d.svg","10":"static/images/thermite_mix.48b12b96.svg","11":"static/images/self_heating.b24239b8.svg","12":"static/images/firebird_magnetic.422b1b1c.svg","13":"static/images/firebird_paralyzing.ce9a0315.svg","14":"static/images/firebird_toxic.53f9cfc2.svg","15":"static/images/crit_mix.bc4eeef4.svg","16":"static/images/freeze_high_pressure_pump.a8244e4a.svg","17":"static/images/corrosive_mix.48b12b96.svg","18":"static/images/deep_cooling.9dee7d51.svg","19":"static/images/self_cooling.7f72e2b9.svg","20":"static/images/freeze_emp.999d4c2f.svg","21":"static/images/freeze_stun.ce9a0315.svg","22":"static/images/stable_mix.854e4ede.svg","23":"static/images/vampire.369dbf7a.svg","24":"static/images/damage_nanobots.88a331a6.svg","25":"static/images/support_nanobots.58f8a26a.svg","26":"static/images/broadband_emitters.9113bd6f.svg","27":"static/images/isida_vampiric_nanobots.fd08d293.svg","28":"static/images/isida_enduring_nanobots.c22db7a0.svg","29":"static/images/incendiary_belt.d80e9a8e.svg","30":"static/images/incendiary_bombs.50e991df.svg","31":"static/images/faster_horizontal_tracking_vulcan.843faf29.svg","32":"static/images/modified_firing_rate.f7c9fbd9.svg","33":"static/images/machine_gun_rubber_bullets.b991ff76.svg","34":"static/images/vulcan_shredder.62384918.svg","35":"static/images/magnum_lafet.ffcd092b.svg","36":"static/images/crio_bombs.70e1cab1.svg","37":"static/images/modified_carriage.9b8f19d3.svg","38":"static/images/minelayer.23123790.svg","39":"static/images/incendiary_shot.6fa2b96d.svg","40":"static/images/accelerated_loading_mechanism.4e9d4492.svg","41":"static/images/magnum_carronade.2174f9ae.svg","42":"static/images/hyperspace_projectiles.b4d4c1bb.svg","43":"static/images/destabilized_shells.fe1994b2.svg","44":"static/images/capacitor_power_reduction.36d059ee.svg","45":"static/images/adaptive_recharge_railgun.ca70ee86.svg","46":"static/images/faster_horizontal_tracking_railgun.fef446a1.svg","47":"static/images/high_caliber_ammo.7d27e891.svg","48":"static/images/shell_stabilization.1721fbfe.svg","49":"static/images/railgun_incendiary projectiles.f37f8a30.svg","50":"static/images/railgun_emp_projectiles.a38fb7ed.svg","51":"static/images/railgun_freezing_projectiles.d1dda9d2.svg","52":"static/images/railgun_stun_projectiles.ce9a0315.svg","53":"static/images/railgun_hyperspeed_shells.97017b02.svg","54":"static/images/railgun_shotgun.40a0fb07.svg","55":"static/images/excalibur.04bf0648.svg","56":"static/images/railgun_detonator.ab8cc577.svg","57":"static/images/adapted_charging.57598ece.svg","58":"static/images/plasma_torch.b06f598e.svg","59":"static/images/unstable_plazma.9ab051b7.svg","60":"static/images/minus_field_stabilization.5a5754eb.svg","61":"static/images/incineratingRicochet.d5e1474a.svg","62":"static/images/crioFieldRicochet.d1dda9d2.svg","63":"static/images/maghetronRicochet.13b5e353.svg","64":"static/images/tectonicRicochet.ce9a0315.svg","65":"static/images/pulsarRicochet.af282303.svg","66":"static/images/heliosRicochet.2d0f9e6c.svg","67":"static/images/radio_explosion.e80117ad.svg","68":"static/images/stun_striker.ce9a0315.svg","69":"static/images/bfg.9a60aeb4.svg","70":"static/images/rocket_launcher_hunter.517e6c9c.svg","71":"static/images/rocket_launcher_cyclone.deb25fdb.svg","72":"static/images/striker_tandem_missile_launcher.9859b400.svg","73":"static/images/striker_hydra_missile_launcher.0728e898.svg","74":"static/images/striker_faust_missile_launcher.07f4364a.svg","75":"static/images/railgun_freezing_projectiles.d1dda9d2.svg","76":"static/images/striker_meteor.cf9e3d55.svg","77":"static/images/jammer.a43c0424.svg","78":"static/images/lightweight_capacitors.89165c09.svg","79":"static/images/robo_crosshair.2ef965e2.svg","80":"static/images/triple_shot.53afa709.svg","81":"static/images/assault_emitters.7d27e891.svg","82":"static/images/heavy_capacitors.6a7b069b.svg","83":"static/images/shaft_healing_emitters.94038ef9.svg","84":"static/images/adaptive_recharge_hammer.f9c879de.svg","85":"static/images/double_barrel.85637e6b.svg","86":"static/images/dragons_breath.fe8b0feb.svg","87":"static/images/high_capacity_drum.2ed109c1.svg","88":"static/images/slugger.0ff79d90.svg","89":"static/images/hammer_blunderbuss.1ef20536.svg","90":"static/images/hammerWyversBreath.d1dda9d2.svg","91":"static/images/hammerStunningPellets.ce9a0315.svg","92":"static/images/hammerMagneticPellets.b50a317e.svg","93":"static/images/hammer_heavy_slugger.7a0d279d.svg","94":"static/images/hammer_hunter_duplet.b6718440.svg","95":"static/images/freezing_shot.d1dda9d2.svg","96":"static/images/super_cumulative.7d27e891.svg","97":"static/images/auto_turret.856b1a96.svg","98":"static/images/precision_targeting_system.1721fbfe.svg","99":"static/images/assault_ammunition.53afa709.svg","100":"static/images/smoky_stun.ce9a0315.svg","101":"static/images/smoky_emp_shell.e95b61b7.svg","102":"static/images/smoky_rubber.b991ff76.svg","103":"static/images/smoky_explosive.fe1994b2.svg","104":"static/images/smoky_sorted_ammo.531b9ad7.svg","105":"static/images/hyperspeed_shells.97017b02.svg","106":"static/images/hammer_shells.7a262076.svg","107":"static/images/lightweight_ordnance_autoloader.db3c43fc.svg","108":"static/images/armor_piercing_ammo.3a9177d1.svg","109":"static/images/adaptive_reload.321ac9df.svg","110":"static/images/anvil_shells.b4d4c1bb.svg","111":"static/images/corrosion_shells.53f9cfc2.svg","112":"static/images/hyperspeed_shells.97017b02.svg","113":"static/images/magneeto_shells.05362150.svg","114":"static/images/nanotech_shells.94038ef9.svg","115":"static/images/noise_shells.27ca209f.svg","116":"static/images/peacemaker_shells.ce9a0315.svg","117":"static/images/salamandra_shells.c59e2245.svg","118":"static/images/strict_loading_ammunition.531b9ad7.svg","119":"static/images/vivern_shells.d1dda9d2.svg","120":"static/images/gauss_emp.04e86cc8.svg","121":"static/images/super_solenoids.136bdfd5.svg","122":"static/images/faster_horizontal_tracking.610c4011.svg","123":"static/images/hacked_processor.c787fca4.svg","124":"static/images/hyperspeed_shells.97017b02.svg","125":"static/images/jammer.a43c0424.svg","126":"static/images/large_caliber.246743df.svg","127":"static/images/solenoid_cooling.a308d483.svg","128":"static/images/nemesis.84239c03.svg","129":"static/images/modulators_of_plasma.d4d50811.svg","130":"static/images/bfg_twins.ddef2ac9.svg","131":"static/images/stable_plazma.5046b47c.svg","132":"static/images/plazma_core_accelerators.76f38ac6.svg","133":"static/images/TwinsVaporizer.ac45b820.svg","134":"static/images/TwinsCryotron.d1dda9d2.svg","135":"static/images/TwinsMagnetron.2b2616a5.svg","136":"static/images/TwinsTectonicPlasma.ce9a0315.svg","137":"static/images/TwinsPlasmaInjector.53f9cfc2.svg","138":"static/images/TwinsPlasmaDisruptor.27ca209f.svg","139":"static/images/TwinsPlasmaTurboAccelerators.9a195099.svg","140":"static/images/twins_plasmatron.4294d6dd.svg","141":"static/images/accelerated_lightning.4b384476.svg","142":"static/images/penetration_effect.21deab47.svg","143":"static/images/stabilized_lightning.5b5469d1.svg","144":"static/images/electroturret.5503cb45.svg","145":"static/images/freeze_effect.b9cc4b3f.svg","146":"static/images/minus_field.0c494806.svg","147":"static/images/stun_effect.d3e40fe1.svg","148":"static/images/emi_effect.c537df83.svg","149":"static/images/shock_therapy.c11c390d.svg","150":"static/images/large_caliber.fe4da415.svg","151":"static/images/assault_magazine.77d2e1cd.svg","152":"static/images/armour_piercing.53f9cfc2.svg","153":"static/images/cryo.d1dda9d2.svg","154":"static/images/emp.4e760200.svg","155":"static/images/explosive_shells.fe1994b2.svg","156":"static/images/explosive_warheads.d05207d0.svg","157":"static/images/incendiary.8867f776.svg","158":"static/images/swarm.1c109353.svg","159":"static/images/tornado.deb25fdb.svg","160":"static/images/wolfpack.517e6c9c.svg","161":"static/images/stun.ce9a0315.svg","162":"static/images/uran_shells.b991ff76.svg","163":"static/images/javelin.1721fbfe.svg","164":"static/images/adaptive_reload.0e12f909.svg","165":"static/images/rapier_mode.a254fcd0.svg","166":"static/images/manual_mode.246743df.svg","167":"static/images/explosive_shells.c68f37c5.svg","168":"static/images/tungstenshells.f19a2433.svg","169":"static/images/duplet.8da7d9f3.svg","170":"static/images/megatsunami.a308d483.svg","171":"static/images/triplet.7da6b70e.svg","172":"static/images/autocannon.47db9e92.svg","173":"static/images/sorted_ammunition.6a7811e3.svg","174":"static/images/hyperspeed_shells.97017b02.svg","175":"static/images/north_star.4eb10738.svg","176":"static/images/uranium_shells.ca6f15fc.svg","177":"static/images/adaptive_autocannon.c0f4334a.svg","178":"static/images/phoenix.d1b99809.svg","179":"static/images/demoman.d52cfed6.svg","180":"static/images/driver.0c6e1a01.svg","181":"static/images/engineer.81e7a551.svg","182":"static/images/miner.9e793eb7.svg","183":"static/images/rescuer.dab2966b.svg","184":"static/images/stun.ce9a0315.svg","185":"static/images/bombard.a58f8bea.svg","186":"static/images/emp.04e86cc8.svg","187":"static/images/parkour.02e3455b.svg","188":"static/images/adrenalin.6c46d7e2.svg","189":"static/images/grenadier.d3753e03.svg","190":"static/images/destroyer.d849105e.svg","191":"static/images/quasar.ca409d7b.svg","192":"static/images/bolter.e897e58a.svg","193":"static/images/hull_pierce_immunity.53f9cfc2.svg","194":"static/images/hull_overheat_immunity.ceb4cd29.svg","195":"static/images/hull_overheat_protection.6ab44f29.svg","196":"static/images/hull_freezing_immunity.d1dda9d2.svg","197":"static/images/hull_freezing_protection.1e959682.svg","198":"static/images/hull_emp_immunity.7d4854f7.svg","199":"static/images/hull_default.56ad9071.svg","200":"static/images/hull_less_mass.8a135999.svg","201":"static/images/hull_more_mass.474ae82a.svg","202":"static/images/hull_stun_immunity.ce9a0315.svg","203":"static/images/button.22e4056c.svg","204":"static/images/buttonReady.53dd51e7.svg","205":"static/images/dictator.48e6b050.svg","206":"static/images/hornet.e403abbc.svg","207":"static/images/hunter.faef601d.svg","208":"static/images/juggernaut.77e55855.svg","209":"static/images/mammonth.24f55e8c.svg","210":"static/images/titan.885b04ed.svg","211":"static/images/viking.23c652d9.svg","212":"static/images/wasp.72c29025.svg","213":"static/images/paladin.d9a9fa6e.svg","214":"static/images/ares.3e860aa7.svg","215":"static/images/hopper.7de9d626.svg","216":"static/images/crusader.8d6be837.svg","217":"static/images/ctf_blue_base.13dd239f.svg","218":"static/images/ctf_blue_base_no_flag.94a2bf33.svg","219":"static/images/ctf_red_base.725359a2.svg","220":"static/images/ctf_red_base_no_flag.8053d3fa.svg","221":"static/images/ctf_blue_delivering.51894cf2.svg","222":"static/images/ctf_blue_delivering_down.f9808b5c.svg","223":"static/images/ctf_red_delivering.05f7555f.svg","224":"static/images/ctf_red_delivering_down.ee038857.svg","225":"static/images/rgb_free.0ba25488.svg","226":"static/images/rgb_blue_delivering.6c8a284b.svg","227":"static/images/rgb_red_delivering.8ebc011a.svg","228":"static/images/rgb_blue_base.48f0209e.svg","229":"static/images/rgb_red_base.f7d852a9.svg","230":"static/images/asl_blue_base.48f0209e.svg","231":"static/images/asl_red_base.f7d852a9.svg","232":"static/images/jgr_red.8e25466e.svg","233":"static/images/jgr_blue.c3fe3a20.svg","234":"static/images/jgr_red_ally.ad048a31.svg","235":"static/images/jgr_blue_ally.051eb141.svg","236":"static/images/jgr_red_hostile.8e25466e.svg","237":"static/images/jgr_blue_hostile.c3fe3a20.svg","238":"static/images/blue-base-indicator.adc00d07.svg","239":"static/images/red-base-indicator.f2de75f8.svg","240":"static/images/blue-attack-indicator.9e8adfa6.svg","241":"static/images/red-attack-indicator.dc2612ad.svg","242":"static/images/blue-a.c46da4bf.svg","243":"static/images/blue-b.fad1cee0.svg","244":"static/images/blue-c.c21fd0b1.svg","245":"static/images/blue-d.a8b80bd8.svg","246":"static/images/blue-e.6a26c991.svg","247":"static/images/blue-f.36e513e4.svg","248":"static/images/blue-g.691ccdc7.svg","249":"static/images/blue-h.84e8d693.svg","250":"static/images/indicator-blue.23ed1adc.svg","251":"static/images/red-a.e82caf68.svg","252":"static/images/red-b.b3fd135b.svg","253":"static/images/red-c.49c46faa.svg","254":"static/images/red-d.3fd7d7c2.svg","255":"static/images/red-e.5d14c195.svg","256":"static/images/red-f.f4e866c2.svg","257":"static/images/red-g.f33ddb69.svg","258":"static/images/red-h.c8a10d9a.svg","259":"static/images/neutral-a.24abb664.svg","260":"static/images/neutral-b.6371af26.svg","261":"static/images/neutral-c.a18f381d.svg","262":"static/images/neutral-d.4c0f6412.svg","263":"static/images/neutral-e.43e9f5ee.svg","264":"static/images/neutral-f.8d5ff476.svg","265":"static/images/neutral-g.1560f6ee.svg","266":"static/images/neutral-h.44771d90.svg","267":"static/images/blue-a.3c4e9fd3.svg","268":"static/images/blue-b.626263b9.svg","269":"static/images/blue-c.0f460fde.svg","270":"static/images/blue-d.4bc1fb70.svg","271":"static/images/blue-e.540e34eb.svg","272":"static/images/blue-f.ed409afd.svg","273":"static/images/blue-g.d42ae2fe.svg","274":"static/images/blue-h.64d9bd9a.svg","275":"static/images/red-a.4bb56d31.svg","276":"static/images/red-b.f8a6ca02.svg","277":"static/images/red-c.0da087f5.svg","278":"static/images/red-d.0984fa7a.svg","279":"static/images/red-e.8f24ab84.svg","280":"static/images/red-f.786788ab.svg","281":"static/images/red-g.710c2485.svg","282":"static/images/red-h.3dec069d.svg","283":"static/images/neutral-a.42ac9dd1.svg","284":"static/images/neutral-b.678833d3.svg","285":"static/images/neutral-c.0c9c83e3.svg","286":"static/images/neutral-d.a9f4ddf1.svg","287":"static/images/neutral-e.d525937c.svg","288":"static/images/neutral-f.128b0328.svg","289":"static/images/neutral-g.38b68a23.svg","290":"static/images/neutral-h.3276e30e.svg","291":"static/images/indicator-red-sge.09547444.svg","292":"static/images/indicator-blue-sge.45d60052.svg","293":"static/images/red-indicator.285d02f6.svg","294":"static/images/blue-indicator.82508438.svg","295":"static/images/red-indicator.6d32aa0e.svg","296":"static/images/white-indicator-left.890affb7.svg","297":"static/images/white-indicator-right.d2c948fb.svg","298":"static/images/blue-indicator.1208edf8.svg","299":"static/images/red-indicator.ffb20aae.svg","300":"static/images/blue-indicator.2704912b.svg","301":"static/images/red-indicator.cd1c9bd3.svg","302":"static/images/white-indicator-left.fdae109e.svg","303":"static/images/white-indicator-right.1ce33d9b.svg","304":"static/images/blue-indicator.1208edf8.svg","305":"static/images/red-indicator.ffb20aae.svg","306":"static/images/progress-background.9f69a68c.svg","307":"static/images/score.f4ee69d2.svg","308":"static/images/tjr-score-blue.c7565943.svg","309":"static/images/tjr-score-red.f5aee776.svg","310":"static/images/ar-indicator-red.59875923.svg","311":"static/images/ar-indicator-blue.c2910b77.svg","312":"static/images/blue-health-indicator.acf2ce82.svg","313":"static/images/green-health-indicator.d0d7151e.svg","314":"static/images/red-health-indicator.3c0e5d61.svg","315":"static/images/weapon-reload-indicator.eaa91ece.svg","316":"static/images/armorWithBackground.3a887dad.svg","317":"static/images/damageWithBackground.4a80d6f3.svg","318":"static/images/nitroWithBackground.9d0ffef7.svg","319":"static/images/mineWithBackground.eced36f5.svg","320":"static/images/repairWithBackground.58991a46.svg","321":"static/images/ultimateWithBackground.d18cf95d.svg","322":"static/images/booster_armor.3dfb0e1e.svg","323":"static/images/booster_damage.c3e7e6b3.svg","324":"static/images/booster_speed.6b64eec3.svg","325":"static/images/emp.65915d57.svg","326":"static/images/freezing.bff72869.svg","327":"static/images/immortality.6f56f3ad.svg","328":"static/images/polarization.f4c89bee.svg","329":"static/images/rescue.3fb29ae9.svg","330":"static/images/overheat.c5da6580.svg","331":"static/images/stun.afaad6e7.svg","332":"static/images/jamming.42ebef54.svg","333":"static/images/supercharge.df85d848.svg","335":"static/images/pierce.349b551d.svg","336":"static/images/overdrive_ready.b56250e7.svg","337":"static/images/overdrive_active.cfe36fdd.svg","338":"static/images/armorWithoutBackground.7c123edf.svg","339":"static/images/damageWithoutBackground.4fb8a2b2.svg","340":"static/images/nitroWithoutBackground.bf86a6bd.svg","341":"static/images/repairWithoutBackground.b9e452b0.svg","342":"static/images/ultimateWithoutBackground.18426912.svg","343":"static/images/overdrive_active.f6ac659d.svg","344":"static/images/booster_armor.e4f3bf2e.svg","345":"static/images/booster_damage.eea41239.svg","346":"static/images/booster_speed.323be23b.svg","347":"static/images/emp.f3975b48.svg","348":"static/images/freezing.7f014edf.svg","349":"static/images/rescue.8306a7b4.svg","350":"static/images/immortality.c6cb0f55.svg","351":"static/images/polarization.8633cb05.svg","352":"static/images/overheat.89c0e772.svg","353":"static/images/stun.074235c8.svg","354":"static/images/jamming.22e2534d.svg","355":"static/images/supercharge.d24b4ed3.svg","356":"static/images/suicide.fa6ce4b0.svg","357":"static/images/pierce.b182be03.svg","358":"static/images/background.6252e553.svg","359":"static/images/tank.ea532f21.svg","360":"static/images/tank-above.ac5b8ccc.svg","361":"static/images/tank-below.4ec90bd7.svg","362":"static/images/tank-dead.4e8ccc19.svg","363":"static/images/change_equipment.66cf72d8.svg","364":"static/images/self_destroy.88e26ae8.svg","365":"static/images/join_battle.b0903eb3.svg","366":"static/images/leave_battle.b562acdc.svg","367":"static/images/crystalSmall.242e6c15.svg","368":"static/images/fund.7496d0c5.svg","369":"static/images/tsunami_resistance.6200aad9.svg","370":"static/images/firebird_resistance.785a9d6b.svg","371":"static/images/freeze_resistance.33bdf642.svg","372":"static/images/isis_resistance.30a69ffc.svg","373":"static/images/vulcan_resistance.824f6f0e.svg","374":"static/images/artillery_resistance.9b4cbc34.svg","375":"static/images/railgun_resistance.636a554f.svg","376":"static/images/ricochet_resistance.8247beaa.svg","377":"static/images/rocket_launcher_resistance.b7dfd64f.svg","378":"static/images/shaft_resistance.0778fd3e.svg","379":"static/images/hammer_resistance.6c549d29.svg","380":"static/images/smoky_resistance.845afc14.svg","381":"static/images/thunder_resistance.6d7f4531.svg","382":"static/images/gauss_resistance.bb8f409c.svg","383":"static/images/twins_resistance.ad189f61.svg","384":"static/images/tesla_resistance.3e686c8e.svg","385":"static/images/scorpio_resistance.e8f1787f.svg","386":"static/images/Jugger.b9550587.svg","387":"static/images/bonk.37ae6633.svg","388":"static/images/defaultDevice.47b22a7c.svg","389":"static/images/mine_resistance.dd581c90.svg","390":"static/images/all_resistance.b45eae39.svg","391":"static/images/crit_resistance.94e32312.svg","392":"static/images/Dictator.770a5277.svg","393":"static/images/Hornet.aa9c33f2.svg","394":"static/images/Mammoth.1c266702.svg","395":"static/images/Titan.d2bf1806.svg","396":"static/images/Viking.45549e00.svg","397":"static/images/Wasp.63098bb6.svg","398":"static/images/Hunter.82b7d58f.svg","399":"static/images/Ares.e0b4f363.svg","400":"static/images/Hopper.17d5fd98.svg","401":"static/images/Crusader.f14927dc.svg","402":"static/images/Jugger.b9550587.svg","403":"static/images/Paladin.7b656d82.svg","404":"static/images/defaultDevice.47b22a7c.svg","405":"static/images/burn_effect.7ac3028a.svg","406":"static/images/revolver.3f7f2a0e.svg","407":"static/images/camper.1d12b50f.svg","408":"static/images/excelsior.4ea675f7.svg","409":"static/images/knuckles.1b4b2cd3.svg","410":"static/images/boxer.de1f9ba8.svg","411":"static/images/stellarator.b5b7ca7a.svg","412":"static/images/explosive_shell.77ac7b70.svg","413":"static/images/vacuum_shell.b1432566.svg","414":"static/images/yamato.e9264376.svg","415":"static/images/high_voltage.7a7ef903.svg","416":"static/images/needler.91bf89e9.svg"},"Turret":{"5":"static/images/UltraResist.fc42ccb2.svg","6":"static/images/Firebird.f17e7721.svg","7":"static/images/Smoky.a2623c59.svg","8":"static/images/Scorpion.17f0ca64.svg","9":"static/images/Twins.0809b017.svg","10":"static/images/Railgun.e2aea740.svg","11":"static/images/Isida.e0a284b9.svg","12":"static/images/Mine.de6d4b99.svg","13":"static/images/Thunder.5e4fe37f.svg","14":"static/images/Freeze.51cbde00.svg","15":"static/images/Ricochet.126321e9.svg","16":"static/images/new_icon_shaft.4a28c47a.svg","17":"static/images/Hammer.d7ddf1ba.svg","18":"static/images/Vulcan.eb748cd8.svg","19":"static/images/Striker.4d0c2fad.svg","20":"static/images/Magnum.54718a7f.svg","21":"static/images/Gauss.da869a90.svg","22":"static/images/Tesla.da7544a4.svg","23":"static/images/Critical.41589075.svg","24":"static/images/Tsunami.1dff89b7.svg"},"Resistance":{"23":"static/images/firebird_resistance.785a9d6b.svg","25":"static/images/smoky_resistance.845afc14.svg","26":"static/images/tsunami_resistance.6200aad9.svg","27":"static/images/scorpio_resistance.e8f1787f.svg","28":"static/images/twins_resistance.ad189f61.svg","29":"static/images/railgun_resistance.636a554f.svg","30":"static/images/isis_resistance.30a69ffc.svg","31":"static/images/mine_resistance.dd581c90.svg","32":"static/images/thunder_resistance.6d7f4531.svg","33":"static/images/freeze_resistance.33bdf642.svg","34":"static/images/ricochet_resistance.8247beaa.svg","35":"static/images/shaft_resistance.0778fd3e.svg","36":"static/images/vulcan_resistance.824f6f0e.svg","37":"static/images/hammer_resistance.6c549d29.svg","38":"static/images/rocket_launcher_resistance.b7dfd64f.svg","39":"static/images/artillery_resistance.9b4cbc34.svg","40":"static/images/all_resistance.b45eae39.svg","41":"static/images/gauss_resistance.bb8f409c.svg","42":"static/images/tesla_resistance.3e686c8e.svg","43":"static/images/crit_resistance.94e32312.svg","44":"static/images/all_resistance.b45eae39.svg"},"Supply":[{"Key":"n1()","Icon":"static/images/Ultimate.94f7d547.svg","ModuleId":"5012","Line":91126},{"Key":"nS()","Icon":"static/images/Repair.b9f61bf0.svg","ModuleId":"64414","Line":91127},{"Key":"iS()","Icon":"static/images/Double armor.064daed8.svg","ModuleId":"64379","Line":91128},{"Key":"rS()","Icon":"static/images/Double damage.868dc39c.svg","ModuleId":"53749","Line":91129},{"Key":"eS()","Icon":"static/images/Double speed.b917b469.svg","ModuleId":"55271","Line":91130},{"Key":"uS()","Icon":"static/images/Mine.cf7c3a30.svg","ModuleId":"22512","Line":91131},{"Key":"i1()","Icon":"static/images/Gold box.6efd6936.svg","ModuleId":"69604","Line":91132}],"OtherTables":[{"Module":"module_82347.js","Line":37290,"Cases":6,"Icons":6,"Sample":["0=static/images/iconCasualGray.3eea12e7.svg","1=static/images/uncommon.ca77d7da.svg","2=static/images/iconRareBlue.4e3c7303.svg","3=static/images/iconEpicFiolet.d91b1151.svg"],"All":{"0":"static/images/iconCasualGray.3eea12e7.svg","1":"static/images/uncommon.ca77d7da.svg","2":"static/images/iconRareBlue.4e3c7303.svg","3":"static/images/iconEpicFiolet.d91b1151.svg","4":"static/images/iconLegendaryGold.7c76cb29.svg","5":"static/images/iconCustomiseRed.2b5c8828.svg"}},{"Module":"module_82347.js","Line":62117,"Cases":7,"Icons":7,"Sample":["0=static/images/iconCustomiseRed.2b5c8828.svg","1=static/images/iconLegendaryGold.7c76cb29.svg","2=static/images/iconEpicFiolet.d91b1151.svg","3=static/images/iconRareBlue.4e3c7303.svg"],"All":{"0":"static/images/iconCustomiseRed.2b5c8828.svg","1":"static/images/iconLegendaryGold.7c76cb29.svg","2":"static/images/iconEpicFiolet.d91b1151.svg","3":"static/images/iconRareBlue.4e3c7303.svg","4":"static/images/uncommon.ca77d7da.svg","5":"static/images/iconCasualGray.3eea12e7.svg","6":"static/images/favoriteStar.1ce58570.svg"}},{"Module":"module_82347.js","Line":118947,"Cases":17,"Icons":17,"Sample":["0=static/images/privateIcon.4427d3a1.svg","1=static/images/BonusBoxes.59bbac9e.svg","2=static/images/Upgrades.fe5718ef.svg","3=static/images/Devices.ae5dfc92.svg"],"All":{"0":"static/images/privateIcon.4427d3a1.svg","1":"static/images/BonusBoxes.59bbac9e.svg","2":"static/images/Upgrades.fe5718ef.svg","3":"static/images/Devices.ae5dfc92.svg","4":"static/images/Autobalance.e011e48f.svg","5":"static/images/ClanWar.18ddff9a.svg","6":"static/images/GoldBoxes.eea52483.svg","7":"static/images/FriendFire.cde8d0d5.svg","8":"static/images/WeaponChange.89d8adcc.svg","9":"static/images/Overdrives.ac06ab56.svg","10":"static/images/Supplies.57cf4b22.svg","11":"static/images/Nuclear.6c595d3c.svg","12":"static/images/SmartSupplies.d7f811ef.svg","13":"static/images/Drones.07e10f28.svg","14":"static/images/ProtectionModules.41103d70.svg","15":"static/images/Valuable.b03ff5d2.svg","16":"static/images/Grenades.25dcf096.svg"}}],"GarageLabels":{"GRENADE":"手榴弹","TITLE":"车库","GEAR_SCORE":"装备分数","TURRET":"炮塔","DAMAGE":"伤害","IMPACT_FORCE":"冲击力","RANGE":"范围","HULL":"底盘","PROTECTION":"防护","MAXIMUM_SPEED":"最快速度","POWER":"启动加速度","SKINS":"皮肤","SUPPLIES":"道具","BATTERIES":"电池","ALTERATION":"装备改造","WEAPON":"炮塔","ARMOR":"底盘","DRONE":"辅助无人机","RESISTANCE":"防御模块","PAINT":"迷彩","INVENTORY":"道具","KIT":"套件","GIVEN_PRESENTS":"收到的礼物","SPECIAL":"特殊","TOP_TIP_WEAPONS":"您总的装备分数是坦克所有参数的总和,包括无人机,迷彩,道具等。","UPGRADES":"升级","OVERDRIVE":"过速装置","PROTECTION_LABEL":"防护","UNEQUIP_ALL_BUTTON":"取消所有装备","STATS_LABEL":"统计","DAMAGE_REDUCTION":"减少伤害","PROPERTIES":"统计","BUY_BUTTON":"购买","PAINTS":"迷彩","EQUIPPED":"已装备","EQUIP_BUTTON":"装备","UNEQUIP_BUTTON":"取消装备","ALL_PAINTS":"显示所有","PAINTS_SPECIAL":"特殊","PAINTS_LEGENDARY":"传奇","PAINTS_EPIC":"史诗","PAINTS_RARE":"稀有","PAINTS_UNCOMMON":"罕见","PAINTS_COMMON":"普通","PAINTS_FAVORITE":"收藏","BACK_BUTTON":"返回","UPGRADE_LABEL":"升级","UPGRADE_BUTTON":"升级","STANDARD_SETTINGS_NAME":"标准设置","STANDARD_SETTINGS_DESCRIPTION":"标准出厂设置无特殊效果","STANDARD_SKIN_NAME":"标准设置","STANDARD_SKIN_DESCRIPTION":"标准出厂设置无特殊效果","LABEL_VIDEO_PLAYER":"它是如何工作的","FULLSCREEN_BUTTON":"全屏观看","HEADER_DIALOG_BUY_ITEM":"购买物品","TEXT_DIALOG_BUY_ITEM":"您要购买 %ITEM%","DIALOG_BUY_ITEM_CANCEL_BUTTON":"取消","DIALOG_BUY_ITEM_BUY_BUTTON":"购买","HEADER_DIALOG_UPGRADE_ITEM":"升级","TEXT_DIALOG_UPGRADE_ITEM":"升级 %ITEM%","DIALOG_BUY_ITEM_UPGRADE_BUTTON":"升级","DIALOG_NOT_ENOUGH_CRYSTAL_HEADER":"没有足够的水晶","DIALOG_NOT_ENOUGH_CRYSTAL":"您没有足够的水晶 (%CRYSTAL%)","DIALOG_NOT_ENOUGH_CRYSTAL_FOR_BUY_ITEM":"您没有足够的水晶 (%CRYSTAL%) 来购买这个 %ITEM%","DIALOG__NOT_ENOUGH_CRYSTAL_BUY_CRYSTAL_BUTTON":"购买水晶","DIALOG_NOT_ENOUGH_COIN_HEADER":"没有足够的坦克币","DIALOG_NOT_ENOUGH_COIN":"您没有足够的坦克币 (%COIN%)","DIALOG_NOT_ENOUGH_COIN_FOR_ITEM":"您没有足够的坦克币 (%COIN%) 购买这 %ITEM%","DIALOG_NOT_ENOUGH_COIN_FOR_UPGRADE_ITEM":"您没有足够的坦克币 (%COIN%) 来升级 %ITEM%","DIALOG__NOT_ENOUGH_COIN_BUY_COIN_BUTTON":"购买坦克币","DIALOG_NOT_ENOUGH_RUBY_HEADER":"没有足够红宝石","DIALOG_NOT_ENOUGH_RUBY_FOR_ITEM":"您没有足够红宝石 (%RUBY%) 来获得 %ITEM%","DIALOG_NOT_ENOUGH_RUBY_FOR_UPGRADE_ITEM":"您没有足够红宝石 (%RUBY%) 来升级 %ITEM%","DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPGRADE_TITLE":"升级","DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPGRADE_MESSAGE":"没有足够的水晶 (%PRICE%) 来升级 %ITEM%","DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPGRADE_BUTTON":"升级需要 %PRICE%","SPEED_UP_BUTTON":"立即完成","NOT_AVAILABLE":"无法使用","LEVEL":"等级","NO_DRONE_EQUIPPED":"未装备无人机","RANK_IS_NEEDED":"需要等级 %RANK_NAME%","MAX_LEVEL":"最高等级","READ_MORE":"阅读更多","PREMIUM_NEEDED":"需要VIP","RANK_AND_PREMIUM_NEEDED":"需要Vip和%RANK_NAME%等级","NO_RESISTANCE":"没有防护","INSTALL_HERE":"在这里安装","REMOVE_RESISTANCE":"移除防护","EMPTY":"空","LOCKED":"已锁定","BATTERY_PROPERTY":"一节电池为无人机供电一分钟。","SLOT_LOCKED":"已锁定","SLOT_UNLOCK":"解锁","BUY_PAINT_DESCRIPTION":"购买迷彩","LOCK_PAINT_DESCRIPTION":"迷彩不可用","EQUIP_PAINT_DESCRIPTION":"装备迷彩","ITEM_LOCKED":"更改将应用于","SELECT_SLOT_PROTECTION":"选择装备防护的插槽","ALL":"全部","GO_STORE":"去商店","BUY_CONTAINER_BUTTON":"购买幸运礼盒","PROTECTION_MODULE":"防御模块","SHOT_COLOR":"弹道颜色​","NEEDED_RANK":"解锁等级​","COMPLETED":"完成","OPEN_THE_SHOP":"进入商店","DIALOG_NOT_ENOUGH_CRYSTAL_FOR_UPDATE_ITEM":"您没有足够的水晶 (%CRYSTAL%) 来升级 %ITEM%","BOUGHT":"已购买"}} diff --git a/client/tools/AssetRelay.mjs b/client/tools/AssetRelay.mjs new file mode 100644 index 0000000..4a80071 --- /dev/null +++ b/client/tools/AssetRelay.mjs @@ -0,0 +1,189 @@ +// AssetRelay.mjs —— 素材中继(只读本地 HTTP 服务,默认端口 8130) +// +// 目的:把「客户端产物」与「官方静态素材本地镜像」拼成一个可浏览的站点,给浏览器侧客户端用。 +// 1) `/` 与其它静态文件 → client/build/dist/js/productionExecutable/(Kotlin/JS 产物;不存在就清晰报错) +// 2) `/play/static/**` → 3DTank/web/play/static/**(**本地镜像优先**,只读) +// 3) 本地镜像缺文件时(可选)回落到 --upstream(用户的 3DTank/tools/serve.js,默认不启用),并打日志 +// +// 安全:只读;路径做规范化 + 前缀校验,禁止用 `..`(含 %2e%2e)逃出映射根。 +// +// 用法: +// node client/tools/AssetRelay.mjs +// node client/tools/AssetRelay.mjs --port 8130 --upstream http://localhost:8080 +'use strict'; +import fs from 'node:fs'; +import http from 'node:http'; +import https from 'node:https'; +import path from 'node:path'; +import url from 'node:url'; + +const Here = path.dirname(url.fileURLToPath(import.meta.url)); +const ClientDir = path.resolve(Here, '..'); +const RepoDir = path.resolve(ClientDir, '..'); +const DistDir = path.join(ClientDir, 'build', 'dist', 'js', 'productionExecutable'); +const MirrorRoot = path.join(RepoDir, '3DTank', 'web', 'play', 'static'); + +// ---- 参数 +const Args = process.argv.slice(2); +function ArgOf(Name, Default) { + const I = Args.indexOf('--' + Name); + return I >= 0 && Args[I + 1] ? Args[I + 1] : Default; +} +const Port = Number(ArgOf('port', '8130')); +const Upstream = ArgOf('upstream', ''); + +// ---- MIME(按扩展名;够用即可,未列的按 application/octet-stream) +const MimeByExt = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.txt': 'text/plain; charset=utf-8', + '.xml': 'application/xml; charset=utf-8', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.ttf': 'font/ttf', + '.otf': 'font/otf', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.mp3': 'audio/mpeg', + '.ogg': 'audio/ogg', + '.wav': 'audio/wav', + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.3ds': 'application/octet-stream', + '.wasm': 'application/wasm', +}; +function MimeOf(P) { + return MimeByExt[path.extname(P).toLowerCase()] || 'application/octet-stream'; +} + +// 防目录穿越:把相对路径 join 到根后规范化,必须仍在根之内 +function SafeJoin(Root, Rel) { + const Normalized = path.normalize(Rel).replace(/^([/\\])+/, ''); + const Full = path.normalize(path.join(Root, Normalized)); + if (Full !== Root && !Full.startsWith(Root + path.sep)) return null; + return Full; +} + +const UrlPathOf = (Req) => decodeURIComponent((Req.url || '/').split('?')[0].split('#')[0]); + +function Log(Msg) { + console.log('TankIcAssetRelay | ' + Msg); +} + +function SendFile(Res, File, ExtraHeaders) { + const Headers = Object.assign({ 'Content-Type': MimeOf(File), 'Content-Length': String(fs.statSync(File).size) }, ExtraHeaders || {}); + Res.writeHead(200, Headers); + fs.createReadStream(File).pipe(Res); +} + +function SendText(Res, Code, Text, ExtraHeaders) { + const Buf = Buffer.from(Text, 'utf8'); + Res.writeHead(Code, Object.assign({ 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': String(Buf.length) }, ExtraHeaders || {})); + Res.end(Buf); +} + +// 回落:把请求转发给 --upstream(只读代理;命中时打日志) +function RelayUpstream(Req, Res, RelPath) { + const Target = new URL(RelPath, Upstream); + const Lib = Target.protocol === 'https:' ? https : http; + const P = Lib.request( + Target, + { method: 'GET', headers: { Accept: Req.headers.accept || '*/*' } }, + (Up) => { + if (Up.statusCode !== 200) { + Up.resume(); + SendText(Res, 404, '本地镜像没有,且上游返回 ' + Up.statusCode + ':' + RelPath + '\n'); + return; + } + const Headers = {}; + for (const K of ['content-type', 'content-length', 'etag', 'last-modified']) if (Up.headers[K]) Headers[K] = Up.headers[K]; + Headers['Access-Control-Allow-Origin'] = '*'; + Res.writeHead(200, Headers); + Up.pipe(Res); + }, + ); + P.on('error', (E) => SendText(Res, 502, '回落到上游失败:' + E.message + '\n')); + P.end(); +} + +if (!fs.existsSync(DistDir)) { + console.error('✗ 客户端产物目录不存在:' + DistDir); + console.error(' 请先构建:cd client && ' + (process.platform === 'win32' ? 'gradlew.bat' : './gradlew') + ' jsProductionExecutableCompileSync'); + process.exit(1); +} +if (!fs.existsSync(MirrorRoot)) { + console.error('✗ 官方素材本地镜像目录不存在:' + MirrorRoot); + process.exit(1); +} + +const Server = http.createServer((Req, Res) => { + const Pathname = UrlPathOf(Req); + + // CORS(/play/static/** 加宽;其余也给,方便本地调试) + if (Req.method === 'OPTIONS') { + Res.writeHead(204, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Max-Age': '86400', + }); + Res.end(); + return; + } + + // 1) 官方素材:/play/static/** → 3DTank/web/play/static/** + if (Pathname === '/play/static' || Pathname.startsWith('/play/static/')) { + const Rel = Pathname.slice('/play/static/'.length); + const CORS = { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' }; + const Full = SafeJoin(MirrorRoot, Rel); + if (Full === null) { + Log('✗ 目录穿越被拒:' + Pathname); + SendText(Res, 400, '非法路径(试图逃出映射根):' + Pathname + '\n', CORS); + return; + } + if (Rel && fs.existsSync(Full) && fs.statSync(Full).isFile()) { + Log('200 镜像 ' + Pathname + ' (' + MimeOf(Full) + ', ' + fs.statSync(Full).size + 'B)'); + SendFile(Res, Full, CORS); + return; + } + if (Upstream) { + Log('↩ 回落上游 ' + Upstream + Pathname + '(本地镜像缺 ' + Rel + ')'); + RelayUpstream(Req, Res, Pathname); + return; + } + Log('404 镜像缺文件 ' + Pathname); + SendText(Res, 404, '本地镜像里没有这个素材:' + Rel + '\n(默认不启用上游回落;可用 --upstream http://localhost:8080)\n', CORS); + return; + } + + // 2) 客户端产物:/ 与其它路径 → build/dist/js/productionExecutable/ + const Rel = Pathname === '/' ? 'index.html' : Pathname.slice(1); + const Full = SafeJoin(DistDir, Rel); + if (Full === null) { + Log('✗ 目录穿越被拒:' + Pathname); + SendText(Res, 400, '非法路径(试图逃出产物根):' + Pathname + '\n'); + return; + } + if (fs.existsSync(Full) && fs.statSync(Full).isFile()) { + Log('200 产物 ' + Pathname + ' (' + MimeOf(Full) + ', ' + fs.statSync(Full).size + 'B)'); + SendFile(Res, Full); + return; + } + Log('404 产物缺文件 ' + Pathname); + SendText(Res, 404, '客户端产物里没有:' + Rel + '\n产物目录:' + DistDir + '\n'); +}); + +Server.listen(Port, () => { + Log('素材中继已启动:http://localhost:' + Port + '/'); + Log(' 产物目录 ' + DistDir); + Log(' 素材镜像 ' + MirrorRoot + '(只读,/play/static/**)'); + Log(' 上游回落 ' + (Upstream || '未启用(本地镜像优先;缺文件返回 404)')); +}); diff --git a/client/tools/ExportItemIcons.mjs b/client/tools/ExportItemIcons.mjs new file mode 100644 index 0000000..f5e0daf --- /dev/null +++ b/client/tools/ExportItemIcons.mjs @@ -0,0 +1,347 @@ +// ExportItemIcons.mjs —— 从官方解包源码导出「型号枚举 → 官方图标文件」表(客户端可用) +// +// 产物(幂等可重复运行): +// 1. client/src/jsMain/resources/ItemIcons.json 给人看 / 给其它语言用的 JSON 表 +// 2. client/src/jsMain/kotlin/Protocol/ItemIconsData.kt 内嵌同一份 JSON 的 Kotlin 常量 +// (Kotlin/JS 没有跨 browser/node 一致的资源读取 API,口径与 ExportProtocolTables.mjs 相同) +// +// 数据来源(只读,绝不改动): +// 3dtank.com/play/static/js/unpacked/resources.json webpack 模块 id → static/images/xxx.svg +// 3dtank.com/play/static/js/unpacked/module_36659.js 底盘:Mb(eA).upf = switch (t.k3_1) { case → n = <形参>; } +// 3dtank.com/play/static/js/unpacked/module_57128.js 炮塔:ct(Dn).duk = switch (t.k3_1) { … } +// 抗性:ct(Dn).euk = switch (t.k3_1) { case → return <形参>; } +// 3dtank.com/play/static/js/unpacked/module_82347.js 7 格道具图标表(ZMi 数组) +// 3DTank/web/play/static/translations/zh-CN.*.json lobby.garage.* 类别中文(显示层) +// +// 口径(为什么不能「case 号 = 实参位置」): +// 工厂形参只有 714 个,而 switch 里引用的实参是**形参名**;形参与实参一一对应: +// 形参#k 在参数列表第 k 行,实参#k 在调用实参列表第 k 行(两者行号差是文件的固定常量)。 +// 所以必须走:case → 形参名 → 形参声明行 → 同序实参行 → i(<模块id>) → resources.json → 文件名。 +// 自验:用 Dn.euk(抗性 20 项)全套跑一遍,全部命中且都落在本地镜像里(见 stdout 的 SelfCheck)。 +// +// 用法:node client/tools/ExportItemIcons.mjs +'use strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +const Here = path.dirname(url.fileURLToPath(import.meta.url)); +const ClientDir = path.resolve(Here, '..'); +const RepoDir = path.resolve(ClientDir, '..'); +const UnpackedDir = path.join(RepoDir, '3dtank.com', 'play', 'static', 'js', 'unpacked'); +// 图标路径形如 "static/images/xxx.svg",相对 web/play 根(不是 web/play/static) +const WebPlayDir = path.join(RepoDir, '3DTank', 'web', 'play'); +const WebStaticDir = path.join(WebPlayDir, 'static'); +const OutJson = path.join(ClientDir, 'src', 'jsMain', 'resources', 'ItemIcons.json'); +const OutKotlin = path.join(ClientDir, 'src', 'jsMain', 'kotlin', 'Protocol', 'ItemIconsData.kt'); + +const Resources = JSON.parse(fs.readFileSync(path.join(UnpackedDir, 'resources.json'), 'utf8')).resourceMap; + +// ---------------------------------------------------------------- 通用解析:形参表 ↔ 实参表 + +function ReadLines(File) { + return fs.readFileSync(File, 'utf8').split('\n'); +} + +// 形参声明行形如 " Ns,";实参行形如 " i(5012)," 或 " t.exports,"。 +function TrimComma(Text) { + let S = Text.trim(); + if (S.endsWith(',')) S = S.slice(0, -1); + return S; +} + +function ModuleIdOfArg(Line) { + const M = /i\(([0-9]+)\)/.exec(Line); + return M ? M[1] : null; +} + +// 为单个文件建「形参名 → 模块 id」:参数列表从 `function (` 行之后开始,实参列表从 `})(` 行之后开始。 +// 两者同序、同长(取较短者为长度)。返回 null 表示该文件不是这种工厂结构。 +function BuildParamMap(Lines) { + let FnAt = -1; + for (let I = 0; I < Lines.length; I++) if (Lines[I].trim() === '!(function (') { FnAt = I; break; } + if (FnAt < 0) return null; + let ParamEnd = -1; + for (let I = FnAt + 1; I < Lines.length; I++) if (Lines[I] === ' ) {') { ParamEnd = I; break; } + if (ParamEnd < 0) return null; + let InvAt = -1; + for (let I = ParamEnd + 1; I < Lines.length; I++) if (Lines[I] === ' })(') { InvAt = I; break; } + if (InvAt < 0) return null; + let ArgEnd = -1; + for (let I = InvAt + 1; I < Lines.length; I++) if (Lines[I] === ' );' || Lines[I] === ' )') { ArgEnd = I; break; } + if (ArgEnd < 0) return null; + const Count = Math.min(ParamEnd - FnAt - 1, ArgEnd - InvAt - 1); + const Map = {}; + for (let K = 0; K < Count; K++) { + const Name = TrimComma(Lines[FnAt + 1 + K]); + const Id = ModuleIdOfArg(Lines[InvAt + 1 + K]); + if (Name && Id) Map[Name] = Id; + } + return Map; +} + +// 解一个 switch:支持两种体—— +// ① 赋值式(底盘/炮塔):case N: / n = <形参>; / break; +// ② 返回式(抗性):case N: return <形参>; 以及 fall-through(case 40: case 44: return M;) +// 返回 { 序号: 形参名 }。遇到 default / return n; / 收尾 } 即停。 +function ParseSwitch(Lines, SwitchAt) { + const Out = {}; + let Pending = []; + for (let I = SwitchAt + 1; I < Lines.length; I++) { + const L = Lines[I].trim(); + if (L.startsWith('default:') || L === '}' || /^return [A-Za-z0-9_$]+;$/.test(L)) break; + const M = /^case ([0-9]+):(.*)$/.exec(L); + if (!M) continue; + const C = Number(M[1]); + const Rest = M[2].trim(); + let Name = null; + if (Rest.startsWith('return ')) Name = Rest.slice(7).replace(/;.*/, ''); + else if (Rest.startsWith('n = ')) Name = Rest.slice(4).replace(/;.*/, ''); + else if (Rest === '') { + const Nx = Lines[I + 1] ? Lines[I + 1].trim() : ''; + if (Nx.startsWith('return ')) { Name = Nx.slice(7).replace(/;.*/, ''); I++; } + else if (Nx.startsWith('n = ')) { Name = Nx.slice(4).replace(/;.*/, ''); I++; } + else { Pending.push(C); continue; } // fall-through(本 case 与下一个 case 共用体) + } + if (Name != null) { + for (const P of Pending) Out[P] = Name; + Pending = []; + Out[C] = Name; + } + } + return Out; +} + +// 找函数体内第一个 switch (...) 行号;找不到返回 null +function FindSwitch(Lines, FromLine1Based) { + for (let I = FromLine1Based - 1; I < Lines.length; I++) if (/switch \([A-Za-z0-9_$]+\.k3_1\)/.test(Lines[I])) return I; + return null; +} + +// case 表 → { 序号: 图标文件 }(形参 → 模块 id → resources.json) +function ResolveCaseTable(Cases, ParamMap) { + const Out = {}; + const Misses = []; + for (const C of Object.keys(Cases).sort((A, B) => Number(A) - Number(B))) { + const Param = Cases[C]; + const Id = ParamMap[Param]; + const File = Id ? Resources[Id] : null; + const Rel = File && File.startsWith('/') ? File.slice(1) : File; + if (!Rel) Misses.push({ Case: Number(C), Param, ModuleId: Id || null }); + else Out[C] = Rel; // 统一成 "static/images/xxx.svg"(不带前导 /) + } + return { Table: Out, Misses }; +} + +// ---------------------------------------------------------------- 底盘 / 炮塔 / 抗性 + +const HullLines = ReadLines(path.join(UnpackedDir, 'module_36659.js')); +const TurretLines = ReadLines(path.join(UnpackedDir, 'module_57128.js')); +const HullParams = BuildParamMap(HullLines); +const TurretParams = BuildParamMap(TurretLines); +if (!HullParams || !TurretParams) { + console.error('✗ 解析形参/实参表失败(工厂结构变了?)'); + process.exit(1); +} + +const HullSwitchAt = FindSwitch(HullLines, 5133); +const HullRaw = ParseSwitch(HullLines, HullSwitchAt); +const Hull = ResolveCaseTable(HullRaw, HullParams); +const HullMissLines = Hull.Misses.map((M) => 'case ' + M.Case + ' -> 形参 ' + M.Param + '(模块 id ' + (M.ModuleId || '无') + ')'); + +const TurretSwitchAt = FindSwitch(TurretLines, 838); +const TurretRaw = ParseSwitch(TurretLines, TurretSwitchAt); +const Turret = ResolveCaseTable(TurretRaw, TurretParams); + +// 抗性:ct(Dn).euk(约 906 行)—— 自验用 +const ResistSwitchAt = FindSwitch(TurretLines, 906); +const ResistRaw = ParseSwitch(TurretLines, ResistSwitchAt); +const Resist = ResolveCaseTable(ResistRaw, TurretParams); + +// 自验:抗性每一项都要(a)解析出图标文件、(b)文件在本地镜像里真的存在。 +function LocalExists(Rel) { + return fs.existsSync(path.join(WebPlayDir, Rel)); +} +const ResistLines = []; +let ResistLocal = 0; +for (const C of Object.keys(Resist.Table)) { + const Rel = Resist.Table[C]; + const Ok = LocalExists(Rel); + if (Ok) ResistLocal++; + ResistLines.push('case ' + C + ' -> ' + Resist.Table[C] + (Ok ? ' ✓本地存在' : ' ✗本地缺失')); +} +const ResistDistinct = new Set(Object.values(Resist.Table)).size; + +// ---------------------------------------------------------------- 7 格道具图标表(module_82347) + +const SupplyLines = ReadLines(path.join(UnpackedDir, 'module_82347.js')); +const SupplyParams = BuildParamMap(SupplyLines); +const SupplyEntries = []; +if (SupplyParams) { + // 表体:7 个 new ZMi(<类型函数>, <图标资源形参>, <缩放函数>) + for (let I = 0; I < SupplyLines.length; I++) { + const M = /new ZMi\(([^,]+),\s*([A-Za-z0-9_$]+),\s*([^)]+)\)/.exec(SupplyLines[I]); + if (!M) continue; + const TypeFn = M[1].trim(); + const IconParam = M[2].trim(); + const Id = SupplyParams[IconParam]; + const File = Id ? Resources[Id] : null; + const Rel = File || null; + SupplyEntries.push({ + Key: TypeFn, // 产出「道具类型枚举」的函数名(源码里的局部名,语义未解,照抄不猜) + Icon: Rel, + ModuleId: Id || null, + Line: I + 1, + }); + } +} + +// ---------------------------------------------------------------- 其它同类表(自动扫描,如实报告) + +// 扫描所有解包模块:对每个 `switch (X.k3_1)`,若其 case 体解析出的形参**绝大多数**都落在 +// resources.json 的图标资源上(≥ 4 个 case 且 ≥ 80% 命中),即认定是一张「case → 图标」表。 +function ScanOtherTables() { + const Found = []; + // 已单独导出的三张 switch 表不再重复计入「其它表」 + const Known = new Set([ + 'module_36659.js:' + (HullSwitchAt + 1), + 'module_57128.js:' + (TurretSwitchAt + 1), + 'module_57128.js:' + (ResistSwitchAt + 1), + ]); + const Files = fs.readdirSync(UnpackedDir).filter((F) => /^module_\d+\.js$/.test(F)); + for (const F of Files) { + let Lines; + try { + Lines = ReadLines(path.join(UnpackedDir, F)); + } catch (E) { + continue; + } + const Params = BuildParamMap(Lines); + if (!Params) continue; + for (let I = 0; I < Lines.length; I++) { + if (!/switch \([A-Za-z0-9_$]+\.k3_1\)/.test(Lines[I])) continue; + if (Known.has(F + ':' + (I + 1))) continue; + const Cases = ParseSwitch(Lines, I); + const Nums = Object.keys(Cases); + if (Nums.length < 4) continue; + const R = ResolveCaseTable(Cases, Params); + const Hit = Object.keys(R.Table).length; + if (Hit / Nums.length >= 0.8 && Hit >= 4) { + Found.push({ + Module: F, + Line: I + 1, + Cases: Nums.length, + Icons: Object.keys(R.Table).length, + Sample: Object.keys(R.Table).sort((A, B) => Number(A) - Number(B)).slice(0, 4).map((C) => C + '=' + R.Table[C]), + All: R.Table, + }); + } + } + } + return Found; +} +const OtherTables = ScanOtherTables(); + +// ---------------------------------------------------------------- 类别中文(lobby.garage.*) + +const ZhFiles = fs.readdirSync(path.join(WebStaticDir, 'translations')).filter((F) => /^zh-CN\..*\.json$/.test(F)); +const Zh = JSON.parse(fs.readFileSync(path.join(WebStaticDir, 'translations', ZhFiles[0]), 'utf8')); +const GarageLabels = {}; +for (const [K, V] of Object.entries((Zh.lobby && Zh.lobby.garage) || {})) { + if (typeof V === 'string') GarageLabels[K] = V; +} + +// ---------------------------------------------------------------- 组装产物 + +const Selection = { + HullCases: Object.keys(Hull.Table).length, + HullMisses: Hull.Misses.length, + TurretCases: Object.keys(Turret.Table).length, + TurretMisses: Turret.Misses.length, + ResistanceCases: Object.keys(Resist.Table).length, + ResistanceMisses: Resist.Misses.length, + SupplyCount: SupplyEntries.length, + OtherTableCount: OtherTables.length, + GarageLabelCount: Object.keys(GarageLabels).length, +}; + +const Tables = { + Note: + '物品图标表,由 client/tools/ExportItemIcons.mjs 从官方解包源码生成,请勿手改。' + + 'Hull = module_36659.js 的 Mb(eA).upf switch:case(底盘型号)→ i(<模块id>) → resources.json 文件名;' + + 'Turret = module_57128.js 的 ct(Dn).duk switch(炮塔型号);Resistance = 同文件 ct(Dn).euk switch(抗性,' + + '用于自验解析算法)。解析口径:case → 形参名 → 形参声明行 → 同序实参行 → i(<模块id>),' + + '不能假设「case 号 = 实参位置」。Supply = module_82347.js 的 7 格道具图标表(new ZMi 数组,非 switch)。' + + 'OtherTables = 自动扫描到的其它同类 case→图标 表(供手榴弹/无人机/防御模块查用;没有就是空数组)。' + + 'GarageLabels = zh-CN 的 lobby.garage.*(只取字符串叶子),显示层类别中文用。' + + '图标路径统一为 "static/images/xxx.svg"(相对 web/play 根,客户端配 AssetRelay 的 /play 前缀取用)。', + GeneratedAt: new Date().toISOString(), + Source: + '3dtank.com/play/static/js/unpacked/{resources.json,module_36659.js,module_57128.js,module_82347.js} + ' + + '3DTank/web/play/static/translations/' + ZhFiles[0], + Selection, + SelfCheck: { + ResistanceCases: Object.keys(Resist.Table).length, + ResistanceResolved: Object.keys(Resist.Table).length, + ResistanceLocalFilesPresent: ResistLocal, + ResistanceDistinctFiles: ResistDistinct, + Lines: ResistLines, + }, + Hull: Hull.Table, + Turret: Turret.Table, + Resistance: Resist.Table, + Supply: SupplyEntries, + OtherTables, + GarageLabels, +}; + +const Json = JSON.stringify(Tables); +fs.mkdirSync(path.dirname(OutJson), { recursive: true }); +fs.writeFileSync(OutJson, Json + '\n', 'utf8'); + +function EscapeKotlin(S) { + let Out = ''; + for (const Ch of S) { + const Code = Ch.codePointAt(0); + if (Ch === '\\') Out += '\\\\'; + else if (Ch === '"') Out += '\\"'; + else if (Ch === '$') Out += '\\$'; + else if (Code < 0x20) Out += '\\u' + Code.toString(16).padStart(4, '0'); + else Out += Ch; + } + return Out; +} + +const Kotlin = + '// 本文件由 client/tools/ExportItemIcons.mjs 生成,请勿手改。\n' + + '// 内容 = client/src/jsMain/resources/ItemIcons.json(同一份 JSON)。\n' + + 'package Protocol\n' + + '\n' + + 'internal const val ItemIconsJsonText: String =\n "' + EscapeKotlin(Json) + '"\n'; +fs.mkdirSync(path.dirname(OutKotlin), { recursive: true }); +fs.writeFileSync(OutKotlin, Kotlin, 'utf8'); + +// ---------------------------------------------------------------- 自验输出 + +console.log('已生成物品图标表:'); +console.log(' ' + OutJson + ' ' + Json.length + ' 字符'); +console.log(' ' + OutKotlin + ' ' + Kotlin.length + ' 字符'); +console.log(' module_36659.js 形参数 ' + Object.keys(HullParams).length + ' / module_57128.js 形参数 ' + Object.keys(TurretParams).length); +console.log(''); +console.log('【自验】抗性表 Dn.euk(module_57128.js:' + (ResistSwitchAt + 1) + '):' + Object.keys(Resist.Table).length + ' 个 case → 解析出 ' + + Object.keys(Resist.Table).length + ' 个图标,本地镜像里存在 ' + ResistLocal + ' 个,去重后 ' + ResistDistinct + ' 个文件'); +for (const L of ResistLines) console.log(' ' + L); +console.log(' 自验结论:' + (ResistLocal === Object.keys(Resist.Table).length ? '✓ ' + ResistLocal + '/' + Object.keys(Resist.Table).length + ' 全命中且本地存在(去重 ' + ResistDistinct + ' 个文件),case→形参→实参 的解析算法成立' : '✗ 有未命中')); +console.log(''); +console.log('【导出】底盘 upf(module_36659.js:' + (HullSwitchAt + 1) + '):' + Selection.HullCases + ' case → ' + Selection.HullCases + ' 图标(未解 ' + Selection.HullMisses + ')'); +for (const L of HullMissLines) console.log(' 未解:' + L); +console.log('【导出】炮塔 duk(module_57128.js:' + (TurretSwitchAt + 1) + '):' + Selection.TurretCases + ' case → ' + Selection.TurretCases + ' 图标(未解 ' + Selection.TurretMisses + ')'); +console.log('【导出】7 格道具图标表(module_82347.js):' + SupplyEntries.length + ' 项 → ' + SupplyEntries.map((S) => S.Icon).join(', ')); +console.log(''); +console.log('【自动扫描】其它 case→图标 表:' + OtherTables.length + ' 张'); +for (const T of OtherTables) { + console.log(' ' + T.Module + ':' + T.Line + ' case ' + T.Cases + ' → 图标 ' + T.Icons + ' 样例 ' + T.Sample.join(', ')); +} +if (!OtherTables.length) console.log(' (无:手榴弹 / 无人机 / 防御模块未找到同类 case→图标 表,如实标注「无此表」)'); +console.log(''); +console.log('【显示层】lobby.garage.* 中文标签 ' + Object.keys(GarageLabels).length + ' 条');