From ab1a905d288b02754068784a6a86a5516351bbe9 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Fri, 18 Sep 2026 12:01:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M7=20=E7=82=B9=E8=A3=85=E5=A4=87?= =?UTF-8?q?=E5=8D=B3=E8=90=BD=E5=BA=93=EF=BC=8C=E9=87=8D=E5=90=AF=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=AB=AF=E5=90=8E=E4=BB=8D=E5=9B=9E=E6=98=BE=EF=BC=88?= =?UTF-8?q?=E9=97=AD=E5=90=88=E3=80=8C=E8=83=BD=E5=88=87=E6=8D=A2=E4=BD=86?= =?UTF-8?q?=E6=B2=A1=E6=8C=81=E4=B9=85=E5=8C=96=E3=80=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 客户端:Uplink 加 M_.fcn 构造(6 条抓包样本逐字节相同)、仓库每行加「装备」按钮、Equip() 入队 + 本地乐观标记(对齐真实客户端的 jU reducer)。服务端:store.js 加 LoadMountedFlags/SetMounted(UPDATE ... WHERE Mounted<>1,幂等)/OverrideMountedRecord;session.js 加可选 RecordOverride 钩子;index.js 进车库时刷新快照、收 M_.fcn 落库。回显只在库与模板有差异时覆盖并打日志(装备前帧仍与抓包逐字节一致 775441B;装备后 775449B,+8B = 多一个 i64)。验收:浏览器点装备 → 服务端落盘 c2s_0064.bin 与抓包 c2s_0141.bin 逐字节相同;MySQL Items.Mounted=1 共 6 行;重启 9092 后启动日志显示 Items.Mounted=1 共 6 件、重进车库 UI 6 个装备按钮置灰与库一致。回归:selftest 1108/9/90、CompareContainerDecode 5 帧 0 不一致、三构建全绿。未实现并如实记录:类别互斥(物品类别来源未解出,不硬猜)、挂载位设备 mountedDevice 未处理。 --- 3DTank/tools/server/index.js | 39 +++++++++- 3DTank/tools/server/session.js | 12 ++- 3DTank/tools/server/store.js | 73 ++++++++++++++++++- client/src/jsMain/kotlin/Main.kt | 11 ++- .../src/jsMain/kotlin/Protocol/ClientState.kt | 7 ++ .../jsMain/kotlin/Protocol/GarageSession.kt | 21 ++++++ client/src/jsMain/kotlin/Protocol/SelfTest.kt | 5 +- client/src/jsMain/kotlin/Protocol/Uplink.kt | 11 +++ .../jsMain/kotlin/Protocol/UplinkSelfTest.kt | 29 +++++++- 9 files changed, 202 insertions(+), 6 deletions(-) diff --git a/3DTank/tools/server/index.js b/3DTank/tools/server/index.js index 514701b..90ab80a 100644 --- a/3DTank/tools/server/index.js +++ b/3DTank/tools/server/index.js @@ -174,7 +174,20 @@ function HandleEnterSpace(State, Client, Conn, Body, At) { // space-init-*.json 模板本身保留不动(本分支只是"有会话文件时优先用它")。 const Session = SessionReplay.Load(path.join(PROJECT, 'names'), E.SpaceId); if (Session) { - const Replay = SessionReplay.MakeReplay(Session.Frames, { Container, Semantic, Log: (M) => State.Log(M) }); + // M7 回显:进空间时顺手刷一次「已挂载物品」快照,保证下发的是库里此刻的真实状态 + Store.LoadMountedFlags().catch(() => {}); + // 字段级覆盖钩子:只改 mountedItems 这一条记录的内容(库里 Items.Mounted=1 的集合), + // 其余记录/字段一字不动;集合无差异时返回原数组 ⇒ 帧字节仍与抓包逐字节一致。 + const RecordOverride = (F, Records) => { + const R = Store.OverrideMountedRecord(Records); + if (!R || !R.Changed) return Records; + State.Log(' → 回显覆盖:mountedItems 由模板 ' + R.Before.length + ' 项改为库中 ' + R.After.length + + ' 项(' + F.File + ';库值 [' + R.After.join(', ') + '])'); + return R.Records; + }; + const Replay = SessionReplay.MakeReplay(Session.Frames, { + Container, Semantic, Log: (M) => State.Log(M), RecordOverride, + }); SpaceReplays.set(String(E.SpaceId), Replay); const Push = Session.Frames.filter((F) => !(F.After || []).length).length; State.Log(' 会话回放: names/space-session-' + E.SpaceId + '.json 帧 ' + Session.Frames.length + @@ -236,6 +249,10 @@ const LOGIN_SPACES = [ // 菜单项名就是 "GARAGE" 这类值——比 button_click 更可靠的"打开了某屏"信号。 const USER_ACTIVITY_METHOD = '2522279596878141909'; +// M_.fcn(M7「装备」上行;module_85196,字段 ycm_1: i64 = 物品 id,靶 = 空间 id)。 +// 抓包 space-100073070 里 6 条样本(c2s_0081/0101/0114/0126/0129/0141.bin),逐字节口径见客户端 UplinkSelfTest.kt。 +const MOUNT_ITEM_METHOD = '1079642233486876258'; + // 入口层命令(Hash Response / Open space)只能走「控制通道」——即发过 Hash Request 的那条连接。 // 实测(packets/relay-logout/session_007):真实服的 4 条 Open space 都发在控制通道上; // 空间连接上只跑空间命令帧([nullmap][spaceId][(methodId)(payload)])。 @@ -414,6 +431,26 @@ function HandleFrame(State, Client, Conn, Frame) { }; const Pending = []; const OnRecord = (R, TargetId) => { + // M7「装备」上行 M_.fcn(ItemId):落库(幂等,见 store.js 的 SetMounted)+ 照常让会话回放器换出它触发的帧。 + // 注意:这条上行**不属于**查库定向应答的接管范围(responder.js 只管 ba.lcr/ls.xbl/G.q1wd), + // 它是一次「写」而不是「读」,所以直接在这里接。 + if (String(R.MethodId) === MOUNT_ITEM_METHOD) { + const ItemId = + Buffer.isBuffer(R.Payload) && R.Payload.length >= 8 ? R.Payload.readBigInt64BE(0).toString() : null; + if (ItemId === null) { + State.Log(' ⚠ M_.fcn 负载不足 8 字节,解不出物品 id:' + (R.Payload ? R.Payload.toString('hex') : '(无负载)')); + } else if (Store.Available()) { + State.Log(' 装备请求 M_.fcn(ItemId=' + ItemId + ') → 落库 Items.Mounted=1'); + Pending.push(() => Store.SetMounted(ItemId)); + } else { + State.Log(' ⚠ 库不可用,装备 ItemId=' + ItemId + ' 未能落库'); + } + if (Replay) { + const Hits = Replay.OnUplink({ MethodId: R.MethodId, Target: String(TargetId), Name: R.Name }, Sender); + if (Hits) State.Log(' → 会话重放命中:发出 ' + Hits + ' 帧(该上行不再计入未实现)'); + } + return true; + } const Ctx = { Sender: StoreSender, Replay, ReplaySender: Sender }; const Task = Responder ? Responder.Prepare(R, TargetId, Ctx) : null; if (Task) { diff --git a/3DTank/tools/server/session.js b/3DTank/tools/server/session.js index 6254045..25fab05 100644 --- a/3DTank/tools/server/session.js +++ b/3DTank/tools/server/session.js @@ -72,6 +72,10 @@ function MakeReplay(Frames, Deps) { // 没给 Container / Semantic 时自己加载一份(用法上 index.js 会传现成的实例,避免重复读表) const Semantic = Opts.Semantic || SemanticModule.Load(NAMES_DIR); const Container = Opts.Container || Semantic.Container; + // 可选的「字段级覆盖」钩子(M7 回显用):(FrameSpec, Records) → Records。 + // 只在帧是结构化形态(有 Records、非 Hex 透传)时调用;返回的新数组会替换原记录再组帧。 + // 返回原数组(或 null)⇒ 一字不改,帧字节与抓包保持逐字节一致(对照验收因此不受污染)。 + const RecordOverride = typeof Opts.RecordOverride === 'function' ? Opts.RecordOverride : null; const All = Frames || []; const Sent = new Set(); const Waited = new Set(); @@ -94,7 +98,13 @@ function MakeReplay(Frames, Deps) { const NmSize = Link.NullmapSize(Body, 0); Buf = Link.EncodeFrame(Body.slice(NmSize), Body.slice(0, NmSize)); } else { - Buf = Space.BuildInitFrames({ Frames: [F] }, Container, Semantic)[0].Frame; + // 字段级覆盖(可选):只在结构化帧上生效;没差异时 RecordOverride 会返回原数组 + let Spec = F; + if (RecordOverride && Array.isArray(F.Records)) { + const Out = RecordOverride(F, F.Records); + if (Out && Array.isArray(Out) && Out !== F.Records) Spec = Object.assign({}, F, { Records: Out }); + } + Buf = Space.BuildInitFrames({ Frames: [Spec] }, Container, Semantic)[0].Frame; } BytesByFile.set(F.File, Buf); return Buf; diff --git a/3DTank/tools/server/store.js b/3DTank/tools/server/store.js index 0ad525b..5fbd693 100644 --- a/3DTank/tools/server/store.js +++ b/3DTank/tools/server/store.js @@ -69,6 +69,7 @@ let Sem = null; // semantic.js 实例(组帧要用;index.js 会把自己那 const Cache = { Devices: null, // { Slots: Set, BySlot: Map, Mounted: Map } Flags: new Map(), // SpaceId|Kind → string[] + MountedFlags: null, // Items.Mounted=1 的 ItemId 列表(按 ItemId 升序);null = 还没读到库 Order: new Map(), // SpaceId → Map OrderList: new Map(), // SpaceId → string[](首次出现序) Slices: new Map(), // SpaceId|ObjectId → [{Id, Value}] @@ -109,9 +110,11 @@ async function Open(Config) { Pool = P; Enabled = true; await LoadDeviceTables(); + await LoadMountedFlags(); // 回显覆盖是**同步**路径(组帧),连库时先整表读进内存 Log('已连库(' + Db.LoadConfig(Config).Database + '@' + Db.LoadConfig(Config).Host + '):挂载位 ' + (Cache.Devices ? Cache.Devices.Slots.size : 0) + ' 个 / 可用设备 ' + - (Cache.Devices ? [...Cache.Devices.BySlot.values()].reduce((A, B) => A + B.length, 0) : 0) + ' 条'); + (Cache.Devices ? [...Cache.Devices.BySlot.values()].reduce((A, B) => A + B.length, 0) : 0) + ' 条' + + ' / Items.Mounted=1 共 ' + (Cache.MountedFlags ? Cache.MountedFlags.length : 0) + ' 件'); } catch (Err) { Enabled = false; Pool = null; @@ -157,6 +160,72 @@ async function LoadDeviceTables() { Cache.Devices = { Slots: new Set(Slots.map((R) => String(R.SlotObjectId))), BySlot, Mounted: M }; } +// ---------------------------------------------------------------- 已挂载物品(M7:装备落库 + 回显覆盖) + +// Items.Mounted=1 的内存快照(整表很小,几十行级别)。回显覆盖发生在**同步**的组帧路径里, +// 所以像 DeviceSlots 一样连库时读进内存,落库后再刷新。 +async function LoadMountedFlags() { + if (!Pool) return null; + const Rows = await Db.Many(Pool, 'SELECT ItemId FROM Items WHERE Mounted = 1 ORDER BY ItemId'); + Cache.MountedFlags = Rows.map((R) => String(R.ItemId)); + return Cache.MountedFlags; +} + +// 同步取快照(没读到库返回 null —— 调用方据此「不覆盖」,绝不用空集合冒充"库里没有") +function MountedFlagsSnapshot() { + return Cache.MountedFlags ? Cache.MountedFlags.slice() : null; +} + +// 装备落库:把该 ItemId 置 Mounted=1。 +// ★ 幂等:UPDATE 带 `AND Mounted <> 1`,重复收到同一件时 affectedRows=0、不产生任何副作用。 +// ★ 类别替换(同类互斥)**未实现**:物品的「类别」字段来源还没解出来,不硬猜 —— +// 本函数只置该件为已挂载,不清除同类别里原来挂着的那件。 +async function SetMounted(ItemId) { + Require(); + const Id = String(ItemId); + const [Res] = await Pool.query('UPDATE Items SET Mounted = 1 WHERE ItemId = ? AND Mounted <> 1', [Id]); + const Changed = Res.affectedRows > 0; + if (Changed) { + Log('落库:Items.Mounted=1(ItemId=' + Id + ')已写入'); + await LoadMountedFlags(); // 刷新快照,下一次进车库的回显覆盖就能看到它 + Cache.Flags.clear(); // 查库路径(FlaggedItems)的旗标缓存一并失效 + } else { + Log('落库:Items.Mounted=1(ItemId=' + Id + ')已是 1,幂等跳过(无副作用)'); + const Row = await Db.One(Pool, 'SELECT Mounted FROM Items WHERE ItemId = ?', [Id]); + if (!Row) Log(' ⚠ 库里没有 ItemId=' + Id + ' 这一行,落库未生效'); + } + return Changed; +} + +// 字段级覆盖:把一组记录里的 mountedItems(methodId 3692495084100053385)换成库里 Items.Mounted=1 的集合, +// 其余记录与字段一字不动。返回 { Records, Changed, Before, After };没有该记录 / 快照未就绪 → null。 +// ★ 「只在确实有差异时生效」:集合相同 ⇒ Changed=false 且 Records 原样返回,帧字节与抓包保持逐字节一致。 +// ★ 顺序口径:原有项按模板顺序保留(仍在库里的),库里新增的按 ItemId 升序接在后面 —— +// 列表顺序在这条协议里没有语义(库里也没有对应列),这样能让「只加了一件」的差异最小。 +function OverrideMountedRecord(Records) { + if (!Array.isArray(Records)) return null; + const DbIds = MountedFlagsSnapshot(); + if (!DbIds) return null; // 还没读到库 → 不覆盖 + const Idx = Records.findIndex( + (R) => String(R.MethodId) === METHOD.MountedItems && R.Fields && Array.isArray(R.Fields.mountedItems), + ); + if (Idx < 0) return null; + const Before = Records[Idx].Fields.mountedItems.map(String); + const DbSet = new Set(DbIds); + const BeforeSet = new Set(Before); + const SameSet = DbSet.size === BeforeSet.size && Before.every((X) => DbSet.has(X)); + if (SameSet) return { Records, Changed: false, Before, After: Before }; + const Kept = Before.filter((X) => DbSet.has(X)); + const Added = DbIds + .filter((X) => !BeforeSet.has(X)) + .sort((A, B) => (BigInt(A) < BigInt(B) ? -1 : BigInt(A) > BigInt(B) ? 1 : 0)); + const After = Kept.concat(Added); + const New = Records.map((R, I) => + I === Idx ? Object.assign({}, R, { Fields: Object.assign({}, R.Fields, { mountedItems: After }) }) : R, + ); + return { Records: New, Changed: true, Before, After }; +} + function Require() { if (!Pool) throw new Error('store 还没连库'); } @@ -716,6 +785,8 @@ module.exports = { METHOD, Open, Available, Close, SetLog, SetSemantic, DevicesLoaded, UnloadObjects, ObjectsData, Dependencies, BatchObjectIds, BatchInstances, FlaggedItems, SpaceObjectIds, BuildFrame, MakeNullmap, CountBits, Cache, + // M7:装备落库 + mountedItems 回显覆盖 + SetMounted, LoadMountedFlags, MountedFlagsSnapshot, OverrideMountedRecord, }; if (process.argv.indexOf('--selfcheck') >= 0 || process.argv.indexOf('--verify') >= 0) { diff --git a/client/src/jsMain/kotlin/Main.kt b/client/src/jsMain/kotlin/Main.kt index 831f392..8c46235 100644 --- a/client/src/jsMain/kotlin/Main.kt +++ b/client/src/jsMain/kotlin/Main.kt @@ -68,7 +68,16 @@ private val App = FC { val Mounted = Session.MountedItems.toHashSet() ul { for (Id in Session.DepotItems) { - li { +("$Id " + (if (Mounted.contains(Id)) "已挂载" else "未挂载")) } + val IsMounted = Mounted.contains(Id) + li { + +("$Id " + (if (IsMounted) "已挂载" else "未挂载") + " ") + // 「装备」按钮:点击发 M_.fcn(ItemId);已挂载的置灰(防重复点,落库本身也幂等) + button { + disabled = IsMounted + onClick = { Session.Equip(Id) } + +"装备" + } + } } } if (Session.DepotItems.isEmpty()) p { +"(尚未收到 itemsOnDepot;挂载依据 mountedItems 集合,共 ${Session.MountedItems.size} 件)" } diff --git a/client/src/jsMain/kotlin/Protocol/ClientState.kt b/client/src/jsMain/kotlin/Protocol/ClientState.kt index 861c3a0..711585c 100644 --- a/client/src/jsMain/kotlin/Protocol/ClientState.kt +++ b/client/src/jsMain/kotlin/Protocol/ClientState.kt @@ -100,6 +100,13 @@ internal class ClientState { var MountedItems: List = emptyList(); private set var MountedItemsFrame = -1; private set + // 装备动作的本地乐观更新(真实客户端的 jU reducer 就是这么做的):点一下就把该件标成已挂载, + // 界面立即可见;下一次进车库时服务端下发的 mountedItems(已按库覆盖)会把这里整体替换成权威值。 + fun MarkMounted(ItemId: String) { + if (MountedItems.contains(ItemId)) return + MountedItems = MountedItems + ItemId + } + // ---- 挂载位 / 设备 // devicesLoaded 在服务端是**覆盖写设备行**(见 3DTank/tools/server/responder.js 头注), // 所以按挂载位 id 收敛、后到者覆盖;同时保留收到过的记录条数以便追溯。 diff --git a/client/src/jsMain/kotlin/Protocol/GarageSession.kt b/client/src/jsMain/kotlin/Protocol/GarageSession.kt index 1c9ea57..ed7b6e7 100644 --- a/client/src/jsMain/kotlin/Protocol/GarageSession.kt +++ b/client/src/jsMain/kotlin/Protocol/GarageSession.kt @@ -220,6 +220,27 @@ internal class GarageSession(private val OnChanged: () -> Unit) { Queue.add(QueuedUplink(UplinkName(MethodId.toString()), TargetId, Bytes)) } + // ---------------------------------------------------------------- 装备动作(M7) + + // 装备一件仓库物品:发 M_.fcn(ItemId)(靶 = 空间 id,口径同 M4/M5),并按本地乐观语义立刻标成已挂载。 + // 落库与持久化由服务端负责;这里只保证「点一下 → 上行入队(下一拍发出)→ 界面立即显示已挂载」。 + fun Equip(ItemId: String) { + val Id = ItemId.toLongOrNull() + if (Id == null) { + AddLog("✗ 装备失败:物品 id 不是 i64:$ItemId") + OnChanged() + return + } + val Already = StateObj.MountedItems.contains(ItemId) + val Bytes = Uplink.BuildMFcn(SpaceId, Id) + EnqueueManual(Uplink.M_FCN, SpaceId.toString(), Bytes) + StateObj.MarkMounted(ItemId) + AddLog( + "⟳ 装备 $ItemId → 已入队 M_.fcn(靶=$SpaceId,${Bytes.size} 字节${if (Already) ",本地已是已挂载" else ",本地乐观标为已挂载"})" + ) + OnChanged() + } + // ---------------------------------------------------------------- 日志与命名 private fun AddLog(Line: String) { diff --git a/client/src/jsMain/kotlin/Protocol/SelfTest.kt b/client/src/jsMain/kotlin/Protocol/SelfTest.kt index 6feea30..abb219e 100644 --- a/client/src/jsMain/kotlin/Protocol/SelfTest.kt +++ b/client/src/jsMain/kotlin/Protocol/SelfTest.kt @@ -97,7 +97,10 @@ internal object SelfTest { AllPassed = AllPassed && Report.Passed } BuildSelfTestPassed = AllPassed - Log(if (AllPassed) "✓ 帧构造自检通过:4 条上行帧的构造结果与抓包 hex 完全相同" else "✗ 帧构造自检未通过") + Log( + if (AllPassed) "✓ 帧构造自检通过:${UplinkSelfTest.Cases.size} 条上行帧的构造结果与抓包 hex 完全相同" + else "✗ 帧构造自检未通过" + ) Log("") } diff --git a/client/src/jsMain/kotlin/Protocol/Uplink.kt b/client/src/jsMain/kotlin/Protocol/Uplink.kt index fb7cb7a..b90e868 100644 --- a/client/src/jsMain/kotlin/Protocol/Uplink.kt +++ b/client/src/jsMain/kotlin/Protocol/Uplink.kt @@ -12,6 +12,9 @@ internal object Uplink { const val BA_LCR = 7806260029063678398L // ba.lcr 空负载,靶 = 槽位对象 id const val LS_XBL = 2528403217084400809L // ls.xbl 空负载,靶 = 要卸载的对象 id const val G_Q1WD = 1816792453857564692L // G.q1wd 负载 = i32(X = 服务端 _loadDependencies 给的回调号) + // M_.fcn(module_85196,字段 ycm_1: i64)负载 = 物品 id,靶 = 空间 id —— 「装备」动作的上行。 + // 抓包 space-100073070 里 6 条样本(c2s_0081/0101/0114/0126/0129/0141.bin)逐字节核对见 UplinkSelfTest.kt。 + const val M_FCN = 1079642233486876258L // 下行 methodId(服务端下发的那几条;布局见 Container.LayoutByMethod) const val LOAD_DEPENDENCIES = 3216143066888387731L // zt @@ -38,4 +41,12 @@ internal object Uplink { W.I32(X) return Build(TargetId, G_Q1WD, W.ToByteArray()) } + + // M_.fcn:负载就是那一个 i64(物品 id,如 920009630969);靶 = 客户端进入的那个空间 id + // (实测 6 条样本靶全是 100073070,不是 s2c 记录头上的 objectId)。 + fun BuildMFcn(TargetId: Long, ItemId: Long): ByteArray { + val W = ByteWriter() + W.I64(ItemId) + return Build(TargetId, M_FCN, W.ToByteArray()) + } } diff --git a/client/src/jsMain/kotlin/Protocol/UplinkSelfTest.kt b/client/src/jsMain/kotlin/Protocol/UplinkSelfTest.kt index 4ede0dd..6c5ed53 100644 --- a/client/src/jsMain/kotlin/Protocol/UplinkSelfTest.kt +++ b/client/src/jsMain/kotlin/Protocol/UplinkSelfTest.kt @@ -1,10 +1,11 @@ -// UplinkSelfTest.kt —— c2s 空间帧构造的**字节级**自检(M4 验收 (a)) +// UplinkSelfTest.kt —— c2s 空间帧构造的**字节级**自检(M4 验收 (a),M7 追加 M_.fcn) // // 期望 hex 是真实抓包整帧的原样十六进制(3DTank/packets/plain/,只读): // space-100073070/c2s_0007.bin ba.lcr 19 字节 // space-100073070/c2s_0002.bin ls.xbl 19 字节 // space-100073070/c2s_0001.bin G.q1wd 23 字节(负载 i32=1) // space-100045260/c2s_0001.bin G.q1wd 23 字节(负载 i32=1;自动应答实际发出的形态,靶 = 空间 id) +// space-100073070/c2s_0081/0101/0114/0126/0129/0141.bin M_.fcn 27 字节(帧体 25B = nullmap 1 + 靶 8 + methodId 8 + 负载 i64 8) // node 侧还能读到抓包文件时,额外做一次「构造结果 vs 文件字节」的全字节比对(不只看 hex 串)。 package Protocol @@ -24,6 +25,7 @@ internal object UplinkSelfTest { private const val LsXblTargetId = -2147481788L private const val GQ1wd073070TargetId = 100073070L private const val GQ1wd045260TargetId = 100045260L + private const val MFcnTargetId = 100073070L val Cases: List = listOf( UplinkCase( @@ -42,6 +44,31 @@ internal object UplinkSelfTest { "G.q1wd(i32=1) 空间 100045260(自动应答形态)", "space-100045260/c2s_0001.bin", "0015000000000005f691cc19368b1c6aca581400000001", Uplink.BuildGQ1wd(GQ1wd045260TargetId, 1), ), + // M_.fcn(装备):抓包空间 100073070 里一共 6 条,逐条列出(负载 = 物品 id) + UplinkCase( + "M_.fcn(物品 920009630969)", "space-100073070/c2s_0081.bin", + "0019000000000005f6fe6e0efba8e4237e9e62000000d634d8e4f9", Uplink.BuildMFcn(MFcnTargetId, 920009630969L), + ), + UplinkCase( + "M_.fcn(物品 920009631011)", "space-100073070/c2s_0101.bin", + "0019000000000005f6fe6e0efba8e4237e9e62000000d634d8e523", Uplink.BuildMFcn(MFcnTargetId, 920009631011L), + ), + UplinkCase( + "M_.fcn(物品 920009630955)", "space-100073070/c2s_0114.bin", + "0019000000000005f6fe6e0efba8e4237e9e62000000d634d8e4eb", Uplink.BuildMFcn(MFcnTargetId, 920009630955L), + ), + UplinkCase( + "M_.fcn(物品 921009707333)", "space-100073070/c2s_0126.bin", + "0019000000000005f6fe6e0efba8e4237e9e62000000d67074d945", Uplink.BuildMFcn(MFcnTargetId, 921009707333L), + ), + UplinkCase( + "M_.fcn(物品 920009631039)", "space-100073070/c2s_0129.bin", + "0019000000000005f6fe6e0efba8e4237e9e62000000d634d8e53f", Uplink.BuildMFcn(MFcnTargetId, 920009631039L), + ), + UplinkCase( + "M_.fcn(物品 920009631018)", "space-100073070/c2s_0141.bin", + "0019000000000005f6fe6e0efba8e4237e9e62000000d634d8e52a", Uplink.BuildMFcn(MFcnTargetId, 920009631018L), + ), ) // FileBytes = node 侧读到的抓包整帧(浏览器侧传 null)