feat: M7 点装备即落库,重启服务端后仍回显(闭合「能切换但没持久化」)
客户端: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 未处理。
This commit is contained in:
1 parent
8964ea28cd
commit
ab1a905d28
9 files changed
+202
-6
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -69,6 +69,7 @@ let Sem = null; // semantic.js 实例(组帧要用;index.js 会把自己那
|
||||
const Cache = {
|
||||
Devices: null, // { Slots: Set<slot>, BySlot: Map<slot, string[]>, Mounted: Map<slot, string|null> }
|
||||
Flags: new Map(), // SpaceId|Kind → string[]
|
||||
MountedFlags: null, // Items.Mounted=1 的 ItemId 列表(按 ItemId 升序);null = 还没读到库
|
||||
Order: new Map(), // SpaceId → Map<ObjectId, 首次出现序号>
|
||||
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) {
|
||||
|
||||
@@ -68,7 +68,16 @@ private val App = FC<Props> {
|
||||
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} 件)" }
|
||||
|
||||
@@ -100,6 +100,13 @@ internal class ClientState {
|
||||
var MountedItems: List<String> = 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 收敛、后到者覆盖;同时保留收到过的记录条数以便追溯。
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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<UplinkCase> = 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)
|
||||
|
||||
Reference in new issue
Block a user