using Ananta.SDK.Network; namespace Ananta.Server.Handlers.Game; /// /// 马桶坐骑的原生操控桥:让骑乘状态下的「加速 / 跳跃」按键真的送到坐骑身上 /// (2026-10-02 自 7k-v0.6 捐赠端 GameRouter.ToiletControls.cs 逐字搬运)。 /// /// ★ 为什么需要这一段 Lua:客户端 GameplayMountPanelStore 本来就会把 /// JumpPress/Release、DashPress/Release 通过 /// UnitAttachModule.CheckAndSend3CEventToRideTarget(RVA 0x11CFBCE0)转给坐骑, /// 但这条路被骑手自身的战斗 / 跑酷输入闸门挡住了(骑着马桶时按键被判定成人物动作)。 /// 本桥直接把同一批 3C 事件送到已确认的挂载目标,绕过那道闸门。 /// /// ★ 挂的钩子(原生 HUD 回调,不是硬编码轮询按键): /// C_GameplayMountPanelStore.OnRushBtnPress/Release(加速)、 /// OnJumpBtnPress/Release(跳跃)、 /// C_CoreHudCharacterControlStore.OnJumpSwingBtnPress/Release、 /// gBattleMgr.HighSpeedDown/Up。 /// /// ⛑⛑ 边界(捐赠端原注释,别自行夸大): /// "Animation, speed and jump physics remain native; event delivery is NOT proof /// that an incomplete client animal graph implements every transition." /// ⇒ 这段只保证事件送达;坐骑动画图里缺的过渡(比如马桶鸵鸟根本没有冲刺状态)送到了也不会动。 /// /// ⛑ 安全设计(照抄,别改): /// /// 只在 PlayerIsRiding() 且 GetAttachUnitID(pid) == mountUid 时才转发 /// —— 必须是真正的原生挂载,不是旁边站着一只沙盒 NPC。 /// 0.1s 定时器 + Application.isFocused 检查 ⇒ 下马 / QTE / 重生 / 失焦后 /// 一定会补一条 Release,不会把加速或跳跃卡在按下状态。 /// QTE 面板 TOILET_OSTRICH_GAME_PANEL 显示期间不转发,避免和音游判定抢按键。 /// mode='clear' 会反注册全部钩子并清状态,不留残留。 /// /// /// ★ 触发点:GameRouter.ToiletQte.PublishToiletRideAsync —— /// 上马(enter=true)下发 arm,下马 / 逃跑(enter=false)下发 clear。 /// 开关 gameplay.toiletQte.nativeControls(默认 true)。 /// /// ⛑ 回滚:① 配置里 nativeControls=false(最快,不下发 Lua); /// ② 删本文件 + 撤 PublishToiletRideAsync 里那一次调用(备份 /// GameRouter.ToiletQte.cs.bak-20261002-toiletcontrols)。 /// 客户端文件一个都没动,Lua 走的是已有 GATE 通道。 /// internal sealed partial class GameRouter { /// /// 下发的模式:arm = 装钩子并武装;clear = 卸载并释放所有按键。 /// 脚本里只判断 mode == 'clear',其余任何值都按武装处理。 /// internal const string ToiletControlsArm = "arm"; internal const string ToiletControlsClear = "clear"; /// /// 下发 / 撤销坐骑操控桥。 /// /// 游戏会话(GATE 会话从它身上取,取不到就只记日志,不抛)。 /// 骑手 unit uid(脚本里的 owner,用来确认「是本人在操作」)。 /// 坐骑 unit uid(脚本里的 npc,事件转发目标)。 /// 或 。 internal static async Task PublishToiletControls4229938(TcpSession session, ulong rider, ulong mount, string mode) { if (rider == 0 || mount == 0) return; var script = ToiletControlsScript4229938 .Replace("__OWNER_LOW__", ((uint)rider).ToString(System.Globalization.CultureInfo.InvariantCulture)) .Replace("__OWNER_HIGH__", (rider >> 32).ToString(System.Globalization.CultureInfo.InvariantCulture)) .Replace("__NPC_LOW__", ((uint)mount).ToString(System.Globalization.CultureInfo.InvariantCulture)) .Replace("__NPC_HIGH__", (mount >> 32).ToString(System.Globalization.CultureInfo.InvariantCulture)) .Replace("__MODE__", mode); var (ok, message) = await RunClientLuaAsync(session, script); session.Log.Info($"[TOILET-CTRL] mode={mode} rider={rider} mount={mount} sent={ok} {message}"); } // Server-delivered Lua, scoped to the exact sandbox mount and rider. Client files // are untouched. Use existing rebound HUD callbacks, not hard-coded key polling. // Native GameplayMountPanelStore forwards JumpPress/Release and DashPress/Release // through UnitAttachModule.CheckAndSend3CEventToRideTarget (RVA 0x11CFBCE0). // This compatibility bridge delivers those same events directly to the confirmed // attachment target, bypassing the rider's unrelated combat/parkour input gates. // Animation, speed and jump physics remain native; event delivery is NOT proof // that an incomplete client animal graph implements every transition. internal static string ToiletControlsScript4229938 => """ local ok,err=pcall(function() local mode='__MODE__' local owner=ulong.new(__OWNER_LOW__,__OWNER_HIGH__) local npc=ulong.new(__NPC_LOW__,__NPC_HIGH__) local function same(a,b) return a~=nil and b~=nil and ulong.equals(a,b) end local function now() return UnityEngine.Time.realtimeSinceStartup end local s=_G.__drmkToiletControls if not s then s={hooks={},held={}};_G.__drmkToiletControls=s end local function stopTimer(name) if s[name] then s[name]:Stop();s[name]=nil end end local function release() for key,h in pairs(s.held) do -- Never leave sprint/jump latched after dismount, QTE, respawn or focus loss. pcall(function() local u=gCS.SceneDataMgr.GetUnit(h.npc) if u then gCS.LogicStateMachineManager.Send3CEvent(u,h.up) end end) s.held[key]=nil end stopTimer('releaseTimer') end if mode=='clear' then if same(s.owner,owner) and same(s.npc,npc) then release();stopTimer('installTimer');s.active=false for _,h in ipairs(s.hooks) do if h.target[h.name]==h.wrapper then h.target[h.name]=h.original end end s.hooks={} end return end release();stopTimer('installTimer') s.owner=owner;s.npc=npc;s.active=true local function target() if not s.active then return nil end local player=gCS.MyPlayerManager.PlayerUnit if not player or not same(player.Pid,s.owner) then return nil end -- Must be the actual native attachment, not merely a nearby sandbox NPC. if not gCS.BaseUnitUtils.PlayerIsRiding() or not same(gCS.BaseUnitUtils.GetAttachUnitID(player.Pid),s.npc) then return nil end if gPanelManager:IsPanelShowing(gPanelId.TOILET_OSTRICH_GAME_PANEL) then return nil end return gCS.SceneDataMgr.GetUnit(s.npc) end local function log(message) print_error_without_stack('[TOILET-CONTROL] '..message) end s.dispatch=function(key,down) local good,u=pcall(target) if not good or not u then release();return false end local cfg=LTConfig.ABPCCCEventConfig local press=key=='rush' and cfg.DashPress or cfg.JumpPress local up=key=='rush' and cfg.DashRelease or cfg.JumpRelease if not press or not up then return false end local held=s.held[key] if down and held then return true end -- keyboard repeat / overlapping HUD callbacks if not down and not held then return true end gCS.LogicStateMachineManager.Send3CEvent(u,down and press or up) s.held[key]=down and {npc=s.npc,up=up} or nil log(key..(down and ' press' or ' release')..' event='..tostring(down and press or up)..' target='..tostring(s.npc)) if down and not s.releaseTimer then s.releaseTimer=Timer.New(function() local valid,mount=pcall(target) if not valid or not mount or UnityEngine.Application.isFocused==false then release() end end,0.1,-1,true) s.releaseTimer:Start() elseif not next(s.held) then stopTimer('releaseTimer') end return true end local function methods(c) if c and type(c.GetVtbl)=='function' then return c.GetVtbl() end return c end local function hook(t,name,key,down) if not t or type(t[name])~='function' then return false end for _,h in ipairs(s.hooks) do if h.target==t and h.name==name then return t[name]==h.wrapper end end local original=t[name] local wrapper=function(self,...) local good,handled=pcall(s.dispatch,key,down) if good and handled then return end if not good then release();log('input failed: '..tostring(handled)) end return original(self,...) end t[name]=wrapper s.hooks[#s.hooks+1]={target=t,name=name,original=original,wrapper=wrapper} return true end local function install() local mount=methods(C_GameplayMountPanelStore) local core=methods(C_CoreHudCharacterControlStore) local done=true for _,b in ipairs({ {mount,'OnRushBtnPress','rush',true},{mount,'OnRushBtnRelease','rush',false}, {mount,'OnJumpBtnPress','jump',true},{mount,'OnJumpBtnRelease','jump',false}, {core,'OnJumpSwingBtnPress','jump',true},{core,'OnJumpSwingBtnRelease','jump',false}, {gBattleMgr,'HighSpeedDown','rush',true},{gBattleMgr,'HighSpeedUp','rush',false} }) do if not hook(b[1],b[2],b[3],b[4]) then done=false end end return done end if not install() then local deadline=now()+15 s.installTimer=Timer.New(function() local good,done=pcall(install) if not s.active or not good or done or now()>=deadline then stopTimer('installTimer') log(good and done and 'native HUD bindings ready' or 'some HUD bindings unavailable; inspect input logs') end end,0.2,-1,true) s.installTimer:Start() end log('scoped native controls armed; waiting for mounted input') end) if not ok then print_error('[TOILET-CONTROL] '..tostring(err)) end """; }