feat(session): 解析 Pt 容器为物品目录,未覆盖项从 30029 降到 2203

This commit is contained in:
TermiNexus committed 2026-10-25 13:15:06 +08:00
1 parent f29ad2caaa
commit ee7504000f
4 files changed
+331 -2

No files matched your search

+18
View File
@@ -501,6 +501,24 @@
(`ju`/`Su`/`Eu`/`Iu`,对应 `BuyableCC.buyable`/`currencyId`、`ItemCategoryCC.category` 等)
是 module_85196 的内部函数、不导出,18334 也不直接读 CC 槽位,
说明转换经 DI 服务接口完成;下一步用别名表追该接口的实现方法。
- 物品目录(`App/Session/ItemCatalog.kt`):
- 结论修正:抓包里这 30029 条未覆盖 Pt 项**不是用户状态**,而是静态物品表
(1371 个物品对象 + 515 个装置对象),原包也不把它放进 Redux,而是由服务按需读取。
- 容器形态(`names/model_registry.json` + 抓包实测):`pt.Items` 按 `Id == 0` 的对象标记分组,
标记项的值就是 ObjectId;组内按 modelId 建索引。物品组通常是
`CountableItemCC, ItemPropertiesCC, ItemSaleByKitCC, BuyableCC, TimePeriodModelCC,
ItemModelCC, ItemRarityCC, DescriptionModelCC, ItemCategoryCC, PremiumItemCC,
ItemViewCategoryCC, DescriptionMediaCC, VisibleCC`;装置组换成
`DevicePropertiesCC + TemporaryItemCC`。
- `CC → GarageItemObject` / `GarageDeviceObject` 的字段顺序取自 module_18334 的
`S$`(物品)与 `gct`(装置)的构造函数实参,token → 模型类由跨模块别名表反查,逐条登记在文件头。
- `SessionRouter` 在 Pt 分支先调 `Catalog.Ingest`,被目录吸收的项不再计入未覆盖:
抓包实测 **物品 1399 / 装置 516 / 吸收 27826 项**,未覆盖从 30029 降到 2203
(余下主要是 methodId 级未覆盖、`0` 对象标记、以及 ResistancesCC/DurationCC 等非物品对象)。
自检同时断言目录非空,并打印名称样本(如 `史诗幸运礼盒`、`紧凑型油箱`)验证字段映射。
- 下一步:用户拥有的物品列表来自 GarageCC 的引用字段(`itemsOnDepot` / `itemsOnMarket` /
`mountedItems`)与模型命令流;等这部分抓包/命令解码补齐后,用目录把 id 解析成
`GarageItemObject` 再派发 `SetItemsOnDepot` / `SetItemsOnMarket` / `SetMountedItems`。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
`launchParams` 是纯透传(`(pass-through)`,无 reducer),已把字段收口为 `Map<String, String>`。
@@ -0,0 +1,261 @@
// ItemCatalog.kt —— 车库 / 商店物品目录(Pt 容器 → GarageItemObject / GarageDeviceObject)
//
// 为什么单列一个「目录」而不是 Redux 切片:
// · 抓包里最大的 Pt 容器是**静态物品表**(1371 个物品对象 + 515 个装置对象),
// 不是用户拥有的物品列表;原包也不把它放状态层,而是由服务按需读取。
// · 原包的「CC → GarageItemObject」转换在 module_18334 的 `S$`(物品)与 `gct`(装置)里,
// 逐个从对象上下文取子模型:`Gr(n, Z(token))`。token → 模型类的对应关系见下表的注释。
//
// 容器形态(names/model_registry.json + 抓包实测):
// Pt.Items = [0(对象标记,value 是 ObjectId), 模型1, 模型2, …, 0(下一个对象), …]
// 每个对象组里模型顺序不固定,按 modelId 建索引即可;组边界 = Id == 0 的项。
// 物品组的模型集合(实测):
// CountableItemCC, ItemPropertiesCC, ItemSaleByKitCC, BuyableCC,
// TimePeriodModelCC, ItemModelCC, ItemRarityCC, DescriptionModelCC,
// ItemCategoryCC, PremiumItemCC, ItemViewCategoryCC, DescriptionMediaCC, VisibleCC
// 装置组在前述集合上换成 DevicePropertiesCC + TemporaryItemCC。
//
// 字段对应(module_18334 的 `S$`,new GarageItemObject(...) 的实参顺序):
// id←对象标记 | description←DescriptionModelCC | item←ItemModelCC
// isPremium←PremiumItemCC.PremiumItem | itemCategory←ItemCategoryCC.Category
// itemViewCategory←ItemViewCategoryCC.Category | buyable←BuyableCC.Buyable
// availableForAll←AvailableForAllCC | discount←DiscountCC | timePeriod←TimePeriodModelCC
// modification←ModificationCC(baseItemId=Field1, level=ModificationIndex)
// properties←ItemPropertiesCC | devices←ItemDevicesCC | count←CountableItemCC.Field1
// kit←GarageKitCC | countable←CountableItemCC 是否存在 | mountIndex←调用点下标(目录里为 0)
// availableSkins←AvailableSkinsCC | availableShotSkins←AvailableShotSkinsCC
// mountedSkin←MountedSkinCC(834829861153964627) | mountedShotSkin←MountedSkinCC(905970599512839002)
// skinPreview←SkinPreviewCC | grouped←GroupedCC | rarity←ItemRarityCC.Rarity
// resistanceBackgroundImg←ResistanceBackgroundCC | visible←VisibleCC.Visible
// descriptionMedia←DescriptionMediaCC | saleByKitItems←ItemSaleByKitCC
// isSellingForCoin/Crystal/Ruby 在原包传 `Ct`(未提供),这里保持 null。
//
// 装置字段对应(`gct`,new GarageDeviceObject(...)):
// id←对象标记 | baseItemId←容器级 ModificationCC.Field1(组里没有则 0)
// temporaryItemCC←TemporaryItemCC | description←DescriptionModelCC | item←ItemModelCC
// isPremium←PremiumItemCC | itemCategory←ItemCategoryCC | itemViewCategory←ItemViewCategoryCC
// buyable←BuyableCC | discount←DiscountCC | timePeriod←TimePeriodModelCC
// deviceProperty←DevicePropertiesCC | rarity←ItemRarityCC | visible←VisibleCC
// descriptionMedia←DescriptionMediaCC
package App.Session
import App.State.GarageDeviceObject
import App.State.GarageItemModification
import App.State.GarageItemObject
import Net.Generated.AvailableForAllCC
import Net.Generated.AvailableShotSkinsCC
import Net.Generated.AvailableSkinsCC
import Net.Generated.BuyableCC
import Net.Generated.CountableItemCC
import Net.Generated.DescriptionMediaCC
import Net.Generated.DescriptionModelCC
import Net.Generated.DevicePropertiesCC
import Net.Generated.DiscountCC
import Net.Generated.GarageKitCC
import Net.Generated.GroupedCC
import Net.Generated.ItemCategoryCC
import Net.Generated.ItemDevicesCC
import Net.Generated.ItemModelCC
import Net.Generated.ItemPropertiesCC
import Net.Generated.ItemRarityCC
import Net.Generated.ItemSaleByKitCC
import Net.Generated.ItemViewCategoryCC
import Net.Generated.ModificationCC
import Net.Generated.MountedSkinCC
import Net.Generated.PremiumItemCC
import Net.Generated.ResistanceBackgroundCC
import Net.Generated.SkinPreviewCC
import Net.Generated.TemporaryItemCC
import Net.Generated.TimePeriodModelCC
import Net.Generated.VisibleCC
import Net.Protocol.PtPayload
/** 目录模型的 modelId(来源:names/model_registry.json)。 */
object CatalogModelIds {
const val CountableItem = 120961863156462946L
const val ItemProperties = 4245000165576407500L
const val ItemSaleByKit = 4309274349580876968L
const val Buyable = 2769268442698022653L
const val TimePeriod = 8032192927455575509L
const val ItemModel = 3438505192580052999L
const val ItemRarity = 658797348514407988L
const val DescriptionModel = 3935107155251354407L
const val ItemCategory = 2114179554921095892L
const val PremiumItem = 1139677084511624062L
const val ItemViewCategory = 6117306895090150422L
const val DescriptionMedia = 5952355564848177159L
const val Visible = 5738770490413536673L
const val AvailableForAll = 6589716392600418185L
const val Discount = 2951667042968033849L
const val Modification = 2494628067134376039L
const val UpgradeParams = 4743567393313674375L
const val ItemDevices = 7391892068905080676L
const val GarageKit = 5219530271535770658L
const val AvailableSkins = 5207857306014573244L
const val AvailableShotSkins = 1164458258646872337L
const val MountedSkin = 834829861153964627L
const val MountedShotSkin = 905970599512839002L
const val SkinPreview = 6680653332718076818L
const val Grouped = 1865478986390468300L
const val ResistanceBackground = 6628468327090974523L
const val DeviceProperties = 7640081207058999117L
const val TemporaryItem = 3893855999545616334L
/** 物品组只要出现其中任意一个模型,就按物品目录处理。 */
val ItemGroupMarkers: Set<Long> = setOf(
ItemModel, ItemCategory, Buyable, DescriptionModel, TimePeriod, ItemRarity,
PremiumItem, ItemViewCategory,
)
}
/**
* 静态物品目录:`Pt` 容器里按「对象」分组后,把 CC 组合成 `GarageItemObject` / `GarageDeviceObject`。
*
* 同一个 modelId 可能随对象重复出现(每个对象一份),所以目录以对象 id 为键。
*/
class ItemCatalog {
val Items: MutableMap<Long, GarageItemObject> = LinkedHashMap()
val Devices: MutableMap<Long, GarageDeviceObject> = LinkedHashMap()
/** 被识别并吸收的 Pt 项数。 */
var ConsumedItems: Int = 0
private set
/** 已识别的对象组数(物品 + 装置)。 */
var Groups: Int = 0
private set
/** 不是目录的对象组数(例如车库配置对象:PassToShopCC / TemperatureCC …)。 */
var SkippedGroups: Int = 0
private set
/** 组里既不是物品也不是装置的模型名,用于诊断未识别目录。 */
val SkippedModels: MutableMap<String, Int> = LinkedHashMap()
/**
* 尝试吸收一个 Pt 容器。
*
* @return 与 `Payload.Items` 等长的「已吸收」标记;完全不是目录容器时返回 null。
*/
fun Ingest(Payload: PtPayload): BooleanArray? {
val Raw = Payload.Items
if (Raw.isEmpty()) return null
val Consumed = BooleanArray(Raw.size)
var Any = false
var Start = -1
fun Flush(Begin: Int, End: Int) {
if (Begin < 0) return
val Zero = Raw[Begin].Value as? Long
val Models = HashMap<Long, Any>(16)
var I = Begin + 1
while (I < End) {
val Id = Raw[I].Id
if (Id != 0L) Models[Id] = Raw[I].Value
I++
}
if (Models.isEmpty()) return
val ObjectId = Zero ?: 0L
val IsDevice = Models.containsKey(CatalogModelIds.DeviceProperties) ||
Models.containsKey(CatalogModelIds.TemporaryItem)
val IsItem = CatalogModelIds.ItemGroupMarkers.any { Models.containsKey(it) }
if (IsDevice) {
Devices[ObjectId] = MakeDevice(ObjectId, Models)
Mark(Consumed, Begin, End)
Groups++
Any = true
} else if (IsItem) {
Items[ObjectId] = MakeItem(ObjectId, Models)
Mark(Consumed, Begin, End)
Groups++
Any = true
} else {
SkippedGroups++
for (Name in Models.keys) {
val Text = App.State.ModelNamesByModelId[Name] ?: Name.toString()
SkippedModels[Text] = (SkippedModels[Text] ?: 0) + 1
}
}
}
for (I in Raw.indices) {
if (Raw[I].Id != 0L) continue
if (Start >= 0) Flush(Start, I)
Start = I
}
if (Start >= 0) Flush(Start, Raw.size)
if (!Any) return null
ConsumedItems += Consumed.count { it }
return Consumed
}
private fun Mark(Consumed: BooleanArray, Start: Int, End: Int) {
for (I in Start until End) Consumed[I] = true
}
private fun MakeItem(Id: Long, Models: Map<Long, Any>): GarageItemObject {
val Modification = Models[CatalogModelIds.Modification] as? ModificationCC
val Countable = Models[CatalogModelIds.CountableItem] as? CountableItemCC
return GarageItemObject(
id = Id,
description = Models[CatalogModelIds.DescriptionModel],
item = Models[CatalogModelIds.ItemModel],
isPremium = (Models[CatalogModelIds.PremiumItem] as? PremiumItemCC)?.PremiumItem ?: false,
itemCategory = (Models[CatalogModelIds.ItemCategory] as? ItemCategoryCC)?.Category ?: 0,
itemViewCategory = (Models[CatalogModelIds.ItemViewCategory] as? ItemViewCategoryCC)?.Category ?: 0,
buyable = (Models[CatalogModelIds.Buyable] as? BuyableCC)?.Buyable ?: false,
availableForAll = (Models[CatalogModelIds.AvailableForAll] as? AvailableForAllCC)?.AvailableForAll ?: false,
discount = Models[CatalogModelIds.Discount],
timePeriod = Models[CatalogModelIds.TimePeriod],
modification = Modification?.let {
GarageItemModification(vcf_1 = it.Field1, xcf_1 = it.ModificationIndex.toInt())
},
properties = Models[CatalogModelIds.ItemProperties],
upgradeableParams = null,
devices = Models[CatalogModelIds.ItemDevices],
count = Countable?.Field1 ?: 0,
kit = Models[CatalogModelIds.GarageKit] as? GarageKitCC,
countable = Countable != null,
mountIndex = 0,
availableSkins = Models[CatalogModelIds.AvailableSkins] as? AvailableSkinsCC,
availableShotSkins = Models[CatalogModelIds.AvailableShotSkins] as? AvailableShotSkinsCC,
mountedSkin = Models[CatalogModelIds.MountedSkin] as? MountedSkinCC,
mountedShotSkin = Models[CatalogModelIds.MountedShotSkin] as? MountedSkinCC,
skinPreview = Models[CatalogModelIds.SkinPreview] as? SkinPreviewCC,
grouped = Models[CatalogModelIds.Grouped] as? GroupedCC,
rarity = (Models[CatalogModelIds.ItemRarity] as? ItemRarityCC)?.Rarity,
favorite = false,
isSellingForCoin = null,
isSellingForCrystal = null,
isSellingForRuby = null,
resistanceBackgroundImg = Models[CatalogModelIds.ResistanceBackground] as? ResistanceBackgroundCC,
visible = (Models[CatalogModelIds.Visible] as? VisibleCC)?.Visible ?: true,
descriptionMedia = Models[CatalogModelIds.DescriptionMedia] as? DescriptionMediaCC,
saleByKitItems = Models[CatalogModelIds.ItemSaleByKit] as? ItemSaleByKitCC,
)
}
private fun MakeDevice(Id: Long, Models: Map<Long, Any>): GarageDeviceObject {
val Modification = Models[CatalogModelIds.Modification] as? ModificationCC
return GarageDeviceObject(
id = Id,
baseItemId = Modification?.Field1 ?: 0L,
temporaryItemCC = Models[CatalogModelIds.TemporaryItem] as? TemporaryItemCC,
description = Models[CatalogModelIds.DescriptionModel],
item = Models[CatalogModelIds.ItemModel],
isPremium = (Models[CatalogModelIds.PremiumItem] as? PremiumItemCC)?.PremiumItem ?: false,
itemCategory = (Models[CatalogModelIds.ItemCategory] as? ItemCategoryCC)?.Category ?: 0,
itemViewCategory = (Models[CatalogModelIds.ItemViewCategory] as? ItemViewCategoryCC)?.Category ?: 0,
buyable = (Models[CatalogModelIds.Buyable] as? BuyableCC)?.Buyable ?: false,
discount = Models[CatalogModelIds.Discount],
timePeriod = Models[CatalogModelIds.TimePeriod],
deviceProperty = Models[CatalogModelIds.DeviceProperties] as? DevicePropertiesCC,
rarity = (Models[CatalogModelIds.ItemRarity] as? ItemRarityCC)?.Rarity,
visible = (Models[CatalogModelIds.Visible] as? VisibleCC)?.Visible ?: true,
descriptionMedia = Models[CatalogModelIds.DescriptionMedia] as? DescriptionMediaCC,
)
}
}
@@ -78,6 +78,10 @@ object SessionCheck {
var Bad = 0
var HeaderErrors = 0
var Errors = 0
var PtRecords = 0
var PtItems = 0
var PtMaxItems = 0
val PtSample = ArrayList<String>()
val Methods = LinkedHashMap<Long, Int>()
for (Path in Files) {
@@ -95,11 +99,47 @@ object SessionCheck {
for (R in List) {
Methods[R.MethodId] = (Methods[R.MethodId] ?: 0) + 1
if (R.Payload.contentEquals(R.Reencoded)) Ok++ else Bad++
val V = R.Value
if (V is Net.Protocol.PtPayload) {
PtRecords++
PtItems += V.Items.size
if (V.Items.size > PtMaxItems) PtMaxItems = V.Items.size
if (PtSample.size < 3) {
PtSample.add(
"obj=${V.Objects.size},items=${V.Items.size},ids=" +
V.Items.take(16).joinToString(",") { it.Id.toString() }
)
}
}
}
Router.RouteAll(List)
}
println("文件 $Frames|记录 $Records|回编码一致 $Ok|不一致 $Bad|记录头截断 $HeaderErrors|其它错误 $Errors|别名表=${Session.AliasCount}")
println("Pt 容器 $PtRecords 条|项 $PtItems|单条最多 $PtMaxItems")
for (S in PtSample) println(" pt 样本:$S")
println(
"目录:物品 ${Router.Catalog.Items.size}|装置 ${Router.Catalog.Devices.size}" +
"|吸收项 ${Router.Cataloged}|跳过组 ${Router.Catalog.SkippedGroups}"
)
if (Router.Catalog.SkippedModels.isNotEmpty()) {
val TopSkip = Router.Catalog.SkippedModels.entries.sortedByDescending { it.value }.take(6)
println("目录跳过模型:" + TopSkip.joinToString(",") { "${it.key}×${it.value}" })
}
for (S in Router.Catalog.Items.values.take(3)) {
val D = S.description as? Net.Generated.DescriptionModelCC
println(
" 物品样本:id=${S.id}|name=${D?.Name}|category=${S.itemCategory}|view=${S.itemViewCategory}" +
"|premium=${S.isPremium}|count=${S.count}|rarity=${S.rarity}"
)
}
for (S in Router.Catalog.Devices.values.take(2)) {
val D = S.description as? Net.Generated.DescriptionModelCC
println(
" 装置样本:id=${S.id}|base=${S.baseItemId}|name=${D?.Name}" +
"|category=${S.itemCategory}|rarity=${S.rarity}"
)
}
println("路由:已派发 ${Router.Applied}|未覆盖 ${Router.Unmapped}|失败 ${Router.Failed}|用户=${Router.Store.State.User.Uid}|屏幕=${Router.Store.State.Screen.Current}")
val TopModels = Router.UnmappedModels.entries.sortedByDescending { it.value }.take(8)
if (TopModels.isNotEmpty()) {
@@ -114,7 +154,7 @@ object SessionCheck {
println("高频 methodId:" + Top.joinToString(",") { "${it.key}×${it.value}" })
}
// 抓包目录里混有入口/握手帧,解不出记录是正常现象;只要所有解出的记录都能回编码即可。
val Passed = Bad == 0 && Records > 0
val Passed = Bad == 0 && Records > 0 && Router.Catalog.Items.isNotEmpty()
println(if (Passed) "结果:通过" else "结果:失败")
ExitSession(if (Passed) 0 else 1)
}
@@ -17,8 +17,13 @@ class SessionRouter(InitialState: RootState = RootState()) {
val Store: ReduxStore<RootState> = ReduxStore(InitialState, ::ReduceRoot)
/** 静态物品目录:吸收 Pt 容器里的物品 / 装置 CC 组,不进 Redux 状态。 */
val Catalog: ItemCatalog = ItemCatalog()
var Applied: Int = 0
private set
var Cataloged: Int = 0
private set
var Unmapped: Int = 0
private set
var Failed: Int = 0
@@ -39,7 +44,12 @@ class SessionRouter(InitialState: RootState = RootState()) {
val Value = Record.Value
if (Value is PtPayload) {
var Last: Any? = null
for (Item in Value.Items) {
val Consumed = Catalog.Ingest(Value)
for ((Index, Item) in Value.Items.withIndex()) {
if (Consumed != null && Consumed[Index]) {
Cataloged++
continue
}
val MakeModel = DownlinkModelActions[Item.Id]
if (MakeModel == null) {
Unmapped++