220 lines
15 KiB
C#
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
|
|
""";
|
|
}
|