Code commit
This commit is contained in:
230
.editorconfig
Normal file
230
.editorconfig
Normal file
@@ -0,0 +1,230 @@
|
||||
root = true
|
||||
|
||||
file_header_template = © Customize+.\nLicensed under the MIT license.
|
||||
|
||||
dotnet_diagnostic.sa1000.severity = none
|
||||
dotnet_diagnostic.sa1027.severity = none
|
||||
dotnet_diagnostic.sa1600.severity = none
|
||||
dotnet_diagnostic.sa1503.severity = none
|
||||
dotnet_diagnostic.sa1633.severity = none
|
||||
dotnet_diagnostic.sa1401.severity = none
|
||||
dotnet_diagnostic.sa1601.severity = none
|
||||
dotnet_diagnostic.sa1602.severity = none
|
||||
dotnet_diagnostic.sa1516.severity = none
|
||||
dotnet_diagnostic.sa1618.severity = none
|
||||
dotnet_diagnostic.sa1611.severity = none
|
||||
dotnet_diagnostic.sa1615.severity = none
|
||||
dotnet_diagnostic.sa1011.severity = none
|
||||
dotnet_diagnostic.sa1134.severity = none
|
||||
|
||||
dotnet_diagnostic.ide0011.severity = none
|
||||
dotnet_diagnostic.ide0022.severity = none
|
||||
dotnet_diagnostic.ide0023.severity = none
|
||||
dotnet_diagnostic.ide1006.severity = warning
|
||||
dotnet_diagnostic.ide0044.severity = suggestion
|
||||
dotnet_diagnostic.ide0058.severity = none
|
||||
dotnet_diagnostic.ide0073.severity = warning
|
||||
dotnet_diagnostic.ide0025.severity = none
|
||||
dotnet_diagnostic.ide0027.severity = none
|
||||
dotnet_diagnostic.ide0059.severity = none
|
||||
dotnet_diagnostic.ide0065.severity = none
|
||||
|
||||
dotnet_diagnostic.unt0001.severity = warning
|
||||
dotnet_diagnostic.unt0002.severity = warning
|
||||
dotnet_diagnostic.unt0003.severity = warning
|
||||
dotnet_diagnostic.unt0004.severity = warning
|
||||
dotnet_diagnostic.unt0005.severity = warning
|
||||
dotnet_diagnostic.unt0006.severity = warning
|
||||
dotnet_diagnostic.unt0007.severity = warning
|
||||
dotnet_diagnostic.unt0008.severity = warning
|
||||
dotnet_diagnostic.unt0009.severity = warning
|
||||
dotnet_diagnostic.unt0010.severity = warning
|
||||
dotnet_diagnostic.unt0011.severity = warning
|
||||
dotnet_diagnostic.unt0012.severity = warning
|
||||
dotnet_diagnostic.unt0013.severity = none
|
||||
dotnet_diagnostic.unt0014.severity = warning
|
||||
dotnet_diagnostic.unt0015.severity = warning
|
||||
dotnet_diagnostic.unt0016.severity = warning
|
||||
dotnet_diagnostic.unt0017.severity = warning
|
||||
dotnet_diagnostic.unt0018.severity = warning
|
||||
dotnet_diagnostic.unt0019.severity = warning
|
||||
dotnet_diagnostic.unt0020.severity = warning
|
||||
|
||||
dotnet_diagnostic.cs1591.severity = none
|
||||
dotnet_diagnostic.cs0067.severity = none
|
||||
|
||||
dotnet_diagnostic.ca1416.severity = none
|
||||
dotnet_diagnostic.ca2201.severity = none
|
||||
|
||||
# SA1101: Prefix local calls with this
|
||||
dotnet_diagnostic.sa1101.severity = silent
|
||||
|
||||
# CS8602: Dereference of a possibly null reference.
|
||||
dotnet_diagnostic.cs8602.severity = silent
|
||||
|
||||
# CS0168: Variable is declared but never used
|
||||
dotnet_diagnostic.cs0168.severity = silent
|
||||
|
||||
# CS8600: Converting null literal or possible null value to non-nullable type.
|
||||
dotnet_diagnostic.cs8600.severity = suggestion
|
||||
|
||||
[*.{cs,vb}]
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
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: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
|
||||
|
||||
# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.SpacingRules'
|
||||
dotnet_analyzer_diagnostic.category-stylecop.csharp.spacingrules.severity = silent
|
||||
|
||||
# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.ReadabilityRules'
|
||||
dotnet_analyzer_diagnostic.category-stylecop.csharp.readabilityrules.severity = silent
|
||||
|
||||
# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.LayoutRules'
|
||||
dotnet_analyzer_diagnostic.category-stylecop.csharp.layoutrules.severity = silent
|
||||
|
||||
# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.OrderingRules'
|
||||
dotnet_analyzer_diagnostic.category-stylecop.csharp.orderingrules.severity = silent
|
||||
|
||||
[*.{asax,ascx,aspx,axaml,cs,cshtml,htm,html,master,paml,razor,skin,vb,xaml,xamlx,xoml}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
||||
[*]
|
||||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
dotnet_naming_rule.private_constants_rule.import_to_resharper = as_predefined
|
||||
dotnet_naming_rule.private_constants_rule.resharper_style = Label + AaBb, AaBb
|
||||
dotnet_naming_rule.private_constants_rule.severity = warning
|
||||
dotnet_naming_rule.private_constants_rule.style = label_upper_camel_case_style
|
||||
dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
|
||||
dotnet_naming_style.label_upper_camel_case_style.capitalization = pascal_case
|
||||
dotnet_naming_style.label_upper_camel_case_style.required_prefix = Label
|
||||
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
|
||||
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
|
||||
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||
|
||||
# ReSharper inspection severities
|
||||
resharper_arrange_redundant_parentheses_highlighting = hint
|
||||
resharper_arrange_this_qualifier_highlighting = hint
|
||||
resharper_arrange_type_member_modifiers_highlighting = hint
|
||||
resharper_arrange_type_modifiers_highlighting = hint
|
||||
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
|
||||
resharper_built_in_type_reference_style_highlighting = hint
|
||||
resharper_redundant_base_qualifier_highlighting = warning
|
||||
resharper_suggest_var_or_type_built_in_types_highlighting = hint
|
||||
resharper_suggest_var_or_type_elsewhere_highlighting = hint
|
||||
resharper_suggest_var_or_type_simple_types_highlighting = hint
|
||||
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 = block_scoped:suggestion
|
||||
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
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
publish/
|
||||
\.vs/
|
||||
packages/
|
||||
*.user
|
||||
bin/
|
||||
obj/
|
||||
deploy/
|
||||
Setup/Release/
|
||||
Setup/Debug/
|
||||
*.tlog
|
||||
*.iobj
|
||||
*.ipdb
|
||||
*.filters
|
||||
*.pdb
|
||||
*.log
|
||||
*.obj
|
||||
*.idb
|
||||
*.recipe
|
||||
*.ilk
|
||||
\.vscode/
|
||||
16
.gitmodules
vendored
Normal file
16
.gitmodules
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
[submodule "submodules/OtterGui"]
|
||||
path = submodules/OtterGui
|
||||
url = https://github.com/Ottermandias/OtterGui.git
|
||||
branch = main
|
||||
[submodule "submodules/Penumbra.GameData"]
|
||||
path = submodules/Penumbra.GameData
|
||||
url = https://github.com/Ottermandias/Penumbra.GameData
|
||||
branch = main
|
||||
[submodule "submodules/Penumbra.Api"]
|
||||
path = submodules/Penumbra.Api
|
||||
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||
branch = main
|
||||
[submodule "submodules/Penumbra.String"]
|
||||
path = submodules/Penumbra.String
|
||||
url = https://github.com/Ottermandias/Penumbra.String.git
|
||||
branch = main
|
||||
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))
|
||||
{ }
|
||||
}
|
||||
120
CustomizePlus.sln
Normal file
120
CustomizePlus.sln
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.31911.260
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4CF55D1D-C5C5-4368-89D6-31D79EF6978C}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomizePlus", "CustomizePlus\CustomizePlus.csproj", "{5BA385F5-C17E-4CE4-828A-24F7F19C434B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "submodules", "submodules", "{121C2200-A844-44FD-85C4-22D6C7E35553}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "submodules\OtterGui\OtterGui.csproj", "{0D465539-6133-4088-B4BB-F260FA2A1557}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomizePlus.GameData", "CustomizePlus.GameData\CustomizePlus.GameData.csproj", "{CDB26C94-1200-45AA-AF96-D4526DC76AD5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "submodules\Penumbra.GameData\Penumbra.GameData.csproj", "{D79C8833-D241-4867-BF6F-8097E0ED8067}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "submodules\Penumbra.Api\Penumbra.Api.csproj", "{CC460943-1E07-4FA0-8B8C-67F0EF385290}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "submodules\Penumbra.String\Penumbra.String.csproj", "{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
ReleaseObfuscated|Any CPU = ReleaseObfuscated|Any CPU
|
||||
ReleaseObfuscated|x64 = ReleaseObfuscated|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Debug|x64.Build.0 = Debug|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Release|Any CPU.Build.0 = Release|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Release|x64.ActiveCfg = Release|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.Release|x64.Build.0 = Release|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.ReleaseObfuscated|Any CPU.ActiveCfg = ReleaseObfuscated|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.ReleaseObfuscated|Any CPU.Build.0 = ReleaseObfuscated|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.ReleaseObfuscated|x64.ActiveCfg = ReleaseObfuscated|x64
|
||||
{5BA385F5-C17E-4CE4-828A-24F7F19C434B}.ReleaseObfuscated|x64.Build.0 = ReleaseObfuscated|x64
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.ReleaseObfuscated|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.ReleaseObfuscated|Any CPU.Build.0 = Release|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.ReleaseObfuscated|x64.ActiveCfg = ReleaseObfuscated|Any CPU
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557}.ReleaseObfuscated|x64.Build.0 = ReleaseObfuscated|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.ReleaseObfuscated|Any CPU.ActiveCfg = ReleaseObfuscated|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.ReleaseObfuscated|Any CPU.Build.0 = ReleaseObfuscated|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.ReleaseObfuscated|x64.ActiveCfg = ReleaseObfuscated|Any CPU
|
||||
{CDB26C94-1200-45AA-AF96-D4526DC76AD5}.ReleaseObfuscated|x64.Build.0 = ReleaseObfuscated|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.ReleaseObfuscated|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.ReleaseObfuscated|Any CPU.Build.0 = Release|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.ReleaseObfuscated|x64.ActiveCfg = ReleaseObfuscated|Any CPU
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067}.ReleaseObfuscated|x64.Build.0 = ReleaseObfuscated|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.ReleaseObfuscated|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.ReleaseObfuscated|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.ReleaseObfuscated|x64.ActiveCfg = ReleaseObfuscated|Any CPU
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290}.ReleaseObfuscated|x64.Build.0 = ReleaseObfuscated|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.ReleaseObfuscated|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.ReleaseObfuscated|Any CPU.Build.0 = Release|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.ReleaseObfuscated|x64.ActiveCfg = ReleaseObfuscated|Any CPU
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823}.ReleaseObfuscated|x64.Build.0 = ReleaseObfuscated|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0D465539-6133-4088-B4BB-F260FA2A1557} = {121C2200-A844-44FD-85C4-22D6C7E35553}
|
||||
{D79C8833-D241-4867-BF6F-8097E0ED8067} = {121C2200-A844-44FD-85C4-22D6C7E35553}
|
||||
{CC460943-1E07-4FA0-8B8C-67F0EF385290} = {121C2200-A844-44FD-85C4-22D6C7E35553}
|
||||
{CB1DFB63-22D9-4E90-A8C1-A4F7CFEF7823} = {121C2200-A844-44FD-85C4-22D6C7E35553}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
85
CustomizePlus/.editorconfig
Normal file
85
CustomizePlus/.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
|
||||
65
CustomizePlus/Anamnesis/Data/PoseFile.cs
Normal file
65
CustomizePlus/Anamnesis/Data/PoseFile.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CustomizePlus.Anamnesis.Data;
|
||||
|
||||
[Serializable]
|
||||
public class PoseFile
|
||||
{
|
||||
public Vector? Scale { get; set; }
|
||||
|
||||
public Dictionary<string, Bone?>? Bones { get; set; }
|
||||
|
||||
[Serializable]
|
||||
public class Bone
|
||||
{
|
||||
public Vector? Scale { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Vector
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Z { get; set; }
|
||||
|
||||
public static Vector FromString(string str)
|
||||
{
|
||||
var parts = str.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length != 3)
|
||||
{
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
Vector v = new()
|
||||
{
|
||||
X = float.Parse(parts[0], CultureInfo.InvariantCulture),
|
||||
Y = float.Parse(parts[1], CultureInfo.InvariantCulture),
|
||||
Z = float.Parse(parts[2], CultureInfo.InvariantCulture)
|
||||
};
|
||||
return v;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{X}, {Y}, {Z}";
|
||||
}
|
||||
}
|
||||
|
||||
public class VectorConverter : JsonConverter<Vector>
|
||||
{
|
||||
public override Vector? ReadJson(JsonReader reader, Type objectType, Vector? existingValue,
|
||||
bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
return reader.Value is not string str ? null : Vector.FromString(str);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, Vector? value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
74
CustomizePlus/Anamnesis/PoseFileBoneLoader.cs
Normal file
74
CustomizePlus/Anamnesis/PoseFileBoneLoader.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using CustomizePlus.Anamnesis.Data;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CustomizePlus.Anamnesis;
|
||||
|
||||
public class PoseFileBoneLoader
|
||||
{
|
||||
public Dictionary<string, BoneTransform>? LoadBoneTransformsFromFile(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
|
||||
JsonSerializerSettings settings = new()
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = new List<JsonConverter> { new PoseFile.VectorConverter() }
|
||||
};
|
||||
|
||||
var pose = JsonConvert.DeserializeObject<PoseFile>(json, settings);
|
||||
|
||||
if (pose == null)
|
||||
{
|
||||
throw new Exception("Failed to deserialize pose file");
|
||||
}
|
||||
|
||||
if (pose.Bones == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var retDict = new Dictionary<string, BoneTransform>();
|
||||
|
||||
foreach (var kvp in pose.Bones)
|
||||
{
|
||||
if (kvp.Key == Constants.RootBoneName || kvp.Value == null || kvp.Value.Scale == null)
|
||||
continue;
|
||||
|
||||
var scale = kvp.Value!.Scale!.GetAsNumericsVector();
|
||||
if (scale == Vector3.One)
|
||||
continue;
|
||||
|
||||
retDict[kvp.Key] = new BoneTransform
|
||||
{
|
||||
Scaling = scale
|
||||
};
|
||||
}
|
||||
|
||||
//load up root, but check it more rigorously
|
||||
|
||||
var validRoot = pose.Bones.TryGetValue(Constants.RootBoneName, out var root)
|
||||
&& root != null
|
||||
&& root.Scale != null
|
||||
&& root.Scale.GetAsNumericsVector() != Vector3.Zero
|
||||
&& root.Scale.GetAsNumericsVector() != Vector3.One;
|
||||
|
||||
if (validRoot)
|
||||
{
|
||||
retDict[Constants.RootBoneName] = new BoneTransform
|
||||
{
|
||||
Scaling = root!.Scale!.GetAsNumericsVector()
|
||||
};
|
||||
}
|
||||
|
||||
return retDict;
|
||||
}
|
||||
}
|
||||
243
CustomizePlus/Api/Compatibility/CustomizePlusIpc.cs
Normal file
243
CustomizePlus/Api/Compatibility/CustomizePlusIpc.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Plugin;
|
||||
using System;
|
||||
using System.Text;
|
||||
using OtterGui.Log;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO.Compression;
|
||||
using System.IO;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using CustomizePlus.Configuration.Helpers;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Configuration.Data.Version3;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.GameData.Data;
|
||||
|
||||
namespace CustomizePlus.Api.Compatibility;
|
||||
|
||||
public class CustomizePlusIpc : IDisposable
|
||||
{
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly Logger _logger;
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
|
||||
private readonly TemplateChanged _templateChangedEvent;
|
||||
private readonly ProfileChanged _profileChangedEvent;
|
||||
|
||||
private const int _configurationVersion = 3;
|
||||
|
||||
public const string ProviderApiVersionLabel = $"CustomizePlus.{nameof(GetApiVersion)}";
|
||||
public const string GetProfileFromCharacterLabel = $"CustomizePlus.{nameof(GetProfileFromCharacter)}";
|
||||
public const string SetProfileToCharacterLabel = $"CustomizePlus.{nameof(SetProfileToCharacter)}";
|
||||
public const string RevertCharacterLabel = $"CustomizePlus.{nameof(RevertCharacter)}";
|
||||
//public const string OnProfileUpdateLabel = $"CustomizePlus.{nameof(OnProfileUpdate)}"; //I'm honestly not sure this is even used by mare
|
||||
public static readonly (int, int) ApiVersion = (3, 0);
|
||||
|
||||
//Sends local player's profile on hooks reload (plugin startup) as well as any updates to their profile.
|
||||
//If no profile is applied sends null
|
||||
internal ICallGateProvider<string?, string?, object?>? ProviderOnProfileUpdate;
|
||||
internal ICallGateProvider<Character?, object>? ProviderRevertCharacter;
|
||||
internal ICallGateProvider<string, Character?, object>? ProviderSetProfileToCharacter;
|
||||
internal ICallGateProvider<Character?, string?>? ProviderGetProfileFromCharacter;
|
||||
internal ICallGateProvider<(int, int)>? ProviderGetApiVersion;
|
||||
|
||||
public CustomizePlusIpc(
|
||||
IObjectTable objectTable,
|
||||
DalamudPluginInterface pluginInterface,
|
||||
Logger logger,
|
||||
ProfileManager profileManager,
|
||||
GameObjectService gameObjectService//,
|
||||
/*TemplateChanged templateChangedEvent,
|
||||
ProfileChanged profileChangedEvent*/)
|
||||
{
|
||||
_objectTable = objectTable;
|
||||
_pluginInterface = pluginInterface;
|
||||
_logger = logger;
|
||||
_profileManager = profileManager;
|
||||
_gameObjectService = gameObjectService;
|
||||
/* _templateChangedEvent = templateChangedEvent;
|
||||
_profileChangedEvent = profileChangedEvent;*/
|
||||
|
||||
InitializeProviders();
|
||||
|
||||
/*_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.CustomizePlusIpc);
|
||||
_profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.CustomizePlusIpc);*/
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeProviders();
|
||||
}
|
||||
|
||||
private void InitializeProviders()
|
||||
{
|
||||
_logger.Debug("Initializing legacy Customize+ IPC providers.");
|
||||
try
|
||||
{
|
||||
ProviderGetApiVersion = _pluginInterface.GetIpcProvider<(int, int)>(ProviderApiVersionLabel);
|
||||
ProviderGetApiVersion.RegisterFunc(GetApiVersion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error registering legacy Customize+ IPC provider for {ProviderApiVersionLabel}: {ex}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderGetProfileFromCharacter =
|
||||
_pluginInterface.GetIpcProvider<Character?, string?>(GetProfileFromCharacterLabel);
|
||||
ProviderGetProfileFromCharacter.RegisterFunc(GetProfileFromCharacter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error registering legacy Customize+ IPC provider for {GetProfileFromCharacterLabel}: {ex}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderSetProfileToCharacter =
|
||||
_pluginInterface.GetIpcProvider<string, Character?, object>(SetProfileToCharacterLabel);
|
||||
ProviderSetProfileToCharacter.RegisterAction(SetProfileToCharacter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error registering legacy Customize+ IPC provider for {SetProfileToCharacterLabel}: {ex}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProviderRevertCharacter =
|
||||
_pluginInterface.GetIpcProvider<Character?, object>(RevertCharacterLabel);
|
||||
ProviderRevertCharacter.RegisterAction(RevertCharacter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error registering legacy Customize+ IPC provider for {RevertCharacterLabel}: {ex}");
|
||||
}
|
||||
/*
|
||||
try
|
||||
{
|
||||
ProviderOnProfileUpdate = _pluginInterface.GetIpcProvider<string?, string?, object?>(OnProfileUpdateLabel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error registering legacy Customize+ IPC provider for {OnProfileUpdateLabel}: {ex}");
|
||||
}*/
|
||||
}
|
||||
|
||||
private void DisposeProviders()
|
||||
{
|
||||
ProviderGetProfileFromCharacter?.UnregisterFunc();
|
||||
ProviderSetProfileToCharacter?.UnregisterAction();
|
||||
ProviderRevertCharacter?.UnregisterAction();
|
||||
ProviderGetApiVersion?.UnregisterFunc();
|
||||
ProviderOnProfileUpdate?.UnregisterFunc();
|
||||
}
|
||||
|
||||
private void OnProfileUpdate(Profile? profile)
|
||||
{
|
||||
//Get player's body profile string and send IPC message
|
||||
_logger.Debug($"Sending local player update message: {profile?.Name ?? "no profile"} - {profile?.CharacterName ?? "no profile"}");
|
||||
|
||||
var convertedProfile = profile != null ? GetVersion3Profile(profile) : null;
|
||||
|
||||
ProviderOnProfileUpdate?.SendMessage(convertedProfile?.CharacterName ?? null, convertedProfile == null ? null : JsonConvert.SerializeObject(convertedProfile));
|
||||
}
|
||||
|
||||
private static (int, int) GetApiVersion()
|
||||
{
|
||||
return ApiVersion;
|
||||
}
|
||||
|
||||
private string? GetCharacterProfile(string characterName)
|
||||
{
|
||||
var profile = _profileManager.GetProfileByCharacterName(characterName, true);
|
||||
|
||||
var convertedProfile = profile != null ? GetVersion3Profile(profile) : null;
|
||||
|
||||
return convertedProfile != null ? JsonConvert.SerializeObject(convertedProfile) : null;
|
||||
}
|
||||
|
||||
private string? GetProfileFromCharacter(Character? character)
|
||||
{
|
||||
return character == null ? null : GetCharacterProfile(character.Name.ToString());
|
||||
}
|
||||
|
||||
private void SetProfileToCharacter(string profileJson, Character? character)
|
||||
{
|
||||
if (character == null)
|
||||
return;
|
||||
|
||||
var actor = (Actor)character.Address;
|
||||
if (!actor.Valid)
|
||||
return;
|
||||
|
||||
/*if (character == _objectTable[0])
|
||||
{
|
||||
_logger.Error($"Received request to set profile on local character, this is not allowed");
|
||||
return;
|
||||
}*/
|
||||
|
||||
try
|
||||
{
|
||||
var profile = JsonConvert.DeserializeObject<Version3Profile>(profileJson);
|
||||
if (profile != null)
|
||||
{
|
||||
if (profile.ConfigVersion != _configurationVersion)
|
||||
throw new Exception("Incompatible version");
|
||||
|
||||
_profileManager.AddTemporaryProfile(GetProfileFromVersion3(profile).Item1, actor);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Unable to set body profile. Character: {character?.Name}, exception: {ex}, debug data: {GetBase64String(profileJson)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RevertCharacter(Character? character)
|
||||
{
|
||||
if (character == null)
|
||||
return;
|
||||
|
||||
var actor = (Actor)character.Address;
|
||||
if (!actor.Valid)
|
||||
return;
|
||||
|
||||
/*if (character == _objectTable[0])
|
||||
{
|
||||
_logger.Error($"Received request to revert profile on local character, this is not allowed");
|
||||
return;
|
||||
}*/
|
||||
|
||||
_profileManager.RemoveTemporaryProfile(actor);
|
||||
}
|
||||
|
||||
private string GetBase64String(string data)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(data, Formatting.None);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
using var compressedStream = new MemoryStream();
|
||||
using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
|
||||
zipStream.Write(bytes, 0, bytes.Length);
|
||||
|
||||
return Convert.ToBase64String(compressedStream.ToArray());
|
||||
}
|
||||
|
||||
private Version3Profile GetVersion3Profile(Profile profile)
|
||||
{
|
||||
return V4ProfileToV3Converter.Convert(profile);
|
||||
}
|
||||
|
||||
private (Profile, Template) GetProfileFromVersion3(Version3Profile profile)
|
||||
{
|
||||
return V3ProfileToV4Converter.Convert(profile);
|
||||
}
|
||||
}
|
||||
355
CustomizePlus/Armatures/Data/Armature.cs
Normal file
355
CustomizePlus/Armatures/Data/Armature.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Penumbra.GameData.Actors;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
|
||||
namespace CustomizePlus.Armatures.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "copy" of the ingame skeleton upon which the linked character profile is meant to operate.
|
||||
/// Acts as an interface by which the in-game skeleton can be manipulated on a bone-by-bone basis.
|
||||
/// </summary>
|
||||
public unsafe class Armature
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Customize+ profile for which this mockup applies transformations.
|
||||
/// </summary>
|
||||
public Profile Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Static identifier of the actor associated with this armature
|
||||
/// </summary>
|
||||
public ActorIdentifier ActorIdentifier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not this armature has any renderable objects on which it should act.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this armature has successfully built itself with bone information.
|
||||
/// </summary>
|
||||
public bool IsBuilt => _partialSkeletons.Any();
|
||||
|
||||
/// <summary>
|
||||
/// Internal flag telling ArmatureManager that it should attempt to rebind profile to (another) profile whenever possible.
|
||||
/// </summary>
|
||||
public bool IsPendingProfileRebind { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Represents date and time until which any kind of removal protections will not be applying to this armature.
|
||||
/// Implemented mostly as a armature cleanup protection hack due to how mare works when downloading files for the first time
|
||||
/// </summary>
|
||||
public DateTime ProtectedUntil { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// For debugging purposes, each armature is assigned a globally-unique ID number upon creation.
|
||||
/// </summary>
|
||||
private static uint _nextGlobalId;
|
||||
private readonly uint _localId;
|
||||
|
||||
/// <summary>
|
||||
/// Binding telling which bones are bound to each template for this armature. Built from template list in profile.
|
||||
/// </summary>
|
||||
public Dictionary<string, Template> BoneTemplateBinding { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Each skeleton is made up of several smaller "partial" skeletons.
|
||||
/// Each partial skeleton has its own list of bones, with a root bone at index zero.
|
||||
/// The root bone of a partial skeleton may also be a regular bone in a different partial skeleton.
|
||||
/// </summary>
|
||||
private ModelBone[][] _partialSkeletons;
|
||||
|
||||
#region Bone Accessors -------------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of partial skeletons contained in this armature.
|
||||
/// </summary>
|
||||
public int PartialSkeletonCount => _partialSkeletons.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of bones belonging to the partial skeleton at the given index.
|
||||
/// </summary>
|
||||
public ModelBone[] this[int i]
|
||||
{
|
||||
get => _partialSkeletons[i];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of bones contained within the partial skeleton with the given index.
|
||||
/// </summary>
|
||||
public int GetBoneCountOfPartial(int partialIndex) => _partialSkeletons[partialIndex].Length;
|
||||
|
||||
/// <summary>
|
||||
/// Get the bone at index 'j' within the partial skeleton at index 'i'.
|
||||
/// </summary>
|
||||
public ModelBone this[int i, int j]
|
||||
{
|
||||
get => _partialSkeletons[i][j];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the bone at the given indices, if it exists
|
||||
/// </summary>
|
||||
public ModelBone? GetBoneAt(int partialIndex, int boneIndex)
|
||||
{
|
||||
if (_partialSkeletons.Length > partialIndex
|
||||
&& _partialSkeletons[partialIndex].Length > boneIndex)
|
||||
{
|
||||
return this[partialIndex, boneIndex];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the root bone of the partial skeleton with the given index.
|
||||
/// </summary>
|
||||
public ModelBone GetRootBoneOfPartial(int partialIndex) => this[partialIndex, 0];
|
||||
|
||||
public ModelBone MainRootBone => GetRootBoneOfPartial(0);
|
||||
|
||||
/// <summary>
|
||||
/// Get the total number of bones in each partial skeleton combined.
|
||||
/// </summary>
|
||||
// In exactly one partial skeleton will the root bone be an independent bone. In all others, it's a reference to a separate, real bone.
|
||||
// For that reason we must subtract the number of duplicate bones
|
||||
public int TotalBoneCount => _partialSkeletons.Sum(x => x.Length);
|
||||
|
||||
public IEnumerable<ModelBone> GetAllBones()
|
||||
{
|
||||
for (var i = 0; i < _partialSkeletons.Length; ++i)
|
||||
{
|
||||
for (var j = 0; j < _partialSkeletons[i].Length; ++j)
|
||||
{
|
||||
yield return this[i, j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
#endregion
|
||||
|
||||
public Armature(ActorIdentifier actorIdentifier, Profile profile)
|
||||
{
|
||||
_localId = _nextGlobalId++;
|
||||
|
||||
_partialSkeletons = Array.Empty<ModelBone[]>();
|
||||
|
||||
BoneTemplateBinding = new Dictionary<string, Template>();
|
||||
|
||||
ActorIdentifier = actorIdentifier;
|
||||
Profile = profile;
|
||||
IsVisible = false;
|
||||
|
||||
ProtectFromRemoval();
|
||||
|
||||
Profile.Armatures.Add(this);
|
||||
|
||||
Plugin.Logger.Debug($"Instantiated {this}, attached to {Profile}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return IsBuilt
|
||||
? $"Armature (#{_localId}) on {ActorIdentifier.IncognitoDebug()} ({Profile}) with {TotalBoneCount} bone/s"
|
||||
: $"Armature (#{_localId}) on {ActorIdentifier.IncognitoDebug()} ({Profile}) with no skeleton reference";
|
||||
}
|
||||
|
||||
public bool NewBonesAvailable(CharacterBase* cBase)
|
||||
{
|
||||
if (cBase == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (cBase->Skeleton->PartialSkeletonCount > _partialSkeletons.Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < cBase->Skeleton->PartialSkeletonCount; ++i)
|
||||
{
|
||||
var newPose = cBase->Skeleton->PartialSkeletons[i].GetHavokPose(Constants.TruePoseIndex);
|
||||
if (newPose != null
|
||||
&& newPose->Skeleton->Bones.Length > _partialSkeletons[i].Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild the armature using the provided character base as a reference.
|
||||
/// </summary>
|
||||
public void RebuildSkeleton(CharacterBase* cBase)
|
||||
{
|
||||
if (cBase == null)
|
||||
return;
|
||||
|
||||
var newPartials = ParseBonesFromObject(this, cBase);
|
||||
|
||||
_partialSkeletons = newPartials.Select(x => x.ToArray()).ToArray();
|
||||
|
||||
RebuildBoneTemplateBinding();
|
||||
|
||||
Plugin.Logger.Debug($"Rebuilt {this}");
|
||||
}
|
||||
|
||||
public void AugmentSkeleton(CharacterBase* cBase)
|
||||
{
|
||||
if (cBase == null)
|
||||
return;
|
||||
|
||||
var oldPartials = _partialSkeletons.Select(x => x.ToList()).ToList();
|
||||
var newPartials = ParseBonesFromObject(this, cBase);
|
||||
|
||||
//for each of the new partial skeletons discovered...
|
||||
for (var i = 0; i < newPartials.Count; ++i)
|
||||
{
|
||||
//if the old skeleton doesn't contain the new partial at all, add the whole thing
|
||||
if (i > oldPartials.Count)
|
||||
{
|
||||
oldPartials.Add(newPartials[i]);
|
||||
}
|
||||
//otherwise, add every model bone the new partial has that the old one doesn't
|
||||
else
|
||||
{
|
||||
//Case: get carbuncle, enable profile for it, turn carbuncle into human via glamourer
|
||||
if (oldPartials.Count <= i)
|
||||
oldPartials.Add(new List<ModelBone>());
|
||||
|
||||
for (var j = oldPartials[i].Count; j < newPartials[i].Count; ++j)
|
||||
{
|
||||
oldPartials[i].Add(newPartials[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_partialSkeletons = oldPartials.Select(x => x.ToArray()).ToArray();
|
||||
|
||||
RebuildBoneTemplateBinding();
|
||||
|
||||
Plugin.Logger.Debug($"Augmented {this} with new bones");
|
||||
}
|
||||
|
||||
public BoneTransform? GetAppliedBoneTransform(string boneName)
|
||||
{
|
||||
if (BoneTemplateBinding.TryGetValue(boneName, out var template)
|
||||
&& template != null)
|
||||
{
|
||||
if (template.Bones.TryGetValue(boneName, out var boneTransform))
|
||||
return boneTransform;
|
||||
else
|
||||
Plugin.Logger.Error($"Bone {boneName} is null in template {template.UniqueId}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply removal protection for 30 seconds starting from current time. For the most part this is a hack for mare.
|
||||
/// </summary>
|
||||
public void ProtectFromRemoval()
|
||||
{
|
||||
ProtectedUntil = DateTime.UtcNow.AddSeconds(30);
|
||||
}
|
||||
|
||||
private static unsafe List<List<ModelBone>> ParseBonesFromObject(Armature arm, CharacterBase* cBase)
|
||||
{
|
||||
List<List<ModelBone>> newPartials = new();
|
||||
|
||||
try
|
||||
{
|
||||
//build the skeleton
|
||||
for (var pSkeleIndex = 0; pSkeleIndex < cBase->Skeleton->PartialSkeletonCount; ++pSkeleIndex)
|
||||
{
|
||||
var currentPartial = cBase->Skeleton->PartialSkeletons[pSkeleIndex];
|
||||
var currentPose = currentPartial.GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
newPartials.Add(new());
|
||||
|
||||
if (currentPose == null)
|
||||
continue;
|
||||
|
||||
for (var boneIndex = 0; boneIndex < currentPose->Skeleton->Bones.Length; ++boneIndex)
|
||||
{
|
||||
if (currentPose->Skeleton->Bones[boneIndex].Name.String is string boneName &&
|
||||
boneName != null)
|
||||
{
|
||||
//time to build a new bone
|
||||
ModelBone newBone = new(arm, boneName, pSkeleIndex, boneIndex);
|
||||
Plugin.Logger.Debug($"Created new bone: {boneName} on {pSkeleIndex}->{boneIndex} arm: {arm._localId}");
|
||||
|
||||
if (currentPose->Skeleton->ParentIndices[boneIndex] is short parentIndex
|
||||
&& parentIndex >= 0)
|
||||
{
|
||||
newBone.AddParent(pSkeleIndex, parentIndex);
|
||||
newPartials[pSkeleIndex][parentIndex].AddChild(pSkeleIndex, boneIndex);
|
||||
}
|
||||
|
||||
foreach (var mb in newPartials.SelectMany(x => x))
|
||||
{
|
||||
if (AreTwinnedNames(boneName, mb.BoneName))
|
||||
{
|
||||
newBone.AddTwin(mb.PartialSkeletonIndex, mb.BoneIndex);
|
||||
mb.AddTwin(pSkeleIndex, boneIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//linking is performed later
|
||||
|
||||
newPartials.Last().Add(newBone);
|
||||
}
|
||||
else
|
||||
{
|
||||
Plugin.Logger.Error($"Failed to process bone @ <{pSkeleIndex}, {boneIndex}> while parsing bones from {cBase->ToString()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BoneData.LogNewBones(newPartials.SelectMany(x => x.Select(y => y.BoneName)).ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"Error parsing armature skeleton from {cBase->ToString()}:\n\t{ex}");
|
||||
}
|
||||
|
||||
return newPartials;
|
||||
}
|
||||
|
||||
public void RebuildBoneTemplateBinding()
|
||||
{
|
||||
BoneTemplateBinding.Clear();
|
||||
|
||||
foreach (var template in Profile.Templates)
|
||||
{
|
||||
foreach (var kvPair in template.Bones)
|
||||
{
|
||||
BoneTemplateBinding[kvPair.Key] = template;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bone in GetAllBones())
|
||||
bone.LinkToTemplate(BoneTemplateBinding.ContainsKey(bone.BoneName) ? BoneTemplateBinding[bone.BoneName] : null);
|
||||
|
||||
Plugin.Logger.Debug($"Rebuilt template binding for armature {_localId}");
|
||||
}
|
||||
|
||||
private static bool AreTwinnedNames(string name1, string name2)
|
||||
{
|
||||
return name1[^1] == 'r' ^ name2[^1] == 'r'
|
||||
&& name1[^1] == 'l' ^ name2[^1] == 'l'
|
||||
&& name1[0..^1] == name2[0..^1];
|
||||
}
|
||||
}
|
||||
301
CustomizePlus/Armatures/Data/ModelBone.cs
Normal file
301
CustomizePlus/Armatures/Data/ModelBone.cs
Normal file
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Armatures.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single bone of an ingame character's skeleton.
|
||||
/// </summary>
|
||||
public unsafe class ModelBone
|
||||
{
|
||||
public enum PoseType
|
||||
{
|
||||
Local, Model, BindPose, World
|
||||
}
|
||||
|
||||
public readonly Armature MasterArmature;
|
||||
|
||||
public readonly int PartialSkeletonIndex;
|
||||
public readonly int BoneIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bone corresponding to this model bone's parent, if it exists.
|
||||
/// (It should in all cases but the root of the skeleton)
|
||||
/// </summary>
|
||||
public ModelBone? ParentBone => _parentPartialIndex >= 0 && _parentBoneIndex >= 0
|
||||
? MasterArmature[_parentPartialIndex, _parentBoneIndex]
|
||||
: null;
|
||||
private int _parentPartialIndex = -1;
|
||||
private int _parentBoneIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets each model bone for which this model bone corresponds to a direct parent thereof.
|
||||
/// A model bone may have zero children.
|
||||
/// </summary>
|
||||
public IEnumerable<ModelBone> ChildBones => _childPartialIndices.Zip(_childBoneIndices, (x, y) => MasterArmature[x, y]);
|
||||
private List<int> _childPartialIndices = new();
|
||||
private List<int> _childBoneIndices = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model bone that forms a mirror image of this model bone, if one exists.
|
||||
/// </summary>
|
||||
public ModelBone? TwinBone => _twinPartialIndex >= 0 && _twinBoneIndex >= 0
|
||||
? MasterArmature[_twinPartialIndex, _twinBoneIndex]
|
||||
: null;
|
||||
private int _twinPartialIndex = -1;
|
||||
private int _twinBoneIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the bone within the in-game skeleton. Referred to in some places as its "code name".
|
||||
/// </summary>
|
||||
public string BoneName;
|
||||
|
||||
/// <summary>
|
||||
/// The transform that this model bone will impart upon its in-game sibling when the master armature
|
||||
/// is applied to the in-game skeleton. Reference to transform contained in top most template in profile applied to character.
|
||||
/// </summary>
|
||||
public BoneTransform? CustomizedTransform { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if bone is linked to any template
|
||||
/// </summary>
|
||||
public bool IsActive => CustomizedTransform != null;
|
||||
|
||||
public ModelBone(Armature arm, string codeName, int partialIdx, int boneIdx)
|
||||
{
|
||||
MasterArmature = arm;
|
||||
PartialSkeletonIndex = partialIdx;
|
||||
BoneIndex = boneIdx;
|
||||
|
||||
BoneName = codeName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Link bone to specific template, unlinks if null is passed
|
||||
/// </summary>
|
||||
/// <param name="template"></param>
|
||||
/// <returns></returns>
|
||||
public bool LinkToTemplate(Template? template)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
if (CustomizedTransform == null)
|
||||
return false;
|
||||
|
||||
CustomizedTransform = null;
|
||||
|
||||
Plugin.Logger.Information($"Unlinked {BoneName} from all templates");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!template.Bones.ContainsKey(BoneName))
|
||||
return false;
|
||||
|
||||
Plugin.Logger.Information($"Linking {BoneName} to {template.Name}");
|
||||
CustomizedTransform = template.Bones[BoneName];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate a bone to act as this model bone's "parent".
|
||||
/// </summary>
|
||||
public void AddParent(int parentPartialIdx, int parentBoneIdx)
|
||||
{
|
||||
if (_parentPartialIndex != -1 || _parentBoneIndex != -1)
|
||||
{
|
||||
throw new Exception($"Tried to add redundant parent to model bone -- {this}");
|
||||
}
|
||||
|
||||
_parentPartialIndex = parentPartialIdx;
|
||||
_parentBoneIndex = parentBoneIdx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that a bone is one of this model bone's "children".
|
||||
/// </summary>
|
||||
public void AddChild(int childPartialIdx, int childBoneIdx)
|
||||
{
|
||||
_childPartialIndices.Add(childPartialIdx);
|
||||
_childBoneIndices.Add(childBoneIdx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicate a bone that acts as this model bone's mirror image, or "twin".
|
||||
/// </summary>
|
||||
public void AddTwin(int twinPartialIdx, int twinBoneIdx)
|
||||
{
|
||||
_twinPartialIndex = twinPartialIdx;
|
||||
_twinBoneIndex = twinBoneIdx;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
//string numCopies = _copyIndices.Count > 0 ? $" ({_copyIndices.Count} copies)" : string.Empty;
|
||||
return $"{BoneName} ({BoneData.GetBoneDisplayName(BoneName)}) @ <{PartialSkeletonIndex}, {BoneIndex}>";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the lineage of this model bone, going back to the skeleton's root bone.
|
||||
/// </summary>
|
||||
public IEnumerable<ModelBone> GetAncestors(bool includeSelf = true) => includeSelf
|
||||
? GetAncestors(new List<ModelBone>() { this })
|
||||
: GetAncestors(new List<ModelBone>());
|
||||
|
||||
private IEnumerable<ModelBone> GetAncestors(List<ModelBone> tail)
|
||||
{
|
||||
tail.Add(this);
|
||||
if (ParentBone is ModelBone mb && mb != null)
|
||||
{
|
||||
return mb.GetAncestors(tail);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tail;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all model bones with a lineage that contains this one.
|
||||
/// </summary>
|
||||
public IEnumerable<ModelBone> GetDescendants(bool includeSelf = false) => includeSelf
|
||||
? GetDescendants(this)
|
||||
: GetDescendants(null);
|
||||
|
||||
private IEnumerable<ModelBone> GetDescendants(ModelBone? first)
|
||||
{
|
||||
var output = first != null
|
||||
? new List<ModelBone>() { first }
|
||||
: new List<ModelBone>();
|
||||
|
||||
output.AddRange(ChildBones);
|
||||
|
||||
using (var iter = output.GetEnumerator())
|
||||
{
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
output.AddRange(iter.Current.ChildBones);
|
||||
yield return iter.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a character base to which this model bone's master armature (presumably) applies,
|
||||
/// return the game's transform value for this model's in-game sibling within the given reference frame.
|
||||
/// </summary>
|
||||
public hkQsTransformf GetGameTransform(CharacterBase* cBase, PoseType refFrame)
|
||||
{
|
||||
|
||||
var skelly = cBase->Skeleton;
|
||||
var pSkelly = skelly->PartialSkeletons[PartialSkeletonIndex];
|
||||
var targetPose = pSkelly.GetHavokPose(Constants.TruePoseIndex);
|
||||
//hkaPose* targetPose = cBase->Skeleton->PartialSkeletons[PartialSkeletonIndex].GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
if (targetPose == null) return Constants.NullTransform;
|
||||
|
||||
return refFrame switch
|
||||
{
|
||||
PoseType.Local => targetPose->LocalPose[BoneIndex],
|
||||
PoseType.Model => targetPose->ModelPose[BoneIndex],
|
||||
_ => Constants.NullTransform
|
||||
//TODO properly implement the other options
|
||||
};
|
||||
}
|
||||
|
||||
private void SetGameTransform(CharacterBase* cBase, hkQsTransformf transform, PoseType refFrame)
|
||||
{
|
||||
SetGameTransform(cBase, transform, PartialSkeletonIndex, BoneIndex, refFrame);
|
||||
}
|
||||
|
||||
private static void SetGameTransform(CharacterBase* cBase, hkQsTransformf transform, int partialIndex, int boneIndex, PoseType refFrame)
|
||||
{
|
||||
var skelly = cBase->Skeleton;
|
||||
var pSkelly = skelly->PartialSkeletons[partialIndex];
|
||||
var targetPose = pSkelly.GetHavokPose(Constants.TruePoseIndex);
|
||||
//hkaPose* targetPose = cBase->Skeleton->PartialSkeletons[PartialSkeletonIndex].GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
if (targetPose == null || targetPose->ModelInSync == 0) return;
|
||||
|
||||
switch (refFrame)
|
||||
{
|
||||
case PoseType.Local:
|
||||
targetPose->LocalPose.Data[boneIndex] = transform;
|
||||
return;
|
||||
|
||||
case PoseType.Model:
|
||||
targetPose->ModelPose.Data[boneIndex] = transform;
|
||||
return;
|
||||
|
||||
default:
|
||||
return;
|
||||
|
||||
//TODO properly implement the other options
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply this model bone's associated transformation to its in-game sibling within
|
||||
/// the skeleton of the given character base.
|
||||
/// </summary>
|
||||
public void ApplyModelTransform(CharacterBase* cBase)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
if (cBase != null
|
||||
&& CustomizedTransform.IsEdited()
|
||||
&& GetGameTransform(cBase, PoseType.Model) is hkQsTransformf gameTransform
|
||||
&& !gameTransform.Equals(Constants.NullTransform)
|
||||
&& CustomizedTransform.ModifyExistingTransform(gameTransform) is hkQsTransformf modTransform
|
||||
&& !modTransform.Equals(Constants.NullTransform))
|
||||
{
|
||||
SetGameTransform(cBase, modTransform, PoseType.Model);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyModelScale(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingScale);
|
||||
public void ApplyModelRotation(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingRotation);
|
||||
public void ApplyModelFullTranslation(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingTranslationWithRotation);
|
||||
public void ApplyStraightModelTranslation(CharacterBase* cBase) => ApplyTransFunc(cBase, CustomizedTransform.ModifyExistingTranslation);
|
||||
|
||||
private void ApplyTransFunc(CharacterBase* cBase, Func<hkQsTransformf, hkQsTransformf> modTrans)
|
||||
{
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
if (cBase != null
|
||||
&& CustomizedTransform.IsEdited()
|
||||
&& GetGameTransform(cBase, PoseType.Model) is hkQsTransformf gameTransform
|
||||
&& !gameTransform.Equals(Constants.NullTransform))
|
||||
{
|
||||
var modTransform = modTrans(gameTransform);
|
||||
|
||||
if (!modTransform.Equals(gameTransform) && !modTransform.Equals(Constants.NullTransform))
|
||||
{
|
||||
SetGameTransform(cBase, modTransform, PoseType.Model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks for a non-zero and non-identity (root) scale.
|
||||
/// </summary>
|
||||
/// <param name="mb">The bone to check</param>
|
||||
/// <returns>If the scale should be applied.</returns>
|
||||
public bool IsModifiedScale()
|
||||
{
|
||||
if (!IsActive)
|
||||
return false;
|
||||
return CustomizedTransform.Scaling.X != 0 && CustomizedTransform.Scaling.X != 1 ||
|
||||
CustomizedTransform.Scaling.Y != 0 && CustomizedTransform.Scaling.Y != 1 ||
|
||||
CustomizedTransform.Scaling.Z != 0 && CustomizedTransform.Scaling.Z != 1;
|
||||
}
|
||||
}
|
||||
30
CustomizePlus/Armatures/Events/ArmatureChanged.cs
Normal file
30
CustomizePlus/Armatures/Events/ArmatureChanged.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Armatures.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when armature is changed
|
||||
/// </summary>
|
||||
public sealed class ArmatureChanged() : EventWrapper<ArmatureChanged.Type, Armature?, object?, ArmatureChanged.Priority>(nameof(ArmatureChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
//Created,
|
||||
Deleted
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
ProfileManager
|
||||
}
|
||||
|
||||
public enum DeletionReason
|
||||
{
|
||||
Gone,
|
||||
NoActiveProfiles,
|
||||
ProfileManagerEvent,
|
||||
TemplateEditorEvent
|
||||
}
|
||||
}
|
||||
535
CustomizePlus/Armatures/Services/ArmatureManager.cs
Normal file
535
CustomizePlus/Armatures/Services/ArmatureManager.cs
Normal file
@@ -0,0 +1,535 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Actors;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Armatures.Events;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.GameData.Data;
|
||||
using CustomizePlus.GameData.Services;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
|
||||
namespace CustomizePlus.Armatures.Services;
|
||||
|
||||
public unsafe sealed class ArmatureManager : IDisposable
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
private readonly TemplateChanged _templateChangedEvent;
|
||||
private readonly ProfileChanged _profileChangedEvent;
|
||||
private readonly Logger _logger;
|
||||
private readonly FrameworkManager _framework;
|
||||
private readonly ObjectManager _objectManager;
|
||||
private readonly ActorService _actorService;
|
||||
private readonly ArmatureChanged _event;
|
||||
|
||||
public Dictionary<ActorIdentifier, Armature> Armatures { get; private set; } = new();
|
||||
|
||||
public ArmatureManager(
|
||||
ProfileManager profileManager,
|
||||
IObjectTable objectTable,
|
||||
GameObjectService gameObjectService,
|
||||
TemplateChanged templateChangedEvent,
|
||||
ProfileChanged profileChangedEvent,
|
||||
Logger logger,
|
||||
FrameworkManager framework,
|
||||
ObjectManager objectManager,
|
||||
ActorService actorService,
|
||||
ArmatureChanged @event)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
_objectTable = objectTable;
|
||||
_gameObjectService = gameObjectService;
|
||||
_templateChangedEvent = templateChangedEvent;
|
||||
_profileChangedEvent = profileChangedEvent;
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_objectManager = objectManager;
|
||||
_actorService = actorService;
|
||||
_event = @event;
|
||||
|
||||
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.ArmatureManager);
|
||||
_profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.ArmatureManager);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_templateChangedEvent.Unsubscribe(OnTemplateChange);
|
||||
_profileChangedEvent.Unsubscribe(OnProfileChange);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main rendering function, called from rendering hook
|
||||
/// </summary>
|
||||
public void OnRender()
|
||||
{
|
||||
try
|
||||
{
|
||||
RefreshArmatures();
|
||||
ApplyArmatureTransforms();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Exception while rendering armatures:\n\t{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function called when game object movement is detected
|
||||
/// </summary>
|
||||
public void OnGameObjectMove(Actor actor)
|
||||
{
|
||||
if (!actor.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return;
|
||||
|
||||
if (Armatures.TryGetValue(identifier, out var armature) && armature.IsBuilt && armature.IsVisible)
|
||||
ApplyRootTranslation(armature, actor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes armatures which no longer have actor associated with them and creates armatures for new actors
|
||||
/// </summary>
|
||||
private void RefreshArmatures()
|
||||
{
|
||||
_objectManager.Update();
|
||||
|
||||
var currentTime = DateTime.UtcNow;
|
||||
foreach (var kvPair in Armatures.ToList())
|
||||
{
|
||||
var armature = kvPair.Value;
|
||||
if (!_objectManager.ContainsKey(kvPair.Value.ActorIdentifier) &&
|
||||
currentTime > armature.ProtectedUntil) //Only remove armatures which are no longer protected
|
||||
{
|
||||
_logger.Debug($"Removing armature {armature} because {kvPair.Key.IncognitoDebug()} is gone");
|
||||
RemoveArmature(armature, ArmatureChanged.DeletionReason.Gone);
|
||||
}
|
||||
}
|
||||
|
||||
Profile? GetProfileForActor(ActorIdentifier identifier)
|
||||
{
|
||||
foreach (var profile in _profileManager.GetEnabledProfilesByActor(identifier))
|
||||
{
|
||||
if (profile.LimitLookupToOwnedObjects &&
|
||||
identifier.Type == IdentifierType.Owned &&
|
||||
identifier.PlayerName != _objectManager.PlayerData.Identifier.PlayerName)
|
||||
continue;
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var obj in _objectManager)
|
||||
{
|
||||
if (!Armatures.ContainsKey(obj.Key))
|
||||
{
|
||||
var activeProfile = GetProfileForActor(obj.Key);
|
||||
if (activeProfile == null)
|
||||
continue;
|
||||
|
||||
var newArm = new Armature(obj.Key, activeProfile);
|
||||
TryLinkSkeleton(newArm);
|
||||
Armatures.Add(obj.Key, newArm);
|
||||
_logger.Debug($"Added '{newArm}' for {obj.Key.IncognitoDebug()} to cache");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var armature = Armatures[obj.Key];
|
||||
|
||||
if (armature.IsPendingProfileRebind)
|
||||
{
|
||||
_logger.Debug($"Armature {armature} is pending profile rebind, rebinding...");
|
||||
armature.IsPendingProfileRebind = false;
|
||||
|
||||
var activeProfile = GetProfileForActor(obj.Key);
|
||||
if (activeProfile == armature.Profile)
|
||||
continue;
|
||||
|
||||
if (activeProfile == null)
|
||||
{
|
||||
_logger.Debug($"Removing armature {armature} because it doesn't have any active profiles");
|
||||
RemoveArmature(armature, ArmatureChanged.DeletionReason.NoActiveProfiles);
|
||||
continue;
|
||||
}
|
||||
|
||||
armature.Profile.Armatures.Remove(armature);
|
||||
armature.Profile = activeProfile;
|
||||
activeProfile.Armatures.Add(armature);
|
||||
armature.RebuildBoneTemplateBinding();
|
||||
|
||||
}
|
||||
|
||||
armature.IsVisible = armature.Profile.Enabled && TryLinkSkeleton(armature); //todo: remove armatures which are not visible?
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ApplyArmatureTransforms()
|
||||
{
|
||||
foreach (var kvPair in Armatures)
|
||||
{
|
||||
var armature = kvPair.Value;
|
||||
if (armature.IsBuilt && armature.IsVisible && _objectManager.ContainsKey(armature.ActorIdentifier))
|
||||
{
|
||||
foreach (var actor in _objectManager[armature.ActorIdentifier].Objects)
|
||||
ApplyPiecewiseTransformation(armature, actor, armature.ActorIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not a link can be established between the armature and an in-game object.
|
||||
/// If unbuilt, the armature will be rebuilded.
|
||||
/// </summary>
|
||||
private bool TryLinkSkeleton(Armature armature, bool forceRebuild = false)
|
||||
{
|
||||
_objectManager.Update();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_objectManager.ContainsKey(armature.ActorIdentifier))
|
||||
return false;
|
||||
|
||||
var actor = _objectManager[armature.ActorIdentifier].Objects[0];
|
||||
|
||||
if (!armature.IsBuilt || forceRebuild)
|
||||
{
|
||||
armature.RebuildSkeleton(actor.Model.AsCharacterBase);
|
||||
}
|
||||
else if (armature.NewBonesAvailable(actor.Model.AsCharacterBase))
|
||||
{
|
||||
armature.AugmentSkeleton(actor.Model.AsCharacterBase);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This is on wait until isse #191 on Github responds. Keeping it in code, delete it if I forget and this is longer then a month ago.
|
||||
|
||||
// Disabling this if its any Default Profile due to Log spam. A bit crazy but hey, if its for me id Remove Default profiles all together so this is as much as ill do for now! :)
|
||||
//if(!(Profile.CharacterName.Equals(Constants.DefaultProfileCharacterName) || Profile.CharacterName.Equals("DefaultCutscene"))) {
|
||||
_logger.Error($"Error occured while attempting to link skeleton: {armature}");
|
||||
throw;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterate through the skeleton of the given character base, and apply any transformations
|
||||
/// for which this armature contains corresponding model bones. This method of application
|
||||
/// is safer but more computationally costly
|
||||
/// </summary>
|
||||
private void ApplyPiecewiseTransformation(Armature armature, Actor actor, ActorIdentifier actorIdentifier)
|
||||
{
|
||||
var cBase = actor.Model.AsCharacterBase;
|
||||
|
||||
var isMount = actorIdentifier.Type == IdentifierType.Owned &&
|
||||
actorIdentifier.Kind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.MountType;
|
||||
|
||||
Actor? mountOwner = null;
|
||||
Armature? mountOwnerArmature = null;
|
||||
if (isMount)
|
||||
{
|
||||
(var ident, mountOwner) = _gameObjectService.FindActorsByName(actorIdentifier.PlayerName.ToString()).FirstOrDefault();
|
||||
Armatures.TryGetValue(ident, out mountOwnerArmature);
|
||||
}
|
||||
|
||||
if (cBase != null)
|
||||
{
|
||||
for (var pSkeleIndex = 0; pSkeleIndex < cBase->Skeleton->PartialSkeletonCount; ++pSkeleIndex)
|
||||
{
|
||||
var currentPose = cBase->Skeleton->PartialSkeletons[pSkeleIndex].GetHavokPose(Constants.TruePoseIndex);
|
||||
|
||||
if (currentPose != null)
|
||||
{
|
||||
for (var boneIndex = 0; boneIndex < currentPose->Skeleton->Bones.Length; ++boneIndex)
|
||||
{
|
||||
if (armature.GetBoneAt(pSkeleIndex, boneIndex) is ModelBone mb
|
||||
&& mb != null
|
||||
&& mb.BoneName == currentPose->Skeleton->Bones[boneIndex].Name.String)
|
||||
{
|
||||
if (mb == armature.MainRootBone)
|
||||
{
|
||||
if (_gameObjectService.IsActorHasScalableRoot(actor) && mb.IsModifiedScale())
|
||||
{
|
||||
cBase->DrawObject.Object.Scale = mb.CustomizedTransform!.Scaling;
|
||||
|
||||
//Fix mount owner's scale if needed
|
||||
//todo: always keep owner's scale proper instead of scaling with mount if no armature found
|
||||
if (isMount && mountOwner != null && mountOwnerArmature != null)
|
||||
{
|
||||
var ownerDrawObject = cBase->DrawObject.Object.ChildObject;
|
||||
|
||||
//limit to only modified scales because that is just easier to handle
|
||||
//because we don't need to hook into dismount code to reset character scale
|
||||
//todo: hook into dismount
|
||||
//https://github.com/Cytraen/SeatedSidekickSpectator/blob/main/SetModeHook.cs?
|
||||
if (cBase->DrawObject.Object.ChildObject == mountOwner.Value.Model &&
|
||||
mountOwnerArmature.MainRootBone.IsModifiedScale())
|
||||
{
|
||||
var baseScale = mountOwnerArmature.MainRootBone.CustomizedTransform!.Scaling;
|
||||
|
||||
ownerDrawObject->Scale = new Vector3(Math.Abs(baseScale.X / cBase->DrawObject.Object.Scale.X),
|
||||
Math.Abs(baseScale.Y / cBase->DrawObject.Object.Scale.Y),
|
||||
Math.Abs(baseScale.Z / cBase->DrawObject.Object.Scale.Z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mb.ApplyModelTransform(cBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRootTranslation(Armature arm, Actor actor)
|
||||
{
|
||||
//I'm honestly not sure if we should or even can check if cBase->DrawObject or cBase->DrawObject.Object is a valid object
|
||||
//So for now let's assume we don't need to check for that
|
||||
|
||||
var cBase = actor.Model.AsCharacterBase;
|
||||
if (cBase != null)
|
||||
{
|
||||
var rootBoneTransform = arm.GetAppliedBoneTransform("n_root");
|
||||
if (rootBoneTransform == null)
|
||||
return;
|
||||
|
||||
if (rootBoneTransform.Translation.X == 0 &&
|
||||
rootBoneTransform.Translation.Y == 0 &&
|
||||
rootBoneTransform.Translation.Z == 0)
|
||||
return;
|
||||
|
||||
if (!cBase->DrawObject.IsVisible)
|
||||
return;
|
||||
|
||||
var newPosition = new FFXIVClientStructs.FFXIV.Common.Math.Vector3
|
||||
{
|
||||
X = cBase->DrawObject.Object.Position.X + MathF.Max(rootBoneTransform.Translation.X, 0.01f),
|
||||
Y = cBase->DrawObject.Object.Position.Y + MathF.Max(rootBoneTransform.Translation.Y, 0.01f),
|
||||
Z = cBase->DrawObject.Object.Position.Z + MathF.Max(rootBoneTransform.Translation.Z, 0.01f)
|
||||
};
|
||||
|
||||
cBase->DrawObject.Object.Position = newPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveArmature(Armature armature, ArmatureChanged.DeletionReason reason)
|
||||
{
|
||||
armature.Profile.Armatures.Remove(armature);
|
||||
Armatures.Remove(armature.ActorIdentifier);
|
||||
_logger.Debug($"Armature {armature} removed from cache");
|
||||
|
||||
_event.Invoke(ArmatureChanged.Type.Deleted, armature, reason);
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Templates.Data.Template? template, object? arg3)
|
||||
{
|
||||
if (type is not TemplateChanged.Type.NewBone &&
|
||||
type is not TemplateChanged.Type.DeletedBone &&
|
||||
type is not TemplateChanged.Type.EditorCharacterChanged &&
|
||||
type is not TemplateChanged.Type.EditorEnabled &&
|
||||
type is not TemplateChanged.Type.EditorDisabled)
|
||||
return;
|
||||
|
||||
if (type == TemplateChanged.Type.NewBone ||
|
||||
type == TemplateChanged.Type.DeletedBone) //type == TemplateChanged.Type.EditorCharacterChanged?
|
||||
{
|
||||
//In case a lot of events are triggered at the same time for the same template this should limit the amount of times bindings are unneccessary rebuilt
|
||||
_framework.RegisterImportant($"TemplateRebuild @ {template.UniqueId}", () =>
|
||||
{
|
||||
foreach (var profile in _profileManager.GetProfilesUsingTemplate(template))
|
||||
{
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange New/Deleted bone or character changed: {type}, template: {template.Name.Text.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}->{profile.Armatures.Count} armatures");
|
||||
if (!profile.Enabled || profile.Armatures.Count == 0)
|
||||
continue;
|
||||
|
||||
profile.Armatures.ForEach(x => x.RebuildBoneTemplateBinding());
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == TemplateChanged.Type.EditorCharacterChanged)
|
||||
{
|
||||
(var characterName, var profile) = ((string, Profile))arg3;
|
||||
|
||||
foreach (var armature in GetArmaturesForCharacterName(characterName))
|
||||
{
|
||||
armature.IsPendingProfileRebind = true;
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, {armature}");
|
||||
}
|
||||
|
||||
if (profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
//Rebuild armatures for previous character
|
||||
foreach (var armature in profile.Armatures)
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}, new name: {characterName.Incognify()}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == TemplateChanged.Type.EditorEnabled ||
|
||||
type == TemplateChanged.Type.EditorDisabled)
|
||||
{
|
||||
foreach (var armature in GetArmaturesForCharacterName((string)arg3!))
|
||||
{
|
||||
armature.IsPendingProfileRebind = true;
|
||||
_logger.Debug($"ArmatureManager.OnTemplateChange template editor enabled/disabled: {type}, pending profile set for {armature}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3)
|
||||
{
|
||||
if (type is not ProfileChanged.Type.AddedTemplate &&
|
||||
type is not ProfileChanged.Type.RemovedTemplate &&
|
||||
type is not ProfileChanged.Type.MovedTemplate &&
|
||||
type is not ProfileChanged.Type.ChangedTemplate &&
|
||||
type is not ProfileChanged.Type.Toggled &&
|
||||
type is not ProfileChanged.Type.Deleted &&
|
||||
type is not ProfileChanged.Type.TemporaryProfileAdded &&
|
||||
type is not ProfileChanged.Type.TemporaryProfileDeleted &&
|
||||
type is not ProfileChanged.Type.ChangedCharacterName &&
|
||||
type is not ProfileChanged.Type.ChangedDefaultProfile &&
|
||||
type is not ProfileChanged.Type.LimitLookupToOwnedChanged)
|
||||
return;
|
||||
|
||||
if (type == ProfileChanged.Type.ChangedDefaultProfile)
|
||||
{
|
||||
var oldProfile = (Profile?)arg3;
|
||||
|
||||
if (oldProfile == null || oldProfile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var armature in oldProfile.Armatures)
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange Profile no longer default, armatures rebind scheduled: {type}, old profile: {oldProfile.Name.Text.Incognify()}->{oldProfile.Enabled}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile == null)
|
||||
{
|
||||
_logger.Error($"ArmatureManager.OnProfileChange Invalid input for event: {type}, profile is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ProfileChanged.Type.Toggled)
|
||||
{
|
||||
if (!profile.Enabled && profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
if (profile == _profileManager.DefaultProfile)
|
||||
{
|
||||
foreach (var kvPair in Armatures)
|
||||
{
|
||||
var armature = kvPair.Value;
|
||||
if (armature.Profile == profile)
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange default profile toggled, planning rebind for armature {armature}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(profile.CharacterName))
|
||||
return;
|
||||
|
||||
foreach (var armature in GetArmaturesForCharacterName(profile.CharacterName))
|
||||
{
|
||||
armature.IsPendingProfileRebind = true;
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange profile {profile} toggled, planning rebind for armature {armature}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ProfileChanged.Type.TemporaryProfileAdded)
|
||||
{
|
||||
if (!profile.TemporaryActor.IsValid || !Armatures.ContainsKey(profile.TemporaryActor))
|
||||
return;
|
||||
|
||||
var armature = Armatures[profile.TemporaryActor];
|
||||
if (armature.Profile == profile)
|
||||
return;
|
||||
|
||||
armature.ProtectFromRemoval();
|
||||
|
||||
armature.IsPendingProfileRebind = true;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange TemporaryProfileAdded, calling rebind for existing armature: {type}, data payload: {arg3?.ToString()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == ProfileChanged.Type.ChangedCharacterName ||
|
||||
type == ProfileChanged.Type.Deleted ||
|
||||
type == ProfileChanged.Type.TemporaryProfileDeleted ||
|
||||
type == ProfileChanged.Type.LimitLookupToOwnedChanged)
|
||||
{
|
||||
if (profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var armature in profile.Armatures)
|
||||
{
|
||||
if (type == ProfileChanged.Type.TemporaryProfileDeleted)
|
||||
armature.ProtectFromRemoval(); //just to be safe
|
||||
|
||||
armature.IsPendingProfileRebind = true;
|
||||
}
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange CCN/DEL/TPD/LLTOC, armature rebind scheduled: {type}, data payload: {arg3?.ToString()?.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//todo: shouldn't happen, but happens sometimes? I think?
|
||||
if (profile.Armatures.Count == 0)
|
||||
return;
|
||||
|
||||
_logger.Debug($"ArmatureManager.OnProfileChange Added/Deleted/Moved/Changed template: {type}, data payload: {arg3?.ToString()}, profile: {profile.Name}->{profile.Enabled}->{profile.Armatures.Count} armatures");
|
||||
|
||||
profile!.Armatures.ForEach(x => x.RebuildBoneTemplateBinding());
|
||||
}
|
||||
|
||||
private IEnumerable<Armature> GetArmaturesForCharacterName(string characterName)
|
||||
{
|
||||
var actors = _gameObjectService.FindActorsByName(characterName).ToList();
|
||||
if (actors.Count == 0)
|
||||
yield break;
|
||||
|
||||
foreach (var actorData in actors)
|
||||
{
|
||||
if (!Armatures.TryGetValue(actorData.Item1, out var armature))
|
||||
continue;
|
||||
|
||||
yield return armature;
|
||||
}
|
||||
}
|
||||
}
|
||||
150
CustomizePlus/Configuration/Data/PluginConfiguration.cs
Normal file
150
CustomizePlus/Configuration/Data/PluginConfiguration.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Widgets;
|
||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Configuration.Services;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.UI.Windows;
|
||||
|
||||
namespace CustomizePlus.Configuration.Data;
|
||||
|
||||
[Serializable]
|
||||
public class PluginConfiguration : IPluginConfiguration, ISavable
|
||||
{
|
||||
public const int CurrentVersion = Constants.ConfigurationVersion;
|
||||
|
||||
public int Version { get; set; } = CurrentVersion;
|
||||
|
||||
public bool PluginEnabled { get; set; } = true;
|
||||
|
||||
public bool DebuggingModeEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the default profile applied to all characters without any profile. Can be set to Empty to disable this feature.
|
||||
/// </summary>
|
||||
public Guid DefaultProfile { get; set; } = Guid.Empty;
|
||||
|
||||
[Serializable]
|
||||
public class ChangelogSettingsEntries
|
||||
{
|
||||
public int LastSeenVersion { get; set; } = CPlusChangeLog.LastChangelogVersion;
|
||||
public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New;
|
||||
}
|
||||
|
||||
public ChangelogSettingsEntries ChangelogSettings { get; set; } = new();
|
||||
|
||||
[Serializable]
|
||||
public class UISettingsEntries
|
||||
{
|
||||
public DoubleModifier DeleteTemplateModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
|
||||
|
||||
public bool FoldersDefaultOpen { get; set; } = true;
|
||||
|
||||
public bool HideWindowInCutscene { get; set; } = true;
|
||||
|
||||
public bool IncognitoMode { get; set; } = false;
|
||||
|
||||
public List<string> ViewedMessageWindows { get; set; } = new();
|
||||
}
|
||||
|
||||
public UISettingsEntries UISettings { get; set; } = new();
|
||||
|
||||
[Serializable]
|
||||
public class EditorConfigurationEntries
|
||||
{
|
||||
/// <summary>
|
||||
/// Hides root position from the UI. DOES NOT DISABLE LOADING IT FROM THE CONFIG!
|
||||
/// </summary>
|
||||
public bool RootPositionEditingEnabled { get; set; } = false;
|
||||
|
||||
public bool ShowLiveBones { get; set; } = true;
|
||||
|
||||
public bool BoneMirroringEnabled { get; set; } = false;
|
||||
public bool LimitLookupToOwnedObjects { get; set; } = false;
|
||||
|
||||
public string? PreviewCharacterName { get; set; } = null;
|
||||
|
||||
public int EditorValuesPrecision { get; set; } = 3;
|
||||
public BoneAttribute EditorMode { get; set; } = BoneAttribute.Position;
|
||||
}
|
||||
|
||||
public EditorConfigurationEntries EditorConfiguration { get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly Logger _logger;
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly ChatService _chatService;
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly MessageService _messageService;
|
||||
|
||||
public PluginConfiguration(
|
||||
SaveService saveService,
|
||||
Logger logger,
|
||||
ChatService chatService,
|
||||
MessageService messageService,
|
||||
ConfigurationMigrator migrator)
|
||||
{
|
||||
_saveService = saveService;
|
||||
_logger = logger;
|
||||
_chatService = chatService;
|
||||
_messageService = messageService;
|
||||
|
||||
Load(migrator);
|
||||
}
|
||||
|
||||
public void Load(ConfigurationMigrator migrator)
|
||||
{
|
||||
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
|
||||
{
|
||||
Plugin.Logger.Error(
|
||||
$"Error parsing configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
|
||||
errorArgs.ErrorContext.Handled = true;
|
||||
}
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.ConfigFile))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(_saveService.FileNames.ConfigFile);
|
||||
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
|
||||
{
|
||||
Error = HandleDeserializationError,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messageService.NotificationMessage(ex,
|
||||
"Error reading configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/CustomizePlus directory.",
|
||||
"Error reading configuration", NotificationType.Error);
|
||||
}
|
||||
|
||||
migrator.Migrate(this);
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.ConfigFile;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(jWriter, this);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
=> _saveService.DelaySave(this);
|
||||
}
|
||||
60
CustomizePlus/Configuration/Data/Version3/V3BoneTransform.cs
Normal file
60
CustomizePlus/Configuration/Data/Version3/V3BoneTransform.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CustomizePlus.Configuration.Data.Version3;
|
||||
|
||||
public class V3BoneTransform
|
||||
{
|
||||
private Vector3 _translation;
|
||||
public Vector3 Translation
|
||||
{
|
||||
get => _translation;
|
||||
set => _translation = ClampVector(value);
|
||||
}
|
||||
|
||||
private Vector3 _rotation;
|
||||
public Vector3 Rotation
|
||||
{
|
||||
get => _rotation;
|
||||
set => _rotation = ClampAngles(value);
|
||||
}
|
||||
|
||||
private Vector3 _scaling;
|
||||
public Vector3 Scaling
|
||||
{
|
||||
get => _scaling;
|
||||
set => _scaling = ClampVector(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp all vector values to be within allowed limits.
|
||||
/// </summary>
|
||||
private Vector3 ClampVector(Vector3 vector)
|
||||
{
|
||||
return new Vector3
|
||||
{
|
||||
X = Math.Clamp(vector.X, -512, 512),
|
||||
Y = Math.Clamp(vector.Y, -512, 512),
|
||||
Z = Math.Clamp(vector.Z, -512, 512)
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector3 ClampAngles(Vector3 rotVec)
|
||||
{
|
||||
static float Clamp(float angle)
|
||||
{
|
||||
if (angle > 180)
|
||||
angle -= 360;
|
||||
else if (angle < -180)
|
||||
angle += 360;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
rotVec.X = Clamp(rotVec.X);
|
||||
rotVec.Y = Clamp(rotVec.Y);
|
||||
rotVec.Z = Clamp(rotVec.Z);
|
||||
|
||||
return rotVec;
|
||||
}
|
||||
}
|
||||
24
CustomizePlus/Configuration/Data/Version3/Version3Profile.cs
Normal file
24
CustomizePlus/Configuration/Data/Version3/Version3Profile.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using CustomizePlus.Core.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CustomizePlus.Configuration.Data.Version3;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates the user-controlled aspects of a character profile, ie all of
|
||||
/// the information that gets saved to disk by the plugin.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class Version3Profile
|
||||
{
|
||||
public string CharacterName { get; set; } = "Default";
|
||||
public string ProfileName { get; set; } = "Profile";
|
||||
public nint? Address { get; set; } = null;
|
||||
public bool OwnedOnly { get; set; } = false;
|
||||
public int ConfigVersion { get; set; } = Constants.ConfigurationVersion;
|
||||
public bool Enabled { get; set; }
|
||||
public DateTime CreationDate { get; set; } = DateTime.Now;
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
public Dictionary<string, V3BoneTransform> Bones { get; init; } = new();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using CustomizePlus.Configuration.Data.Version3;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CustomizePlus.Configuration.Helpers;
|
||||
|
||||
internal static class V3ProfileToV4Converter
|
||||
{
|
||||
public static (Profile, Template) Convert(Version3Profile v3Profile)
|
||||
{
|
||||
var profile = new Profile
|
||||
{
|
||||
Name = $"{v3Profile.ProfileName} - {v3Profile.CharacterName}",
|
||||
CharacterName = v3Profile.CharacterName,
|
||||
CreationDate = v3Profile.CreationDate,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
Enabled = v3Profile.Enabled,
|
||||
LimitLookupToOwnedObjects = v3Profile.OwnedOnly,
|
||||
UniqueId = Guid.NewGuid(),
|
||||
Templates = new List<Template>(1)
|
||||
};
|
||||
|
||||
var template = new Template
|
||||
{
|
||||
Name = $"{profile.Name}'s template",
|
||||
CreationDate = profile.CreationDate,
|
||||
ModifiedDate = profile.ModifiedDate,
|
||||
UniqueId = Guid.NewGuid(),
|
||||
Bones = new Dictionary<string, BoneTransform>(v3Profile.Bones.Count)
|
||||
};
|
||||
|
||||
foreach (var kvPair in v3Profile.Bones)
|
||||
template.Bones.Add(kvPair.Key,
|
||||
new BoneTransform { Translation = kvPair.Value.Translation, Rotation = kvPair.Value.Rotation, Scaling = kvPair.Value.Scaling });
|
||||
|
||||
profile.Templates.Add(template);
|
||||
|
||||
return (profile, template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using CustomizePlus.Configuration.Data.Version3;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CustomizePlus.Configuration.Helpers;
|
||||
|
||||
internal static class V4ProfileToV3Converter
|
||||
{
|
||||
public static Version3Profile Convert(Profile v4Profile)
|
||||
{
|
||||
var profile = new Version3Profile
|
||||
{
|
||||
ProfileName = v4Profile.Name,
|
||||
CharacterName = v4Profile.CharacterName,
|
||||
CreationDate = v4Profile.CreationDate.DateTime,
|
||||
ModifiedDate = DateTime.UtcNow,
|
||||
Enabled = v4Profile.Enabled,
|
||||
OwnedOnly = v4Profile.LimitLookupToOwnedObjects,
|
||||
ConfigVersion = 3,
|
||||
Bones = new Dictionary<string, V3BoneTransform>()
|
||||
};
|
||||
|
||||
foreach (var template in v4Profile.Templates)
|
||||
{
|
||||
foreach (var kvPair in template.Bones) //not super optimal but whatever
|
||||
{
|
||||
profile.Bones[kvPair.Key] = new V3BoneTransform
|
||||
{
|
||||
Translation = kvPair.Value.Translation,
|
||||
Rotation = kvPair.Value.Rotation,
|
||||
Scaling = kvPair.Value.Scaling
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
107
CustomizePlus/Configuration/Services/ConfigurationMigrator.cs
Normal file
107
CustomizePlus/Configuration/Services/ConfigurationMigrator.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Configuration.Helpers;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Core.Events;
|
||||
using CustomizePlus.Configuration.Data.Version3;
|
||||
|
||||
namespace CustomizePlus.Configuration.Services;
|
||||
|
||||
public class ConfigurationMigrator
|
||||
{
|
||||
private readonly SaveService _saveService;
|
||||
private readonly BackupService _backupService;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Logger _logger;
|
||||
private readonly ReloadEvent _reloadEvent;
|
||||
|
||||
public ConfigurationMigrator(
|
||||
SaveService saveService,
|
||||
BackupService backupService,
|
||||
MessageService messageService,
|
||||
Logger logger,
|
||||
ReloadEvent reloadEvent
|
||||
)
|
||||
{
|
||||
_saveService = saveService;
|
||||
_backupService = backupService;
|
||||
_messageService = messageService;
|
||||
_logger = logger;
|
||||
_reloadEvent = reloadEvent;
|
||||
}
|
||||
|
||||
public void Migrate(PluginConfiguration config)
|
||||
{
|
||||
var configVersion = config.Version;
|
||||
|
||||
if (configVersion >= Constants.ConfigurationVersion)
|
||||
return;
|
||||
|
||||
//V3 migration code
|
||||
if (configVersion < 3)
|
||||
{
|
||||
_messageService.NotificationMessage($"Unable to migrate your Customize+ configuration because it is too old. Manually install latest version of Customize+ 1.x to migrate your configuration to supported version first.", NotificationType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
MigrateV3ToV4();
|
||||
// /V3 migration code
|
||||
|
||||
//I'm sorry, I'm too lazy so v3's enable root position setting is not getting migrated for now
|
||||
//MigrateV3ToV4(configVersion);
|
||||
|
||||
config.Version = Constants.ConfigurationVersion;
|
||||
_saveService.ImmediateSave(config);
|
||||
}
|
||||
|
||||
private void MigrateV3ToV4()
|
||||
{
|
||||
_backupService.CreateV3Backup();
|
||||
|
||||
//I'm sorry, I'm too lazy so v3's enable root position setting is not getting migrated
|
||||
|
||||
var usedGuids = new HashSet<Guid>();
|
||||
foreach (var file in Directory.EnumerateFiles(_saveService.FileNames.ConfigDirectory, "*.profile", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
_logger.Debug($"Migrating v3 profile {file}");
|
||||
|
||||
var legacyProfile = JsonConvert.DeserializeObject<Version3Profile>(File.ReadAllText(file));
|
||||
if (legacyProfile == null)
|
||||
continue;
|
||||
|
||||
_logger.Debug($"v3 profile {file} loaded as {legacyProfile.ProfileName}");
|
||||
|
||||
(var profile, var template) = V3ProfileToV4Converter.Convert(legacyProfile);
|
||||
|
||||
//regenerate guids just to be safe
|
||||
do
|
||||
{
|
||||
profile.UniqueId = Guid.NewGuid();
|
||||
}
|
||||
while (profile.UniqueId == Guid.Empty || usedGuids.Contains(profile.UniqueId));
|
||||
usedGuids.Add(profile.UniqueId);
|
||||
|
||||
do
|
||||
{
|
||||
template.UniqueId = Guid.NewGuid();
|
||||
}
|
||||
while (template.UniqueId == Guid.Empty || usedGuids.Contains(template.UniqueId));
|
||||
usedGuids.Add(template.UniqueId);
|
||||
|
||||
_saveService.ImmediateSave(template);
|
||||
_saveService.ImmediateSave(profile);
|
||||
|
||||
_logger.Debug($"Migrated v3 profile {legacyProfile.ProfileName} to profile {profile.UniqueId} and template {template.UniqueId}");
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
_reloadEvent.Invoke(ReloadEvent.Type.ReloadAll);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using CustomizePlus.Core.Services;
|
||||
using OtterGui.Log;
|
||||
using System.IO;
|
||||
|
||||
namespace CustomizePlus.Configuration.Services.Temporary;
|
||||
|
||||
internal class FantasiaPlusConfigMover
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly FilenameService _filenameService;
|
||||
private readonly BackupService _backupService;
|
||||
|
||||
public FantasiaPlusConfigMover(
|
||||
BackupService backupService,
|
||||
Logger logger,
|
||||
FilenameService filenameService
|
||||
)
|
||||
{
|
||||
_backupService = backupService;
|
||||
_logger = logger;
|
||||
_filenameService = filenameService;
|
||||
}
|
||||
|
||||
public void MoveConfigsIfNeeded()
|
||||
{
|
||||
string fantasiaPlusConfig = _filenameService.ConfigFile.Replace("CustomizePlus.json", "FantasiaPlus.json");
|
||||
if (!File.Exists(_filenameService.ConfigFile.Replace("CustomizePlus.json", "FantasiaPlus.json")))
|
||||
return;
|
||||
|
||||
_logger.Information("Found FantasiaPlus configuration, moving it to CustomizePlus folders");
|
||||
if(File.Exists(_filenameService.ConfigFile) || Directory.Exists(_filenameService.ConfigDirectory))
|
||||
{
|
||||
_logger.Debug("Creating a backup of current c+ config");
|
||||
_backupService.CreateV3Backup("fantasia_plus_migration");
|
||||
}
|
||||
|
||||
_logger.Debug("Removing current c+ data");
|
||||
File.Delete(_filenameService.ConfigFile);
|
||||
Directory.Delete(_filenameService.ConfigDirectory, true);
|
||||
|
||||
_logger.Debug("Copying fantasia+ data");
|
||||
File.Copy(fantasiaPlusConfig, _filenameService.ConfigFile);
|
||||
|
||||
string fantasiaPlusDirectory = _filenameService.ConfigDirectory.Replace("CustomizePlus", "FantasiaPlus");
|
||||
CopyDirectory(fantasiaPlusDirectory, _filenameService.ConfigDirectory, true);
|
||||
|
||||
_logger.Debug("Deleting fantasia+ data");
|
||||
File.Delete(fantasiaPlusConfig);
|
||||
Directory.Delete(fantasiaPlusDirectory, true);
|
||||
|
||||
_logger.Information("Done moving fantasia+ configuration");
|
||||
}
|
||||
|
||||
static void CopyDirectory(string sourceDir, string destinationDir, bool recursive)
|
||||
{
|
||||
// Get information about the source directory
|
||||
var dir = new DirectoryInfo(sourceDir);
|
||||
|
||||
// Check if the source directory exists
|
||||
if (!dir.Exists)
|
||||
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
|
||||
|
||||
// Cache directories before we start copying
|
||||
DirectoryInfo[] dirs = dir.GetDirectories();
|
||||
|
||||
// Create the destination directory
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
// Get the files in the source directory and copy to the destination directory
|
||||
foreach (FileInfo file in dir.GetFiles())
|
||||
{
|
||||
string targetFilePath = Path.Combine(destinationDir, file.Name);
|
||||
file.CopyTo(targetFilePath);
|
||||
}
|
||||
|
||||
// If recursive and copying subdirectories, recursively call this method
|
||||
if (recursive)
|
||||
{
|
||||
foreach (DirectoryInfo subDir in dirs)
|
||||
{
|
||||
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
|
||||
CopyDirectory(subDir.FullName, newDestinationDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
570
CustomizePlus/Core/Data/BoneData.cs
Normal file
570
CustomizePlus/Core/Data/BoneData.cs
Normal file
@@ -0,0 +1,570 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace CustomizePlus.Core.Data;
|
||||
|
||||
public static class BoneData //todo: DI, do not show IVCS unless IVCS is installed/user enabled it, do not show weapon bones
|
||||
{
|
||||
public enum BoneFamily
|
||||
{
|
||||
Root,
|
||||
Spine,
|
||||
Hair,
|
||||
Face,
|
||||
Ears,
|
||||
Chest,
|
||||
Arms,
|
||||
Hands,
|
||||
Tail,
|
||||
Groin,
|
||||
Legs,
|
||||
Feet,
|
||||
Earrings,
|
||||
Hat,
|
||||
Cape,
|
||||
Armor,
|
||||
Skirt,
|
||||
Equipment,
|
||||
Unknown
|
||||
}
|
||||
|
||||
//TODO move the csv data to an external (compressed?) file
|
||||
private static readonly string[] BoneRawTable =
|
||||
{
|
||||
//Codename, Display Name, Bone Family, Parent (if any), Mirrored Bone (if any)
|
||||
"n_root,Root,Root,TRUE,FALSE,,", "n_hara,Abdomen,Root,TRUE,FALSE,,", "j_kao,Head,Spine,TRUE,FALSE,j_kubi,",
|
||||
"j_kubi,Neck,Spine,TRUE,FALSE,j_sebo_c,", "j_sebo_c,Spine C,Spine,TRUE,FALSE,j_sebo_b,",
|
||||
"j_sebo_b,Spine B,Spine,TRUE,FALSE,j_sebo_a,", "j_sebo_a,Spine A,Spine,TRUE,FALSE,j_kosi,",
|
||||
"j_kosi,Waist,Spine,TRUE,FALSE,,", "j_kami_a,Hair A,Hair,TRUE,FALSE,j_kao,",
|
||||
"j_kami_b,Hair B,Hair,TRUE,FALSE,j_kami_a,", "j_kami_f_l,Hair Front Left,Hair,TRUE,FALSE,j_kao,j_kami_f_r",
|
||||
"j_kami_f_r,Hair Front Right,Hair,TRUE,FALSE,j_kao,j_kami_f_l",
|
||||
"j_f_mayu_l,Brow Outer Left,Face,TRUE,FALSE,j_kao,j_f_mayu_r",
|
||||
"j_f_mayu_r,Brow Outer Right,Face,TRUE,FALSE,j_kao,j_f_mayu_l",
|
||||
"j_f_miken_l,Brow Inner Left,Face,TRUE,FALSE,j_kao,j_f_miken_r",
|
||||
"j_f_miken_r,Brow Inner Right,Face,TRUE,FALSE,j_kao,j_f_miken_l",
|
||||
"j_f_memoto,Bridge,Face,TRUE,FALSE,j_kao,", "j_f_umab_l,Eyelid Upper Left,Face,TRUE,FALSE,j_kao,j_f_umab_r",
|
||||
"j_f_umab_r,Eyelid Upper Right,Face,TRUE,FALSE,j_kao,j_f_umab_l",
|
||||
"j_f_dmab_l,Eyelid Lower Left,Face,TRUE,FALSE,j_kao,j_f_dmab_r",
|
||||
"j_f_dmab_r,Eyelid Lower Right,Face,TRUE,FALSE,j_kao,j_f_dmab_l",
|
||||
"j_f_eye_l,Eye Left,Face,TRUE,FALSE,j_kao,j_f_eye_r", "j_f_eye_r,Eye Right,Face,TRUE,FALSE,j_kao,j_f_eye_l",
|
||||
"j_f_hoho_l,Cheek Left,Face,TRUE,FALSE,j_kao,j_f_hoho_r",
|
||||
"j_f_hoho_r,Cheek Right,Face,TRUE,FALSE,j_kao,j_f_hoho_l",
|
||||
"j_f_hige_l,Hrothgar Whiskers Left,Face,FALSE,FALSE,j_kao,j_f_hige_r",
|
||||
"j_f_hige_r,Hrothgar Whiskers Right,Face,FALSE,FALSE,j_kao,j_f_hige_l",
|
||||
"j_f_hana,Nose,Face,TRUE,FALSE,j_kao,", "j_f_lip_l,Lips Left,Face,TRUE,FALSE,j_kao,j_f_lip_r",
|
||||
"j_f_lip_r,Lips Right,Face,TRUE,FALSE,j_kao,j_f_lip_l", "j_f_ulip_a,Lip Upper A,Face,TRUE,FALSE,j_kao,",
|
||||
"j_f_ulip_b,Lip Upper B,Face,TRUE,FALSE,j_kao,", "j_f_dlip_a,Lip Lower A,Face,TRUE,FALSE,j_kao,",
|
||||
"j_f_dlip_b,Lip Lower B,Face,TRUE,FALSE,j_kao,",
|
||||
"n_f_lip_l,Hrothgar Cheek Left,Face,FALSE,FALSE,j_kao,n_f_lip_r",
|
||||
"n_f_lip_r,Hrothgar Cheek Right,Face,FALSE,FALSE,j_kao,n_f_lip_l",
|
||||
"n_f_ulip_l,Hrothgar Lip Upper Left,Face,FALSE,FALSE,j_kao,n_f_ulip_r",
|
||||
"n_f_ulip_r,Hrothgar Lip Upper Right,Face,FALSE,FALSE,j_kao,n_f_ulip_l",
|
||||
"j_f_dlip,Hrothgar Lip Lower,Face,FALSE,FALSE,j_kao,", "j_ago,Jaw,Face,TRUE,FALSE,j_kao,",
|
||||
"j_f_uago,Hrothgar Palate Upper,Face,FALSE,FALSE,j_kao,",
|
||||
"j_f_ulip,Hrothgar Palate Lower,Face,FALSE,FALSE,j_kao,",
|
||||
"j_mimi_l,Ear Left,Ears,TRUE,FALSE,j_kao,j_mimi_r", "j_mimi_r,Ear Right,Ears,TRUE,FALSE,j_kao,j_mimi_l",
|
||||
"j_zera_a_l,Viera Ear 01 A Left,Ears,FALSE,FALSE,j_kao,j_zera_a_r",
|
||||
"j_zera_a_r,Viera Ear 01 A Right,Ears,FALSE,FALSE,j_kao,j_zera_a_l",
|
||||
"j_zera_b_l,Viera Ear 01 B Left,Ears,FALSE,FALSE,j_kao,j_zera_b_r",
|
||||
"j_zera_b_r,Viera Ear 01 B Right,Ears,FALSE,FALSE,j_kao,j_zera_b_l",
|
||||
"j_zerb_a_l,Viera Ear 02 A Left,Ears,FALSE,FALSE,j_kao,j_zerb_a_r",
|
||||
"j_zerb_a_r,Viera Ear 02 A Right,Ears,FALSE,FALSE,j_kao,j_zerb_a_l",
|
||||
"j_zerb_b_l,Viera Ear 02 B Left,Ears,FALSE,FALSE,j_kao,j_zerb_b_r",
|
||||
"j_zerb_b_r,Viera Ear 02 B Right,Ears,FALSE,FALSE,j_kao,j_zerb_b_l",
|
||||
"j_zerc_a_l,Viera Ear 03 A Left,Ears,FALSE,FALSE,j_kao,j_zerc_a_r",
|
||||
"j_zerc_a_r,Viera Ear 03 A Right,Ears,FALSE,FALSE,j_kao,j_zerc_a_l",
|
||||
"j_zerc_b_l,Viera Ear 03 B Left,Ears,FALSE,FALSE,j_kao,j_zerc_b_r",
|
||||
"j_zerc_b_r,Viera Ear 03 B Right,Ears,FALSE,FALSE,j_kao,j_zerc_b_l",
|
||||
"j_zerd_a_l,Viera Ear 04 A Left,Ears,FALSE,FALSE,j_kao,j_zerd_a_r",
|
||||
"j_zerd_a_r,Viera Ear 04 A Right,Ears,FALSE,FALSE,j_kao,j_zerd_a_l",
|
||||
"j_zerd_b_l,Viera Ear 04 B Left,Ears,FALSE,FALSE,j_kao,j_zerd_b_r",
|
||||
"j_zerd_b_r,Viera Ear 04 B Right,Ears,FALSE,FALSE,j_kao,j_zerd_b_l",
|
||||
"j_sako_l,Clavicle Left,Chest,TRUE,FALSE,j_sebo_c,j_sako_r",
|
||||
"j_sako_r,Clavicle Right,Chest,TRUE,FALSE,j_sebo_c,j_sako_l",
|
||||
"j_mune_l,Breast Left,Chest,TRUE,FALSE,j_sebo_b,j_mune_r",
|
||||
"j_mune_r,Breast Right,Chest,TRUE,FALSE,j_sebo_b,j_mune_l",
|
||||
"iv_c_mune_l,Breast B Left,Chest,FALSE,TRUE,j_mune_l,iv_c_mune_r",
|
||||
"iv_c_mune_r,Breast B Right,Chest,FALSE,TRUE,j_mune_r,iv_c_mune_l",
|
||||
"n_hkata_l,Shoulder Left,Arms,TRUE,FALSE,j_ude_a_l,n_hkata_r",
|
||||
"n_hkata_r,Shoulder Right,Arms,TRUE,FALSE,j_ude_a_r,n_hkata_l",
|
||||
"j_ude_a_l,Arm Left,Arms,TRUE,FALSE,j_sako_l,j_ude_a_r",
|
||||
"j_ude_a_r,Arm Right,Arms,TRUE,FALSE,j_sako_r,j_ude_a_l",
|
||||
"iv_nitoukin_l,Bicep Left,Arms,FALSE,TRUE,j_ude_a_l,iv_nitoukin_r",
|
||||
"iv_nitoukin_r,Bicep Right,Arms,FALSE,TRUE,j_ude_a_r,iv_nitoukin_l",
|
||||
"n_hhiji_l,Elbow Left,Arms,TRUE,FALSE,j_ude_b_l,n_hhiji_r",
|
||||
"n_hhiji_r,Elbow Right,Arms,TRUE,FALSE,j_ude_b_r,n_hhiji_l",
|
||||
"j_ude_b_l,Forearm Left,Arms,TRUE,FALSE,j_ude_a_l,j_ude_b_r",
|
||||
"j_ude_b_r,Forearm Right,Arms,TRUE,FALSE,j_ude_a_r,j_ude_b_l",
|
||||
"n_hte_l,Wrist Left,Arms,TRUE,FALSE,j_ude_b_l,n_hte_r",
|
||||
"n_hte_r,Wrist Right,Arms,TRUE,FALSE,j_ude_b_r,n_hte_l", "j_te_l,Hand Left,Hands,TRUE,FALSE,n_hte_l,j_te_r",
|
||||
"j_te_r,Hand Right,Hands,TRUE,FALSE,n_hte_r,j_te_l",
|
||||
"j_oya_a_l,Thumb A Left,Hands,TRUE,FALSE,j_te_l,j_oya_a_r",
|
||||
"j_oya_a_r,Thumb A Right,Hands,TRUE,FALSE,j_te_r,j_oya_a_l",
|
||||
"j_oya_b_l,Thumb B Left,Hands,TRUE,FALSE,j_oya_a_l,j_oya_b_r",
|
||||
"j_oya_b_r,Thumb B Right,Hands,TRUE,FALSE,j_oya_a_r,j_oya_b_l",
|
||||
"j_hito_a_l,Index A Left,Hands,TRUE,FALSE,j_te_l,j_hito_a_r",
|
||||
"j_hito_a_r,Index A Right,Hands,TRUE,FALSE,j_te_r,j_hito_a_l",
|
||||
"j_hito_b_l,Index B Left,Hands,TRUE,FALSE,j_hito_a_l,j_hito_b_r",
|
||||
"j_hito_b_r,Index B Right,Hands,TRUE,FALSE,j_hito_a_r,j_hito_b_l",
|
||||
"j_naka_a_l,Middle A Left,Hands,TRUE,FALSE,j_te_l,j_naka_a_r",
|
||||
"j_naka_a_r,Middle A Right,Hands,TRUE,FALSE,j_te_r,j_naka_a_l",
|
||||
"j_naka_b_l,Middle B Left,Hands,TRUE,FALSE,j_naka_a_l,j_naka_b_r",
|
||||
"j_naka_b_r,Middle B Right,Hands,TRUE,FALSE,j_naka_a_r,j_naka_b_l",
|
||||
"j_kusu_a_l,Ring A Left,Hands,TRUE,FALSE,j_te_l,j_kusu_a_r",
|
||||
"j_kusu_a_r,Ring A Right,Hands,TRUE,FALSE,j_te_r,j_kusu_a_l",
|
||||
"j_kusu_b_l,Ring B Left,Hands,TRUE,FALSE,j_kusu_a_l,j_kusu_b_r",
|
||||
"j_kusu_b_r,Ring B Right,Hands,TRUE,FALSE,j_kusu_a_r,j_kusu_b_l",
|
||||
"j_ko_a_l,Pinky A Left,Hands,TRUE,FALSE,j_te_l,j_ko_a_r",
|
||||
"j_ko_a_r,Pinky A Right,Hands,TRUE,FALSE,j_te_r,j_ko_a_l",
|
||||
"j_ko_b_l,Pinky B Left,Hands,TRUE,FALSE,j_ko_a_l,j_ko_b_r",
|
||||
"j_ko_b_r,Pinky B Right,Hands,TRUE,FALSE,j_ko_a_r,j_ko_b_l",
|
||||
"iv_hito_c_l,Index C Left,Hands,FALSE,TRUE,j_hito_b_l,iv_hito_c_r",
|
||||
"iv_hito_c_r,Index C Right,Hands,FALSE,TRUE,j_hito_b_r,iv_hito_c_l",
|
||||
"iv_naka_c_l,Middle C Left,Hands,FALSE,TRUE,j_naka_b_l,iv_naka_c_r",
|
||||
"iv_naka_c_r,Middle C Right,Hands,FALSE,TRUE,j_naka_b_r,iv_naka_c_l",
|
||||
"iv_kusu_c_l,Ring C Left,Hands,FALSE,TRUE,j_kusu_b_l,iv_kusu_c_r",
|
||||
"iv_kusu_c_r,Ring C Right,Hands,FALSE,TRUE,j_kusu_b_r,iv_kusu_c_l",
|
||||
"iv_ko_c_l,Pinky C Left,Hands,FALSE,TRUE,j_ko_b_l,iv_ko_c_r",
|
||||
"iv_ko_c_r,Pinky C Right,Hands,FALSE,TRUE,j_ko_b_r,iv_ko_c_l", "n_sippo_a,Tail A,Tail,FALSE,FALSE,j_kosi,",
|
||||
"n_sippo_b,Tail B,Tail,FALSE,FALSE,n_sippo_a,", "n_sippo_c,Tail C,Tail,FALSE,FALSE,n_sippo_b,",
|
||||
"n_sippo_d,Tail D,Tail,FALSE,FALSE,n_sippo_c,", "n_sippo_e,Tail E,Tail,FALSE,FALSE,n_sippo_d,",
|
||||
"iv_shiri_l,Buttock Left,Groin,FALSE,TRUE,j_kosi,iv_shiri_r",
|
||||
"iv_shiri_r,Buttock Right,Groin,FALSE,TRUE,j_kosi,iv_shiri_l",
|
||||
"iv_kougan_l,Scrotum Left,Groin,FALSE,TRUE,iv_ochinko_a,iv_kougan_r",
|
||||
"iv_kougan_r,Scrotum Right,Groin,FALSE,TRUE,iv_ochinko_a,iv_kougan_l",
|
||||
"iv_ochinko_a,Penis A,Groin,FALSE,TRUE,j_kosi,", "iv_ochinko_b,Penis B,Groin,FALSE,TRUE,iv_ochinko_a,",
|
||||
"iv_ochinko_c,Penis C,Groin,FALSE,TRUE,iv_ochinko_b,",
|
||||
"iv_ochinko_d,Penis D,Groin,FALSE,TRUE,iv_ochinko_c,",
|
||||
"iv_ochinko_e,Penis E,Groin,FALSE,TRUE,iv_ochinko_d,",
|
||||
"iv_ochinko_f,Penis F,Groin,FALSE,TRUE,iv_ochinko_e,", "iv_omanko,Vagina,Groin,FALSE,TRUE,j_kosi,",
|
||||
"iv_kuritto,Clitoris,Groin,FALSE,TRUE,iv_omanko,",
|
||||
"iv_inshin_l,Labia Left,Groin,FALSE,TRUE,iv_omanko,iv_inshin_r",
|
||||
"iv_inshin_r,Labia Right,Groin,FALSE,TRUE,iv_omanko,iv_inshin_l", "iv_koumon,Anus,Groin,FALSE,TRUE,j_kosi,",
|
||||
"iv_koumon_l,Anus B Right,Groin,FALSE,TRUE,iv_koumon,iv_koumon_r",
|
||||
"iv_koumon_r,Anus B Left,Groin,FALSE,TRUE,iv_koumon,iv_koumon_l",
|
||||
"j_asi_a_l,Leg Left,Legs,TRUE,FALSE,j_kosi,j_asi_a_r",
|
||||
"j_asi_a_r,Leg Right,Legs,TRUE,FALSE,j_kosi,j_asi_a_l",
|
||||
"j_asi_b_l,Knee Left,Legs,TRUE,FALSE,j_asi_a_l,j_asi_b_r",
|
||||
"j_asi_b_r,Knee Right,Legs,TRUE,FALSE,j_asi_a_r,j_asi_b_l",
|
||||
"j_asi_c_l,Calf Left,Legs,TRUE,FALSE,j_asi_b_l,j_asi_c_r",
|
||||
"j_asi_c_r,Calf Right,Legs,TRUE,FALSE,j_asi_b_r,j_asi_c_l",
|
||||
"j_asi_d_l,Foot Left,Feet,TRUE,FALSE,j_asi_c_l,j_asi_d_r",
|
||||
"j_asi_d_r,Foot Right,Feet,TRUE,FALSE,j_asi_c_r,j_asi_d_l",
|
||||
"j_asi_e_l,Toes Left,Feet,TRUE,FALSE,j_asi_d_l,j_asi_e_r",
|
||||
"j_asi_e_r,Toes Right,Feet,TRUE,FALSE,j_asi_d_r,j_asi_e_l",
|
||||
"iv_asi_oya_a_l,Big Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_oya_a_r",
|
||||
"iv_asi_oya_a_r,Big Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_oya_a_l",
|
||||
"iv_asi_oya_b_l,Big Toe B Left,Feet,FALSE,TRUE,j_asi_oya_a_l,iv_asi_oya_b_r",
|
||||
"iv_asi_oya_b_r,Big Toe B Right,Feet,FALSE,TRUE,j_asi_oya_a_r,iv_asi_oya_b_l",
|
||||
"iv_asi_hito_a_l,Index Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_hito_a_r",
|
||||
"iv_asi_hito_a_r,Index Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_hito_a_l",
|
||||
"iv_asi_hito_b_l,Index Toe B Left,Feet,FALSE,TRUE,j_asi_hito_a_l,iv_asi_hito_b_r",
|
||||
"iv_asi_hito_b_r,Index Toe B Right,Feet,FALSE,TRUE,j_asi_hito_a_r,iv_asi_hito_b_l",
|
||||
"iv_asi_naka_a_l,Middle Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_naka_a_r",
|
||||
"iv_asi_naka_a_r,Middle Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_naka_a_l",
|
||||
"iv_asi_naka_b_l,Middle Toe B Left,Feet,FALSE,TRUE,j_asi_naka_b_l,iv_asi_naka_b_r",
|
||||
"iv_asi_naka_b_r,Middle Toe B Right,Feet,FALSE,TRUE,j_asi_naka_b_r,iv_asi_naka_b_l",
|
||||
"iv_asi_kusu_a_l,Fore Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_kusu_a_r",
|
||||
"iv_asi_kusu_a_r,Fore Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_kusu_a_l",
|
||||
"iv_asi_kusu_b_l,Fore Toe B Left,Feet,FALSE,TRUE,j_asi_kusu_a_l,iv_asi_kusu_b_r",
|
||||
"iv_asi_kusu_b_r,Fore Toe B Right,Feet,FALSE,TRUE,j_asi_kusu_a_r,iv_asi_kusu_b_l",
|
||||
"iv_asi_ko_a_l,Pinky Toe A Left,Feet,FALSE,TRUE,j_asi_e_l,iv_asi_ko_a_r",
|
||||
"iv_asi_ko_a_r,Pinky Toe A Right,Feet,FALSE,TRUE,j_asi_e_r,iv_asi_ko_a_l",
|
||||
"iv_asi_ko_b_l,Pinky Toe B Left,Feet,FALSE,TRUE,j_asi_ko_a_l,iv_asi_ko_b_r",
|
||||
"iv_asi_ko_b_r,Pinky Toe B Right,Feet,FALSE,TRUE,j_asi_ko_a_r,iv_asi_ko_b_l",
|
||||
"j_ex_met_va,Visor,Hat,FALSE,FALSE,j_kao,", "j_ex_met_a,Hat Accessory A,Hat,FALSE,FALSE,j_kao,",
|
||||
"j_ex_met_b,Hat Accessory B,Hat,FALSE,FALSE,j_kao,",
|
||||
"n_ear_b_l,Earring B Left,Earrings,FALSE,FALSE,n_ear_a_l,n_ear_b_r",
|
||||
"n_ear_b_r,Earring B Right,Earrings,FALSE,FALSE,n_ear_a_r,n_ear_b_l",
|
||||
"n_ear_a_l,Earring A Left,Earrings,FALSE,FALSE,j_kao,n_ear_a_r",
|
||||
"n_ear_a_r,Earring A Right,Earrings,FALSE,FALSE,j_kao,n_ear_a_l",
|
||||
"j_ex_top_a_l,Cape A Left,Cape,FALSE,FALSE,j_sebo_c,j_ex_top_a_r",
|
||||
"j_ex_top_a_r,Cape A Right,Cape,FALSE,FALSE,j_sebo_c,j_ex_top_a_l",
|
||||
"j_ex_top_b_l,Cape B Left,Cape,FALSE,FALSE,j_ex_top_a_l,j_ex_top_b_r",
|
||||
"j_ex_top_b_r,Cape B Right,Cape,FALSE,FALSE,j_ex_top_a_r,j_ex_top_b_l",
|
||||
"n_kataarmor_l,Pauldron Left,Armor,FALSE,FALSE,n_hkata_l,n_kataarmor_r",
|
||||
"n_kataarmor_r,Pauldron Right,Armor,FALSE,FALSE,n_hkata_r,n_kataarmor_l",
|
||||
"n_hijisoubi_l,Elbow Plate Left,Armor,FALSE,FALSE,n_hhiji_l,n_hijisoubi_r",
|
||||
"n_hijisoubi_r,Elbow Plate Right,Armor,FALSE,FALSE,n_hhiji_r,n_hijisoubi_l",
|
||||
"n_hizasoubi_l,Knee Plate Left,Armor,FALSE,FALSE,j_asi_b_l,n_hizasoubi_r",
|
||||
"n_hizasoubi_r,Knee Plate Right,Armor,FALSE,FALSE,j_asi_b_r,n_hizasoubi_l",
|
||||
"j_sk_b_a_l,Skirt Back A Left,Skirt,FALSE,FALSE,j_kosi,j_sk_b_a_r",
|
||||
"j_sk_b_a_r,Skirt Back A Right,Skirt,FALSE,FALSE,j_kosi,j_sk_b_a_l",
|
||||
"j_sk_b_b_l,Skirt Back B Left,Skirt,FALSE,FALSE,j_sk_b_a_l,j_sk_b_b_r",
|
||||
"j_sk_b_b_r,Skirt Back B Right,Skirt,FALSE,FALSE,j_sk_b_a_r,j_sk_b_b_l",
|
||||
"j_sk_b_c_l,Skirt Back C Left,Skirt,FALSE,FALSE,j_sk_b_b_l,j_sk_b_c_r",
|
||||
"j_sk_b_c_r,Skirt Back C Right,Skirt,FALSE,FALSE,j_sk_b_b_r,j_sk_b_c_l",
|
||||
"j_sk_f_a_l,Skirt Front A Left,Skirt,FALSE,FALSE,j_kosi,j_sk_f_a_r",
|
||||
"j_sk_f_a_r,Skirt Front A Right,Skirt,FALSE,FALSE,j_kosi,j_sk_f_a_l",
|
||||
"j_sk_f_b_l,Skirt Front B Left,Skirt,FALSE,FALSE,j_sk_f_a_l,j_sk_f_b_r",
|
||||
"j_sk_f_b_r,Skirt Front B Right,Skirt,FALSE,FALSE,j_sk_f_a_r,j_sk_f_b_l",
|
||||
"j_sk_f_c_l,Skirt Front C Left,Skirt,FALSE,FALSE,j_sk_f_b_l,j_sk_f_c_r",
|
||||
"j_sk_f_c_r,Skirt Front C Right,Skirt,FALSE,FALSE,j_sk_f_b_r,j_sk_f_c_l",
|
||||
"j_sk_s_a_l,Skirt Side A Left,Skirt,FALSE,FALSE,j_kosi,j_sk_s_a_r",
|
||||
"j_sk_s_a_r,Skirt Side A Right,Skirt,FALSE,FALSE,j_kosi,j_sk_s_a_l",
|
||||
"j_sk_s_b_l,Skirt Side B Left,Skirt,FALSE,FALSE,j_sk_s_a_l,j_sk_s_b_r",
|
||||
"j_sk_s_b_r,Skirt Side B Right,Skirt,FALSE,FALSE,j_sk_s_a_r,j_sk_s_b_l",
|
||||
"j_sk_s_c_l,Skirt Side C Left,Skirt,FALSE,FALSE,j_sk_s_b_l,j_sk_s_c_r",
|
||||
"j_sk_s_c_r,Skirt Side C Right,Skirt,FALSE,FALSE,j_sk_s_b_r,j_sk_s_c_l",
|
||||
"n_throw,Throw,Root,FALSE,FALSE,j_kosi,",
|
||||
"j_buki_sebo_l,Scabbard Left,Equipment,FALSE,FALSE,j_kosi,j_buki_sebo_r",
|
||||
"j_buki_sebo_r,Scabbard Right,Equipment,FALSE,FALSE,j_kosi,j_buki_sebo_l",
|
||||
"j_buki2_kosi_l,Holster Left,Equipment,FALSE,FALSE,j_kosi,j_buki2_kosi_r",
|
||||
"j_buki2_kosi_r,Holster Right,Equipment,FALSE,FALSE,j_kosi,j_buki2_kosi_l",
|
||||
"j_buki_kosi_l,Sheath Left,Equipment,FALSE,FALSE,j_kosi,j_buki_kosi_r",
|
||||
"j_buki_kosi_r,Sheath Right,Equipment,FALSE,FALSE,j_kosi,j_buki_kosi_l",
|
||||
"n_buki_tate_l,Shield Left,Equipment,FALSE,FALSE,n_hte_l,n_buki_tate_r",
|
||||
"n_buki_tate_r,Shield Right,Equipment,FALSE,FALSE,n_hte_r,n_buki_tate_l",
|
||||
"n_buki_l,Weapon Left,Equipment,FALSE,FALSE,j_te_l,n_buki_r",
|
||||
"n_buki_r,Weapon Right,Equipment,FALSE,FALSE,j_te_r,n_buki_l"
|
||||
};
|
||||
|
||||
public static readonly Dictionary<BoneFamily, string?> DisplayableFamilies = new()
|
||||
{
|
||||
{ BoneFamily.Spine, null },
|
||||
{ BoneFamily.Hair, null },
|
||||
{ BoneFamily.Face, null },
|
||||
{ BoneFamily.Ears, null },
|
||||
{ BoneFamily.Chest, null },
|
||||
{ BoneFamily.Arms, null },
|
||||
{ BoneFamily.Hands, null },
|
||||
{ BoneFamily.Tail, null },
|
||||
{ BoneFamily.Groin, "NSFW IVCS Bones" },
|
||||
{ BoneFamily.Legs, null },
|
||||
{ BoneFamily.Feet, null },
|
||||
{ BoneFamily.Earrings, "Some mods utilize these bones for their physics properties" },
|
||||
{ BoneFamily.Hat, null },
|
||||
{ BoneFamily.Cape, "Some mods utilize these bones for their physics properties" },
|
||||
{ BoneFamily.Armor, null },
|
||||
{ BoneFamily.Skirt, null },
|
||||
{ BoneFamily.Equipment, "These may behave oddly" },
|
||||
{
|
||||
BoneFamily.Unknown,
|
||||
"These bones weren't immediately identifiable.\nIf you can figure out what they're for, let us know and we'll add them to the table."
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, BoneDatum> BoneTable = new();
|
||||
|
||||
private static readonly Dictionary<string, string> BoneLookupByDispName = new();
|
||||
|
||||
static BoneData()
|
||||
{
|
||||
//apparently static constructors are only guaranteed to START before the class is called
|
||||
//which can apparently lead to race conditions, as I've found out
|
||||
//this lock is to make sure the table is fully initialized before anything else can try to look at it
|
||||
lock (BoneTable)
|
||||
{
|
||||
var rowIndex = 0;
|
||||
foreach (var entry in BoneRawTable)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cells = entry.Split(',');
|
||||
var codename = cells[0];
|
||||
var dispName = cells[1];
|
||||
|
||||
BoneTable[codename] = new BoneDatum(rowIndex, cells);
|
||||
BoneLookupByDispName[dispName] = codename;
|
||||
|
||||
if (BoneTable[codename].Family == BoneFamily.Unknown)
|
||||
{
|
||||
throw new Exception("what the fuck?");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidCastException($"Couldn't parse raw bone table @ row {rowIndex}");
|
||||
}
|
||||
|
||||
++rowIndex;
|
||||
}
|
||||
|
||||
//iterate through the complete collection and assign children to their parents
|
||||
foreach (var kvp in BoneTable)
|
||||
{
|
||||
var datum = BoneTable[kvp.Key];
|
||||
|
||||
datum.Children = BoneTable.Where(x => x.Value.Parent == kvp.Key).Select(x => x.Key).ToArray();
|
||||
|
||||
BoneTable[kvp.Key] = datum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogNewBones(params string[] boneNames)
|
||||
{
|
||||
var probablyHairstyleBones = boneNames.Where(IsProbablyHairstyle).ToArray();
|
||||
|
||||
foreach (var hairBone in ParseHairstyle(probablyHairstyleBones))
|
||||
{
|
||||
BoneTable[hairBone.Codename] = hairBone;
|
||||
}
|
||||
|
||||
foreach (var boneName in boneNames.Except(BoneTable.Keys))
|
||||
{
|
||||
var newBone = new BoneDatum
|
||||
{
|
||||
RowIndex = -1,
|
||||
Codename = boneName,
|
||||
DisplayName = $"Unknown ({boneName})",
|
||||
Family = BoneFamily.Unknown,
|
||||
Parent = "j_kosi",
|
||||
Children = Array.Empty<string>(),
|
||||
MirroredCodename = null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateParentage(string parentName, string childName)
|
||||
{
|
||||
var child = BoneTable[childName];
|
||||
var parent = BoneTable[parentName];
|
||||
|
||||
child.Parent = parentName;
|
||||
parent.Children = parent.Children.Append(childName).Distinct().ToArray();
|
||||
|
||||
BoneTable[childName] = child;
|
||||
BoneTable[parentName] = parent;
|
||||
}
|
||||
|
||||
public static string GetBoneDisplayName(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.DisplayName : codename;
|
||||
}
|
||||
|
||||
public static string? GetBoneCodename(string boneDisplayName)
|
||||
{
|
||||
return BoneLookupByDispName.TryGetValue(boneDisplayName, out var name) ? name : null;
|
||||
}
|
||||
|
||||
public static List<string> GetBoneCodenames()
|
||||
{
|
||||
return BoneTable.Keys.ToList();
|
||||
}
|
||||
|
||||
public static List<string> GetBoneCodenames(Func<BoneDatum, bool> predicate)
|
||||
{
|
||||
return BoneTable.Where(x => predicate(x.Value)).Select(x => x.Key).ToList();
|
||||
}
|
||||
|
||||
public static List<string> GetBoneDisplayNames()
|
||||
{
|
||||
return BoneLookupByDispName.Keys.ToList();
|
||||
}
|
||||
|
||||
public static BoneFamily GetBoneFamily(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.Family : BoneFamily.Unknown;
|
||||
}
|
||||
|
||||
public static bool IsDefaultBone(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) && row.IsDefault;
|
||||
}
|
||||
|
||||
public static int GetBoneRanking(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.RowIndex : 0;
|
||||
}
|
||||
|
||||
public static bool IsIVCSBone(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) && row.IsIVCS;
|
||||
}
|
||||
|
||||
public static string? GetBoneMirror(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.MirroredCodename : null;
|
||||
}
|
||||
|
||||
public static string[] GetChildren(string codename)
|
||||
{
|
||||
return BoneTable.TryGetValue(codename, out var row) ? row.Children : Array.Empty<string>();
|
||||
}
|
||||
|
||||
public static bool IsProbablyHairstyle(string codename)
|
||||
{
|
||||
return Regex.IsMatch(codename, @"j_ex_h\d\d\d\d_ke_[abcdeflrsu](_[abcdeflrsu])?");
|
||||
}
|
||||
|
||||
public static bool IsNewBone(string codename)
|
||||
{
|
||||
return !BoneTable.ContainsKey(codename);
|
||||
}
|
||||
|
||||
private static BoneFamily ParseFamilyName(string n)
|
||||
{
|
||||
var simplified = n.Split(' ').FirstOrDefault()?.ToLower() ?? string.Empty;
|
||||
|
||||
var fam = simplified switch
|
||||
{
|
||||
"root" => BoneFamily.Root,
|
||||
"spine" => BoneFamily.Spine,
|
||||
"hair" => BoneFamily.Hair,
|
||||
"face" => BoneFamily.Face,
|
||||
"ears" => BoneFamily.Ears,
|
||||
"chest" => BoneFamily.Chest,
|
||||
"arms" => BoneFamily.Arms,
|
||||
"hands" => BoneFamily.Hands,
|
||||
"tail" => BoneFamily.Tail,
|
||||
"groin" => BoneFamily.Groin,
|
||||
"legs" => BoneFamily.Legs,
|
||||
"feet" => BoneFamily.Feet,
|
||||
"earrings" => BoneFamily.Earrings,
|
||||
"hat" => BoneFamily.Hat,
|
||||
"cape" => BoneFamily.Cape,
|
||||
"armor" => BoneFamily.Armor,
|
||||
"skirt" => BoneFamily.Skirt,
|
||||
"equipment" => BoneFamily.Equipment,
|
||||
_ => BoneFamily.Unknown
|
||||
};
|
||||
|
||||
return fam;
|
||||
}
|
||||
|
||||
public struct BoneDatum : IComparable<BoneDatum>
|
||||
{
|
||||
public int RowIndex;
|
||||
|
||||
public string Codename;
|
||||
public string DisplayName;
|
||||
public BoneFamily Family;
|
||||
|
||||
public bool IsDefault;
|
||||
public bool IsIVCS;
|
||||
|
||||
public string? Parent;
|
||||
public string? MirroredCodename;
|
||||
|
||||
public string[] Children;
|
||||
|
||||
public BoneDatum(int row, string[] fields)
|
||||
{
|
||||
RowIndex = row;
|
||||
|
||||
var i = 0;
|
||||
|
||||
Codename = fields[i++];
|
||||
DisplayName = fields[i++];
|
||||
|
||||
Family = ParseFamilyName(fields[i++]);
|
||||
|
||||
IsDefault = bool.Parse(fields[i++]);
|
||||
IsIVCS = bool.Parse(fields[i++]);
|
||||
|
||||
Parent = fields[i].IsNullOrEmpty() ? null : fields[i];
|
||||
i++;
|
||||
MirroredCodename = fields[i].IsNullOrEmpty() ? null : fields[i];
|
||||
i++;
|
||||
|
||||
Children = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public int CompareTo(BoneDatum other)
|
||||
{
|
||||
return RowIndex != other.RowIndex
|
||||
? RowIndex.CompareTo(other.RowIndex)
|
||||
: string.Compare(DisplayName, other.DisplayName, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
#region hair stuff
|
||||
|
||||
private static IEnumerable<BoneDatum> ParseHairstyle(params string[] boneNames)
|
||||
{
|
||||
List<BoneDatum> output = new();
|
||||
|
||||
var index = 0;
|
||||
foreach (var style in boneNames.GroupBy(x => Regex.Match(x, @"\d\d\d\d").Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
var parsedBones = style.Select(ParseHairBone).ToArray();
|
||||
|
||||
// if any of the first subs is nonstandard letter, we can presume that any bcd... are part of a rising sequence
|
||||
var firstAsc =
|
||||
parsedBones.Any(x => x.sub1 is "a" or "c" or "d" or "e");
|
||||
//and we can then presume that the second subs are directional
|
||||
//or vice versa. the naming conventions aren't really consistent about whether the sequence is first or second
|
||||
|
||||
foreach (var boneInfo in parsedBones)
|
||||
{
|
||||
StringBuilder dispName = new();
|
||||
dispName.Append($"Hair #{boneInfo.id}");
|
||||
|
||||
var sub1 = GetHairBoneSubLabel(boneInfo.sub1, firstAsc);
|
||||
var sub2 = boneInfo.sub2 == null ? null : GetHairBoneSubLabel(boneInfo.sub2, !firstAsc);
|
||||
|
||||
dispName.Append($" {sub1}");
|
||||
if (sub2 != null)
|
||||
{
|
||||
dispName.Append($" {sub2}");
|
||||
}
|
||||
|
||||
var result = new BoneDatum
|
||||
{
|
||||
RowIndex = -1,
|
||||
Codename = boneInfo.name,
|
||||
DisplayName = dispName.ToString(),
|
||||
Family = BoneFamily.Hair,
|
||||
IsDefault = false,
|
||||
IsIVCS = false,
|
||||
Parent = "j_kao",
|
||||
Children = Array.Empty<string>(),
|
||||
MirroredCodename = null
|
||||
};
|
||||
|
||||
output.Add(result);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Logger.Error($"Failed to dynamically parse bones for hairstyle of '{boneNames[index]}'");
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static (string name, int id, string sub1, string? sub2) ParseHairBone(string boneName)
|
||||
{
|
||||
var groups = Regex.Match(boneName.ToLower(), @"j_ex_h(\d\d\d\d)_ke_([abcdeflrsu])(?:_([abcdeflrsu]))?")
|
||||
.Groups;
|
||||
|
||||
var idNo = int.Parse(groups[1].Value);
|
||||
var subFirst = groups[2].Value;
|
||||
var subSecond = groups[3].Value.IsNullOrWhitespace() ? null : groups[3].Value;
|
||||
|
||||
return (boneName, idNo, subFirst, subSecond);
|
||||
}
|
||||
|
||||
private static string GetHairBoneSubLabel(string sub, bool ascending)
|
||||
{
|
||||
return (sub.ToLower(), ascending) switch
|
||||
{
|
||||
("a", _) => "A",
|
||||
("b", true) => "B",
|
||||
("b", false) => "Back",
|
||||
("c", _) => "C",
|
||||
("d", _) => "D",
|
||||
("e", _) => "E",
|
||||
("f", true) => "F",
|
||||
("f", false) => "Front",
|
||||
("l", _) => "Left",
|
||||
("r", _) => "Right",
|
||||
("u", _) => "Upper",
|
||||
("s", _) => "Side",
|
||||
(_, true) => "Next",
|
||||
(_, false) => "Bone"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
243
CustomizePlus/Core/Data/BoneTransform.cs
Normal file
243
CustomizePlus/Core/Data/BoneTransform.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Serialization;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.Game.Services.GPose.ExternalTools;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Core.Data;
|
||||
|
||||
//not the correct terms but they double as user-visible labels so ¯\_(ツ)_/¯
|
||||
public enum BoneAttribute
|
||||
{
|
||||
//hard-coding the backing values for legacy purposes
|
||||
Position = 0,
|
||||
Rotation = 1,
|
||||
Scale = 2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class BoneTransform
|
||||
{
|
||||
//TODO if if ever becomes a point of concern, I might be able to marginally speed things up
|
||||
//by natively storing translation and scaling values as their own vector4s
|
||||
//that way the cost of translating back and forth to vector3s would be frontloaded
|
||||
//to when the user is updating things instead of during the render loop
|
||||
|
||||
public BoneTransform()
|
||||
{
|
||||
Translation = Vector3.Zero;
|
||||
Rotation = Vector3.Zero;
|
||||
Scaling = Vector3.One;
|
||||
}
|
||||
|
||||
public BoneTransform(BoneTransform original)
|
||||
{
|
||||
UpdateToMatch(original);
|
||||
}
|
||||
|
||||
private Vector3 _translation;
|
||||
public Vector3 Translation
|
||||
{
|
||||
get => _translation;
|
||||
set => _translation = ClampVector(value);
|
||||
}
|
||||
|
||||
private Vector3 _rotation;
|
||||
public Vector3 Rotation
|
||||
{
|
||||
get => _rotation;
|
||||
set => _rotation = ClampAngles(value);
|
||||
}
|
||||
|
||||
private Vector3 _scaling;
|
||||
public Vector3 Scaling
|
||||
{
|
||||
get => _scaling;
|
||||
set => _scaling = ClampVector(value);
|
||||
}
|
||||
|
||||
[OnDeserialized]
|
||||
internal void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
//Sanitize all values on deserialization
|
||||
_translation = ClampToDefaultLimits(_translation);
|
||||
_rotation = ClampAngles(_rotation);
|
||||
_scaling = ClampToDefaultLimits(_scaling);
|
||||
}
|
||||
|
||||
public bool IsEdited()
|
||||
{
|
||||
return !Translation.IsApproximately(Vector3.Zero, 0.00001f)
|
||||
|| !Rotation.IsApproximately(Vector3.Zero, 0.1f)
|
||||
|| !Scaling.IsApproximately(Vector3.One, 0.00001f);
|
||||
}
|
||||
|
||||
public BoneTransform DeepCopy()
|
||||
{
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = Translation,
|
||||
Rotation = Rotation,
|
||||
Scaling = Scaling
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateAttribute(BoneAttribute which, Vector3 newValue)
|
||||
{
|
||||
if (which == BoneAttribute.Position)
|
||||
{
|
||||
Translation = newValue;
|
||||
}
|
||||
else if (which == BoneAttribute.Rotation)
|
||||
{
|
||||
Rotation = newValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
Scaling = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateToMatch(BoneTransform newValues)
|
||||
{
|
||||
Translation = newValues.Translation;
|
||||
Rotation = newValues.Rotation;
|
||||
Scaling = newValues.Scaling;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flip a bone's transforms from left to right, so you can use it to update its sibling.
|
||||
/// IVCS bones need to use the special reflection instead.
|
||||
/// </summary>
|
||||
public BoneTransform GetStandardReflection()
|
||||
{
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = new Vector3(Translation.X, Translation.Y, -1 * Translation.Z),
|
||||
Rotation = new Vector3(-1 * Rotation.X, -1 * Rotation.Y, Rotation.Z),
|
||||
Scaling = Scaling
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flip a bone's transforms from left to right, so you can use it to update its sibling.
|
||||
/// IVCS bones are oriented in a system with different symmetries, so they're handled specially.
|
||||
/// </summary>
|
||||
public BoneTransform GetSpecialReflection()
|
||||
{
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = new Vector3(Translation.X, -1 * Translation.Y, Translation.Z),
|
||||
Rotation = new Vector3(Rotation.X, -1 * Rotation.Y, -1 * Rotation.Z),
|
||||
Scaling = Scaling
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize all vectors inside of this container.
|
||||
/// </summary>
|
||||
private void Sanitize()
|
||||
{
|
||||
_translation = ClampVector(_translation);
|
||||
_rotation = ClampAngles(_rotation);
|
||||
_scaling = ClampVector(_scaling);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp all vector values to be within allowed limits.
|
||||
/// </summary>
|
||||
private Vector3 ClampVector(Vector3 vector)
|
||||
{
|
||||
return new Vector3
|
||||
{
|
||||
X = Math.Clamp(vector.X, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit),
|
||||
Y = Math.Clamp(vector.Y, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit),
|
||||
Z = Math.Clamp(vector.Z, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit)
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector3 ClampAngles(Vector3 rotVec)
|
||||
{
|
||||
static float Clamp(float angle)
|
||||
{
|
||||
if (angle > 180)
|
||||
angle -= 360;
|
||||
else if (angle < -180)
|
||||
angle += 360;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
rotVec.X = Clamp(rotVec.X);
|
||||
rotVec.Y = Clamp(rotVec.Y);
|
||||
rotVec.Z = Clamp(rotVec.Z);
|
||||
|
||||
return rotVec;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingTransform(hkQsTransformf tr)
|
||||
{
|
||||
return ModifyExistingTranslationWithRotation(ModifyExistingRotation(ModifyExistingScale(tr)));
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingScale(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisScalingFrozen) return tr;
|
||||
|
||||
tr.Scale.X *= Scaling.X;
|
||||
tr.Scale.Y *= Scaling.Y;
|
||||
tr.Scale.Z *= Scaling.Z;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingRotation(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisRotationFrozen) return tr;
|
||||
|
||||
var newRotation = Quaternion.Multiply(tr.Rotation.ToQuaternion(), Rotation.ToQuaternion());
|
||||
tr.Rotation.X = newRotation.X;
|
||||
tr.Rotation.Y = newRotation.Y;
|
||||
tr.Rotation.Z = newRotation.Z;
|
||||
tr.Rotation.W = newRotation.W;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingTranslationWithRotation(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisPositionFrozen) return tr;
|
||||
|
||||
var adjustedTranslation = Vector4.Transform(Translation, tr.Rotation.ToQuaternion());
|
||||
tr.Translation.X += adjustedTranslation.X;
|
||||
tr.Translation.Y += adjustedTranslation.Y;
|
||||
tr.Translation.Z += adjustedTranslation.Z;
|
||||
tr.Translation.W += adjustedTranslation.W;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
public hkQsTransformf ModifyExistingTranslation(hkQsTransformf tr)
|
||||
{
|
||||
if (PosingModeDetectService.IsAnamnesisPositionFrozen) return tr;
|
||||
|
||||
tr.Translation.X += Translation.X;
|
||||
tr.Translation.Y += Translation.Y;
|
||||
tr.Translation.Z += Translation.Z;
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp all vector values to be within allowed limits.
|
||||
/// </summary>
|
||||
private static Vector3 ClampToDefaultLimits(Vector3 vector)
|
||||
{
|
||||
vector.X = Math.Clamp(vector.X, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit);
|
||||
vector.Y = Math.Clamp(vector.Y, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit);
|
||||
vector.Z = Math.Clamp(vector.Z, Constants.MinVectorValueLimit, Constants.MaxVectorValueLimit);
|
||||
|
||||
return vector;
|
||||
}
|
||||
}
|
||||
82
CustomizePlus/Core/Data/Constants.cs
Normal file
82
CustomizePlus/Core/Data/Constants.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Core.Data;
|
||||
|
||||
internal static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the configuration file, when increased a converter should be implemented if necessary.
|
||||
/// </summary>
|
||||
public const int ConfigurationVersion = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the root bone.
|
||||
/// </summary>
|
||||
public const string RootBoneName = "n_root";
|
||||
|
||||
/// <summary>
|
||||
/// Minimum allowed value for any of the vector values.
|
||||
/// </summary>
|
||||
public const int MinVectorValueLimit = -512;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed value for any of the vector values.
|
||||
/// </summary>
|
||||
public const int MaxVectorValueLimit = 512;
|
||||
|
||||
/// <summary>
|
||||
/// Predicate function for determining if the given object table index represents an
|
||||
/// NPC in a busy area (i.e. there are ~245 other objects already).
|
||||
/// </summary>
|
||||
public static bool IsInObjectTableBusyNPCRange(int index) => index > 245;
|
||||
|
||||
/// <summary>
|
||||
/// A "null" havok vector. Since the type isn't inherently nullable, and the default value (0, 0, 0, 0)
|
||||
/// is valid input in a lot of cases, we can use this instead.
|
||||
/// </summary>
|
||||
public static readonly hkVector4f NullVector = new()
|
||||
{
|
||||
X = float.NaN,
|
||||
Y = float.NaN,
|
||||
Z = float.NaN,
|
||||
W = float.NaN
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A "null" havok quaternion. Since the type isn't inherently nullable, and the default value (0, 0, 0, 0)
|
||||
/// is valid input in a lot of cases, we can use this instead.
|
||||
/// </summary>
|
||||
public static readonly hkQuaternionf NullQuaternion = new()
|
||||
{
|
||||
X = float.NaN,
|
||||
Y = float.NaN,
|
||||
Z = float.NaN,
|
||||
W = float.NaN
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A "null" havok transform. Since the type isn't inherently nullable, and the default values
|
||||
/// aren't immediately obviously wrong, we can use this instead.
|
||||
/// </summary>
|
||||
public static readonly hkQsTransformf NullTransform = new()
|
||||
{
|
||||
Translation = NullVector,
|
||||
Rotation = NullQuaternion,
|
||||
Scale = NullVector
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The pose at index 0 is the only one we apparently need to care about.
|
||||
/// </summary>
|
||||
public const int TruePoseIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Main render hook address
|
||||
/// </summary>
|
||||
public const string RenderHookAddress = "E8 ?? ?? ?? ?? 48 81 C3 ?? ?? ?? ?? BF ?? ?? ?? ?? 33 ED";
|
||||
|
||||
/// <summary>
|
||||
/// Movement hook address, used for position offset and other changes which cannot be done in main hook
|
||||
/// </summary>
|
||||
public const string MovementHookAddress = "E8 ?? ?? ?? ?? EB 29 48 8B 5F 08";
|
||||
}
|
||||
23
CustomizePlus/Core/Events/ReloadEvent.cs
Normal file
23
CustomizePlus/Core/Events/ReloadEvent.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Core.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when complete plugin reload is requested
|
||||
/// </summary>
|
||||
public sealed class ReloadEvent() : EventWrapper<ReloadEvent.Type, ReloadEvent.Priority>(nameof(ReloadEvent))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
ReloadAll,
|
||||
ReloadProfiles,
|
||||
ReloadTemplates
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
TemplateManager = -2,
|
||||
ProfileManager = -1
|
||||
}
|
||||
}
|
||||
32
CustomizePlus/Core/Extensions/StringExtensions.cs
Normal file
32
CustomizePlus/Core/Extensions/StringExtensions.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace CustomizePlus.Core.Extensions
|
||||
{
|
||||
internal static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Incognify string. Usually used for logging character names and stuff. Does nothing in debug build.
|
||||
/// </summary>
|
||||
public static string Incognify(this string str)
|
||||
{
|
||||
if (str.IsNullOrWhitespace())
|
||||
return str;
|
||||
|
||||
#if DEBUG
|
||||
return str;
|
||||
#endif
|
||||
|
||||
if (str.Contains(" "))
|
||||
{
|
||||
var split = str.Split(' ');
|
||||
|
||||
if (split.Length > 2)
|
||||
return $"{str[..2]}...";
|
||||
|
||||
return $"{split[0][0]}.{split[1][0]}";
|
||||
}
|
||||
|
||||
return $"{str[..2]}...";
|
||||
}
|
||||
}
|
||||
}
|
||||
56
CustomizePlus/Core/Extensions/TransformExtensions.cs
Normal file
56
CustomizePlus/Core/Extensions/TransformExtensions.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Core.Data;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
//using FFXIVClientStructs.FFXIV.Client.Graphics;
|
||||
|
||||
namespace CustomizePlus.Core.Extensions;
|
||||
|
||||
internal static class TransformExtensions
|
||||
{
|
||||
public static bool Equals(this hkQsTransformf first, hkQsTransformf second)
|
||||
{
|
||||
return first.Translation.Equals(second.Translation)
|
||||
&& first.Rotation.Equals(second.Rotation)
|
||||
&& first.Scale.Equals(second.Scale);
|
||||
}
|
||||
|
||||
public static bool IsNull(this hkQsTransformf t)
|
||||
{
|
||||
return t.Equals(Constants.NullTransform);
|
||||
}
|
||||
|
||||
public static hkQsTransformf ToHavokTransform(this BoneTransform bt)
|
||||
{
|
||||
return new hkQsTransformf
|
||||
{
|
||||
Translation = bt.Translation.ToHavokTranslation(),
|
||||
Rotation = bt.Rotation.ToQuaternion().ToHavokRotation(),
|
||||
Scale = bt.Scaling.ToHavokScaling()
|
||||
};
|
||||
}
|
||||
|
||||
public static BoneTransform ToBoneTransform(this hkQsTransformf t)
|
||||
{
|
||||
var rotVec = Quaternion.Divide(t.Translation.ToQuaternion(), t.Rotation.ToQuaternion());
|
||||
|
||||
return new BoneTransform
|
||||
{
|
||||
Translation = new Vector3(rotVec.X / rotVec.W, rotVec.Y / rotVec.W, rotVec.Z / rotVec.W),
|
||||
Rotation = t.Rotation.ToQuaternion().ToEulerAngles(),
|
||||
Scaling = new Vector3(t.Scale.X, t.Scale.Y, t.Scale.Z)
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f GetAttribute(this hkQsTransformf t, BoneAttribute att)
|
||||
{
|
||||
return att switch
|
||||
{
|
||||
BoneAttribute.Position => t.Translation,
|
||||
BoneAttribute.Rotation => t.Rotation.ToQuaternion().GetAsNumericsVector().ToHavokVector(),
|
||||
BoneAttribute.Scale => t.Scale,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
}
|
||||
144
CustomizePlus/Core/Extensions/VectorExtensions.cs
Normal file
144
CustomizePlus/Core/Extensions/VectorExtensions.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Anamnesis.Data;
|
||||
using FFXIVClientStructs.Havok;
|
||||
|
||||
namespace CustomizePlus.Core.Extensions;
|
||||
|
||||
internal static class VectorExtensions
|
||||
{
|
||||
public static bool IsApproximately(this hkVector4f vector, Vector3 other, float errorMargin = 0.001f)
|
||||
{
|
||||
return IsApproximately(vector.X, other.X, errorMargin)
|
||||
&& IsApproximately(vector.Y, other.Y, errorMargin)
|
||||
&& IsApproximately(vector.Z, other.Z, errorMargin);
|
||||
}
|
||||
|
||||
public static bool IsApproximately(this Vector3 vector, Vector3 other, float errorMargin = 0.001f)
|
||||
{
|
||||
return IsApproximately(vector.X, other.X, errorMargin)
|
||||
&& IsApproximately(vector.Y, other.Y, errorMargin)
|
||||
&& IsApproximately(vector.Z, other.Z, errorMargin);
|
||||
}
|
||||
|
||||
private static bool IsApproximately(float a, float b, float errorMargin)
|
||||
{
|
||||
var d = MathF.Abs(a - b);
|
||||
return d < errorMargin;
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this Vector3 rotation)
|
||||
{
|
||||
return Quaternion.CreateFromYawPitchRoll(
|
||||
rotation.X * MathF.PI / 180,
|
||||
rotation.Y * MathF.PI / 180,
|
||||
rotation.Z * MathF.PI / 180);
|
||||
}
|
||||
|
||||
public static Vector3 ToEulerAngles(this Quaternion q)
|
||||
{
|
||||
var nq = Vector4.Normalize(q.GetAsNumericsVector());
|
||||
|
||||
var rollX = MathF.Atan2(
|
||||
2 * (nq.W * nq.X + nq.Y * nq.Z),
|
||||
1 - 2 * (nq.X * nq.X + nq.Y * nq.Y));
|
||||
|
||||
var pitchY = 2 * MathF.Atan2(
|
||||
MathF.Sqrt(1 + 2 * (nq.W * nq.Y - nq.X * nq.Z)),
|
||||
MathF.Sqrt(1 - 2 * (nq.W * nq.Y - nq.X * nq.Z)));
|
||||
|
||||
var yawZ = MathF.Atan2(
|
||||
2 * (nq.W * nq.Z + nq.X * nq.Y),
|
||||
1 - 2 * (nq.Y * nq.Y + nq.Z * nq.Z));
|
||||
|
||||
return new Vector3(rollX, pitchY, yawZ);
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this Vector4 rotation)
|
||||
{
|
||||
return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this hkQuaternionf rotation)
|
||||
{
|
||||
return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
}
|
||||
|
||||
public static Quaternion ToQuaternion(this hkVector4f rotation)
|
||||
{
|
||||
return new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
}
|
||||
|
||||
|
||||
public static hkQuaternionf ToHavokRotation(this Quaternion rotation)
|
||||
{
|
||||
return new hkQuaternionf
|
||||
{
|
||||
X = rotation.X,
|
||||
Y = rotation.Y,
|
||||
Z = rotation.Z,
|
||||
W = rotation.W
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f ToHavokTranslation(this Vector3 translation)
|
||||
{
|
||||
return new hkVector4f
|
||||
{
|
||||
X = translation.X,
|
||||
Y = translation.Y,
|
||||
Z = translation.Z,
|
||||
W = 0.0f
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f ToHavokScaling(this Vector3 scaling)
|
||||
{
|
||||
return new hkVector4f
|
||||
{
|
||||
X = scaling.X,
|
||||
Y = scaling.Y,
|
||||
Z = scaling.Z,
|
||||
W = 1.0f
|
||||
};
|
||||
}
|
||||
|
||||
public static hkVector4f ToHavokVector(this Vector4 vec)
|
||||
{
|
||||
return new hkVector4f
|
||||
{
|
||||
X = vec.X,
|
||||
Y = vec.Y,
|
||||
Z = vec.Z,
|
||||
W = vec.W
|
||||
};
|
||||
}
|
||||
|
||||
public static Vector3 GetAsNumericsVector(this PoseFile.Vector vec)
|
||||
{
|
||||
return new Vector3(vec.X, vec.Y, vec.Z);
|
||||
}
|
||||
|
||||
public static Vector4 GetAsNumericsVector(this hkVector4f vec)
|
||||
{
|
||||
return new Vector4(vec.X, vec.Y, vec.Z, vec.W);
|
||||
}
|
||||
|
||||
public static Vector4 GetAsNumericsVector(this Quaternion q)
|
||||
{
|
||||
return new Vector4(q.X, q.Y, q.Z, q.W);
|
||||
}
|
||||
|
||||
public static Vector3 RemoveWTerm(this Vector4 vec)
|
||||
{
|
||||
return new Vector3(vec.X, vec.Y, vec.Z);
|
||||
}
|
||||
|
||||
public static bool Equals(this hkVector4f first, hkVector4f second)
|
||||
{
|
||||
return first.X == second.X
|
||||
&& first.Y == second.Y
|
||||
&& first.Z == second.Z
|
||||
&& first.W == second.W;
|
||||
}
|
||||
}
|
||||
60
CustomizePlus/Core/Helpers/Base64Helper.cs
Normal file
60
CustomizePlus/Core/Helpers/Base64Helper.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CustomizePlus.Core.Helpers;
|
||||
|
||||
public static class Base64Helper
|
||||
{
|
||||
// Compress any type to a base64 encoding of its compressed json representation, prepended with a version byte.
|
||||
// Returns an empty string on failure.
|
||||
// Original by Ottermandias: OtterGui <3
|
||||
public static unsafe string ExportToBase64<T>(T obj, byte version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(obj, Formatting.None);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
using var compressedStream = new MemoryStream();
|
||||
using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
|
||||
{
|
||||
zipStream.Write(new ReadOnlySpan<byte>(&version, 1));
|
||||
zipStream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(compressedStream.ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// Decompress a base64 encoded string to the given type and a prepended version byte if possible.
|
||||
// On failure, data will be String error and version will be byte.MaxValue.
|
||||
// Original by Ottermandias: OtterGui <3
|
||||
public static byte ImportFromBase64(string base64, out string data)
|
||||
{
|
||||
var version = byte.MaxValue;
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
using var compressedStream = new MemoryStream(bytes);
|
||||
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
|
||||
using var resultStream = new MemoryStream();
|
||||
zipStream.CopyTo(resultStream);
|
||||
bytes = resultStream.ToArray();
|
||||
version = bytes[0];
|
||||
var json = Encoding.UTF8.GetString(bytes, 1, bytes.Length - 1);
|
||||
data = json;
|
||||
}
|
||||
catch
|
||||
{
|
||||
data = "error";
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
}
|
||||
122
CustomizePlus/Core/Helpers/CtrlHelper.cs
Normal file
122
CustomizePlus/Core/Helpers/CtrlHelper.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace CustomizePlus.Core.Helpers;
|
||||
|
||||
public static class CtrlHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the width of an icon button, checkbox, etc...
|
||||
/// </summary>
|
||||
/// per https://github.com/ocornut/imgui/issues/3714#issuecomment-759319268
|
||||
public static float IconButtonWidth => ImGui.GetFrameHeight() + 2 * ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
|
||||
public static bool TextBox(string label, ref string value)
|
||||
{
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
return ImGui.InputText(label, ref value, 1024);
|
||||
}
|
||||
|
||||
public static bool TextPropertyBox(string label, Func<string> get, Action<string> set)
|
||||
{
|
||||
var temp = get();
|
||||
var result = TextBox(label, ref temp);
|
||||
if (result)
|
||||
{
|
||||
set(temp);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static bool Checkbox(string label, ref bool value)
|
||||
{
|
||||
return ImGui.Checkbox(label, ref value);
|
||||
}
|
||||
|
||||
public static bool CheckboxWithTextAndHelp(string label, string text, string helpText, ref bool value)
|
||||
{
|
||||
var checkBoxState = ImGui.Checkbox(label, ref value);
|
||||
ImGui.SameLine();
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.Text(FontAwesomeIcon.InfoCircle.ToIconString());
|
||||
ImGui.PopFont();
|
||||
AddHoverText(helpText);
|
||||
ImGui.SameLine();
|
||||
ImGui.Text(text);
|
||||
|
||||
AddHoverText(helpText);
|
||||
|
||||
return checkBoxState;
|
||||
}
|
||||
|
||||
public static bool CheckboxToggle(string label, in bool shown, Action<bool> toggle)
|
||||
{
|
||||
var temp = shown;
|
||||
var toggled = ImGui.Checkbox(label, ref temp);
|
||||
|
||||
if (toggled)
|
||||
{
|
||||
toggle(temp);
|
||||
}
|
||||
|
||||
return toggled;
|
||||
}
|
||||
|
||||
public static bool ArrowToggle(string label, ref bool value)
|
||||
{
|
||||
var toggled = ImGui.ArrowButton(label, value ? ImGuiDir.Down : ImGuiDir.Right);
|
||||
|
||||
if (toggled)
|
||||
{
|
||||
value = !value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static void AddHoverText(string text)
|
||||
{
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.SetTooltip(text);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TextAlignment { Left, Center, Right };
|
||||
public static void StaticLabel(string? text, TextAlignment align = TextAlignment.Left, string tooltip = "")
|
||||
{
|
||||
if (text != null)
|
||||
{
|
||||
if (align == TextAlignment.Center)
|
||||
{
|
||||
ImGui.Dummy(new System.Numerics.Vector2((ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X) / 2, 0));
|
||||
ImGui.SameLine();
|
||||
}
|
||||
else if (align == TextAlignment.Right)
|
||||
{
|
||||
ImGui.Dummy(new System.Numerics.Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X, 0));
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.Text(text);
|
||||
if (!tooltip.IsNullOrWhitespace())
|
||||
{
|
||||
AddHoverText(tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void LabelWithIcon(FontAwesomeIcon icon, string text, bool isSameLine = true)
|
||||
{
|
||||
if (isSameLine)
|
||||
ImGui.SameLine();
|
||||
ImGui.PushFont(UiBuilder.IconFont);
|
||||
ImGui.Text(icon.ToIconString());
|
||||
ImGui.PopFont();
|
||||
ImGui.SameLine();
|
||||
ImGui.TextWrapped(text);
|
||||
}
|
||||
}
|
||||
22
CustomizePlus/Core/Helpers/NameParsingHelper.cs
Normal file
22
CustomizePlus/Core/Helpers/NameParsingHelper.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace CustomizePlus.Core.Helpers;
|
||||
|
||||
//todo: better name
|
||||
internal static class NameParsingHelper
|
||||
{
|
||||
internal static (string Name, string? Path) ParseName(string name, bool handlePath)
|
||||
{
|
||||
var actualName = name;
|
||||
string? path = null;
|
||||
if (handlePath)
|
||||
{
|
||||
var slashPos = name.LastIndexOf('/');
|
||||
if (slashPos >= 0)
|
||||
{
|
||||
path = name[..slashPos];
|
||||
actualName = slashPos >= name.Length - 1 ? "<Unnamed>" : name[(slashPos + 1)..];
|
||||
}
|
||||
}
|
||||
|
||||
return (actualName, path);
|
||||
}
|
||||
}
|
||||
194
CustomizePlus/Core/ServiceManager.cs
Normal file
194
CustomizePlus/Core/ServiceManager.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Configuration.Services;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.Armatures.Events;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Core.Events;
|
||||
using CustomizePlus.UI;
|
||||
using CustomizePlus.UI.Windows.Controls;
|
||||
using CustomizePlus.Anamnesis;
|
||||
using CustomizePlus.Armatures.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
|
||||
using CustomizePlus.UI.Windows.MainWindow;
|
||||
using CustomizePlus.Game.Events;
|
||||
using CustomizePlus.UI.Windows;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Api.Compatibility;
|
||||
using CustomizePlus.Game.Services.GPose;
|
||||
using CustomizePlus.Game.Services.GPose.ExternalTools;
|
||||
using CustomizePlus.GameData.Services;
|
||||
using CustomizePlus.Configuration.Services.Temporary;
|
||||
|
||||
namespace CustomizePlus.Core;
|
||||
|
||||
public static class ServiceManager
|
||||
{
|
||||
public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger logger)
|
||||
{
|
||||
var services = new ServiceCollection()
|
||||
.AddSingleton(logger)
|
||||
.AddDalamud(pi)
|
||||
.AddCore()
|
||||
.AddEvents()
|
||||
.AddGPoseServices()
|
||||
.AddArmatureServices()
|
||||
.AddUI()
|
||||
.AddGameDataServices()
|
||||
.AddTemplateServices()
|
||||
.AddProfileServices()
|
||||
.AddGameServices()
|
||||
.AddConfigServices()
|
||||
.AddRestOfServices();
|
||||
return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
|
||||
}
|
||||
|
||||
private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
new DalamudServices(pluginInterface).AddServices(services);
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddGPoseServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<PosingModeDetectService>()
|
||||
.AddSingleton<GPoseService>()
|
||||
.AddSingleton<GPoseStateChanged>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddArmatureServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<ArmatureManager>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddUI(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<TemplateCombo>()
|
||||
.AddSingleton<PluginStateBlock>()
|
||||
.AddSingleton<SettingsTab>()
|
||||
// template
|
||||
.AddSingleton<TemplatesTab>()
|
||||
.AddSingleton<TemplateFileSystemSelector>()
|
||||
.AddSingleton<TemplatePanel>()
|
||||
.AddSingleton<BoneEditorPanel>()
|
||||
// /template
|
||||
// profile
|
||||
.AddSingleton<ProfilesTab>()
|
||||
.AddSingleton<ProfileFileSystemSelector>()
|
||||
.AddSingleton<ProfilePanel>()
|
||||
// /profile
|
||||
// messages
|
||||
.AddSingleton<MessageService>()
|
||||
.AddSingleton<MessagesTab>()
|
||||
// /messages
|
||||
//
|
||||
.AddSingleton<IPCTestTab>()
|
||||
.AddSingleton<StateMonitoringTab>()
|
||||
//
|
||||
.AddSingleton<PopupSystem>()
|
||||
.AddSingleton<CPlusChangeLog>()
|
||||
.AddSingleton<CPlusWindowSystem>()
|
||||
.AddSingleton<MainWindow>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddEvents(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<ProfileChanged>()
|
||||
.AddSingleton<TemplateChanged>()
|
||||
.AddSingleton<ReloadEvent>()
|
||||
.AddSingleton<ArmatureChanged>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddCore(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<HookingService>()
|
||||
.AddSingleton<ChatService>()
|
||||
.AddSingleton<CommandService>()
|
||||
.AddSingleton<SaveService>()
|
||||
.AddSingleton<FilenameService>()
|
||||
.AddSingleton<BackupService>()
|
||||
.AddSingleton<FantasiaPlusDetectService>()
|
||||
.AddSingleton<FrameworkManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddRestOfServices(this IServiceCollection services) //temp
|
||||
{
|
||||
services
|
||||
.AddSingleton<PoseFileBoneLoader>()
|
||||
.AddSingleton<CustomizePlusIpc>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddConfigServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<PluginConfiguration>()
|
||||
.AddSingleton<ConfigurationMigrator>()
|
||||
.AddSingleton<FantasiaPlusConfigMover>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddGameServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<GameObjectService>()
|
||||
.AddSingleton<GameStateService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddProfileServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<ProfileManager>()
|
||||
.AddSingleton<ProfileFileSystem>()
|
||||
.AddSingleton<TemplateEditorManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddTemplateServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<TemplateManager>()
|
||||
.AddSingleton<TemplateFileSystem>()
|
||||
.AddSingleton<TemplateEditorManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddGameDataServices(this IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton<CutsceneService>()
|
||||
.AddSingleton<GameEventManager>()
|
||||
.AddSingleton<ActorService>()
|
||||
.AddSingleton<ObjectManager>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
59
CustomizePlus/Core/Services/BackupService.cs
Normal file
59
CustomizePlus/Core/Services/BackupService.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class BackupService
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly FilenameService _filenameService;
|
||||
private readonly DirectoryInfo _configDirectory;
|
||||
private readonly IReadOnlyList<FileInfo> _fileNames;
|
||||
|
||||
public BackupService(Logger logger, FilenameService filenameService)
|
||||
{
|
||||
_logger = logger;
|
||||
_filenameService = filenameService;
|
||||
_fileNames = PluginFiles(_filenameService);
|
||||
_configDirectory = new DirectoryInfo(_filenameService.ConfigDirectory);
|
||||
Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a permanent backup with a given name for migrations.
|
||||
/// </summary>
|
||||
public void CreateMigrationBackup(string name)
|
||||
=> Backup.CreatePermanentBackup(_logger, _configDirectory, _fileNames, name);
|
||||
|
||||
/// <summary>
|
||||
/// Create backup for all version 3 configuration files
|
||||
/// </summary>
|
||||
public void CreateV3Backup(string name = "v3_to_v4_migration")
|
||||
{
|
||||
var list = new List<FileInfo>(16) { new(_filenameService.ConfigFile) };
|
||||
list.AddRange(Directory.EnumerateFiles(_filenameService.ConfigDirectory, "*.profile", SearchOption.TopDirectoryOnly).Select(x => new FileInfo(x)));
|
||||
|
||||
Backup.CreatePermanentBackup(_logger, _configDirectory, list, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all relevant files for plugin configuration.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<FileInfo> PluginFiles(FilenameService fileNames)
|
||||
{
|
||||
var list = new List<FileInfo>(16)
|
||||
{
|
||||
new(fileNames.ConfigFile),
|
||||
new(fileNames.ProfileFileSystem),
|
||||
new(fileNames.TemplateFileSystem)
|
||||
};
|
||||
|
||||
list.AddRange(fileNames.Profiles());
|
||||
list.AddRange(fileNames.Templates());
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
182
CustomizePlus/Core/Services/CommandService.cs
Normal file
182
CustomizePlus/Core/Services/CommandService.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OtterGui.Log;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.UI.Windows.MainWindow;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class CommandService : IDisposable
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
private readonly ICommandManager _commandManager;
|
||||
private readonly Logger _logger;
|
||||
private readonly ChatService _chatService;
|
||||
private readonly MainWindow _mainWindow;
|
||||
private readonly BoneEditorPanel _boneEditorPanel;
|
||||
private readonly MessageService _messageService;
|
||||
|
||||
private static readonly string[] Commands = new[] { "/customize", "/c+" };
|
||||
|
||||
public CommandService(
|
||||
ProfileManager profileManager,
|
||||
GameObjectService gameObjectService,
|
||||
ICommandManager commandManager,
|
||||
MainWindow mainWindow,
|
||||
ChatService chatService,
|
||||
BoneEditorPanel boneEditorPanel,
|
||||
Logger logger,
|
||||
MessageService messageService)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
_gameObjectService = gameObjectService;
|
||||
_commandManager = commandManager;
|
||||
_logger = logger;
|
||||
_chatService = chatService;
|
||||
_mainWindow = mainWindow;
|
||||
_boneEditorPanel = boneEditorPanel;
|
||||
_messageService = messageService;
|
||||
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
_commandManager.AddHandler(command, new CommandInfo(OnMainCommand) { HelpMessage = "Toggles main plugin window if no commands passed. Use \"/customize help\" for list of available commands." });
|
||||
}
|
||||
|
||||
chatService.PrintInChat($"Started!"); //safe to assume at this point we have successfully initialized
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
_commandManager.RemoveHandler(command);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMainCommand(string command, string arguments)
|
||||
{
|
||||
if (_boneEditorPanel.IsEditorActive)
|
||||
{
|
||||
_messageService.NotificationMessage("Customize+ commands cannot be used when editor is active", NotificationType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var argumentList = arguments.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var argument = argumentList.Length == 2 ? argumentList[1] : string.Empty;
|
||||
|
||||
if (arguments.Length > 0)
|
||||
{
|
||||
switch (argumentList[0].ToLowerInvariant())
|
||||
{
|
||||
case "apply":
|
||||
Apply(argument);
|
||||
return;
|
||||
case "toggle":
|
||||
Apply(argument, true);
|
||||
return;
|
||||
default:
|
||||
case "help":
|
||||
PrintHelp(argumentList[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_mainWindow.Toggle();
|
||||
}
|
||||
|
||||
private bool PrintHelp(string argument)
|
||||
{
|
||||
if (!string.Equals(argument, "help", StringComparison.OrdinalIgnoreCase) && argument != "?")
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText("The given argument ").AddRed(argument, true)
|
||||
.AddText(" is not valid. Valid arguments are:").BuiltString);
|
||||
else
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText("Valid arguments for /customize are:").BuiltString);
|
||||
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddCommand("apply", "Applies a given profile for a given character. Use without arguments for help.")
|
||||
.BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddCommand("toggle", "Toggles a given profile for a given character. Use without arguments for help.")
|
||||
.BuiltString);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void Apply(string argument, bool toggle = false)
|
||||
{
|
||||
var argumentList = argument.Split(',', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (argumentList.Length != 2)
|
||||
{
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText($"Usage: /customize {(toggle ? "toggle" : "apply")} ").AddBlue("Character Name", true)
|
||||
.AddText(",")
|
||||
.AddRed("Profile Name", true)
|
||||
.BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 ")
|
||||
.AddBlue("Character Name", true).AddText("can be either full character name or one of the following:").BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 To apply to yourself: ").AddBlue("<me>").AddText(", ").AddBlue("self").BuiltString);
|
||||
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 To apply to target: ").AddBlue("<t>").AddText(", ").AddBlue("target").BuiltString);
|
||||
return;
|
||||
}
|
||||
|
||||
string charaName = "", profName = "";
|
||||
|
||||
try
|
||||
{
|
||||
(charaName, profName) = argumentList switch { var a => (a[0].Trim(), a[1].Trim()) };
|
||||
|
||||
charaName = charaName switch
|
||||
{
|
||||
"<me>" => _gameObjectService.GetCurrentPlayerName() ?? string.Empty,
|
||||
"self" => _gameObjectService.GetCurrentPlayerName() ?? string.Empty,
|
||||
"<t>" => _gameObjectService.GetCurrentPlayerTargetName() ?? string.Empty,
|
||||
"target" => _gameObjectService.GetCurrentPlayerTargetName() ?? string.Empty,
|
||||
_ => charaName,
|
||||
};
|
||||
|
||||
if (!_profileManager.Profiles.Any())
|
||||
{
|
||||
_chatService.PrintInChat(
|
||||
$"Can't {(toggle ? "toggle" : "apply")} profile \"{profName}\" for character \"{charaName}\" because no profiles exist", ChatService.ChatMessageColor.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_profileManager.Profiles.Count(x => x.Name == profName && x.CharacterName == charaName) > 1)
|
||||
{
|
||||
_logger.Warning(
|
||||
$"Found more than one profile matching profile \"{profName}\" and character \"{charaName}\". Using first match.");
|
||||
}
|
||||
|
||||
var outProf = _profileManager.Profiles.FirstOrDefault(x => x.Name == profName && x.CharacterName == charaName);
|
||||
|
||||
if (outProf == null)
|
||||
{
|
||||
_chatService.PrintInChat(
|
||||
$"Can't {(toggle ? "toggle" : "apply")} profile \"{(string.IsNullOrWhiteSpace(profName) ? "empty (none provided)" : profName)}\" " +
|
||||
$"for Character \"{(string.IsNullOrWhiteSpace(charaName) ? "empty (none provided)" : charaName)}\"\n" +
|
||||
"Check if the profile and character names were provided correctly and said profile exists for chosen character", ChatService.ChatMessageColor.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toggle)
|
||||
_profileManager.SetEnabled(outProf, true);
|
||||
else
|
||||
_profileManager.SetEnabled(outProf, !outProf.Enabled);
|
||||
|
||||
_chatService.PrintInChat(
|
||||
$"{outProf.Name} was successfully {(toggle ? "toggled" : "applied")} for {outProf.CharacterName}", ChatService.ChatMessageColor.Info);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_chatService.PrintInChat($"Error while {(toggle ? "toggling" : "applying")} profile, details are available in dalamud log", ChatService.ChatMessageColor.Error);
|
||||
_logger.Error($"Error {(toggle ? "toggling" : "applying")} profile by command: \n" +
|
||||
$"Profile name \"{(string.IsNullOrWhiteSpace(profName) ? "empty (none provided)" : profName)}\"\n" +
|
||||
$"Character name \"{(string.IsNullOrWhiteSpace(charaName) ? "empty (none provided)" : charaName)}\"\n" +
|
||||
$"Error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
93
CustomizePlus/Core/Services/DalamudServices.cs
Normal file
93
CustomizePlus/Core/Services/DalamudServices.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class DalamudServices
|
||||
{
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public DalamudPluginInterface PluginInterface { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ISigScanner SigScanner { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public IFramework Framework { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IObjectTable ObjectTable { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ICommandManager CommandManager { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IChatGui ChatGui { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IClientState ClientState { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IGameGui GameGui { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
internal IGameInteropProvider Hooker { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IKeyState KeyState { get; private set; } = null!;
|
||||
|
||||
//GameData
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IDataManager DataManager { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public IPluginLog PluginLog { get; private set; } = null!;
|
||||
|
||||
/*[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ICondition Condition { get; private set; } = null!;*/
|
||||
|
||||
[PluginService]
|
||||
[RequiredVersion("1.0")]
|
||||
public ITargetManager TargetManager { get; private set; } = null!;
|
||||
|
||||
public DalamudServices(DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
pluginInterface.Inject(this);
|
||||
}
|
||||
|
||||
public void AddServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddSingleton(PluginInterface)
|
||||
.AddSingleton(SigScanner)
|
||||
.AddSingleton(Framework)
|
||||
.AddSingleton(ObjectTable)
|
||||
.AddSingleton(CommandManager)
|
||||
.AddSingleton(ChatGui)
|
||||
.AddSingleton(ClientState)
|
||||
.AddSingleton(GameGui)
|
||||
.AddSingleton(Hooker)
|
||||
.AddSingleton(KeyState)
|
||||
.AddSingleton(this)
|
||||
.AddSingleton(PluginInterface.UiBuilder)
|
||||
.AddSingleton(DataManager)
|
||||
.AddSingleton(PluginLog)
|
||||
//.AddSingleton(Condition)
|
||||
.AddSingleton(TargetManager);
|
||||
}
|
||||
}
|
||||
77
CustomizePlus/Core/Services/FantasiaPlusDetectService.cs
Normal file
77
CustomizePlus/Core/Services/FantasiaPlusDetectService.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using CustomizePlus.UI.Windows;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Timers;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Detects is Fantasia+ is installed and shows a message if it is. The check is performed every 15 seconds.
|
||||
/// </summary>
|
||||
public class FantasiaPlusDetectService : IDisposable
|
||||
{
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly PopupSystem _popupSystem;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private Timer? _checkTimer = null;
|
||||
|
||||
/// <summary>
|
||||
/// Note: if this is set to true then this is locked until the plugin or game is restarted
|
||||
/// </summary>
|
||||
public bool IsFantasiaPlusInstalled { get; private set; }
|
||||
|
||||
public FantasiaPlusDetectService(DalamudPluginInterface pluginInterface, PopupSystem popupSystem, Logger logger)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_popupSystem = popupSystem;
|
||||
_logger = logger;
|
||||
|
||||
_popupSystem.RegisterPopup("fantasia_detected_warn", "Customize+ detected that you have Fantasia+ installed.\nPlease delete or turn it off and restart your game to use Customize+.");
|
||||
|
||||
if (CheckFantasiaPlusPresence())
|
||||
{
|
||||
_popupSystem.ShowPopup("fantasia_detected_warn");
|
||||
_logger.Error("Fantasia+ detected during startup, plugin will be locked");
|
||||
}
|
||||
else
|
||||
{
|
||||
_checkTimer = new Timer(15 * 1000);
|
||||
_checkTimer.Elapsed += CheckTimerOnElapsed;
|
||||
_checkTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if Fantasia+ is installed and loaded
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool CheckFantasiaPlusPresence()
|
||||
{
|
||||
if (IsFantasiaPlusInstalled)
|
||||
return true;
|
||||
|
||||
IsFantasiaPlusInstalled = _pluginInterface.InstalledPlugins.Any(pluginInfo => pluginInfo is { InternalName: "FantasiaPlus", IsLoaded: true });
|
||||
|
||||
return IsFantasiaPlusInstalled;
|
||||
}
|
||||
|
||||
private void CheckTimerOnElapsed(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (CheckFantasiaPlusPresence())
|
||||
{
|
||||
_popupSystem.ShowPopup("fantasia_detected_warn");
|
||||
_checkTimer!.Stop();
|
||||
_checkTimer?.Dispose();
|
||||
_logger.Error("Fantasia+ detected by timer, plugin will be locked");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_checkTimer != null)
|
||||
_checkTimer.Dispose();
|
||||
}
|
||||
}
|
||||
57
CustomizePlus/Core/Services/FilenameService.cs
Normal file
57
CustomizePlus/Core/Services/FilenameService.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Dalamud.Plugin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class FilenameService
|
||||
{
|
||||
public readonly string ConfigDirectory;
|
||||
public readonly string ConfigFile;
|
||||
public readonly string ProfileDirectory;
|
||||
public readonly string ProfileFileSystem;
|
||||
public readonly string TemplateDirectory;
|
||||
public readonly string TemplateFileSystem;
|
||||
|
||||
public FilenameService(DalamudPluginInterface pi)
|
||||
{
|
||||
ConfigDirectory = pi.ConfigDirectory.FullName;
|
||||
ConfigFile = pi.ConfigFile.FullName;
|
||||
ProfileDirectory = Path.Combine(ConfigDirectory, "profiles");
|
||||
ProfileFileSystem = Path.Combine(ConfigDirectory, "profile_sort_order.json");
|
||||
TemplateDirectory = Path.Combine(ConfigDirectory, "templates");
|
||||
TemplateFileSystem = Path.Combine(ConfigDirectory, "template_sort_order.json");
|
||||
}
|
||||
|
||||
public IEnumerable<FileInfo> Templates()
|
||||
{
|
||||
if (!Directory.Exists(TemplateDirectory))
|
||||
yield break;
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(TemplateDirectory, "*.json", SearchOption.TopDirectoryOnly))
|
||||
yield return new FileInfo(file);
|
||||
}
|
||||
|
||||
public string TemplateFile(Guid id)
|
||||
=> Path.Combine(TemplateDirectory, $"{id}.json");
|
||||
|
||||
public string TemplateFile(Templates.Data.Template template)
|
||||
=> TemplateFile(template.UniqueId);
|
||||
|
||||
public IEnumerable<FileInfo> Profiles()
|
||||
{
|
||||
if (!Directory.Exists(ProfileDirectory))
|
||||
yield break;
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(ProfileDirectory, "*.json", SearchOption.TopDirectoryOnly))
|
||||
yield return new FileInfo(file);
|
||||
}
|
||||
|
||||
public string ProfileFile(Guid id)
|
||||
=> Path.Combine(ProfileDirectory, $"{id}.json");
|
||||
|
||||
public string ProfileFile(Profile profile)
|
||||
=> ProfileFile(profile.UniqueId);
|
||||
}
|
||||
156
CustomizePlus/Core/Services/HookingService.cs
Normal file
156
CustomizePlus/Core/Services/HookingService.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using OtterGui.Log;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Armatures.Services;
|
||||
using CustomizePlus.GameData.Data;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
public class HookingService : IDisposable
|
||||
{
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly ISigScanner _sigScanner;
|
||||
private readonly IGameInteropProvider _hooker;
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly ArmatureManager _armatureManager;
|
||||
private readonly GameStateService _gameStateService;
|
||||
private readonly FantasiaPlusDetectService _fantasiaPlusDetectService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private Hook<RenderDelegate>? _renderManagerHook;
|
||||
private Hook<GameObjectMovementDelegate>? _gameObjectMovementHook;
|
||||
|
||||
private delegate nint RenderDelegate(nint a1, nint a2, int a3, int a4);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
private delegate void GameObjectMovementDelegate(nint gameObject);
|
||||
|
||||
public HookingService(
|
||||
PluginConfiguration configuration,
|
||||
ISigScanner sigScanner,
|
||||
IGameInteropProvider hooker,
|
||||
ProfileManager profileManager,
|
||||
ArmatureManager armatureManager,
|
||||
GameStateService gameStateService,
|
||||
FantasiaPlusDetectService fantasiaPlusDetectService,
|
||||
Logger logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_sigScanner = sigScanner;
|
||||
_hooker = hooker;
|
||||
_profileManager = profileManager;
|
||||
_armatureManager = armatureManager;
|
||||
_gameStateService = gameStateService;
|
||||
_fantasiaPlusDetectService = fantasiaPlusDetectService;
|
||||
_logger = logger;
|
||||
|
||||
ReloadHooks();
|
||||
}
|
||||
|
||||
public void ReloadHooks()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_configuration.PluginEnabled)
|
||||
{
|
||||
if (_renderManagerHook == null)
|
||||
{
|
||||
var renderAddress = _sigScanner.ScanText(Constants.RenderHookAddress);
|
||||
_renderManagerHook = _hooker.HookFromAddress<RenderDelegate>(renderAddress, OnRender);
|
||||
_logger.Debug("Render hook established");
|
||||
}
|
||||
|
||||
if (_gameObjectMovementHook == null)
|
||||
{
|
||||
var movementAddress = _sigScanner.ScanText(Constants.MovementHookAddress);
|
||||
_gameObjectMovementHook = _hooker.HookFromAddress<GameObjectMovementDelegate>(movementAddress, OnGameObjectMove);
|
||||
_logger.Debug("Movement hook established");
|
||||
}
|
||||
|
||||
_logger.Debug("Hooking render & movement functions");
|
||||
_renderManagerHook.Enable();
|
||||
_gameObjectMovementHook.Enable();
|
||||
|
||||
_logger.Debug("Hooking render manager");
|
||||
_renderManagerHook.Enable();
|
||||
|
||||
//Send current player's profile update message to IPC
|
||||
//IPCManager.OnProfileUpdate(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Unhooking...");
|
||||
_renderManagerHook?.Disable();
|
||||
_gameObjectMovementHook?.Disable();
|
||||
_renderManagerHook?.Disable();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error($"Failed to hook Render::Manager::Render {e}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private nint OnRender(nint a1, nint a2, int a3, int a4)
|
||||
{
|
||||
if (_renderManagerHook == null)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
if (_fantasiaPlusDetectService.IsFantasiaPlusInstalled)
|
||||
{
|
||||
_logger.Error($"Fantasia+ detected, disabling all hooks");
|
||||
_renderManagerHook?.Disable();
|
||||
_gameObjectMovementHook.Disable();
|
||||
|
||||
return _renderManagerHook.Original(a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_armatureManager.OnRender();
|
||||
_profileManager.OnRender();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error($"Error in Customize+ render hook {e}");
|
||||
_renderManagerHook?.Disable();
|
||||
}
|
||||
|
||||
return _renderManagerHook.Original(a1, a2, a3, a4);
|
||||
}
|
||||
|
||||
private unsafe void OnGameObjectMove(nint gameObjectPtr)
|
||||
{
|
||||
// Call the original function.
|
||||
_gameObjectMovementHook.Original(gameObjectPtr);
|
||||
|
||||
// If GPose and a 3rd-party posing service are active simultneously, abort
|
||||
if (_gameStateService.GameInPosingMode())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var actor = (Actor)gameObjectPtr;
|
||||
if (actor.Valid)
|
||||
_armatureManager.OnGameObjectMove((Actor)gameObjectPtr);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gameObjectMovementHook?.Disable();
|
||||
_gameObjectMovementHook?.Dispose();
|
||||
|
||||
_renderManagerHook?.Disable();
|
||||
_renderManagerHook?.Dispose();
|
||||
}
|
||||
}
|
||||
17
CustomizePlus/Core/Services/SaveService.cs
Normal file
17
CustomizePlus/Core/Services/SaveService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
|
||||
namespace CustomizePlus.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Any file type that we want to save via SaveService.
|
||||
/// </summary>
|
||||
public interface ISavable : ISavable<FilenameService>
|
||||
{ }
|
||||
|
||||
public sealed class SaveService : SaveServiceBase<FilenameService>
|
||||
{
|
||||
public SaveService(Logger logger, FrameworkManager framework, FilenameService fileNames)
|
||||
: base(logger, framework, fileNames)
|
||||
{ }
|
||||
}
|
||||
91
CustomizePlus/CustomizePlus.csproj
Normal file
91
CustomizePlus/CustomizePlus.csproj
Normal file
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>2.0.0.0</Version>
|
||||
<Description>Customize+</Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Aether-Tools/CustomizePlus</PackageProjectUrl>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<CopyLocalLockfileAssemblies>true</CopyLocalLockfileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Configuration\Data\Version2\**" />
|
||||
<Compile Remove="Util\**" />
|
||||
<EmbeddedResource Remove="Configuration\Data\Version2\**" />
|
||||
<EmbeddedResource Remove="Util\**" />
|
||||
<None Remove="Configuration\Data\Version2\**" />
|
||||
<None Remove="Util\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\Data\icon.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Visible>false</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<ProjectReference Include="..\CustomizePlus.GameData\CustomizePlus.GameData.csproj" />
|
||||
<ProjectReference Include="..\submodules\OtterGui\OtterGui.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>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="CustomizePlus.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
15
CustomizePlus/CustomizePlus.json
Normal file
15
CustomizePlus/CustomizePlus.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Author": "Risa",
|
||||
"Name": "Customize+",
|
||||
"Punchline": "Customize your character beyond FFXIV's limitations.",
|
||||
"Description": "A plugin that allows you to customize your character beyond FFXIV limitations by directly editing bone parameters.",
|
||||
"InternalName": "CustomizePlus",
|
||||
"ApplicableVersion": "any",
|
||||
"DalamudApiLevel": 9,
|
||||
"Tags": [
|
||||
"anamnesis",
|
||||
"customize",
|
||||
"Risa",
|
||||
"Aether Tools"
|
||||
]
|
||||
}
|
||||
11
CustomizePlus/DalamudPackager.targets
Normal file
11
CustomizePlus/DalamudPackager.targets
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Project>
|
||||
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<DalamudPackager
|
||||
ProjectDir="$(ProjectDir)"
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
24
CustomizePlus/Game/Events/GPoseStateChanged.cs
Normal file
24
CustomizePlus/Game/Events/GPoseStateChanged.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Game.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when GPose is entered/exited
|
||||
/// </summary>
|
||||
public sealed class GPoseStateChanged() : EventWrapper<GPoseStateChanged.Type, GPoseStateChanged.Priority>(nameof(GPoseStateChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
Entered,
|
||||
AttemptingExit,
|
||||
Exiting,
|
||||
Exited
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
TemplateEditorManager = -1,
|
||||
GPoseAmnesisKtisisWarningService
|
||||
}
|
||||
}
|
||||
35
CustomizePlus/Game/Services/ChatService.cs
Normal file
35
CustomizePlus/Game/Services/ChatService.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace CustomizePlus.Game.Services;
|
||||
|
||||
public class ChatService
|
||||
{
|
||||
private readonly IChatGui _chatGui;
|
||||
|
||||
public ChatService(IChatGui chatGui)
|
||||
{
|
||||
_chatGui = chatGui;
|
||||
}
|
||||
|
||||
public void PrintInChat(string message, ChatMessageColor color = ChatMessageColor.Info)
|
||||
{
|
||||
var stringBuilder = new SeStringBuilder();
|
||||
stringBuilder.AddUiForeground((ushort)color);
|
||||
stringBuilder.AddText($"[Customize+] {message}");
|
||||
stringBuilder.AddUiForegroundOff();
|
||||
_chatGui.Print(stringBuilder.BuiltString);
|
||||
}
|
||||
|
||||
public void PrintInChat(SeString seString)
|
||||
{
|
||||
_chatGui.Print(seString);
|
||||
}
|
||||
|
||||
public enum ChatMessageColor : ushort
|
||||
{
|
||||
Info = 45,
|
||||
Warning = 500,
|
||||
Error = 14
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Dalamud.Game;
|
||||
|
||||
namespace CustomizePlus.Game.Services.GPose.ExternalTools;
|
||||
|
||||
/// <summary>
|
||||
/// Service which detects if Anamnesis/Ktisis posing mode is enabled.
|
||||
/// </summary>
|
||||
public class PosingModeDetectService
|
||||
{
|
||||
// Borrowed from Ktisis:
|
||||
// If this is NOP'd, Anam posing is enabled.
|
||||
internal static unsafe byte* AnamnesisFreezePosition;
|
||||
internal static unsafe byte* AnamnesisFreezeRotation;
|
||||
internal static unsafe byte* AnamnesisFreezeScale;
|
||||
|
||||
internal static unsafe bool IsAnamnesisPositionFrozen =>
|
||||
AnamnesisFreezePosition != null && *AnamnesisFreezePosition == 0x90 || *AnamnesisFreezePosition == 0x00;
|
||||
|
||||
internal static unsafe bool IsAnamnesisRotationFrozen =>
|
||||
AnamnesisFreezeRotation != null && *AnamnesisFreezeRotation == 0x90 || *AnamnesisFreezeRotation == 0x00;
|
||||
|
||||
internal static unsafe bool IsAnamnesisScalingFrozen =>
|
||||
AnamnesisFreezeScale != null && *AnamnesisFreezeScale == 0x90 || *AnamnesisFreezeScale == 0x00;
|
||||
|
||||
internal static bool IsAnamnesis =>
|
||||
IsAnamnesisPositionFrozen || IsAnamnesisRotationFrozen || IsAnamnesisScalingFrozen;
|
||||
|
||||
public bool IsInPosingMode => IsAnamnesis; //Can't detect Ktisis for now
|
||||
|
||||
public unsafe PosingModeDetectService(ISigScanner sigScanner)
|
||||
{
|
||||
AnamnesisFreezePosition = (byte*)sigScanner.ScanText("41 0F 29 24 12");
|
||||
AnamnesisFreezeRotation = (byte*)sigScanner.ScanText("41 0F 29 5C 12 10");
|
||||
AnamnesisFreezeScale = (byte*)sigScanner.ScanText("41 0F 29 44 12 20");
|
||||
}
|
||||
}
|
||||
138
CustomizePlus/Game/Services/GPose/GPoseService.cs
Normal file
138
CustomizePlus/Game/Services/GPose/GPoseService.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using OtterGui.Log;
|
||||
using CustomizePlus.Game.Events;
|
||||
|
||||
namespace CustomizePlus.Game.Services.GPose;
|
||||
|
||||
public class GPoseService : IDisposable
|
||||
{
|
||||
private readonly ChatService _chatService;
|
||||
private readonly GPoseStateChanged _event;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private Hook<EnterGPoseDelegate>? _enterGPoseHook;
|
||||
private Hook<ExitGPoseDelegate>? _exitGPoseHook;
|
||||
|
||||
private bool _fakeGPose;
|
||||
public GPoseState GPoseState { get; private set; }
|
||||
public bool IsInGPose => GPoseState == GPoseState.Inside;
|
||||
|
||||
public bool FakeGPose
|
||||
{
|
||||
get => _fakeGPose;
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _fakeGPose)
|
||||
{
|
||||
if (!value)
|
||||
{
|
||||
_fakeGPose = false;
|
||||
HandleGPoseChange(GPoseState.Exiting);
|
||||
HandleGPoseChange(GPoseState.Outside);
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleGPoseChange(GPoseState.Inside);
|
||||
_fakeGPose = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe GPoseService(
|
||||
IClientState clientState,
|
||||
IGameInteropProvider hooker,
|
||||
ChatService chatService,
|
||||
GPoseStateChanged @event,
|
||||
Logger logger)
|
||||
{
|
||||
_chatService = chatService;
|
||||
_event = @event;
|
||||
_logger = logger;
|
||||
|
||||
GPoseState = clientState.IsGPosing ? GPoseState.Inside : GPoseState.Outside;
|
||||
|
||||
var uiModule = Framework.Instance()->GetUiModule();
|
||||
var enterGPoseAddress = (nint)uiModule->VTable->EnterGPose;
|
||||
var exitGPoseAddress = (nint)uiModule->VTable->ExitGPose;
|
||||
|
||||
_enterGPoseHook = hooker.HookFromAddress<EnterGPoseDelegate>(enterGPoseAddress, EnteringGPoseDetour);
|
||||
_enterGPoseHook.Enable();
|
||||
|
||||
_exitGPoseHook = hooker.HookFromAddress<ExitGPoseDelegate>(exitGPoseAddress, ExitingGPoseDetour);
|
||||
_exitGPoseHook.Enable();
|
||||
}
|
||||
|
||||
private void ExitingGPoseDetour(nint addr)
|
||||
{
|
||||
if (HandleGPoseChange(GPoseState.AttemptExit))
|
||||
{
|
||||
HandleGPoseChange(GPoseState.Exiting);
|
||||
_exitGPoseHook!.Original.Invoke(addr);
|
||||
HandleGPoseChange(GPoseState.Outside);
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnteringGPoseDetour(nint addr)
|
||||
{
|
||||
var didEnter = _enterGPoseHook!.Original.Invoke(addr);
|
||||
if (didEnter)
|
||||
{
|
||||
_fakeGPose = false;
|
||||
HandleGPoseChange(GPoseState.Inside);
|
||||
}
|
||||
|
||||
return didEnter;
|
||||
}
|
||||
|
||||
private bool HandleGPoseChange(GPoseState state)
|
||||
{
|
||||
if (state == GPoseState || _fakeGPose)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
GPoseState = state;
|
||||
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case GPoseState.Inside:
|
||||
_event.Invoke(GPoseStateChanged.Type.Entered);
|
||||
break;
|
||||
case GPoseState.AttemptExit:
|
||||
_event.Invoke(GPoseStateChanged.Type.AttemptingExit);
|
||||
break;
|
||||
case GPoseState.Exiting:
|
||||
_event.Invoke(GPoseStateChanged.Type.Exiting);
|
||||
break;
|
||||
case GPoseState.Outside:
|
||||
_event.Invoke(GPoseStateChanged.Type.Exited);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_exitGPoseHook?.Dispose();
|
||||
_enterGPoseHook?.Dispose();
|
||||
}
|
||||
|
||||
private delegate void ExitGPoseDelegate(nint addr);
|
||||
|
||||
private delegate bool EnterGPoseDelegate(nint addr);
|
||||
}
|
||||
|
||||
public enum GPoseState
|
||||
{
|
||||
Inside,
|
||||
AttemptExit,
|
||||
Exiting,
|
||||
Outside
|
||||
}
|
||||
71
CustomizePlus/Game/Services/GameObjectService.cs
Normal file
71
CustomizePlus/Game/Services/GameObjectService.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using Penumbra.GameData.Actors;
|
||||
using System.Collections.Generic;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.GameData.Data;
|
||||
using CustomizePlus.GameData.Services;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
|
||||
namespace CustomizePlus.Game.Services;
|
||||
|
||||
public class GameObjectService
|
||||
{
|
||||
private readonly ActorService _actorService;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly ObjectManager _objectManager;
|
||||
|
||||
public GameObjectService(ActorService actorService, IObjectTable objectTable, ObjectManager objectManager)
|
||||
{
|
||||
_actorService = actorService;
|
||||
_objectTable = objectTable;
|
||||
_objectManager = objectManager;
|
||||
}
|
||||
|
||||
public string GetCurrentPlayerName()
|
||||
{
|
||||
return _objectManager.PlayerData.Identifier.ToName();
|
||||
}
|
||||
|
||||
public string GetCurrentPlayerTargetName()
|
||||
{
|
||||
return _objectManager.TargetData.Identifier.ToNameWithoutOwnerName();
|
||||
}
|
||||
|
||||
public bool IsActorHasScalableRoot(Actor actor)
|
||||
{
|
||||
if (!actor.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return false;
|
||||
|
||||
return !Constants.IsInObjectTableBusyNPCRange(actor.Index.Index)
|
||||
&& (identifier.IsAllowedForProfiles()
|
||||
|| actor == _objectTable.GetObjectAddress(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Case sensitive
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<(ActorIdentifier, Actor)> FindActorsByName(string name)
|
||||
{
|
||||
foreach (var kvPair in _objectManager)
|
||||
{
|
||||
var identifier = kvPair.Key;
|
||||
|
||||
if (kvPair.Key.Type == IdentifierType.Special)
|
||||
identifier = identifier.GetTrueActorForSpecialType();
|
||||
|
||||
if (!identifier.IsValid)
|
||||
continue;
|
||||
|
||||
if (identifier.ToNameWithoutOwnerName() == name)
|
||||
{
|
||||
if (kvPair.Value.Objects.Count > 1) //in gpose we can have more than a single object for one actor
|
||||
foreach (var obj in kvPair.Value.Objects)
|
||||
yield return (kvPair.Key, obj);
|
||||
else
|
||||
yield return (kvPair.Key, kvPair.Value.Objects[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CustomizePlus/Game/Services/GameStateService.cs
Normal file
21
CustomizePlus/Game/Services/GameStateService.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using CustomizePlus.Game.Services.GPose;
|
||||
using CustomizePlus.Game.Services.GPose.ExternalTools;
|
||||
|
||||
namespace CustomizePlus.Game.Services;
|
||||
|
||||
public class GameStateService
|
||||
{
|
||||
private readonly GPoseService _gposeService;
|
||||
private readonly PosingModeDetectService _posingModeDetectService;
|
||||
|
||||
public GameStateService(GPoseService gposeService, PosingModeDetectService posingModeDetectService)
|
||||
{
|
||||
_gposeService = gposeService;
|
||||
_posingModeDetectService = posingModeDetectService;
|
||||
}
|
||||
|
||||
public bool GameInPosingMode()
|
||||
{
|
||||
return _gposeService.GPoseState == GPoseState.Inside || _posingModeDetectService.IsInPosingMode;
|
||||
}
|
||||
}
|
||||
6
CustomizePlus/GlobalSuppressions.cs
Normal file
6
CustomizePlus/GlobalSuppressions.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly:
|
||||
SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore",
|
||||
Justification = "Supressed in favor of CA1500. Use _camelCase for private fields.",
|
||||
Scope = "namespaceanddescendants", Target = "~N:CustomizePlus")]
|
||||
55
CustomizePlus/Plugin.cs
Normal file
55
CustomizePlus/Plugin.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtterGui.Log;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.UI;
|
||||
using CustomizePlus.Core;
|
||||
using CustomizePlus.Api.Compatibility;
|
||||
using CustomizePlus.Configuration.Services.Temporary;
|
||||
|
||||
namespace CustomizePlus;
|
||||
|
||||
public sealed class Plugin : IDalamudPlugin
|
||||
{
|
||||
#if DEBUG
|
||||
public static readonly string Version = $"{Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty} [DEBUG]";
|
||||
#else
|
||||
public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
|
||||
#endif
|
||||
|
||||
private readonly ServiceProvider _services;
|
||||
|
||||
public static readonly Logger Logger = new(); //for loggin in static classes/methods
|
||||
|
||||
public Plugin(DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
try
|
||||
{
|
||||
_services = ServiceManager.CreateProvider(pluginInterface, Logger);
|
||||
|
||||
//temporary
|
||||
var configMover = _services.GetRequiredService<FantasiaPlusConfigMover>();
|
||||
configMover.MoveConfigsIfNeeded();
|
||||
|
||||
_services.GetRequiredService<CustomizePlusIpc>();
|
||||
_services.GetRequiredService<CPlusWindowSystem>();
|
||||
_services.GetRequiredService<CommandService>();
|
||||
|
||||
Logger.Information($"Customize+ v{Version} [FantasiaPlus] started");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Error instantiating plugin: {ex}");
|
||||
|
||||
Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_services?.Dispose();
|
||||
}
|
||||
}
|
||||
204
CustomizePlus/Profiles/Data/Profile.cs
Normal file
204
CustomizePlus/Profiles/Data/Profile.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace CustomizePlus.Profiles.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates the user-controlled aspects of a character profile, ie all of
|
||||
/// the information that gets saved to disk by the plugin.
|
||||
/// </summary>
|
||||
public sealed class Profile : ISavable
|
||||
{
|
||||
private static int _nextGlobalId;
|
||||
|
||||
private readonly int _localId;
|
||||
|
||||
public List<Armature> Armatures = new();
|
||||
|
||||
public LowerString CharacterName { get; set; } = LowerString.Empty;
|
||||
public LowerString Name { get; set; } = LowerString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to search only through local player owned characters or all characters when searching for game object by name
|
||||
/// </summary>
|
||||
public bool LimitLookupToOwnedObjects { get; set; } = false;
|
||||
|
||||
public int Version { get; set; } = Constants.ConfigurationVersion;
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
public DateTimeOffset CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTimeOffset ModifiedDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Guid UniqueId { get; set; } = Guid.NewGuid();
|
||||
|
||||
public List<Template> Templates { get; init; } = new();
|
||||
|
||||
public bool IsWriteProtected { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies if this profile is not persistent (ex. was made via IPC calls) and should not be displayed in UI.
|
||||
/// WARNING, TEMPLATES FOR TEMPORARY PROFILES *ARE NOT* STORED IN TemplateManager
|
||||
/// </summary>
|
||||
public bool IsTemporary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identificator specifying specific actor this profile applies to, only works for temporary profiles
|
||||
/// </summary>
|
||||
public ActorIdentifier TemporaryActor { get; set; } = ActorIdentifier.Invalid;
|
||||
|
||||
public string Incognito
|
||||
=> UniqueId.ToString()[..8];
|
||||
|
||||
public Profile()
|
||||
{
|
||||
_localId = _nextGlobalId++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new profile based on data from another one
|
||||
/// </summary>
|
||||
/// <param name="original"></param>
|
||||
public Profile(Profile original) : this()
|
||||
{
|
||||
CharacterName = original.CharacterName;
|
||||
LimitLookupToOwnedObjects = original.LimitLookupToOwnedObjects;
|
||||
|
||||
foreach (var template in original.Templates)
|
||||
{
|
||||
Templates.Add(template);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Profile '{Name.Text.Incognify()}' on {CharacterName.Text.Incognify()} [{UniqueId}]";
|
||||
}
|
||||
|
||||
#region Serialization
|
||||
|
||||
public new JObject JsonSerialize()
|
||||
{
|
||||
var ret = new JObject()
|
||||
{
|
||||
["Version"] = Version,
|
||||
["UniqueId"] = UniqueId,
|
||||
["CreationDate"] = CreationDate,
|
||||
["ModifiedDate"] = ModifiedDate,
|
||||
["CharacterName"] = CharacterName.Text,
|
||||
["Name"] = Name.Text,
|
||||
["LimitLookupToOwnedObjects"] = LimitLookupToOwnedObjects,
|
||||
["Enabled"] = Enabled,
|
||||
["IsWriteProtected"] = IsWriteProtected,
|
||||
["Templates"] = SerializeTemplates()
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private JArray SerializeTemplates()
|
||||
{
|
||||
var ret = new JArray();
|
||||
foreach (var template in Templates)
|
||||
{
|
||||
ret.Add(new JObject()
|
||||
{
|
||||
["TemplateId"] = template.UniqueId
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization
|
||||
|
||||
public static Profile Load(TemplateManager templateManager, JObject obj)
|
||||
{
|
||||
var version = obj["Version"]?.ToObject<int>() ?? 0;
|
||||
return version switch
|
||||
{
|
||||
//Ignore everything below v4 for now
|
||||
4 => LoadV4(templateManager, obj),
|
||||
_ => throw new Exception("The design to be loaded has no valid Version."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Profile LoadV4(TemplateManager templateManager, JObject obj)
|
||||
{
|
||||
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
|
||||
|
||||
var profile = new Profile()
|
||||
{
|
||||
CreationDate = creationDate,
|
||||
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
|
||||
Name = new LowerString(obj["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
|
||||
CharacterName = new LowerString(obj["CharacterName"]?.ToObject<string>() ?? throw new ArgumentNullException("CharacterName")),
|
||||
LimitLookupToOwnedObjects = obj["LimitLookupToOwnedObjects"]?.ToObject<bool>() ?? throw new ArgumentNullException("LimitLookupToOwnedObjects"),
|
||||
Enabled = obj["Enabled"]?.ToObject<bool>() ?? throw new ArgumentNullException("Enabled"),
|
||||
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
|
||||
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false,
|
||||
Templates = new List<Template>()
|
||||
};
|
||||
if (profile.ModifiedDate < creationDate)
|
||||
profile.ModifiedDate = creationDate;
|
||||
|
||||
if (obj["Templates"] is not JArray templateArray)
|
||||
return profile;
|
||||
|
||||
foreach (var templateObj in templateArray)
|
||||
{
|
||||
if (templateObj is not JObject templateObjCast)
|
||||
{
|
||||
//todo: warning
|
||||
continue;
|
||||
}
|
||||
|
||||
var templateId = templateObjCast["TemplateId"]?.ToObject<Guid>();
|
||||
if (templateId == null)
|
||||
continue; //todo: error
|
||||
|
||||
var template = templateManager.GetTemplate((Guid)templateId);
|
||||
if (template != null)
|
||||
profile.Templates.Add(template);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISavable
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.ProfileFile(this);
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
//saving of temporary profiles is not allowed
|
||||
if (IsTemporary)
|
||||
return;
|
||||
|
||||
using var j = new JsonTextWriter(writer)
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
var obj = JsonSerialize();
|
||||
obj.WriteTo(j);
|
||||
}
|
||||
|
||||
public string LogName(string fileName)
|
||||
=> Path.GetFileNameWithoutExtension(fileName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
46
CustomizePlus/Profiles/Events/ProfileChanged.cs
Normal file
46
CustomizePlus/Profiles/Events/ProfileChanged.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Profiles.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when profile is changed
|
||||
/// </summary>
|
||||
public sealed class ProfileChanged() : EventWrapper<ProfileChanged.Type, Profile?, object?, ProfileChanged.Priority>(nameof(ProfileChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
Created,
|
||||
Deleted,
|
||||
Renamed,
|
||||
Toggled,
|
||||
ChangedCharacterName,
|
||||
AddedTemplate,
|
||||
RemovedTemplate,
|
||||
MovedTemplate,
|
||||
ChangedTemplate,
|
||||
ReloadedAll,
|
||||
WriteProtection,
|
||||
LimitLookupToOwnedChanged,
|
||||
ChangedDefaultProfile,
|
||||
TemporaryProfileAdded,
|
||||
TemporaryProfileDeleted,
|
||||
/*
|
||||
ToggledProfile,
|
||||
AddedTemplate,
|
||||
RemovedTemplate,
|
||||
MovedTemplate,
|
||||
ChangedTemplate*/
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
ProfileFileSystemSelector = -2,
|
||||
TemplateFileSystemSelector = -1,
|
||||
ProfileFileSystem,
|
||||
ArmatureManager,
|
||||
TemplateManager,
|
||||
CustomizePlusIpc
|
||||
}
|
||||
}
|
||||
149
CustomizePlus/Profiles/ProfileFileSystem.cs
Normal file
149
CustomizePlus/Profiles/ProfileFileSystem.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Log;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
|
||||
namespace CustomizePlus.Profiles;
|
||||
|
||||
public class ProfileFileSystem : FileSystem<Profile>, IDisposable, ISavable
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly ProfileChanged _profileChanged;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ProfileFileSystem(
|
||||
ProfileManager profileManager,
|
||||
SaveService saveService,
|
||||
ProfileChanged profileChanged,
|
||||
MessageService messageService,
|
||||
Logger logger)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
_saveService = saveService;
|
||||
_profileChanged = profileChanged;
|
||||
_messageService = messageService;
|
||||
_logger = logger;
|
||||
|
||||
_profileChanged.Subscribe(OnProfileChange, ProfileChanged.Priority.ProfileFileSystem);
|
||||
|
||||
Changed += OnChange;
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_profileChanged.Unsubscribe(OnProfileChange);
|
||||
}
|
||||
|
||||
// Search the entire filesystem for the leaf corresponding to a profile.
|
||||
public bool FindLeaf(Profile profile, [NotNullWhen(true)] out Leaf? leaf)
|
||||
{
|
||||
leaf = Root.GetAllDescendants(ISortMode<Profile>.Lexicographical)
|
||||
.OfType<Leaf>()
|
||||
.FirstOrDefault(l => l.Value == profile);
|
||||
return leaf != null;
|
||||
}
|
||||
|
||||
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? data)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ProfileChanged.Type.Created:
|
||||
var parent = Root;
|
||||
if (data is string path)
|
||||
try
|
||||
{
|
||||
parent = FindOrCreateAllFolders(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messageService.NotificationMessage(ex, $"Could not move profile to {path} because the folder could not be created.", NotificationType.Error);
|
||||
}
|
||||
|
||||
CreateDuplicateLeaf(parent, profile.Name.Text, profile);
|
||||
|
||||
return;
|
||||
case ProfileChanged.Type.Deleted:
|
||||
if (FindLeaf(profile, out var leaf1))
|
||||
Delete(leaf1);
|
||||
return;
|
||||
case ProfileChanged.Type.ReloadedAll:
|
||||
Reload();
|
||||
return;
|
||||
case ProfileChanged.Type.Renamed when data is string oldName:
|
||||
if (!FindLeaf(profile, out var leaf2))
|
||||
return;
|
||||
|
||||
var old = oldName.FixName();
|
||||
if (old == leaf2.Name || leaf2.Name.IsDuplicateName(out var baseName, out _) && baseName == old)
|
||||
RenameWithDuplicates(leaf2, profile.Name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
if (Load(new FileInfo(_saveService.FileNames.ProfileFileSystem), _profileManager.Profiles, ProfileToIdentifier, ProfileToName))
|
||||
{
|
||||
var shouldReloadAgain = false;
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.ProfileFileSystem))
|
||||
shouldReloadAgain = true;
|
||||
|
||||
_saveService.ImmediateSave(this);
|
||||
|
||||
//this is a workaround for FileSystem's weird behavior where it doesn't load objects into itself if its file does not exist
|
||||
if (shouldReloadAgain)
|
||||
{
|
||||
_logger.Debug("BUG WORKAROUND: reloading profile filesystem again");
|
||||
Reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Reloaded profile filesystem.");
|
||||
}
|
||||
|
||||
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
|
||||
{
|
||||
if (type != FileSystemChangeType.Reload)
|
||||
_saveService.QueueSave(this);
|
||||
}
|
||||
|
||||
// Used for saving and loading.
|
||||
private static string ProfileToIdentifier(Profile profile)
|
||||
=> profile.UniqueId.ToString();
|
||||
|
||||
private static string ProfileToName(Profile profile)
|
||||
=> profile.Name.Text.FixName();
|
||||
|
||||
private static bool ProfileHasDefaultPath(Profile profile, string fullPath)
|
||||
{
|
||||
var regex = new Regex($@"^{Regex.Escape(ProfileToName(profile))}( \(\d+\))?$");
|
||||
return regex.IsMatch(fullPath);
|
||||
}
|
||||
|
||||
private static (string, bool) SaveProfile(Profile profile, string fullPath)
|
||||
// Only save pairs with non-default paths.
|
||||
=> ProfileHasDefaultPath(profile, fullPath)
|
||||
? (string.Empty, false)
|
||||
: (ProfileToIdentifier(profile), true);
|
||||
|
||||
public string ToFilename(FilenameService fileNames) => fileNames.ProfileFileSystem;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
SaveToFile(writer, SaveProfile, true);
|
||||
}
|
||||
}
|
||||
618
CustomizePlus/Profiles/ProfileManager.cs
Normal file
618
CustomizePlus/Profiles/ProfileManager.cs
Normal file
@@ -0,0 +1,618 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.GameData.Actors;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
using CustomizePlus.Armatures.Events;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Core.Events;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.GameData.Data;
|
||||
using CustomizePlus.GameData.Services;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
|
||||
namespace CustomizePlus.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Container class for administrating <see cref="Profile" />s during runtime.
|
||||
/// </summary>
|
||||
public class ProfileManager : IDisposable
|
||||
{
|
||||
private readonly TemplateManager _templateManager;
|
||||
private readonly TemplateEditorManager _templateEditorManager;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly Logger _logger;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly ActorService _actorService;
|
||||
private readonly ProfileChanged _event;
|
||||
private readonly TemplateChanged _templateChangedEvent;
|
||||
private readonly ReloadEvent _reloadEvent;
|
||||
private readonly ArmatureChanged _armatureChangedEvent;
|
||||
|
||||
public readonly List<Profile> Profiles = new();
|
||||
|
||||
public Profile? DefaultProfile { get; private set; }
|
||||
|
||||
public ProfileManager(
|
||||
TemplateManager templateManager,
|
||||
TemplateEditorManager templateEditorManager,
|
||||
SaveService saveService,
|
||||
Logger logger,
|
||||
PluginConfiguration configuration,
|
||||
ActorService actorService,
|
||||
ProfileChanged @event,
|
||||
TemplateChanged templateChangedEvent,
|
||||
ReloadEvent reloadEvent,
|
||||
ArmatureChanged armatureChangedEvent)
|
||||
{
|
||||
_templateManager = templateManager;
|
||||
_templateEditorManager = templateEditorManager;
|
||||
_saveService = saveService;
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_actorService = actorService;
|
||||
_event = @event;
|
||||
_templateChangedEvent = templateChangedEvent;
|
||||
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.ProfileManager);
|
||||
_reloadEvent = reloadEvent;
|
||||
_reloadEvent.Subscribe(OnReload, ReloadEvent.Priority.ProfileManager);
|
||||
_armatureChangedEvent = armatureChangedEvent;
|
||||
_armatureChangedEvent.Subscribe(OnArmatureChange, ArmatureChanged.Priority.ProfileManager);
|
||||
|
||||
CreateProfileFolder(saveService);
|
||||
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_templateChangedEvent.Unsubscribe(OnTemplateChange);
|
||||
}
|
||||
|
||||
public void LoadProfiles()
|
||||
{
|
||||
_logger.Information("Loading profiles from directory...");
|
||||
|
||||
//todo: hot reload was not tested
|
||||
//save temp profiles
|
||||
var temporaryProfiles = Profiles.Where(x => x.IsTemporary).ToList();
|
||||
|
||||
Profiles.Clear();
|
||||
List<(Profile, string)> invalidNames = new();
|
||||
foreach (var file in _saveService.FileNames.Profiles())
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(file.FullName);
|
||||
var data = JObject.Parse(text);
|
||||
var profile = Profile.Load(_templateManager, data);
|
||||
if (profile.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
|
||||
invalidNames.Add((profile, file.FullName));
|
||||
if (Profiles.Any(f => f.UniqueId == profile.UniqueId))
|
||||
throw new Exception($"ID {profile.UniqueId} was not unique.");
|
||||
|
||||
Profiles.Add(profile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Could not load profile, skipped:\n{ex}");
|
||||
//++skipped;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
//This will solve any issues if file on disk was manually edited and we have more than a single active profile
|
||||
if (profile.Enabled)
|
||||
SetEnabled(profile, true, true);
|
||||
|
||||
if (_configuration.DefaultProfile == profile.UniqueId)
|
||||
DefaultProfile = profile;
|
||||
}
|
||||
|
||||
//insert temp profiles back into profile list
|
||||
if (temporaryProfiles.Count > 0)
|
||||
{
|
||||
Profiles.AddRange(temporaryProfiles);
|
||||
Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary));
|
||||
}
|
||||
|
||||
var failed = MoveInvalidNames(invalidNames);
|
||||
if (invalidNames.Count > 0)
|
||||
_logger.Information(
|
||||
$"Moved {invalidNames.Count - failed} profiles to correct names.{(failed > 0 ? $" Failed to move {failed} profiles to correct names." : string.Empty)}");
|
||||
|
||||
_logger.Information("Directory load complete");
|
||||
_event.Invoke(ProfileChanged.Type.ReloadedAll, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main rendering function, called from rendering hook after calling ArmatureManager.OnRender
|
||||
/// </summary>
|
||||
public void OnRender()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Profile Create(string name, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var profile = new Profile
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName
|
||||
};
|
||||
|
||||
Profiles.Add(profile);
|
||||
_logger.Debug($"Added new profile {profile.UniqueId}.");
|
||||
|
||||
_saveService.ImmediateSave(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.Created, profile, path);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new profile by cloning passed profile
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Profile Clone(Profile clone, string name, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var profile = new Profile(clone)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Enabled = false
|
||||
};
|
||||
|
||||
Profiles.Add(profile);
|
||||
_logger.Debug($"Added new profile {profile.UniqueId} by cloning.");
|
||||
|
||||
_saveService.ImmediateSave(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.Created, profile, path);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rename profile
|
||||
/// </summary>
|
||||
public void Rename(Profile profile, string newName)
|
||||
{
|
||||
var oldName = profile.Name.Text;
|
||||
if (oldName == newName)
|
||||
return;
|
||||
|
||||
profile.Name = newName;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Renamed profile {profile.UniqueId}.");
|
||||
_event.Invoke(ProfileChanged.Type.Renamed, profile, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change character name for profile
|
||||
/// </summary>
|
||||
public void ChangeCharacterName(Profile profile, string newName)
|
||||
{
|
||||
var oldName = profile.CharacterName.Text;
|
||||
if (oldName == newName)
|
||||
return;
|
||||
|
||||
profile.CharacterName = newName;
|
||||
|
||||
//Called so all other active profiles for new character name get disabled
|
||||
//saving is performed there
|
||||
SetEnabled(profile, profile.Enabled, true);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Changed character name for profile {profile.UniqueId}.");
|
||||
_event.Invoke(ProfileChanged.Type.ChangedCharacterName, profile, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete profile
|
||||
/// </summary>
|
||||
/// <param name="profile"></param>
|
||||
public void Delete(Profile profile)
|
||||
{
|
||||
Profiles.Remove(profile);
|
||||
_saveService.ImmediateDelete(profile);
|
||||
_event.Invoke(ProfileChanged.Type.Deleted, profile, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set write protection state for profile
|
||||
/// </summary>
|
||||
public void SetWriteProtection(Profile profile, bool value)
|
||||
{
|
||||
if (profile.IsWriteProtected == value)
|
||||
return;
|
||||
|
||||
profile.IsWriteProtected = value;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Set profile {profile.UniqueId} to {(value ? string.Empty : "no longer be ")} write-protected.");
|
||||
_event.Invoke(ProfileChanged.Type.WriteProtection, profile, value);
|
||||
}
|
||||
|
||||
public void SetEnabled(Profile profile, bool value, bool force = false)
|
||||
{
|
||||
if (profile.Enabled == value && !force)
|
||||
return;
|
||||
|
||||
var oldValue = profile.Enabled;
|
||||
|
||||
if (value)
|
||||
{
|
||||
_logger.Debug($"Setting {profile} as enabled...");
|
||||
|
||||
foreach (var otherProfile in Profiles
|
||||
.Where(x => x.CharacterName == profile.CharacterName && x != profile && x.Enabled && !x.IsTemporary))
|
||||
{
|
||||
_logger.Debug($"\t-> {otherProfile} disabled");
|
||||
SetEnabled(otherProfile, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldValue != value)
|
||||
{
|
||||
profile.Enabled = value;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.Toggled, profile, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLimitLookupToOwned(Profile profile, bool value)
|
||||
{
|
||||
if (profile.LimitLookupToOwnedObjects != value)
|
||||
{
|
||||
profile.LimitLookupToOwnedObjects = value;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.LimitLookupToOwnedChanged, profile, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteTemplate(Profile profile, int templateIndex)
|
||||
{
|
||||
_logger.Debug($"Deleting template #{templateIndex} from {profile}...");
|
||||
|
||||
var template = profile.Templates[templateIndex];
|
||||
profile.Templates.RemoveAt(templateIndex);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.RemovedTemplate, profile, template);
|
||||
}
|
||||
|
||||
public void AddTemplate(Profile profile, Template template)
|
||||
{
|
||||
if (profile.Templates.Contains(template))
|
||||
return;
|
||||
|
||||
profile.Templates.Add(template);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Added template: {template.UniqueId} to {profile.UniqueId}");
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.AddedTemplate, profile, template);
|
||||
}
|
||||
|
||||
public void ChangeTemplate(Profile profile, int index, Template newTemplate)
|
||||
{
|
||||
if (index >= profile.Templates.Count || index < 0)
|
||||
return;
|
||||
|
||||
if (profile.Templates[index] == newTemplate)
|
||||
return;
|
||||
|
||||
var oldTemplate = profile.Templates[index];
|
||||
profile.Templates[index] = newTemplate;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Changed template on profile {profile.UniqueId} from {oldTemplate.UniqueId} to {newTemplate.UniqueId}");
|
||||
_event.Invoke(ProfileChanged.Type.ChangedTemplate, profile, (index, oldTemplate, newTemplate));
|
||||
}
|
||||
|
||||
public void MoveTemplate(Profile profile, int fromIndex, int toIndex)
|
||||
{
|
||||
if (!profile.Templates.Move(fromIndex, toIndex))
|
||||
return;
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Moved template {fromIndex + 1} to position {toIndex + 1}.");
|
||||
_event.Invoke(ProfileChanged.Type.MovedTemplate, profile, (fromIndex, toIndex));
|
||||
}
|
||||
|
||||
public void SetDefaultProfile(Profile? profile)
|
||||
{
|
||||
if (profile == null)
|
||||
{
|
||||
if (DefaultProfile == null)
|
||||
return;
|
||||
}
|
||||
else if (!Profiles.Contains(profile))
|
||||
return;
|
||||
|
||||
var previousProfile = DefaultProfile;
|
||||
|
||||
DefaultProfile = profile;
|
||||
_configuration.DefaultProfile = profile?.UniqueId ?? Guid.Empty;
|
||||
_configuration.Save();
|
||||
|
||||
_logger.Debug($"Set profile {profile?.Incognito ?? "no profile"} as default");
|
||||
_event.Invoke(ProfileChanged.Type.ChangedDefaultProfile, profile, previousProfile);
|
||||
}
|
||||
|
||||
public void AddTemporaryProfile(Profile profile, Actor actor/*, Template template*/)
|
||||
{
|
||||
if (!actor.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return;
|
||||
|
||||
profile.Enabled = true;
|
||||
profile.IsTemporary = true;
|
||||
profile.TemporaryActor = identifier;
|
||||
profile.CharacterName = identifier.ToNameWithoutOwnerName();
|
||||
profile.LimitLookupToOwnedObjects = false;
|
||||
|
||||
var existingProfile = Profiles.FirstOrDefault(x => x.CharacterName.Lower == profile.CharacterName.Lower && x.IsTemporary);
|
||||
if (existingProfile != null)
|
||||
{
|
||||
_logger.Debug($"Temporary profile for {existingProfile.CharacterName} already exists, removing...");
|
||||
Profiles.Remove(existingProfile);
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, existingProfile, null);
|
||||
}
|
||||
|
||||
Profiles.Add(profile);
|
||||
|
||||
//Make sure temporary profiles come first, so they are returned by all other methods first
|
||||
Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary));
|
||||
|
||||
_logger.Debug($"Added temporary profile for {identifier}");
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileAdded, profile, null);
|
||||
}
|
||||
|
||||
public void RemoveTemporaryProfile(Profile profile)
|
||||
{
|
||||
if (!Profiles.Remove(profile))
|
||||
return;
|
||||
|
||||
_logger.Debug($"Removed temporary profile for {profile.CharacterName}");
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
|
||||
}
|
||||
|
||||
public void RemoveTemporaryProfile(Actor actor)
|
||||
{
|
||||
if (!actor.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return;
|
||||
|
||||
var profile = Profiles.FirstOrDefault(x => x.TemporaryActor == identifier && x.IsTemporary);
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
RemoveTemporaryProfile(profile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return profile by character name, does not return temporary profiles
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="enabledOnly"></param>
|
||||
/// <returns></returns>
|
||||
public Profile? GetProfileByCharacterName(string name, bool enabledOnly = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return null;
|
||||
|
||||
var query = Profiles.Where(x => x.CharacterName == name);
|
||||
if (enabledOnly)
|
||||
query = query.Where(x => x.Enabled);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
//todo: replace with dictionary
|
||||
/// <summary>
|
||||
/// Returns all enabled profiles which might apply to the given object, prioritizing temporary profiles and editor profile.
|
||||
/// </summary>
|
||||
public IEnumerable<Profile> GetEnabledProfilesByActor(ActorIdentifier actorIdentifier)
|
||||
{
|
||||
//performance: using textual override for ProfileAppliesTo here to not call
|
||||
//GetGameObjectName every time we are trying to check object against profiles
|
||||
|
||||
if (actorIdentifier.Type == IdentifierType.Special)
|
||||
actorIdentifier = actorIdentifier.GetTrueActorForSpecialType();
|
||||
|
||||
if (!actorIdentifier.IsValid)
|
||||
yield break;
|
||||
|
||||
var name = actorIdentifier.ToNameWithoutOwnerName();
|
||||
|
||||
if (name.IsNullOrWhitespace())
|
||||
yield break;
|
||||
|
||||
if (_templateEditorManager.IsEditorActive && _templateEditorManager.EditorProfile.Enabled)
|
||||
{
|
||||
if (ProfileAppliesTo(_templateEditorManager.EditorProfile, name))
|
||||
{
|
||||
yield return _templateEditorManager.EditorProfile;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
if (ProfileAppliesTo(profile, name) && profile.Enabled)
|
||||
yield return profile;
|
||||
}
|
||||
|
||||
if (DefaultProfile != null &&
|
||||
DefaultProfile.Enabled &&
|
||||
(actorIdentifier.Type == IdentifierType.Player || actorIdentifier.Type == IdentifierType.Retainer))
|
||||
yield return DefaultProfile;
|
||||
}
|
||||
|
||||
public IEnumerable<Profile> GetProfilesUsingTemplate(Template template)
|
||||
{
|
||||
if (template == null)
|
||||
yield break;
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
if (profile.Templates.Contains(template))
|
||||
yield return profile;
|
||||
|
||||
if (_templateEditorManager.EditorProfile.Templates.Contains(template))
|
||||
yield return _templateEditorManager.EditorProfile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not profile applies to the object with the indicated name.
|
||||
/// </summary>
|
||||
public bool ProfileAppliesTo(Profile profile, string objectName) => !string.IsNullOrWhiteSpace(objectName) && objectName == profile.CharacterName.Text;
|
||||
|
||||
private void SaveProfile(Profile profile)
|
||||
{
|
||||
profile.ModifiedDate = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(profile);
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Template? template, object? arg3)
|
||||
{
|
||||
if (type is not TemplateChanged.Type.Deleted)
|
||||
return;
|
||||
|
||||
foreach (var profile in Profiles)
|
||||
{
|
||||
for (var i = 0; i < profile.Templates.Count; ++i)
|
||||
{
|
||||
if (profile.Templates[i] != template)
|
||||
continue;
|
||||
|
||||
profile.Templates.RemoveAt(i--);
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.RemovedTemplate, profile, template);
|
||||
|
||||
SaveProfile(profile);
|
||||
|
||||
_logger.Debug($"Removed template {template.UniqueId} from {profile.UniqueId} because template was deleted");
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnReload(ReloadEvent.Type type)
|
||||
{
|
||||
if (type != ReloadEvent.Type.ReloadProfiles &&
|
||||
type != ReloadEvent.Type.ReloadAll)
|
||||
return;
|
||||
|
||||
_logger.Debug("Reload event received");
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
|
||||
private void OnArmatureChange(ArmatureChanged.Type type, Armature? armature, object? arg3)
|
||||
{
|
||||
if (type == ArmatureChanged.Type.Deleted)
|
||||
{
|
||||
//hack: sending TemporaryProfileDeleted will result in OnArmatureChange being sent
|
||||
//so we need to make sure that we do not end up with endless loop here
|
||||
//the whole reason DeletionReason exists is this
|
||||
if ((ArmatureChanged.DeletionReason)arg3 != ArmatureChanged.DeletionReason.Gone)
|
||||
return;
|
||||
|
||||
var profile = armature!.Profile;
|
||||
|
||||
//todo: TemporaryProfileDeleted ends up calling this again, fix this.
|
||||
//Profiles.Remove check won't allow for infinite loop but this isn't good anyway
|
||||
if (!profile.IsTemporary || !Profiles.Remove(profile))
|
||||
return;
|
||||
|
||||
_logger.Debug($"ProfileManager.OnArmatureChange: Removed unused temporary profile for {profile.CharacterName}");
|
||||
|
||||
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateProfileFolder(SaveService service)
|
||||
{
|
||||
var ret = service.FileNames.ProfileDirectory;
|
||||
if (Directory.Exists(ret))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"Could not create profile directory {ret}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Move all files that were discovered to have names not corresponding to their identifier to correct names, if possible. </summary>
|
||||
/// <returns>The number of files that could not be moved.</returns>
|
||||
private int MoveInvalidNames(IEnumerable<(Profile, string)> invalidNames)
|
||||
{
|
||||
var failed = 0;
|
||||
foreach (var (profile, name) in invalidNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var correctName = _saveService.FileNames.ProfileFile(profile);
|
||||
File.Move(name, correctName, false);
|
||||
_logger.Information($"Moved invalid profile file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
++failed;
|
||||
_logger.Error($"Failed to move invalid profile file from {Path.GetFileName(name)}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
return failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new guid until we find one which isn't used by existing profile
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Guid CreateNewGuid()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
if (Profiles.All(d => d.UniqueId != guid))
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
128
CustomizePlus/Templates/Data/Template.cs
Normal file
128
CustomizePlus/Templates/Data/Template.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Extensions;
|
||||
using CustomizePlus.Core.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace CustomizePlus.Templates.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates the user-controlled aspects of a template, ie all of
|
||||
/// the information that gets saved to disk by the plugin.
|
||||
/// </summary>
|
||||
public sealed class Template : ISavable
|
||||
{
|
||||
public LowerString Name { get; internal set; } = "Template";
|
||||
|
||||
public int Version { get; internal set; } = Constants.ConfigurationVersion;
|
||||
public DateTimeOffset CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTimeOffset ModifiedDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public Guid UniqueId { get; internal set; } = Guid.NewGuid();
|
||||
|
||||
public bool IsWriteProtected { get; internal set; }
|
||||
|
||||
public string Incognito
|
||||
=> UniqueId.ToString()[..8];
|
||||
|
||||
public Dictionary<string, BoneTransform> Bones { get; init; } = new();
|
||||
|
||||
public Template()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new template based on bone data from another one
|
||||
/// </summary>
|
||||
public Template(Template original) : this()
|
||||
{
|
||||
foreach (var kvp in original.Bones)
|
||||
{
|
||||
Bones[kvp.Key] = new BoneTransform();
|
||||
Bones[kvp.Key].UpdateToMatch(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Template '{Name.Text.Incognify()}' with {Bones.Count} bone edits [{UniqueId}]";
|
||||
}
|
||||
|
||||
#region Serialization
|
||||
|
||||
public new JObject JsonSerialize()
|
||||
{
|
||||
var ret = new JObject()
|
||||
{
|
||||
["Version"] = Version,
|
||||
["UniqueId"] = UniqueId,
|
||||
["CreationDate"] = CreationDate,
|
||||
["ModifiedDate"] = ModifiedDate,
|
||||
["Name"] = Name.Text,
|
||||
["Bones"] = JObject.FromObject(Bones),
|
||||
["IsWriteProtected"] = IsWriteProtected
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deserialization
|
||||
|
||||
public static Template Load(JObject obj)
|
||||
{
|
||||
var version = obj["Version"]?.ToObject<int>() ?? 0;
|
||||
return version switch
|
||||
{
|
||||
//Did not exist before v4
|
||||
4 => LoadV4(obj),
|
||||
_ => throw new Exception("The design to be loaded has no valid Version."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Template LoadV4(JObject obj)
|
||||
{
|
||||
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
|
||||
|
||||
var template = new Template()
|
||||
{
|
||||
CreationDate = creationDate,
|
||||
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
|
||||
Name = new LowerString(obj["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
|
||||
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
|
||||
Bones = obj["Bones"]?.ToObject<Dictionary<string, BoneTransform>>() ?? throw new ArgumentNullException("Bones"),
|
||||
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false
|
||||
};
|
||||
if (template.ModifiedDate < creationDate)
|
||||
template.ModifiedDate = creationDate;
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISavable
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.TemplateFile(this);
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var j = new JsonTextWriter(writer)
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
var obj = JsonSerialize();
|
||||
obj.WriteTo(j);
|
||||
}
|
||||
|
||||
public string LogName(string fileName)
|
||||
=> Path.GetFileNameWithoutExtension(fileName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
36
CustomizePlus/Templates/Events/TemplateChanged.cs
Normal file
36
CustomizePlus/Templates/Events/TemplateChanged.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using CustomizePlus.Templates.Data;
|
||||
using OtterGui.Classes;
|
||||
using System;
|
||||
|
||||
namespace CustomizePlus.Templates.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when Template is changed
|
||||
/// </summary>
|
||||
public class TemplateChanged() : EventWrapper<TemplateChanged.Type, Template?, object?, TemplateChanged.Priority>(nameof(TemplateChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
Created,
|
||||
Deleted,
|
||||
Renamed,
|
||||
NewBone,
|
||||
UpdatedBone,
|
||||
DeletedBone,
|
||||
EditorEnabled,
|
||||
EditorDisabled,
|
||||
EditorCharacterChanged,
|
||||
ReloadedAll,
|
||||
WriteProtection
|
||||
}
|
||||
|
||||
public enum Priority
|
||||
{
|
||||
TemplateCombo = -2,
|
||||
TemplateFileSystemSelector = -1,
|
||||
TemplateFileSystem,
|
||||
ArmatureManager,
|
||||
ProfileManager,
|
||||
CustomizePlusIpc
|
||||
}
|
||||
}
|
||||
288
CustomizePlus/Templates/TemplateEditorManager.cs
Normal file
288
CustomizePlus/Templates/TemplateEditorManager.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Game.Events;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CustomizePlus.Templates;
|
||||
|
||||
public class TemplateEditorManager : IDisposable
|
||||
{
|
||||
private readonly TemplateChanged _event;
|
||||
private readonly GPoseStateChanged _gposeStateChanged;
|
||||
private readonly Logger _logger;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
private readonly TemplateManager _templateManager;
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the original template which is currently being edited, should not be edited!
|
||||
/// </summary>
|
||||
private Template _currentlyEditedTemplateOriginal;
|
||||
|
||||
/// <summary>
|
||||
/// Internal profile for the editor
|
||||
/// </summary>
|
||||
public Profile EditorProfile { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Original ID of the template which is currently being edited
|
||||
/// </summary>
|
||||
public Guid CurrentlyEditedTemplateId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A copy of currently edited template, all changes must be done on this template
|
||||
/// </summary>
|
||||
public Template? CurrentlyEditedTemplate { get; private set; }
|
||||
|
||||
public bool IsEditorActive { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is editor currently paused? Happens automatically when editor is not compatible with the current game state.
|
||||
/// Keeps editor state frozen and prevents any changes to it, also sets editor profile as disabled.
|
||||
/// </summary>
|
||||
public bool IsEditorPaused { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if there are any changes in current editing session or not
|
||||
/// </summary>
|
||||
public bool HasChanges { get; private set; }
|
||||
|
||||
public bool IsKeepOnlyEditorProfileActive { get; set; } //todo
|
||||
|
||||
public TemplateEditorManager(
|
||||
TemplateChanged @event,
|
||||
GPoseStateChanged gposeStateChanged,
|
||||
Logger logger,
|
||||
TemplateManager templateManager,
|
||||
GameObjectService gameObjectService)
|
||||
{
|
||||
_event = @event;
|
||||
_gposeStateChanged = gposeStateChanged;
|
||||
_logger = logger;
|
||||
_templateManager = templateManager;
|
||||
_gameObjectService = gameObjectService;
|
||||
|
||||
_gposeStateChanged.Subscribe(OnGPoseStateChanged, GPoseStateChanged.Priority.TemplateEditorManager);
|
||||
|
||||
EditorProfile = new Profile() { Templates = new List<Template>(), Enabled = false, Name = "Template editor profile" };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gposeStateChanged.Unsubscribe(OnGPoseStateChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn on editing of a specific template. If character name not set will default to local player.
|
||||
/// </summary>
|
||||
internal bool EnableEditor(Template template, string? characterName = null) //todo: editor is borked
|
||||
{
|
||||
if (IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
_logger.Debug($"Enabling editor profile for {template.Name} via character {characterName}");
|
||||
|
||||
CurrentlyEditedTemplateId = template.UniqueId;
|
||||
_currentlyEditedTemplateOriginal = template;
|
||||
CurrentlyEditedTemplate = new Template(template)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = Guid.NewGuid(),
|
||||
Name = "Template editor temporary template"
|
||||
};
|
||||
|
||||
if (characterName != null)
|
||||
EditorProfile.CharacterName = characterName;
|
||||
else //safeguard
|
||||
EditorProfile.CharacterName = _gameObjectService.GetCurrentPlayerName();
|
||||
|
||||
EditorProfile.Templates.Clear(); //safeguard
|
||||
EditorProfile.Templates.Add(CurrentlyEditedTemplate);
|
||||
EditorProfile.Enabled = true;
|
||||
HasChanges = false;
|
||||
IsEditorActive = true;
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.EditorEnabled, template, characterName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn off editing of a specific template
|
||||
/// </summary>
|
||||
internal bool DisableEditor()
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
_logger.Debug($"Disabling editor profile");
|
||||
|
||||
string characterName = EditorProfile.CharacterName;
|
||||
|
||||
CurrentlyEditedTemplateId = Guid.Empty;
|
||||
CurrentlyEditedTemplate = null;
|
||||
EditorProfile.Enabled = false;
|
||||
EditorProfile.CharacterName = "";
|
||||
EditorProfile.Templates.Clear();
|
||||
IsEditorActive = false;
|
||||
HasChanges = false;
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.EditorDisabled, null, characterName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SaveChanges(bool asCopy = false)
|
||||
{
|
||||
var targetTemplate = _templateManager.GetTemplate(CurrentlyEditedTemplateId);
|
||||
if (targetTemplate == null)
|
||||
throw new Exception($"Fatal editor error: Template with ID {CurrentlyEditedTemplateId} not found in template manager");
|
||||
|
||||
if (asCopy)
|
||||
targetTemplate = _templateManager.Clone(targetTemplate, $"{targetTemplate.Name} - Copy {Guid.NewGuid().ToString().Substring(0, 4)}", false);
|
||||
|
||||
_templateManager.ApplyBoneChangesAndSave(targetTemplate, CurrentlyEditedTemplate!);
|
||||
}
|
||||
|
||||
public bool ChangeEditorCharacter(string characterName)
|
||||
{
|
||||
if (!IsEditorActive || EditorProfile.CharacterName == characterName || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
_logger.Debug($"Changing character name for editor profile from {EditorProfile.CharacterName} to {characterName}");
|
||||
|
||||
EditorProfile.CharacterName = characterName;
|
||||
_event.Invoke(TemplateChanged.Type.EditorCharacterChanged, CurrentlyEditedTemplate, (characterName, EditorProfile));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets changes in currently edited template to default values
|
||||
/// </summary>
|
||||
public bool ResetBoneAttributeChanges(string boneName, BoneAttribute attribute)
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
if (!CurrentlyEditedTemplate!.Bones.ContainsKey(boneName))
|
||||
return false;
|
||||
|
||||
var resetValue = GetResetValueForAttribute(attribute);
|
||||
|
||||
switch (attribute)
|
||||
{
|
||||
case BoneAttribute.Position:
|
||||
if (resetValue == CurrentlyEditedTemplate!.Bones[boneName].Translation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Rotation:
|
||||
if (resetValue == CurrentlyEditedTemplate!.Bones[boneName].Rotation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Scale:
|
||||
if (resetValue == CurrentlyEditedTemplate!.Bones[boneName].Scaling)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
CurrentlyEditedTemplate!.Bones[boneName].UpdateAttribute(attribute, resetValue);
|
||||
|
||||
if (!HasChanges)
|
||||
HasChanges = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts changes in currently edited template to values set in saved copy of the template.
|
||||
/// Resets to default value if saved copy doesn't have that bone edited
|
||||
/// </summary>
|
||||
public bool RevertBoneAttributeChanges(string boneName, BoneAttribute attribute)
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
if (!CurrentlyEditedTemplate!.Bones.ContainsKey(boneName))
|
||||
return false;
|
||||
|
||||
Vector3? originalValue = null!;
|
||||
|
||||
if (_currentlyEditedTemplateOriginal.Bones.ContainsKey(boneName))
|
||||
{
|
||||
switch (attribute)
|
||||
{
|
||||
case BoneAttribute.Position:
|
||||
originalValue = _currentlyEditedTemplateOriginal.Bones[boneName].Translation;
|
||||
if (originalValue == CurrentlyEditedTemplate!.Bones[boneName].Translation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Rotation:
|
||||
originalValue = _currentlyEditedTemplateOriginal.Bones[boneName].Rotation;
|
||||
if (originalValue == CurrentlyEditedTemplate!.Bones[boneName].Rotation)
|
||||
return false;
|
||||
break;
|
||||
case BoneAttribute.Scale:
|
||||
originalValue = _currentlyEditedTemplateOriginal.Bones[boneName].Scaling;
|
||||
if (originalValue == CurrentlyEditedTemplate!.Bones[boneName].Scaling)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
originalValue = GetResetValueForAttribute(attribute);
|
||||
|
||||
CurrentlyEditedTemplate!.Bones[boneName].UpdateAttribute(attribute, originalValue.Value);
|
||||
|
||||
if (!HasChanges)
|
||||
HasChanges = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ModifyBoneTransform(string boneName, BoneTransform transform)
|
||||
{
|
||||
if (!IsEditorActive || IsEditorPaused)
|
||||
return false;
|
||||
|
||||
if (!_templateManager.ModifyBoneTransform(CurrentlyEditedTemplate!, boneName, transform))
|
||||
return false;
|
||||
|
||||
if (!HasChanges)
|
||||
HasChanges = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector3 GetResetValueForAttribute(BoneAttribute attribute)
|
||||
{
|
||||
switch (attribute)
|
||||
{
|
||||
case BoneAttribute.Scale:
|
||||
return Vector3.One;
|
||||
default:
|
||||
return Vector3.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGPoseStateChanged(GPoseStateChanged.Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case GPoseStateChanged.Type.Entered:
|
||||
IsEditorPaused = true;
|
||||
EditorProfile.Enabled = false;
|
||||
break;
|
||||
case GPoseStateChanged.Type.Exited:
|
||||
EditorProfile.Enabled = true;
|
||||
IsEditorPaused = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
150
CustomizePlus/Templates/TemplateFileSystem.cs
Normal file
150
CustomizePlus/Templates/TemplateFileSystem.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
|
||||
namespace CustomizePlus.Templates;
|
||||
|
||||
//Adapted from glamourer source code
|
||||
public sealed class TemplateFileSystem : FileSystem<Template>, IDisposable, ISavable
|
||||
{
|
||||
private readonly TemplateManager _templateManager;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly TemplateChanged _templateChanged;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public TemplateFileSystem(
|
||||
TemplateManager templateManager,
|
||||
SaveService saveService,
|
||||
TemplateChanged templateChanged,
|
||||
MessageService messageService,
|
||||
Logger logger)
|
||||
{
|
||||
_templateManager = templateManager;
|
||||
_saveService = saveService;
|
||||
_templateChanged = templateChanged;
|
||||
_messageService = messageService;
|
||||
_logger = logger;
|
||||
|
||||
_templateChanged.Subscribe(OnTemplateChange, TemplateChanged.Priority.TemplateFileSystem);
|
||||
|
||||
Changed += OnChange;
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_templateChanged.Unsubscribe(OnTemplateChange);
|
||||
}
|
||||
|
||||
// Search the entire filesystem for the leaf corresponding to a template.
|
||||
public bool FindLeaf(Template template, [NotNullWhen(true)] out Leaf? leaf)
|
||||
{
|
||||
leaf = Root.GetAllDescendants(ISortMode<Template>.Lexicographical)
|
||||
.OfType<Leaf>()
|
||||
.FirstOrDefault(l => l.Value == template);
|
||||
return leaf != null;
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Template? template, object? data)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TemplateChanged.Type.Created:
|
||||
var parent = Root;
|
||||
if (data is string path)
|
||||
try
|
||||
{
|
||||
parent = FindOrCreateAllFolders(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messageService.NotificationMessage(ex, $"Could not move template to {path} because the folder could not be created.", NotificationType.Error);
|
||||
}
|
||||
|
||||
CreateDuplicateLeaf(parent, template.Name.Text, template);
|
||||
|
||||
return;
|
||||
case TemplateChanged.Type.Deleted:
|
||||
if (FindLeaf(template, out var leaf1))
|
||||
Delete(leaf1);
|
||||
return;
|
||||
case TemplateChanged.Type.ReloadedAll:
|
||||
Reload();
|
||||
return;
|
||||
case TemplateChanged.Type.Renamed when data is string oldName:
|
||||
if (!FindLeaf(template, out var leaf2))
|
||||
return;
|
||||
|
||||
var old = oldName.FixName();
|
||||
if (old == leaf2.Name || leaf2.Name.IsDuplicateName(out var baseName, out _) && baseName == old)
|
||||
RenameWithDuplicates(leaf2, template.Name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
if (Load(new FileInfo(_saveService.FileNames.TemplateFileSystem), _templateManager.Templates, TemplateToIdentifier, TemplateToName))
|
||||
{
|
||||
var shouldReloadAgain = false;
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.TemplateFileSystem))
|
||||
shouldReloadAgain = true;
|
||||
|
||||
_saveService.ImmediateSave(this);
|
||||
|
||||
//this is a workaround for FileSystem's weird behavior where it doesn't load objects into itself if its file does not exist
|
||||
if (shouldReloadAgain)
|
||||
{
|
||||
_logger.Debug("BUG WORKAROUND: reloading template filesystem again");
|
||||
Reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Reloaded template filesystem.");
|
||||
}
|
||||
|
||||
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
|
||||
{
|
||||
if (type != FileSystemChangeType.Reload)
|
||||
_saveService.QueueSave(this);
|
||||
}
|
||||
|
||||
// Used for saving and loading.
|
||||
private static string TemplateToIdentifier(Template template)
|
||||
=> template.UniqueId.ToString();
|
||||
|
||||
private static string TemplateToName(Template template)
|
||||
=> template.Name.Text.FixName();
|
||||
|
||||
private static bool TemplateHasDefaultPath(Template template, string fullPath)
|
||||
{
|
||||
var regex = new Regex($@"^{Regex.Escape(TemplateToName(template))}( \(\d+\))?$");
|
||||
return regex.IsMatch(fullPath);
|
||||
}
|
||||
|
||||
private static (string, bool) SaveTemplate(Template template, string fullPath)
|
||||
// Only save pairs with non-default paths.
|
||||
=> TemplateHasDefaultPath(template, fullPath)
|
||||
? (string.Empty, false)
|
||||
: (TemplateToIdentifier(template), true);
|
||||
|
||||
public string ToFilename(FilenameService fileNames) => fileNames.TemplateFileSystem;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
SaveToFile(writer, SaveTemplate, true);
|
||||
}
|
||||
}
|
||||
332
CustomizePlus/Templates/TemplateManager.cs
Normal file
332
CustomizePlus/Templates/TemplateManager.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Core.Events;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Log;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace CustomizePlus.Templates;
|
||||
|
||||
public class TemplateManager
|
||||
{
|
||||
private readonly SaveService _saveService;
|
||||
private readonly Logger _logger;
|
||||
private readonly TemplateChanged _event;
|
||||
private readonly ReloadEvent _reloadEvent;
|
||||
|
||||
private readonly List<Template> _templates = new();
|
||||
|
||||
public IReadOnlyList<Template> Templates
|
||||
=> _templates;
|
||||
|
||||
public TemplateManager(
|
||||
SaveService saveService,
|
||||
Logger logger,
|
||||
TemplateChanged @event,
|
||||
ReloadEvent reloadEvent)
|
||||
{
|
||||
_saveService = saveService;
|
||||
_logger = logger;
|
||||
_event = @event;
|
||||
_reloadEvent = reloadEvent;
|
||||
_reloadEvent.Subscribe(OnReload, ReloadEvent.Priority.TemplateManager);
|
||||
|
||||
CreateTemplateFolder(saveService);
|
||||
LoadTemplates();
|
||||
}
|
||||
|
||||
public Template? GetTemplate(Guid templateId) => _templates.FirstOrDefault(d => d.UniqueId == templateId);
|
||||
|
||||
public void LoadTemplates()
|
||||
{
|
||||
_logger.Information("Loading templates from directory...");
|
||||
|
||||
_templates.Clear();
|
||||
List<(Template, string)> invalidNames = new();
|
||||
foreach (var file in _saveService.FileNames.Templates())
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(file.FullName);
|
||||
var data = JObject.Parse(text);
|
||||
var template = Template.Load(data);
|
||||
if (template.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
|
||||
invalidNames.Add((template, file.FullName));
|
||||
if (_templates.Any(f => f.UniqueId == template.UniqueId))
|
||||
throw new Exception($"ID {template.UniqueId} was not unique.");
|
||||
|
||||
PruneIdempotentTransforms(template);
|
||||
|
||||
_templates.Add(template);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Could not load template, skipped:\n{ex}");
|
||||
//++skipped;
|
||||
}
|
||||
}
|
||||
|
||||
var failed = MoveInvalidNames(invalidNames);
|
||||
if (invalidNames.Count > 0)
|
||||
_logger.Information(
|
||||
$"Moved {invalidNames.Count - failed} templates to correct names.{(failed > 0 ? $" Failed to move {failed} templates to correct names." : string.Empty)}");
|
||||
|
||||
_logger.Information("Directory load complete");
|
||||
_event.Invoke(TemplateChanged.Type.ReloadedAll, null, null);
|
||||
}
|
||||
|
||||
public Template Create(string name, Dictionary<string, BoneTransform>? bones, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var template = new Template
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName,
|
||||
Bones = bones != null && bones.Count > 0 ? new Dictionary<string, BoneTransform>(bones) : new()
|
||||
};
|
||||
|
||||
if (template.Bones.Count > 0)
|
||||
PruneIdempotentTransforms(template);
|
||||
|
||||
_templates.Add(template);
|
||||
_logger.Debug($"Added new template {template.UniqueId}.");
|
||||
|
||||
_saveService.ImmediateSave(template);
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.Created, template, path);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
public Template Create(string name, bool handlePath)
|
||||
{
|
||||
return Create(name, null, handlePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new template by cloning passed template
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Template Clone(Template clone, string name, bool handlePath)
|
||||
{
|
||||
var (actualName, path) = NameParsingHelper.ParseName(name, handlePath);
|
||||
var template = new Template(clone)
|
||||
{
|
||||
CreationDate = DateTimeOffset.UtcNow,
|
||||
ModifiedDate = DateTimeOffset.UtcNow,
|
||||
UniqueId = CreateNewGuid(),
|
||||
Name = actualName
|
||||
};
|
||||
|
||||
_templates.Add(template);
|
||||
_logger.Debug($"Added new template {template.UniqueId} by cloning.");
|
||||
|
||||
_saveService.ImmediateSave(template);
|
||||
|
||||
_event.Invoke(TemplateChanged.Type.Created, template, path);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rename template
|
||||
/// </summary>
|
||||
public void Rename(Template template, string newName)
|
||||
{
|
||||
var oldName = template.Name.Text;
|
||||
if (oldName == newName)
|
||||
return;
|
||||
|
||||
template.Name = newName;
|
||||
|
||||
SaveTemplate(template);
|
||||
|
||||
_logger.Debug($"Renamed template {template.UniqueId}.");
|
||||
_event.Invoke(TemplateChanged.Type.Renamed, template, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete template
|
||||
/// </summary>
|
||||
/// <param name="template"></param>
|
||||
public void Delete(Template template)
|
||||
{
|
||||
_templates.Remove(template);
|
||||
_saveService.ImmediateDelete(template);
|
||||
_event.Invoke(TemplateChanged.Type.Deleted, template, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set write protection state for template
|
||||
/// </summary>
|
||||
public void SetWriteProtection(Template template, bool value)
|
||||
{
|
||||
if (template.IsWriteProtected == value)
|
||||
return;
|
||||
|
||||
template.IsWriteProtected = value;
|
||||
|
||||
SaveTemplate(template);
|
||||
|
||||
_logger.Debug($"Set template {template.UniqueId} to {(value ? string.Empty : "no longer be ")} write-protected.");
|
||||
_event.Invoke(TemplateChanged.Type.WriteProtection, template, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy bone data from source template to target template and queue a save for target template
|
||||
/// </summary>
|
||||
/// <param name="targetTemplate"></param>
|
||||
/// <param name="sourceTemplate"></param>
|
||||
public void ApplyBoneChangesAndSave(Template targetTemplate, Template sourceTemplate)
|
||||
{
|
||||
_logger.Debug($"Copying bones from {sourceTemplate.Name} to {targetTemplate.Name}");
|
||||
var deletedBones = targetTemplate.Bones.Keys.Except(sourceTemplate.Bones.Keys).ToList();
|
||||
foreach (var kvPair in sourceTemplate.Bones)
|
||||
{
|
||||
ModifyBoneTransform(targetTemplate, kvPair.Key, kvPair.Value);
|
||||
}
|
||||
|
||||
foreach (var boneName in deletedBones)
|
||||
{
|
||||
DeleteBoneTransform(targetTemplate, boneName);
|
||||
}
|
||||
|
||||
SaveTemplate(targetTemplate);
|
||||
}
|
||||
|
||||
//Creates, updates or deletes bone transform
|
||||
//not to be used on editor-related features by anything but TemplateEditorManager
|
||||
public bool ModifyBoneTransform(Template template, string boneName, BoneTransform transform)
|
||||
{
|
||||
if (template.Bones.TryGetValue(boneName, out var boneTransform)
|
||||
&& boneTransform != null)
|
||||
{
|
||||
if (boneTransform == transform)
|
||||
return false;
|
||||
|
||||
if (transform.IsEdited())
|
||||
{
|
||||
template.Bones[boneName].UpdateToMatch(transform);
|
||||
|
||||
_logger.Debug($"Updated bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.UpdatedBone, template, boneName);
|
||||
}
|
||||
else
|
||||
{
|
||||
template.Bones.Remove(boneName);
|
||||
|
||||
_logger.Debug($"Deleted bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.DeletedBone, template, boneName);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
template.Bones[boneName] = new BoneTransform(transform);
|
||||
|
||||
_logger.Debug($"Created bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.NewBone, template, boneName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DeleteBoneTransform(Template template, string boneName)
|
||||
{
|
||||
if (!template.Bones.ContainsKey(boneName))
|
||||
return;
|
||||
|
||||
template.Bones.Remove(boneName);
|
||||
|
||||
_logger.Debug($"Deleted bone {boneName} on {template.Name}");
|
||||
_event.Invoke(TemplateChanged.Type.DeletedBone, template, boneName);
|
||||
}
|
||||
|
||||
private static void PruneIdempotentTransforms(Template template)
|
||||
{
|
||||
foreach (var kvp in template.Bones)
|
||||
{
|
||||
if (!kvp.Value.IsEdited())
|
||||
{
|
||||
template.Bones.Remove(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveTemplate(Template template)
|
||||
{
|
||||
template.ModifiedDate = DateTimeOffset.UtcNow;
|
||||
_saveService.QueueSave(template);
|
||||
}
|
||||
|
||||
private void OnReload(ReloadEvent.Type type)
|
||||
{
|
||||
if (type != ReloadEvent.Type.ReloadTemplates &&
|
||||
type != ReloadEvent.Type.ReloadAll)
|
||||
return;
|
||||
|
||||
_logger.Debug("Reload event received");
|
||||
LoadTemplates();
|
||||
}
|
||||
|
||||
private static void CreateTemplateFolder(SaveService service)
|
||||
{
|
||||
var ret = service.FileNames.TemplateDirectory;
|
||||
if (Directory.Exists(ret))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ret);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Logger.Error($"Could not create template directory {ret}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Move all files that were discovered to have names not corresponding to their identifier to correct names, if possible. </summary>
|
||||
/// <returns>The number of files that could not be moved.</returns>
|
||||
private int MoveInvalidNames(IEnumerable<(Template, string)> invalidNames)
|
||||
{
|
||||
var failed = 0;
|
||||
foreach (var (template, name) in invalidNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var correctName = _saveService.FileNames.TemplateFile(template);
|
||||
File.Move(name, correctName, false);
|
||||
_logger.Information($"Moved invalid template file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
++failed;
|
||||
_logger.Error($"Failed to move invalid template file from {Path.GetFileName(name)}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
return failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new guid until we find one which isn't used by existing template
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Guid CreateNewGuid()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
if (_templates.All(d => d.UniqueId != guid))
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
CustomizePlus/UI/CPlusWindowSystem.cs
Normal file
48
CustomizePlus/UI/CPlusWindowSystem.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using System;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.UI.Windows.MainWindow;
|
||||
using CustomizePlus.UI.Windows;
|
||||
|
||||
namespace CustomizePlus.UI;
|
||||
|
||||
public class CPlusWindowSystem : IDisposable
|
||||
{
|
||||
private readonly WindowSystem _windowSystem = new("Customize+");
|
||||
private readonly UiBuilder _uiBuilder;
|
||||
private readonly MainWindow _mainWindow;
|
||||
private readonly PopupSystem _popupSystem;
|
||||
|
||||
public CPlusWindowSystem(
|
||||
UiBuilder uiBuilder,
|
||||
MainWindow mainWindow,
|
||||
CPlusChangeLog changelog,
|
||||
PopupSystem popupSystem,
|
||||
PluginConfiguration configuration)
|
||||
{
|
||||
_uiBuilder = uiBuilder;
|
||||
_mainWindow = mainWindow;
|
||||
_popupSystem = popupSystem;
|
||||
|
||||
_windowSystem.AddWindow(mainWindow);
|
||||
_windowSystem.AddWindow(changelog.Changelog);
|
||||
_uiBuilder.Draw += OnDraw;
|
||||
_uiBuilder.OpenConfigUi += _mainWindow.Toggle;
|
||||
|
||||
_uiBuilder.DisableGposeUiHide = true;
|
||||
_uiBuilder.DisableCutsceneUiHide = !configuration.UISettings.HideWindowInCutscene;
|
||||
}
|
||||
|
||||
private void OnDraw()
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_popupSystem.Draw();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiBuilder.Draw -= _windowSystem.Draw;
|
||||
_uiBuilder.OpenConfigUi -= _mainWindow.Toggle;
|
||||
}
|
||||
}
|
||||
51
CustomizePlus/UI/Colors.cs
Normal file
51
CustomizePlus/UI/Colors.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CustomizePlus.UI;
|
||||
|
||||
public enum ColorId
|
||||
{
|
||||
UsedTemplate,
|
||||
UnusedTemplate,
|
||||
EnabledProfile,
|
||||
DisabledProfile,
|
||||
LocalCharacterEnabledProfile,
|
||||
LocalCharacterDisabledProfile,
|
||||
FolderExpanded,
|
||||
FolderCollapsed,
|
||||
FolderLine,
|
||||
HeaderButtons,
|
||||
}
|
||||
|
||||
public static class Colors
|
||||
{
|
||||
public const uint SelectedRed = 0xFF2020D0;
|
||||
|
||||
public static (uint DefaultColor, string Name, string Description) Data(this ColorId color)
|
||||
=> color switch
|
||||
{
|
||||
// @formatter:off
|
||||
ColorId.UsedTemplate => (0xFFFFFFFF, "Used Template", "A template which is being used by at least one profile."),
|
||||
//ColorId.EnabledTemplate => (0xFFA0F0A0, "Enabled Automation Set", "An automation set that is currently enabled. Only one set can be enabled for each identifier at once."),
|
||||
ColorId.UnusedTemplate => (0xFF808080, "Unused template", "Template which is currently not being used by any profile."),
|
||||
ColorId.EnabledProfile => (0xFFFFFFFF, "Enabled profile", "A profile which is currently enabled."),
|
||||
ColorId.DisabledProfile => (0xFF808080, "Disabled profile", "A profile which is currently disabled"),
|
||||
ColorId.LocalCharacterEnabledProfile => (0xFF18C018, "Current character profile (enabled)", "A profile which is currently enabled and associated with your character."),
|
||||
ColorId.LocalCharacterDisabledProfile => (0xFF808080, "Current character profile (disabled)", "A profile which is currently disabled and associated with your character."),
|
||||
ColorId.FolderExpanded => (0xFFFFF0C0, "Expanded Folder", "A folder that is currently expanded."),
|
||||
ColorId.FolderCollapsed => (0xFFFFF0C0, "Collapsed Folder", "A folder that is currently collapsed."),
|
||||
ColorId.FolderLine => (0xFFFFF0C0, "Expanded Folder Line", "The line signifying which descendants belong to an expanded folder."),
|
||||
ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the write protection toggle."),
|
||||
_ => (0x00000000, string.Empty, string.Empty),
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
private static IReadOnlyDictionary<ColorId, uint> _colors = new Dictionary<ColorId, uint>();
|
||||
|
||||
/// <summary> Obtain the configured value for a color. </summary>
|
||||
public static uint Value(this ColorId color)
|
||||
=> _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor;
|
||||
|
||||
/// <summary> Set the configurable colors dictionary to a value. </summary>
|
||||
/*public static void SetColors(Configuration config)
|
||||
=> _colors = config.Colors;*/
|
||||
}
|
||||
60
CustomizePlus/UI/Windows/CPlusChangeLog.cs
Normal file
60
CustomizePlus/UI/Windows/CPlusChangeLog.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using OtterGui.Widgets;
|
||||
|
||||
namespace CustomizePlus.UI.Windows;
|
||||
|
||||
public class CPlusChangeLog
|
||||
{
|
||||
public const int LastChangelogVersion = 0;
|
||||
private readonly PluginConfiguration _config;
|
||||
public readonly Changelog Changelog;
|
||||
|
||||
public CPlusChangeLog(PluginConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
Changelog = new Changelog("Customize+ update history", ConfigData, Save);
|
||||
|
||||
Add2_0_0_0(Changelog);
|
||||
}
|
||||
|
||||
private (int, ChangeLogDisplayType) ConfigData()
|
||||
=> (_config.ChangelogSettings.LastSeenVersion, _config.ChangelogSettings.ChangeLogDisplayType);
|
||||
|
||||
private void Save(int version, ChangeLogDisplayType type)
|
||||
{
|
||||
_config.ChangelogSettings.LastSeenVersion = version;
|
||||
_config.ChangelogSettings.ChangeLogDisplayType = type;
|
||||
_config.Save();
|
||||
}
|
||||
private static void Add2_0_0_0(Changelog log)
|
||||
=> log.NextVersion("Version 2.0.0.0")
|
||||
.RegisterHighlight("Major rework of the entire plugin.")
|
||||
.RegisterEntry("Migration of your Customize+ settings and profiles should be performed without any issues.", 1)
|
||||
.RegisterImportant("Old version configuration is backed up in case something goes wrong, please report any issues with configuration migration as soon as possible.", 1)
|
||||
|
||||
.RegisterHighlight("Major changes:")
|
||||
|
||||
.RegisterEntry("Plugin has been almost completely rewritten from scratch.", 1)
|
||||
.RegisterImportant("Clipboard copies and profiles from previous versions are not currently supported.", 2)
|
||||
|
||||
.RegisterEntry("User interface has been moved to the framework used by Glamourer and Penumbra, so the interface should feel familiar to the users of those plugins.", 1)
|
||||
.RegisterEntry("User interface issues related to different resolutions and font sizes should *mostly* not occur anymore.", 2)
|
||||
.RegisterImportant("There are several issues with text not fitting in some places depending on your resolution and font size. This will be fixed later.", 3)
|
||||
|
||||
.RegisterEntry("Template system has been added", 1)
|
||||
.RegisterEntry("All bone edits are now stored in templates which can be used by multiple profiles and single profile can reference unlimited number of templates.", 2)
|
||||
|
||||
.RegisterImportant("Chat commands have been changed, refer to \"/customize help\" for information about available commands.", 1)
|
||||
.RegisterEntry("Added \"toggle\" chat command which can be used to toggle given profile on a given character.", 2)
|
||||
|
||||
.RegisterEntry("Profiles can be applied to summons, mounts and pets without any limitations.", 1)
|
||||
.RegisterImportant("Root scaling of mounts is not available for now.", 2)
|
||||
|
||||
.RegisterEntry("Fixed \"Only owned\" not working properly in various cases and renamed it to \"Limit to my creatures\".", 1)
|
||||
|
||||
.RegisterEntry("Fixed profiles \"leaking\" to other characters due to the way original Mare Synchronos integration implementation was handled.", 1)
|
||||
|
||||
.RegisterEntry("Compatibility with cutscenes is improved, but that was not extensively tested.", 1)
|
||||
|
||||
.RegisterEntry("Plugin configuration is now being regularly backed up, the backup is located in %appdata%\\XIVLauncher\\backups\\CustomizePlus folder", 1);
|
||||
}
|
||||
102
CustomizePlus/UI/Windows/Controls/PluginStateBlock.cs
Normal file
102
CustomizePlus/UI/Windows/Controls/PluginStateBlock.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.Controls;
|
||||
|
||||
public class PluginStateBlock
|
||||
{
|
||||
private readonly BoneEditorPanel _boneEditorPanel;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly GameStateService _gameStateService;
|
||||
private readonly FantasiaPlusDetectService _fantasiaPlusDetectService;
|
||||
|
||||
private static Vector4 normalColor = new Vector4(1, 1, 1, 1);
|
||||
private static Vector4 warnColor = new Vector4(1, 0.5f, 0, 1);
|
||||
private static Vector4 errorColor = new Vector4(1, 0, 0, 1);
|
||||
|
||||
public PluginStateBlock(
|
||||
BoneEditorPanel boneEditorPanel,
|
||||
PluginConfiguration configuration,
|
||||
GameStateService gameStateService,
|
||||
FantasiaPlusDetectService fantasiaPlusDetectService)
|
||||
{
|
||||
_boneEditorPanel = boneEditorPanel;
|
||||
_configuration = configuration;
|
||||
_gameStateService = gameStateService;
|
||||
_fantasiaPlusDetectService = fantasiaPlusDetectService;
|
||||
}
|
||||
|
||||
public void Draw(float yPos)
|
||||
{
|
||||
var severity = PluginStateSeverity.Normal;
|
||||
string? message = null;
|
||||
|
||||
if (_fantasiaPlusDetectService.IsFantasiaPlusInstalled)
|
||||
{
|
||||
severity = PluginStateSeverity.Error;
|
||||
message = $"Fantasia+ detected. The plugin is disabled until Fantasia+ is disabled and the game is restarted.";
|
||||
}
|
||||
else if (_gameStateService.GameInPosingMode())
|
||||
{
|
||||
severity = PluginStateSeverity.Warning;
|
||||
message = $"GPose active. Most editor features are unavailable while you're in this mode.";
|
||||
}
|
||||
else if (!_configuration.PluginEnabled)
|
||||
{
|
||||
severity = PluginStateSeverity.Warning;
|
||||
message = "Plugin is disabled, template bone editing is not available.";
|
||||
}
|
||||
else if (_boneEditorPanel.IsEditorActive)
|
||||
{
|
||||
if (!_boneEditorPanel.IsCharacterFound)
|
||||
{
|
||||
severity = PluginStateSeverity.Error;
|
||||
message = $"Selected preview character was not found.";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_boneEditorPanel.HasChanges)
|
||||
severity = PluginStateSeverity.Warning;
|
||||
|
||||
message = $"Editor is active.{(_boneEditorPanel.HasChanges ? " You have unsaved changes, finish template bone editing to open save/revert dialog." : "")}";
|
||||
}
|
||||
}
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
ImGui.SetCursorPos(new Vector2(ImGui.GetWindowContentRegionMax().X - ImGui.CalcTextSize(message).X - 30, yPos - ImGuiHelpers.GlobalScale));
|
||||
|
||||
var icon = FontAwesomeIcon.InfoCircle;
|
||||
var color = normalColor;
|
||||
switch (severity)
|
||||
{
|
||||
case PluginStateSeverity.Warning:
|
||||
icon = FontAwesomeIcon.ExclamationTriangle;
|
||||
color = warnColor;
|
||||
break;
|
||||
case PluginStateSeverity.Error:
|
||||
icon = FontAwesomeIcon.ExclamationTriangle;
|
||||
color = errorColor;
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
CtrlHelper.LabelWithIcon(icon, message, false);
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
private enum PluginStateSeverity
|
||||
{
|
||||
Normal,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
||||
151
CustomizePlus/UI/Windows/Controls/TemplateCombo.cs
Normal file
151
CustomizePlus/UI/Windows/Controls/TemplateCombo.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Widgets;
|
||||
using OtterGui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.Controls;
|
||||
|
||||
public abstract class TemplateComboBase : FilterComboCache<Tuple<Template, string>>, IDisposable
|
||||
{
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly TemplateChanged _templateChanged;
|
||||
// protected readonly TabSelected TabSelected;
|
||||
protected float InnerWidth;
|
||||
|
||||
protected TemplateComboBase(
|
||||
Func<IReadOnlyList<Tuple<Template, string>>> generator,
|
||||
Logger logger,
|
||||
TemplateChanged templateChanged,
|
||||
//TabSelected tabSelected,
|
||||
PluginConfiguration configuration)
|
||||
: base(generator, logger)
|
||||
{
|
||||
_templateChanged = templateChanged;
|
||||
//TabSelected = tabSelected;
|
||||
_configuration = configuration;
|
||||
_templateChanged.Subscribe(OnTemplateChange, TemplateChanged.Priority.TemplateCombo);
|
||||
}
|
||||
|
||||
public bool Incognito
|
||||
=> _configuration.UISettings.IncognitoMode;
|
||||
|
||||
void IDisposable.Dispose()
|
||||
=> _templateChanged.Unsubscribe(OnTemplateChange);
|
||||
|
||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||
{
|
||||
var ret = base.DrawSelectable(globalIdx, selected);
|
||||
var (design, path) = Items[globalIdx];
|
||||
if (path.Length > 0 && design.Name != path)
|
||||
{
|
||||
var start = ImGui.GetItemRectMin();
|
||||
var pos = start.X + ImGui.CalcTextSize(design.Name).X;
|
||||
var maxSize = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X;
|
||||
var remainingSpace = maxSize - pos;
|
||||
var requiredSize = ImGui.CalcTextSize(path).X + ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
var offset = remainingSpace - requiredSize;
|
||||
if (ImGui.GetScrollMaxY() == 0)
|
||||
offset -= ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
|
||||
if (offset < ImGui.GetStyle().ItemSpacing.X)
|
||||
ImGuiUtil.HoverTooltip(path);
|
||||
else
|
||||
ImGui.GetWindowDrawList().AddText(start with { X = pos + offset },
|
||||
ImGui.GetColorU32(ImGuiCol.TextDisabled), path);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected bool Draw(Template? currentTemplate, string? label, float width)
|
||||
{
|
||||
InnerWidth = 400 * ImGuiHelpers.GlobalScale;
|
||||
CurrentSelectionIdx = Math.Max(Items.IndexOf(p => currentTemplate == p.Item1), 0);
|
||||
CurrentSelection = Items[CurrentSelectionIdx];
|
||||
var name = label ?? "Select Template Here...";
|
||||
var ret = Draw("##template", name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing())
|
||||
&& CurrentSelection != null;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected override string ToString(Tuple<Template, string> obj)
|
||||
=> obj.Item1.Name.Text;
|
||||
|
||||
protected override float GetFilterWidth()
|
||||
=> InnerWidth - 2 * ImGui.GetStyle().FramePadding.X;
|
||||
|
||||
protected override bool IsVisible(int globalIndex, LowerString filter)
|
||||
{
|
||||
var (design, path) = Items[globalIndex];
|
||||
return filter.IsContained(path) || design.Name.Lower.Contains(filter.Lower);
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Template template, object? data = null)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TemplateChanged.Type.Created:
|
||||
case TemplateChanged.Type.Renamed:
|
||||
Cleanup();
|
||||
break;
|
||||
case TemplateChanged.Type.Deleted:
|
||||
Cleanup();
|
||||
if (CurrentSelection?.Item1 == template)
|
||||
{
|
||||
CurrentSelectionIdx = -1;
|
||||
CurrentSelection = null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TemplateCombo : TemplateComboBase
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
|
||||
public TemplateCombo(
|
||||
TemplateManager templateManager,
|
||||
ProfileManager profileManager,
|
||||
TemplateFileSystem fileSystem,
|
||||
Logger logger,
|
||||
TemplateChanged templateChanged,
|
||||
//TabSelected tabSelected,
|
||||
PluginConfiguration configuration)
|
||||
: base(
|
||||
() => templateManager.Templates
|
||||
.Select(d => new Tuple<Template, string>(d, fileSystem.FindLeaf(d, out var l) ? l.FullName() : string.Empty))
|
||||
.OrderBy(d => d.Item2)
|
||||
.ToList(), logger, templateChanged,/* tabSelected, */configuration)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
}
|
||||
|
||||
public Template? Template
|
||||
=> CurrentSelection?.Item1;
|
||||
|
||||
public void Draw(Profile profile, Template? template, int templateIndex)
|
||||
{
|
||||
if (!Draw(template, Incognito ? template?.Incognito : template?.Name, ImGui.GetContentRegionAvail().X))
|
||||
return;
|
||||
|
||||
if (templateIndex >= 0)
|
||||
_profileManager.ChangeTemplate(profile, templateIndex, CurrentSelection!.Item1);
|
||||
else
|
||||
_profileManager.AddTemplate(profile, CurrentSelection!.Item1);
|
||||
}
|
||||
}
|
||||
145
CustomizePlus/UI/Windows/MainWindow/MainWindow.cs
Normal file
145
CustomizePlus/UI/Windows/MainWindow/MainWindow.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SettingsTab = CustomizePlus.UI.Windows.MainWindow.Tabs.SettingsTab;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
using CustomizePlus.UI.Windows.Controls;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
|
||||
using CustomizePlus.UI.Windows.MainWindow.Tabs;
|
||||
using CustomizePlus.Templates;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow;
|
||||
|
||||
public class MainWindow : Window, IDisposable
|
||||
{
|
||||
private readonly SettingsTab _settingsTab;
|
||||
private readonly TemplatesTab _templatesTab;
|
||||
private readonly ProfilesTab _profilesTab;
|
||||
private readonly MessagesTab _messagesTab;
|
||||
private readonly IPCTestTab _ipcTestTab;
|
||||
private readonly StateMonitoringTab _stateMonitoringTab;
|
||||
|
||||
private readonly PluginStateBlock _pluginStateBlock;
|
||||
|
||||
private readonly TemplateEditorManager _templateEditorManager;
|
||||
private readonly FantasiaPlusDetectService _cPlusDetectService;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
|
||||
public MainWindow(
|
||||
DalamudPluginInterface pluginInterface,
|
||||
SettingsTab settingsTab,
|
||||
TemplatesTab templatesTab,
|
||||
ProfilesTab profilesTab,
|
||||
MessagesTab messagesTab,
|
||||
IPCTestTab ipcTestTab,
|
||||
StateMonitoringTab stateMonitoringTab,
|
||||
PluginStateBlock pluginStateBlock,
|
||||
TemplateEditorManager templateEditorManager,
|
||||
PluginConfiguration configuration,
|
||||
FantasiaPlusDetectService cPlusDetectService
|
||||
) : base($"Customize+ v{Plugin.Version}###CPlusMainWindow")
|
||||
{
|
||||
_settingsTab = settingsTab;
|
||||
_templatesTab = templatesTab;
|
||||
_profilesTab = profilesTab;
|
||||
_messagesTab = messagesTab;
|
||||
_ipcTestTab = ipcTestTab;
|
||||
_stateMonitoringTab = stateMonitoringTab;
|
||||
|
||||
_pluginStateBlock = pluginStateBlock;
|
||||
|
||||
_templateEditorManager = templateEditorManager;
|
||||
_cPlusDetectService = cPlusDetectService;
|
||||
_configuration = configuration;
|
||||
|
||||
pluginInterface.UiBuilder.DisableGposeUiHide = true;
|
||||
SizeConstraints = new WindowSizeConstraints()
|
||||
{
|
||||
MinimumSize = new Vector2(700, 675),
|
||||
MaximumSize = ImGui.GetIO().DisplaySize,
|
||||
};
|
||||
|
||||
IsOpen = pluginInterface.IsDevMenuOpen && configuration.DebuggingModeEnabled;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
var yPos = ImGui.GetCursorPosY();
|
||||
|
||||
using (var disabled = ImRaii.Disabled(_cPlusDetectService.IsFantasiaPlusInstalled))
|
||||
{
|
||||
LockWindowClosureIfNeeded();
|
||||
if (ImGui.BeginTabBar("##tabs", ImGuiTabBarFlags.None)) //todo: remember last selected tab
|
||||
{
|
||||
if (ImGui.BeginTabItem("Settings"))
|
||||
{
|
||||
_settingsTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("Templates"))
|
||||
{
|
||||
_templatesTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("Profiles"))
|
||||
{
|
||||
_profilesTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
//if(_messagesTab.IsVisible)
|
||||
//{
|
||||
/*if (ImGui.BeginTabItem("Messages"))
|
||||
{
|
||||
_messagesTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}*/
|
||||
//}
|
||||
|
||||
if (_configuration.DebuggingModeEnabled)
|
||||
{
|
||||
if (ImGui.BeginTabItem("IPC Test"))
|
||||
{
|
||||
_ipcTestTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("State monitoring"))
|
||||
{
|
||||
_stateMonitoringTab.Draw();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_pluginStateBlock.Draw(yPos);
|
||||
}
|
||||
|
||||
private void LockWindowClosureIfNeeded()
|
||||
{
|
||||
if (_templateEditorManager.IsEditorActive)
|
||||
{
|
||||
ShowCloseButton = false;
|
||||
RespectCloseHotkey = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowCloseButton = true;
|
||||
RespectCloseHotkey = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs
Normal file
152
CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Raii;
|
||||
using System.Linq;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Configuration.Helpers;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.GameData.Services;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
|
||||
|
||||
public class IPCTestTab //: IDisposable
|
||||
{
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly PopupSystem _popupSystem;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
private readonly ObjectManager _objectManager;
|
||||
private readonly ActorService _actorService;
|
||||
|
||||
private readonly ICallGateSubscriber<(int, int)>? _getApiVersion;
|
||||
private readonly ICallGateSubscriber<string, Character?, object>? _setCharacterProfile;
|
||||
private readonly ICallGateSubscriber<Character?, string>? _getProfileFromCharacter;
|
||||
private readonly ICallGateSubscriber<Character?, object>? _revertCharacter;
|
||||
//private readonly ICallGateSubscriber<string?, string?, object?>? _onProfileUpdate;
|
||||
|
||||
private string? _rememberedProfileJson;
|
||||
|
||||
private (int, int) _apiVersion;
|
||||
|
||||
private string? _targetCharacterName;
|
||||
|
||||
public IPCTestTab(
|
||||
DalamudPluginInterface pluginInterface,
|
||||
IObjectTable objectTable,
|
||||
ProfileManager profileManager,
|
||||
PopupSystem popupSystem,
|
||||
ObjectManager objectManager,
|
||||
GameObjectService gameObjectService,
|
||||
ActorService actorService)
|
||||
{
|
||||
_objectTable = objectTable;
|
||||
_profileManager = profileManager;
|
||||
_popupSystem = popupSystem;
|
||||
_objectManager = objectManager;
|
||||
_gameObjectService = gameObjectService;
|
||||
_actorService = actorService;
|
||||
|
||||
_popupSystem.RegisterPopup("ipc_v4_profile_remembered", "Current profile has been copied into memory");
|
||||
_popupSystem.RegisterPopup("ipc_get_profile_from_character_remembered", "GetProfileFromCharacter result has been copied into memory");
|
||||
_popupSystem.RegisterPopup("ipc_set_profile_to_character_done", "SetProfileToCharacter has been called with data from memory");
|
||||
_popupSystem.RegisterPopup("ipc_revert_done", "Revert has been called");
|
||||
|
||||
_getApiVersion = pluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.GetApiVersion");
|
||||
_apiVersion = _getApiVersion.InvokeFunc();
|
||||
|
||||
_setCharacterProfile = pluginInterface.GetIpcSubscriber<string, Character?, object>("CustomizePlus.SetProfileToCharacter");
|
||||
_getProfileFromCharacter = pluginInterface.GetIpcSubscriber<Character?, string>("CustomizePlus.GetProfileFromCharacter");
|
||||
_revertCharacter = pluginInterface.GetIpcSubscriber<Character?, object>("CustomizePlus.RevertCharacter");
|
||||
/*_onProfileUpdate = pluginInterface.GetIpcSubscriber<string?, string?, object?>("CustomizePlus.OnProfileUpdate");
|
||||
_onProfileUpdate.Subscribe(OnProfileUpdate);*/
|
||||
}
|
||||
/* public void Dispose()
|
||||
{
|
||||
_onProfileUpdate?.Unsubscribe(OnProfileUpdate);
|
||||
}
|
||||
|
||||
private void OnProfileUpdate(string? characterName, string? profileJson)
|
||||
{
|
||||
_lastProfileUpdate = DateTime.Now;
|
||||
_lastProfileUpdateName = characterName;
|
||||
}
|
||||
*/
|
||||
public unsafe void Draw()
|
||||
{
|
||||
_objectManager.Update();
|
||||
|
||||
if (_targetCharacterName == null)
|
||||
_targetCharacterName = _gameObjectService.GetCurrentPlayerName();
|
||||
|
||||
ImGui.Text($"Version: {_apiVersion.Item1}.{_apiVersion.Item2}");
|
||||
//ImGui.Text($"Last profile update: {_lastProfileUpdate}, Character: {_lastProfileUpdateName}");
|
||||
ImGui.Text($"Memory: {(string.IsNullOrWhiteSpace(_rememberedProfileJson) ? "empty" : "has data")}");
|
||||
|
||||
ImGui.Text("Character to operate on:");
|
||||
ImGui.SameLine();
|
||||
ImGui.InputText("##operateon", ref _targetCharacterName, 128);
|
||||
|
||||
if (ImGui.Button("Copy current profile into memory as V3"))
|
||||
{
|
||||
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
|
||||
if (actors.Count == 0)
|
||||
return;
|
||||
|
||||
if (!actors[0].Item2.Identifier(_actorService.AwaitedService, out var identifier))
|
||||
return;
|
||||
|
||||
var profile = _profileManager.GetEnabledProfilesByActor(identifier).FirstOrDefault();
|
||||
if (profile == null)
|
||||
return;
|
||||
|
||||
_rememberedProfileJson = JsonConvert.SerializeObject(V4ProfileToV3Converter.Convert(profile));
|
||||
_popupSystem.ShowPopup("ipc_v4_profile_remembered");
|
||||
}
|
||||
|
||||
if (ImGui.Button("GetProfileFromCharacter into memory"))
|
||||
{
|
||||
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
|
||||
if (actors.Count == 0)
|
||||
return;
|
||||
|
||||
_rememberedProfileJson = _getProfileFromCharacter!.InvokeFunc(FindCharacterByAddress(actors[0].Item2.Address));
|
||||
_popupSystem.ShowPopup("ipc_get_profile_from_character_remembered");
|
||||
}
|
||||
|
||||
using (var disabled = ImRaii.Disabled(_rememberedProfileJson == null))
|
||||
{
|
||||
if (ImGui.Button("SetProfileToCharacter from memory") && _rememberedProfileJson != null)
|
||||
{
|
||||
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
|
||||
if (actors.Count == 0)
|
||||
return;
|
||||
|
||||
_setCharacterProfile!.InvokeAction(_rememberedProfileJson, FindCharacterByAddress(actors[0].Item2.Address));
|
||||
_popupSystem.ShowPopup("ipc_set_profile_to_character_done");
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.Button("RevertCharacter") && _rememberedProfileJson != null)
|
||||
{
|
||||
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
|
||||
if (actors.Count == 0)
|
||||
return;
|
||||
|
||||
_revertCharacter!.InvokeAction(FindCharacterByAddress(actors[0].Item2.Address));
|
||||
_popupSystem.ShowPopup("ipc_revert_done");
|
||||
}
|
||||
}
|
||||
|
||||
private Character? FindCharacterByAddress(nint address)
|
||||
{
|
||||
foreach (var obj in _objectTable)
|
||||
if (obj.Address == address)
|
||||
return (Character)obj;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using ImGuiNET;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Armatures.Services;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Data;
|
||||
using CustomizePlus.GameData.Extensions;
|
||||
using CustomizePlus.GameData.Services;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
|
||||
|
||||
public class StateMonitoringTab
|
||||
{
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly TemplateManager _templateManager;
|
||||
private readonly ArmatureManager _armatureManager;
|
||||
private readonly ObjectManager _objectManager;
|
||||
|
||||
public StateMonitoringTab(
|
||||
ProfileManager profileManager,
|
||||
TemplateManager templateManager,
|
||||
ArmatureManager armatureManager,
|
||||
ObjectManager objectManager)
|
||||
{
|
||||
_profileManager = profileManager;
|
||||
_templateManager = templateManager;
|
||||
_armatureManager = armatureManager;
|
||||
_objectManager = objectManager;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var showProfiles = ImGui.CollapsingHeader($"Profiles ({_profileManager.Profiles.Count})###profiles_header");
|
||||
|
||||
if (showProfiles)
|
||||
DrawProfiles();
|
||||
|
||||
var showTemplates = ImGui.CollapsingHeader($"Templates ({_templateManager.Templates.Count})###templates_header");
|
||||
|
||||
if (showTemplates)
|
||||
DrawTemplates();
|
||||
|
||||
var showArmatures = ImGui.CollapsingHeader($"Armatures ({_armatureManager.Armatures.Count})###armatures_header");
|
||||
|
||||
if (showArmatures)
|
||||
DrawArmatures();
|
||||
|
||||
var showObjectManager = ImGui.CollapsingHeader($"Object manager ({_objectManager.Count})###objectmanager_header");
|
||||
|
||||
if (showObjectManager)
|
||||
DrawObjectManager();
|
||||
}
|
||||
|
||||
private void DrawProfiles()
|
||||
{
|
||||
foreach (var profile in _profileManager.Profiles.OrderByDescending(x => x.Enabled))
|
||||
{
|
||||
DrawSingleProfile("root", profile);
|
||||
ImGui.Spacing();
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTemplates()
|
||||
{
|
||||
foreach (var template in _templateManager.Templates)
|
||||
{
|
||||
DrawSingleTemplate($"root", template);
|
||||
ImGui.Spacing();
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawArmatures()
|
||||
{
|
||||
foreach (var armature in _armatureManager.Armatures)
|
||||
{
|
||||
DrawSingleArmature($"root", armature.Value);
|
||||
ImGui.Spacing();
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawObjectManager()
|
||||
{
|
||||
foreach (var kvPair in _objectManager)
|
||||
{
|
||||
var show = ImGui.CollapsingHeader($"{kvPair.Key} ({kvPair.Value.Objects.Count} objects)###object-{kvPair.Key}");
|
||||
|
||||
if (!show)
|
||||
continue;
|
||||
|
||||
ImGui.Text($"ActorIdentifier");
|
||||
ImGui.Text($"PlayerName: {kvPair.Key.PlayerName}");
|
||||
ImGui.Text($"HomeWorld: {kvPair.Key.HomeWorld}");
|
||||
ImGui.Text($"Retainer: {kvPair.Key.Retainer}");
|
||||
ImGui.Text($"Kind: {kvPair.Key.Kind}");
|
||||
ImGui.Text($"Data id: {kvPair.Key.DataId}");
|
||||
ImGui.Text($"Index: {kvPair.Key.Index.Index}");
|
||||
ImGui.Text($"Type: {kvPair.Key.Type}");
|
||||
ImGui.Text($"Special: {kvPair.Key.Special.ToString()}");
|
||||
ImGui.Text($"ToName: {kvPair.Key.ToName()}");
|
||||
ImGui.Text($"ToNameWithoutOwnerName: {kvPair.Key.ToNameWithoutOwnerName()}");
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Spacing();
|
||||
|
||||
ImGui.Text($"Objects");
|
||||
ImGui.Text($"Valid: {kvPair.Value.Valid}");
|
||||
ImGui.Text($"Label: {kvPair.Value.Label}");
|
||||
ImGui.Text($"Count: {kvPair.Value.Objects.Count}");
|
||||
foreach (var item in kvPair.Value.Objects)
|
||||
{
|
||||
ImGui.Text($"{item}, valid: {item.Valid}");
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Spacing();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSingleProfile(string prefix, Profile profile)
|
||||
{
|
||||
var show = ImGui.CollapsingHeader($"[{(profile.Enabled ? "E" : "D")}] {profile.Name} on {profile.CharacterName} [{(profile.IsTemporary ? "Temporary" : "Permanent")}]###{prefix}-profile-{profile.UniqueId}");
|
||||
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
ImGui.Text($"ID: {profile.UniqueId}");
|
||||
ImGui.Text($"Enabled: {(profile.Enabled ? "Enabled" : "Disabled")}");
|
||||
ImGui.Text($"State : {(profile.IsTemporary ? "Temporary" : "Permanent")}");
|
||||
ImGui.Text($"Lookup: {(profile.LimitLookupToOwnedObjects ? "Limited lookup" : "Global lookup")}");
|
||||
var showTemplates = ImGui.CollapsingHeader($"Templates###{prefix}-profile-{profile.UniqueId}-templates");
|
||||
|
||||
if (showTemplates)
|
||||
{
|
||||
foreach (var template in profile.Templates)
|
||||
{
|
||||
DrawSingleTemplate($"profile-{profile.UniqueId}", template);
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.Armatures.Count > 0)
|
||||
foreach (var armature in profile.Armatures)
|
||||
DrawSingleArmature($"profile-{profile.UniqueId}", armature);
|
||||
else
|
||||
ImGui.Text("No armatures");
|
||||
}
|
||||
|
||||
private void DrawSingleTemplate(string prefix, Template template)
|
||||
{
|
||||
var show = ImGui.CollapsingHeader($"{template.Name}###{prefix}-template-{template.UniqueId}");
|
||||
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
ImGui.Text($"ID: {template.UniqueId}");
|
||||
|
||||
ImGui.Text($"Bones:");
|
||||
foreach (var kvPair in template.Bones)
|
||||
{
|
||||
ImGui.Text($"{kvPair.Key}: p:{kvPair.Value.Translation} | r: {kvPair.Value.Rotation} | s: {kvPair.Value.Scaling}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSingleArmature(string prefix, Armature armature)
|
||||
{
|
||||
var show = ImGui.CollapsingHeader($"{armature} [{(armature.IsBuilt ? "Built" : "Not built")}, {(armature.IsVisible ? "Visible" : "Not visible")}]###{prefix}-armature-{armature.GetHashCode()}");
|
||||
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
if (armature.IsBuilt)
|
||||
{
|
||||
ImGui.Text($"Total bones: {armature.TotalBoneCount}");
|
||||
ImGui.Text($"Partial skeletons: {armature.PartialSkeletonCount}");
|
||||
ImGui.Text($"Root bone: {armature.MainRootBone}");
|
||||
}
|
||||
|
||||
ImGui.Text($"Profile: {armature.Profile.Name} ({armature.Profile.UniqueId})");
|
||||
ImGui.Text($"Actor: {armature.ActorIdentifier}");
|
||||
ImGui.Text($"Protection: {(armature.ProtectedUntil >= DateTime.UtcNow ? "Active" : "NOT active")} [{armature.ProtectedUntil} (UTC)]");
|
||||
//ImGui.Text("Profile:");
|
||||
//DrawSingleProfile($"armature-{armature.GetHashCode()}", armature.Profile);
|
||||
|
||||
ImGui.Text($"Bone template bindings:");
|
||||
foreach (var kvPair in armature.BoneTemplateBinding)
|
||||
{
|
||||
ImGui.Text($"{kvPair.Key} -> {kvPair.Value.Name} ({kvPair.Value.UniqueId})");
|
||||
}
|
||||
}
|
||||
}
|
||||
101
CustomizePlus/UI/Windows/MainWindow/Tabs/HeaderDrawer.cs
Normal file
101
CustomizePlus/UI/Windows/MainWindow/Tabs/HeaderDrawer.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using OtterGui.Raii;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs;
|
||||
|
||||
public static class HeaderDrawer
|
||||
{
|
||||
public struct Button
|
||||
{
|
||||
public static readonly Button Invisible = new()
|
||||
{
|
||||
Visible = false,
|
||||
Width = 0,
|
||||
};
|
||||
|
||||
public Action? OnClick;
|
||||
public string Description = string.Empty;
|
||||
public float Width;
|
||||
public uint BorderColor;
|
||||
public uint TextColor;
|
||||
public FontAwesomeIcon Icon;
|
||||
public bool Disabled;
|
||||
public bool Visible;
|
||||
|
||||
public Button()
|
||||
{
|
||||
Visible = true;
|
||||
Width = ImGui.GetFrameHeightWithSpacing();
|
||||
BorderColor = ColorId.HeaderButtons.Value();
|
||||
TextColor = ColorId.HeaderButtons.Value();
|
||||
Disabled = false;
|
||||
}
|
||||
|
||||
public readonly void Draw()
|
||||
{
|
||||
if (!Visible)
|
||||
return;
|
||||
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Border, BorderColor)
|
||||
.Push(ImGuiCol.Text, TextColor, TextColor != 0);
|
||||
if (ImGuiUtil.DrawDisabledButton(Icon.ToIconString(), new Vector2(Width, ImGui.GetFrameHeight()), string.Empty, Disabled, true))
|
||||
OnClick?.Invoke();
|
||||
color.Pop();
|
||||
ImGuiUtil.HoverTooltip(Description);
|
||||
}
|
||||
|
||||
public static Button IncognitoButton(bool current, Action<bool> setter)
|
||||
=> current
|
||||
? new Button
|
||||
{
|
||||
Description = "Toggle incognito mode off.",
|
||||
Icon = FontAwesomeIcon.EyeSlash,
|
||||
OnClick = () => setter(false),
|
||||
}
|
||||
: new Button
|
||||
{
|
||||
Description = "Toggle incognito mode on.",
|
||||
Icon = FontAwesomeIcon.Eye,
|
||||
OnClick = () => setter(true),
|
||||
};
|
||||
}
|
||||
|
||||
public static void Draw(string text, uint textColor, uint frameColor, int leftButtons, params Button[] buttons)
|
||||
{
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||
.Push(ImGuiStyleVar.FrameRounding, 0)
|
||||
.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
|
||||
|
||||
var leftButtonSize = 0f;
|
||||
foreach (var button in buttons.Take(leftButtons).Where(b => b.Visible))
|
||||
{
|
||||
button.Draw();
|
||||
ImGui.SameLine();
|
||||
leftButtonSize += button.Width;
|
||||
}
|
||||
|
||||
var rightButtonSize = buttons.Length > leftButtons ? buttons.Skip(leftButtons).Where(b => b.Visible).Select(b => b.Width).Sum() : 0f;
|
||||
var midSize = ImGui.GetContentRegionAvail().X - rightButtonSize - ImGuiHelpers.GlobalScale;
|
||||
|
||||
style.Pop();
|
||||
style.Push(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.5f + (rightButtonSize - leftButtonSize) / midSize, 0.5f));
|
||||
if (textColor != 0)
|
||||
ImGuiUtil.DrawTextButton(text, new Vector2(midSize, ImGui.GetFrameHeight()), frameColor, textColor);
|
||||
else
|
||||
ImGuiUtil.DrawTextButton(text, new Vector2(midSize, ImGui.GetFrameHeight()), frameColor);
|
||||
style.Pop();
|
||||
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
|
||||
|
||||
foreach (var button in buttons.Skip(leftButtons).Where(b => b.Visible))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
button.Draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
CustomizePlus/UI/Windows/MainWindow/Tabs/MessagesTab.cs
Normal file
16
CustomizePlus/UI/Windows/MainWindow/Tabs/MessagesTab.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs;
|
||||
|
||||
public class MessagesTab
|
||||
{
|
||||
private readonly MessageService _messages;
|
||||
|
||||
public MessagesTab(MessageService messages)
|
||||
=> _messages = messages;
|
||||
|
||||
public bool IsVisible
|
||||
=> _messages.Count > 0;
|
||||
|
||||
public void Draw() => _messages.Draw();
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.FileSystem.Selector;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Log;
|
||||
using OtterGui;
|
||||
using System;
|
||||
using static CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles.ProfileFileSystemSelector;
|
||||
using OtterGui.Raii;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
|
||||
|
||||
public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileState>
|
||||
{
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly ProfileChanged _event;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
|
||||
private Profile? _cloneProfile;
|
||||
private string _newName = string.Empty;
|
||||
|
||||
public bool IncognitoMode
|
||||
{
|
||||
get => _configuration.UISettings.IncognitoMode;
|
||||
set
|
||||
{
|
||||
_configuration.UISettings.IncognitoMode = value;
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public struct ProfileState
|
||||
{
|
||||
public ColorId Color;
|
||||
}
|
||||
|
||||
public ProfileFileSystemSelector(
|
||||
ProfileFileSystem fileSystem,
|
||||
IKeyState keyState,
|
||||
Logger logger,
|
||||
PluginConfiguration configuration,
|
||||
ProfileManager profileManager,
|
||||
ProfileChanged @event,
|
||||
GameObjectService gameObjectService)
|
||||
: base(fileSystem, keyState, logger, allowMultipleSelection: true)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_profileManager = profileManager;
|
||||
_event = @event;
|
||||
_gameObjectService = gameObjectService;
|
||||
|
||||
_event.Subscribe(OnProfileChange, ProfileChanged.Priority.ProfileFileSystemSelector);
|
||||
|
||||
AddButton(NewButton, 0);
|
||||
AddButton(CloneButton, 20);
|
||||
AddButton(DeleteButton, 1000);
|
||||
SetFilterTooltip();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_event.Unsubscribe(OnProfileChange);
|
||||
}
|
||||
|
||||
protected override uint ExpandedFolderColor
|
||||
=> ColorId.FolderExpanded.Value();
|
||||
|
||||
protected override uint CollapsedFolderColor
|
||||
=> ColorId.FolderCollapsed.Value();
|
||||
|
||||
protected override uint FolderLineColor
|
||||
=> ColorId.FolderLine.Value();
|
||||
|
||||
protected override bool FoldersDefaultOpen
|
||||
=> _configuration.UISettings.FoldersDefaultOpen;
|
||||
|
||||
protected override void DrawLeafName(FileSystem<Profile>.Leaf leaf, in ProfileState state, bool selected)
|
||||
{
|
||||
var flag = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
|
||||
var name = IncognitoMode ? leaf.Value.Incognito : leaf.Value.Name.Text;
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value());
|
||||
using var _ = ImRaii.TreeNode(name, flag);
|
||||
}
|
||||
|
||||
protected override void DrawPopups()
|
||||
{
|
||||
DrawNewProfilePopup();
|
||||
}
|
||||
|
||||
private void DrawNewProfilePopup()
|
||||
{
|
||||
if (!ImGuiUtil.OpenNameField("##NewProfile", ref _newName))
|
||||
return;
|
||||
|
||||
if (_cloneProfile != null)
|
||||
{
|
||||
_profileManager.Clone(_cloneProfile, _newName, true);
|
||||
_cloneProfile = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_profileManager.Create(_newName, true);
|
||||
}
|
||||
|
||||
_newName = string.Empty;
|
||||
}
|
||||
|
||||
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3 = null)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ProfileChanged.Type.Created:
|
||||
case ProfileChanged.Type.Deleted:
|
||||
case ProfileChanged.Type.Renamed:
|
||||
case ProfileChanged.Type.Toggled:
|
||||
case ProfileChanged.Type.ChangedCharacterName:
|
||||
case ProfileChanged.Type.ReloadedAll:
|
||||
SetFilterDirty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void NewButton(Vector2 size)
|
||||
{
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new profile with default configuration.", false,
|
||||
true))
|
||||
return;
|
||||
|
||||
ImGui.OpenPopup("##NewProfile");
|
||||
}
|
||||
|
||||
private void CloneButton(Vector2 size)
|
||||
{
|
||||
var tt = SelectedLeaf == null
|
||||
? "No profile selected."
|
||||
: "Clone the currently selected profile to a duplicate";
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clone.ToIconString(), size, tt, SelectedLeaf == null, true))
|
||||
return;
|
||||
|
||||
_cloneProfile = Selected!;
|
||||
ImGui.OpenPopup("##NewProfile");
|
||||
}
|
||||
|
||||
private void DeleteButton(Vector2 size)
|
||||
=> DeleteSelectionButton(size, _configuration.UISettings.DeleteTemplateModifier, "profile", "profiles", _profileManager.Delete);
|
||||
|
||||
#region Filters
|
||||
|
||||
private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase;
|
||||
private LowerString _filter = LowerString.Empty;
|
||||
private int _filterType = -1;
|
||||
|
||||
private void SetFilterTooltip()
|
||||
{
|
||||
FilterTooltip = "Filter profiles for those where their full paths or names contain the given substring.\n"
|
||||
+ "Enter n:[string] to filter only for profile names and no paths.";
|
||||
}
|
||||
|
||||
/// <summary> Appropriately identify and set the string filter and its type. </summary>
|
||||
protected override bool ChangeFilter(string filterValue)
|
||||
{
|
||||
(_filter, _filterType) = filterValue.Length switch
|
||||
{
|
||||
0 => (LowerString.Empty, -1),
|
||||
> 1 when filterValue[1] == ':' =>
|
||||
filterValue[0] switch
|
||||
{
|
||||
'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
|
||||
'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
|
||||
_ => (new LowerString(filterValue), 0),
|
||||
},
|
||||
_ => (new LowerString(filterValue), 0),
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The overwritten filter method also computes the state.
|
||||
/// Folders have default state and are filtered out on the direct string instead of the other options.
|
||||
/// If any filter is set, they should be hidden by default unless their children are visible,
|
||||
/// or they contain the path search string.
|
||||
/// </summary>
|
||||
protected override bool ApplyFiltersAndState(FileSystem<Profile>.IPath path, out ProfileState state)
|
||||
{
|
||||
if (path is ProfileFileSystem.Folder f)
|
||||
{
|
||||
state = default;
|
||||
return FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase);
|
||||
}
|
||||
|
||||
return ApplyFiltersAndState((ProfileFileSystem.Leaf)path, out state);
|
||||
}
|
||||
|
||||
/// <summary> Apply the string filters. </summary>
|
||||
private bool ApplyStringFilters(ProfileFileSystem.Leaf leaf, Profile profile)
|
||||
{
|
||||
return _filterType switch
|
||||
{
|
||||
-1 => false,
|
||||
0 => !(_filter.IsContained(leaf.FullName()) || profile.Name.Contains(_filter)),
|
||||
1 => !profile.Name.Contains(_filter),
|
||||
_ => false, // Should never happen
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary> Combined wrapper for handling all filters and setting state. </summary>
|
||||
private bool ApplyFiltersAndState(ProfileFileSystem.Leaf leaf, out ProfileState state)
|
||||
{
|
||||
//Do not display temporary profiles;
|
||||
if (leaf.Value.IsTemporary)
|
||||
{
|
||||
state.Color = ColorId.DisabledProfile;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (leaf.Value.Enabled)
|
||||
state.Color = leaf.Value.CharacterName == _gameObjectService.GetCurrentPlayerName() ? ColorId.LocalCharacterEnabledProfile : ColorId.EnabledProfile;
|
||||
else
|
||||
state.Color = leaf.Value.CharacterName == _gameObjectService.GetCurrentPlayerName() ? ColorId.LocalCharacterDisabledProfile : ColorId.DisabledProfile;
|
||||
|
||||
return ApplyStringFilters(leaf, leaf.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.UI.Windows.Controls;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
|
||||
|
||||
public class ProfilePanel
|
||||
{
|
||||
private readonly ProfileFileSystemSelector _selector;
|
||||
private readonly ProfileManager _manager;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly TemplateCombo _templateCombo;
|
||||
private readonly GameStateService _gameStateService;
|
||||
|
||||
private string? _newName;
|
||||
private string? _newCharacterName;
|
||||
private Profile? _changedProfile;
|
||||
|
||||
private Action? _endAction;
|
||||
|
||||
private int _dragIndex = -1;
|
||||
|
||||
private string SelectionName
|
||||
=> _selector.Selected == null ? "No Selection" : _selector.IncognitoMode ? _selector.Selected.Incognito : _selector.Selected.Name.Text;
|
||||
|
||||
public ProfilePanel(
|
||||
ProfileFileSystemSelector selector,
|
||||
ProfileManager manager,
|
||||
PluginConfiguration configuration,
|
||||
TemplateCombo templateCombo,
|
||||
GameStateService gameStateService)
|
||||
{
|
||||
_selector = selector;
|
||||
_manager = manager;
|
||||
_configuration = configuration;
|
||||
_templateCombo = templateCombo;
|
||||
_gameStateService = gameStateService;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var group = ImRaii.Group();
|
||||
if (_selector.SelectedPaths.Count > 1)
|
||||
{
|
||||
DrawMultiSelection();
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawHeader();
|
||||
DrawPanel();
|
||||
}
|
||||
}
|
||||
|
||||
private HeaderDrawer.Button LockButton()
|
||||
=> _selector.Selected == null
|
||||
? HeaderDrawer.Button.Invisible
|
||||
: _selector.Selected.IsWriteProtected
|
||||
? new HeaderDrawer.Button
|
||||
{
|
||||
Description = "Make this profile editable.",
|
||||
Icon = FontAwesomeIcon.Lock,
|
||||
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, false)
|
||||
}
|
||||
: new HeaderDrawer.Button
|
||||
{
|
||||
Description = "Write-protect this profile.",
|
||||
Icon = FontAwesomeIcon.LockOpen,
|
||||
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, true)
|
||||
};
|
||||
|
||||
private void DrawHeader()
|
||||
=> HeaderDrawer.Draw(SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg),
|
||||
0, LockButton(),
|
||||
HeaderDrawer.Button.IncognitoButton(_selector.IncognitoMode, v => _selector.IncognitoMode = v));
|
||||
|
||||
private void DrawMultiSelection()
|
||||
{
|
||||
if (_selector.SelectedPaths.Count == 0)
|
||||
return;
|
||||
|
||||
var sizeType = ImGui.GetFrameHeight();
|
||||
var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100;
|
||||
var sizeMods = availableSizePercent * 35;
|
||||
var sizeFolders = availableSizePercent * 65;
|
||||
|
||||
ImGui.NewLine();
|
||||
ImGui.TextUnformatted("Currently Selected Profiles");
|
||||
ImGui.Separator();
|
||||
using var table = ImRaii.Table("profile", 3, ImGuiTableFlags.RowBg);
|
||||
ImGui.TableSetupColumn("btn", ImGuiTableColumnFlags.WidthFixed, sizeType);
|
||||
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, sizeMods);
|
||||
ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders);
|
||||
|
||||
var i = 0;
|
||||
foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p))
|
||||
.OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
using var id = ImRaii.PushId(i++);
|
||||
ImGui.TableNextColumn();
|
||||
var icon = (path is ProfileFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString();
|
||||
if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true))
|
||||
_selector.RemovePathFromMultiSelection(path);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(path is ProfileFileSystem.Leaf l ? _selector.IncognitoMode ? l.Value.Incognito : l.Value.Name.Text : string.Empty);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(_selector.IncognitoMode ? "Incognito is active" : fullName);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPanel()
|
||||
{
|
||||
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
|
||||
if (!child || _selector.Selected == null)
|
||||
return;
|
||||
|
||||
DrawEnabledSetting();
|
||||
using (var disabled = ImRaii.Disabled(_selector.Selected?.IsWriteProtected ?? true))
|
||||
{
|
||||
DrawBasicSettings();
|
||||
DrawTemplateArea();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEnabledSetting()
|
||||
{
|
||||
var spacing = ImGui.GetStyle().ItemInnerSpacing with { X = ImGui.GetStyle().ItemSpacing.X, Y = ImGui.GetStyle().ItemSpacing.Y };
|
||||
|
||||
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
|
||||
{
|
||||
var enabled = _selector.Selected?.Enabled ?? false;
|
||||
if (ImGui.Checkbox("##Enabled", ref enabled))
|
||||
_manager.SetEnabled(_selector.Selected!, enabled);
|
||||
ImGuiUtil.LabeledHelpMarker("Enabled",
|
||||
"Whether the templates in this profile should be applied at all. Only one profile can be enabled for a character at the same time.");
|
||||
|
||||
ImGui.SameLine();
|
||||
var isDefault = _manager.DefaultProfile == _selector.Selected;
|
||||
if (ImGui.Checkbox("##DefaultProfile", ref isDefault))
|
||||
_manager.SetDefaultProfile(isDefault ? _selector.Selected! : null);
|
||||
ImGuiUtil.LabeledHelpMarker("Default profile (Players and Retainers only)",
|
||||
"Whether the templates in this profile are applied to all players and retainers without a specific profile. Only one profile can be default at the same time.");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBasicSettings()
|
||||
{
|
||||
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)))
|
||||
{
|
||||
using (var table = ImRaii.Table("BasicSettings", 2))
|
||||
{
|
||||
ImGui.TableSetupColumn("BasicCol1", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("lorem ipsum dolor").X);
|
||||
ImGui.TableSetupColumn("BasicCol2", ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGuiUtil.DrawFrameColumn("Profile Name");
|
||||
ImGui.TableNextColumn();
|
||||
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
|
||||
var name = _newName ?? _selector.Selected!.Name;
|
||||
ImGui.SetNextItemWidth(width.X);
|
||||
|
||||
if (!_selector.IncognitoMode)
|
||||
{
|
||||
if (ImGui.InputText("##ProfileName", ref name, 128))
|
||||
{
|
||||
_newName = name;
|
||||
_changedProfile = _selector.Selected;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit() && _changedProfile != null)
|
||||
{
|
||||
_manager.Rename(_changedProfile, name);
|
||||
_newName = null;
|
||||
_changedProfile = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
ImGui.TextUnformatted(_selector.Selected!.Incognito);
|
||||
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGuiUtil.DrawFrameColumn("Character Name");
|
||||
ImGui.TableNextColumn();
|
||||
width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
|
||||
name = _newCharacterName ?? _selector.Selected!.CharacterName;
|
||||
ImGui.SetNextItemWidth(width.X);
|
||||
|
||||
using (var disabled = ImRaii.Disabled(_gameStateService.GameInPosingMode()))
|
||||
{
|
||||
if (!_selector.IncognitoMode)
|
||||
{
|
||||
if (ImGui.InputText("##CharacterName", ref name, 128))
|
||||
{
|
||||
_newCharacterName = name;
|
||||
_changedProfile = _selector.Selected;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit() && _changedProfile != null)
|
||||
{
|
||||
_manager.ChangeCharacterName(_changedProfile, name);
|
||||
_newCharacterName = null;
|
||||
_changedProfile = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
ImGui.TextUnformatted("Incognito active");
|
||||
|
||||
ImGui.SameLine();
|
||||
var enabled = _selector.Selected?.LimitLookupToOwnedObjects ?? false;
|
||||
if (ImGui.Checkbox("##LimitLookupToOwnedObjects", ref enabled))
|
||||
_manager.SetLimitLookupToOwned(_selector.Selected!, enabled);
|
||||
ImGuiUtil.LabeledHelpMarker("Limit to my creatures",
|
||||
"When enabled limits the character search to only your own summons, mounts and minions.\nUseful when there is possibility there will be another character with that name owned by another player.\n* For battle chocobo use \"Chocobo\" as character name.\n** If you are changing root scale for mount and want to keep your scale make sure your own scale is set to anything other than default value.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawTemplateArea()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled(_gameStateService.GameInPosingMode());
|
||||
using var table = ImRaii.Table("SetTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImGui.TableSetupColumn("##del", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight());
|
||||
ImGui.TableSetupColumn("##Index", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale);
|
||||
|
||||
ImGui.TableSetupColumn("Template", ImGuiTableColumnFlags.WidthFixed, 220 * ImGuiHelpers.GlobalScale);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
//warn: .ToList() might be performance critical at some point
|
||||
//the copying via ToList is done because manipulations with .Templates list result in "Collection was modified" exception here
|
||||
foreach (var (template, idx) in _selector.Selected!.Templates.WithIndex().ToList())
|
||||
{
|
||||
using var id = ImRaii.PushId(idx);
|
||||
ImGui.TableNextColumn();
|
||||
var keyValid = _configuration.UISettings.DeleteTemplateModifier.IsActive();
|
||||
var tt = keyValid
|
||||
? "Remove this template from the profile."
|
||||
: $"Remove this template from the profile.\nHold {_configuration.UISettings.DeleteTemplateModifier} to remove.";
|
||||
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, !keyValid, true))
|
||||
_endAction = () => _manager.DeleteTemplate(_selector.Selected!, idx);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Selectable($"#{idx + 1:D2}");
|
||||
DrawDragDrop(_selector.Selected!, idx);
|
||||
ImGui.TableNextColumn();
|
||||
_templateCombo.Draw(_selector.Selected!, template, idx);
|
||||
DrawDragDrop(_selector.Selected!, idx);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("New");
|
||||
ImGui.TableNextColumn();
|
||||
_templateCombo.Draw(_selector.Selected!, null, -1);
|
||||
ImGui.TableNextRow();
|
||||
|
||||
_endAction?.Invoke();
|
||||
_endAction = null;
|
||||
}
|
||||
|
||||
private void DrawDragDrop(Profile profile, int index)
|
||||
{
|
||||
const string dragDropLabel = "TemplateDragDrop";
|
||||
using (var target = ImRaii.DragDropTarget())
|
||||
{
|
||||
if (target.Success && ImGuiUtil.IsDropping(dragDropLabel))
|
||||
{
|
||||
if (_dragIndex >= 0)
|
||||
{
|
||||
var idx = _dragIndex;
|
||||
_endAction = () => _manager.MoveTemplate(profile, idx, index);
|
||||
}
|
||||
|
||||
_dragIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
using (var source = ImRaii.DragDropSource())
|
||||
{
|
||||
if (source)
|
||||
{
|
||||
ImGui.TextUnformatted($"Moving template #{index + 1:D2}...");
|
||||
if (ImGui.SetDragDropPayload(dragDropLabel, nint.Zero, 0))
|
||||
{
|
||||
_dragIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
|
||||
|
||||
public class ProfilesTab
|
||||
{
|
||||
private readonly ProfileFileSystemSelector _selector;
|
||||
private readonly ProfilePanel _panel;
|
||||
|
||||
public ProfilesTab(ProfileFileSystemSelector selector, ProfilePanel panel)
|
||||
{
|
||||
_selector = selector;
|
||||
_panel = panel;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
_selector.Draw(200f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.SameLine();
|
||||
_panel.Draw();
|
||||
}
|
||||
}
|
||||
223
CustomizePlus/UI/Windows/MainWindow/Tabs/SettingsTab.cs
Normal file
223
CustomizePlus/UI/Windows/MainWindow/Tabs/SettingsTab.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
//using CustomizePlus.UI.Windows.Debug;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Core.Services;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs;
|
||||
|
||||
public class SettingsTab
|
||||
{
|
||||
private const uint DiscordColor = 0xFFDA8972;
|
||||
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly TemplateManager _templateManager;
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly HookingService _hookingService;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly TemplateEditorManager _templateEditorManager;
|
||||
private readonly CPlusChangeLog _changeLog;
|
||||
private readonly MessageService _messageService;
|
||||
|
||||
public SettingsTab(
|
||||
PluginConfiguration configuration,
|
||||
TemplateManager templateManager,
|
||||
ProfileManager profileManager,
|
||||
HookingService hookingService,
|
||||
SaveService saveService,
|
||||
TemplateEditorManager templateEditorManager,
|
||||
CPlusChangeLog changeLog,
|
||||
MessageService messageService)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_templateManager = templateManager;
|
||||
_profileManager = profileManager;
|
||||
_hookingService = hookingService;
|
||||
_saveService = saveService;
|
||||
_templateEditorManager = templateEditorManager;
|
||||
_changeLog = changeLog;
|
||||
_messageService = messageService;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var child = ImRaii.Child("MainWindowChild");
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
DrawGeneralSettings();
|
||||
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
|
||||
using (var child2 = ImRaii.Child("SettingsChild"))
|
||||
{
|
||||
DrawInterface();
|
||||
DrawAdvancedSettings();
|
||||
}
|
||||
|
||||
DrawSupportButtons();
|
||||
}
|
||||
|
||||
#region General Settings
|
||||
// General Settings
|
||||
private void DrawGeneralSettings()
|
||||
{
|
||||
DrawPluginEnabledCheckbox();
|
||||
}
|
||||
|
||||
private void DrawPluginEnabledCheckbox()
|
||||
{
|
||||
using (var disabled = ImRaii.Disabled(_templateEditorManager.IsEditorActive))
|
||||
{
|
||||
var isChecked = _configuration.PluginEnabled;
|
||||
|
||||
//users doesn't really need to know what exactly this checkbox does so we just tell them it toggles all profiles
|
||||
if (CtrlHelper.CheckboxWithTextAndHelp("##pluginenabled", "Enable Customize+",
|
||||
"Globally enables or disables all plugin functionality.", ref isChecked))
|
||||
{
|
||||
_configuration.PluginEnabled = isChecked;
|
||||
_configuration.Save();
|
||||
_hookingService.ReloadHooks();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Interface Settings
|
||||
|
||||
private void DrawInterface()
|
||||
{
|
||||
var isShouldDraw = ImGui.CollapsingHeader("Interface");
|
||||
|
||||
if (!isShouldDraw)
|
||||
return;
|
||||
|
||||
DrawHideWindowInCutscene();
|
||||
DrawFoldersDefaultOpen();
|
||||
|
||||
if (Widget.DoubleModifierSelector("Template Deletion Modifier",
|
||||
"A modifier you need to hold while clicking the Delete Template button for it to take effect.", 100 * ImGuiHelpers.GlobalScale,
|
||||
_configuration.UISettings.DeleteTemplateModifier, v => _configuration.UISettings.DeleteTemplateModifier = v))
|
||||
_configuration.Save();
|
||||
}
|
||||
|
||||
private void DrawHideWindowInCutscene()
|
||||
{
|
||||
var isChecked = _configuration.UISettings.HideWindowInCutscene;
|
||||
|
||||
if (CtrlHelper.CheckboxWithTextAndHelp("##hidewindowincutscene", "Hide plugin windows in cutscenes",
|
||||
"Controls whether any Fantasia+ windows are hidden during cutscenes or not.", ref isChecked))
|
||||
{
|
||||
_configuration.UISettings.HideWindowInCutscene = isChecked;
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFoldersDefaultOpen()
|
||||
{
|
||||
var isChecked = _configuration.UISettings.FoldersDefaultOpen;
|
||||
|
||||
if (CtrlHelper.CheckboxWithTextAndHelp("##foldersdefaultopen", "Open all folders by default",
|
||||
"Controls whether folders in template and profile lists are open by default or not.", ref isChecked))
|
||||
{
|
||||
_configuration.UISettings.FoldersDefaultOpen = isChecked;
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Advanced Settings
|
||||
// Advanced Settings
|
||||
private void DrawAdvancedSettings()
|
||||
{
|
||||
var isShouldDraw = ImGui.CollapsingHeader("Advanced");
|
||||
|
||||
if (!isShouldDraw)
|
||||
return;
|
||||
|
||||
ImGui.NewLine();
|
||||
CtrlHelper.LabelWithIcon(FontAwesomeIcon.ExclamationTriangle,
|
||||
"These are advanced settings. Enable them at your own risk.");
|
||||
ImGui.NewLine();
|
||||
|
||||
DrawEnableRootPositionCheckbox();
|
||||
DrawDebugModeCheckbox();
|
||||
}
|
||||
|
||||
private void DrawEnableRootPositionCheckbox()
|
||||
{
|
||||
var isChecked = _configuration.EditorConfiguration.RootPositionEditingEnabled;
|
||||
if (CtrlHelper.CheckboxWithTextAndHelp("##rootpos", "Root editing",
|
||||
"Enables ability to edit the root bones.", ref isChecked))
|
||||
{
|
||||
_configuration.EditorConfiguration.RootPositionEditingEnabled = isChecked;
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDebugModeCheckbox()
|
||||
{
|
||||
var isChecked = _configuration.DebuggingModeEnabled;
|
||||
if (CtrlHelper.CheckboxWithTextAndHelp("##debugmode", "Debug mode",
|
||||
"Enables debug mode", ref isChecked))
|
||||
{
|
||||
_configuration.DebuggingModeEnabled = isChecked;
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Support Area
|
||||
private void DrawSupportButtons()
|
||||
{
|
||||
var width = ImGui.CalcTextSize("Join Discord for Support").X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
var xPos = ImGui.GetWindowWidth() - width;
|
||||
// Respect the scroll bar width.
|
||||
if (ImGui.GetScrollMaxY() > 0)
|
||||
xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X;
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(xPos, 0));
|
||||
DrawDiscordButton(width);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(xPos, 1 * ImGui.GetFrameHeightWithSpacing()));
|
||||
if (ImGui.Button("Show update history", new Vector2(width, 0)))
|
||||
_changeLog.Changelog.ForceOpen = true;
|
||||
}
|
||||
|
||||
/// <summary> Draw a button to open the official discord server. </summary>
|
||||
private void DrawDiscordButton(float width)
|
||||
{
|
||||
const string address = @"https://discord.gg/KvGJCCnG8t";
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Button, DiscordColor);
|
||||
if (ImGui.Button("Join Discord for Support", new Vector2(width, 0)))
|
||||
try
|
||||
{
|
||||
var process = new ProcessStartInfo(address)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
};
|
||||
Process.Start(process);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_messageService.NotificationMessage($"Unable to open Discord at {address}.", NotificationType.Error, false);
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip($"Open {address}");
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface.Utility;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Armatures.Data;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Templates.Data;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
|
||||
public class BoneEditorPanel
|
||||
{
|
||||
private readonly TemplateFileSystemSelector _templateFileSystemSelector;
|
||||
private readonly TemplateEditorManager _editorManager;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly GameObjectService _gameObjectService;
|
||||
|
||||
private BoneAttribute _editingAttribute;
|
||||
private int _precision;
|
||||
|
||||
private bool _isShowLiveBones;
|
||||
private bool _isMirrorModeEnabled;
|
||||
|
||||
public bool HasChanges => _editorManager.HasChanges;
|
||||
public bool IsEditorActive => _editorManager.IsEditorActive;
|
||||
public bool IsEditorPaused => _editorManager.IsEditorPaused;
|
||||
|
||||
/// <summary>
|
||||
/// Was character with name from CharacterName found in the object table or not
|
||||
/// </summary>
|
||||
public bool IsCharacterFound { get; private set; }
|
||||
public string CharacterName { get; private set; }
|
||||
|
||||
private ModelBone? _changedBone;
|
||||
private string? _changedBoneName;
|
||||
private BoneTransform? _changedBoneTransform;
|
||||
|
||||
private string? _newCharacterName;
|
||||
|
||||
private Dictionary<BoneData.BoneFamily, bool> _groupExpandedState = new();
|
||||
|
||||
private bool _openSavePopup;
|
||||
|
||||
private bool _isUnlocked = false;
|
||||
|
||||
public BoneEditorPanel(
|
||||
TemplateFileSystemSelector templateFileSystemSelector,
|
||||
TemplateEditorManager editorManager,
|
||||
PluginConfiguration configuration,
|
||||
GameObjectService gameObjectService)
|
||||
{
|
||||
_templateFileSystemSelector = templateFileSystemSelector;
|
||||
_editorManager = editorManager;
|
||||
_configuration = configuration;
|
||||
_gameObjectService = gameObjectService;
|
||||
|
||||
_isShowLiveBones = configuration.EditorConfiguration.ShowLiveBones;
|
||||
_isMirrorModeEnabled = configuration.EditorConfiguration.BoneMirroringEnabled;
|
||||
_precision = configuration.EditorConfiguration.EditorValuesPrecision;
|
||||
_editingAttribute = configuration.EditorConfiguration.EditorMode;
|
||||
CharacterName = configuration.EditorConfiguration.PreviewCharacterName!;
|
||||
}
|
||||
|
||||
public bool EnableEditor(Template template)
|
||||
{
|
||||
if (_editorManager.EnableEditor(template, CharacterName))
|
||||
{
|
||||
_editorManager.EditorProfile.LimitLookupToOwnedObjects = _configuration.EditorConfiguration.LimitLookupToOwnedObjects;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool DisableEditor()
|
||||
{
|
||||
if (!_editorManager.HasChanges)
|
||||
return _editorManager.DisableEditor();
|
||||
|
||||
if (_editorManager.HasChanges && !IsEditorActive)
|
||||
throw new Exception("Invalid state in BoneEditorPanel: has changes but editor is not active");
|
||||
|
||||
_openSavePopup = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
IsCharacterFound = _gameObjectService.FindActorsByName(CharacterName).Count() > 0;
|
||||
_isUnlocked = IsCharacterFound && IsEditorActive && !IsEditorPaused;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CharacterName))
|
||||
{
|
||||
CharacterName = _gameObjectService.GetCurrentPlayerName();
|
||||
_editorManager.ChangeEditorCharacter(CharacterName);
|
||||
|
||||
_configuration.EditorConfiguration.PreviewCharacterName = CharacterName;
|
||||
_configuration.Save();
|
||||
}
|
||||
|
||||
DrawEditorConfirmationPopup();
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)))
|
||||
{
|
||||
using (var table = ImRaii.Table("BasicSettings", 2))
|
||||
{
|
||||
ImGui.TableSetupColumn("BasicCol1", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Show editor preview on").X);
|
||||
ImGui.TableSetupColumn("BasicCol2", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGuiUtil.DrawFrameColumn("Show editor preview on");
|
||||
ImGui.TableNextColumn();
|
||||
var width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
|
||||
var name = _newCharacterName ?? CharacterName;
|
||||
ImGui.SetNextItemWidth(width.X);
|
||||
|
||||
using (var disabled = ImRaii.Disabled(!IsEditorActive || IsEditorPaused))
|
||||
{
|
||||
if (!_templateFileSystemSelector.IncognitoMode)
|
||||
{
|
||||
if (ImGui.InputText("##PreviewCharacterName", ref name, 128))
|
||||
{
|
||||
_newCharacterName = name;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
if (_newCharacterName == "")
|
||||
_newCharacterName = _gameObjectService.GetCurrentPlayerName();
|
||||
CharacterName = _newCharacterName!;
|
||||
_editorManager.ChangeEditorCharacter(CharacterName);
|
||||
|
||||
_configuration.EditorConfiguration.PreviewCharacterName = CharacterName;
|
||||
_configuration.Save();
|
||||
|
||||
_newCharacterName = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
ImGui.TextUnformatted("Incognito active");
|
||||
|
||||
ImGui.SameLine();
|
||||
var enabled = _editorManager.EditorProfile.LimitLookupToOwnedObjects;
|
||||
if (ImGui.Checkbox("##LimitLookupToOwnedObjects", ref enabled))
|
||||
{
|
||||
_editorManager.EditorProfile.LimitLookupToOwnedObjects = enabled;
|
||||
|
||||
_configuration.EditorConfiguration.LimitLookupToOwnedObjects = enabled;
|
||||
_configuration.Save();
|
||||
}
|
||||
ImGuiUtil.LabeledHelpMarker("Limit to my creatures",
|
||||
"When enabled limits the character search to only your own summons, mounts and minions.\nUseful when there is possibility there will be another character with that name owned by another player.\n* For battle chocobo use \"Chocobo\" as character name.\n** If you are changing root scale for mount and want to keep your scale make sure your own scale is set to anything other than default value.");
|
||||
}
|
||||
}
|
||||
|
||||
using (var table = ImRaii.Table("BoneEditorMenu", 2))
|
||||
{
|
||||
ImGui.TableSetupColumn("Attributes", ImGuiTableColumnFlags.WidthFixed);
|
||||
ImGui.TableSetupColumn("Space", ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
var modeChanged = false;
|
||||
if (ImGui.RadioButton("Position", _editingAttribute == BoneAttribute.Position))
|
||||
{
|
||||
_editingAttribute = BoneAttribute.Position;
|
||||
modeChanged = true;
|
||||
}
|
||||
CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!");
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.RadioButton("Rotation", _editingAttribute == BoneAttribute.Rotation))
|
||||
{
|
||||
_editingAttribute = BoneAttribute.Rotation;
|
||||
modeChanged = true;
|
||||
}
|
||||
CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!");
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.RadioButton("Scale", _editingAttribute == BoneAttribute.Scale))
|
||||
{
|
||||
_editingAttribute = BoneAttribute.Scale;
|
||||
modeChanged = true;
|
||||
}
|
||||
|
||||
if (modeChanged)
|
||||
{
|
||||
_configuration.EditorConfiguration.EditorMode = _editingAttribute;
|
||||
_configuration.Save();
|
||||
}
|
||||
|
||||
using (var disabled = ImRaii.Disabled(!_isUnlocked))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if (CtrlHelper.Checkbox("Show Live Bones", ref _isShowLiveBones))
|
||||
{
|
||||
_configuration.EditorConfiguration.ShowLiveBones = _isShowLiveBones;
|
||||
_configuration.Save();
|
||||
}
|
||||
CtrlHelper.AddHoverText($"If selected, present for editing all bones found in the game data,\nelse show only bones for which the profile already contains edits.");
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.BeginDisabled(!_isShowLiveBones);
|
||||
if (CtrlHelper.Checkbox("Mirror Mode", ref _isMirrorModeEnabled))
|
||||
{
|
||||
_configuration.EditorConfiguration.BoneMirroringEnabled = _isMirrorModeEnabled;
|
||||
_configuration.Save();
|
||||
}
|
||||
CtrlHelper.AddHoverText($"Bone changes will be reflected from left to right and vice versa");
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
if (ImGui.SliderInt("##Precision", ref _precision, 0, 6, $"{_precision} Place{(_precision == 1 ? "" : "s")}"))
|
||||
{
|
||||
_configuration.EditorConfiguration.EditorValuesPrecision = _precision;
|
||||
_configuration.Save();
|
||||
}
|
||||
CtrlHelper.AddHoverText("Level of precision to display while editing values");
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
using (var table = ImRaii.Table("BoneEditorContents", 6, ImGuiTableFlags.BordersOuterH | ImGuiTableFlags.BordersV | ImGuiTableFlags.ScrollY))
|
||||
{
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
var col1Label = _editingAttribute == BoneAttribute.Rotation ? "Roll" : "X";
|
||||
var col2Label = _editingAttribute == BoneAttribute.Rotation ? "Pitch" : "Y";
|
||||
var col3Label = _editingAttribute == BoneAttribute.Rotation ? "Yaw" : "Z";
|
||||
var col4Label = _editingAttribute == BoneAttribute.Scale ? "All" : "N/A";
|
||||
|
||||
ImGui.TableSetupColumn("Bones", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthFixed, 3 * CtrlHelper.IconButtonWidth);
|
||||
|
||||
ImGui.TableSetupColumn($"{col1Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn($"{col2Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn($"{col3Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn($"{col4Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetColumnEnabled(4, _editingAttribute == BoneAttribute.Scale);
|
||||
|
||||
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
IEnumerable<EditRowParams> relevantModelBones = null!;
|
||||
if (_editorManager.IsEditorActive && _editorManager.EditorProfile != null && _editorManager.EditorProfile.Armatures.Count > 0)
|
||||
relevantModelBones = _isShowLiveBones && _editorManager.EditorProfile.Armatures.Count > 0
|
||||
? _editorManager.EditorProfile.Armatures[0].GetAllBones().DistinctBy(x => x.BoneName).Select(x => new EditRowParams(x))
|
||||
: _editorManager.EditorProfile.Armatures[0].BoneTemplateBinding.Where(x => x.Value.Bones.ContainsKey(x.Key))
|
||||
.Select(x => new EditRowParams(x.Key, x.Value.Bones[x.Key])); //todo: this is awful
|
||||
else
|
||||
relevantModelBones = _templateFileSystemSelector.Selected!.Bones.Select(x => new EditRowParams(x.Key, x.Value));
|
||||
|
||||
var groupedBones = relevantModelBones.GroupBy(x => BoneData.GetBoneFamily(x.BoneCodeName));
|
||||
|
||||
foreach (var boneGroup in groupedBones.OrderBy(x => (int)x.Key))
|
||||
{
|
||||
//Hide root bone if it's not enabled in settings or if we are in rotation mode
|
||||
if (boneGroup.Key == BoneData.BoneFamily.Root &&
|
||||
(!_configuration.EditorConfiguration.RootPositionEditingEnabled ||
|
||||
_editingAttribute == BoneAttribute.Rotation))
|
||||
continue;
|
||||
|
||||
//create a dropdown entry for the family if one doesn't already exist
|
||||
//mind that it'll only be rendered if bones exist to fill it
|
||||
if (!_groupExpandedState.TryGetValue(boneGroup.Key, out var expanded))
|
||||
{
|
||||
_groupExpandedState[boneGroup.Key] = false;
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
if (expanded)
|
||||
{
|
||||
//paint the row in header colors if it's expanded
|
||||
ImGui.TableNextRow(ImGuiTableRowFlags.Headers);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
}
|
||||
|
||||
using var id = ImRaii.PushId(boneGroup.Key.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
CtrlHelper.ArrowToggle($"##{boneGroup.Key}", ref expanded);
|
||||
ImGui.SameLine();
|
||||
CtrlHelper.StaticLabel(boneGroup.Key.ToString());
|
||||
if (BoneData.DisplayableFamilies.TryGetValue(boneGroup.Key, out var tip) && tip != null)
|
||||
CtrlHelper.AddHoverText(tip);
|
||||
|
||||
if (expanded)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
foreach (var erp in boneGroup.OrderBy(x => BoneData.GetBoneRanking(x.BoneCodeName)))
|
||||
{
|
||||
CompleteBoneEditor(erp);
|
||||
}
|
||||
}
|
||||
|
||||
_groupExpandedState[boneGroup.Key] = expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawEditorConfirmationPopup()
|
||||
{
|
||||
if (_openSavePopup)
|
||||
{
|
||||
ImGui.OpenPopup("SavePopup");
|
||||
_openSavePopup = false;
|
||||
}
|
||||
|
||||
var viewportSize = ImGui.GetWindowViewport().Size;
|
||||
ImGui.SetNextWindowSize(new Vector2(viewportSize.X / 4, viewportSize.Y / 12));
|
||||
ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f));
|
||||
using var popup = ImRaii.Popup("SavePopup", ImGuiWindowFlags.Modal);
|
||||
if (!popup)
|
||||
return;
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(ImGui.GetWindowWidth() / 4 - 40, ImGui.GetWindowHeight() / 4));
|
||||
ImGuiUtil.TextWrapped("You have unsaved changes in current template, what would you like to do?");
|
||||
|
||||
var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 0);
|
||||
var yPos = ImGui.GetWindowHeight() - 2 * ImGui.GetFrameHeight();
|
||||
var xPos = (ImGui.GetWindowWidth() - ImGui.GetStyle().ItemSpacing.X) / 4 - buttonWidth.X;
|
||||
ImGui.SetCursorPos(new Vector2(xPos, yPos));
|
||||
if (ImGui.Button("Save", buttonWidth))
|
||||
{
|
||||
_editorManager.SaveChanges();
|
||||
_editorManager.DisableEditor();
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Save as a copy", buttonWidth))
|
||||
{
|
||||
_editorManager.SaveChanges(true);
|
||||
_editorManager.DisableEditor();
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Do not save", buttonWidth))
|
||||
{
|
||||
_editorManager.DisableEditor();
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Keep editing", buttonWidth))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
#region ImGui helper functions
|
||||
|
||||
public bool ResetBoneButton(string codename)
|
||||
{
|
||||
var output = ImGuiComponents.IconButton(codename, FontAwesomeIcon.Recycle);
|
||||
CtrlHelper.AddHoverText(
|
||||
$"Reset '{BoneData.GetBoneDisplayName(codename)}' to default {_editingAttribute} values");
|
||||
|
||||
if (output)
|
||||
_editorManager.ResetBoneAttributeChanges(codename, _editingAttribute);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private bool RevertBoneButton(string codename)
|
||||
{
|
||||
var output = ImGuiComponents.IconButton(codename, FontAwesomeIcon.ArrowCircleLeft);
|
||||
CtrlHelper.AddHoverText(
|
||||
$"Revert '{BoneData.GetBoneDisplayName(codename)}' to last saved {_editingAttribute} values");
|
||||
|
||||
if (output)
|
||||
_editorManager.RevertBoneAttributeChanges(codename, _editingAttribute);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private bool FullBoneSlider(string label, ref Vector3 value)
|
||||
{
|
||||
var velocity = _editingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f;
|
||||
var minValue = _editingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f;
|
||||
var maxValue = _editingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f;
|
||||
|
||||
var temp = _editingAttribute switch
|
||||
{
|
||||
BoneAttribute.Position => 0.0f,
|
||||
BoneAttribute.Rotation => 0.0f,
|
||||
_ => value.X == value.Y && value.Y == value.Z ? value.X : 1.0f
|
||||
};
|
||||
|
||||
|
||||
ImGui.PushItemWidth(ImGui.GetColumnWidth());
|
||||
if (ImGui.DragFloat(label, ref temp, velocity, minValue, maxValue, $"%.{_precision}f"))
|
||||
{
|
||||
value = new Vector3(temp, temp, temp);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool SingleValueSlider(string label, ref float value)
|
||||
{
|
||||
var velocity = _editingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f;
|
||||
var minValue = _editingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f;
|
||||
var maxValue = _editingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f;
|
||||
|
||||
ImGui.PushItemWidth(ImGui.GetColumnWidth());
|
||||
var temp = value;
|
||||
if (ImGui.DragFloat(label, ref temp, velocity, minValue, maxValue, $"%.{_precision}f"))
|
||||
{
|
||||
value = temp;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CompleteBoneEditor(EditRowParams bone)
|
||||
{
|
||||
var codename = bone.BoneCodeName;
|
||||
var displayName = bone.BoneDisplayName;
|
||||
var transform = new BoneTransform(bone.Transform);
|
||||
|
||||
var flagUpdate = false;
|
||||
|
||||
var newVector = _editingAttribute switch
|
||||
{
|
||||
BoneAttribute.Position => transform.Translation,
|
||||
BoneAttribute.Rotation => transform.Rotation,
|
||||
_ => transform.Scaling
|
||||
};
|
||||
|
||||
using var id = ImRaii.PushId(codename);
|
||||
ImGui.TableNextColumn();
|
||||
using (var disabled = ImRaii.Disabled(!_isUnlocked))
|
||||
{
|
||||
//----------------------------------
|
||||
ImGui.Dummy(new Vector2(CtrlHelper.IconButtonWidth * 0.75f, 0));
|
||||
ImGui.SameLine();
|
||||
ResetBoneButton(codename);
|
||||
ImGui.SameLine();
|
||||
RevertBoneButton(codename);
|
||||
|
||||
//----------------------------------
|
||||
ImGui.TableNextColumn();
|
||||
flagUpdate |= SingleValueSlider($"##{displayName}-X", ref newVector.X);
|
||||
|
||||
//----------------------------------
|
||||
ImGui.TableNextColumn();
|
||||
flagUpdate |= SingleValueSlider($"##{displayName}-Y", ref newVector.Y);
|
||||
|
||||
//-----------------------------------
|
||||
ImGui.TableNextColumn();
|
||||
flagUpdate |= SingleValueSlider($"##{displayName}-Z", ref newVector.Z);
|
||||
|
||||
//----------------------------------
|
||||
if (_editingAttribute != BoneAttribute.Scale)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var tempVec = new Vector3(newVector.X, newVector.Y, newVector.Z);
|
||||
flagUpdate |= FullBoneSlider($"##{displayName}-All", ref newVector);
|
||||
|
||||
if (_editingAttribute != BoneAttribute.Scale)
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
|
||||
//----------------------------------
|
||||
ImGui.TableNextColumn();
|
||||
CtrlHelper.StaticLabel(displayName, CtrlHelper.TextAlignment.Left, BoneData.IsIVCSBone(codename) ? $"(IVCS) {codename}" : codename);
|
||||
|
||||
if (flagUpdate)
|
||||
{
|
||||
transform.UpdateAttribute(_editingAttribute, newVector);
|
||||
|
||||
_editorManager.ModifyBoneTransform(codename, transform);
|
||||
if (_isMirrorModeEnabled && bone.Basis?.TwinBone != null) //todo: put it inside manager
|
||||
_editorManager.ModifyBoneTransform(bone.Basis.TwinBone.BoneName,
|
||||
BoneData.IsIVCSBone(codename) ? transform.GetSpecialReflection() : transform.GetStandardReflection());
|
||||
}
|
||||
|
||||
ImGui.TableNextRow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple structure for representing arguments to the editor table.
|
||||
/// Can be constructed with or without access to a live armature.
|
||||
/// </summary>
|
||||
internal struct EditRowParams
|
||||
{
|
||||
public string BoneCodeName;
|
||||
public string BoneDisplayName => BoneData.GetBoneDisplayName(BoneCodeName);
|
||||
public BoneTransform Transform;
|
||||
public ModelBone? Basis = null;
|
||||
|
||||
public EditRowParams(ModelBone mb)
|
||||
{
|
||||
BoneCodeName = mb.BoneName;
|
||||
Transform = mb.CustomizedTransform ?? new BoneTransform();
|
||||
Basis = mb;
|
||||
}
|
||||
|
||||
public EditRowParams(string codename, BoneTransform tr)
|
||||
{
|
||||
BoneCodeName = codename;
|
||||
Transform = tr;
|
||||
Basis = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.FileSystem.Selector;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Raii;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using static CustomizePlus.UI.Windows.MainWindow.Tabs.Templates.TemplateFileSystemSelector;
|
||||
using Newtonsoft.Json;
|
||||
using System.Windows.Forms;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Profiles;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
using CustomizePlus.Anamnesis;
|
||||
using CustomizePlus.Profiles.Data;
|
||||
using CustomizePlus.Templates.Events;
|
||||
using CustomizePlus.Profiles.Events;
|
||||
using CustomizePlus.Templates.Data;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
|
||||
public class TemplateFileSystemSelector : FileSystemSelector<Template, TemplateState>
|
||||
{
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly TemplateEditorManager _editorManager;
|
||||
private readonly TemplateManager _templateManager;
|
||||
private readonly TemplateChanged _templateChangedEvent;
|
||||
private readonly ProfileChanged _profileChangedEvent;
|
||||
private readonly ProfileManager _profileManager;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly PoseFileBoneLoader _poseFileBoneLoader;
|
||||
private readonly Logger _logger;
|
||||
private readonly PopupSystem _popupSystem;
|
||||
|
||||
private readonly FileDialogManager _importFilePicker = new();
|
||||
|
||||
private string? _clipboardText;
|
||||
private Template? _cloneTemplate;
|
||||
private string _newName = string.Empty;
|
||||
|
||||
public bool IncognitoMode
|
||||
{
|
||||
get => _configuration.UISettings.IncognitoMode;
|
||||
set
|
||||
{
|
||||
_configuration.UISettings.IncognitoMode = value;
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
|
||||
public struct TemplateState
|
||||
{
|
||||
public ColorId Color;
|
||||
}
|
||||
|
||||
public TemplateFileSystemSelector(
|
||||
TemplateFileSystem fileSystem,
|
||||
IKeyState keyState,
|
||||
Logger logger,
|
||||
PluginConfiguration configuration,
|
||||
TemplateEditorManager editorManager,
|
||||
TemplateManager templateManager,
|
||||
TemplateChanged templateChangedEvent,
|
||||
ProfileChanged profileChangedEvent,
|
||||
ProfileManager profileManager,
|
||||
MessageService messageService,
|
||||
PoseFileBoneLoader poseFileBoneLoader,
|
||||
PopupSystem popupSystem)
|
||||
: base(fileSystem, keyState, logger, allowMultipleSelection: true)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_editorManager = editorManager;
|
||||
_templateManager = templateManager;
|
||||
_templateChangedEvent = templateChangedEvent;
|
||||
_profileChangedEvent = profileChangedEvent;
|
||||
_profileManager = profileManager;
|
||||
_messageService = messageService;
|
||||
_poseFileBoneLoader = poseFileBoneLoader;
|
||||
_logger = logger;
|
||||
_popupSystem = popupSystem;
|
||||
|
||||
_popupSystem.RegisterPopup("template_editor_active_warn", "You need to stop bone editing before doing this action"/*, false, new Vector2(5, 12)*/);
|
||||
|
||||
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.TemplateFileSystemSelector);
|
||||
_profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.TemplateFileSystemSelector);
|
||||
|
||||
AddButton(NewButton, 0);
|
||||
AddButton(AnamnesisImportButton, 10);
|
||||
AddButton(ClipboardImportButton, 20);
|
||||
AddButton(CloneButton, 30);
|
||||
AddButton(DeleteButton, 1000);
|
||||
SetFilterTooltip();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_templateChangedEvent.Unsubscribe(OnTemplateChange);
|
||||
_profileChangedEvent.Unsubscribe(OnProfileChange);
|
||||
}
|
||||
|
||||
protected override uint ExpandedFolderColor
|
||||
=> ColorId.FolderExpanded.Value();
|
||||
|
||||
protected override uint CollapsedFolderColor
|
||||
=> ColorId.FolderCollapsed.Value();
|
||||
|
||||
protected override uint FolderLineColor
|
||||
=> ColorId.FolderLine.Value();
|
||||
|
||||
protected override bool FoldersDefaultOpen
|
||||
=> _configuration.UISettings.FoldersDefaultOpen;
|
||||
|
||||
protected override void DrawLeafName(FileSystem<Template>.Leaf leaf, in TemplateState state, bool selected)
|
||||
{
|
||||
var flag = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
|
||||
var name = IncognitoMode ? leaf.Value.Incognito : leaf.Value.Name.Text;
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value());
|
||||
using var _ = ImRaii.TreeNode(name, flag);
|
||||
}
|
||||
|
||||
protected override void Select(FileSystem<Template>.Leaf? leaf, bool clear, in TemplateState storage = default)
|
||||
{
|
||||
if (_editorManager.IsEditorActive)
|
||||
{
|
||||
Plugin.Logger.Debug("Blocked edited item change");
|
||||
ShowEditorWarningPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
base.Select(leaf, clear, storage);
|
||||
}
|
||||
|
||||
protected override void DrawPopups()
|
||||
{
|
||||
_importFilePicker.Draw();
|
||||
|
||||
//DrawEditorWarningPopup();
|
||||
DrawNewTemplatePopup();
|
||||
}
|
||||
|
||||
private void ShowEditorWarningPopup()
|
||||
{
|
||||
_popupSystem.ShowPopup("template_editor_active_warn");
|
||||
}
|
||||
|
||||
private void DrawNewTemplatePopup()
|
||||
{
|
||||
if (!ImGuiUtil.OpenNameField("##NewTemplate", ref _newName))
|
||||
return;
|
||||
|
||||
if (_clipboardText != null)
|
||||
{
|
||||
var importVer = Base64Helper.ImportFromBase64(Clipboard.GetText(), out var json);
|
||||
|
||||
var template = Convert.ToInt32(importVer) switch
|
||||
{
|
||||
//0 => ProfileConverter.ConvertFromConfigV0(json),
|
||||
//2 => ProfileConverter.ConvertFromConfigV2(json),
|
||||
//3 =>
|
||||
4 => JsonConvert.DeserializeObject<Template>(json),
|
||||
_ => null
|
||||
};
|
||||
if (template is Template tpl)
|
||||
_templateManager.Clone(tpl, _newName, true);
|
||||
else
|
||||
//Messager.NotificationMessage("Could not create a template, clipboard did not contain valid template data.", NotificationType.Error, false);
|
||||
throw new Exception("Invalid template"); //todo: temporary
|
||||
_clipboardText = null;
|
||||
}
|
||||
else if (_cloneTemplate != null)
|
||||
{
|
||||
_templateManager.Clone(_cloneTemplate, _newName, true);
|
||||
_cloneTemplate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_templateManager.Create(_newName, true);
|
||||
}
|
||||
|
||||
_newName = string.Empty;
|
||||
}
|
||||
|
||||
private void OnTemplateChange(TemplateChanged.Type type, Template? nullable, object? arg3 = null)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TemplateChanged.Type.Created:
|
||||
case TemplateChanged.Type.Deleted:
|
||||
case TemplateChanged.Type.Renamed:
|
||||
case TemplateChanged.Type.ReloadedAll:
|
||||
SetFilterDirty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3 = null)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ProfileChanged.Type.Created:
|
||||
case ProfileChanged.Type.Deleted:
|
||||
case ProfileChanged.Type.AddedTemplate:
|
||||
case ProfileChanged.Type.ChangedTemplate:
|
||||
case ProfileChanged.Type.RemovedTemplate:
|
||||
case ProfileChanged.Type.ReloadedAll:
|
||||
SetFilterDirty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void NewButton(Vector2 size)
|
||||
{
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new template with default configuration.", false,
|
||||
true))
|
||||
return;
|
||||
|
||||
if (_editorManager.IsEditorActive)
|
||||
{
|
||||
ShowEditorWarningPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.OpenPopup("##NewTemplate");
|
||||
}
|
||||
|
||||
private void ClipboardImportButton(Vector2 size)
|
||||
{
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), size, "Try to import a template from your clipboard.", false,
|
||||
true))
|
||||
return;
|
||||
|
||||
if (_editorManager.IsEditorActive)
|
||||
{
|
||||
ShowEditorWarningPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_clipboardText = ImGui.GetClipboardText();
|
||||
ImGui.OpenPopup("##NewTemplate");
|
||||
}
|
||||
catch
|
||||
{
|
||||
_messageService.NotificationMessage("Could not import data from clipboard.", NotificationType.Error, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void AnamnesisImportButton(Vector2 size)
|
||||
{
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, "Import a template from anamnesis pose file (scaling only)", false,
|
||||
true))
|
||||
return;
|
||||
|
||||
if (_editorManager.IsEditorActive)
|
||||
{
|
||||
ShowEditorWarningPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
_importFilePicker.OpenFileDialog("Import Pose File", ".pose", (isSuccess, path) =>
|
||||
{
|
||||
if (isSuccess)
|
||||
{
|
||||
var selectedFilePath = path.FirstOrDefault();
|
||||
//todo: check for selectedFilePath == null?
|
||||
|
||||
var bones = _poseFileBoneLoader.LoadBoneTransformsFromFile(selectedFilePath);
|
||||
|
||||
if (bones != null)
|
||||
{
|
||||
if (bones.Count == 0)
|
||||
{
|
||||
_messageService.NotificationMessage("Selected anamnesis pose file doesn't contain any scaled bones", NotificationType.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
_templateManager.Create(Path.GetFileNameWithoutExtension(selectedFilePath), bones, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_messageService.NotificationMessage(
|
||||
$"Error parsing anamnesis pose file at '{path}'", NotificationType.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug(isSuccess + " NO valid file has been selected. " + path);
|
||||
}
|
||||
}, 1, null, true);
|
||||
|
||||
/*MessageDialog.Show(
|
||||
"Due to technical limitations, Customize+ is only able to import scale values from *.pose files.\nPosition and rotation information will be ignored.",
|
||||
new Vector2(570, 100), ImportAction, "ana_import_pos_rot_warning");*/
|
||||
//todo: message dialog?
|
||||
}
|
||||
|
||||
private void CloneButton(Vector2 size)
|
||||
{
|
||||
var tt = SelectedLeaf == null
|
||||
? "No template selected."
|
||||
: "Clone the currently selected template to a duplicate";
|
||||
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clone.ToIconString(), size, tt, SelectedLeaf == null, true))
|
||||
return;
|
||||
|
||||
if (_editorManager.IsEditorActive)
|
||||
{
|
||||
ShowEditorWarningPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
_cloneTemplate = Selected!;
|
||||
ImGui.OpenPopup("##NewTemplate");
|
||||
}
|
||||
|
||||
private void DeleteButton(Vector2 size)
|
||||
=> DeleteSelectionButton(size, _configuration.UISettings.DeleteTemplateModifier, "template", "templates", (template) =>
|
||||
{
|
||||
if (_editorManager.IsEditorActive)
|
||||
{
|
||||
ShowEditorWarningPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
_templateManager.Delete(template);
|
||||
});
|
||||
|
||||
#region Filters
|
||||
|
||||
private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase;
|
||||
private LowerString _filter = LowerString.Empty;
|
||||
private int _filterType = -1;
|
||||
|
||||
private void SetFilterTooltip()
|
||||
{
|
||||
FilterTooltip = "Filter templates for those where their full paths or names contain the given substring.\n"
|
||||
+ "Enter n:[string] to filter only for template names and no paths.";
|
||||
}
|
||||
|
||||
/// <summary> Appropriately identify and set the string filter and its type. </summary>
|
||||
protected override bool ChangeFilter(string filterValue)
|
||||
{
|
||||
(_filter, _filterType) = filterValue.Length switch
|
||||
{
|
||||
0 => (LowerString.Empty, -1),
|
||||
> 1 when filterValue[1] == ':' =>
|
||||
filterValue[0] switch
|
||||
{
|
||||
'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
|
||||
'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
|
||||
_ => (new LowerString(filterValue), 0),
|
||||
},
|
||||
_ => (new LowerString(filterValue), 0),
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The overwritten filter method also computes the state.
|
||||
/// Folders have default state and are filtered out on the direct string instead of the other options.
|
||||
/// If any filter is set, they should be hidden by default unless their children are visible,
|
||||
/// or they contain the path search string.
|
||||
/// </summary>
|
||||
protected override bool ApplyFiltersAndState(FileSystem<Template>.IPath path, out TemplateState state)
|
||||
{
|
||||
if (path is TemplateFileSystem.Folder f)
|
||||
{
|
||||
state = default;
|
||||
return FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase);
|
||||
}
|
||||
|
||||
return ApplyFiltersAndState((TemplateFileSystem.Leaf)path, out state);
|
||||
}
|
||||
|
||||
/// <summary> Apply the string filters. </summary>
|
||||
private bool ApplyStringFilters(TemplateFileSystem.Leaf leaf, Template template)
|
||||
{
|
||||
return _filterType switch
|
||||
{
|
||||
-1 => false,
|
||||
0 => !(_filter.IsContained(leaf.FullName()) || template.Name.Contains(_filter)),
|
||||
1 => !template.Name.Contains(_filter),
|
||||
_ => false, // Should never happen
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary> Combined wrapper for handling all filters and setting state. </summary>
|
||||
private bool ApplyFiltersAndState(TemplateFileSystem.Leaf leaf, out TemplateState state)
|
||||
{
|
||||
//todo: more efficient to store links to profiles in templates than iterating here
|
||||
state.Color = _profileManager.GetProfilesUsingTemplate(leaf.Value).Any() ? ColorId.UsedTemplate : ColorId.UnusedTemplate;
|
||||
return ApplyStringFilters(leaf, leaf.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Windows.Forms;
|
||||
using CustomizePlus.Core.Data;
|
||||
using CustomizePlus.Game.Services;
|
||||
using CustomizePlus.Templates;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
using CustomizePlus.Core.Helpers;
|
||||
using CustomizePlus.Templates.Data;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
|
||||
public class TemplatePanel
|
||||
{
|
||||
private readonly TemplateFileSystemSelector _selector;
|
||||
private readonly TemplateManager _manager;
|
||||
private readonly GameStateService _gameStateService;
|
||||
private readonly BoneEditorPanel _boneEditor;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
private readonly MessageService _messageService;
|
||||
|
||||
private string? _newName;
|
||||
private Template? _changedTemplate;
|
||||
|
||||
private string SelectionName
|
||||
=> _selector.Selected == null ? "No Selection" : _selector.IncognitoMode ? _selector.Selected.Incognito : _selector.Selected.Name.Text;
|
||||
|
||||
public TemplatePanel(
|
||||
TemplateFileSystemSelector selector,
|
||||
TemplateManager manager,
|
||||
GameStateService gameStateService,
|
||||
BoneEditorPanel boneEditor,
|
||||
PluginConfiguration configuration,
|
||||
MessageService messageService)
|
||||
{
|
||||
_selector = selector;
|
||||
_manager = manager;
|
||||
_gameStateService = gameStateService;
|
||||
_boneEditor = boneEditor;
|
||||
_configuration = configuration;
|
||||
_messageService = messageService;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var group = ImRaii.Group();
|
||||
if (_selector.SelectedPaths.Count > 1)
|
||||
{
|
||||
DrawMultiSelection();
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawHeader();
|
||||
DrawPanel();
|
||||
}
|
||||
}
|
||||
|
||||
private HeaderDrawer.Button LockButton()
|
||||
=> _selector.Selected == null
|
||||
? HeaderDrawer.Button.Invisible
|
||||
: _selector.Selected.IsWriteProtected
|
||||
? new HeaderDrawer.Button
|
||||
{
|
||||
Description = "Make this template editable.",
|
||||
Icon = FontAwesomeIcon.Lock,
|
||||
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, false),
|
||||
Disabled = _boneEditor.IsEditorActive
|
||||
}
|
||||
: new HeaderDrawer.Button
|
||||
{
|
||||
Description = "Write-protect this template.",
|
||||
Icon = FontAwesomeIcon.LockOpen,
|
||||
OnClick = () => _manager.SetWriteProtection(_selector.Selected!, true),
|
||||
Disabled = _boneEditor.IsEditorActive
|
||||
};
|
||||
|
||||
/*private HeaderDrawer.Button SetFromClipboardButton()
|
||||
=> new()
|
||||
{
|
||||
Description =
|
||||
"Try to apply a template from your clipboard over this template.",
|
||||
Icon = FontAwesomeIcon.Clipboard,
|
||||
OnClick = SetFromClipboard,
|
||||
Visible = _selector.Selected != null,
|
||||
Disabled = (_selector.Selected?.IsWriteProtected ?? true) || _boneEditor.IsEditorActive,
|
||||
};*/
|
||||
|
||||
private HeaderDrawer.Button ExportToClipboardButton()
|
||||
=> new()
|
||||
{
|
||||
Description = "Copy the current template to your clipboard.",
|
||||
Icon = FontAwesomeIcon.Copy,
|
||||
OnClick = ExportToClipboard,
|
||||
Visible = _selector.Selected != null,
|
||||
Disabled = _boneEditor.IsEditorActive
|
||||
};
|
||||
|
||||
private void DrawHeader()
|
||||
=> HeaderDrawer.Draw(SelectionName, 0, ImGui.GetColorU32(ImGuiCol.FrameBg),
|
||||
1, /*SetFromClipboardButton(),*/ ExportToClipboardButton(), LockButton(),
|
||||
HeaderDrawer.Button.IncognitoButton(_selector.IncognitoMode, v => _selector.IncognitoMode = v));
|
||||
|
||||
private void DrawMultiSelection()
|
||||
{
|
||||
if (_selector.SelectedPaths.Count == 0)
|
||||
return;
|
||||
|
||||
var sizeType = ImGui.GetFrameHeight();
|
||||
var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100;
|
||||
var sizeMods = availableSizePercent * 35;
|
||||
var sizeFolders = availableSizePercent * 65;
|
||||
|
||||
ImGui.NewLine();
|
||||
ImGui.TextUnformatted("Currently Selected Templates");
|
||||
ImGui.Separator();
|
||||
using var table = ImRaii.Table("templates", 3, ImGuiTableFlags.RowBg);
|
||||
ImGui.TableSetupColumn("btn", ImGuiTableColumnFlags.WidthFixed, sizeType);
|
||||
ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, sizeMods);
|
||||
ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders);
|
||||
|
||||
var i = 0;
|
||||
foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p))
|
||||
.OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
using var id = ImRaii.PushId(i++);
|
||||
ImGui.TableNextColumn();
|
||||
var icon = (path is TemplateFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString();
|
||||
if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true))
|
||||
_selector.RemovePathFromMultiSelection(path);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(path is TemplateFileSystem.Leaf l ? _selector.IncognitoMode ? l.Value.Incognito : l.Value.Name.Text : string.Empty);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(_selector.IncognitoMode ? "Incognito is active" : fullName);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPanel()
|
||||
{
|
||||
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
|
||||
if (!child || _selector.Selected == null)
|
||||
return;
|
||||
|
||||
using (var disabled = ImRaii.Disabled(_selector.Selected?.IsWriteProtected ?? true))
|
||||
{
|
||||
DrawBasicSettings();
|
||||
DrawEditorToggle();
|
||||
}
|
||||
|
||||
_boneEditor.Draw();
|
||||
}
|
||||
|
||||
private void DrawEditorToggle()
|
||||
{
|
||||
if (ImGuiUtil.DrawDisabledButton($"{(_boneEditor.IsEditorActive ? "Finish" : "Start")} bone editing", Vector2.Zero,
|
||||
"Toggle the bone editor for this template",
|
||||
(_selector.Selected?.IsWriteProtected ?? true) || _gameStateService.GameInPosingMode() || !_configuration.PluginEnabled))
|
||||
{
|
||||
if (!_boneEditor.IsEditorActive)
|
||||
_boneEditor.EnableEditor(_selector.Selected!);
|
||||
else
|
||||
_boneEditor.DisableEditor();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBasicSettings()
|
||||
{
|
||||
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)))
|
||||
{
|
||||
using (var table = ImRaii.Table("BasicSettings", 2))
|
||||
{
|
||||
ImGui.TableSetupColumn("BasicCol1", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("lorem ipsum dolor").X);
|
||||
ImGui.TableSetupColumn("BasicCol2", ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
ImGuiUtil.DrawFrameColumn("Template Name");
|
||||
ImGui.TableNextColumn();
|
||||
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
|
||||
var name = _newName ?? _selector.Selected!.Name;
|
||||
ImGui.SetNextItemWidth(width.X);
|
||||
|
||||
if (!_selector.IncognitoMode)
|
||||
{
|
||||
if (ImGui.InputText("##Name", ref name, 128))
|
||||
{
|
||||
_newName = name;
|
||||
_changedTemplate = _selector.Selected;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit() && _changedTemplate != null)
|
||||
{
|
||||
_manager.Rename(_changedTemplate, name);
|
||||
_newName = null;
|
||||
_changedTemplate = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
ImGui.TextUnformatted(_selector.Selected!.Incognito);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*private void SetFromClipboard()
|
||||
{
|
||||
|
||||
}*/
|
||||
|
||||
private void ExportToClipboard()
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(Base64Helper.ExportToBase64(_selector.Selected!, Constants.ConfigurationVersion));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_messageService.NotificationMessage(ex, $"Could not copy {_selector.Selected!.Name} data to clipboard.",
|
||||
$"Could not copy data from template {_selector.Selected!.UniqueId} to clipboard", NotificationType.Error, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
|
||||
|
||||
public class TemplatesTab
|
||||
{
|
||||
private readonly TemplateFileSystemSelector _selector;
|
||||
private readonly TemplatePanel _panel;
|
||||
|
||||
public TemplatesTab(TemplateFileSystemSelector selector, TemplatePanel panel)
|
||||
{
|
||||
_selector = selector;
|
||||
_panel = panel;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
_selector.Draw(200f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.SameLine();
|
||||
_panel.Draw();
|
||||
}
|
||||
}
|
||||
127
CustomizePlus/UI/Windows/PopupSystem.cs
Normal file
127
CustomizePlus/UI/Windows/PopupSystem.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Raii;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using CustomizePlus.Configuration.Data;
|
||||
|
||||
namespace CustomizePlus.UI.Windows;
|
||||
|
||||
public class PopupSystem
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly PluginConfiguration _configuration;
|
||||
|
||||
private readonly Dictionary<string, PopupData> _popups = new();
|
||||
private readonly List<PopupData> _displayedPopups = new();
|
||||
|
||||
public PopupSystem(Logger logger, PluginConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public void RegisterPopup(string name, string text, bool displayOnce = false, Vector2? sizeDividers = null)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
|
||||
if (_popups.ContainsKey(name))
|
||||
throw new Exception($"Popup \"{name}\" is already registered");
|
||||
|
||||
_popups[name] = new PopupData { Name = name, Text = text, DisplayOnce = displayOnce, SizeDividers = sizeDividers };
|
||||
|
||||
_logger.Debug($"Popup \"{name}\" registered");
|
||||
}
|
||||
|
||||
public void ShowPopup(string name)
|
||||
{
|
||||
name = name.ToLowerInvariant();
|
||||
|
||||
if (!_popups.ContainsKey(name))
|
||||
throw new Exception($"Popup \"{name}\" is not registered");
|
||||
|
||||
var popup = _popups[name];
|
||||
|
||||
if (popup.DisplayRequested || _configuration.UISettings.ViewedMessageWindows.Contains(name))
|
||||
return;
|
||||
|
||||
popup.DisplayRequested = true;
|
||||
|
||||
//_logger.Debug($"Popup \"{name}\" set as requested to be displayed");
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
var viewportSize = ImGui.GetWindowViewport().Size;
|
||||
|
||||
foreach (var popup in _popups.Values)
|
||||
{
|
||||
if (popup.DisplayRequested)
|
||||
_displayedPopups.Add(popup);
|
||||
}
|
||||
|
||||
if (_displayedPopups.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _displayedPopups.Count; ++i)
|
||||
{
|
||||
var popup = _displayedPopups[i];
|
||||
if (popup.DisplayRequested)
|
||||
{
|
||||
ImGui.OpenPopup(popup.Name);
|
||||
popup.DisplayRequested = false;
|
||||
}
|
||||
|
||||
var xDiv = popup.SizeDividers?.X ?? 5;
|
||||
var yDiv = popup.SizeDividers?.Y ?? 12;
|
||||
|
||||
ImGui.SetNextWindowSize(new Vector2(viewportSize.X / xDiv, viewportSize.Y / yDiv));
|
||||
ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f));
|
||||
using var popupWindow = ImRaii.Popup(popup.Name, ImGuiWindowFlags.Modal);
|
||||
if (!popupWindow)
|
||||
{
|
||||
//fixes bug with windows being closed after going into gpose
|
||||
ImGui.OpenPopup(popup.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(10, ImGui.GetWindowHeight() / 4));
|
||||
ImGuiUtil.TextWrapped(popup.Text);
|
||||
|
||||
var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 0);
|
||||
var yPos = ImGui.GetWindowHeight() - 2 * ImGui.GetFrameHeight();
|
||||
var xPos = (ImGui.GetWindowWidth() - ImGui.GetStyle().ItemSpacing.X - buttonWidth.X) / 2;
|
||||
ImGui.SetCursorPos(new Vector2(xPos, yPos));
|
||||
if (ImGui.Button("Ok", buttonWidth))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
_displayedPopups.RemoveAt(i--);
|
||||
|
||||
if (popup.DisplayOnce)
|
||||
{
|
||||
_configuration.UISettings.ViewedMessageWindows.Add(popup.Name);
|
||||
_configuration.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PopupData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public bool DisplayRequested { get; set; }
|
||||
|
||||
public bool DisplayOnce { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Divider values used to divide viewport size when setting window size
|
||||
/// </summary>
|
||||
public Vector2? SizeDividers { get; set; }
|
||||
}
|
||||
}
|
||||
1
submodules/OtterGui
Submodule
1
submodules/OtterGui
Submodule
Submodule submodules/OtterGui added at df754445aa
1
submodules/Penumbra.Api
Submodule
1
submodules/Penumbra.Api
Submodule
Submodule submodules/Penumbra.Api added at 80f9793ef2
1
submodules/Penumbra.GameData
Submodule
1
submodules/Penumbra.GameData
Submodule
Submodule submodules/Penumbra.GameData added at ffdb966fec
1
submodules/Penumbra.String
Submodule
1
submodules/Penumbra.String
Submodule
Submodule submodules/Penumbra.String added at 620a7edf00
Reference in New Issue
Block a user