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"`
}