package routers import ( "context" "sync" ) var _ = sync.Mutex{} var _ = context.Background // // 厕所 QTE / 马桶鸵鸟骑乘(2026-09-30 新增)。 // ★ 玩法本体:ToiletOstrichGamePanelStore.lua —— 骑上马桶鸵鸟后弹一个音游式三键 QTE // (QTE_DURATION=2 / PERFECT_WINDOW=0.1 / GREAT_WINDOW=0.2,满分 100), // 失败时鸵鸟逃跑。面板与判定全在客户端,服务端要做的是「让骑乘坐实」+「接住逃跑上报」。 // ★ 触发链(三条证据实锤,见 GameMethods.AskRideAgent4229938 的契约注释) // // 面板由 C#→lua 回调打开:CsToLuaHandler.lua 的 // module.OnStartRideQTE(riderPid, beRidenPid) ⇒ // gPanelManager:CheckShow(gPanelId.TOILET_OSTRICH_GAME_PANEL, { npcPid = beRidenPid })。 // 也就是说 C# 认定「骑乘成立」时才 fire,lua 只是被动开面板。 // 面板里 SceneDataMgr.GetUnit(self.npcPid) ⇒ 被骑的那只必须是世界里真实存在的 unit。 // 失败分支会主动上报 AskToiletOstrichFlee;成功分支只发本地信号 5753。 // // ★ 为什么不需要新的生成通道 // 马桶鸵鸟(40122028 / 40652007 / 40922011 / 42046401 / 45000446,规范 id // AgentConfig.ToiletOstrich = 42046401)在 NpcCatalog.json 里是标准 Agent // (GeneralModelId=86963029、EnemyClassType=1、faction 18000300「通用怪物」), // 而且 AgentSummonCatalog.json 里本来就有它 ⇒ 直接用既有的服务端直生成通道 // (GameAdminBridge.SpawnAgentAsync)就能出,客户端 ai 状态树 // (MassAI.Npc.MassAetherAI.Tasks.TryOnRideAgent / TryDownRideAgent,走 // BehaviorActionConfigId 40214321 + AISetting 40213146)自带骑乘能力,服务端不用给标记。 // 面板入口见 GameAdminBridge.AgentSummonCatalog(它被置顶到「生成敌人」下拉框第一位)。 // ★ 一次只动一个(实测顺序,按这个走) // // 生成 42046401 → 走到旁边按骑乘。 // 面板出来了 ⇒ 本文件的默认配置已经够用,接下来看 // [TOILET] 日志里 flee 出现时鸵鸟是否正确消失。 // 面板不出来 ⇒ 把 gameplay.toiletQte.pushRideState 打开(推 // SyncRideAgent(rider, ridee, true)),这与自动驾驶那条「服务端推状态」是同一个模式; // ⛑ 注意 GameRouter.VehicleNav.cs §43.1 的包序教训:状态包要排在动作包之前, // 所以这里也是先推状态、再回 ack。 // // ⛑ 回滚:把本文件涉及的三个 mid 从 GameRouter.cs 的 // Enabled4229938MethodIds 里删掉即可(handler 不再注册 = 回到 // ReturnEmptyOkAsync / DefaultUnknownNotify 兜底,不在场时行为与今天之前一致); // 或者一个开关 gameplay.toiletQte.enabled=false 只关「回头推 + 回收」,不动注册。 // // 模板是不是马桶鸵鸟。GameAdminBridge.SpawnAgentAsync 用它决定「生成成坐骑还是生成成敌人」。 func GameRouter_ToiletQte_IsToiletOstrichAgent(ctx context.Context, agentId uint32) bool { // Router implementation for GameRouter_ToiletQte_IsToiletOstrichAgent return true } // 当前正骑着哪只(session 级)。下马/逃跑时要靠它知道该撤哪个 uid 的状态。 func GameRouter_ToiletQte_GetRidingMount(ctx context.Context, session *GameSession) uint64 { // Router implementation for GameRouter_ToiletQte_GetRidingMount return 0 } func GameRouter_ToiletQte_SetRidingMount(ctx context.Context, session *GameSession, uid uint64) any { // Router implementation for GameRouter_ToiletQte_SetRidingMount var zero any; return zero } // // 建立/解除完整骑乘状态(② + ③④)。顺序照抄捐赠端:SyncRideAgent 先,345/346 后。 // ⛑⛑ 必须在 ReturnEmptyOkAsync **之前**发完 —— §43.1 的包序教训。 // func GameRouter_ToiletQte_PublishToiletRideAsync(ctx context.Context, conn any, rider uint64, mount uint64, enter bool) any { // Router implementation for GameRouter_ToiletQte_PublishToiletRideAsync var zero any; return zero } // // uid → 模板 id,**只为马桶鸵鸟**记一条。 // ⛑ 为什么不直接查敌人账本:坐骑模式是 engage=false(不下令开战、不登记进 // LocalEnemyRuntimeState.Enemies),账本里根本没有它;而 AskRideAgent / // AskToiletOstrichFlee 收到的载荷只有 unit uid,没有模板 id ⇒ 日志里 // isOstrich=? 就再也判不出来了。 // 会话销毁即失效(存在 session.Items 里),本来也是一次性的东西。 // func GameRouter_ToiletQte_ToiletMounts(ctx context.Context, session *GameSession) map[string]any { // Router implementation for GameRouter_ToiletQte_ToiletMounts return nil } // 面板生成马桶鸵鸟时登记一条(由 GameAdminBridge.SpawnAgentAsync 调用)。 func GameRouter_ToiletQte_NoteToiletMountSpawned(ctx context.Context, session *GameSession, uid uint64, agentId uint32) any { // Router implementation for GameRouter_ToiletQte_NoteToiletMountSpawned var zero any; return zero } // // uid 是不是本服自己生成的(GameAdminBridge.ServerSpawnUidBase 起的池子)。 // ⛑⛑ 2026-09-30 实测事故:AskToiletOstrichFlee 送来过 uid=19000, // 而 19000 是场景自带的真实敌人([LENEMY] ... uid=19000 serverSpawned=False hp=1500, // 在 -4739,169,-2828),当时代码无条件回收 ⇒ 把一只无辜的场景敌人删了。 // 根因是 AskRideAgent 我们一律回 0 字节成功,客户端于是对任何可按 F 的单位 // 都 fire OnStartRideQTE,失败后又对那个单位发 flee。 // ⇒ 回收的硬门槛改成「只回收本服生成的单位」:客户端这条 Notify 只在马桶 QTE // 失败时发,而我们的马桶一定是我们自己 spawn 的,两条判据交集足够;场景单位落在池子外, // 天然被排除。ServerSpawnUidBase = 0x200000000000(= 35184372088832)远高于任何 // 场景单位 uid。 // func GameRouter_ToiletQte_IsServerSpawnedUid(ctx context.Context, uid uint64) bool { // Router implementation for GameRouter_ToiletQte_IsServerSpawnedUid return true } // ======================================================================================== // AskRideAgent(67575069) —— C2S Invoke,载荷 ulong agentId,**回包必须是 0 字节** // ======================================================================================== func GameRouter_ToiletQte_AskRideAgent4229938Handler(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_ToiletQte_AskRideAgent4229938Handler var zero any; return zero } // ======================================================================================== // AskStopRideAgent(67449345) —— C2S Invoke,**无参**,回包也是 0 字节 // ======================================================================================== func GameRouter_ToiletQte_AskStopRideAgent4229938Handler(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_ToiletQte_AskStopRideAgent4229938Handler var zero any; return zero } // ======================================================================================== // AskToiletOstrichFlee(67154315) —— C2S **Notify**,载荷 ulong agentid(QTE 失败时客户端上报) // ======================================================================================== func GameRouter_ToiletQte_AskToiletOstrichFlee4229938Handler(ctx context.Context, conn any, msg any) any { // Router implementation for GameRouter_ToiletQte_AskToiletOstrichFlee4229938Handler var zero any; return zero } // ------------------------------------------------------------------ helpers // // 当前「骑的人」是谁 —— 也就是 SyncRideAgent 的 rider。 // 出场单位优先(WorldEntryControlUnit),没有就退回当前操控体 // (ActiveSpiritUnitId,切人后它会跟着变)。拿不到返回 0(日志里就是 0,此时不推状态包)。 // func GameRouter_ToiletQte_CurrentRiderUnit(ctx context.Context, session *GameSession) uint64 { // Router implementation for GameRouter_ToiletQte_CurrentRiderUnit return 0 } // // 读出 AskRideAgent 里的单位 id。 // ⛑ AskStopRideAgent 没有载荷,别拿它来调这个方法。 // 解析失败不能抛:这是客户端**上报**类请求,服务端炸了会把整帧链路带断(规则 168 的同族问题)。 // func GameRouter_ToiletQte_TryReadRideTarget(ctx context.Context, msg any) uint64 { // Router implementation for GameRouter_ToiletQte_TryReadRideTarget return 0 } // // 读出 AskToiletOstrichFlee 里的单位 id。 // ⛑ 两条载荷在线上字节相同(都是单个 u64),但**各用各的契约** —— 互相借会在将来某一方 // 加字段时静默解析错位,而这里正好是「客户端上报」路径,错了只会静默丢事件。 // func GameRouter_ToiletQte_TryReadFleeTarget(ctx context.Context, msg any) uint64 { // Router implementation for GameRouter_ToiletQte_TryReadFleeTarget return 0 } // // uid → 模板 id。先查马桶鸵鸟登记表(坐骑模式不 engage ⇒ 不进敌人账本),再查敌人账本, // 都没有返回 0(日志里打成 ?,那是正常的,不代表解析失败)。 // func GameRouter_ToiletQte_LookupEnemyTemplate(ctx context.Context, session *GameSession, uid uint64) uint32 { // Router implementation for GameRouter_ToiletQte_LookupEnemyTemplate return 0 }