fix(session): wire 模型经适配器转为状态模型,跳过同名类型冲突

This commit is contained in:
TermiNexus committed 2026-10-25 13:42:00 +08:00
1 parent ee7504000f
commit 1d1479592f
7 files changed
+252 -45

No files matched your search

+18
View File
@@ -519,6 +519,24 @@
- 下一步:用户拥有的物品列表来自 GarageCC 的引用字段(`itemsOnDepot` / `itemsOnMarket` /
`mountedItems`)与模型命令流;等这部分抓包/命令解码补齐后,用目录把 id 解析成
`GarageItemObject` 再派发 `SetItemsOnDepot` / `SetItemsOnMarket` / `SetMountedItems`。
- wire → 状态层模型适配(`App/Session/WireAdapters.kt`):
- 修正了一个会静默失败的问题:`DownlinkActions` 之前直接 `Value as UserPropertiesCC`,
但 codec 解出的是 `Net.Generated.UserPropertiesCC`(字段 `Field1…`),
动作要的是 `App.State.UserPropertiesCC`(语义字段),运行时必然 ClassCastException
(会话自检里的「失败 3」就是这类转换)。
- 生成器 `gen_downlink_actions.js` 增加 `Adapters` 表:需要转换的模型改为调用
`WireAdapters.ToXxx(Value as Net.Generated.Xxx)`;当前覆盖 3 个模型
(UserProperties / ReferralEventUserModel / GaragePreview)。
- `UserPropertiesCC` 的 wire 字段 ↔ 语义名取自 module_47243 的 `Gs.t7h`(写入顺序)
与 `fi(lg).toString`(槽位 → 名称),逐位对应关系登记在适配器文件头;
`rankBounds` 的嵌套对象拆成状态层的 `rankBounds` / `topBound` 两个字符串。
- 会话自检新增合成用例:构造一条 `UserPropertiesCC` Pt 记录,断言
`Uid / Id / Crystals / Rank / Score / HasSpectatorPermissions` 写进 `RootState.User`。
- 同名类型冲突:`ProtocolMetadata.Downlink` 里部分方法的实际 Kotlin 类型带后缀
(`GaragePreviewModelCC0004`、`UserPropertiesCC5452` 等,跨切片同名类改名)。
生成器现在读取该表,跳过「实际类型 ≠ 语义模型名」的方法(4 条),
不再生成必然 ClassCastException 的映射;清单记在 `downlink_actions.json` 的
`SkippedTypeConflicts`。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
`launchParams` 是纯透传(`(pass-through)`,无 reducer),已把字段收口为 `Map<String, String>`。
+23 -25
View File
@@ -8,7 +8,7 @@
],
"Methods": 681,
"Resolved": 477,
"Matched": 13,
"Matched": 9,
"MatchedModels": [
{
"Model": "GaragePreviewModelCC",
@@ -26,6 +26,28 @@
"Field": "user"
}
],
"SkippedTypeConflicts": [
{
"MethodId": "8836555743203700004",
"Model": "GaragePreviewModelCC",
"Type": "GaragePreviewModelCC0004"
},
{
"MethodId": "2940608322101861340",
"Model": "ReferralEventUserModelCC",
"Type": "ReferralEventUserModelCC1340"
},
{
"MethodId": "5628691596950015452",
"Model": "UserPropertiesCC",
"Type": "UserPropertiesCC5452"
},
{
"MethodId": "7864034006236084309",
"Model": "UserPropertiesCC",
"Type": "UserPropertiesCC4309"
}
],
"Models": [
{
"Model": "AndroidPayCC",
@@ -830,24 +852,12 @@
}
],
"Actions": [
{
"MethodId": "8836555743203700004",
"Model": "GaragePreviewModelCC",
"Action": "InitGarageResources",
"Field": "params"
},
{
"MethodId": "2940606982196178690",
"Model": "ReferralEventUserModelCC",
"Action": "SetReferralEventData",
"Field": "eventData"
},
{
"MethodId": "2940608322101861340",
"Model": "ReferralEventUserModelCC",
"Action": "SetReferralEventData",
"Field": "eventData"
},
{
"MethodId": "1880027114297758713",
"Model": "ReferralEventUserModelCC",
@@ -878,18 +888,6 @@
"Action": "SetUserPropertiesAction",
"Field": "user"
},
{
"MethodId": "5628691596950015452",
"Model": "UserPropertiesCC",
"Action": "SetUserPropertiesAction",
"Field": "user"
},
{
"MethodId": "7864034006236084309",
"Model": "UserPropertiesCC",
"Action": "SetUserPropertiesAction",
"Field": "user"
},
{
"MethodId": "3977381235093458189",
"Model": "UserPropertiesCC",
+47 -3
View File
@@ -25,6 +25,22 @@ const MethodTable = JSON.parse(fs.readFileSync(path.join(Names, 'method_table.js
const CodecFields = JSON.parse(fs.readFileSync(path.join(Names, 'codec_fields.json'), 'utf8')).Codecs;
const ModelBinding = JSON.parse(fs.readFileSync(path.join(Names, 'model_binding.json'), 'utf8')).Entries;
const Inventory = fs.readFileSync(path.join(KotlinRoot, 'Inventory.kt'), 'utf8');
const MetadataPath = path.resolve(
P, '..', 'TankiOffline', 'net', 'src', 'jsMain', 'kotlin', 'Net', 'Generated', 'Metadata.kt',
);
// 方法 → 生成代码里实际使用的 Kotlin 类型名。
// 同名协议模型跨切片冲突时,生成器会加后缀(如 GaragePreviewModelCC0004);
// 这类方法不能按语义名去匹配动作,否则运行时 ClassCastException。
const MethodType = new Map();
try {
const Meta = fs.readFileSync(MetadataPath, 'utf8');
const Re = /(\d+)L to ProtocolCommandMeta\(\d+L, "([^"]+)"/g;
let M;
while ((M = Re.exec(Meta))) MethodType.set(M[1], M[2]);
} catch (E) {
// 没有生成产物时退化为「不检查类型冲突」,保持旧行为。
}
// ---------------------------------------------------------------- 模型名解析
const ByModuleCodec = new Map();
@@ -62,6 +78,21 @@ function ParseActions(Text) {
const Actions = ParseActions(Inventory);
const ModelNames = new Set(Rows.map((R) => R.Model).filter(Boolean));
// 模型名 → 候选动作(字段类型恰为该模型,且动作只有这一个字段)
// 注意:wire 模型(Net.Generated)与状态层模型(App.State)同名但类型不同,
// 这些模型必须经 App/Session/WireAdapters.kt 显式转换,不能直接 `as`。
const Adapters = {
UserPropertiesCC: 'App.Session.WireAdapters.ToUserProperties',
ReferralEventUserModelCC: 'App.Session.WireAdapters.ToReferralEvent',
GaragePreviewModelCC: 'App.Session.WireAdapters.ToGaragePreview',
};
function ValueExpr(Model, TypeName) {
return Adapters[Model]
? `${Adapters[Model]}(Value as Net.Generated.${TypeName})`
: `Value as ${TypeName}`;
}
// 模型名 → 候选动作(字段类型恰为该模型,且动作只有这一个字段)
const ActionByModel = new Map();
for (const [Name, Fields] of Actions) {
@@ -75,6 +106,7 @@ for (const [Name, Fields] of Actions) {
// ---------------------------------------------------------------- 汇总
const Matched = [];
const MatchedModels = [];
const SkippedTypeConflicts = [];
const Models = [];
for (const Name of [...ModelNames].sort()) {
const Cand = ActionByModel.get(Name) || [];
@@ -82,6 +114,11 @@ for (const Name of [...ModelNames].sort()) {
Models.push({ Model: Name, Methods: Methods.length, Actions: Cand.map((C) => C.Action) });
if (Cand.length === 1) {
for (const Id of Methods) {
const ActualType = MethodType.get(String(Id));
if (ActualType && ActualType !== Name) {
SkippedTypeConflicts.push({ MethodId: Id, Model: Name, Type: ActualType });
continue;
}
Matched.push({ MethodId: Id, Model: Name, Action: Cand[0].Action, Field: Cand[0].Field });
}
MatchedModels.push({ Model: Name, Action: Cand[0].Action, Field: Cand[0].Field });
@@ -101,6 +138,7 @@ fs.writeFileSync(
Resolved: Rows.filter((R) => R.Model).length,
Matched: Matched.length,
MatchedModels,
SkippedTypeConflicts,
Models,
Actions: Matched,
},
@@ -121,13 +159,16 @@ Lines.push('// → Inventory.kt 里唯一一个「只带该模型字段」的
Lines.push('//');
Lines.push('// 覆盖范围:仅自动可判定的组合;其余协议模型需要 CC → 多字段的转换动作,');
Lines.push('// 见 names/downlink_actions.json 的 Models 清单(后续按协议逐个补转换器)。');
Lines.push('//');
Lines.push('// wire 模型(Net.Generated)与状态层模型(App.State)同名但类型不同;');
Lines.push('// 需要转换的模型走 App/Session/WireAdapters.kt,生成器里登记在 Adapters 表。');
Lines.push('package App.State');
Lines.push('');
Lines.push('/** 每条记录解码后,用 (Any) -> Any? 构造对应动作;未覆盖的 methodId 不在表里。 */');
Lines.push('val DownlinkActions: Map<Long, (Any) -> Any?> = mapOf(');
for (const M of Matched) {
Lines.push(
' ' + M.MethodId + 'L to { Value -> ' + M.Action + '(Value as ' + M.Model + ') },',
' ' + M.MethodId + 'L to { Value -> ' + M.Action + '(' + ValueExpr(M.Model, M.Model) + ') },',
);
}
Lines.push(')');
@@ -140,7 +181,9 @@ for (const M of MatchedModels) {
return ByModuleCodec.get(Key) === M.Model;
}).map((E) => E.ModelId);
for (const Id of Ids) {
Lines.push(' ' + Id + 'L to { Value -> ' + M.Action + '(Value as ' + M.Model + ') },');
Lines.push(
' ' + Id + 'L to { Value -> ' + M.Action + '(' + ValueExpr(M.Model, M.Model) + ') },',
);
}
}
Lines.push(')');
@@ -149,5 +192,6 @@ fs.writeFileSync(path.join(KotlinRoot, 'DownlinkActions.kt'), Lines.join('\n'),
console.log(
`methodId ${Rows.length} 个,解析出协议模型 ${Rows.filter((R) => R.Model).length} 个;` +
`自动匹配动作 ${Matched.length} 条,涉及模型 ${new Set(Matched.map((M) => M.Model)).size} 个。`,
`自动匹配动作 ${Matched.length} 条,涉及模型 ${new Set(Matched.map((M) => M.Model)).size} 个;` +
`跳过同名类型冲突 ${SkippedTypeConflicts.length} 条。`,
);
@@ -115,6 +115,46 @@ object SessionCheck {
Router.RouteAll(List)
}
// 合成一条 UserPropertiesCC 容器记录:验证 wire 模型 → 动作 → reducer 的闭环。
// 抓包里没有用户属性帧,所以这里只用固定的 wire 值做通路自检(不依赖抓包)。
val Probe = SessionRouter()
val UserWire = Net.Generated.UserPropertiesCC(
Field1 = 11,
CanUseGroup = true,
Field3 = 500,
Field4 = 7,
Field5 = 22,
Field6 = 0L,
Field7 = 33,
Field8 = 9,
HasSpectatorPermissions = true,
Field10 = 44,
Field11 = 777001L,
PartnerUid = null,
Field13 = 5,
RankBounds = Net.Generated.RankBounds(4, 9),
Field15 = 1,
Field16 = 55,
Field17 = 66,
Field18 = 77,
UID = "uid-check",
UserProfileUrl = "https://example/",
Field21 = 88,
Field22 = 99,
)
val UserPt = Net.Protocol.PtPayload(
listOf(Net.Protocol.PtObject(777001L, 0L)),
listOf(Net.Protocol.PtItem(1893408944113965505L, UserWire)),
)
Probe.Route(Net.Protocol.DecodedRecord(777001L, 0L, UserPt, ByteArray(0), ByteArray(0)))
val U = Probe.Store.State.User
val UserOk = U.Uid == "uid-check" && U.Id == 777001L && U.Crystals == 500 &&
U.Rank == 5 && U.Score == 66 && U.HasSpectatorPermissions && Probe.Applied == 1
println(
"用户属性闭环:Uid=${U.Uid}|Id=${U.Id}|Crystals=${U.Crystals}|Rank=${U.Rank}" +
"|Score=${U.Score}|应用=${Probe.Applied}|${if (UserOk) "通过" else "失败"}"
)
println("文件 $Frames|记录 $Records|回编码一致 $Ok|不一致 $Bad|记录头截断 $HeaderErrors|其它错误 $Errors|别名表=${Session.AliasCount}")
println("Pt 容器 $PtRecords 条|项 $PtItems|单条最多 $PtMaxItems")
for (S in PtSample) println(" pt 样本:$S")
@@ -149,12 +189,25 @@ object SessionCheck {
if (TopUnmapped.isNotEmpty()) {
println("未覆盖 id:" + TopUnmapped.joinToString(",") { "${it.key}×${it.value}" })
}
if (Router.FailedIds.isNotEmpty()) {
val TopFailed = Router.FailedIds.entries.sortedByDescending { it.value }.take(6)
println(
"失败 id:" + TopFailed.joinToString(",") {
val Name = App.State.ModelNamesByModelId[it.key]
(if (Name != null) "$Name(${it.key})" else it.key.toString()) + "×" + it.value
}
)
}
if (Router.FailedReasons.isNotEmpty()) {
val TopReason = Router.FailedReasons.entries.sortedByDescending { it.value }.take(2)
println("失败原因:" + TopReason.joinToString(";") { "${it.key}×${it.value}" })
}
val Top = Methods.entries.sortedByDescending { it.value }.take(5)
if (Top.isNotEmpty()) {
println("高频 methodId:" + Top.joinToString(",") { "${it.key}×${it.value}" })
}
// 抓包目录里混有入口/握手帧,解不出记录是正常现象;只要所有解出的记录都能回编码即可。
val Passed = Bad == 0 && Records > 0 && Router.Catalog.Items.isNotEmpty()
val Passed = Bad == 0 && Records > 0 && Router.Catalog.Items.isNotEmpty() && UserOk
println(if (Passed) "结果:通过" else "结果:失败")
ExitSession(if (Passed) 0 else 1)
}
@@ -12,6 +12,7 @@ import App.State.RootState
import Net.Protocol.DecodedRecord
import Net.Protocol.PtPayload
import State.ReduxStore
import kotlin.js.js
class SessionRouter(InitialState: RootState = RootState()) {
@@ -35,6 +36,15 @@ class SessionRouter(InitialState: RootState = RootState()) {
/** 未覆盖的协议模型名计数(model_registry 能解析出名字的部分)。 */
val UnmappedModels: MutableMap<String, Int> = LinkedHashMap()
/** 转换/派发失败的 id 计数(methodId 或 Pt item modelId)。 */
val FailedIds: MutableMap<Long, Int> = LinkedHashMap()
/** 失败原因计数(异常信息),便于定位是类型不匹配还是字段缺失。 */
val FailedReasons: MutableMap<String, Int> = LinkedHashMap()
private fun RuntimeName(V: Any?): String =
js("(function (V) { return V && V.constructor ? V.constructor.name : '?' })(V)") as String
/** 已应用动作的类名(调试 / 自检用)。 */
val AppliedNames: MutableList<String> = ArrayList()
@@ -69,6 +79,9 @@ class SessionRouter(InitialState: RootState = RootState()) {
}
} catch (E: Throwable) {
Failed++
FailedIds[Item.Id] = (FailedIds[Item.Id] ?: 0) + 1
val Why = (E.message ?: E.toString()) + " @ " + RuntimeName(Item.Value)
FailedReasons[Why] = (FailedReasons[Why] ?: 0) + 1
}
}
return Last
@@ -89,6 +102,9 @@ class SessionRouter(InitialState: RootState = RootState()) {
Action
} catch (E: Throwable) {
Failed++
FailedIds[Record.MethodId] = (FailedIds[Record.MethodId] ?: 0) + 1
val Why = E.message ?: E.toString()
FailedReasons[Why] = (FailedReasons[Why] ?: 0) + 1
null
}
}
@@ -0,0 +1,79 @@
// WireAdapters.kt —— 线上协议模型(Net.Generated)→ 状态层动作载荷(App.State)
//
// 为什么需要这一层:
// codec 解出来的是 `Net.Generated.*`(按字段表生成的 wire 模型,匿名字段叫 Field1…),
// 而动作 / reducer 期望的是 `App.State.*`(按原包 toString / 调用点还原的语义模型)。
// 两边同名但类型不同,直接 `as` 会在运行时 ClassCastException,所以必须显式转换。
//
// 字段对应证据:
// · UserPropertiesCC:module_47243 的 codec `Gs.t7h` 按 wire 字段顺序写入模型槽位,
// 同模块 `fi(lg).toString` 给出槽位 → 语义名。逐位对应:
// Field1 bazookaGearScore, Field2 canUseGroup, Field3 crystals, Field4 crystalsRating,
// Field5 droneGearScore, Field6 eventPassEndTime, Field7 gearScore, Field8 goldsTakenRating,
// Field9 hasSpectatorPermissions, Field10 hullGearScore, Field11 id, Field12 partnerUid,
// Field13 rank, Field14 rankBounds, Field15 registrationTimestamp, Field16 resistanceGearScore,
// Field17 score, Field18 scoreRating, Field19 uid, Field20 userProfileUrl,
// Field21 userRating, Field22 weaponGearScore.
// 原包 `toString` 里 rankBounds 是嵌套对象,`he4().toString()`;状态层拆成 rankBounds / topBound 两个字符串。
// · ReferralEventUserModelCC:wire 命名字段与 App.State 顺序一致(Field6/7/8 对应
// referralCategory / referralQuestsSize / referrerCategory)。
// · GaragePreviewModelCC:Field1..4 是相机参数(float),状态层用 Int/Int/Double/Int 收口,
// GarageBox / SkyboxFrontSide 是资源 id(long)。
package App.Session
import App.State.GaragePreviewModelCC as StateGaragePreview
import App.State.ReferralEventUserModelCC as StateReferralEvent
import App.State.UserPropertiesCC as StateUserProperties
import Net.Generated.GaragePreviewModelCC as WireGaragePreview
import Net.Generated.ReferralEventUserModelCC as WireReferralEvent
import Net.Generated.UserPropertiesCC as WireUserProperties
object WireAdapters {
fun ToUserProperties(V: WireUserProperties): StateUserProperties = StateUserProperties(
bazookaGearScore = V.Field1,
canUseGroup = V.CanUseGroup,
crystals = V.Field3,
crystalsRating = V.Field4,
droneGearScore = V.Field5,
eventPassEndTime = V.Field6.toInt(),
gearScore = V.Field7,
goldsTakenRating = V.Field8,
hasSpectatorPermissions = V.HasSpectatorPermissions,
hullGearScore = V.Field10,
id = V.Field11,
partnerUid = V.PartnerUid,
rank = V.Field13,
rankBounds = V.RankBounds.Field1.toString(),
topBound = V.RankBounds.Field2.toString(),
registrationTimestamp = V.Field15,
resistanceGearScore = V.Field16,
score = V.Field17,
scoreRating = V.Field18,
uid = V.UID,
userProfileUrl = V.UserProfileUrl,
userRating = V.Field21,
weaponGearScore = V.Field22,
)
fun ToReferralEvent(V: WireReferralEvent): StateReferralEvent = StateReferralEvent(
hideReferralCategory = V.HideReferralCategory,
hideReferrerCategory = V.HideReferrerCategory,
hidedAnnounce = V.HidedAnnounce ?: 0L,
promoCodeDialogBackGround = V.PromoCodeDialogBackGround,
promoCodeDialogImage = V.PromoCodeDialogImage,
referralCategory = V.Field6,
referralQuestsSize = V.Field7,
referrerCategory = V.Field8,
referrerPanelTitle = V.ReferrerPanelTitle,
)
fun ToGaragePreview(V: WireGaragePreview): StateGaragePreview = StateGaragePreview(
cameraAltitude = V.Field1.toInt(),
cameraDistance = V.Field2.toInt(),
cameraFov = V.Field3.toDouble(),
cameraPitch = V.Field4.toInt(),
garageBox = V.GarageBox,
skyboxFrontSide = V.SkyboxFrontSide,
)
}
@@ -7,28 +7,27 @@
//
// 覆盖范围:仅自动可判定的组合;其余协议模型需要 CC → 多字段的转换动作,
// 见 names/downlink_actions.json 的 Models 清单(后续按协议逐个补转换器)。
//
// wire 模型(Net.Generated)与状态层模型(App.State)同名但类型不同;
// 需要转换的模型走 App/Session/WireAdapters.kt,生成器里登记在 Adapters 表。
package App.State
/** 每条记录解码后,用 (Any) -> Any? 构造对应动作;未覆盖的 methodId 不在表里。 */
val DownlinkActions: Map<Long, (Any) -> Any?> = mapOf(
8836555743203700004L to { Value -> InitGarageResources(Value as GaragePreviewModelCC) },
2940606982196178690L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) },
2940608322101861340L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) },
1880027114297758713L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) },
276076212290602640L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
7864195052961084235L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
3982372259754314184L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
6492680006777979140L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
5628691596950015452L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
7864034006236084309L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
3977381235093458189L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
848734776772436751L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
8468360636859619961L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
2940606982196178690L to { Value -> SetReferralEventData(App.Session.WireAdapters.ToReferralEvent(Value as Net.Generated.ReferralEventUserModelCC)) },
1880027114297758713L to { Value -> SetReferralEventData(App.Session.WireAdapters.ToReferralEvent(Value as Net.Generated.ReferralEventUserModelCC)) },
276076212290602640L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
7864195052961084235L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
3982372259754314184L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
6492680006777979140L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
3977381235093458189L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
848734776772436751L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
8468360636859619961L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
)
/** Pt/Lt 容器里的 modelId → 动作(同一批协议模型的容器形态)。 */
val DownlinkModelActions: Map<Long, (Any) -> Any?> = mapOf(
3730185794267514852L to { Value -> InitGarageResources(Value as GaragePreviewModelCC) },
5167916989664044333L to { Value -> SetReferralEventData(Value as ReferralEventUserModelCC) },
1893408944113965505L to { Value -> SetUserPropertiesAction(Value as UserPropertiesCC) },
3730185794267514852L to { Value -> InitGarageResources(App.Session.WireAdapters.ToGaragePreview(Value as Net.Generated.GaragePreviewModelCC)) },
5167916989664044333L to { Value -> SetReferralEventData(App.Session.WireAdapters.ToReferralEvent(Value as Net.Generated.ReferralEventUserModelCC)) },
1893408944113965505L to { Value -> SetUserPropertiesAction(App.Session.WireAdapters.ToUserProperties(Value as Net.Generated.UserPropertiesCC)) },
)