using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.Hooks.Objects; using Penumbra.String; namespace Penumbra.Interop.PathResolving; public sealed class CutsceneService : Luna.IRequiredService, IDisposable { public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart; public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; private readonly ObjectManager _objects; private readonly CopyCharacter _copyCharacter; private readonly CharacterDestructor _characterDestructor; private readonly ConstructCutsceneCharacter _constructCutsceneCharacter; private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) .Where(i => _objects[i].Valid) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!)); public CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor, ConstructCutsceneCharacter constructCutsceneCharacter, IClientState clientState) { _objects = objects; _copyCharacter = copyCharacter; _characterDestructor = characterDestructor; _constructCutsceneCharacter = constructCutsceneCharacter; _copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService); _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService); _constructCutsceneCharacter.Subscribe(OnSetupPlayerNpc, ConstructCutsceneCharacter.Priority.CutsceneService); if (clientState.IsGPosing) RecoverGPoseActors(); } /// /// Get the related actor to a cutscene actor. /// Does not check for valid input index. /// Returns null if no connected actor is set or the actor does not exist anymore. /// private IGameObject? this[int idx] { get { Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx); idx = _copiedCharacters[idx - CutsceneStartIdx]; return idx < 0 ? null : _objects.GetDalamudObject(idx); } } /// Return the currently set index of a parent or -1 if none is set or the index is invalid. public int GetParentIndex(int idx) => GetParentIndex((ushort)idx); public bool SetParentIndex(int copyIdx, int parentIdx) { if (copyIdx is < CutsceneStartIdx or >= CutsceneEndIdx) return false; if (parentIdx is < -1 or >= CutsceneEndIdx) return false; if (!_objects[copyIdx].Valid) return false; if (parentIdx != -1 && !_objects[parentIdx].Valid) return false; _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; _objects.InvokeRequiredUpdates(); return true; } public short GetParentIndex(ushort idx) { if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) return _copiedCharacters[idx - CutsceneStartIdx]; return -1; } public void Dispose() { _copyCharacter.Unsubscribe(OnCharacterCopy); _characterDestructor.Unsubscribe(OnCharacterDestructor); _constructCutsceneCharacter.Unsubscribe(OnSetupPlayerNpc); } private unsafe void OnCharacterDestructor(in CharacterDestructor.Arguments arguments) { var character = arguments.Character.AsCharacter; if (character->GameObject.ObjectIndex < CutsceneStartIdx) { // Remove all associations for now non-existing actor. for (var i = 0; i < _copiedCharacters.Length; ++i) { if (_copiedCharacters[i] == character->GameObject.ObjectIndex) { // A hack to deal with GPose actors leaving and thus losing the link, we just set the home world instead. // I do not think this breaks anything? var address = _objects[i + CutsceneStartIdx]; if (address.IsPlayer) address.AsCharacter->HomeWorld = character->HomeWorld; _copiedCharacters[i] = -1; } } } else if (character->GameObject.ObjectIndex < CutsceneEndIdx) { var idx = character->GameObject.ObjectIndex - CutsceneStartIdx; _copiedCharacters[idx] = -1; } } private void OnCharacterCopy(in CopyCharacter.Arguments arguments) { if (!arguments.TargetCharacter.Valid || arguments.TargetCharacter.Index.Index is < CutsceneStartIdx or >= CutsceneEndIdx) return; var idx = arguments.TargetCharacter.Index.Index - CutsceneStartIdx; _copiedCharacters[idx] = (short)(arguments.SourceCharacter.Valid ? arguments.SourceCharacter.Index : -1); } private void OnSetupPlayerNpc(in ConstructCutsceneCharacter.Arguments arguments) { if (!arguments.Character.Valid || arguments.Character.Index.Index is < CutsceneStartIdx or >= CutsceneEndIdx) return; var idx = arguments.Character.Index.Index - CutsceneStartIdx; _copiedCharacters[idx] = 0; } /// Try to recover GPose actors on reloads into a running game. /// This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. private void RecoverGPoseActors() { Dictionary? actors = null; for (var i = CutsceneStartIdx; i < CutsceneEndIdx; ++i) { if (!TryGetName(i, out var name)) continue; if ((actors ??= CreateActors()).TryGetValue(name, out var idx)) _copiedCharacters[i - CutsceneStartIdx] = idx; } return; bool TryGetName(int idx, out ByteString name) { name = ByteString.Empty; var address = _objects[idx]; if (!address.Valid) return false; name = address.Utf8Name; return !name.IsEmpty; } Dictionary CreateActors() { var ret = new Dictionary(); for (short i = 0; i < CutsceneStartIdx; ++i) { if (TryGetName(i, out var name)) ret.TryAdd(name, i); } return ret; } } }