Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.ToiletControls.cs

205 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Ananta.SDK.Network;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// 马桶坐骑的<b>原生操控桥</b>:让骑乘状态下的「加速 / 跳跃」按键真的送到坐骑身上
/// (2026-10-02 自 7k-v0.6 捐赠端 <c>GameRouter.ToiletControls.cs</c> 逐字搬运)。
///
/// <para>★ 为什么需要这一段 Lua:客户端 <c>GameplayMountPanelStore</c> 本来就会把
/// JumpPress/Release、DashPress/Release 通过
/// <c>UnitAttachModule.CheckAndSend3CEventToRideTarget</c>(RVA 0x11CFBCE0)转给坐骑,
/// 但这条路被<b>骑手自身的战斗 / 跑酷输入闸门</b>挡住了(骑着马桶时按键被判定成人物动作)。
/// 本桥直接把同一批 3C 事件送到<b>已确认的挂载目标</b>,绕过那道闸门。</para>
///
/// <para>★ 挂的钩子(原生 HUD 回调,不是硬编码轮询按键):
/// <c>C_GameplayMountPanelStore.OnRushBtnPress/Release</c>(加速)、
/// <c>OnJumpBtnPress/Release</c>(跳跃)、
/// <c>C_CoreHudCharacterControlStore.OnJumpSwingBtnPress/Release</c>、
/// <c>gBattleMgr.HighSpeedDown/Up</c>。</para>
///
/// <para>⛑⛑ 边界(捐赠端原注释,别自行夸大):
/// <c>"Animation, speed and jump physics remain native; event delivery is NOT proof
/// that an incomplete client animal graph implements every transition."</c>
/// ⇒ 这段只保证<b>事件送达</b>;坐骑动画图里缺的过渡(比如马桶鸵鸟根本没有冲刺状态)送到了也不会动。</para>
///
/// <para>⛑ 安全设计(照抄,别改):</para>
/// <list type="bullet">
/// <item>只在 <c>PlayerIsRiding()</c> 且 <c>GetAttachUnitID(pid) == mountUid</c> 时才转发
/// —— 必须是真正的原生挂载,不是旁边站着一只沙盒 NPC。</item>
/// <item>0.1s 定时器 + <c>Application.isFocused</c> 检查 ⇒ 下马 / QTE / 重生 / 失焦后
/// 一定会补一条 Release,不会把加速或跳跃卡在按下状态。</item>
/// <item>QTE 面板 <c>TOILET_OSTRICH_GAME_PANEL</c> 显示期间不转发,避免和音游判定抢按键。</item>
/// <item><c>mode='clear'</c> 会反注册全部钩子并清状态,不留残留。</item>
/// </list>
///
/// <para>★ 触发点:<c>GameRouter.ToiletQte.PublishToiletRideAsync</c> ——
/// 上马(<c>enter=true</c>)下发 <c>arm</c>,下马 / 逃跑(<c>enter=false</c>)下发 <c>clear</c>。
/// 开关 <c>gameplay.toiletQte.nativeControls</c>(默认 true)。</para>
///
/// <para>⛑ 回滚:① 配置里 <c>nativeControls=false</c>(最快,不下发 Lua);
/// ② 删本文件 + 撤 <c>PublishToiletRideAsync</c> 里那一次调用(备份
/// <c>GameRouter.ToiletQte.cs.bak-20261002-toiletcontrols</c>)。
/// 客户端文件<b>一个都没动</b>,Lua 走的是已有 GATE 通道。</para>
/// </summary>
internal sealed partial class GameRouter
{
/// <summary>
/// 下发的模式:<c>arm</c> = 装钩子并武装;<c>clear</c> = 卸载并释放所有按键。
/// 脚本里只判断 <c>mode == 'clear'</c>,其余任何值都按武装处理。
/// </summary>
internal const string ToiletControlsArm = "arm";
internal const string ToiletControlsClear = "clear";
/// <summary>
/// 下发 / 撤销坐骑操控桥。
/// </summary>
/// <param name="session">游戏会话(GATE 会话从它身上取,取不到就只记日志,不抛)。</param>
/// <param name="rider">骑手 unit uid(脚本里的 owner,用来确认「是本人在操作」)。</param>
/// <param name="mount">坐骑 unit uid(脚本里的 npc,事件转发目标)。</param>
/// <param name="mode"><see cref="ToiletControlsArm"/> 或 <see cref="ToiletControlsClear"/>。</param>
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
""";
}