package routers import ( "context" "sync" "time" ) var _ = sync.Mutex{} var _ = context.Background var _ = time.Now // // 可接管召唤物(android-control 一家子): // 黑客 APP 蜘蛛机器人(51938181)/ 送货无人机(51938183)→ 40120908 / 40120909 // 巡卫官 APP 机械狗 NCCA Dog(51938184 召唤 / 51938186 驻场态)→ 40120911 // 三条是同一套通道,不是三个功能。2026-09-25 补狗时的取证: // · AgentSummonCatalog 里 40120908 蜘蛛 / 40120909 无人机 / 40120911 机械狗 紧挨着, // EnemyClassType=1、GroupEnemy=40211001、AI=40213004 三个字段完全相同 // (狗多一个 InitWeaponId 98006028);EnemyCatalog.json:82 再确认一次。 // · SummonConfig 只有 5 条:1 无人机 / 2 蜘蛛 / **3 机器狗** / 4 鸵鸟马桶 / 5 扫地机器人 // ⇒ 狗是官方召唤物,SyncSwitchControl 的 SummonTag 校验对它成立(硬规则 52)。 // · 客户端有完整的 RobotDogControlsStore.lua(召回/切换操控/普攻/蹲/闪避/拍照…), // 它调的正是 AskStopControlAgent(true/false) —— 与蜘蛛/无人机逐字相同。 // · 51938184 / 51938186 挂在里希(15021021)的 FightSpiritConfig.TempSkill 上, // 所以 GameRouter.Combat.cs 的 AllowsAuthoredCharacterSkill 已经放行, // 技能包到得了这里(Combat.cs 里那句 TryHandleControllableSummonSkill)。 // 为什么以前点了没反应 —— 2026-09-23 抓包确认。 // 客户端其实一直在上报。packets 日志里这两行进得很清楚: // // C2S DECODED AskClientUseCommonSkill type=AskUseSkill // {"data":{"Releaser":100000000012,"Location":{...},"SkillId":51938181,"SkillInstanceId":-211}} // [COMBAT] accept temp-skill template=15021023 skill=51938181 inst=-211 // [COMBAT] use unit=100000000012 weapon=200141 style=39000005 skill=51938181 target=0 // // 也就是说:C2S 到了,服务端也认了(走 AllowsAuthoredCharacterSkill 分支),然后——什么 // 都没做。 OnClientUseSkill 只更新了 combat 沙盘记账就返回了,世界上一个实体也没多出来。 // 客户端那边则是全套自备的(lua.zip): // HackScriptFunc.lua SpiderSkill = function() gCS.BattleManager.UseSkillByPid(pid, 51938181) end // SkillConfig.json 51938181 召唤蜘蛛机器人 / 51938183 召唤送货无人机,ClientActionAdvance=true // 所以玩家看到的"放置动作"是客户端先行演出;真正的单位、操控面板、操控本身,都要服务端 // 把单位放进世界之后客户端才成立。 // 分工(用户 2026-09-23 确认):召唤出来之后由玩家接管操控,这套逻辑在客户端。 // 服务端只做三件事: // 1. 把实体刷出来(不参与战斗 —— 见 engage:false) // 2. 推一次 SyncSwitchControl(68552741) 把玩家放进操控位 // 3. 接住 AskStopControlAgent(67046103) / AskControlAgent(67291468) 这一对「交还 / 重新接管」 // 之后 DoHackEnemy → ANDROID_CONTROL_SWITCH → RobotSpiderControlStore / DeliveryDroneStore // 全在客户端,我们不插手。 // ⛔⛔ 2026-09-23 18:02 实机抓到的头号坑:spiritId 不能拿 Profile.InitialUnitId。 // 第一版用 state.WorldEntryControlUnit(= 100000000000 = player.initialUnitId)当 // spiritId,结果是单位出来了、客户端也收到了两个包,但操控没交出去 —— 表现为「机器人站着 // 不动,我还在控角色,而且手机打不开」。客户端日志里是这一行铁证: // // [error] SyncHackEnemy enterOrLeave True unit or master is null // enemyId= 35184372188839 enemy is nullFalse // spiritId= 100000000000 master is nullTrue // LX6.Units.Module.BaseUnitModuleUtils:DoHackEnemy(UInt64, UInt64, Boolean) // // enemy is nullFalse = 单位找得到;master is nullTrue = 按 spiritId 找不到施法者。 // 玩家真正的 spirit uid 是 100000000012,三处互相印证: // · AskUseSkill 的 Releaser(客户端自己报的施法者) // · 我们下发的 SyncPlayerCurrentSpirit {"playerPid":100,"spiritId":100000000012} // · 客户端 AskUnitMoveActionSimple 上报自己移动时带的那个 UnitId // 而 100000000000 只是 config/private-server.json 的 player.initialUnitId, // 是进世界时的占位值,切过角色之后就不再是当前 spirit。 // ⇒ spiritId 一律优先用 req.Releaser。 // 手机打不开也是同一个 bug 的副作用,不是新问题:客户端 SyncSwitchControl 在 DoHackEnemy // 之后会执行 OnSetSkillBtnState(PhoneCall, "isSummonAgentControl", true) 把手机按钮禁掉 // —— 它以为自己已经在操控召唤物了,而 DoHackEnemy 静默失败,于是「手机锁了但没接管」。 // func GameRouter_HackerSummon_ControllableSummonAgentId(ctx context.Context, skillId uint32) any { // Router implementation for GameRouter_HackerSummon_ControllableSummonAgentId var zero any; return zero } // 召唤物的展示名,只用于日志。 func GameRouter_HackerSummon_ControllableSummonName(ctx context.Context, agentId uint32) string { // Router implementation for GameRouter_HackerSummon_ControllableSummonName return "" } // // SyncAgentWeaponWheel(68121167) 的载荷。抽成方法是为了让 _ux_wire_check 能反射 // 直接断言生产代码产出的字节,而不是在测试里重写一遍。 // 客户端 reader(RPCDeserializeAuto.lua:171397): // // local agentId = reader.ReadUInt64(reader) // local currentWeaponId = reader.ReadUInt32(reader) // local weaponSlots = Base.ReadList7Bit(reader, function(r) return r.ReadUInt32(r) end) // // ReadList7Bit = 标记字节 + Int7(count) + count 个 u32。 // ⛑ 标记字节按项目硬规则 5 用 0xFF(= 有值);UxWriter.Int7(n) 内部写 n+1,别再加偏置。 // func GameRouter_HackerSummon_BuildWeaponWheelWire(ctx context.Context, uid uint64, weaponWheelSceneItemId uint32) []byte { // Router implementation for GameRouter_HackerSummon_BuildWeaponWheelWire return nil } // // 召唤物的武器轮盘 SceneItem id。0 = 这个模板没有轮盘,不要发这一帧。 // ⛑⛑ 2026-09-26 订正(原注释写错了,按它办的 ⇒ 无人机一直没有 E 技能): // 原注释拿 AgentSummonCatalog.InitWeaponId 当判据,结论是「只有机械狗有武器, // 蜘蛛和无人机 InitWeaponId 都是 0 ⇒ 不用发轮盘」。这个判据是错的 —— 轮盘的 // 命名空间是 SceneItem(AgentConfig.InitWeaponSceneItemId),而 HUD 上"有没有 // 技能键"只看 AgentWeaponManager:GetAgentCurrentWeapon 拿不拿得到值,跟单位 // 实际绑了哪把武器无关。 // 客户端的判据(RobotFlyerControlsStore.lua,无人机操控 HUD): // // M.RefreshBattleSkillInfo = function(self, weaponId) // if weaponId then btnHitch:SetActive(true); btnDrop:SetActive(true) // else btnHitch:SetActive(false); btnDrop:SetActive(false) end // local cfg = gAgentWeaponManager:GetAgentWeaponInfoCfg(weaponId) // if cfg then btnHitch:SetTipNameTotally(cfg.Normal) -- 挂钩 = 普通攻击 // btnDrop:SetTipNameTotally(cfg.Basic) end -- 投放 = E 技能 // // 即:无人机 HUD 上没有 btnSkillPc(那是机械狗/蜘蛛的),它的"原生 E 技能" // 就是 btnDrop(长按走 SkillBtnType.Basic),配套还有 btnHitch。 // 不发轮盘 ⇒ 这两个键直接 SetActive(false) ⇒ 玩家看到的就是"没有 E 技能"。 // 这是硬规则 173 的同一条链路,只是消费方从 RobotDogControlsStore 换成了 // RobotFlyerControlsStore。 // 蜘蛛 40120908 暂未纳入:它的 HUD(RobotSpiderControlStore)同样按 weaponId 控制 // btnSkillPc,映射也已知(80005973「蜘蛛机器人默认武器」→ 98006056,FixedFightSkill // 39010099),但本次只改无人机这一处,蜘蛛留作后续。 // func GameRouter_HackerSummon_ControllableSummonWeaponWheel(ctx context.Context, agentId uint32) uint32 { // Router implementation for GameRouter_HackerSummon_ControllableSummonWeaponWheel return 0 } // // 每个家族一个独立回滚开关(黑客 / 巡卫犬),别共用 —— 见 // PrivateServerConfig.PoliceDogSummonEnabled 的注释。 // func GameRouter_HackerSummon_ControllableSummonEnabled(ctx context.Context, skillId uint32, switchName string) bool { // Router implementation for GameRouter_HackerSummon_ControllableSummonEnabled return true } // ================================================================================= // ① 施放召唤技能 // ================================================================================= // // 接住一次可接管召唤技能(蜘蛛 / 无人机 / 机械狗)。 // 返回 false 表示这不是召唤技能,调用方应继续走普通战斗流程。 // func GameRouter_HackerSummon_TryHandleControllableSummonSkill(ctx context.Context, rpcCtx *RpcContext, req any) bool { // Router implementation for GameRouter_HackerSummon_TryHandleControllableSummonSkill return true } // ================================================================================= // ② 交还操控 / 回收(长按 F)与重新接管(N 键) // ================================================================================= // // C2S AskStopControlAgent(67046103) — clearAgent=true 是长按 F 回收(连单位一起销毁), // false 是 N 键交还操控(单位留在场上,之后还能用 AskControlAgent 切回去)。 // 客户端用 gReliableRpcManager:RegisterRPC 等回执,所以 无论解析成功与否都必须先 // 回一个 empty OK,否则操控面板的 await 永远不落地(按钮按了没反应)。 // func GameRouter_HackerSummon_AskStopControlAgent(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_HackerSummon_AskStopControlAgent var zero any; return zero } // C2S AskControlAgent(67291468) — 重新接管一个已在场的召唤物(N 键的另一半)。 // // ⛔⛔ 2026-09-23 18:36 实机抓到的第二个坑:包种类是 NOTIFY,不是 INVOKE。 // 第一版按 HandlerPacketKind.Invoke 注册,结果 packets 日志里全是这一行: // // C2S NOTIFY AskControlAgent [0x0402C94C] body=9b // ! [RPC4229938] unimplemented notify AskControlAgent [0x0402C94C] -> accepted // // 也就是说 N 键切回去的包到了服务端,但因为 kind 不匹配被视为未实现的 notify 直接 // accept 掉,handler 一次都没进(控制台里一条 [SUMMON] 都没有)。表现就是"按 N 没反应"。 // 对照:交还操控那条 AskStopControlAgent(67046103) 是 C2S INVOKE(要回执), // 所以两个必须分开注册 —— 这里用 Both 兜住,将来客户端改成 invoke 也不用改代码。 // func GameRouter_HackerSummon_AskControlAgent(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_HackerSummon_AskControlAgent var zero any; return zero } func GameRouter_HackerSummon_OnStopControlAgent(ctx context.Context, rpcCtx *RpcContext, clearAgent bool) any { // Router implementation for GameRouter_HackerSummon_OnStopControlAgent var zero any; return zero } func GameRouter_HackerSummon_OnControlAgent(ctx context.Context, rpcCtx *RpcContext, agentEntityId uint64, reason byte) any { // Router implementation for GameRouter_HackerSummon_OnControlAgent var zero any; return zero } // ================================================================================= // ③ 机械狗「聚焦模式」—— ⛔ 2026-09-26 05:4x 已删除(实机失败,用户要求撤掉)。 // 原设计:控制狗时按住鼠标左键 ⇒ 准星对准谁就盘查谁。实测不work, // 盘查入口改走 GM 面板「盘查模式」+ 里希按 R(见 GameRouter.PoliceExamAuto.cs)。 // 回滚记录:日志 2026-09-26。 // ================================================================================= // // 交出操控位的固定两件套。顺序是硬要求:SyncControllableAgent 先(不查 ClientData), // SyncSwitchControl 后(它第一件事就是 GetUnit + ClientData.AgentId,单位没建号就 print_error)。 // func GameRouter_HackerSummon_HandOverSummonControl(ctx context.Context, rpcCtx *RpcContext, summon any, reason byte, why string) any { // Router implementation for GameRouter_HackerSummon_HandOverSummonControl var zero any; return zero } // 一次召唤的运行时快照。只存交接操控需要的三个 id。 type ControllableSummonState struct { SpiritId uint64 `json:"spiritId"` AgentEntityId uint64 `json:"agentEntityId"` AgentId uint32 `json:"agentId"` // 服务端视角:操控位此刻是否在召唤物手上(true)还是已交还玩家(false)。 Controlling bool `json:"controlling"` // 上次下发"接管"的时刻,用于给连发的 AskControlAgent 去抖。 LastHandoverUtc time.Time `json:"lastHandoverUtc"` }