Updated to latest Penumbra.GameData, updated ObjectManager

This commit is contained in:
RisaDev
2024-02-03 03:07:50 +03:00
parent 24aaa30e9c
commit dc7fb73d84
16 changed files with 313 additions and 246 deletions

View File

@@ -1,5 +1,7 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using PenumbraExtensions = Penumbra.GameData.Actors.ActorIdentifierExtensions;
namespace CustomizePlus.GameData.Extensions;
@@ -19,10 +21,10 @@ public static class ActorIdentifierExtensions
if (identifier.Type != IdentifierType.Owned)
return identifier.ToName();
if (ActorIdentifier.Manager == null)
if (PenumbraExtensions.Manager == null)
throw new Exception("ActorIdentifier.Manager is not initialized");
return ActorIdentifier.Manager.Data.ToName(identifier.Kind, identifier.DataId);
return PenumbraExtensions.Manager.Data.ToName(identifier.Kind, identifier.DataId);
}
/// <summary>
@@ -93,7 +95,7 @@ public static class ActorIdentifierExtensions
if (identifier.Type != IdentifierType.Special)
return ActorIdentifier.Invalid;
if (ActorIdentifier.Manager == null)
if (PenumbraExtensions.Manager == null)
throw new Exception("ActorIdentifier.Manager is not initialized");
switch (identifier.Special)
@@ -103,12 +105,12 @@ public static class ActorIdentifierExtensions
case ScreenActor.FittingRoom:
case ScreenActor.DyePreview:
case ScreenActor.Portrait:
return ActorIdentifier.Manager.GetCurrentPlayer();
return PenumbraExtensions.Manager.GetCurrentPlayer();
case ScreenActor.ExamineScreen:
var examineIdentifier = ActorIdentifier.Manager.GetInspectPlayer();
var examineIdentifier = PenumbraExtensions.Manager.GetInspectPlayer();
if (!examineIdentifier.IsValid)
examineIdentifier = ActorIdentifier.Manager.GetGlamourPlayer(); //returns ActorIdentifier.Invalid if player is invalid
examineIdentifier = PenumbraExtensions.Manager.GetGlamourPlayer(); //returns ActorIdentifier.Invalid if player is invalid
if (!examineIdentifier.IsValid)
return ActorIdentifier.Invalid;
@@ -117,7 +119,7 @@ public static class ActorIdentifierExtensions
case ScreenActor.Card6:
case ScreenActor.Card7:
case ScreenActor.Card8:
return ActorIdentifier.Manager.GetCardPlayer();
return PenumbraExtensions.Manager.GetCardPlayer();
}
return ActorIdentifier.Invalid;

View File

@@ -0,0 +1,48 @@
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData;
namespace CustomizePlus.GameData.Hooks.Objects;
public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, CharacterDestructor.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.CutsceneService"/>
CutsceneService = 0,
/// <seealso cref="PathResolving.IdentifiedCollectionCache"/>
IdentifiedCollectionCache = 0,
}
public CharacterDestructor(HookManager hooks)
: base("Character Destructor")
=> _task = hooks.CreateHook<Delegate>(Name, Sigs.CharacterDestructor, Detour, true);
private readonly Task<Hook<Delegate>> _task;
public nint Address
=> _task.Result.Address;
public void Enable()
=> _task.Result.Enable();
public void Disable()
=> _task.Result.Disable();
public Task Awaiter
=> _task;
public bool Finished
=> _task.IsCompletedSuccessfully;
private delegate void Delegate(Character* character);
private void Detour(Character* character)
{
//Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)character:X}.");
Invoke(character);
_task.Result.Original(character);
}
}

View File

@@ -0,0 +1,46 @@
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using OtterGui.Classes;
using OtterGui.Services;
namespace CustomizePlus.GameData.Hooks.Objects;
public sealed unsafe class CopyCharacter : EventWrapperPtr<Character, Character, CopyCharacter.Priority>, IHookService
{
public enum Priority
{
/// <seealso cref="PathResolving.CutsceneService"/>
CutsceneService = 0,
}
public CopyCharacter(HookManager hooks)
: base("Copy Character")
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
private readonly Task<Hook<Delegate>> _task;
public nint Address
=> (nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter;
public void Enable()
=> _task.Result.Enable();
public void Disable()
=> _task.Result.Disable();
public Task Awaiter
=> _task;
public bool Finished
=> _task.IsCompletedSuccessfully;
private delegate ulong Delegate(CharacterSetup* target, Character* source, uint unk);
private ulong Detour(CharacterSetup* target, Character* source, uint unk)
{
// TODO: update when CS updated.
var character = ((Character**)target)[1];
//Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}.");
Invoke(character, source);
return _task.Result.Original(target, source, unk);
}
}

View File

@@ -1,23 +1,23 @@
using Dalamud.Plugin.Services;
using CustomizePlus.GameData.Hooks.Objects;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Penumbra.GameData.Actors;
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.String;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.GameData.Services;
public class CutsceneService : IDisposable
public class CutsceneService : IService, IDisposable
{
public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart;
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
private readonly GameEventManager _events;
private readonly IObjectTable _objects;
private readonly CopyCharacter _copyCharacter;
private readonly CharacterDestructor _characterDestructor;
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
public IEnumerable<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> Actors
@@ -25,14 +25,19 @@ public class CutsceneService : IDisposable
.Where(i => _objects[i] != null)
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
public unsafe CutsceneService(IObjectTable objects, GameEventManager events)
public unsafe CutsceneService(IObjectTable objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor,
IClientState clientState)
{
_objects = objects;
_events = events;
_events.CopyCharacter += OnCharacterCopy;
_events.CharacterDestructor += OnCharacterDestructor;
_copyCharacter = copyCharacter;
_characterDestructor = characterDestructor;
_copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService);
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService);
if (clientState.IsGPosing)
RecoverGPoseActors();
}
/// <summary>
/// Get the related actor to a cutscene actor.
/// Does not check for valid input index.
@@ -50,6 +55,27 @@ public class CutsceneService : IDisposable
/// <summary> Return the currently set index of a parent or -1 if none is set or the index is invalid. </summary>
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.GetObjectAddress(copyIdx) == nint.Zero)
return false;
if (parentIdx != -1 && _objects.GetObjectAddress(parentIdx) == nint.Zero)
return false;
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
return true;
}
public short GetParentIndex(ushort idx)
{
if (idx is >= CutsceneStartIdx and < CutsceneEndIdx)
return _copiedCharacters[idx - CutsceneStartIdx];
@@ -59,17 +85,34 @@ public class CutsceneService : IDisposable
public unsafe void Dispose()
{
_events.CopyCharacter -= OnCharacterCopy;
_events.CharacterDestructor -= OnCharacterDestructor;
_copyCharacter.Unsubscribe(OnCharacterCopy);
_characterDestructor.Unsubscribe(OnCharacterDestructor);
}
private unsafe void OnCharacterDestructor(Character* character)
{
if (character->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx)
return;
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 = (GameObject*)_objects.GetObjectAddress(i + CutsceneStartIdx);
if (address != null && address->GetObjectKind() is (byte)ObjectKind.Pc)
((Character*)address)->HomeWorld = character->HomeWorld;
var idx = character->GameObject.ObjectIndex - CutsceneStartIdx;
_copiedCharacters[idx] = -1;
_copiedCharacters[i] = -1;
}
}
}
else if (character->GameObject.ObjectIndex < CutsceneEndIdx)
{
var idx = character->GameObject.ObjectIndex - CutsceneStartIdx;
_copiedCharacters[idx] = -1;
}
}
private unsafe void OnCharacterCopy(Character* target, Character* source)
@@ -80,4 +123,45 @@ public class CutsceneService : IDisposable
var idx = target->GameObject.ObjectIndex - CutsceneStartIdx;
_copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1);
}
/// <summary> Try to recover GPose actors on reloads into a running game. </summary>
/// <remarks> This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. </remarks>
private unsafe void RecoverGPoseActors()
{
Dictionary<ByteString, short>? 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 = (GameObject*)_objects.GetObjectAddress(idx);
if (address == null)
return false;
name = new ByteString(address->Name);
return !name.IsEmpty;
}
Dictionary<ByteString, short> CreateActors()
{
var ret = new Dictionary<ByteString, short>();
for (short i = 0; i < CutsceneStartIdx; ++i)
{
if (TryGetName(i, out var name))
ret.TryAdd(name, i);
}
return ret;
}
}
}

View File

@@ -4,6 +4,7 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -18,18 +19,18 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
private readonly IFramework _framework;
private readonly IClientState _clientState;
private readonly IObjectTable _objects;
private readonly ActorService _actors;
private readonly ActorManager _actorManager;
private readonly ITargetManager _targets;
public IObjectTable Objects
=> _objects;
public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorService actors, ITargetManager targets)
public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorManager actorManager, ITargetManager targets)
{
_framework = framework;
_clientState = clientState;
_objects = objects;
_actors = actors;
_actorManager = actorManager;
_targets = targets;
}
@@ -60,23 +61,26 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (character.Identifier(_actors.AwaitedService, out var identifier))
if (character.Identifier(_actorManager, out var identifier))
HandleIdentifier(identifier, character);
}
for (var i = (int)ScreenActor.CutsceneStart; i < (int)ScreenActor.CutsceneEnd; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (!character.Valid)
// Technically the game does not create holes in cutscenes or GPose.
// But for Brio compatibility, we allow holes in GPose.
// Since GPose always has the event actor in the first cutscene slot, we can still optimize in this case.
if (!character.Valid && i == (int)ScreenActor.CutsceneStart)
break;
HandleIdentifier(character.GetIdentifier(_actors.AwaitedService), character);
HandleIdentifier(character.GetIdentifier(_actorManager), character);
}
void AddSpecial(ScreenActor idx, string label)
{
Actor actor = _objects.GetObjectAddress((int)idx);
if (actor.Identifier(_actors.AwaitedService, out var ident))
if (actor.Identifier(_actorManager, out var ident))
{
var data = new ActorData(actor, label);
_identifiers.Add(ident, data);
@@ -95,7 +99,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (character.Identifier(_actors.AwaitedService, out var identifier))
if (character.Identifier(_actorManager, out var identifier))
HandleIdentifier(identifier, character);
}
@@ -120,7 +124,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
if (identifier.Type is IdentifierType.Player or IdentifierType.Owned)
{
var allWorld = _actors.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue,
var allWorld = _actorManager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue,
identifier.Kind,
identifier.DataId);
@@ -137,7 +141,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
if (identifier.Type is IdentifierType.Owned)
{
var nonOwned = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId);
var nonOwned = _actorManager.CreateNpc(identifier.Kind, identifier.DataId);
if (!_nonOwnedIdentifiers.TryGetValue(nonOwned, out var nonOwnedData))
{
nonOwnedData = new ActorData(character, nonOwned.ToString());
@@ -170,7 +174,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
get
{
Update();
return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
return Player.Identifier(_actorManager, out var ident) && _identifiers.TryGetValue(ident, out var data)
? (ident, data)
: (ident, ActorData.Invalid);
}
@@ -181,7 +185,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
get
{
Update();
return Target.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
return Target.Identifier(_actorManager, out var ident) && _identifiers.TryGetValue(ident, out var data)
? (ident, data)
: (ident, ActorData.Invalid);
}

View File

@@ -1,78 +0,0 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Penumbra.GameData;
using Penumbra.GameData.Actors;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.GameData.Services;
public abstract class AsyncServiceWrapper<T> : IDisposable
{
public string Name { get; }
public T? Service { get; private set; }
public T AwaitedService
{
get
{
_task?.Wait();
return Service!;
}
}
public bool Valid
=> Service != null && !_isDisposed;
public event Action? FinishedCreation;
private Task? _task;
private bool _isDisposed;
protected AsyncServiceWrapper(string name, Func<T> factory)
{
Name = name;
_task = Task.Run(() =>
{
var service = factory();
if (_isDisposed)
{
if (service is IDisposable d)
d.Dispose();
}
else
{
Service = service;
_task = null;
}
});
_task.ContinueWith((t, x) =>
{
if (!_isDisposed)
FinishedCreation?.Invoke();
}, null);
}
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_task = null;
if (Service is IDisposable d)
d.Dispose();
}
}
public sealed class ActorService : AsyncServiceWrapper<ActorManager>
{
public ActorService(DalamudPluginInterface pi, IObjectTable objects, IClientState clientState, IFramework framework, IGameInteropProvider interop, IDataManager gameData,
IGameGui gui, CutsceneService cutsceneService, IPluginLog log)
: base(nameof(ActorService),
() => new ActorManager(pi, objects, clientState, framework, interop, gameData, gui, idx => (short)cutsceneService.GetParentIndex(idx), log))
{ }
}