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

220 lines
15 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
{
// Runs only after a validated EscortToWard request, BEFORE reaction 30001.
// Never call DoBindUnit, ForceReset, teleport, or rebuild the director graph:
// those would discard the working paired movement and native end callback.
private static async Task PreparePolicePrisonAnimation4229938(TcpSession session, ulong owner, ulong npc, string token)
{
if (!session.Items.TryGetValue(PoliceGateSessionKey4229938, out var item)
|| item is not TcpSession gate || gate.ClosedToken.IsCancellationRequested) return;
var endBytes = new UxWriter().U8(255).Int7(1).U8(4).U32(PoliceRootNid4229938)
.UxString("PolicePrisonTimelineEnd").U8(42).UxString(token).I64(0).Bool(false).ToArray();
var script = PolicePrisonAnimationScript4229938
.Replace("__END_BYTES__", string.Join(',', endBytes))
.Replace("__OWNER_LOW__", ((uint)owner).ToString(System.Globalization.CultureInfo.InvariantCulture))
.Replace("__OWNER_HIGH__", (owner >> 32).ToString(System.Globalization.CultureInfo.InvariantCulture))
.Replace("__NPC_LOW__", ((uint)npc).ToString(System.Globalization.CultureInfo.InvariantCulture))
.Replace("__NPC_HIGH__", (npc >> 32).ToString(System.Globalization.CultureInfo.InvariantCulture));
try
{
await gate.NotifyAsync(MethodId.SyncDoToLuaMaster,
new UxWriter().UxString(script).ToArray(), session.ClosedToken);
session.Log.Info($"[LIXI-PRISON-ANIM] scoped actor binding recovery sent owner={owner} npc={npc}");
}
catch (OperationCanceledException) when (session.ClosedToken.IsCancellationRequested) { throw; }
catch (Exception ex) { session.Log.Warn($"[LIXI-PRISON-ANIM] install unavailable: {ex.GetType().Name}"); }
}
internal static string PolicePrisonAnimationScript4229938 => """
local ok, err = pcall(function()
local prefix='[LIXI-PRISON-ANIM] '
local get,field,call,set=ReflectionGetProperty,ReflectionGetField,ReflectionCallMethod,ReflectionSetProperty
if not get or not field or not call or not set or not Timer or not Timer.New then
error('reflection/timer unavailable')
end
if _G.__drmkWardActors then _G.__drmkWardActors.stop('replaced') end
local ownerId=ulong.new(__OWNER_LOW__,__OWNER_HIGH__)
local npcId=ulong.new(__NPC_LOW__,__NPC_HIGH__)
local owner=gCS.SceneDataMgr.GetUnit(ownerId)
local npc=gCS.SceneDataMgr.GetUnit(npcId)
if not owner or not npc then error('escort actors unavailable') end
local mgr=L50.L50App.L50Game.CutsceneManager
local names={'play_police_arrest_lf','play_police_arrest_lm','play_police_arrest_mf',
'play_police_arrest_mm','play_police_arrest_sf'}
local T='LX6.TimelineScript.Timeline'
local A='LX6.TimelineScript.IActor'
local D='UnityEngine.Playables.PlayableDirector'
local Tr='UnityEngine.Timeline.TrackAsset'
local R='LX6.TimelineScript.TimelineDefine+TimelineTrackRef'
-- Exists in C#, but is NOT exported by CutsceneManager's Lua wrapper.
local function playing(name)
return call('LX6.TimelineScript.CutsceneManager','GetPlayingTimelineByName',mgr,name)
end
local function count(list) return get('System.Collections.ICollection','Count',list) end
local function item(list,index) return call('System.Collections.IList','get_Item',list,index) end
local s={started=UnityEngine.Time.realtimeSinceStartup,stopped=false,undo={},seen={},reported={},skip={}}
s.stop=function(reason)
if s.stopped then return end
s.stopped=true
if s.timer then pcall(function() s.timer:Stop() end) end
-- Restore only values this request actually changed, including shared track mute.
for i=#s.undo,1,-1 do pcall(s.undo[i]) end
if _G.__drmkWardActors==s then _G.__drmkWardActors=nil end
print_error_without_stack(prefix..'cleanup '..tostring(reason))
end
for _,name in ipairs(names) do
local existing=playing(name)
if existing then s.skip[existing]=true end
end
local function repair(tl)
local actors=field(T,'iActors',tl)
local director=field(T,'director',tl)
local asset=field(T,'timelineAsset',tl)
if not actors or not director or not asset then return end
for i=0,count(actors)-1 do
local actor=item(actors,i)
local name=tostring(field(A,'actorName',actor) or '')
local real=field(A,'realUnit',actor)
local officer=real==owner or (not real and name:find('main_A104003_gm_actor',1,true)==1)
local prisoner=real==npc or (not real and name:find('fwnpc_A401058_p021_gm_actor',1,true)==1)
if officer or prisoner then
-- Never repair an actor explicitly attached to someone else.
if not real or real==(officer and owner or npc) then
local animator=field(A,'animAnimator',actor)
local refs=field(A,'refTracks',actor)
local state=get(A,'loadState_Anim',actor)
local report=tostring(state)..':'..tostring(animator~=nil)
if s.reported[actor]~=report then
s.reported[actor]=report
print_error_without_stack(prefix..'actor='..name..' animLoad='..tostring(state)
..' animator='..tostring(animator~=nil)..' streaming='..tostring(field(A,'streamingAnim',actor)))
end
-- Do not re-enter LoadAnim: its native Loaded branch increments a
-- completion counter. Repeated loading would corrupt the lifecycle.
if animator and refs and (state==2 or tostring(state)=='Loaded') then
for j=0,count(refs)-1 do
local ref=item(refs,j)
local kind=field(R,'TrackType',ref)
local index=field(R,'TrackIndex',ref)
-- Skeleton only. RootAnimation, RootMotion, camera and door
-- tracks are intentionally excluded, as are rejected body variants.
if (kind==1 or tostring(kind)=='Animation') and field(R,'canUse',ref)==true then
local track=call('UnityEngine.Timeline.TimelineAsset','GetOutputTrack',asset,index)
if track and not s.seen[track] then
local ty=call('System.Object','GetType',track)
if get('System.Type','FullName',ty)=='UnityEngine.Timeline.AnimationTrack' then
local binding=call(D,'GetGenericBinding',director,track)
local muted=get(Tr,'muted',track)
-- Loaded is a lifecycle state, not proof every clip was
-- resolved. Log payloads without restarting native loading.
local inspected,details=pcall(function()
local clips=field(Tr,'m_Clips',track)
local total,ready,split=0,0,0
if clips then
total=count(clips)
for k=0,total-1 do
local clipAsset=get('UnityEngine.Timeline.TimelineClip','asset',item(clips,k))
if clipAsset then
local clipType=call('System.Object','GetType',clipAsset)
if get('System.Type','FullName',clipType)=='UnityEngine.Timeline.AnimationPlayableAsset' then
if field('UnityEngine.Timeline.AnimationPlayableAsset','m_Clip',clipAsset) then ready=ready+1 end
if field('UnityEngine.Timeline.AnimationPlayableAsset','m_Split',clipAsset) then split=split+1 end
end
end
end
end
return ' clips='..total..' directClips='..ready..' splitClips='..split
end)
print_error_without_stack(prefix..'track actor='..name..' index='..tostring(index)
..' binding='..tostring(binding~=nil)..' muted='..tostring(muted)
..(inspected and details or ' clips=unknown'))
if binding==nil then
call(D,'SetGenericBinding',director,track,animator)
table.insert(s.undo,function()
if call(D,'GetGenericBinding',director,track)==animator then
call(D,'ClearGenericBinding',director,track)
end
end)
end
if muted then
-- TrackUtils.SetMute also changes child tracks. Only
-- this body's eligible track may be changed here.
set(Tr,'muted',track,false)
table.insert(s.undo,function()
if get(Tr,'muted',track)==false then set(Tr,'muted',track,true) end
end)
end
if binding==nil or muted then
-- Keeps timeline time and root movement; unlike RebuildGraph.
call(D,'RebindPlayableGraphOutputs',director)
print_error_without_stack(prefix..'recovered actor='..name..' track='..tostring(index)
..' missingBinding='..tostring(binding==nil)..' muted='..tostring(muted))
end
s.seen[track]=true
end
end
end
end
end
end
end
end
end
s.tick=function()
if s.stopped then return end
local good,why=pcall(function()
if UnityEngine.Time.realtimeSinceStartup-s.started>60
or gCS.SceneDataMgr.GetUnit(ownerId)~=owner or gCS.SceneDataMgr.GetUnit(npcId)~=npc then
s.stop('timeout/actors changed');return
end
local active=nil
for _,name in ipairs(names) do
local tl=playing(name)
if tl and not s.skip[tl] then active=tl;break end
end
if s.timeline and active~=s.timeline then
-- InteractEnd is sent at the beginning of the cutscene in this
-- client. Wait for the actual timeline to disappear and allow
-- its native unit restore/OnTimelineEnd to finish first.
s.endedAt=s.endedAt or UnityEngine.Time.realtimeSinceStartup
if UnityEngine.Time.realtimeSinceStartup-s.endedAt<0.3 then return end
s.stop('timeline ended')
local bytes={__END_BYTES__}
gClientToGameSceneDelegate:Notify(67130745,function(writer)
for i=1,#bytes do writer:WriteByte(bytes[i]) end
end)
return
end
if active then
s.endedAt=nil;s.timeline=active
-- A skeleton diagnosis failure must not lose the independent
-- timeline-end handshake and strand custody indefinitely.
if not s.repairFailed then
local repaired,reason=pcall(repair,active)
if not repaired then
s.repairFailed=true
print_error(prefix..'skeletal repair failed: '..tostring(reason))
end
end
end
end)
if not good then s.stop('error');print_error(prefix..tostring(why)) end
end
_G.__drmkWardActors=s
s.timer=Timer.New(s.tick,0.1,-1,true)
s.timer:Start()
print_error_without_stack(prefix..'armed for native paired ward timeline')
end)
if not ok then
if _G.__drmkWardActors then _G.__drmkWardActors.stop('install failed') end
print_error('[LIXI-PRISON-ANIM] install failed: '..tostring(err))
end
""";
}