205 lines
11 KiB
C#
205 lines
11 KiB
C#
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
|
||
""";
|
||
}
|