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
}