Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.PoliceOfficerBoardingPosition.cs
T

131 lines
8.3 KiB
C#

using Ananta.SDK.Network;
using Ananta.SDK.Serialization;
using Ananta.Server.Protocol.Client4229938;
namespace Ananta.Server.Handlers.Game;
internal sealed partial class GameRouter
{
// Hold the officer's initial visual X/Z during the door animation, as
// requested by the user. Do NOT move her to police point43/44. The native
// controller and passenger bindings remain untouched.
private static async Task PoliceOfficerBoardingPosition4229938(TcpSession session,
PoliceRuntime4229938 runtime, ulong owner, bool restore = false)
{
var passenger = runtime.PendingBoarding;
if (!restore)
{
if (owner == 0 || passenger == null || passenger.Seat is not (2 or 3)
|| !runtime.Npcs.TryGetValue(runtime.Agent, out var npc) || npc.Template != 4065053) return;
var vehicles = GetVehicleRuntimeState(session);
lock (vehicles.SyncRoot)
if (!vehicles.Vehicles.TryGetValue(passenger.Vehicle, out var car) || car.ConfigId != 81004027) return;
}
if (!session.Items.TryGetValue(PoliceGateSessionKey4229938, out var gateValue)
|| gateValue is not TcpSession gate || gate.ClosedToken.IsCancellationRequested) return;
ulong vehicleId = passenger?.Vehicle ?? 0;
var script = $$"""
local ok, err = pcall(function()
local prefix = '[LIXI-BOARD] officer-inplace-v2 '
_G.__lixiOfficerBoardPosition = _G.__lixiOfficerBoardPosition or {}
local key = '{{runtime.Agent}}'
local saved = _G.__lixiOfficerBoardPosition[key]
if {{(restore ? "true" : "false")}} then
if saved then saved.stop() end
return
end
if saved then return end
local get, call = ReflectionGetProperty, ReflectionCallMethod
if type(get) ~= 'function' or type(call) ~= 'function' then return end
local id = ulong.new({{(uint)owner}}, {{owner >> 32}})
local vid = ulong.new({{(uint)vehicleId}}, {{vehicleId >> 32}})
local player = gCS.SceneDataMgr.GetUnit(id)
local car = LX6.Drive.DriveManager.Instance:GetBaseVehicle(vid)
if not player or not player.MeshRoot or not car then return end
local moduleType = 'LX6.Drive.VehicleInteractModule'
local actionType = 'LX6.Drive.Interact.VehicleInteractAction'
local officerType = 'LX6.Drive.Interact.PlayerEscortToVehicleAction'
local module = get('LX6.Drive.BaseVehicle','interactModule',car)
local action = module and call(moduleType,'GetUnitCurrentAction',module,id)
if not action then return end
local kind = get(actionType,'interactType',action)
if kind ~= 10 and tostring(kind) ~= 'PoliceEscortToVehicle' then return end
if call(officerType,'GetInteractSeat',action) ~= {{passenger?.Seat ?? 0}} then return end
local root = player.MeshRoot
local parent, pos = root.parent, root.localPosition
if not parent then return end
local original = UnityEngine.Vector3.New(pos.x,pos.y,pos.z)
local initial = parent:TransformPoint(original)
local anchorX, anchorY, anchorZ = initial.x,initial.y,initial.z
local hook = 'lixi-officer-board-position-' .. key
local s = {started=UnityEngine.Time.realtimeSinceStartup, stopped=false,applied=false}
s.stop = function(reason)
if s.stopped then return end
s.stopped = true
if s.hook == 'camera' then pcall(function() gCameraUtils.RemoveCameraUpdateEvent(hook) end) end
if s.timer then pcall(function() s.timer:Stop() end) end
if s.applied then pcall(function() root.localPosition=original end) end
if _G.__lixiOfficerBoardPosition[key] == s then _G.__lixiOfficerBoardPosition[key]=nil end
if reason then print_error(prefix .. 'cleanup: ' .. reason .. ' applied=' .. tostring(s.applied)) end
end
s.tick = function()
if s.stopped then return end
local good, failure = pcall(function()
local now = UnityEngine.Time.realtimeSinceStartup
if now-s.started >= 15 or gCS.SceneDataMgr.GetUnit(id) ~= player
or player.MeshRoot ~= root or root.parent ~= parent
or LX6.Drive.DriveManager.Instance:GetBaseVehicle(vid) ~= car then s.stop('timeout/unit changed'); return end
-- Never leak into walking, driving or the successful alighting action.
if call(moduleType,'GetUnitCurrentAction',module,id) ~= action
or call(officerType,'GetInteractSeat',action) ~= {{passenger?.Seat ?? 0}} then s.stop('action/seat changed'); return end
local phase = call(actionType,'GetActionState',action)
local pending = phase == 1 or tostring(phase) == 'Pending'
local running = phase == 2 or tostring(phase) == 'Running'
if not pending and not running then s.stop('phase=' .. tostring(phase)); return end
-- Install before the boarding reaction, including Pending:
-- the previous Running-only gate never reached 'applied' in
-- the 00:49:54 test. Keep the original start location instead
-- of chasing a configured door point or smoothing toward it.
local base = parent:TransformPoint(original)
local dx,dz = anchorX-base.x,anchorZ-base.z
if dx ~= dx or dz ~= dz or dx*dx+dz*dz > 2.25
or base.y ~= base.y or math.abs(anchorY-base.y) > 1 then s.stop('position safety limit'); return end
-- Only cancel horizontal root drift. Preserve native height,
-- body rotation and the skeletal door-opening animation.
local corrected = parent:InverseTransformPoint(UnityEngine.Vector3.New(anchorX,base.y,anchorZ))
root.localPosition = corrected
if not s.applied then
print_error(prefix .. 'applied seat={{passenger?.Seat ?? 0}} phase=' .. tostring(phase)
.. ' anchorXZ=' .. tostring(anchorX) .. ',' .. tostring(anchorZ))
end
s.applied=true
end)
if not good then s.stop(); print_error(prefix .. 'stopped: ' .. tostring(failure)) end
end
_G.__lixiOfficerBoardPosition[key]=s
local installed=false
if gCameraUtils and gCameraUtils.AddCameraUpdateEvent and gCameraUtils.RemoveCameraUpdateEvent then
s.hook='camera'
installed=pcall(function() gCameraUtils.AddCameraUpdateEvent(hook,s.tick) end)
if not installed then pcall(function() gCameraUtils.RemoveCameraUpdateEvent(hook) end);s.hook=nil end
end
if not installed and Timer and Timer.New then
installed=pcall(function() s.timer=Timer.New(s.tick,0.02,-1,true);s.timer:Start() end)
if installed then s.hook='timer' end
end
if not installed then s.stop(); error('no camera/timer hook') end
print_error(prefix .. 'installed seat={{passenger?.Seat ?? 0}} hook=' .. s.hook)
s.tick()
end)
if not ok then print_error('[LIXI-BOARD] officer-inplace-v2 failed: ' .. tostring(err)) end
""";
try
{
await gate.NotifyAsync(MethodId.SyncDoToLuaMaster,
new UxWriter().UxString(script).ToArray(), session.ClosedToken);
}
catch (OperationCanceledException) when (session.ClosedToken.IsCancellationRequested) { throw; }
catch (Exception ex) { session.Log.Warn($"[LIXI-BOARD] officer position unavailable: {ex.GetType().Name}"); }
}
}