Code commit
This commit is contained in:
85
CustomizePlus.GameData/.editorconfig
Normal file
85
CustomizePlus.GameData/.editorconfig
Normal file
@@ -0,0 +1,85 @@
|
||||
[*.{cs,vb}]
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
[*.cs]
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = file_scoped:error
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_namespace_match_folder = true:error
|
||||
48
CustomizePlus.GameData/CustomizePlus.GameData.csproj
Normal file
48
CustomizePlus.GameData/CustomizePlus.GameData.csproj
Normal file
@@ -0,0 +1,48 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\submodules\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Dalamud">
|
||||
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGui.NET">
|
||||
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGuiScene">
|
||||
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
116
CustomizePlus.GameData/Data/Actor.cs
Normal file
116
CustomizePlus.GameData/Data/Actor.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using Penumbra.GameData.Actors;
|
||||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace CustomizePlus.GameData.Data;
|
||||
|
||||
public readonly unsafe struct Actor : IEquatable<Actor>
|
||||
{
|
||||
private Actor(nint address)
|
||||
=> Address = address;
|
||||
|
||||
public static readonly Actor Null = new(nint.Zero);
|
||||
|
||||
public readonly nint Address;
|
||||
|
||||
public GameObject* AsObject
|
||||
=> (GameObject*)Address;
|
||||
|
||||
public Character* AsCharacter
|
||||
=> (Character*)Address;
|
||||
|
||||
public bool Valid
|
||||
=> Address != nint.Zero;
|
||||
|
||||
public bool IsCharacter
|
||||
=> Valid && AsObject->IsCharacter();
|
||||
|
||||
public static implicit operator Actor(nint? pointer)
|
||||
=> new(pointer ?? nint.Zero);
|
||||
|
||||
public static implicit operator Actor(GameObject* pointer)
|
||||
=> new((nint)pointer);
|
||||
|
||||
public static implicit operator Actor(Character* pointer)
|
||||
=> new((nint)pointer);
|
||||
|
||||
public static implicit operator nint(Actor actor)
|
||||
=> actor.Address;
|
||||
|
||||
public bool IsGPoseOrCutscene
|
||||
=> Index.Index is >= (int)ScreenActor.CutsceneStart and < (int)ScreenActor.CutsceneEnd;
|
||||
|
||||
public ActorIdentifier GetIdentifier(ActorManager actors)
|
||||
=> actors.FromObject(AsObject, out _, true, true, false);
|
||||
|
||||
public ByteString Utf8Name
|
||||
=> Valid ? new ByteString(AsObject->Name) : ByteString.Empty;
|
||||
|
||||
public bool Identifier(ActorManager actors, out ActorIdentifier ident)
|
||||
{
|
||||
if (Valid)
|
||||
{
|
||||
ident = GetIdentifier(actors);
|
||||
return ident.IsValid;
|
||||
}
|
||||
|
||||
ident = ActorIdentifier.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
public ObjectIndex Index
|
||||
=> Valid ? AsObject->ObjectIndex : ObjectIndex.AnyIndex;
|
||||
|
||||
public Model Model
|
||||
=> Valid ? AsObject->DrawObject : null;
|
||||
|
||||
public byte Job
|
||||
=> IsCharacter ? AsCharacter->CharacterData.ClassJob : (byte)0;
|
||||
|
||||
public static implicit operator bool(Actor actor)
|
||||
=> actor.Address != nint.Zero;
|
||||
|
||||
public static bool operator true(Actor actor)
|
||||
=> actor.Address != nint.Zero;
|
||||
|
||||
public static bool operator false(Actor actor)
|
||||
=> actor.Address == nint.Zero;
|
||||
|
||||
public static bool operator !(Actor actor)
|
||||
=> actor.Address == nint.Zero;
|
||||
|
||||
public bool Equals(Actor other)
|
||||
=> Address == other.Address;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is Actor other && Equals(other);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Address.GetHashCode();
|
||||
|
||||
public static bool operator ==(Actor lhs, Actor rhs)
|
||||
=> lhs.Address == rhs.Address;
|
||||
|
||||
public static bool operator !=(Actor lhs, Actor rhs)
|
||||
=> lhs.Address != rhs.Address;
|
||||
/*
|
||||
/// <summary> Only valid for characters. </summary>
|
||||
public CharacterArmor GetArmor(EquipSlot slot)
|
||||
=> ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()];
|
||||
|
||||
public CharacterWeapon GetMainhand()
|
||||
=> new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).ModelId.Value);
|
||||
|
||||
public CharacterWeapon GetOffhand()
|
||||
=> new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).ModelId.Value);
|
||||
|
||||
public Customize GetCustomize()
|
||||
=> *(Customize*)&AsCharacter->DrawData.CustomizeData;
|
||||
*/
|
||||
public override string ToString()
|
||||
=> $"0x{Address:X}";
|
||||
}
|
||||
44
CustomizePlus.GameData/Data/ActorData.cs
Normal file
44
CustomizePlus.GameData/Data/ActorData.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
namespace CustomizePlus.GameData.Data;
|
||||
|
||||
/// <summary>
|
||||
/// A single actor with its label and the list of associated game objects.
|
||||
/// </summary>
|
||||
public readonly struct ActorData
|
||||
{
|
||||
public readonly List<Actor> Objects;
|
||||
public readonly string Label;
|
||||
|
||||
public bool Valid
|
||||
=> Objects.Count > 0;
|
||||
|
||||
public ActorData(Actor actor, string label)
|
||||
{
|
||||
Objects = new List<Actor> { actor };
|
||||
Label = label;
|
||||
}
|
||||
|
||||
public static readonly ActorData Invalid = new(false);
|
||||
|
||||
private ActorData(bool _)
|
||||
{
|
||||
Objects = new List<Actor>(0);
|
||||
Label = string.Empty;
|
||||
}
|
||||
|
||||
/*public LazyString ToLazyString(string invalid)
|
||||
{
|
||||
var objects = Objects;
|
||||
return Valid
|
||||
? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString())))
|
||||
: new LazyString(() => invalid);
|
||||
}*/
|
||||
|
||||
private ActorData(List<Actor> objects, string label)
|
||||
{
|
||||
Objects = objects;
|
||||
Label = label;
|
||||
}
|
||||
|
||||
public ActorData OnlyGPose()
|
||||
=> new(Objects.Where(o => o.IsGPoseOrCutscene).ToList(), Label);
|
||||
}
|
||||
199
CustomizePlus.GameData/Data/Model.cs
Normal file
199
CustomizePlus.GameData/Data/Model.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
|
||||
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
|
||||
|
||||
namespace CustomizePlus.GameData.Data;
|
||||
|
||||
public readonly unsafe struct Model : IEquatable<Model>
|
||||
{
|
||||
private Model(nint address)
|
||||
=> Address = address;
|
||||
|
||||
public readonly nint Address;
|
||||
|
||||
public static readonly Model Null = new(0);
|
||||
|
||||
public DrawObject* AsDrawObject
|
||||
=> (DrawObject*)Address;
|
||||
|
||||
public CharacterBase* AsCharacterBase
|
||||
=> (CharacterBase*)Address;
|
||||
|
||||
public Weapon* AsWeapon
|
||||
=> (Weapon*)Address;
|
||||
|
||||
public Human* AsHuman
|
||||
=> (Human*)Address;
|
||||
|
||||
public static implicit operator Model(nint? pointer)
|
||||
=> new(pointer ?? nint.Zero);
|
||||
|
||||
public static implicit operator Model(Object* pointer)
|
||||
=> new((nint)pointer);
|
||||
|
||||
public static implicit operator Model(DrawObject* pointer)
|
||||
=> new((nint)pointer);
|
||||
|
||||
public static implicit operator Model(Human* pointer)
|
||||
=> new((nint)pointer);
|
||||
|
||||
public static implicit operator Model(CharacterBase* pointer)
|
||||
=> new((nint)pointer);
|
||||
|
||||
public static implicit operator nint(Model model)
|
||||
=> model.Address;
|
||||
|
||||
public bool Valid
|
||||
=> Address != nint.Zero;
|
||||
|
||||
public bool IsCharacterBase
|
||||
=> Valid && AsDrawObject->Object.GetObjectType() == ObjectType.CharacterBase;
|
||||
|
||||
public bool IsHuman
|
||||
=> IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human;
|
||||
|
||||
public bool IsWeapon
|
||||
=> IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Weapon;
|
||||
|
||||
public static implicit operator bool(Model actor)
|
||||
=> actor.Address != nint.Zero;
|
||||
|
||||
public static bool operator true(Model actor)
|
||||
=> actor.Address != nint.Zero;
|
||||
|
||||
public static bool operator false(Model actor)
|
||||
=> actor.Address == nint.Zero;
|
||||
|
||||
public static bool operator !(Model actor)
|
||||
=> actor.Address == nint.Zero;
|
||||
|
||||
public bool Equals(Model other)
|
||||
=> Address == other.Address;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is Model other && Equals(other);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Address.GetHashCode();
|
||||
|
||||
public static bool operator ==(Model lhs, Model rhs)
|
||||
=> lhs.Address == rhs.Address;
|
||||
|
||||
public static bool operator !=(Model lhs, Model rhs)
|
||||
=> lhs.Address != rhs.Address;
|
||||
/*
|
||||
/// <summary> Only valid for humans. </summary>
|
||||
public CharacterArmor GetArmor(EquipSlot slot)
|
||||
=> ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()];
|
||||
|
||||
public Customize GetCustomize()
|
||||
=> *(Customize*)&AsHuman->Customize;
|
||||
|
||||
public (Model Address, CharacterWeapon Data) GetMainhand()
|
||||
{
|
||||
Model weapon = AsDrawObject->Object.ChildObject;
|
||||
return !weapon.IsWeapon
|
||||
? (Null, CharacterWeapon.Empty)
|
||||
: (weapon, new CharacterWeapon(weapon.AsWeapon->ModelSetId, weapon.AsWeapon->SecondaryId, (Variant)weapon.AsWeapon->Variant,
|
||||
(StainId)weapon.AsWeapon->ModelUnknown));
|
||||
}
|
||||
|
||||
public (Model Address, CharacterWeapon Data) GetOffhand()
|
||||
{
|
||||
var mainhand = AsDrawObject->Object.ChildObject;
|
||||
if (mainhand == null)
|
||||
return (Null, CharacterWeapon.Empty);
|
||||
|
||||
Model offhand = mainhand->NextSiblingObject;
|
||||
if (offhand == mainhand || !offhand.IsWeapon)
|
||||
return (Null, CharacterWeapon.Empty);
|
||||
|
||||
return (offhand, new CharacterWeapon(offhand.AsWeapon->ModelSetId, offhand.AsWeapon->SecondaryId, (Variant)offhand.AsWeapon->Variant,
|
||||
(StainId)offhand.AsWeapon->ModelUnknown));
|
||||
}
|
||||
|
||||
/// <summary> Obtain the mainhand and offhand and their data by guesstimating which child object is which. </summary>
|
||||
public (Model Mainhand, Model Offhand, CharacterWeapon MainData, CharacterWeapon OffData) GetWeapons()
|
||||
{
|
||||
var (first, second, count) = GetChildrenWeapons();
|
||||
switch (count)
|
||||
{
|
||||
case 0: return (Null, Null, CharacterWeapon.Empty, CharacterWeapon.Empty);
|
||||
case 1:
|
||||
return (first, Null, new CharacterWeapon(first.AsWeapon->ModelSetId, first.AsWeapon->SecondaryId,
|
||||
(Variant)first.AsWeapon->Variant,
|
||||
(StainId)first.AsWeapon->ModelUnknown), CharacterWeapon.Empty);
|
||||
default:
|
||||
var (main, off) = DetermineMainhand(first, second);
|
||||
var mainData = new CharacterWeapon(main.AsWeapon->ModelSetId, main.AsWeapon->SecondaryId, (Variant)main.AsWeapon->Variant,
|
||||
(StainId)main.AsWeapon->ModelUnknown);
|
||||
var offData = new CharacterWeapon(off.AsWeapon->ModelSetId, off.AsWeapon->SecondaryId, (Variant)off.AsWeapon->Variant,
|
||||
(StainId)off.AsWeapon->ModelUnknown);
|
||||
return (main, off, mainData, offData);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Obtain the mainhand and offhand and their data by using the drawdata container from the corresponding actor. </summary>
|
||||
public (Model Mainhand, Model Offhand, CharacterWeapon MainData, CharacterWeapon OffData) GetWeapons(Actor actor)
|
||||
{
|
||||
if (!Valid || !actor.IsCharacter || actor.Model.Address != Address)
|
||||
return (Null, Null, CharacterWeapon.Empty, CharacterWeapon.Empty);
|
||||
|
||||
Model main = actor.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).DrawObject;
|
||||
var mainData = CharacterWeapon.Empty;
|
||||
if (main.IsWeapon)
|
||||
mainData = new CharacterWeapon(main.AsWeapon->ModelSetId, main.AsWeapon->SecondaryId, (Variant)main.AsWeapon->Variant,
|
||||
(StainId)main.AsWeapon->ModelUnknown);
|
||||
else
|
||||
main = Null;
|
||||
Model off = actor.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject;
|
||||
var offData = CharacterWeapon.Empty;
|
||||
if (off.IsWeapon)
|
||||
offData = new CharacterWeapon(off.AsWeapon->ModelSetId, off.AsWeapon->SecondaryId, (Variant)off.AsWeapon->Variant,
|
||||
(StainId)off.AsWeapon->ModelUnknown);
|
||||
else
|
||||
off = Null;
|
||||
return (main, off, mainData, offData);
|
||||
}
|
||||
|
||||
private (Model, Model, int) GetChildrenWeapons()
|
||||
{
|
||||
Span<Model> weapons = stackalloc Model[2];
|
||||
weapons[0] = Null;
|
||||
weapons[1] = Null;
|
||||
var count = 0;
|
||||
|
||||
if (!Valid || AsDrawObject->Object.ChildObject == null)
|
||||
return (weapons[0], weapons[1], count);
|
||||
|
||||
Model starter = AsDrawObject->Object.ChildObject;
|
||||
var iterator = starter;
|
||||
do
|
||||
{
|
||||
if (iterator.IsWeapon)
|
||||
weapons[count++] = iterator;
|
||||
if (count == 2)
|
||||
return (weapons[0], weapons[1], count);
|
||||
|
||||
iterator = iterator.AsDrawObject->Object.NextSiblingObject;
|
||||
} while (iterator.Address != starter.Address);
|
||||
|
||||
return (weapons[0], weapons[1], count);
|
||||
}
|
||||
|
||||
/// <summary> I don't know a safe way to do this but in experiments this worked.
|
||||
/// The first uint at +0x8 was set to non-zero for the mainhand and zero for the offhand. </summary>
|
||||
private static (Model Mainhand, Model Offhand) DetermineMainhand(Model first, Model second)
|
||||
{
|
||||
var discriminator1 = *(ulong*)(first.Address + 0x10);
|
||||
var discriminator2 = *(ulong*)(second.Address + 0x10);
|
||||
return discriminator1 == 0 && discriminator2 != 0 ? (second, first) : (first, second);
|
||||
}
|
||||
*/
|
||||
public override string ToString()
|
||||
=> $"0x{Address:X}";
|
||||
}
|
||||
125
CustomizePlus.GameData/Extensions/ActorIdentifierExtensions.cs
Normal file
125
CustomizePlus.GameData/Extensions/ActorIdentifierExtensions.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace CustomizePlus.GameData.Extensions;
|
||||
|
||||
public static class ActorIdentifierExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get actor name. Without owner's name if this is owned object.
|
||||
/// </summary>
|
||||
/// <param name="identifier"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static string ToNameWithoutOwnerName(this ActorIdentifier identifier)
|
||||
{
|
||||
if (identifier == ActorIdentifier.Invalid)
|
||||
return "Invalid";
|
||||
|
||||
if (!identifier.IsValid || identifier.Type != IdentifierType.Owned)
|
||||
return identifier.ToName();
|
||||
|
||||
if (ActorIdentifier.Manager == null)
|
||||
throw new Exception("ActorIdentifier.Manager is not initialized");
|
||||
|
||||
return ActorIdentifier.Manager.Data.ToName(identifier.Kind, identifier.DataId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper around Incognito which returns non-incognito name in debug builds
|
||||
/// </summary>
|
||||
/// <param name="identifier"></param>
|
||||
/// <returns></returns>
|
||||
public static string IncognitoDebug(this ActorIdentifier identifier)
|
||||
{
|
||||
if (identifier == ActorIdentifier.Invalid)
|
||||
return "Invalid";
|
||||
|
||||
try
|
||||
{
|
||||
#if DEBUG
|
||||
return identifier.ToString();
|
||||
#else
|
||||
return identifier.Incognito(null);
|
||||
#endif
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
#if DEBUG
|
||||
throw;
|
||||
#else
|
||||
return "Unknown";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For now used to determine if root scaling should be allowed or not
|
||||
/// </summary>
|
||||
/// <param name="identifier"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsAllowedForProfiles(this ActorIdentifier identifier)
|
||||
{
|
||||
if (identifier == ActorIdentifier.Invalid)
|
||||
return false;
|
||||
|
||||
switch (identifier.Type)
|
||||
{
|
||||
case IdentifierType.Player:
|
||||
case IdentifierType.Retainer:
|
||||
case IdentifierType.Npc:
|
||||
return true;
|
||||
case IdentifierType.Owned:
|
||||
return
|
||||
identifier.Kind == ObjectKind.BattleNpc ||
|
||||
//identifier.Kind == ObjectKind.MountType ||
|
||||
identifier.Kind == ObjectKind.Companion;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get "true" actor for special actors. Returns ActorIdentifier.Invalid for non-special actors or if actor cannot be found.
|
||||
/// </summary>
|
||||
/// <param name="identifier"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static ActorIdentifier GetTrueActorForSpecialType(this ActorIdentifier identifier)
|
||||
{
|
||||
if (!identifier.IsValid)
|
||||
return ActorIdentifier.Invalid;
|
||||
|
||||
if (identifier.Type != IdentifierType.Special)
|
||||
return ActorIdentifier.Invalid;
|
||||
|
||||
if (ActorIdentifier.Manager == null)
|
||||
throw new Exception("ActorIdentifier.Manager is not initialized");
|
||||
|
||||
switch (identifier.Special)
|
||||
{
|
||||
case ScreenActor.GPosePlayer:
|
||||
case ScreenActor.CharacterScreen:
|
||||
case ScreenActor.FittingRoom:
|
||||
case ScreenActor.DyePreview:
|
||||
case ScreenActor.Portrait:
|
||||
return ActorIdentifier.Manager.GetCurrentPlayer();
|
||||
case ScreenActor.ExamineScreen:
|
||||
var examineIdentifier = ActorIdentifier.Manager.GetInspectPlayer();
|
||||
|
||||
if (!examineIdentifier.IsValid)
|
||||
examineIdentifier = ActorIdentifier.Manager.GetGlamourPlayer(); //returns ActorIdentifier.Invalid if player is invalid
|
||||
|
||||
if (!examineIdentifier.IsValid)
|
||||
return ActorIdentifier.Invalid;
|
||||
|
||||
return examineIdentifier;
|
||||
case ScreenActor.Card6:
|
||||
case ScreenActor.Card7:
|
||||
case ScreenActor.Card8:
|
||||
return ActorIdentifier.Manager.GetCardPlayer();
|
||||
}
|
||||
|
||||
return ActorIdentifier.Invalid;
|
||||
}
|
||||
}
|
||||
83
CustomizePlus.GameData/Services/CutsceneService.cs
Normal file
83
CustomizePlus.GameData/Services/CutsceneService.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using Penumbra.GameData.Actors;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CustomizePlus.GameData.Services;
|
||||
|
||||
public class CutsceneService : 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 short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
|
||||
|
||||
public IEnumerable<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> Actors
|
||||
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
|
||||
.Where(i => _objects[i] != null)
|
||||
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
|
||||
|
||||
public unsafe CutsceneService(IObjectTable objects, GameEventManager events)
|
||||
{
|
||||
_objects = objects;
|
||||
_events = events;
|
||||
_events.CopyCharacter += OnCharacterCopy;
|
||||
_events.CharacterDestructor += OnCharacterDestructor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx]
|
||||
{
|
||||
get
|
||||
{
|
||||
Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx);
|
||||
idx = _copiedCharacters[idx - CutsceneStartIdx];
|
||||
return idx < 0 ? null : _objects[idx];
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (idx is >= CutsceneStartIdx and < CutsceneEndIdx)
|
||||
return _copiedCharacters[idx - CutsceneStartIdx];
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
_events.CopyCharacter -= OnCharacterCopy;
|
||||
_events.CharacterDestructor -= OnCharacterDestructor;
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterDestructor(Character* character)
|
||||
{
|
||||
if (character->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx)
|
||||
return;
|
||||
|
||||
var idx = character->GameObject.ObjectIndex - CutsceneStartIdx;
|
||||
_copiedCharacters[idx] = -1;
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterCopy(Character* target, Character* source)
|
||||
{
|
||||
if (target == null || target->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx)
|
||||
return;
|
||||
|
||||
var idx = target->GameObject.ObjectIndex - CutsceneStartIdx;
|
||||
_copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1);
|
||||
}
|
||||
}
|
||||
193
CustomizePlus.GameData/Services/GameEventManager.cs
Normal file
193
CustomizePlus.GameData/Services/GameEventManager.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using Penumbra.GameData;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CustomizePlus.GameData.Services;
|
||||
|
||||
public unsafe class GameEventManager : IDisposable
|
||||
{
|
||||
private const string Prefix = $"[{nameof(GameEventManager)}]";
|
||||
|
||||
public event CharacterDestructorEvent? CharacterDestructor;
|
||||
public event CopyCharacterEvent? CopyCharacter;
|
||||
public event CreatingCharacterBaseEvent? CreatingCharacterBase;
|
||||
public event CharacterBaseCreatedEvent? CharacterBaseCreated;
|
||||
public event CharacterBaseDestructorEvent? CharacterBaseDestructor;
|
||||
|
||||
public GameEventManager(IGameInteropProvider interop)
|
||||
{
|
||||
interop.InitializeFromAttributes(this);
|
||||
|
||||
_copyCharacterHook =
|
||||
interop.HookFromAddress<CopyCharacterDelegate>((nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter, CopyCharacterDetour);
|
||||
_characterBaseCreateHook =
|
||||
interop.HookFromAddress<CharacterBaseCreateDelegate>((nint)CharacterBase.MemberFunctionPointers.Create, CharacterBaseCreateDetour);
|
||||
_characterBaseDestructorHook =
|
||||
interop.HookFromAddress<CharacterBaseDestructorEvent>((nint)CharacterBase.MemberFunctionPointers.Destroy,
|
||||
CharacterBaseDestructorDetour);
|
||||
_characterDtorHook.Enable();
|
||||
_copyCharacterHook.Enable();
|
||||
_characterBaseCreateHook.Enable();
|
||||
_characterBaseDestructorHook.Enable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_characterDtorHook.Dispose();
|
||||
_copyCharacterHook.Dispose();
|
||||
_characterBaseCreateHook.Dispose();
|
||||
_characterBaseDestructorHook.Dispose();
|
||||
}
|
||||
|
||||
#region Character Destructor
|
||||
|
||||
private delegate void CharacterDestructorDelegate(Character* character);
|
||||
|
||||
[Signature(Sigs.CharacterDestructor, DetourName = nameof(CharacterDestructorDetour))]
|
||||
private readonly Hook<CharacterDestructorDelegate> _characterDtorHook = null!;
|
||||
|
||||
private void CharacterDestructorDetour(Character* character)
|
||||
{
|
||||
if (CharacterDestructor != null)
|
||||
foreach (var subscriber in CharacterDestructor.GetInvocationList())
|
||||
{
|
||||
try
|
||||
{
|
||||
((CharacterDestructorEvent)subscriber).Invoke(character);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Penumbra.Log.Error($"{Prefix} Error in {nameof(CharacterDestructor)} event when executing {subscriber.Method.Name}:\n{ex}");
|
||||
//todo: log
|
||||
}
|
||||
}
|
||||
|
||||
//Penumbra.Log.Verbose($"{Prefix} {nameof(CharacterDestructor)} triggered with 0x{(nint)character:X}.");
|
||||
//todo: log
|
||||
_characterDtorHook.Original(character);
|
||||
}
|
||||
|
||||
public delegate void CharacterDestructorEvent(Character* character);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Copy Character
|
||||
|
||||
private delegate ulong CopyCharacterDelegate(CharacterSetup* target, GameObject* source, uint unk);
|
||||
|
||||
private readonly Hook<CopyCharacterDelegate> _copyCharacterHook;
|
||||
|
||||
private ulong CopyCharacterDetour(CharacterSetup* target, GameObject* source, uint unk)
|
||||
{
|
||||
// TODO: update when CS updated.
|
||||
var character = ((Character**)target)[1];
|
||||
if (CopyCharacter != null)
|
||||
foreach (var subscriber in CopyCharacter.GetInvocationList())
|
||||
{
|
||||
try
|
||||
{
|
||||
((CopyCharacterEvent)subscriber).Invoke(character, (Character*)source);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
/*Penumbra.Log.Error(
|
||||
$"{Prefix} Error in {nameof(CopyCharacter)} event when executing {subscriber.Method.Name}:\n{ex}");*/
|
||||
//todo: log
|
||||
}
|
||||
}
|
||||
|
||||
/*Penumbra.Log.Verbose(
|
||||
$"{Prefix} {nameof(CopyCharacter)} triggered with target 0x{(nint)target:X} and source 0x{(nint)source:X}.");*/
|
||||
//todo: log
|
||||
return _copyCharacterHook.Original(target, source, unk);
|
||||
}
|
||||
|
||||
public delegate void CopyCharacterEvent(Character* target, Character* source);
|
||||
|
||||
#endregion
|
||||
|
||||
#region CharacterBaseCreate
|
||||
|
||||
private delegate nint CharacterBaseCreateDelegate(uint a, nint b, nint c, byte d);
|
||||
|
||||
private readonly Hook<CharacterBaseCreateDelegate> _characterBaseCreateHook;
|
||||
|
||||
private nint CharacterBaseCreateDetour(uint a, nint b, nint c, byte d)
|
||||
{
|
||||
if (CreatingCharacterBase != null)
|
||||
foreach (var subscriber in CreatingCharacterBase.GetInvocationList())
|
||||
{
|
||||
try
|
||||
{
|
||||
((CreatingCharacterBaseEvent)subscriber).Invoke((nint)(&a), b, c);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
/*Penumbra.Log.Error(
|
||||
$"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}");*/
|
||||
//todo: log
|
||||
}
|
||||
}
|
||||
|
||||
var ret = _characterBaseCreateHook.Original(a, b, c, d);
|
||||
if (CharacterBaseCreated != null)
|
||||
foreach (var subscriber in CharacterBaseCreated.GetInvocationList())
|
||||
{
|
||||
try
|
||||
{
|
||||
((CharacterBaseCreatedEvent)subscriber).Invoke(a, b, c, ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
/*Penumbra.Log.Error(
|
||||
$"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}");*/
|
||||
//todo: log
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public delegate void CreatingCharacterBaseEvent(nint modelCharaId, nint customize, nint equipment);
|
||||
public delegate void CharacterBaseCreatedEvent(uint modelCharaId, nint customize, nint equipment, nint drawObject);
|
||||
|
||||
#endregion
|
||||
|
||||
#region CharacterBase Destructor
|
||||
|
||||
public delegate void CharacterBaseDestructorEvent(nint drawBase);
|
||||
|
||||
private readonly Hook<CharacterBaseDestructorEvent> _characterBaseDestructorHook;
|
||||
|
||||
private void CharacterBaseDestructorDetour(nint drawBase)
|
||||
{
|
||||
if (CharacterBaseDestructor != null)
|
||||
foreach (var subscriber in CharacterBaseDestructor.GetInvocationList())
|
||||
{
|
||||
try
|
||||
{
|
||||
((CharacterBaseDestructorEvent)subscriber).Invoke(drawBase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
/*Penumbra.Log.Error(
|
||||
$"{Prefix} Error in {nameof(CharacterBaseDestructorDetour)} event when executing {subscriber.Method.Name}:\n{ex}");*/
|
||||
//todo: log
|
||||
}
|
||||
}
|
||||
|
||||
_characterBaseDestructorHook.Original.Invoke(drawBase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
238
CustomizePlus.GameData/Services/ObjectManager.cs
Normal file
238
CustomizePlus.GameData/Services/ObjectManager.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using CustomizePlus.GameData.Data;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Penumbra.GameData.Actors;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CustomizePlus.GameData.Services;
|
||||
|
||||
public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
|
||||
{
|
||||
private readonly IFramework _framework;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly ActorService _actors;
|
||||
private readonly ITargetManager _targets;
|
||||
|
||||
public IObjectTable Objects
|
||||
=> _objects;
|
||||
|
||||
public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorService actors, ITargetManager targets)
|
||||
{
|
||||
_framework = framework;
|
||||
_clientState = clientState;
|
||||
_objects = objects;
|
||||
_actors = actors;
|
||||
_targets = targets;
|
||||
}
|
||||
|
||||
public DateTime LastUpdate { get; private set; }
|
||||
|
||||
public bool IsInGPose { get; private set; }
|
||||
public ushort World { get; private set; }
|
||||
|
||||
private readonly Dictionary<ActorIdentifier, ActorData> _identifiers = new(200);
|
||||
private readonly Dictionary<ActorIdentifier, ActorData> _allWorldIdentifiers = new(200);
|
||||
private readonly Dictionary<ActorIdentifier, ActorData> _nonOwnedIdentifiers = new(200);
|
||||
|
||||
public IReadOnlyDictionary<ActorIdentifier, ActorData> Identifiers
|
||||
=> _identifiers;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
var lastUpdate = _framework.LastUpdate;
|
||||
if (lastUpdate <= LastUpdate)
|
||||
return;
|
||||
|
||||
LastUpdate = lastUpdate;
|
||||
World = (ushort)(_clientState.LocalPlayer?.CurrentWorld.Id ?? 0u);
|
||||
_identifiers.Clear();
|
||||
_allWorldIdentifiers.Clear();
|
||||
_nonOwnedIdentifiers.Clear();
|
||||
|
||||
for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i)
|
||||
{
|
||||
Actor character = _objects.GetObjectAddress(i);
|
||||
if (character.Identifier(_actors.AwaitedService, 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)
|
||||
break;
|
||||
|
||||
HandleIdentifier(character.GetIdentifier(_actors.AwaitedService), character);
|
||||
}
|
||||
|
||||
void AddSpecial(ScreenActor idx, string label)
|
||||
{
|
||||
Actor actor = _objects.GetObjectAddress((int)idx);
|
||||
if (actor.Identifier(_actors.AwaitedService, out var ident))
|
||||
{
|
||||
var data = new ActorData(actor, label);
|
||||
_identifiers.Add(ident, data);
|
||||
}
|
||||
}
|
||||
|
||||
AddSpecial(ScreenActor.CharacterScreen, "Character Screen Actor");
|
||||
AddSpecial(ScreenActor.ExamineScreen, "Examine Screen Actor");
|
||||
AddSpecial(ScreenActor.FittingRoom, "Fitting Room Actor");
|
||||
AddSpecial(ScreenActor.DyePreview, "Dye Preview Actor");
|
||||
AddSpecial(ScreenActor.Portrait, "Portrait Actor");
|
||||
AddSpecial(ScreenActor.Card6, "Card Actor 6");
|
||||
AddSpecial(ScreenActor.Card7, "Card Actor 7");
|
||||
AddSpecial(ScreenActor.Card8, "Card Actor 8");
|
||||
|
||||
for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i)
|
||||
{
|
||||
Actor character = _objects.GetObjectAddress(i);
|
||||
if (character.Identifier(_actors.AwaitedService, out var identifier))
|
||||
HandleIdentifier(identifier, character);
|
||||
}
|
||||
|
||||
var gPose = GPosePlayer;
|
||||
IsInGPose = gPose.Utf8Name.Length > 0;
|
||||
}
|
||||
|
||||
private void HandleIdentifier(ActorIdentifier identifier, Actor character)
|
||||
{
|
||||
if (!character.Model || !identifier.IsValid)
|
||||
return;
|
||||
|
||||
if (!_identifiers.TryGetValue(identifier, out var data))
|
||||
{
|
||||
data = new ActorData(character, identifier.ToString());
|
||||
_identifiers[identifier] = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Objects.Add(character);
|
||||
}
|
||||
|
||||
if (identifier.Type is IdentifierType.Player or IdentifierType.Owned)
|
||||
{
|
||||
var allWorld = _actors.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue,
|
||||
identifier.Kind,
|
||||
identifier.DataId);
|
||||
|
||||
if (!_allWorldIdentifiers.TryGetValue(allWorld, out var allWorldData))
|
||||
{
|
||||
allWorldData = new ActorData(character, allWorld.ToString());
|
||||
_allWorldIdentifiers[allWorld] = allWorldData;
|
||||
}
|
||||
else
|
||||
{
|
||||
allWorldData.Objects.Add(character);
|
||||
}
|
||||
}
|
||||
|
||||
if (identifier.Type is IdentifierType.Owned)
|
||||
{
|
||||
var nonOwned = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId);
|
||||
if (!_nonOwnedIdentifiers.TryGetValue(nonOwned, out var nonOwnedData))
|
||||
{
|
||||
nonOwnedData = new ActorData(character, nonOwned.ToString());
|
||||
_nonOwnedIdentifiers[nonOwned] = nonOwnedData;
|
||||
}
|
||||
else
|
||||
{
|
||||
nonOwnedData.Objects.Add(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Actor GPosePlayer
|
||||
=> _objects.GetObjectAddress((int)ScreenActor.GPosePlayer);
|
||||
|
||||
public Actor Player
|
||||
=> _objects.GetObjectAddress(0);
|
||||
|
||||
public unsafe Actor Target
|
||||
=> _clientState.IsGPosing ? TargetSystem.Instance()->GPoseTarget : TargetSystem.Instance()->Target;
|
||||
|
||||
public Actor Focus
|
||||
=> _targets.FocusTarget?.Address ?? nint.Zero;
|
||||
|
||||
public Actor MouseOver
|
||||
=> _targets.MouseOverTarget?.Address ?? nint.Zero;
|
||||
|
||||
public (ActorIdentifier Identifier, ActorData Data) PlayerData
|
||||
{
|
||||
get
|
||||
{
|
||||
Update();
|
||||
return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
|
||||
? (ident, data)
|
||||
: (ident, ActorData.Invalid);
|
||||
}
|
||||
}
|
||||
|
||||
public (ActorIdentifier Identifier, ActorData Data) TargetData
|
||||
{
|
||||
get
|
||||
{
|
||||
Update();
|
||||
return Target.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
|
||||
? (ident, data)
|
||||
: (ident, ActorData.Invalid);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<ActorIdentifier, ActorData>> GetEnumerator()
|
||||
=> Identifiers.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> Identifiers.Count;
|
||||
|
||||
/// <summary> Also handles All Worlds players and non-owned NPCs. </summary>
|
||||
public bool ContainsKey(ActorIdentifier key)
|
||||
=> Identifiers.ContainsKey(key) || _allWorldIdentifiers.ContainsKey(key) || _nonOwnedIdentifiers.ContainsKey(key);
|
||||
|
||||
public bool TryGetValue(ActorIdentifier key, out ActorData value)
|
||||
=> Identifiers.TryGetValue(key, out value);
|
||||
|
||||
public bool TryGetValueAllWorld(ActorIdentifier key, out ActorData value)
|
||||
=> _allWorldIdentifiers.TryGetValue(key, out value);
|
||||
|
||||
public bool TryGetValueNonOwned(ActorIdentifier key, out ActorData value)
|
||||
=> _nonOwnedIdentifiers.TryGetValue(key, out value);
|
||||
|
||||
public ActorData this[ActorIdentifier key]
|
||||
=> Identifiers[key];
|
||||
|
||||
public IEnumerable<ActorIdentifier> Keys
|
||||
=> Identifiers.Keys;
|
||||
|
||||
public IEnumerable<ActorData> Values
|
||||
=> Identifiers.Values;
|
||||
|
||||
public bool GetName(string lowerName, out Actor actor)
|
||||
{
|
||||
(actor, var ret) = lowerName switch
|
||||
{
|
||||
"" => (Actor.Null, true),
|
||||
"<me>" => (Player, true),
|
||||
"self" => (Player, true),
|
||||
"<t>" => (Target, true),
|
||||
"target" => (Target, true),
|
||||
"<f>" => (Focus, true),
|
||||
"focus" => (Focus, true),
|
||||
"<mo>" => (MouseOver, true),
|
||||
"mouseover" => (MouseOver, true),
|
||||
_ => (Actor.Null, false),
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
78
CustomizePlus.GameData/Services/ServiceWrapper.cs
Normal file
78
CustomizePlus.GameData/Services/ServiceWrapper.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
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))
|
||||
{ }
|
||||
}
|
||||
Reference in New Issue
Block a user