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