Add project files.
This commit is contained in:
273
.editorconfig
Normal file
273
.editorconfig
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# C# files
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
|
#### Core EditorConfig Options ####
|
||||||
|
|
||||||
|
# Indentation and spacing
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 4
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
end_of_line = crlf
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
|
#### .NET Coding Conventions ####
|
||||||
|
|
||||||
|
# Organize usings
|
||||||
|
dotnet_separate_import_directive_groups = false
|
||||||
|
dotnet_sort_system_directives_first = false
|
||||||
|
file_header_template = unset
|
||||||
|
|
||||||
|
# this. and Me. preferences
|
||||||
|
dotnet_style_qualification_for_event = false
|
||||||
|
dotnet_style_qualification_for_field = false
|
||||||
|
dotnet_style_qualification_for_method = false
|
||||||
|
dotnet_style_qualification_for_property = false
|
||||||
|
|
||||||
|
# Language keywords vs BCL types preferences
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||||
|
dotnet_style_predefined_type_for_member_access = true
|
||||||
|
|
||||||
|
# Parentheses preferences
|
||||||
|
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
|
||||||
|
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||||
|
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||||
|
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
dotnet_style_coalesce_expression = true
|
||||||
|
dotnet_style_collection_initializer = true
|
||||||
|
dotnet_style_explicit_tuple_names = true
|
||||||
|
dotnet_style_namespace_match_folder = true
|
||||||
|
dotnet_style_null_propagation = true
|
||||||
|
dotnet_style_object_initializer = true
|
||||||
|
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||||
|
dotnet_style_prefer_auto_properties = true
|
||||||
|
dotnet_style_prefer_compound_assignment = true
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true
|
||||||
|
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
|
||||||
|
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||||
|
dotnet_style_prefer_inferred_tuple_names = true
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||||
|
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||||
|
dotnet_style_prefer_simplified_interpolation = true
|
||||||
|
|
||||||
|
# Field preferences
|
||||||
|
dotnet_style_readonly_field = true
|
||||||
|
|
||||||
|
# Parameter preferences
|
||||||
|
dotnet_code_quality_unused_parameters = all
|
||||||
|
|
||||||
|
# Suppression preferences
|
||||||
|
dotnet_remove_unnecessary_suppression_exclusions = 0
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
dotnet_style_allow_multiple_blank_lines_experimental = true
|
||||||
|
dotnet_style_allow_statement_immediately_after_block_experimental = true
|
||||||
|
|
||||||
|
#### C# Coding Conventions ####
|
||||||
|
|
||||||
|
# var preferences
|
||||||
|
csharp_style_var_elsewhere = false
|
||||||
|
csharp_style_var_for_built_in_types = false
|
||||||
|
csharp_style_var_when_type_is_apparent = false
|
||||||
|
|
||||||
|
# Expression-bodied members
|
||||||
|
csharp_style_expression_bodied_accessors = true:silent
|
||||||
|
csharp_style_expression_bodied_constructors = false:silent
|
||||||
|
csharp_style_expression_bodied_indexers = true:silent
|
||||||
|
csharp_style_expression_bodied_lambdas = true:silent
|
||||||
|
csharp_style_expression_bodied_local_functions = false:silent
|
||||||
|
csharp_style_expression_bodied_methods = false:silent
|
||||||
|
csharp_style_expression_bodied_operators = false:silent
|
||||||
|
csharp_style_expression_bodied_properties = true:silent
|
||||||
|
|
||||||
|
# Pattern matching preferences
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true
|
||||||
|
csharp_style_prefer_extended_property_pattern = true
|
||||||
|
csharp_style_prefer_not_pattern = true
|
||||||
|
csharp_style_prefer_pattern_matching = true
|
||||||
|
csharp_style_prefer_switch_expression = true
|
||||||
|
|
||||||
|
# Null-checking preferences
|
||||||
|
csharp_style_conditional_delegate_call = true:suggestion
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
csharp_prefer_static_local_function = true:suggestion
|
||||||
|
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
|
||||||
|
csharp_style_prefer_readonly_struct = true:suggestion
|
||||||
|
|
||||||
|
# Code-block preferences
|
||||||
|
csharp_prefer_braces = true:silent
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
csharp_style_namespace_declarations = block_scoped:silent
|
||||||
|
csharp_style_prefer_method_group_conversion = true:silent
|
||||||
|
csharp_style_prefer_top_level_statements = true:silent
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
csharp_prefer_simple_default_expression = true:suggestion
|
||||||
|
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||||
|
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
||||||
|
csharp_style_inlined_variable_declaration = true:suggestion
|
||||||
|
csharp_style_prefer_index_operator = true:suggestion
|
||||||
|
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||||
|
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||||
|
csharp_style_prefer_range_operator = true:suggestion
|
||||||
|
csharp_style_prefer_tuple_swap = true:suggestion
|
||||||
|
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||||
|
csharp_style_throw_expression = true:suggestion
|
||||||
|
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||||
|
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||||
|
|
||||||
|
# 'using' directive preferences
|
||||||
|
csharp_using_directive_placement = outside_namespace:silent
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
|
||||||
|
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
|
||||||
|
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
|
||||||
|
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
|
||||||
|
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
|
||||||
|
|
||||||
|
#### C# Formatting Rules ####
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_else = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_new_line_between_query_expression_clauses = true
|
||||||
|
|
||||||
|
# Indentation preferences
|
||||||
|
csharp_indent_block_contents = true
|
||||||
|
csharp_indent_braces = false
|
||||||
|
csharp_indent_case_contents = true
|
||||||
|
csharp_indent_case_contents_when_block = true
|
||||||
|
csharp_indent_labels = one_less_than_current
|
||||||
|
csharp_indent_switch_labels = true
|
||||||
|
|
||||||
|
# Space preferences
|
||||||
|
csharp_space_after_cast = false
|
||||||
|
csharp_space_after_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_after_comma = true
|
||||||
|
csharp_space_after_dot = false
|
||||||
|
csharp_space_after_keywords_in_control_flow_statements = true
|
||||||
|
csharp_space_after_semicolon_in_for_statement = true
|
||||||
|
csharp_space_around_binary_operators = before_and_after
|
||||||
|
csharp_space_around_declaration_statements = false
|
||||||
|
csharp_space_before_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_before_comma = false
|
||||||
|
csharp_space_before_dot = false
|
||||||
|
csharp_space_before_open_square_brackets = false
|
||||||
|
csharp_space_before_semicolon_in_for_statement = false
|
||||||
|
csharp_space_between_empty_square_brackets = false
|
||||||
|
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||||
|
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||||
|
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_parentheses = false
|
||||||
|
csharp_space_between_square_brackets = false
|
||||||
|
|
||||||
|
# Wrapping preferences
|
||||||
|
csharp_preserve_single_line_blocks = true
|
||||||
|
csharp_preserve_single_line_statements = true
|
||||||
|
|
||||||
|
#### 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.private_or_internal_field_should_be_fieldstyle.severity = suggestion
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_fieldstyle.symbols = private_or_internal_field
|
||||||
|
dotnet_naming_rule.private_or_internal_field_should_be_fieldstyle.style = fieldstyle
|
||||||
|
|
||||||
|
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.private_or_internal_field.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
|
||||||
|
dotnet_naming_symbols.private_or_internal_field.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.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.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.fieldstyle.required_prefix = _
|
||||||
|
dotnet_naming_style.fieldstyle.required_suffix =
|
||||||
|
dotnet_naming_style.fieldstyle.word_separator =
|
||||||
|
dotnet_naming_style.fieldstyle.capitalization = camel_case
|
||||||
|
dotnet_diagnostic.MA0016.severity = silent
|
||||||
|
dotnet_diagnostic.MA0026.severity = warning
|
||||||
|
dotnet_diagnostic.MA0046.severity = suggestion
|
||||||
|
dotnet_diagnostic.MA0051.severity = suggestion
|
||||||
|
dotnet_diagnostic.MA0011.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
|
||||||
|
dotnet_style_readonly_field = true:suggestion
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:silent
|
||||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||||
|
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
|
||||||
|
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
|
||||||
350
.gitignore
vendored
Normal file
350
.gitignore
vendored
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Mono auto generated files
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.appxupload
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||||
|
MigrationBackup/
|
||||||
|
|
||||||
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
|
.ionide/
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "MareAPI"]
|
||||||
|
path = MareAPI
|
||||||
|
url = https://github.com/Penumbra-Sync/api.git
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Penumbra-Sync
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
46
MareSynchronos.sln
Normal file
46
MareSynchronos.sln
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.1.32328.378
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos", "MareSynchronos\MareSynchronos.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
.editorconfig = .editorconfig
|
||||||
|
EndProjectSection
|
||||||
|
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
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
117
MareSynchronos/.editorconfig
Normal file
117
MareSynchronos/.editorconfig
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
[*.{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_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]
|
||||||
|
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:silent
|
||||||
|
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
|
||||||
|
dotnet_diagnostic.MA0076.severity = silent
|
||||||
|
dotnet_diagnostic.MA0051.severity = 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_utf8_string_literals = true:suggestion
|
||||||
|
dotnet_diagnostic.S1075.severity = silent
|
||||||
|
dotnet_diagnostic.SS3358.severity = true:suggestion
|
||||||
|
dotnet_diagnostic.MA0007.severity = silent
|
||||||
|
dotnet_diagnostic.MA0075.severity = silent
|
||||||
|
|
||||||
|
# S3358: Ternary operators should not be nested
|
||||||
|
dotnet_diagnostic.S3358.severity = suggestion
|
||||||
|
|
||||||
|
# S6678: Use PascalCase for named placeholders
|
||||||
|
dotnet_diagnostic.S6678.severity = none
|
||||||
|
|
||||||
|
# S6605: Collection-specific "Exists" method should be used instead of the "Any" extension
|
||||||
|
dotnet_diagnostic.S6605.severity = none
|
||||||
|
|
||||||
|
# S6667: Logging in a catch clause should pass the caught exception as a parameter.
|
||||||
|
dotnet_diagnostic.S6667.severity = suggestion
|
||||||
|
|
||||||
|
# IDE0290: Use primary constructor
|
||||||
|
csharp_style_prefer_primary_constructors = false
|
||||||
|
|
||||||
|
# S3267: Loops should be simplified with "LINQ" expressions
|
||||||
|
dotnet_diagnostic.S3267.severity = silent
|
||||||
688
MareSynchronos/FileCache/CacheMonitor.cs
Normal file
688
MareSynchronos/FileCache/CacheMonitor.cs
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace MareSynchronos.FileCache;
|
||||||
|
|
||||||
|
public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly FileCompactor _fileCompactor;
|
||||||
|
private readonly FileCacheManager _fileDbManager;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private long _currentFileProgress = 0;
|
||||||
|
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||||
|
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
||||||
|
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"];
|
||||||
|
|
||||||
|
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, MareConfigService configService,
|
||||||
|
FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||||
|
FileCompactor fileCompactor) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_configService = configService;
|
||||||
|
_fileDbManager = fileDbManager;
|
||||||
|
_performanceCollector = performanceCollector;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_fileCompactor = fileCompactor;
|
||||||
|
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
|
||||||
|
StartMareWatcher(configService.Current.CacheFolder);
|
||||||
|
InvokeScan();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<HaltScanMessage>(this, (msg) => HaltScan(msg.Source));
|
||||||
|
Mediator.Subscribe<ResumeScanMessage>(this, (msg) => ResumeScan(msg.Source));
|
||||||
|
Mediator.Subscribe<DalamudLoginMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
StartMareWatcher(configService.Current.CacheFolder);
|
||||||
|
StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
|
||||||
|
InvokeScan();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PenumbraDirectoryChangedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
StartPenumbraWatcher(msg.ModDirectory);
|
||||||
|
InvokeScan();
|
||||||
|
});
|
||||||
|
if (_ipcManager.Penumbra.APIAvailable && !string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory))
|
||||||
|
{
|
||||||
|
StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
|
||||||
|
}
|
||||||
|
if (configService.Current.HasValidSetup())
|
||||||
|
{
|
||||||
|
StartMareWatcher(configService.Current.CacheFolder);
|
||||||
|
InvokeScan();
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = _periodicCalculationTokenSource.Token;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Starting Periodic Storage Directory Calculation Task");
|
||||||
|
var token = _periodicCalculationTokenSource.Token;
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(1).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecalculateFileCacheSize(token);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(1), token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long CurrentFileProgress => _currentFileProgress;
|
||||||
|
public long FileCacheSize { get; set; }
|
||||||
|
public long FileCacheDriveFree { get; set; }
|
||||||
|
public ConcurrentDictionary<string, int> HaltScanLocks { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
|
||||||
|
public long TotalFiles { get; private set; }
|
||||||
|
public long TotalFilesStorage { get; private set; }
|
||||||
|
|
||||||
|
public void HaltScan(string source)
|
||||||
|
{
|
||||||
|
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
||||||
|
HaltScanLocks[source]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
||||||
|
private readonly Dictionary<string, WatcherChange> _watcherChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<string, WatcherChange> _mareChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public void StopMonitoring()
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Stopping monitoring of Penumbra and Mare storage folders");
|
||||||
|
MareWatcher?.Dispose();
|
||||||
|
PenumbraWatcher?.Dispose();
|
||||||
|
MareWatcher = null;
|
||||||
|
PenumbraWatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StorageisNTFS { get; private set; } = false;
|
||||||
|
|
||||||
|
public void StartMareWatcher(string? marePath)
|
||||||
|
{
|
||||||
|
MareWatcher?.Dispose();
|
||||||
|
if (string.IsNullOrEmpty(marePath) || !Directory.Exists(marePath))
|
||||||
|
{
|
||||||
|
MareWatcher = null;
|
||||||
|
Logger.LogWarning("Mare file path is not set, cannot start the FSW for Mare.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
|
||||||
|
StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Logger.LogInformation("Mare Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
|
||||||
|
|
||||||
|
Logger.LogDebug("Initializing Mare FSW on {path}", marePath);
|
||||||
|
MareWatcher = new()
|
||||||
|
{
|
||||||
|
Path = marePath,
|
||||||
|
InternalBufferSize = 8388608,
|
||||||
|
NotifyFilter = NotifyFilters.CreationTime
|
||||||
|
| NotifyFilters.LastWrite
|
||||||
|
| NotifyFilters.FileName
|
||||||
|
| NotifyFilters.DirectoryName
|
||||||
|
| NotifyFilters.Size,
|
||||||
|
Filter = "*.*",
|
||||||
|
IncludeSubdirectories = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
MareWatcher.Deleted += MareWatcher_FileChanged;
|
||||||
|
MareWatcher.Created += MareWatcher_FileChanged;
|
||||||
|
MareWatcher.EnableRaisingEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MareWatcher_FileChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Mare FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||||
|
|
||||||
|
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
lock (_watcherChanges)
|
||||||
|
{
|
||||||
|
_mareChanges[e.FullPath] = new(e.ChangeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = MareWatcherExecution();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartPenumbraWatcher(string? penumbraPath)
|
||||||
|
{
|
||||||
|
PenumbraWatcher?.Dispose();
|
||||||
|
if (string.IsNullOrEmpty(penumbraPath))
|
||||||
|
{
|
||||||
|
PenumbraWatcher = null;
|
||||||
|
Logger.LogWarning("Penumbra is not connected or the path is not set, cannot start FSW for Penumbra.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("Initializing Penumbra FSW on {path}", penumbraPath);
|
||||||
|
PenumbraWatcher = new()
|
||||||
|
{
|
||||||
|
Path = penumbraPath,
|
||||||
|
InternalBufferSize = 8388608,
|
||||||
|
NotifyFilter = NotifyFilters.CreationTime
|
||||||
|
| NotifyFilters.LastWrite
|
||||||
|
| NotifyFilters.FileName
|
||||||
|
| NotifyFilters.DirectoryName
|
||||||
|
| NotifyFilters.Size,
|
||||||
|
Filter = "*.*",
|
||||||
|
IncludeSubdirectories = true
|
||||||
|
};
|
||||||
|
|
||||||
|
PenumbraWatcher.Deleted += Fs_Changed;
|
||||||
|
PenumbraWatcher.Created += Fs_Changed;
|
||||||
|
PenumbraWatcher.Changed += Fs_Changed;
|
||||||
|
PenumbraWatcher.Renamed += Fs_Renamed;
|
||||||
|
PenumbraWatcher.EnableRaisingEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fs_Changed(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(e.FullPath)) return;
|
||||||
|
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_watcherChanges)
|
||||||
|
{
|
||||||
|
_watcherChanges[e.FullPath] = new(e.ChangeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("FSW {event}: {path}", e.ChangeType, e.FullPath);
|
||||||
|
|
||||||
|
_ = PenumbraWatcherExecution();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fs_Renamed(object sender, RenamedEventArgs e)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(e.FullPath))
|
||||||
|
{
|
||||||
|
var directoryFiles = Directory.GetFiles(e.FullPath, "*.*", SearchOption.AllDirectories);
|
||||||
|
lock (_watcherChanges)
|
||||||
|
{
|
||||||
|
foreach (var file in directoryFiles)
|
||||||
|
{
|
||||||
|
if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue;
|
||||||
|
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_watcherChanges.Remove(oldPath);
|
||||||
|
_watcherChanges[file] = new(WatcherChangeTypes.Renamed, oldPath);
|
||||||
|
Logger.LogTrace("FSW Renamed: {path} -> {new}", oldPath, file);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
lock (_watcherChanges)
|
||||||
|
{
|
||||||
|
_watcherChanges.Remove(e.OldFullPath);
|
||||||
|
_watcherChanges[e.FullPath] = new(WatcherChangeTypes.Renamed, e.OldFullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("FSW Renamed: {path} -> {new}", e.OldFullPath, e.FullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = PenumbraWatcherExecution();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CancellationTokenSource _penumbraFswCts = new();
|
||||||
|
private CancellationTokenSource _mareFswCts = new();
|
||||||
|
public FileSystemWatcher? PenumbraWatcher { get; private set; }
|
||||||
|
public FileSystemWatcher? MareWatcher { get; private set; }
|
||||||
|
|
||||||
|
private async Task MareWatcherExecution()
|
||||||
|
{
|
||||||
|
_mareFswCts = _mareFswCts.CancelRecreate();
|
||||||
|
var token = _mareFswCts.Token;
|
||||||
|
var delay = TimeSpan.FromSeconds(5);
|
||||||
|
Dictionary<string, WatcherChange> changes;
|
||||||
|
lock (_mareChanges)
|
||||||
|
changes = _mareChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
} while (HaltScanLocks.Any(f => f.Value > 0));
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_mareChanges)
|
||||||
|
{
|
||||||
|
foreach (var key in changes.Keys)
|
||||||
|
{
|
||||||
|
_mareChanges.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleChanges(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleChanges(Dictionary<string, WatcherChange> changes)
|
||||||
|
{
|
||||||
|
lock (_fileDbManager)
|
||||||
|
{
|
||||||
|
var deletedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
|
var renamedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
|
||||||
|
var remainingEntries = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
|
|
||||||
|
foreach (var entry in deletedEntries)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in renamedEntries)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("FSW Change: Renamed - {oldVal} => {val}", entry.Value.OldPath, entry.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in remainingEntries)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
var allChanges = deletedEntries
|
||||||
|
.Concat(renamedEntries.Select(c => c.Value.OldPath!))
|
||||||
|
.Concat(renamedEntries.Select(c => c.Key))
|
||||||
|
.Concat(remainingEntries)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
|
||||||
|
|
||||||
|
_fileDbManager.WriteOutFullCsv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PenumbraWatcherExecution()
|
||||||
|
{
|
||||||
|
_penumbraFswCts = _penumbraFswCts.CancelRecreate();
|
||||||
|
var token = _penumbraFswCts.Token;
|
||||||
|
Dictionary<string, WatcherChange> changes;
|
||||||
|
lock (_watcherChanges)
|
||||||
|
changes = _watcherChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
|
||||||
|
var delay = TimeSpan.FromSeconds(10);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
} while (HaltScanLocks.Any(f => f.Value > 0));
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_watcherChanges)
|
||||||
|
{
|
||||||
|
foreach (var key in changes.Keys)
|
||||||
|
{
|
||||||
|
_watcherChanges.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleChanges(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeScan()
|
||||||
|
{
|
||||||
|
TotalFiles = 0;
|
||||||
|
_currentFileProgress = 0;
|
||||||
|
_scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
|
var token = _scanCancellationTokenSource.Token;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Starting Full File Scan");
|
||||||
|
TotalFiles = 0;
|
||||||
|
_currentFileProgress = 0;
|
||||||
|
while (_dalamudUtil.IsOnFrameworkThread)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Scanner is on framework, waiting for leaving thread before continuing");
|
||||||
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread scanThread = new(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_performanceCollector.LogPerformance(this, $"FullFileScan", () => FullFileScan(token));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error during Full File Scan");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Priority = ThreadPriority.Lowest,
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
scanThread.Start();
|
||||||
|
while (scanThread.IsAlive)
|
||||||
|
{
|
||||||
|
await Task.Delay(250).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
TotalFiles = 0;
|
||||||
|
_currentFileProgress = 0;
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecalculateFileCacheSize(CancellationToken token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
|
||||||
|
{
|
||||||
|
FileCacheSize = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileCacheSize = -1;
|
||||||
|
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FileCacheDriveFree = di.AvailableFreeSpace;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Could not determine drive size for Storage Folder {folder}", _configService.Current.CacheFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder).Select(f => new FileInfo(f))
|
||||||
|
.OrderBy(f => f.LastAccessTime).ToList();
|
||||||
|
FileCacheSize = files
|
||||||
|
.Sum(f =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _fileCompactor.GetFileSizeOnDisk(f, StorageisNTFS);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
|
||||||
|
|
||||||
|
if (FileCacheSize < maxCacheInBytes) return;
|
||||||
|
|
||||||
|
var maxCacheBuffer = maxCacheInBytes * 0.05d;
|
||||||
|
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer)
|
||||||
|
{
|
||||||
|
var oldestFile = files[0];
|
||||||
|
FileCacheSize -= _fileCompactor.GetFileSizeOnDisk(oldestFile);
|
||||||
|
File.Delete(oldestFile.FullName);
|
||||||
|
files.Remove(oldestFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetLocks()
|
||||||
|
{
|
||||||
|
HaltScanLocks.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeScan(string source)
|
||||||
|
{
|
||||||
|
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
||||||
|
|
||||||
|
HaltScanLocks[source]--;
|
||||||
|
if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
_scanCancellationTokenSource?.Cancel();
|
||||||
|
PenumbraWatcher?.Dispose();
|
||||||
|
MareWatcher?.Dispose();
|
||||||
|
_penumbraFswCts?.CancelDispose();
|
||||||
|
_mareFswCts?.CancelDispose();
|
||||||
|
_periodicCalculationTokenSource?.CancelDispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FullFileScan(CancellationToken ct)
|
||||||
|
{
|
||||||
|
TotalFiles = 1;
|
||||||
|
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||||
|
bool penDirExists = true;
|
||||||
|
bool cacheDirExists = true;
|
||||||
|
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))
|
||||||
|
{
|
||||||
|
penDirExists = false;
|
||||||
|
Logger.LogWarning("Penumbra directory is not set or does not exist.");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
|
||||||
|
{
|
||||||
|
cacheDirExists = false;
|
||||||
|
Logger.LogWarning("Mare Cache directory is not set or does not exist.");
|
||||||
|
}
|
||||||
|
if (!penDirExists || !cacheDirExists)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousThreadPriority = Thread.CurrentThread.Priority;
|
||||||
|
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
|
||||||
|
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, _configService.Current.CacheFolder);
|
||||||
|
|
||||||
|
Dictionary<string, string[]> penumbraFiles = new(StringComparer.Ordinal);
|
||||||
|
foreach (var folder in Directory.EnumerateDirectories(penumbraDir!))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
penumbraFiles[folder] =
|
||||||
|
[
|
||||||
|
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
||||||
|
.AsParallel()
|
||||||
|
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
|
||||||
|
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Could not enumerate path {path}", folder);
|
||||||
|
}
|
||||||
|
Thread.Sleep(50);
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allCacheFiles = Directory.GetFiles(_configService.Current.CacheFolder, "*.*", SearchOption.TopDirectoryOnly)
|
||||||
|
.AsParallel()
|
||||||
|
.Where(f =>
|
||||||
|
{
|
||||||
|
var val = f.Split('\\')[^1];
|
||||||
|
return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
var allScannedFiles = (penumbraFiles.SelectMany(k => k.Value))
|
||||||
|
.Concat(allCacheFiles)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(t => t.ToLowerInvariant(), t => false, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
TotalFiles = allScannedFiles.Count;
|
||||||
|
Thread.CurrentThread.Priority = previousThreadPriority;
|
||||||
|
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
// scan files from database
|
||||||
|
var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8);
|
||||||
|
|
||||||
|
List<FileCacheEntity> entitiesToRemove = [];
|
||||||
|
List<FileCacheEntity> entitiesToUpdate = [];
|
||||||
|
object sync = new();
|
||||||
|
Thread[] workerThreads = new Thread[threadCount];
|
||||||
|
|
||||||
|
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());
|
||||||
|
|
||||||
|
TotalFilesStorage = fileCaches.Count;
|
||||||
|
|
||||||
|
for (int i = 0; i < threadCount; i++)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Creating Thread {i}", i);
|
||||||
|
workerThreads[i] = new((tcounter) =>
|
||||||
|
{
|
||||||
|
var threadNr = (int)tcounter!;
|
||||||
|
Logger.LogTrace("Spawning Worker Thread {i}", threadNr);
|
||||||
|
while (!ct.IsCancellationRequested && fileCaches.TryDequeue(out var workload))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Penumbra not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validatedCacheResult = _fileDbManager.ValidateFileCacheEntity(workload);
|
||||||
|
if (validatedCacheResult.State != FileState.RequireDeletion)
|
||||||
|
{
|
||||||
|
lock (sync) { allScannedFiles[validatedCacheResult.FileCache.ResolvedFilepath] = true; }
|
||||||
|
}
|
||||||
|
if (validatedCacheResult.State == FileState.RequireUpdate)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("To update: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
|
||||||
|
lock (sync) { entitiesToUpdate.Add(validatedCacheResult.FileCache); }
|
||||||
|
}
|
||||||
|
else if (validatedCacheResult.State == FileState.RequireDeletion)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("To delete: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
|
||||||
|
lock (sync) { entitiesToRemove.Add(validatedCacheResult.FileCache); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
Interlocked.Increment(ref _currentFileProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Ending Worker Thread {i}", threadNr);
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Priority = ThreadPriority.Lowest,
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
workerThreads[i].Start(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested && workerThreads.Any(u => u.IsAlive))
|
||||||
|
{
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
Logger.LogTrace("Threads exited");
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Penumbra not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entitiesToUpdate.Any() || entitiesToRemove.Any())
|
||||||
|
{
|
||||||
|
foreach (var entity in entitiesToUpdate)
|
||||||
|
{
|
||||||
|
_fileDbManager.UpdateHashedFile(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entity in entitiesToRemove)
|
||||||
|
{
|
||||||
|
_fileDbManager.RemoveHashedFile(entity.Hash, entity.PrefixedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileDbManager.WriteOutFullCsv();
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Scanner validated existing db files");
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Penumbra not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
// scan new files
|
||||||
|
if (allScannedFiles.Any(c => !c.Value))
|
||||||
|
{
|
||||||
|
Parallel.ForEach(allScannedFiles.Where(c => !c.Value).Select(c => c.Key),
|
||||||
|
new ParallelOptions()
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = threadCount,
|
||||||
|
CancellationToken = ct
|
||||||
|
}, (cachePath) =>
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Penumbra not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
||||||
|
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Increment(ref _currentFileProgress);
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.LogTrace("Scanner added {notScanned} new files to db", allScannedFiles.Count(c => !c.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("Scan complete");
|
||||||
|
TotalFiles = 0;
|
||||||
|
_currentFileProgress = 0;
|
||||||
|
entitiesToRemove.Clear();
|
||||||
|
allScannedFiles.Clear();
|
||||||
|
|
||||||
|
if (!_configService.Current.InitialScanComplete)
|
||||||
|
{
|
||||||
|
_configService.Current.InitialScanComplete = true;
|
||||||
|
_configService.Save();
|
||||||
|
StartMareWatcher(_configService.Current.CacheFolder);
|
||||||
|
StartPenumbraWatcher(penumbraDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
MareSynchronos/FileCache/FileCacheEntity.cs
Normal file
29
MareSynchronos/FileCache/FileCacheEntity.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MareSynchronos.FileCache;
|
||||||
|
|
||||||
|
public class FileCacheEntity
|
||||||
|
{
|
||||||
|
public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null)
|
||||||
|
{
|
||||||
|
Size = size;
|
||||||
|
CompressedSize = compressedSize;
|
||||||
|
Hash = hash;
|
||||||
|
PrefixedFilePath = path;
|
||||||
|
LastModifiedDateTicks = lastModifiedDateTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long? CompressedSize { get; set; }
|
||||||
|
public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}|{Size ?? -1}|{CompressedSize ?? -1}";
|
||||||
|
public string Hash { get; set; }
|
||||||
|
public bool IsCacheEntry => PrefixedFilePath.StartsWith(FileCacheManager.CachePrefix, StringComparison.OrdinalIgnoreCase);
|
||||||
|
public string LastModifiedDateTicks { get; set; }
|
||||||
|
public string PrefixedFilePath { get; init; }
|
||||||
|
public string ResolvedFilepath { get; private set; } = string.Empty;
|
||||||
|
public long? Size { get; set; }
|
||||||
|
|
||||||
|
public void SetResolvedFilePath(string filePath)
|
||||||
|
{
|
||||||
|
ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
509
MareSynchronos/FileCache/FileCacheManager.cs
Normal file
509
MareSynchronos/FileCache/FileCacheManager.cs
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
using K4os.Compression.LZ4.Legacy;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.FileCache;
|
||||||
|
|
||||||
|
public sealed class FileCacheManager : IHostedService
|
||||||
|
{
|
||||||
|
public const string CachePrefix = "{cache}";
|
||||||
|
public const string CsvSplit = "|";
|
||||||
|
public const string PenumbraPrefix = "{penumbra}";
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly string _csvPath;
|
||||||
|
private readonly ConcurrentDictionary<string, List<FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||||
|
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
||||||
|
private readonly object _fileWriteLock = new();
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
|
public string CacheFolder => _configService.Current.CacheFolder;
|
||||||
|
|
||||||
|
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, MareConfigService configService, MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_configService = configService;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_csvPath = Path.Combine(configService.ConfigurationDirectory, "FileCache.csv");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CsvBakPath => _csvPath + ".bak";
|
||||||
|
|
||||||
|
public FileCacheEntity? CreateCacheEntry(string path)
|
||||||
|
{
|
||||||
|
FileInfo fi = new(path);
|
||||||
|
if (!fi.Exists) return null;
|
||||||
|
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||||
|
var fullName = fi.FullName.ToLowerInvariant();
|
||||||
|
if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null;
|
||||||
|
string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
return CreateFileCacheEntity(fi, prefixedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileCacheEntity? CreateFileEntry(string path)
|
||||||
|
{
|
||||||
|
FileInfo fi = new(path);
|
||||||
|
if (!fi.Exists) return null;
|
||||||
|
_logger.LogTrace("Creating file entry for {path}", path);
|
||||||
|
var fullName = fi.FullName.ToLowerInvariant();
|
||||||
|
if (!fullName.Contains(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null;
|
||||||
|
string prefixedPath = fullName.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
return CreateFileCacheEntity(fi, prefixedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList();
|
||||||
|
|
||||||
|
public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
|
||||||
|
{
|
||||||
|
List<FileCacheEntity> output = [];
|
||||||
|
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||||
|
{
|
||||||
|
foreach (var fileCache in fileCacheEntities.Where(c => ignoreCacheEntries ? !c.IsCacheEntry : true).ToList())
|
||||||
|
{
|
||||||
|
if (!validate) output.Add(fileCache);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var validated = GetValidatedFileCache(fileCache);
|
||||||
|
if (validated != null) output.Add(validated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<FileCacheEntity>> ValidateLocalIntegrity(IProgress<(int, int, FileCacheEntity)> progress, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
||||||
|
_logger.LogInformation("Validating local storage");
|
||||||
|
var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList();
|
||||||
|
List<FileCacheEntity> brokenEntities = [];
|
||||||
|
int i = 0;
|
||||||
|
foreach (var fileCache in cacheEntries)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
_logger.LogInformation("Validating {file}", fileCache.ResolvedFilepath);
|
||||||
|
|
||||||
|
progress.Report((i, cacheEntries.Count, fileCache));
|
||||||
|
i++;
|
||||||
|
if (!File.Exists(fileCache.ResolvedFilepath))
|
||||||
|
{
|
||||||
|
brokenEntities.Add(fileCache);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||||
|
if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Failed to validate {file}, got hash {hash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash);
|
||||||
|
brokenEntities.Add(fileCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Error during validation of {file}", fileCache.ResolvedFilepath);
|
||||||
|
brokenEntities.Add(fileCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var brokenEntity in brokenEntities)
|
||||||
|
{
|
||||||
|
RemoveHashedFile(brokenEntity.Hash, brokenEntity.PrefixedFilePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(brokenEntity.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not delete {file}", brokenEntity.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_mareMediator.Publish(new ResumeScanMessage(nameof(ValidateLocalIntegrity)));
|
||||||
|
return Task.FromResult(brokenEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCacheFilePath(string hash, string extension)
|
||||||
|
{
|
||||||
|
return Path.Combine(_configService.Current.CacheFolder, hash + "." + extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
|
||||||
|
{
|
||||||
|
var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath;
|
||||||
|
return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
|
||||||
|
(int)new FileInfo(fileCache).Length));
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileCacheEntity? GetFileCacheByHash(string hash)
|
||||||
|
{
|
||||||
|
if (_fileCaches.TryGetValue(hash, out var hashes))
|
||||||
|
{
|
||||||
|
var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix) ? 0 : 1).FirstOrDefault();
|
||||||
|
if (item != null) return GetValidatedFileCache(item);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileCacheEntity? GetFileCacheByPath(string path)
|
||||||
|
{
|
||||||
|
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant()
|
||||||
|
.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Found no entries for {path}", cleanedPath);
|
||||||
|
return CreateFileEntry(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var validatedCacheEntry = GetValidatedFileCache(entry);
|
||||||
|
|
||||||
|
return validatedCacheEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, FileCacheEntity?> GetFileCachesByPaths(string[] paths)
|
||||||
|
{
|
||||||
|
_getCachesByPathsSemaphore.Wait();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cleanedPaths = paths.Distinct(StringComparer.OrdinalIgnoreCase).ToDictionary(p => p,
|
||||||
|
p => p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace(_ipcManager.Penumbra.ModDirectory!, _ipcManager.Penumbra.ModDirectory!.EndsWith('\\') ? PenumbraPrefix + '\\' : PenumbraPrefix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace(_configService.Current.CacheFolder, _configService.Current.CacheFolder.EndsWith('\\') ? CachePrefix + '\\' : CachePrefix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
.Replace("\\\\", "\\", StringComparison.Ordinal),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
Dictionary<string, FileCacheEntity?> result = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var dict = _fileCaches.SelectMany(f => f.Value)
|
||||||
|
.ToDictionary(d => d.PrefixedFilePath, d => d, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var entry in cleanedPaths)
|
||||||
|
{
|
||||||
|
//_logger.LogDebug("Checking {path}", entry.Value);
|
||||||
|
|
||||||
|
if (dict.TryGetValue(entry.Value, out var entity))
|
||||||
|
{
|
||||||
|
var validatedCache = GetValidatedFileCache(entity);
|
||||||
|
result.Add(entry.Key, validatedCache);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!entry.Value.Contains(CachePrefix, StringComparison.Ordinal))
|
||||||
|
result.Add(entry.Key, CreateFileEntry(entry.Key));
|
||||||
|
else
|
||||||
|
result.Add(entry.Key, CreateCacheEntry(entry.Key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_getCachesByPathsSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
||||||
|
{
|
||||||
|
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||||
|
{
|
||||||
|
var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal));
|
||||||
|
_logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath);
|
||||||
|
|
||||||
|
if (caches?.Count == 0)
|
||||||
|
{
|
||||||
|
_fileCaches.Remove(hash, out var entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath);
|
||||||
|
var oldHash = fileCache.Hash;
|
||||||
|
var prefixedPath = fileCache.PrefixedFilePath;
|
||||||
|
if (computeProperties)
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(fileCache.ResolvedFilepath);
|
||||||
|
fileCache.Size = fi.Length;
|
||||||
|
fileCache.CompressedSize = null;
|
||||||
|
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||||
|
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
RemoveHashedFile(oldHash, prefixedPath);
|
||||||
|
AddHashedFile(fileCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
public (FileState State, FileCacheEntity FileCache) ValidateFileCacheEntity(FileCacheEntity fileCache)
|
||||||
|
{
|
||||||
|
fileCache = ReplacePathPrefixes(fileCache);
|
||||||
|
FileInfo fi = new(fileCache.ResolvedFilepath);
|
||||||
|
if (!fi.Exists)
|
||||||
|
{
|
||||||
|
return (FileState.RequireDeletion, fileCache);
|
||||||
|
}
|
||||||
|
if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return (FileState.RequireUpdate, fileCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (FileState.Valid, fileCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteOutFullCsv()
|
||||||
|
{
|
||||||
|
lock (_fileWriteLock)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
sb.AppendLine(entry.CsvEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(_csvPath))
|
||||||
|
{
|
||||||
|
File.Copy(_csvPath, CsvBakPath, overwrite: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(_csvPath, sb.ToString());
|
||||||
|
File.Delete(CsvBakPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
File.WriteAllText(CsvBakPath, sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||||
|
var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
|
||||||
|
File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
|
||||||
|
var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
|
||||||
|
newHashedEntity.SetResolvedFilePath(extensionPath);
|
||||||
|
AddHashedFile(newHashedEntity);
|
||||||
|
_logger.LogTrace("Migrated from {oldPath} to {newPath}", fileCache.ResolvedFilepath, newHashedEntity.ResolvedFilepath);
|
||||||
|
return newHashedEntity;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddHashedFile(fileCache);
|
||||||
|
_logger.LogWarning(ex, "Failed to migrate entity {entity}", fileCache.PrefixedFilePath);
|
||||||
|
return fileCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddHashedFile(FileCacheEntity fileCache)
|
||||||
|
{
|
||||||
|
if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null)
|
||||||
|
{
|
||||||
|
_fileCaches[fileCache.Hash] = entries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
|
||||||
|
entries.Add(fileCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
||||||
|
{
|
||||||
|
hash ??= Crypto.GetFileHash(fileInfo.FullName);
|
||||||
|
var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length);
|
||||||
|
entity = ReplacePathPrefixes(entity);
|
||||||
|
AddHashedFile(entity);
|
||||||
|
lock (_fileWriteLock)
|
||||||
|
{
|
||||||
|
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||||
|
}
|
||||||
|
var result = GetFileCacheByPath(fileInfo.FullName);
|
||||||
|
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
|
||||||
|
{
|
||||||
|
var resultingFileCache = ReplacePathPrefixes(fileCache);
|
||||||
|
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
|
||||||
|
resultingFileCache = Validate(resultingFileCache);
|
||||||
|
return resultingFileCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache)
|
||||||
|
{
|
||||||
|
if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(PenumbraPrefix, _ipcManager.Penumbra.ModDirectory, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
else if (fileCache.PrefixedFilePath.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(CachePrefix, _configService.Current.CacheFolder, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileCacheEntity? Validate(FileCacheEntity fileCache)
|
||||||
|
{
|
||||||
|
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||||
|
if (!file.Exists)
|
||||||
|
{
|
||||||
|
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
UpdateHashedFile(fileCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting FileCacheManager");
|
||||||
|
|
||||||
|
lock (_fileWriteLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Checking for {bakPath}", CsvBakPath);
|
||||||
|
|
||||||
|
if (File.Exists(CsvBakPath))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{bakPath} found, moving to {csvPath}", CsvBakPath, _csvPath);
|
||||||
|
|
||||||
|
File.Move(CsvBakPath, _csvPath, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to move BAK to ORG, deleting BAK");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(CsvBakPath))
|
||||||
|
File.Delete(CsvBakPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex1)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex1, "Could not delete bak file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(_csvPath))
|
||||||
|
{
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable || string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory))
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new NotificationMessage("Penumbra not connected",
|
||||||
|
"Could not load local file cache data. Penumbra is not connected or not properly set up. Please enable and/or configure Penumbra properly to use Mare. After, reload Mare in the Plugin installer.",
|
||||||
|
MareConfiguration.Models.NotificationType.Error));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("{csvPath} found, parsing", _csvPath);
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
string[] entries = [];
|
||||||
|
int attempts = 0;
|
||||||
|
while (!success && attempts < 10)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Attempting to read {csvPath}", _csvPath);
|
||||||
|
entries = File.ReadAllLines(_csvPath);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
|
||||||
|
Thread.Sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entries.Any())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
|
||||||
|
|
||||||
|
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hash = splittedEntry[0];
|
||||||
|
if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||||
|
var path = splittedEntry[1];
|
||||||
|
var time = splittedEntry[2];
|
||||||
|
|
||||||
|
if (processedFiles.ContainsKey(path))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedFiles.Add(path, value: true);
|
||||||
|
|
||||||
|
long size = -1;
|
||||||
|
long compressed = -1;
|
||||||
|
if (splittedEntry.Length > 3)
|
||||||
|
{
|
||||||
|
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||||
|
{
|
||||||
|
size = result;
|
||||||
|
}
|
||||||
|
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||||
|
{
|
||||||
|
compressed = resultCompressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedFiles.Count != entries.Length)
|
||||||
|
{
|
||||||
|
WriteOutFullCsv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Started FileCacheManager");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
WriteOutFullCsv();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
229
MareSynchronos/FileCache/FileCompactor.cs
Normal file
229
MareSynchronos/FileCache/FileCompactor.cs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MareSynchronos.FileCache;
|
||||||
|
|
||||||
|
public sealed class FileCompactor
|
||||||
|
{
|
||||||
|
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
|
||||||
|
public const ulong WOF_PROVIDER_FILE = 2UL;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, int> _clusterSizes;
|
||||||
|
|
||||||
|
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo;
|
||||||
|
private readonly ILogger<FileCompactor> _logger;
|
||||||
|
|
||||||
|
private readonly MareConfigService _mareConfigService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
|
||||||
|
public FileCompactor(ILogger<FileCompactor> logger, MareConfigService mareConfigService, DalamudUtilService dalamudUtilService)
|
||||||
|
{
|
||||||
|
_clusterSizes = new(StringComparer.Ordinal);
|
||||||
|
_logger = logger;
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_efInfo = new WOF_FILE_COMPRESSION_INFO_V1
|
||||||
|
{
|
||||||
|
Algorithm = CompressionAlgorithm.XPRESS8K,
|
||||||
|
Flags = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CompressionAlgorithm
|
||||||
|
{
|
||||||
|
NO_COMPRESSION = -2,
|
||||||
|
LZNT1 = -1,
|
||||||
|
XPRESS4K = 0,
|
||||||
|
LZX = 1,
|
||||||
|
XPRESS8K = 2,
|
||||||
|
XPRESS16K = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MassCompactRunning { get; private set; } = false;
|
||||||
|
|
||||||
|
public string Progress { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
public void CompactStorage(bool compress)
|
||||||
|
{
|
||||||
|
MassCompactRunning = true;
|
||||||
|
|
||||||
|
int currentFile = 1;
|
||||||
|
var allFiles = Directory.EnumerateFiles(_mareConfigService.Current.CacheFolder).ToList();
|
||||||
|
int allFilesCount = allFiles.Count;
|
||||||
|
foreach (var file in allFiles)
|
||||||
|
{
|
||||||
|
Progress = $"{currentFile}/{allFilesCount}";
|
||||||
|
if (compress)
|
||||||
|
CompactFile(file);
|
||||||
|
else
|
||||||
|
DecompressFile(file);
|
||||||
|
currentFile++;
|
||||||
|
}
|
||||||
|
|
||||||
|
MassCompactRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GetFileSizeOnDisk(FileInfo fileInfo, bool? isNTFS = null)
|
||||||
|
{
|
||||||
|
bool ntfs = isNTFS ?? string.Equals(new DriveInfo(fileInfo.Directory!.Root.FullName).DriveFormat, "NTFS", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (_dalamudUtilService.IsWine || !ntfs) return fileInfo.Length;
|
||||||
|
|
||||||
|
var clusterSize = GetClusterSize(fileInfo);
|
||||||
|
if (clusterSize == -1) return fileInfo.Length;
|
||||||
|
var losize = GetCompressedFileSizeW(fileInfo.FullName, out uint hosize);
|
||||||
|
var size = (long)hosize << 32 | losize;
|
||||||
|
return ((size + clusterSize - 1) / clusterSize) * clusterSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteAllBytesAsync(string filePath, byte[] decompressedFile, CancellationToken token)
|
||||||
|
{
|
||||||
|
await File.WriteAllBytesAsync(filePath, decompressedFile, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_dalamudUtilService.IsWine || !_mareConfigService.Current.UseCompactor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompactFile(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern int DeviceIoControl(IntPtr hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out IntPtr lpBytesReturned, out IntPtr lpOverlapped);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
|
||||||
|
[Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, PreserveSig = true)]
|
||||||
|
private static extern int GetDiskFreeSpaceW([In, MarshalAs(UnmanagedType.LPWStr)] string lpRootPathName,
|
||||||
|
out uint lpSectorsPerCluster, out uint lpBytesPerSector, out uint lpNumberOfFreeClusters,
|
||||||
|
out uint lpTotalNumberOfClusters);
|
||||||
|
|
||||||
|
[DllImport("WoFUtil.dll")]
|
||||||
|
private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WOF_FILE_COMPRESSION_INFO_V1 Info, ref uint BufferLength);
|
||||||
|
|
||||||
|
[DllImport("WofUtil.dll")]
|
||||||
|
private static extern int WofSetFileDataLocation(IntPtr FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length);
|
||||||
|
|
||||||
|
private void CompactFile(string filePath)
|
||||||
|
{
|
||||||
|
var fs = new DriveInfo(new FileInfo(filePath).Directory!.Root.FullName);
|
||||||
|
bool isNTFS = string.Equals(fs.DriveFormat, "NTFS", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!isNTFS)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Drive for file {file} is not NTFS", filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fi = new FileInfo(filePath);
|
||||||
|
var oldSize = fi.Length;
|
||||||
|
var clusterSize = GetClusterSize(fi);
|
||||||
|
|
||||||
|
if (oldSize < Math.Max(clusterSize, 8 * 1024))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("File {file} is smaller than cluster size ({size}), ignoring", filePath, clusterSize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsCompactedFile(filePath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Compacting file to XPRESS8K: {file}", filePath);
|
||||||
|
|
||||||
|
WOFCompressFile(filePath);
|
||||||
|
|
||||||
|
var newSize = GetFileSizeOnDisk(fi);
|
||||||
|
|
||||||
|
_logger.LogDebug("Compressed {file} from {orig}b to {comp}b", filePath, oldSize, newSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("File {file} already compressed", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DecompressFile(string path)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Removing compression from {file}", path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var fs = new FileStream(path, FileMode.Open))
|
||||||
|
{
|
||||||
|
#pragma warning disable S3869 // "SafeHandle.DangerousGetHandle" should not be called
|
||||||
|
var hDevice = fs.SafeFileHandle.DangerousGetHandle();
|
||||||
|
#pragma warning restore S3869 // "SafeHandle.DangerousGetHandle" should not be called
|
||||||
|
_ = DeviceIoControl(hDevice, FSCTL_DELETE_EXTERNAL_BACKING, nint.Zero, 0, nint.Zero, 0, out _, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error decompressing file {path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetClusterSize(FileInfo fi)
|
||||||
|
{
|
||||||
|
if (!fi.Exists) return -1;
|
||||||
|
var root = fi.Directory?.Root.FullName.ToLower() ?? string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(root)) return -1;
|
||||||
|
if (_clusterSizes.TryGetValue(root, out int value)) return value;
|
||||||
|
_logger.LogDebug("Getting Cluster Size for {path}, root {root}", fi.FullName, root);
|
||||||
|
int result = GetDiskFreeSpaceW(root, out uint sectorsPerCluster, out uint bytesPerSector, out _, out _);
|
||||||
|
if (result == 0) return -1;
|
||||||
|
_clusterSizes[root] = (int)(sectorsPerCluster * bytesPerSector);
|
||||||
|
_logger.LogDebug("Determined Cluster Size for root {root}: {cluster}", root, _clusterSizes[root]);
|
||||||
|
return _clusterSizes[root];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCompactedFile(string filePath)
|
||||||
|
{
|
||||||
|
uint buf = 8;
|
||||||
|
_ = WofIsExternalFile(filePath, out int isExtFile, out uint _, out var info, ref buf);
|
||||||
|
if (isExtFile == 0) return false;
|
||||||
|
return info.Algorithm == CompressionAlgorithm.XPRESS8K;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WOFCompressFile(string path)
|
||||||
|
{
|
||||||
|
var efInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_efInfo));
|
||||||
|
Marshal.StructureToPtr(_efInfo, efInfoPtr, fDeleteOld: true);
|
||||||
|
ulong length = (ulong)Marshal.SizeOf(_efInfo);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var fs = new FileStream(path, FileMode.Open))
|
||||||
|
{
|
||||||
|
#pragma warning disable S3869 // "SafeHandle.DangerousGetHandle" should not be called
|
||||||
|
var hFile = fs.SafeFileHandle.DangerousGetHandle();
|
||||||
|
#pragma warning restore S3869 // "SafeHandle.DangerousGetHandle" should not be called
|
||||||
|
if (fs.SafeFileHandle.IsInvalid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid file handle to {file}", path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ret = WofSetFileDataLocation(hFile, WOF_PROVIDER_FILE, efInfoPtr, length);
|
||||||
|
if (!(ret == 0 || ret == unchecked((int)0x80070158)))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to compact {file}: {ret}", path, ret.ToString("X"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error compacting file {path}", path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(efInfoPtr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct WOF_FILE_COMPRESSION_INFO_V1
|
||||||
|
{
|
||||||
|
public CompressionAlgorithm Algorithm;
|
||||||
|
public ulong Flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
MareSynchronos/FileCache/FileState.cs
Normal file
8
MareSynchronos/FileCache/FileState.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.FileCache;
|
||||||
|
|
||||||
|
public enum FileState
|
||||||
|
{
|
||||||
|
Valid,
|
||||||
|
RequireUpdate,
|
||||||
|
RequireDeletion,
|
||||||
|
}
|
||||||
470
MareSynchronos/FileCache/TransientResourceManager.cs
Normal file
470
MareSynchronos/FileCache/TransientResourceManager.cs
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
using MareSynchronos.PlayerData.Data;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.FileCache;
|
||||||
|
|
||||||
|
public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly object _cacheAdditionLock = new();
|
||||||
|
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
||||||
|
private readonly TransientConfigService _configurationService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"];
|
||||||
|
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||||
|
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||||
|
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
||||||
|
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
||||||
|
private uint _lastClassJobId = uint.MaxValue;
|
||||||
|
public bool IsTransientRecording { get; private set; } = false;
|
||||||
|
|
||||||
|
public TransientResourceManager(ILogger<TransientResourceManager> logger, TransientConfigService configurationService,
|
||||||
|
DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
|
||||||
|
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
||||||
|
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (_) => Manager_PenumbraModSettingChanged());
|
||||||
|
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate());
|
||||||
|
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (!msg.OwnedObject) return;
|
||||||
|
_playerRelatedPointers.Add(msg.GameObjectHandler);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (!msg.OwnedObject) return;
|
||||||
|
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransientConfig.TransientPlayerConfig PlayerConfig
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_configurationService.Current.TransientConfigs.TryGetValue(PlayerPersistentDataKey, out var transientConfig))
|
||||||
|
{
|
||||||
|
_configurationService.Current.TransientConfigs[PlayerPersistentDataKey] = transientConfig = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
return transientConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||||
|
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_semiTransientResources == null)
|
||||||
|
{
|
||||||
|
_semiTransientResources = new();
|
||||||
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||||
|
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.Ordinal);
|
||||||
|
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||||
|
_semiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
||||||
|
}
|
||||||
|
|
||||||
|
return _semiTransientResources;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private ConcurrentDictionary<ObjectKind, HashSet<string>> TransientResources { get; } = new();
|
||||||
|
|
||||||
|
public void CleanUpSemiTransientResources(ObjectKind objectKind, List<FileReplacement>? fileReplacement = null)
|
||||||
|
{
|
||||||
|
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (fileReplacement == null)
|
||||||
|
{
|
||||||
|
value.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int removedPaths = 0;
|
||||||
|
foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList())
|
||||||
|
{
|
||||||
|
removedPaths += PlayerConfig.RemovePath(replacement, objectKind);
|
||||||
|
value.Remove(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedPaths > 0)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Removed {amount} of SemiTransient paths during CleanUp, Saving from {name}", removedPaths, nameof(CleanUpSemiTransientResources));
|
||||||
|
// force reload semi transient resources
|
||||||
|
_configurationService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<string> GetSemiTransientResources(ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
SemiTransientResources.TryGetValue(objectKind, out var result);
|
||||||
|
|
||||||
|
return result ?? new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PersistTransientResources(ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? semiTransientResources))
|
||||||
|
{
|
||||||
|
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TransientResources.TryGetValue(objectKind, out var resources))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transientResources = resources.ToList();
|
||||||
|
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
|
||||||
|
List<string> newlyAddedGamePaths = resources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
||||||
|
foreach (var gamePath in transientResources)
|
||||||
|
{
|
||||||
|
semiTransientResources.Add(gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool saveConfig = false;
|
||||||
|
if (objectKind == ObjectKind.Player && newlyAddedGamePaths.Count != 0)
|
||||||
|
{
|
||||||
|
saveConfig = true;
|
||||||
|
foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f)))
|
||||||
|
{
|
||||||
|
PlayerConfig.AddOrElevate(_dalamudUtil.ClassJobId, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (objectKind == ObjectKind.Pet && newlyAddedGamePaths.Count != 0)
|
||||||
|
{
|
||||||
|
saveConfig = true;
|
||||||
|
|
||||||
|
if (!PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petPerma))
|
||||||
|
{
|
||||||
|
PlayerConfig.JobSpecificPetCache[_dalamudUtil.ClassJobId] = petPerma = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f)))
|
||||||
|
{
|
||||||
|
petPerma.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveConfig)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Saving transient.json from {method}", nameof(PersistTransientResources));
|
||||||
|
_configurationService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
TransientResources[objectKind].Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveTransientResource(ObjectKind objectKind, string path)
|
||||||
|
{
|
||||||
|
if (SemiTransientResources.TryGetValue(objectKind, out var resources))
|
||||||
|
{
|
||||||
|
resources.RemoveWhere(f => string.Equals(path, f, StringComparison.Ordinal));
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
PlayerConfig.RemovePath(path, objectKind);
|
||||||
|
Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource));
|
||||||
|
_configurationService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool AddTransientResource(ObjectKind objectKind, string item)
|
||||||
|
{
|
||||||
|
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource))
|
||||||
|
{
|
||||||
|
transientResource = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
TransientResources[objectKind] = transientResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transientResource.Add(item.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ClearTransientPaths(ObjectKind objectKind, List<string> list)
|
||||||
|
{
|
||||||
|
// ignore all recording only datatypes
|
||||||
|
int recordingOnlyRemoved = list.RemoveAll(entry => _handledRecordingFileTypes.Any(ext => entry.EndsWith(ext, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
if (recordingOnlyRemoved > 0)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Ignored {0} game paths when clearing transients", recordingOnlyRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TransientResources.TryGetValue(objectKind, out var set))
|
||||||
|
{
|
||||||
|
foreach (var file in set.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Removing From Transient: {file}", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
int removed = set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
|
||||||
|
Logger.LogDebug("Removed {removed} previously existing transient paths", removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool reloadSemiTransient = false;
|
||||||
|
if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(objectKind, out var semiset))
|
||||||
|
{
|
||||||
|
foreach (var file in semiset.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Removing From SemiTransient: {file}", file);
|
||||||
|
PlayerConfig.RemovePath(file, objectKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
int removed = semiset.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
|
||||||
|
Logger.LogDebug("Removed {removed} previously existing semi transient paths", removed);
|
||||||
|
if (removed > 0)
|
||||||
|
{
|
||||||
|
reloadSemiTransient = true;
|
||||||
|
Logger.LogTrace("Saving transient.json from {method}", nameof(ClearTransientPaths));
|
||||||
|
_configurationService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reloadSemiTransient)
|
||||||
|
_semiTransientResources = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
TransientResources.Clear();
|
||||||
|
SemiTransientResources.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DalamudUtil_FrameworkUpdate()
|
||||||
|
{
|
||||||
|
_cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.Address, c => c.ObjectKind));
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
||||||
|
{
|
||||||
|
_lastClassJobId = _dalamudUtil.ClassJobId;
|
||||||
|
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload config for current new classjob
|
||||||
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||||
|
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||||
|
SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kind in Enum.GetValues(typeof(ObjectKind)))
|
||||||
|
{
|
||||||
|
if (!_cachedFrameAddresses.Any(k => k.Value == (ObjectKind)kind) && TransientResources.Remove((ObjectKind)kind, out _))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Object not present anymore: {kind}", kind.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Manager_PenumbraModSettingChanged()
|
||||||
|
{
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
|
||||||
|
foreach (var item in _playerRelatedPointers)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RebuildSemiTransientResources()
|
||||||
|
{
|
||||||
|
_semiTransientResources = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||||
|
{
|
||||||
|
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||||
|
var gameObjectAddress = msg.GameObject;
|
||||||
|
var filePath = msg.FilePath;
|
||||||
|
|
||||||
|
// ignore files already processed this frame
|
||||||
|
if (_cachedHandledPaths.Contains(gamePath)) return;
|
||||||
|
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace individual mtrl stuff
|
||||||
|
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
filePath = filePath.Split("|")[2];
|
||||||
|
}
|
||||||
|
// replace filepath
|
||||||
|
filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// ignore files that are the same
|
||||||
|
var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore files to not handle
|
||||||
|
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
|
||||||
|
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore files not belonging to anything player related
|
||||||
|
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||||
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ^ all of the code above is just to sanitize the data
|
||||||
|
|
||||||
|
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResources))
|
||||||
|
{
|
||||||
|
transientResources = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
TransientResources[objectKind] = transientResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
var owner = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObjectAddress);
|
||||||
|
bool alreadyTransient = false;
|
||||||
|
|
||||||
|
bool transientContains = transientResources.Contains(replacedGamePath);
|
||||||
|
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (transientContains || semiTransientContains)
|
||||||
|
{
|
||||||
|
if (!IsTransientRecording)
|
||||||
|
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath,
|
||||||
|
transientContains, semiTransientContains);
|
||||||
|
alreadyTransient = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!IsTransientRecording)
|
||||||
|
{
|
||||||
|
bool isAdded = transientResources.Add(replacedGamePath);
|
||||||
|
if (isAdded)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||||
|
SendTransients(gameObjectAddress, objectKind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owner != null && IsTransientRecording)
|
||||||
|
{
|
||||||
|
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendTransients(nint gameObject, ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
_sendTransientCts?.Cancel();
|
||||||
|
_sendTransientCts?.Dispose();
|
||||||
|
_sendTransientCts = new();
|
||||||
|
var token = _sendTransientCts.Token;
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
||||||
|
foreach (var kvp in TransientResources)
|
||||||
|
{
|
||||||
|
if (TransientResources.TryGetValue(objectKind, out var values) && values.Any())
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Sending Transients for {kind}", objectKind);
|
||||||
|
Mediator.Publish(new TransientResourceChangedMessage(gameObject));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartRecording(CancellationToken token)
|
||||||
|
{
|
||||||
|
if (IsTransientRecording) return;
|
||||||
|
_recordedTransients.Clear();
|
||||||
|
IsTransientRecording = true;
|
||||||
|
RecordTimeRemaining.Value = TimeSpan.FromSeconds(150);
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (RecordTimeRemaining.Value > TimeSpan.Zero && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||||
|
RecordTimeRemaining.Value = RecordTimeRemaining.Value.Subtract(TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsTransientRecording = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WaitForRecording(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (IsTransientRecording)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SaveRecording()
|
||||||
|
{
|
||||||
|
HashSet<nint> addedTransients = [];
|
||||||
|
foreach (var item in _recordedTransients)
|
||||||
|
{
|
||||||
|
if (!item.AddTransient || item.AlreadyTransient) continue;
|
||||||
|
if (!TransientResources.TryGetValue(item.Owner.ObjectKind, out var transient))
|
||||||
|
{
|
||||||
|
TransientResources[item.Owner.ObjectKind] = transient = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Adding recorded: {gamePath} => {filePath}", item.GamePath, item.FilePath);
|
||||||
|
|
||||||
|
transient.Add(item.GamePath);
|
||||||
|
addedTransients.Add(item.Owner.Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
_recordedTransients.Clear();
|
||||||
|
|
||||||
|
foreach (var item in addedTransients)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new TransientResourceChangedMessage(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly HashSet<TransientRecord> _recordedTransients = [];
|
||||||
|
public IReadOnlySet<TransientRecord> RecordedTransients => _recordedTransients;
|
||||||
|
|
||||||
|
public ValueProgress<TimeSpan> RecordTimeRemaining { get; } = new();
|
||||||
|
private CancellationTokenSource _sendTransientCts = new();
|
||||||
|
|
||||||
|
public record TransientRecord(GameObjectHandler Owner, string GamePath, string FilePath, bool AlreadyTransient)
|
||||||
|
{
|
||||||
|
public bool AddTransient { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
8
MareSynchronos/GlobalSuppressions.cs
Normal file
8
MareSynchronos/GlobalSuppressions.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// This file is used by Code Analysis to maintain SuppressMessage
|
||||||
|
// attributes that are applied to this project.
|
||||||
|
// Project-level suppressions either have no target or are given
|
||||||
|
// a specific target and scoped to a namespace, type, member, etc.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
[assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>", Scope = "member", Target = "~M:MareSynchronos.Services.CharaDataManager.AttachPoseData(MareSynchronos.API.Dto.CharaData.PoseEntry,MareSynchronos.Services.CharaData.Models.CharaDataExtendedUpdateDto)")]
|
||||||
42
MareSynchronos/Interop/BlockedCharacterHandler.cs
Normal file
42
MareSynchronos/Interop/BlockedCharacterHandler.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop;
|
||||||
|
|
||||||
|
public unsafe class BlockedCharacterHandler
|
||||||
|
{
|
||||||
|
private sealed record CharaData(ulong AccId, ulong ContentId);
|
||||||
|
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
|
||||||
|
|
||||||
|
private readonly ILogger<BlockedCharacterHandler> _logger;
|
||||||
|
|
||||||
|
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider)
|
||||||
|
{
|
||||||
|
gameInteropProvider.InitializeFromAttributes(this);
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CharaData GetIdsFromPlayerPointer(nint ptr)
|
||||||
|
{
|
||||||
|
if (ptr == nint.Zero) return new(0, 0);
|
||||||
|
var castChar = ((BattleChara*)ptr);
|
||||||
|
return new(castChar->Character.AccountId, castChar->Character.ContentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCharacterBlocked(nint ptr, out bool firstTime)
|
||||||
|
{
|
||||||
|
firstTime = false;
|
||||||
|
var combined = GetIdsFromPlayerPointer(ptr);
|
||||||
|
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
|
||||||
|
return isBlocked;
|
||||||
|
|
||||||
|
firstTime = true;
|
||||||
|
var blockStatus = InfoProxyBlacklist.Instance()->GetBlockResultType(combined.AccId, combined.ContentId);
|
||||||
|
_logger.LogTrace("CharaPtr {ptr} is BlockStatus: {status}", ptr, blockStatus);
|
||||||
|
if ((int)blockStatus == 0)
|
||||||
|
return false;
|
||||||
|
return _blockedCharacterCache[combined] = blockStatus != InfoProxyBlacklist.BlockResultType.NotBlocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
MareSynchronos/Interop/DalamudLogger.cs
Normal file
59
MareSynchronos/Interop/DalamudLogger.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop;
|
||||||
|
|
||||||
|
internal sealed class DalamudLogger : ILogger
|
||||||
|
{
|
||||||
|
private readonly MareConfigService _mareConfigService;
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
private readonly bool _hasModifiedGameFiles;
|
||||||
|
|
||||||
|
public DalamudLogger(string name, MareConfigService mareConfigService, IPluginLog pluginLog, bool hasModifiedGameFiles)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
_hasModifiedGameFiles = hasModifiedGameFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable BeginScope<TState>(TState state) => default!;
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
|
{
|
||||||
|
return (int)_mareConfigService.Current.LogLevel <= (int)logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel)) return;
|
||||||
|
|
||||||
|
string unsupported = _hasModifiedGameFiles ? "[UNSUPPORTED]" : string.Empty;
|
||||||
|
|
||||||
|
if ((int)logLevel <= (int)LogLevel.Information)
|
||||||
|
_pluginLog.Information($"{unsupported}[{_name}]{{{(int)logLevel}}} {state}{(_hasModifiedGameFiles ? "." : string.Empty)}");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.Append($"{unsupported}[{_name}]{{{(int)logLevel}}} {state}{(_hasModifiedGameFiles ? "." : string.Empty)} {exception?.Message}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(exception?.StackTrace))
|
||||||
|
sb.AppendLine(exception?.StackTrace);
|
||||||
|
var innerException = exception?.InnerException;
|
||||||
|
while (innerException != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"InnerException {innerException}: {innerException.Message}");
|
||||||
|
sb.AppendLine(innerException.StackTrace);
|
||||||
|
innerException = innerException.InnerException;
|
||||||
|
}
|
||||||
|
if (logLevel == LogLevel.Warning)
|
||||||
|
_pluginLog.Warning(sb.ToString());
|
||||||
|
else if (logLevel == LogLevel.Error)
|
||||||
|
_pluginLog.Error(sb.ToString());
|
||||||
|
else
|
||||||
|
_pluginLog.Fatal(sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
MareSynchronos/Interop/DalamudLoggingProvider.cs
Normal file
46
MareSynchronos/Interop/DalamudLoggingProvider.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop;
|
||||||
|
|
||||||
|
[ProviderAlias("Dalamud")]
|
||||||
|
public sealed class DalamudLoggingProvider : ILoggerProvider
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private readonly MareConfigService _mareConfigService;
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
private readonly bool _hasModifiedGameFiles;
|
||||||
|
|
||||||
|
public DalamudLoggingProvider(MareConfigService mareConfigService, IPluginLog pluginLog, bool hasModifiedGameFiles)
|
||||||
|
{
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
_hasModifiedGameFiles = hasModifiedGameFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
{
|
||||||
|
string catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last();
|
||||||
|
if (catName.Length > 15)
|
||||||
|
{
|
||||||
|
catName = string.Join("", catName.Take(6)) + "..." + string.Join("", catName.TakeLast(6));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
catName = string.Join("", Enumerable.Range(0, 15 - catName.Length).Select(_ => " ")) + catName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _mareConfigService, _pluginLog, _hasModifiedGameFiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_loggers.Clear();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs
Normal file
19
MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop;
|
||||||
|
|
||||||
|
public static class DalamudLoggingProviderExtensions
|
||||||
|
{
|
||||||
|
public static ILoggingBuilder AddDalamudLogging(this ILoggingBuilder builder, IPluginLog pluginLog, bool hasModifiedGameFiles)
|
||||||
|
{
|
||||||
|
builder.ClearProviders();
|
||||||
|
|
||||||
|
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>
|
||||||
|
(b => new DalamudLoggingProvider(b.GetRequiredService<MareConfigService>(), pluginLog, hasModifiedGameFiles)));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
257
MareSynchronos/Interop/GameModel/MdlFile.cs
Normal file
257
MareSynchronos/Interop/GameModel/MdlFile.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using Lumina.Data;
|
||||||
|
using Lumina.Extensions;
|
||||||
|
using System.Text;
|
||||||
|
using static Lumina.Data.Parsing.MdlStructs;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.GameModel;
|
||||||
|
|
||||||
|
#pragma warning disable S1104 // Fields should not have public accessibility
|
||||||
|
|
||||||
|
// This code is completely and shamelessly borrowed from Penumbra to load V5 and V6 model files.
|
||||||
|
// Original Source: https://github.com/Ottermandias/Penumbra.GameData/blob/main/Files/MdlFile.cs
|
||||||
|
public class MdlFile
|
||||||
|
{
|
||||||
|
public const int V5 = 0x01000005;
|
||||||
|
public const int V6 = 0x01000006;
|
||||||
|
public const uint NumVertices = 17;
|
||||||
|
public const uint FileHeaderSize = 0x44;
|
||||||
|
|
||||||
|
// Raw data to write back.
|
||||||
|
public uint Version = 0x01000005;
|
||||||
|
public float Radius;
|
||||||
|
public float ModelClipOutDistance;
|
||||||
|
public float ShadowClipOutDistance;
|
||||||
|
public byte BgChangeMaterialIndex;
|
||||||
|
public byte BgCrestChangeMaterialIndex;
|
||||||
|
public ushort CullingGridCount;
|
||||||
|
public byte Flags3;
|
||||||
|
public byte Unknown6;
|
||||||
|
public ushort Unknown8;
|
||||||
|
public ushort Unknown9;
|
||||||
|
|
||||||
|
// Offsets are stored relative to RuntimeSize instead of file start.
|
||||||
|
public uint[] VertexOffset = [0, 0, 0];
|
||||||
|
public uint[] IndexOffset = [0, 0, 0];
|
||||||
|
|
||||||
|
public uint[] VertexBufferSize = [0, 0, 0];
|
||||||
|
public uint[] IndexBufferSize = [0, 0, 0];
|
||||||
|
public byte LodCount;
|
||||||
|
public bool EnableIndexBufferStreaming;
|
||||||
|
public bool EnableEdgeGeometry;
|
||||||
|
|
||||||
|
public ModelFlags1 Flags1;
|
||||||
|
public ModelFlags2 Flags2;
|
||||||
|
|
||||||
|
public VertexDeclarationStruct[] VertexDeclarations = [];
|
||||||
|
public ElementIdStruct[] ElementIds = [];
|
||||||
|
public MeshStruct[] Meshes = [];
|
||||||
|
public BoundingBoxStruct[] BoneBoundingBoxes = [];
|
||||||
|
public LodStruct[] Lods = [];
|
||||||
|
public ExtraLodStruct[] ExtraLods = [];
|
||||||
|
|
||||||
|
public MdlFile(string filePath)
|
||||||
|
{
|
||||||
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
|
using var r = new LuminaBinaryReader(stream);
|
||||||
|
|
||||||
|
var header = LoadModelFileHeader(r);
|
||||||
|
LodCount = header.LodCount;
|
||||||
|
VertexBufferSize = header.VertexBufferSize;
|
||||||
|
IndexBufferSize = header.IndexBufferSize;
|
||||||
|
VertexOffset = header.VertexOffset;
|
||||||
|
IndexOffset = header.IndexOffset;
|
||||||
|
|
||||||
|
var dataOffset = FileHeaderSize + header.RuntimeSize + header.StackSize;
|
||||||
|
for (var i = 0; i < LodCount; ++i)
|
||||||
|
{
|
||||||
|
VertexOffset[i] -= dataOffset;
|
||||||
|
IndexOffset[i] -= dataOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
VertexDeclarations = new VertexDeclarationStruct[header.VertexDeclarationCount];
|
||||||
|
for (var i = 0; i < header.VertexDeclarationCount; ++i)
|
||||||
|
VertexDeclarations[i] = VertexDeclarationStruct.Read(r);
|
||||||
|
|
||||||
|
_ = LoadStrings(r);
|
||||||
|
|
||||||
|
var modelHeader = LoadModelHeader(r);
|
||||||
|
ElementIds = new ElementIdStruct[modelHeader.ElementIdCount];
|
||||||
|
for (var i = 0; i < modelHeader.ElementIdCount; i++)
|
||||||
|
ElementIds[i] = ElementIdStruct.Read(r);
|
||||||
|
|
||||||
|
Lods = new LodStruct[3];
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var lod = r.ReadStructure<LodStruct>();
|
||||||
|
if (i < LodCount)
|
||||||
|
{
|
||||||
|
lod.VertexDataOffset -= dataOffset;
|
||||||
|
lod.IndexDataOffset -= dataOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lods[i] = lod;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtraLods = (modelHeader.Flags2 & ModelFlags2.ExtraLodEnabled) != 0
|
||||||
|
? r.ReadStructuresAsArray<ExtraLodStruct>(3)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
Meshes = new MeshStruct[modelHeader.MeshCount];
|
||||||
|
for (var i = 0; i < modelHeader.MeshCount; i++)
|
||||||
|
Meshes[i] = MeshStruct.Read(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r)
|
||||||
|
{
|
||||||
|
var header = ModelFileHeader.Read(r);
|
||||||
|
Version = header.Version;
|
||||||
|
EnableIndexBufferStreaming = header.EnableIndexBufferStreaming;
|
||||||
|
EnableEdgeGeometry = header.EnableEdgeGeometry;
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelHeader LoadModelHeader(BinaryReader r)
|
||||||
|
{
|
||||||
|
var modelHeader = r.ReadStructure<ModelHeader>();
|
||||||
|
Radius = modelHeader.Radius;
|
||||||
|
Flags1 = modelHeader.Flags1;
|
||||||
|
Flags2 = modelHeader.Flags2;
|
||||||
|
ModelClipOutDistance = modelHeader.ModelClipOutDistance;
|
||||||
|
ShadowClipOutDistance = modelHeader.ShadowClipOutDistance;
|
||||||
|
CullingGridCount = modelHeader.CullingGridCount;
|
||||||
|
Flags3 = modelHeader.Flags3;
|
||||||
|
Unknown6 = modelHeader.Unknown6;
|
||||||
|
Unknown8 = modelHeader.Unknown8;
|
||||||
|
Unknown9 = modelHeader.Unknown9;
|
||||||
|
BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex;
|
||||||
|
BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex;
|
||||||
|
|
||||||
|
return modelHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (uint[], string[]) LoadStrings(BinaryReader r)
|
||||||
|
{
|
||||||
|
var stringCount = r.ReadUInt16();
|
||||||
|
r.ReadUInt16();
|
||||||
|
var stringSize = (int)r.ReadUInt32();
|
||||||
|
var stringData = r.ReadBytes(stringSize);
|
||||||
|
var start = 0;
|
||||||
|
var strings = new string[stringCount];
|
||||||
|
var offsets = new uint[stringCount];
|
||||||
|
for (var i = 0; i < stringCount; ++i)
|
||||||
|
{
|
||||||
|
var span = stringData.AsSpan(start);
|
||||||
|
var idx = span.IndexOf((byte)'\0');
|
||||||
|
strings[i] = Encoding.UTF8.GetString(span[..idx]);
|
||||||
|
offsets[i] = (uint)start;
|
||||||
|
start = start + idx + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (offsets, strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe struct ModelHeader
|
||||||
|
{
|
||||||
|
// MeshHeader
|
||||||
|
public float Radius;
|
||||||
|
public ushort MeshCount;
|
||||||
|
public ushort AttributeCount;
|
||||||
|
public ushort SubmeshCount;
|
||||||
|
public ushort MaterialCount;
|
||||||
|
public ushort BoneCount;
|
||||||
|
public ushort BoneTableCount;
|
||||||
|
public ushort ShapeCount;
|
||||||
|
public ushort ShapeMeshCount;
|
||||||
|
public ushort ShapeValueCount;
|
||||||
|
public byte LodCount;
|
||||||
|
public ModelFlags1 Flags1;
|
||||||
|
public ushort ElementIdCount;
|
||||||
|
public byte TerrainShadowMeshCount;
|
||||||
|
public ModelFlags2 Flags2;
|
||||||
|
public float ModelClipOutDistance;
|
||||||
|
public float ShadowClipOutDistance;
|
||||||
|
public ushort CullingGridCount;
|
||||||
|
public ushort TerrainShadowSubmeshCount;
|
||||||
|
public byte Flags3;
|
||||||
|
public byte BGChangeMaterialIndex;
|
||||||
|
public byte BGCrestChangeMaterialIndex;
|
||||||
|
public byte Unknown6;
|
||||||
|
public ushort BoneTableArrayCountTotal;
|
||||||
|
public ushort Unknown8;
|
||||||
|
public ushort Unknown9;
|
||||||
|
private fixed byte _padding[6];
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ShapeStruct
|
||||||
|
{
|
||||||
|
public uint StringOffset;
|
||||||
|
public ushort[] ShapeMeshStartIndex;
|
||||||
|
public ushort[] ShapeMeshCount;
|
||||||
|
|
||||||
|
public static ShapeStruct Read(LuminaBinaryReader br)
|
||||||
|
{
|
||||||
|
ShapeStruct ret = new ShapeStruct();
|
||||||
|
ret.StringOffset = br.ReadUInt32();
|
||||||
|
ret.ShapeMeshStartIndex = br.ReadUInt16Array(3);
|
||||||
|
ret.ShapeMeshCount = br.ReadUInt16Array(3);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum ModelFlags1 : byte
|
||||||
|
{
|
||||||
|
DustOcclusionEnabled = 0x80,
|
||||||
|
SnowOcclusionEnabled = 0x40,
|
||||||
|
RainOcclusionEnabled = 0x20,
|
||||||
|
Unknown1 = 0x10,
|
||||||
|
LightingReflectionEnabled = 0x08,
|
||||||
|
WavingAnimationDisabled = 0x04,
|
||||||
|
LightShadowDisabled = 0x02,
|
||||||
|
ShadowDisabled = 0x01,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum ModelFlags2 : byte
|
||||||
|
{
|
||||||
|
Unknown2 = 0x80,
|
||||||
|
BgUvScrollEnabled = 0x40,
|
||||||
|
EnableForceNonResident = 0x20,
|
||||||
|
ExtraLodEnabled = 0x10,
|
||||||
|
ShadowMaskEnabled = 0x08,
|
||||||
|
ForceLodRangeEnabled = 0x04,
|
||||||
|
EdgeGeometryEnabled = 0x02,
|
||||||
|
Unknown3 = 0x01
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct VertexDeclarationStruct
|
||||||
|
{
|
||||||
|
// There are always 17, but stop when stream = -1
|
||||||
|
public VertexElement[] VertexElements;
|
||||||
|
|
||||||
|
public static VertexDeclarationStruct Read(LuminaBinaryReader br)
|
||||||
|
{
|
||||||
|
VertexDeclarationStruct ret = new VertexDeclarationStruct();
|
||||||
|
|
||||||
|
var elems = new List<VertexElement>();
|
||||||
|
|
||||||
|
// Read the vertex elements that we need
|
||||||
|
var thisElem = br.ReadStructure<VertexElement>();
|
||||||
|
do
|
||||||
|
{
|
||||||
|
elems.Add(thisElem);
|
||||||
|
thisElem = br.ReadStructure<VertexElement>();
|
||||||
|
} while (thisElem.Stream != 255);
|
||||||
|
|
||||||
|
// Skip the number of bytes that we don't need to read
|
||||||
|
// We skip elems.Count * 9 because we had to read the invalid element
|
||||||
|
int toSeek = 17 * 8 - (elems.Count + 1) * 8;
|
||||||
|
br.Seek(br.BaseStream.Position + toSeek);
|
||||||
|
|
||||||
|
ret.VertexElements = elems.ToArray();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#pragma warning restore S1104 // Fields should not have public accessibility
|
||||||
7
MareSynchronos/Interop/Ipc/IIpcCaller.cs
Normal file
7
MareSynchronos/Interop/Ipc/IIpcCaller.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public interface IIpcCaller : IDisposable
|
||||||
|
{
|
||||||
|
bool APIAvailable { get; }
|
||||||
|
void CheckAPI();
|
||||||
|
}
|
||||||
146
MareSynchronos/Interop/Ipc/IpcCallerBrio.cs
Normal file
146
MareSynchronos/Interop/Ipc/IpcCallerBrio.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerBrio : IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly ILogger<IpcCallerBrio> _logger;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly ICallGateSubscriber<(int, int)> _brioApiVersion;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<bool, bool, bool, Task<IGameObject>> _brioSpawnActorAsync;
|
||||||
|
private readonly ICallGateSubscriber<IGameObject, bool> _brioDespawnActor;
|
||||||
|
private readonly ICallGateSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool> _brioSetModelTransform;
|
||||||
|
private readonly ICallGateSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)> _brioGetModelTransform;
|
||||||
|
private readonly ICallGateSubscriber<IGameObject, string> _brioGetPoseAsJson;
|
||||||
|
private readonly ICallGateSubscriber<IGameObject, string, bool, bool> _brioSetPoseFromJson;
|
||||||
|
private readonly ICallGateSubscriber<IGameObject, bool> _brioFreezeActor;
|
||||||
|
private readonly ICallGateSubscriber<bool> _brioFreezePhysics;
|
||||||
|
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; }
|
||||||
|
|
||||||
|
public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||||
|
DalamudUtilService dalamudUtilService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
|
||||||
|
_brioApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("Brio.ApiVersion");
|
||||||
|
_brioSpawnActorAsync = dalamudPluginInterface.GetIpcSubscriber<bool, bool, bool, Task<IGameObject>>("Brio.Actor.SpawnExAsync");
|
||||||
|
_brioDespawnActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Despawn");
|
||||||
|
_brioSetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool>("Brio.Actor.SetModelTransform");
|
||||||
|
_brioGetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)>("Brio.Actor.GetModelTransform");
|
||||||
|
_brioGetPoseAsJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string>("Brio.Actor.Pose.GetPoseAsJson");
|
||||||
|
_brioSetPoseFromJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string, bool, bool>("Brio.Actor.Pose.LoadFromJson");
|
||||||
|
_brioFreezeActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Freeze");
|
||||||
|
_brioFreezePhysics = dalamudPluginInterface.GetIpcSubscriber<bool>("Brio.FreezePhysics");
|
||||||
|
|
||||||
|
CheckAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var version = _brioApiVersion.InvokeFunc();
|
||||||
|
APIAvailable = (version.Item1 == 2 && version.Item2 >= 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IGameObject?> SpawnActorAsync()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return null;
|
||||||
|
_logger.LogDebug("Spawning Brio Actor");
|
||||||
|
return await _brioSpawnActorAsync.InvokeFunc(false, false, true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DespawnActorAsync(nint address)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||||
|
if (gameObject == null) return false;
|
||||||
|
_logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
|
||||||
|
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioDespawnActor.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ApplyTransformAsync(nint address, WorldData data)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||||
|
if (gameObject == null) return false;
|
||||||
|
_logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);
|
||||||
|
|
||||||
|
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetModelTransform.InvokeFunc(gameObject,
|
||||||
|
new Vector3(data.PositionX, data.PositionY, data.PositionZ),
|
||||||
|
new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
|
||||||
|
new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorldData> GetTransformAsync(nint address)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return default;
|
||||||
|
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||||
|
if (gameObject == null) return default;
|
||||||
|
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||||
|
//_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
|
||||||
|
|
||||||
|
return new WorldData()
|
||||||
|
{
|
||||||
|
PositionX = data.Item1.Value.X,
|
||||||
|
PositionY = data.Item1.Value.Y,
|
||||||
|
PositionZ = data.Item1.Value.Z,
|
||||||
|
RotationX = data.Item2.Value.X,
|
||||||
|
RotationY = data.Item2.Value.Y,
|
||||||
|
RotationZ = data.Item2.Value.Z,
|
||||||
|
RotationW = data.Item2.Value.W,
|
||||||
|
ScaleX = data.Item3.Value.X,
|
||||||
|
ScaleY = data.Item3.Value.Y,
|
||||||
|
ScaleZ = data.Item3.Value.Z
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetPoseAsync(nint address)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return null;
|
||||||
|
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||||
|
if (gameObject == null) return null;
|
||||||
|
_logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue);
|
||||||
|
|
||||||
|
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetPoseAsync(nint address, string pose)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||||
|
if (gameObject == null) return false;
|
||||||
|
_logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);
|
||||||
|
|
||||||
|
var applicablePose = JsonNode.Parse(pose)!;
|
||||||
|
var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||||
|
applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString());
|
||||||
|
|
||||||
|
await _dalamudUtilService.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
_brioFreezeActor.InvokeFunc(gameObject);
|
||||||
|
_brioFreezePhysics.InvokeFunc();
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
139
MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs
Normal file
139
MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerCustomize : IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion;
|
||||||
|
private readonly ICallGateSubscriber<ushort, (int, Guid?)> _customizePlusGetActiveProfile;
|
||||||
|
private readonly ICallGateSubscriber<Guid, (int, string?)> _customizePlusGetProfileById;
|
||||||
|
private readonly ICallGateSubscriber<ushort, Guid, object> _customizePlusOnScaleUpdate;
|
||||||
|
private readonly ICallGateSubscriber<ushort, int> _customizePlusRevertCharacter;
|
||||||
|
private readonly ICallGateSubscriber<ushort, string, (int, Guid?)> _customizePlusSetBodyScaleToCharacter;
|
||||||
|
private readonly ICallGateSubscriber<Guid, int> _customizePlusDeleteByUniqueId;
|
||||||
|
private readonly ILogger<IpcCallerCustomize> _logger;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
|
||||||
|
public IpcCallerCustomize(ILogger<IpcCallerCustomize> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||||
|
DalamudUtilService dalamudUtil, MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion");
|
||||||
|
_customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber<ushort, (int, Guid?)>("CustomizePlus.Profile.GetActiveProfileIdOnCharacter");
|
||||||
|
_customizePlusGetProfileById = dalamudPluginInterface.GetIpcSubscriber<Guid, (int, string?)>("CustomizePlus.Profile.GetByUniqueId");
|
||||||
|
_customizePlusRevertCharacter = dalamudPluginInterface.GetIpcSubscriber<ushort, int>("CustomizePlus.Profile.DeleteTemporaryProfileOnCharacter");
|
||||||
|
_customizePlusSetBodyScaleToCharacter = dalamudPluginInterface.GetIpcSubscriber<ushort, string, (int, Guid?)>("CustomizePlus.Profile.SetTemporaryProfileOnCharacter");
|
||||||
|
_customizePlusOnScaleUpdate = dalamudPluginInterface.GetIpcSubscriber<ushort, Guid, object>("CustomizePlus.Profile.OnUpdate");
|
||||||
|
_customizePlusDeleteByUniqueId = dalamudPluginInterface.GetIpcSubscriber<Guid, int>("CustomizePlus.Profile.DeleteTemporaryProfileByUniqueId");
|
||||||
|
|
||||||
|
_customizePlusOnScaleUpdate.Subscribe(OnCustomizePlusScaleChange);
|
||||||
|
_logger = logger;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
|
||||||
|
CheckAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; } = false;
|
||||||
|
|
||||||
|
public async Task RevertAsync(nint character)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj is ICharacter c)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("CustomizePlus reverting for {chara}", c.Address.ToString("X"));
|
||||||
|
_customizePlusRevertCharacter!.InvokeFunc(c.ObjectIndex);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid?> SetBodyScaleAsync(nint character, string scale)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return null;
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj is ICharacter c)
|
||||||
|
{
|
||||||
|
string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale));
|
||||||
|
_logger.LogTrace("CustomizePlus applying for {chara}", c.Address.ToString("X"));
|
||||||
|
if (scale.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
_customizePlusRevertCharacter!.InvokeFunc(c.ObjectIndex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var result = _customizePlusSetBodyScaleToCharacter!.InvokeFunc(c.ObjectIndex, decodedScale);
|
||||||
|
return result.Item2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevertByIdAsync(Guid? profileId)
|
||||||
|
{
|
||||||
|
if (!APIAvailable || profileId == null) return;
|
||||||
|
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
_ = _customizePlusDeleteByUniqueId.InvokeFunc(profileId.Value);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetScaleAsync(nint character)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return null;
|
||||||
|
var scale = await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj is ICharacter c)
|
||||||
|
{
|
||||||
|
var res = _customizePlusGetActiveProfile.InvokeFunc(c.ObjectIndex);
|
||||||
|
_logger.LogTrace("CustomizePlus GetActiveProfile returned {err}", res.Item1);
|
||||||
|
if (res.Item1 != 0 || res.Item2 == null) return string.Empty;
|
||||||
|
return _customizePlusGetProfileById.InvokeFunc(res.Item2.Value).Item2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(scale)) return string.Empty;
|
||||||
|
return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var version = _customizePlusApiVersion.InvokeFunc();
|
||||||
|
APIAvailable = (version.Item1 == 6 && version.Item2 >= 0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCustomizePlusScaleChange(ushort c, Guid g)
|
||||||
|
{
|
||||||
|
var obj = _dalamudUtil.GetCharacterFromObjectTableByIndex(c);
|
||||||
|
_mareMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
217
MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs
Normal file
217
MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Glamourer.Api.Helpers;
|
||||||
|
using Glamourer.Api.IpcSubscribers;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly ILogger<IpcCallerGlamourer> _logger;
|
||||||
|
private readonly IDalamudPluginInterface _pi;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly RedrawManager _redrawManager;
|
||||||
|
|
||||||
|
private readonly ApiVersion _glamourerApiVersions;
|
||||||
|
private readonly ApplyState? _glamourerApplyAll;
|
||||||
|
private readonly GetStateBase64? _glamourerGetAllCustomization;
|
||||||
|
private readonly RevertState _glamourerRevert;
|
||||||
|
private readonly RevertStateName _glamourerRevertByName;
|
||||||
|
private readonly UnlockState _glamourerUnlock;
|
||||||
|
private readonly UnlockStateName _glamourerUnlockByName;
|
||||||
|
private readonly EventSubscriber<nint>? _glamourerStateChanged;
|
||||||
|
|
||||||
|
private bool _shownGlamourerUnavailable = false;
|
||||||
|
private readonly uint LockCode = 0x6D617265;
|
||||||
|
|
||||||
|
public IpcCallerGlamourer(ILogger<IpcCallerGlamourer> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator,
|
||||||
|
RedrawManager redrawManager) : base(logger, mareMediator)
|
||||||
|
{
|
||||||
|
_glamourerApiVersions = new ApiVersion(pi);
|
||||||
|
_glamourerGetAllCustomization = new GetStateBase64(pi);
|
||||||
|
_glamourerApplyAll = new ApplyState(pi);
|
||||||
|
_glamourerRevert = new RevertState(pi);
|
||||||
|
_glamourerRevertByName = new RevertStateName(pi);
|
||||||
|
_glamourerUnlock = new UnlockState(pi);
|
||||||
|
_glamourerUnlockByName = new UnlockStateName(pi);
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
_pi = pi;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_redrawManager = redrawManager;
|
||||||
|
CheckAPI();
|
||||||
|
|
||||||
|
_glamourerStateChanged = StateChanged.Subscriber(pi, GlamourerChanged);
|
||||||
|
_glamourerStateChanged.Enable();
|
||||||
|
|
||||||
|
Mediator.Subscribe<DalamudLoginMessage>(this, s => _shownGlamourerUnavailable = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
_redrawManager.Cancel();
|
||||||
|
_glamourerStateChanged?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; }
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
bool apiAvailable = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool versionValid = (_pi.InstalledPlugins
|
||||||
|
.FirstOrDefault(p => string.Equals(p.InternalName, "Glamourer", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?.Version ?? new Version(0, 0, 0, 0)) >= new Version(1, 3, 0, 10);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var version = _glamourerApiVersions.Invoke();
|
||||||
|
if (version is { Major: 1, Minor: >= 1 } && versionValid)
|
||||||
|
{
|
||||||
|
apiAvailable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
_shownGlamourerUnavailable = _shownGlamourerUnavailable && !apiAvailable;
|
||||||
|
|
||||||
|
APIAvailable = apiAvailable;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = apiAvailable;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!apiAvailable && !_shownGlamourerUnavailable)
|
||||||
|
{
|
||||||
|
_shownGlamourerUnavailable = true;
|
||||||
|
_mareMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Mare. If you just updated Glamourer, ignore this message.",
|
||||||
|
NotificationType.Error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false)
|
||||||
|
{
|
||||||
|
if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return;
|
||||||
|
|
||||||
|
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogDebug("[{appid}] Calling on IPC: GlamourerApplyAll", applicationId);
|
||||||
|
_glamourerApplyAll!.Invoke(customization, chara.ObjectIndex, LockCode);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "[{appid}] Failed to apply Glamourer data", applicationId);
|
||||||
|
}
|
||||||
|
}, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_redrawManager.RedrawSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetCharacterCustomizationAsync(IntPtr character)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj is ICharacter c)
|
||||||
|
{
|
||||||
|
return _glamourerGetAllCustomization!.Invoke(c.ObjectIndex).Item2 ?? string.Empty;
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevertAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||||
|
{
|
||||||
|
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId);
|
||||||
|
_glamourerUnlock.Invoke(chara.ObjectIndex, LockCode);
|
||||||
|
logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevert", applicationId);
|
||||||
|
_glamourerRevert.Invoke(chara.ObjectIndex, LockCode);
|
||||||
|
logger.LogDebug("[{appid}] Calling On IPC: PenumbraRedraw", applicationId);
|
||||||
|
|
||||||
|
_mareMediator.Publish(new PenumbraRedrawCharacterMessage(chara));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "[{appid}] Error during GlamourerRevert", applicationId);
|
||||||
|
}
|
||||||
|
}, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_redrawManager.RedrawSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevertByNameAsync(ILogger logger, string name, Guid applicationId)
|
||||||
|
{
|
||||||
|
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||||
|
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
RevertByName(logger, name, applicationId);
|
||||||
|
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RevertByName(ILogger logger, string name, Guid applicationId)
|
||||||
|
{
|
||||||
|
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevertByName", applicationId);
|
||||||
|
_glamourerRevertByName.Invoke(name, LockCode);
|
||||||
|
logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId);
|
||||||
|
_glamourerUnlockByName.Invoke(name, LockCode);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error during Glamourer RevertByName");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GlamourerChanged(nint address)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new GlamourerChangedMessage(address));
|
||||||
|
}
|
||||||
|
}
|
||||||
93
MareSynchronos/Interop/Ipc/IpcCallerHeels.cs
Normal file
93
MareSynchronos/Interop/Ipc/IpcCallerHeels.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerHeels : IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly ILogger<IpcCallerHeels> _logger;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly ICallGateSubscriber<(int, int)> _heelsGetApiVersion;
|
||||||
|
private readonly ICallGateSubscriber<string> _heelsGetOffset;
|
||||||
|
private readonly ICallGateSubscriber<string, object?> _heelsOffsetUpdate;
|
||||||
|
private readonly ICallGateSubscriber<int, string, object?> _heelsRegisterPlayer;
|
||||||
|
private readonly ICallGateSubscriber<int, object?> _heelsUnregisterPlayer;
|
||||||
|
|
||||||
|
public IpcCallerHeels(ILogger<IpcCallerHeels> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_heelsGetApiVersion = pi.GetIpcSubscriber<(int, int)>("SimpleHeels.ApiVersion");
|
||||||
|
_heelsGetOffset = pi.GetIpcSubscriber<string>("SimpleHeels.GetLocalPlayer");
|
||||||
|
_heelsRegisterPlayer = pi.GetIpcSubscriber<int, string, object?>("SimpleHeels.RegisterPlayer");
|
||||||
|
_heelsUnregisterPlayer = pi.GetIpcSubscriber<int, object?>("SimpleHeels.UnregisterPlayer");
|
||||||
|
_heelsOffsetUpdate = pi.GetIpcSubscriber<string, object?>("SimpleHeels.LocalChanged");
|
||||||
|
|
||||||
|
_heelsOffsetUpdate.Subscribe(HeelsOffsetChange);
|
||||||
|
|
||||||
|
CheckAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; } = false;
|
||||||
|
|
||||||
|
private void HeelsOffsetChange(string offset)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new HeelsOffsetMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetOffsetAsync()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return string.Empty;
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(_heelsGetOffset.InvokeFunc).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RestoreOffsetForPlayerAsync(IntPtr character)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj != null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Restoring Heels data to {chara}", character.ToString("X"));
|
||||||
|
_heelsUnregisterPlayer.InvokeAction(gameObj.ObjectIndex);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetOffsetForPlayerAsync(IntPtr character, string data)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj != null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Applying Heels data to {chara}", character.ToString("X"));
|
||||||
|
_heelsRegisterPlayer.InvokeAction(gameObj.ObjectIndex, data);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs
Normal file
132
MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerHonorific : IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion;
|
||||||
|
private readonly ICallGateSubscriber<int, object> _honorificClearCharacterTitle;
|
||||||
|
private readonly ICallGateSubscriber<object> _honorificDisposing;
|
||||||
|
private readonly ICallGateSubscriber<string> _honorificGetLocalCharacterTitle;
|
||||||
|
private readonly ICallGateSubscriber<string, object> _honorificLocalCharacterTitleChanged;
|
||||||
|
private readonly ICallGateSubscriber<object> _honorificReady;
|
||||||
|
private readonly ICallGateSubscriber<int, string, object> _honorificSetCharacterTitle;
|
||||||
|
private readonly ILogger<IpcCallerHonorific> _logger;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
|
||||||
|
public IpcCallerHonorific(ILogger<IpcCallerHonorific> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||||
|
MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_honorificApiVersion = pi.GetIpcSubscriber<(uint, uint)>("Honorific.ApiVersion");
|
||||||
|
_honorificGetLocalCharacterTitle = pi.GetIpcSubscriber<string>("Honorific.GetLocalCharacterTitle");
|
||||||
|
_honorificClearCharacterTitle = pi.GetIpcSubscriber<int, object>("Honorific.ClearCharacterTitle");
|
||||||
|
_honorificSetCharacterTitle = pi.GetIpcSubscriber<int, string, object>("Honorific.SetCharacterTitle");
|
||||||
|
_honorificLocalCharacterTitleChanged = pi.GetIpcSubscriber<string, object>("Honorific.LocalCharacterTitleChanged");
|
||||||
|
_honorificDisposing = pi.GetIpcSubscriber<object>("Honorific.Disposing");
|
||||||
|
_honorificReady = pi.GetIpcSubscriber<object>("Honorific.Ready");
|
||||||
|
|
||||||
|
_honorificLocalCharacterTitleChanged.Subscribe(OnHonorificLocalCharacterTitleChanged);
|
||||||
|
_honorificDisposing.Subscribe(OnHonorificDisposing);
|
||||||
|
_honorificReady.Subscribe(OnHonorificReady);
|
||||||
|
|
||||||
|
CheckAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; } = false;
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged);
|
||||||
|
_honorificDisposing.Unsubscribe(OnHonorificDisposing);
|
||||||
|
_honorificReady.Unsubscribe(OnHonorificReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearTitleAsync(nint character)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj is IPlayerCharacter c)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Honorific removing for {addr}", c.Address.ToString("X"));
|
||||||
|
_honorificClearCharacterTitle!.InvokeAction(c.ObjectIndex);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetTitle()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return string.Empty;
|
||||||
|
string title = await _dalamudUtil.RunOnFrameworkThread(() => _honorificGetLocalCharacterTitle.InvokeFunc()).ConfigureAwait(false);
|
||||||
|
return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetTitleAsync(IntPtr character, string honorificDataB64)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
_logger.LogTrace("Applying Honorific data to {chara}", character.ToString("X"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj is IPlayerCharacter pc)
|
||||||
|
{
|
||||||
|
string honorificData = string.IsNullOrEmpty(honorificDataB64) ? string.Empty : Encoding.UTF8.GetString(Convert.FromBase64String(honorificDataB64));
|
||||||
|
if (string.IsNullOrEmpty(honorificData))
|
||||||
|
{
|
||||||
|
_honorificClearCharacterTitle!.InvokeAction(pc.ObjectIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_honorificSetCharacterTitle!.InvokeAction(pc.ObjectIndex, honorificData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Could not apply Honorific data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHonorificDisposing()
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new HonorificMessage(string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHonorificLocalCharacterTitleChanged(string titleJson)
|
||||||
|
{
|
||||||
|
string titleData = string.IsNullOrEmpty(titleJson) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(titleJson));
|
||||||
|
_mareMediator.Publish(new HonorificMessage(titleData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHonorificReady()
|
||||||
|
{
|
||||||
|
CheckAPI();
|
||||||
|
_mareMediator.Publish(new HonorificReadyMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
104
MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs
Normal file
104
MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerMoodles : IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
||||||
|
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||||
|
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||||
|
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
|
||||||
|
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
|
||||||
|
private readonly ILogger<IpcCallerMoodles> _logger;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
|
||||||
|
public IpcCallerMoodles(ILogger<IpcCallerMoodles> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||||
|
MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
|
||||||
|
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
|
||||||
|
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
|
||||||
|
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtr");
|
||||||
|
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtr");
|
||||||
|
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtr");
|
||||||
|
|
||||||
|
_moodlesOnChange.Subscribe(OnMoodlesChange);
|
||||||
|
|
||||||
|
CheckAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMoodlesChange(IPlayerCharacter character)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new MoodlesMessage(character.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; } = false;
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
APIAvailable = _moodlesApiVersion.InvokeFunc() == 1;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_moodlesOnChange.Unsubscribe(OnMoodlesChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetStatusAsync(nint address)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(() => _moodlesGetStatus.InvokeFunc(address)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Could not Get Moodles Status");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetStatusAsync(nint pointer, string status)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() => _moodlesSetStatus.InvokeAction(pointer, status)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Could not Set Moodles Status");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevertStatusAsync(nint pointer)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() => _moodlesRevertStatus.InvokeAction(pointer)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Could not Set Moodles Status");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
343
MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs
Normal file
343
MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
using Dalamud.Plugin;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.Api.Helpers;
|
||||||
|
using Penumbra.Api.IpcSubscribers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly IDalamudPluginInterface _pi;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly RedrawManager _redrawManager;
|
||||||
|
private bool _shownPenumbraUnavailable = false;
|
||||||
|
private string? _penumbraModDirectory;
|
||||||
|
public string? ModDirectory
|
||||||
|
{
|
||||||
|
get => _penumbraModDirectory;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_penumbraModDirectory = value;
|
||||||
|
_mareMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<IntPtr, bool> _penumbraRedrawRequests = new();
|
||||||
|
|
||||||
|
private readonly EventSubscriber _penumbraDispose;
|
||||||
|
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
|
||||||
|
private readonly EventSubscriber _penumbraInit;
|
||||||
|
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||||
|
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
|
||||||
|
|
||||||
|
private readonly AddTemporaryMod _penumbraAddTemporaryMod;
|
||||||
|
private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection;
|
||||||
|
private readonly ConvertTextureFile _penumbraConvertTextureFile;
|
||||||
|
private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection;
|
||||||
|
private readonly GetEnabledState _penumbraEnabled;
|
||||||
|
private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations;
|
||||||
|
private readonly RedrawObject _penumbraRedraw;
|
||||||
|
private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection;
|
||||||
|
private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod;
|
||||||
|
private readonly GetModDirectory _penumbraResolveModDir;
|
||||||
|
private readonly ResolvePlayerPathsAsync _penumbraResolvePaths;
|
||||||
|
private readonly GetGameObjectResourcePaths _penumbraResourcePaths;
|
||||||
|
|
||||||
|
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||||
|
MareMediator mareMediator, RedrawManager redrawManager) : base(logger, mareMediator)
|
||||||
|
{
|
||||||
|
_pi = pi;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_redrawManager = redrawManager;
|
||||||
|
_penumbraInit = Initialized.Subscriber(pi, PenumbraInit);
|
||||||
|
_penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose);
|
||||||
|
_penumbraResolveModDir = new GetModDirectory(pi);
|
||||||
|
_penumbraRedraw = new RedrawObject(pi);
|
||||||
|
_penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent);
|
||||||
|
_penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi);
|
||||||
|
_penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi);
|
||||||
|
_penumbraAddTemporaryMod = new AddTemporaryMod(pi);
|
||||||
|
_penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi);
|
||||||
|
_penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi);
|
||||||
|
_penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi);
|
||||||
|
_penumbraResolvePaths = new ResolvePlayerPathsAsync(pi);
|
||||||
|
_penumbraEnabled = new GetEnabledState(pi);
|
||||||
|
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) =>
|
||||||
|
{
|
||||||
|
if (change == ModSettingChange.EnableState)
|
||||||
|
_mareMediator.Publish(new PenumbraModSettingChangedMessage());
|
||||||
|
});
|
||||||
|
_penumbraConvertTextureFile = new ConvertTextureFile(pi);
|
||||||
|
_penumbraResourcePaths = new GetGameObjectResourcePaths(pi);
|
||||||
|
|
||||||
|
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded);
|
||||||
|
|
||||||
|
CheckAPI();
|
||||||
|
CheckModDirectory();
|
||||||
|
|
||||||
|
Mediator.Subscribe<PenumbraRedrawCharacterMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<DalamudLoginMessage>(this, (msg) => _shownPenumbraUnavailable = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; } = false;
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
bool penumbraAvailable = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var penumbraVersion = (_pi.InstalledPlugins
|
||||||
|
.FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?.Version ?? new Version(0, 0, 0, 0));
|
||||||
|
penumbraAvailable = penumbraVersion >= new Version(1, 2, 0, 22);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
penumbraAvailable &= _penumbraEnabled.Invoke();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
penumbraAvailable = false;
|
||||||
|
}
|
||||||
|
_shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable;
|
||||||
|
APIAvailable = penumbraAvailable;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = penumbraAvailable;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!penumbraAvailable && !_shownPenumbraUnavailable)
|
||||||
|
{
|
||||||
|
_shownPenumbraUnavailable = true;
|
||||||
|
_mareMediator.Publish(new NotificationMessage("Penumbra inactive",
|
||||||
|
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Mare. If you just updated Penumbra, ignore this message.",
|
||||||
|
NotificationType.Error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckModDirectory()
|
||||||
|
{
|
||||||
|
if (!APIAvailable)
|
||||||
|
{
|
||||||
|
ModDirectory = string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
_redrawManager.Cancel();
|
||||||
|
|
||||||
|
_penumbraModSettingChanged.Dispose();
|
||||||
|
_penumbraGameObjectResourcePathResolved.Dispose();
|
||||||
|
_penumbraDispose.Dispose();
|
||||||
|
_penumbraInit.Dispose();
|
||||||
|
_penumbraObjectIsRedrawn.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true);
|
||||||
|
logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign);
|
||||||
|
return collName;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ConvertTextureFiles(ILogger logger, Dictionary<string, string[]> textures, IProgress<(string, int)> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
|
||||||
|
_mareMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles)));
|
||||||
|
int currentTexture = 0;
|
||||||
|
foreach (var texture in textures)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested) break;
|
||||||
|
|
||||||
|
progress.Report((texture.Key, ++currentTexture));
|
||||||
|
|
||||||
|
logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex);
|
||||||
|
var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, mipMaps: true);
|
||||||
|
await convertTask.ConfigureAwait(false);
|
||||||
|
if (convertTask.IsCompletedSuccessfully && texture.Value.Any())
|
||||||
|
{
|
||||||
|
foreach (var duplicatedTexture in texture.Value)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(texture.Key, duplicatedTexture, overwrite: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_mareMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles)));
|
||||||
|
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(async () =>
|
||||||
|
{
|
||||||
|
var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||||
|
_penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return Guid.Empty;
|
||||||
|
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var collName = "Mare_" + uid;
|
||||||
|
var collId = _penumbraCreateNamedTemporaryCollection.Invoke(collName);
|
||||||
|
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
|
||||||
|
return collId;
|
||||||
|
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, HashSet<string>>?> GetCharacterData(ILogger logger, GameObjectHandler handler)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return null;
|
||||||
|
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||||
|
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||||
|
if (idx == null) return null;
|
||||||
|
return _penumbraResourcePaths.Invoke(idx.Value)[0];
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetMetaManipulations()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return string.Empty;
|
||||||
|
return _penumbraGetMetaManipulations.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||||
|
{
|
||||||
|
logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId);
|
||||||
|
_penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw);
|
||||||
|
|
||||||
|
}, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_redrawManager.RedrawSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
logger.LogTrace("[{applicationId}] Removing temp collection for {collId}", applicationId, collId);
|
||||||
|
var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collId);
|
||||||
|
logger.LogTrace("[{applicationId}] RemoveTemporaryCollection: {ret2}", applicationId, ret2);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||||
|
{
|
||||||
|
return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collId, string manipulationData)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData);
|
||||||
|
var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_Meta", collId, [], manipulationData, 0);
|
||||||
|
logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary<string, string> modPaths)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
foreach (var mod in modPaths)
|
||||||
|
{
|
||||||
|
logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value);
|
||||||
|
}
|
||||||
|
var retRemove = _penumbraRemoveTemporaryMod.Invoke("MareChara_Files", collId, 0);
|
||||||
|
logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove);
|
||||||
|
var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_Files", collId, modPaths, string.Empty, 0);
|
||||||
|
logger.LogTrace("[{applicationId}] Setting temp files mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
||||||
|
{
|
||||||
|
bool wasRequested = false;
|
||||||
|
if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest)
|
||||||
|
{
|
||||||
|
_penumbraRedrawRequests[objectAddress] = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResourceLoaded(IntPtr ptr, string arg1, string arg2)
|
||||||
|
{
|
||||||
|
if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PenumbraDispose()
|
||||||
|
{
|
||||||
|
_redrawManager.Cancel();
|
||||||
|
_mareMediator.Publish(new PenumbraDisposedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PenumbraInit()
|
||||||
|
{
|
||||||
|
APIAvailable = true;
|
||||||
|
ModDirectory = _penumbraResolveModDir.Invoke();
|
||||||
|
_mareMediator.Publish(new PenumbraInitializedMessage());
|
||||||
|
_penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs
Normal file
158
MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerPetNames : IIpcCaller
|
||||||
|
{
|
||||||
|
private readonly ILogger<IpcCallerPetNames> _logger;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<object> _petnamesReady;
|
||||||
|
private readonly ICallGateSubscriber<object> _petnamesDisposing;
|
||||||
|
private readonly ICallGateSubscriber<(uint, uint)> _apiVersion;
|
||||||
|
private readonly ICallGateSubscriber<bool> _enabled;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<string, object> _playerDataChanged;
|
||||||
|
private readonly ICallGateSubscriber<string> _getPlayerData;
|
||||||
|
private readonly ICallGateSubscriber<string, object> _setPlayerData;
|
||||||
|
private readonly ICallGateSubscriber<ushort, object> _clearPlayerData;
|
||||||
|
|
||||||
|
public IpcCallerPetNames(ILogger<IpcCallerPetNames> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||||
|
MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
|
||||||
|
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.Ready");
|
||||||
|
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.Disposing");
|
||||||
|
_apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion");
|
||||||
|
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.Enabled");
|
||||||
|
|
||||||
|
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.PlayerDataChanged");
|
||||||
|
_getPlayerData = pi.GetIpcSubscriber<string>("PetRenamer.GetPlayerData");
|
||||||
|
_setPlayerData = pi.GetIpcSubscriber<string, object>("PetRenamer.SetPlayerData");
|
||||||
|
_clearPlayerData = pi.GetIpcSubscriber<ushort, object>("PetRenamer.ClearPlayerData");
|
||||||
|
|
||||||
|
_petnamesReady.Subscribe(OnPetNicknamesReady);
|
||||||
|
_petnamesDisposing.Subscribe(OnPetNicknamesDispose);
|
||||||
|
_playerDataChanged.Subscribe(OnLocalPetNicknamesDataChange);
|
||||||
|
|
||||||
|
CheckAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; } = false;
|
||||||
|
|
||||||
|
public void CheckAPI()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
APIAvailable = _enabled?.InvokeFunc() ?? false;
|
||||||
|
if (APIAvailable)
|
||||||
|
{
|
||||||
|
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPetNicknamesReady()
|
||||||
|
{
|
||||||
|
CheckAPI();
|
||||||
|
_mareMediator.Publish(new PetNamesReadyMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPetNicknamesDispose()
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new PetNamesMessage(string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetLocalNames()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string localNameData = _getPlayerData.InvokeFunc();
|
||||||
|
return string.IsNullOrEmpty(localNameData) ? string.Empty : localNameData;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Could not obtain Pet Nicknames data");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetPlayerData(nint character, string playerData)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
|
||||||
|
_logger.LogTrace("Applying Pet Nicknames data to {chara}", character.ToString("X"));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(playerData))
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||||
|
if (gameObj is IPlayerCharacter pc)
|
||||||
|
{
|
||||||
|
_clearPlayerData.InvokeAction(pc.ObjectIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_setPlayerData.InvokeAction(playerData);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Could not apply Pet Nicknames data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearPlayerData(nint characterPointer)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(characterPointer);
|
||||||
|
if (gameObj is IPlayerCharacter pc)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Pet Nicknames removing for {addr}", pc.Address.ToString("X"));
|
||||||
|
_clearPlayerData.InvokeAction(pc.ObjectIndex);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Could not clear Pet Nicknames data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLocalPetNicknamesDataChange(string data)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new PetNamesMessage(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_petnamesReady.Unsubscribe(OnPetNicknamesReady);
|
||||||
|
_petnamesDisposing.Unsubscribe(OnPetNicknamesDispose);
|
||||||
|
_playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
MareSynchronos/Interop/Ipc/IpcManager.cs
Normal file
62
MareSynchronos/Interop/Ipc/IpcManager.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
public IpcManager(ILogger<IpcManager> logger, MareMediator mediator,
|
||||||
|
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
||||||
|
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
CustomizePlus = customizeIpc;
|
||||||
|
Heels = heelsIpc;
|
||||||
|
Glamourer = glamourerIpc;
|
||||||
|
Penumbra = penumbraIpc;
|
||||||
|
Honorific = honorificIpc;
|
||||||
|
Moodles = moodlesIpc;
|
||||||
|
PetNames = ipcCallerPetNames;
|
||||||
|
Brio = ipcCallerBrio;
|
||||||
|
|
||||||
|
if (Initialized)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new PenumbraInitializedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => PeriodicApiStateCheck());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PeriodicApiStateCheck();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Failed to check for some IPC, plugin not installed?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Initialized => Penumbra.APIAvailable && Glamourer.APIAvailable;
|
||||||
|
|
||||||
|
public IpcCallerCustomize CustomizePlus { get; init; }
|
||||||
|
public IpcCallerHonorific Honorific { get; init; }
|
||||||
|
public IpcCallerHeels Heels { get; init; }
|
||||||
|
public IpcCallerGlamourer Glamourer { get; }
|
||||||
|
public IpcCallerPenumbra Penumbra { get; }
|
||||||
|
public IpcCallerMoodles Moodles { get; }
|
||||||
|
public IpcCallerPetNames PetNames { get; }
|
||||||
|
|
||||||
|
public IpcCallerBrio Brio { get; }
|
||||||
|
|
||||||
|
private void PeriodicApiStateCheck()
|
||||||
|
{
|
||||||
|
Penumbra.CheckAPI();
|
||||||
|
Penumbra.CheckModDirectory();
|
||||||
|
Glamourer.CheckAPI();
|
||||||
|
Heels.CheckAPI();
|
||||||
|
CustomizePlus.CheckAPI();
|
||||||
|
Honorific.CheckAPI();
|
||||||
|
Moodles.CheckAPI();
|
||||||
|
PetNames.CheckAPI();
|
||||||
|
Brio.CheckAPI();
|
||||||
|
}
|
||||||
|
}
|
||||||
92
MareSynchronos/Interop/Ipc/IpcProvider.cs
Normal file
92
MareSynchronos/Interop/Ipc/IpcProvider.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<IpcProvider> _logger;
|
||||||
|
private readonly IDalamudPluginInterface _pi;
|
||||||
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider;
|
||||||
|
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
|
||||||
|
private ICallGateProvider<List<nint>>? _handledGameAddresses;
|
||||||
|
private readonly List<GameObjectHandler> _activeGameObjectHandlers = [];
|
||||||
|
|
||||||
|
public MareMediator Mediator { get; init; }
|
||||||
|
|
||||||
|
public IpcProvider(ILogger<IpcProvider> logger, IDalamudPluginInterface pi,
|
||||||
|
CharaDataManager charaDataManager, MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_pi = pi;
|
||||||
|
_charaDataManager = charaDataManager;
|
||||||
|
Mediator = mareMediator;
|
||||||
|
|
||||||
|
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.OwnedObject) return;
|
||||||
|
_activeGameObjectHandlers.Add(msg.GameObjectHandler);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.OwnedObject) return;
|
||||||
|
_activeGameObjectHandlers.Remove(msg.GameObjectHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting IpcProviderService");
|
||||||
|
_loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("MareSynchronos.LoadMcdf");
|
||||||
|
_loadFileProvider.RegisterFunc(LoadMcdf);
|
||||||
|
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("MareSynchronos.LoadMcdfAsync");
|
||||||
|
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
|
||||||
|
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("MareSynchronos.GetHandledAddresses");
|
||||||
|
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
|
||||||
|
_logger.LogInformation("Started IpcProviderService");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Stopping IpcProvider Service");
|
||||||
|
_loadFileProvider?.UnregisterFunc();
|
||||||
|
_loadFileAsyncProvider?.UnregisterFunc();
|
||||||
|
_handledGameAddresses?.UnregisterFunc();
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> LoadMcdfAsync(string path, IGameObject target)
|
||||||
|
{
|
||||||
|
await ApplyFileAsync(path, target).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool LoadMcdf(string path, IGameObject target)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () => await ApplyFileAsync(path, target).ConfigureAwait(false)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyFileAsync(string path, IGameObject target)
|
||||||
|
{
|
||||||
|
_charaDataManager.LoadMcdf(path);
|
||||||
|
await (_charaDataManager.LoadedMcdfHeader ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
|
_charaDataManager.McdfApplyToTarget(target.Name.TextValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<nint> GetHandledAddresses()
|
||||||
|
{
|
||||||
|
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
MareSynchronos/Interop/Ipc/RedrawManager.cs
Normal file
54
MareSynchronos/Interop/Ipc/RedrawManager.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public class RedrawManager
|
||||||
|
{
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly ConcurrentDictionary<nint, bool> _penumbraRedrawRequests = [];
|
||||||
|
private CancellationTokenSource _disposalCts = new();
|
||||||
|
|
||||||
|
public SemaphoreSlim RedrawSemaphore { get; init; } = new(2, 2);
|
||||||
|
|
||||||
|
public RedrawManager(MareMediator mareMediator, DalamudUtilService dalamudUtil)
|
||||||
|
{
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PenumbraRedrawInternalAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, Action<ICharacter> action, CancellationToken token)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new PenumbraStartRedrawMessage(handler.Address));
|
||||||
|
|
||||||
|
_penumbraRedrawRequests[handler.Address] = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using CancellationTokenSource cancelToken = new CancellationTokenSource();
|
||||||
|
using CancellationTokenSource combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, token, _disposalCts.Token);
|
||||||
|
var combinedToken = combinedCts.Token;
|
||||||
|
cancelToken.CancelAfter(TimeSpan.FromSeconds(15));
|
||||||
|
await handler.ActOnFrameworkAfterEnsureNoDrawAsync(action, combinedToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!_disposalCts.Token.IsCancellationRequested)
|
||||||
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, handler, applicationId, 30000, combinedToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_penumbraRedrawRequests[handler.Address] = false;
|
||||||
|
_mareMediator.Publish(new PenumbraEndRedrawMessage(handler.Address));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Cancel()
|
||||||
|
{
|
||||||
|
_disposalCts = _disposalCts.CancelRecreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
199
MareSynchronos/Interop/VfxSpawnManager.cs
Normal file
199
MareSynchronos/Interop/VfxSpawnManager.cs
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
using Dalamud.Memory;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Code for spawning mostly taken from https://git.anna.lgbt/anna/OrangeGuidanceTomestone/src/branch/main/client/Vfx.cs
|
||||||
|
/// </summary>
|
||||||
|
public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private static readonly byte[] _pool = "Client.System.Scheduler.Instance.VfxObject\0"u8.ToArray();
|
||||||
|
|
||||||
|
[Signature("E8 ?? ?? ?? ?? F3 0F 10 35 ?? ?? ?? ?? 48 89 43 08")]
|
||||||
|
private readonly delegate* unmanaged<byte*, byte*, VfxStruct*> _staticVfxCreate;
|
||||||
|
|
||||||
|
[Signature("E8 ?? ?? ?? ?? ?? ?? ?? 8B 4A ?? 85 C9")]
|
||||||
|
private readonly delegate* unmanaged<VfxStruct*, float, int, ulong> _staticVfxRun;
|
||||||
|
|
||||||
|
[Signature("40 53 48 83 EC 20 48 8B D9 48 8B 89 ?? ?? ?? ?? 48 85 C9 74 28 33 D2 E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9")]
|
||||||
|
private readonly delegate* unmanaged<VfxStruct*, nint> _staticVfxRemove;
|
||||||
|
|
||||||
|
public VfxSpawnManager(ILogger<VfxSpawnManager> logger, IGameInteropProvider gameInteropProvider, MareMediator mareMediator)
|
||||||
|
: base(logger, mareMediator)
|
||||||
|
{
|
||||||
|
gameInteropProvider.InitializeFromAttributes(this);
|
||||||
|
mareMediator.Subscribe<GposeStartMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
ChangeSpawnVisibility(0f);
|
||||||
|
});
|
||||||
|
mareMediator.Subscribe<GposeEndMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
RestoreSpawnVisiblity();
|
||||||
|
});
|
||||||
|
mareMediator.Subscribe<CutsceneStartMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
ChangeSpawnVisibility(0f);
|
||||||
|
});
|
||||||
|
mareMediator.Subscribe<CutsceneEndMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
RestoreSpawnVisiblity();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void RestoreSpawnVisiblity()
|
||||||
|
{
|
||||||
|
foreach (var vfx in _spawnedObjects)
|
||||||
|
{
|
||||||
|
((VfxStruct*)vfx.Value.Address)->Alpha = vfx.Value.Visibility;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void ChangeSpawnVisibility(float visibility)
|
||||||
|
{
|
||||||
|
foreach (var vfx in _spawnedObjects)
|
||||||
|
{
|
||||||
|
((VfxStruct*)vfx.Value.Address)->Alpha = visibility;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<Guid, (nint Address, float Visibility)> _spawnedObjects = [];
|
||||||
|
|
||||||
|
private VfxStruct* SpawnStatic(string path, Vector3 pos, Quaternion rotation, float r, float g, float b, float a, Vector3 scale)
|
||||||
|
{
|
||||||
|
VfxStruct* vfx;
|
||||||
|
fixed (byte* terminatedPath = Encoding.UTF8.GetBytes(path).NullTerminate())
|
||||||
|
{
|
||||||
|
fixed (byte* pool = _pool)
|
||||||
|
{
|
||||||
|
vfx = _staticVfxCreate(terminatedPath, pool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vfx == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
vfx->Position = new Vector3(pos.X, pos.Y + 1, pos.Z);
|
||||||
|
vfx->Rotation = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||||
|
|
||||||
|
vfx->SomeFlags &= 0xF7;
|
||||||
|
vfx->Flags |= 2;
|
||||||
|
vfx->Red = r;
|
||||||
|
vfx->Green = g;
|
||||||
|
vfx->Blue = b;
|
||||||
|
vfx->Scale = scale;
|
||||||
|
|
||||||
|
vfx->Alpha = a;
|
||||||
|
|
||||||
|
_staticVfxRun(vfx, 0.0f, -1);
|
||||||
|
|
||||||
|
return vfx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid? SpawnObject(Vector3 position, Quaternion rotation, Vector3 scale, float r = 1f, float g = 1f, float b = 1f, float a = 0.5f)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Trying to Spawn orb VFX at {pos}, {rot}", position, rotation);
|
||||||
|
var vfx = SpawnStatic("bgcommon/world/common/vfx_for_event/eff/b0150_eext_y.avfx", position, rotation, r, g, b, a, scale);
|
||||||
|
if (vfx == null || (nint)vfx == nint.Zero)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Failed to Spawn VFX at {pos}, {rot}", position, rotation);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Guid guid = Guid.NewGuid();
|
||||||
|
Logger.LogDebug("Spawned VFX at {pos}, {rot}: 0x{ptr:X}", position, rotation, (nint)vfx);
|
||||||
|
|
||||||
|
_spawnedObjects[guid] = ((nint)vfx, a);
|
||||||
|
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe void MoveObject(Guid id, Vector3 newPosition)
|
||||||
|
{
|
||||||
|
if (_spawnedObjects.TryGetValue(id, out var vfxValue))
|
||||||
|
{
|
||||||
|
if (vfxValue.Address == nint.Zero) return;
|
||||||
|
var vfx = (VfxStruct*)vfxValue.Address;
|
||||||
|
vfx->Position = newPosition with { Y = newPosition.Y + 1 };
|
||||||
|
vfx->Flags |= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DespawnObject(Guid? id)
|
||||||
|
{
|
||||||
|
if (id == null) return;
|
||||||
|
if (_spawnedObjects.Remove(id.Value, out var value))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Despawning {obj:X}", value.Address);
|
||||||
|
_staticVfxRemove((VfxStruct*)value.Address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveAllVfx()
|
||||||
|
{
|
||||||
|
foreach (var obj in _spawnedObjects.Values)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Despawning {obj:X}", obj);
|
||||||
|
_staticVfxRemove((VfxStruct*)obj.Address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
RemoveAllVfx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
internal struct VfxStruct
|
||||||
|
{
|
||||||
|
[FieldOffset(0x38)]
|
||||||
|
public byte Flags;
|
||||||
|
|
||||||
|
[FieldOffset(0x50)]
|
||||||
|
public Vector3 Position;
|
||||||
|
|
||||||
|
[FieldOffset(0x60)]
|
||||||
|
public Quaternion Rotation;
|
||||||
|
|
||||||
|
[FieldOffset(0x70)]
|
||||||
|
public Vector3 Scale;
|
||||||
|
|
||||||
|
[FieldOffset(0x128)]
|
||||||
|
public int ActorCaster;
|
||||||
|
|
||||||
|
[FieldOffset(0x130)]
|
||||||
|
public int ActorTarget;
|
||||||
|
|
||||||
|
[FieldOffset(0x1B8)]
|
||||||
|
public int StaticCaster;
|
||||||
|
|
||||||
|
[FieldOffset(0x1C0)]
|
||||||
|
public int StaticTarget;
|
||||||
|
|
||||||
|
[FieldOffset(0x248)]
|
||||||
|
public byte SomeFlags;
|
||||||
|
|
||||||
|
[FieldOffset(0x260)]
|
||||||
|
public float Red;
|
||||||
|
|
||||||
|
[FieldOffset(0x264)]
|
||||||
|
public float Green;
|
||||||
|
|
||||||
|
[FieldOffset(0x268)]
|
||||||
|
public float Blue;
|
||||||
|
|
||||||
|
[FieldOffset(0x26C)]
|
||||||
|
public float Alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
MareSynchronos/Localization/Strings.cs
Normal file
44
MareSynchronos/Localization/Strings.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using CheapLoc;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Localization;
|
||||||
|
|
||||||
|
public static class Strings
|
||||||
|
{
|
||||||
|
public static ToSStrings ToS { get; set; } = new();
|
||||||
|
|
||||||
|
public class ToSStrings
|
||||||
|
{
|
||||||
|
public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree");
|
||||||
|
public readonly string AgreementLabel = Loc.Localize("AgreementLabel", "Agreement of Usage of Service");
|
||||||
|
public readonly string ButtonWillBeAvailableIn = Loc.Localize("ButtonWillBeAvailableIn", "'I agree' button will be available in");
|
||||||
|
public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language");
|
||||||
|
|
||||||
|
public readonly string Paragraph1 = Loc.Localize("Paragraph1",
|
||||||
|
"All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. " +
|
||||||
|
"The plugin will exclusively upload the necessary mod files and not the whole mod.");
|
||||||
|
|
||||||
|
public readonly string Paragraph2 = Loc.Localize("Paragraph2",
|
||||||
|
"If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " +
|
||||||
|
"Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " +
|
||||||
|
"Files present on the service that already represent your active mod files will not be uploaded again.");
|
||||||
|
|
||||||
|
public readonly string Paragraph3 = Loc.Localize("Paragraph3",
|
||||||
|
"The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " +
|
||||||
|
"Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " +
|
||||||
|
"Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod.");
|
||||||
|
|
||||||
|
public readonly string Paragraph4 = Loc.Localize("Paragraph4",
|
||||||
|
"The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.");
|
||||||
|
|
||||||
|
public readonly string Paragraph5 = Loc.Localize("Paragraph5",
|
||||||
|
"Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. " +
|
||||||
|
"After a period of not being used, the mod files will be automatically deleted. " +
|
||||||
|
"You will also be able to wipe all the files you have personally uploaded on request. " +
|
||||||
|
"The service holds no information about which mod files belong to which mod.");
|
||||||
|
|
||||||
|
public readonly string Paragraph6 = Loc.Localize("Paragraph6",
|
||||||
|
"This service is provided as-is. In case of abuse join the Mare Synchronos Discord.");
|
||||||
|
|
||||||
|
public readonly string ReadLabel = Loc.Localize("ReadLabel", "READ THIS CAREFULLY");
|
||||||
|
}
|
||||||
|
}
|
||||||
46
MareSynchronos/Localization/de.json
Normal file
46
MareSynchronos/Localization/de.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"LanguageLabel": {
|
||||||
|
"message": "Language",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreementLabel": {
|
||||||
|
"message": "Nutzungsbedingungen",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ReadLabel": {
|
||||||
|
"message": "BITTE LIES DIES SORGFÄLTIG",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph1": {
|
||||||
|
"message": "Alle Moddateien, die aktuell auf deinem Charakter aktiv sind und dein Charakterzustand werden automatisch zu dem Service, an dem du dich registriert hast, hochgeladen. Das Plugin wird ausschließlich die nötigen Moddateien hochladen und nicht die gesamte Modifikation.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph2": {
|
||||||
|
"message": "Falls du mit einer getakteten Internetverbindung verbunden bist, können durch den Datentransfer von Hoch- und Runtergeladenen Moddateien höhere Kosten entstehen. Moddateien werden beim Hoch- und Runterladen komprimiert um Bandbreite zu sparen. Durch unterschiedliche Hoch- und Runterladgeschwindigkeiten ist es möglich, dass Änderungen an Charakteren nicht sofort sichtbar sind. Dateien die bereits auf dem Service existieren, werden nicht nochmals hochgeladen.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph3": {
|
||||||
|
"message": "Die Moddateien die du hochlädst sind vertraulich und werden nicht mit anderen Nutzern geteilt, die nicht die exakt selben Dateien anfordern. Bitte überlege dir sorgfältig mit wem du deinen Identifikationscode teilst, da es unvermeidlich ist, dass die andere Person deine Moddateien erhält und lokal zwischenspeichert. Lokal zwischengespeicherte Dateien haben willkürrliche Namen um vor Versuchen abzuschrecken die originalen Moddateien aus diesen wiederherzustellen.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph4": {
|
||||||
|
"message": "Der Ersteller des Plugins hat sein Bestes getan, um deine Sicherheit zu gewährleisten. Es gibt jedoch keine Garantie für 100%ige Sicherheit. Teile deinen Identifikationscode nicht blind mit jedem.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph5": {
|
||||||
|
"message": "Moddateien, die auf dem Service gespeichert sind, verbleiben auf dem Service, solange es Anforderungen für diese Dateien gibt. Nach einer Zeitspanne in der die Dateien nicht verwendet wurden, werden diese automatisch gelöscht. Du hast auch die Möglichkeit manuell alle Dateien auf dem Service zu löschen. Der Service hat keine Informationen welche Moddateien zu welcher Modifikation gehören.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph6": {
|
||||||
|
"message": "Dieser Dienst wird ohne Gewähr angeboten. Im Falle eines Missbrauchs tretet dem Mare Synchronos Discord bei.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreeLabel": {
|
||||||
|
"message": "Ich Stimme zu",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ButtonWillBeAvailableIn": {
|
||||||
|
"message": "\"Ich stimme zu\" Knopf verfügbar in",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
}
|
||||||
|
}
|
||||||
46
MareSynchronos/Localization/fr.json
Normal file
46
MareSynchronos/Localization/fr.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"LanguageLabel": {
|
||||||
|
"message": "Language",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreementLabel": {
|
||||||
|
"message": "Conditions d'Utilisation",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ReadLabel": {
|
||||||
|
"message": "LISEZ CES INFORMATIONS ATTENTIVEMENT",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph1": {
|
||||||
|
"message": "Tous les fichiers moddés actuellement en cours d'utilisation ainsi que le statut actuel de votre personnage vont être mix en ligne via le service sur lequel vous vous êtes automatiquement enregistré. Seuls les fichiers nécessaires seront téléversés par le plugin et non pas le mod en entier.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph2": {
|
||||||
|
"message": "Si le débit de votre connexion internet est limité, le téléchargement et téléversement d'un grand nombre de fichiers peut entraîner des coûts supplémentaires. Les fichiers seront compressés au chargement et versement pour réduire l'impact sur votre bande passants. Selon la rapidité de vos téléchargements et téléversements, les changements ne seront peut-être pas visibles instantanément sur les personnages. Les fichiers déja présents sur le service qui correspondent à ceux de vos mods en cours d'utilisation ne seront pas remis en ligne.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph3": {
|
||||||
|
"message": "Les fichiers que vous allez partager sont confidentiels et ne seront envoyés qu'aux utilisateurs qui feront une requête exacte de ceux-çi. Nous vous demandons de (re)considérer qui sera synchronisé avec vous, puisqu'ils recevront et stockeront inévitablement en local les fichiers nécéssaires utilisés à cet instant. Les noms des fichiers stockés localement sont changés de manière arbitraire afin de décourager toute tentative de réplication des originaux.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph4": {
|
||||||
|
"message": "Le créateur de ce plugin a tenté de sécuriser l'application du mieux possible. Cependant, il ne peut pas garantir une protection 100% infaillible. Pour votre sécurité, ne vous synchronisez pas aveuglément et avec n'importe qui.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph5": {
|
||||||
|
"message": "Les fichiers sauvegardés sur le service resteront en ligne tant que des utilisateurs en feront usage. Ils seront effacés automatiquement après une certaine période d'inactivité. Vous pouvez également demander l'effacement de tous les fichiers que vous avez mis en ligne vous-même. Le service en soi ne contient aucune information pouvant identifier quel fichier appartient à quel mod.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph6": {
|
||||||
|
"message": "Ce service et ses composants vous sont fournis en l'état. En cas d'abus rejoindre le serveur Discord Mare Synchronos.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreeLabel": {
|
||||||
|
"message": "J'accept",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ButtonWillBeAvailableIn": {
|
||||||
|
"message": "Bouton \"J'accept\" disposible dans",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
MareSynchronos/MareConfiguration/CharaDataConfigService.cs
Normal file
11
MareSynchronos/MareConfiguration/CharaDataConfigService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class CharaDataConfigService : ConfigurationServiceBase<CharaDataConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "charadata.json";
|
||||||
|
|
||||||
|
public CharaDataConfigService(string configDir) : base(configDir) { }
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
13
MareSynchronos/MareConfiguration/ConfigurationExtensions.cs
Normal file
13
MareSynchronos/MareConfiguration/ConfigurationExtensions.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public static class ConfigurationExtensions
|
||||||
|
{
|
||||||
|
public static bool HasValidSetup(this MareConfig configuration)
|
||||||
|
{
|
||||||
|
return configuration.AcceptedAgreement && configuration.InitialScanComplete
|
||||||
|
&& !string.IsNullOrEmpty(configuration.CacheFolder)
|
||||||
|
&& Directory.Exists(configuration.CacheFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
MareSynchronos/MareConfiguration/ConfigurationMigrator.cs
Normal file
45
MareSynchronos/MareConfiguration/ConfigurationMigrator.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
||||||
|
ServerConfigService serverConfigService) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||||
|
|
||||||
|
public void Migrate()
|
||||||
|
{
|
||||||
|
if (transientConfigService.Current.Version == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Migrating Transient Config V0 => V1");
|
||||||
|
transientConfigService.Current.TransientConfigs.Clear();
|
||||||
|
transientConfigService.Current.Version = 1;
|
||||||
|
transientConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverConfigService.Current.Version == 1)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Migrating Server Config V1 => V2");
|
||||||
|
var centralServer = serverConfigService.Current.ServerStorage.Find(f => f.ServerName.Equals("Lunae Crescere Incipientis (Central Server EU)", StringComparison.Ordinal));
|
||||||
|
if (centralServer != null)
|
||||||
|
{
|
||||||
|
centralServer.ServerName = ApiController.MainServer;
|
||||||
|
}
|
||||||
|
serverConfigService.Current.Version = 2;
|
||||||
|
serverConfigService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Migrate();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
137
MareSynchronos/MareConfiguration/ConfigurationSaveService.cs
Normal file
137
MareSynchronos/MareConfiguration/ConfigurationSaveService.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class ConfigurationSaveService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly HashSet<object> _configsToSave = [];
|
||||||
|
private readonly ILogger<ConfigurationSaveService> _logger;
|
||||||
|
private readonly SemaphoreSlim _configSaveSemaphore = new(1, 1);
|
||||||
|
private readonly CancellationTokenSource _configSaveCheckCts = new();
|
||||||
|
public const string BackupFolder = "config_backup";
|
||||||
|
private readonly MethodInfo _saveMethod;
|
||||||
|
|
||||||
|
public ConfigurationSaveService(ILogger<ConfigurationSaveService> logger, IEnumerable<IConfigService<IMareConfiguration>> configs)
|
||||||
|
{
|
||||||
|
foreach (var config in configs)
|
||||||
|
{
|
||||||
|
config.ConfigSave += OnConfigurationSave;
|
||||||
|
}
|
||||||
|
_logger = logger;
|
||||||
|
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
|
||||||
|
_saveMethod = GetType().GetMethod(nameof(SaveConfig), BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||||
|
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConfigurationSave(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_configSaveSemaphore.Wait();
|
||||||
|
_configsToSave.Add(sender!);
|
||||||
|
_configSaveSemaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PeriodicSaveCheck(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SaveConfigs().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during SaveConfigs");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveConfigs()
|
||||||
|
{
|
||||||
|
if (_configsToSave.Count == 0) return;
|
||||||
|
|
||||||
|
await _configSaveSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
var configList = _configsToSave.ToList();
|
||||||
|
_configsToSave.Clear();
|
||||||
|
_configSaveSemaphore.Release();
|
||||||
|
|
||||||
|
foreach (var config in configList)
|
||||||
|
{
|
||||||
|
var expectedType = config.GetType().BaseType!.GetGenericArguments()[0];
|
||||||
|
var save = _saveMethod.MakeGenericMethod(expectedType);
|
||||||
|
await ((Task)save.Invoke(this, [config])!).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveConfig<T>(IConfigService<T> config) where T : IMareConfiguration
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
|
||||||
|
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||||
|
if (!Directory.Exists(configBackupFolder))
|
||||||
|
Directory.CreateDirectory(configBackupFolder);
|
||||||
|
|
||||||
|
var configNameSplit = config.ConfigurationName.Split(".");
|
||||||
|
var existingConfigs = Directory.EnumerateFiles(
|
||||||
|
configBackupFolder,
|
||||||
|
configNameSplit[0] + "*")
|
||||||
|
.Select(c => new FileInfo(c))
|
||||||
|
.OrderByDescending(c => c.LastWriteTime).ToList();
|
||||||
|
if (existingConfigs.Skip(10).Any())
|
||||||
|
{
|
||||||
|
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||||
|
{
|
||||||
|
oldBak.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
||||||
|
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
||||||
|
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
||||||
|
FileInfo fi = new(backupPath);
|
||||||
|
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// ignore if file cannot be backupped
|
||||||
|
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var temp = config.ConfigurationPath + ".tmp";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
})).ConfigureAwait(false);
|
||||||
|
File.Move(temp, config.ConfigurationPath, true);
|
||||||
|
config.UpdateLastWriteTime();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error during config save of {config}", config.ConfigurationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() => PeriodicSaveCheck(_configSaveCheckCts.Token));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _configSaveCheckCts.CancelAsync().ConfigureAwait(false);
|
||||||
|
_configSaveCheckCts.Dispose();
|
||||||
|
|
||||||
|
await SaveConfigs().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
146
MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs
Normal file
146
MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public abstract class ConfigurationServiceBase<T> : IConfigService<T> where T : IMareConfiguration
|
||||||
|
{
|
||||||
|
private readonly CancellationTokenSource _periodicCheckCts = new();
|
||||||
|
private DateTime _configLastWriteTime;
|
||||||
|
private Lazy<T> _currentConfigInternal;
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
public event EventHandler? ConfigSave;
|
||||||
|
|
||||||
|
protected ConfigurationServiceBase(string configDirectory)
|
||||||
|
{
|
||||||
|
ConfigurationDirectory = configDirectory;
|
||||||
|
|
||||||
|
_ = Task.Run(CheckForConfigUpdatesInternal, _periodicCheckCts.Token);
|
||||||
|
|
||||||
|
_currentConfigInternal = LazyConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConfigurationDirectory { get; init; }
|
||||||
|
public T Current => _currentConfigInternal.Value;
|
||||||
|
public abstract string ConfigurationName { get; }
|
||||||
|
public string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
ConfigSave?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLastWriteTime()
|
||||||
|
{
|
||||||
|
_configLastWriteTime = GetConfigLastWriteTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!disposing || _disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
_periodicCheckCts.Cancel();
|
||||||
|
_periodicCheckCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected T LoadConfig()
|
||||||
|
{
|
||||||
|
T? config;
|
||||||
|
if (!File.Exists(ConfigurationPath))
|
||||||
|
{
|
||||||
|
config = AttemptToLoadBackup();
|
||||||
|
if (Equals(config, default(T)))
|
||||||
|
{
|
||||||
|
config = (T)Activator.CreateInstance(typeof(T))!;
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
config = JsonSerializer.Deserialize<T>(File.ReadAllText(ConfigurationPath));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// config failed to load for some reason
|
||||||
|
config = AttemptToLoadBackup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config == null || Equals(config, default(T)))
|
||||||
|
{
|
||||||
|
config = (T)Activator.CreateInstance(typeof(T))!;
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_configLastWriteTime = GetConfigLastWriteTime();
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? AttemptToLoadBackup()
|
||||||
|
{
|
||||||
|
var configBackupFolder = Path.Join(ConfigurationDirectory, ConfigurationSaveService.BackupFolder);
|
||||||
|
var configNameSplit = ConfigurationName.Split(".");
|
||||||
|
if (!Directory.Exists(configBackupFolder))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var existingBackups = Directory.EnumerateFiles(configBackupFolder, configNameSplit[0] + "*").OrderByDescending(f => new FileInfo(f).LastWriteTimeUtc);
|
||||||
|
foreach (var file in existingBackups)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var config = JsonSerializer.Deserialize<T>(File.ReadAllText(file));
|
||||||
|
if (Equals(config, default(T)))
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(file, ConfigurationPath, true);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// couldn't load backup, might as well delete it
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckForConfigUpdatesInternal()
|
||||||
|
{
|
||||||
|
while (!_periodicCheckCts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), _periodicCheckCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var lastWriteTime = GetConfigLastWriteTime();
|
||||||
|
if (lastWriteTime != _configLastWriteTime)
|
||||||
|
{
|
||||||
|
_currentConfigInternal = LazyConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime GetConfigLastWriteTime()
|
||||||
|
{
|
||||||
|
try { return new FileInfo(ConfigurationPath).LastWriteTimeUtc; }
|
||||||
|
catch { return DateTime.MinValue; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Lazy<T> LazyConfig()
|
||||||
|
{
|
||||||
|
_configLastWriteTime = GetConfigLastWriteTime();
|
||||||
|
return new Lazy<T>(LoadConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class CharaDataConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public bool OpenMareHubOnGposeStart { get; set; } = false;
|
||||||
|
public string LastSavedCharaDataLocation { get; set; } = string.Empty;
|
||||||
|
public Dictionary<string, CharaDataFavorite> FavoriteCodes { get; set; } = [];
|
||||||
|
public bool DownloadMcdDataOnConnection { get; set; } = true;
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
public bool NearbyOwnServerOnly { get; set; } = false;
|
||||||
|
public bool NearbyIgnoreHousingLimitations { get; set; } = false;
|
||||||
|
public bool NearbyDrawWisps { get; set; } = true;
|
||||||
|
public int NearbyDistanceFilter { get; set; } = 100;
|
||||||
|
public bool NearbyShowOwnData { get; set; } = false;
|
||||||
|
public bool ShowHelpTexts { get; set; } = true;
|
||||||
|
public bool NearbyShowAlways { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public interface IMareConfiguration
|
||||||
|
{
|
||||||
|
int Version { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.UI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class MareConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public bool AcceptedAgreement { get; set; } = false;
|
||||||
|
public string CacheFolder { get; set; } = string.Empty;
|
||||||
|
public bool DisableOptionalPluginWarnings { get; set; } = false;
|
||||||
|
public bool EnableDtrEntry { get; set; } = false;
|
||||||
|
public bool ShowUidInDtrTooltip { get; set; } = true;
|
||||||
|
public bool PreferNoteInDtrTooltip { get; set; } = false;
|
||||||
|
public bool UseColorsInDtr { get; set; } = true;
|
||||||
|
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||||
|
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||||
|
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||||
|
public bool EnableRightClickMenus { get; set; } = true;
|
||||||
|
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||||
|
public string ExportFolder { get; set; } = string.Empty;
|
||||||
|
public bool FileScanPaused { get; set; } = false;
|
||||||
|
public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast;
|
||||||
|
public bool InitialScanComplete { get; set; } = false;
|
||||||
|
public LogLevel LogLevel { get; set; } = LogLevel.Information;
|
||||||
|
public bool LogPerformance { get; set; } = false;
|
||||||
|
public double MaxLocalCacheInGiB { get; set; } = 20;
|
||||||
|
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||||
|
public bool OpenPopupOnAdd { get; set; } = true;
|
||||||
|
public int ParallelDownloads { get; set; } = 10;
|
||||||
|
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||||
|
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||||
|
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
|
public float ProfileDelay { get; set; } = 1.5f;
|
||||||
|
public bool ProfilePopoutRight { get; set; } = false;
|
||||||
|
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||||
|
public bool ProfilesShow { get; set; } = true;
|
||||||
|
public bool ShowSyncshellUsersInVisible { get; set; } = true;
|
||||||
|
public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
|
||||||
|
public bool ShowOfflineUsersSeparately { get; set; } = true;
|
||||||
|
public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true;
|
||||||
|
public bool GroupUpSyncshells { get; set; } = true;
|
||||||
|
public bool ShowOnlineNotifications { get; set; } = false;
|
||||||
|
public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true;
|
||||||
|
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
|
||||||
|
public bool ShowTransferBars { get; set; } = true;
|
||||||
|
public bool ShowTransferWindow { get; set; } = false;
|
||||||
|
public bool ShowUploading { get; set; } = true;
|
||||||
|
public bool ShowUploadingBigText { get; set; } = true;
|
||||||
|
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||||
|
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||||
|
public int TransferBarsHeight { get; set; } = 12;
|
||||||
|
public bool TransferBarsShowText { get; set; } = true;
|
||||||
|
public int TransferBarsWidth { get; set; } = 250;
|
||||||
|
public bool UseAlternativeFileUpload { get; set; } = false;
|
||||||
|
public bool UseCompactor { get; set; } = false;
|
||||||
|
public bool DebugStopWhining { get; set; } = false;
|
||||||
|
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
||||||
|
public bool UseFocusTarget { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class PlayerPerformanceConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public bool ShowPerformanceIndicator { get; set; } = true;
|
||||||
|
public bool WarnOnExceedingThresholds { get; set; } = true;
|
||||||
|
public bool WarnOnPreferredPermissionsExceedingThresholds { get; set; } = false;
|
||||||
|
public int VRAMSizeWarningThresholdMiB { get; set; } = 375;
|
||||||
|
public int TrisWarningThresholdThousands { get; set; } = 165;
|
||||||
|
public bool AutoPausePlayersExceedingThresholds { get; set; } = false;
|
||||||
|
public bool AutoPausePlayersWithPreferredPermissionsExceedingThresholds { get; set; } = false;
|
||||||
|
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 550;
|
||||||
|
public int TrisAutoPauseThresholdThousands { get; set; } = 250;
|
||||||
|
public List<string> UIDsToIgnore { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public int CurrentServer { get; set; } = 0;
|
||||||
|
|
||||||
|
public List<ServerStorage> ServerStorage { get; set; } = new()
|
||||||
|
{
|
||||||
|
{ new ServerStorage() { ServerName = ApiController.MainServer, ServerUri = ApiController.MainServiceUri, UseOAuth2 = true } },
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool SendCensusData { get; set; } = false;
|
||||||
|
public bool ShownCensusPopup { get; set; } = false;
|
||||||
|
|
||||||
|
public int Version { get; set; } = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class ServerTagConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class TransientConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, TransientPlayerConfig> TransientConfigs { get; set; } = [];
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public class TransientPlayerConfig
|
||||||
|
{
|
||||||
|
public List<string> GlobalPersistentCache { get; set; } = [];
|
||||||
|
public Dictionary<uint, List<string>> JobSpecificCache { get; set; } = [];
|
||||||
|
public Dictionary<uint, List<string>> JobSpecificPetCache { get; set; } = [];
|
||||||
|
|
||||||
|
public TransientPlayerConfig()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ElevateIfNeeded(uint jobId, string gamePath)
|
||||||
|
{
|
||||||
|
// check if it's in the job cache of other jobs and elevate if needed
|
||||||
|
foreach (var kvp in JobSpecificCache)
|
||||||
|
{
|
||||||
|
if (kvp.Key == jobId) continue;
|
||||||
|
|
||||||
|
// elevate if the gamepath is included somewhere else
|
||||||
|
if (kvp.Value.Contains(gamePath, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
JobSpecificCache[kvp.Key].Remove(gamePath);
|
||||||
|
GlobalPersistentCache.Add(gamePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RemovePath(string gamePath, ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
int removedEntries = 0;
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
if (GlobalPersistentCache.Remove(gamePath)) removedEntries++;
|
||||||
|
foreach (var kvp in JobSpecificCache)
|
||||||
|
{
|
||||||
|
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (objectKind == ObjectKind.Pet)
|
||||||
|
{
|
||||||
|
foreach (var kvp in JobSpecificPetCache)
|
||||||
|
{
|
||||||
|
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removedEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddOrElevate(uint jobId, string gamePath)
|
||||||
|
{
|
||||||
|
// check if it's in the global cache, if yes, do nothing
|
||||||
|
if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ElevateIfNeeded(jobId, gamePath)) return;
|
||||||
|
|
||||||
|
// check if the jobid is already in the cache to start
|
||||||
|
if (!JobSpecificCache.TryGetValue(jobId, out var jobCache))
|
||||||
|
{
|
||||||
|
JobSpecificCache[jobId] = jobCache = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the path is already in the job specific cache
|
||||||
|
if (!jobCache.Contains(gamePath, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
jobCache.Add(gamePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class UidNotesConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerNotesStorage> ServerNotes { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class XivDataStorageConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
12
MareSynchronos/MareConfiguration/IConfigService.cs
Normal file
12
MareSynchronos/MareConfiguration/IConfigService.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public interface IConfigService<out T> : IDisposable where T : IMareConfiguration
|
||||||
|
{
|
||||||
|
T Current { get; }
|
||||||
|
string ConfigurationName { get; }
|
||||||
|
string ConfigurationPath { get; }
|
||||||
|
public event EventHandler? ConfigSave;
|
||||||
|
void UpdateLastWriteTime();
|
||||||
|
}
|
||||||
14
MareSynchronos/MareConfiguration/MareConfigService.cs
Normal file
14
MareSynchronos/MareConfiguration/MareConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class MareConfigService : ConfigurationServiceBase<MareConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "config.json";
|
||||||
|
|
||||||
|
public MareConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
12
MareSynchronos/MareConfiguration/Models/Authentication.cs
Normal file
12
MareSynchronos/MareConfiguration/Models/Authentication.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public record Authentication
|
||||||
|
{
|
||||||
|
public string CharacterName { get; set; } = string.Empty;
|
||||||
|
public uint WorldId { get; set; } = 0;
|
||||||
|
public int SecretKeyIdx { get; set; } = -1;
|
||||||
|
public string? UID { get; set; }
|
||||||
|
public bool AutoLogin { get; set; } = true;
|
||||||
|
public ulong? LastSeenCID { get; set; } = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class CharaDataFavorite
|
||||||
|
{
|
||||||
|
public DateTime LastDownloaded { get; set; } = DateTime.MaxValue;
|
||||||
|
public string CustomDescription { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
public enum DownloadSpeeds
|
||||||
|
{
|
||||||
|
Bps,
|
||||||
|
KBps,
|
||||||
|
MBps
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
public enum NotificationLocation
|
||||||
|
{
|
||||||
|
Nowhere,
|
||||||
|
Chat,
|
||||||
|
Toast,
|
||||||
|
Both
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NotificationType
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models.Obsolete;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
[Obsolete("Deprecated, use ServerStorage")]
|
||||||
|
public class ServerStorageV0
|
||||||
|
{
|
||||||
|
public List<Authentication> Authentications { get; set; } = [];
|
||||||
|
public bool FullPause { get; set; } = false;
|
||||||
|
public Dictionary<string, string> GidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<int, SecretKey> SecretKeys { get; set; } = [];
|
||||||
|
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public string ServerName { get; set; } = string.Empty;
|
||||||
|
public string ServerUri { get; set; } = string.Empty;
|
||||||
|
public Dictionary<string, string> UidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public ServerStorage ToV1()
|
||||||
|
{
|
||||||
|
return new ServerStorage()
|
||||||
|
{
|
||||||
|
ServerUri = ServerUri,
|
||||||
|
ServerName = ServerName,
|
||||||
|
Authentications = [.. Authentications],
|
||||||
|
FullPause = FullPause,
|
||||||
|
SecretKeys = SecretKeys.ToDictionary(p => p.Key, p => p.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
8
MareSynchronos/MareConfiguration/Models/SecretKey.cs
Normal file
8
MareSynchronos/MareConfiguration/Models/SecretKey.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class SecretKey
|
||||||
|
{
|
||||||
|
public string FriendlyName { get; set; } = string.Empty;
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
public class ServerNotesStorage
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> GidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, string> UidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
17
MareSynchronos/MareConfiguration/Models/ServerStorage.cs
Normal file
17
MareSynchronos/MareConfiguration/Models/ServerStorage.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.AspNetCore.Http.Connections;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerStorage
|
||||||
|
{
|
||||||
|
public List<Authentication> Authentications { get; set; } = [];
|
||||||
|
public bool FullPause { get; set; } = false;
|
||||||
|
public Dictionary<int, SecretKey> SecretKeys { get; set; } = [];
|
||||||
|
public string ServerName { get; set; } = string.Empty;
|
||||||
|
public string ServerUri { get; set; } = string.Empty;
|
||||||
|
public bool UseOAuth2 { get; set; } = false;
|
||||||
|
public string? OAuthToken { get; set; } = null;
|
||||||
|
public HttpTransportType HttpTransportType { get; set; } = HttpTransportType.WebSockets;
|
||||||
|
public bool ForceWebSockets { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerTagStorage
|
||||||
|
{
|
||||||
|
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
14
MareSynchronos/MareConfiguration/NotesConfigService.cs
Normal file
14
MareSynchronos/MareConfiguration/NotesConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class NotesConfigService : ConfigurationServiceBase<UidNotesConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "notes.json";
|
||||||
|
|
||||||
|
public NotesConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class PlayerPerformanceConfigService : ConfigurationServiceBase<PlayerPerformanceConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "playerperformance.json";
|
||||||
|
public PlayerPerformanceConfigService(string configDir) : base(configDir) { }
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
14
MareSynchronos/MareConfiguration/ServerConfigService.cs
Normal file
14
MareSynchronos/MareConfiguration/ServerConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class ServerConfigService : ConfigurationServiceBase<ServerConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "server.json";
|
||||||
|
|
||||||
|
public ServerConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
14
MareSynchronos/MareConfiguration/ServerTagConfigService.cs
Normal file
14
MareSynchronos/MareConfiguration/ServerTagConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class ServerTagConfigService : ConfigurationServiceBase<ServerTagConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "servertags.json";
|
||||||
|
|
||||||
|
public ServerTagConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
14
MareSynchronos/MareConfiguration/TransientConfigService.cs
Normal file
14
MareSynchronos/MareConfiguration/TransientConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class TransientConfigService : ConfigurationServiceBase<TransientConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "transient.json";
|
||||||
|
|
||||||
|
public TransientConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
12
MareSynchronos/MareConfiguration/XivDataStorageService.cs
Normal file
12
MareSynchronos/MareConfiguration/XivDataStorageService.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class XivDataStorageService : ConfigurationServiceBase<XivDataStorageConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "xivdatastorage.json";
|
||||||
|
|
||||||
|
public XivDataStorageService(string configDir) : base(configDir) { }
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
168
MareSynchronos/MarePlugin.cs
Normal file
168
MareSynchronos/MarePlugin.cs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.PlayerData.Services;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace MareSynchronos;
|
||||||
|
|
||||||
|
#pragma warning disable S125 // Sections of code should not be commented out
|
||||||
|
/*
|
||||||
|
(..,,...,,,,,+/, ,,.....,,+
|
||||||
|
..,,+++/((###%%%&&%%#(+,,.,,,+++,,,,//,,#&@@@@%+.
|
||||||
|
...+//////////(/,,,,++,.,(###((//////////,.. .,#@@%/./
|
||||||
|
,..+/////////+///,.,. ,&@@@@,,/////////////+,.. ,(##+,.
|
||||||
|
,,.+//////////++++++.. ./#%#,+/////////////+,....,/((,..,
|
||||||
|
+..////////////+++++++... .../##(,,////////////////++,,,+/(((+,
|
||||||
|
+,.+//////////////+++++++,.,,,/(((+.,////////////////////////((((#/,,
|
||||||
|
/+.+//////////++++/++++++++++,,...,++///////////////////////////((((##,
|
||||||
|
/,.////////+++++++++++++++++++++////////+++//////++/+++++//////////((((#(+,
|
||||||
|
/+.+////////+++++++++++++++++++++++++++++++++++++++++++++++++++++/////((((##+
|
||||||
|
+,.///////////////+++++++++++++++++++++++++++++++++++++++++++++++++++///((((%/
|
||||||
|
/.,/////////////////+++++++++++++++++++++++++++++++++++++++++++++++++++///+/(#+
|
||||||
|
+,./////////////////+++++++++++++++++++++++++++++++++++++++++++++++,,+++++///((,
|
||||||
|
...////////++/++++++++++++++++++++++++,,++++++++++++++++++++++++++++++++++++//(,,
|
||||||
|
..//+,+///++++++++++++++++++,,,,+++,,,,,,,,,,,,++++++++,,+++++++++++++++++++//,,+
|
||||||
|
..,++,.++++++++++++++++++++++,,,,,,,,,,,,,,,,,,,++++++++,,,,,,,,,,++++++++++...
|
||||||
|
..+++,.+++++++++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,++,..,.
|
||||||
|
..,++++,,+++++++++++,+,,,,,,,,,,..,+++++++++,,,,,,.....................,//+,+
|
||||||
|
....,+++++,.,+++++++++++,,,,,,,,.+///(((((((((((((///////////////////////(((+,,,
|
||||||
|
.....,++++++++++..,+++++++++++,,.,,,.////////(((((((((((((((////////////////////+,,/
|
||||||
|
.....,++++++++++++,..,,+++++++++,,.,../////////////////((((((((((//////////////////,,+
|
||||||
|
...,,+++++++++++++,.,,.,,,+++++++++,.,/////////////////(((//++++++++++++++//+++++++++/,,
|
||||||
|
....,++++++++++++++,.,++.,++++++++++++.,+////////////////////+++++++++++++++++++++++++///,,..
|
||||||
|
...,++++++++++++++++..+++..+++++++++++++.,//////////////////////////++++++++++++///////++++......
|
||||||
|
...++++++++++++++++++..++++.,++,++++++++++.+///////////////////////////////////////////++++++..,,,..
|
||||||
|
...+++++++++++++++++++..+++++..,+,,+++++++++.+//////////////////////////////////////////+++++++...,,,,..
|
||||||
|
..++++++++++++++++++++..++++++..,+,,+++++++++.+//////////////////////////////////////++++++++++,....,,,,..
|
||||||
|
...+++//(//////+++++++++..++++++,.,+++++++++++++,..,....,,,+++///////////////////////++++++++++++..,,,,,,,,...
|
||||||
|
..,++/(((((//////+++++++,.,++++++,,.,,,+++++++++++++++++++++++,.++////////////////////+++++++++++.....,,,,,,,...
|
||||||
|
..,//#(((((///////+++++++..++++++++++,...,++,++++++++++++++++,...+++/////////////////////+,,,+++... ....,,,,,,...
|
||||||
|
...+//(((((//////////++++++..+++++++++++++++,......,,,,++++++,,,..+++////////////////////////+,.... ...,,,,,,,...
|
||||||
|
..,//((((////////////++++++..++++++/+++++++++++++,,...,,........,+/+//////////////////////((((/+,.. ....,.,,,,..
|
||||||
|
...+/////////////////////+++..++++++/+///+++++++++++++++++++++///+/+////////////////////////(((((/+... .......,,...
|
||||||
|
..++////+++//////////////++++.+++++++++///////++++++++////////////////////////////////////+++/(((((/+.. .....,,...
|
||||||
|
.,++++++++///////////////++++..++++//////////////////////////////////////////////////////++++++/((((++.. ........
|
||||||
|
.+++++++++////////////////++++,.+++/////////////////////////////////////////////////////+++++++++/((/++..
|
||||||
|
.,++++++++//////////////////++++,.+++//////////////////////////////////////////////////+++++++++++++//+++..
|
||||||
|
.++++++++//////////////////////+/,.,+++////((((////////////////////////////////////////++++++++++++++++++...
|
||||||
|
.++++++++///////////////////////+++..++++//((((((((///////////////////////////////////++++++++++++++++++++ .
|
||||||
|
.++++++///////////////////////////++,.,+++++/(((((((((/////////////////////////////+++++++++++++++++++++++,..
|
||||||
|
.++++++////////////////////////////+++,.,+++++++/((((((((//////////////////////////++++++++++++++++++++++++..
|
||||||
|
.+++++++///////////////////++////////++++,.,+++++++++///////////+////////////////+++++++++++++++++++++++++,..
|
||||||
|
..++++++++++//////////////////////+++++++..+...,+++++++++++++++/++++++++++++++++++++++++++++++++++++++++++,...
|
||||||
|
..++++++++++++///////////////+++++++,...,,,,,.,....,,,,+++++++++++++++++++++++++++++++++++++++++++++++,,,,...
|
||||||
|
...++++++++++++++++++++++++++,,,,...,,,,,,,,,..,,++,,,.,,,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,..
|
||||||
|
...+++++++++++++++,,,,,,,,....,,,,,,,,,,,,,,,..,,++++++,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,,..
|
||||||
|
...++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,...,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,,,...
|
||||||
|
,....,++++++++++++++,,,+++++++,,,,,,,,,,,,,,,,,.,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,..
|
||||||
|
|
||||||
|
*/
|
||||||
|
#pragma warning restore S125 // Sections of code should not be commented out
|
||||||
|
|
||||||
|
public class MarePlugin : MediatorSubscriberBase, IHostedService
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly MareConfigService _mareConfigService;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private IServiceScope? _runtimeServiceScope;
|
||||||
|
private Task? _launchTask = null;
|
||||||
|
|
||||||
|
public MarePlugin(ILogger<MarePlugin> logger, MareConfigService mareConfigService,
|
||||||
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
|
DalamudUtilService dalamudUtil,
|
||||||
|
IServiceScopeFactory serviceScopeFactory, MareMediator mediator) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var version = Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
Logger.LogInformation("Launching {name} {major}.{minor}.{build}", "Mare Synchronos", version.Major, version.Minor, version.Build);
|
||||||
|
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(MarePlugin), Services.Events.EventSeverity.Informational,
|
||||||
|
$"Starting Mare Synchronos {version.Major}.{version.Minor}.{version.Build}")));
|
||||||
|
|
||||||
|
Mediator.Subscribe<SwitchToMainUiMessage>(this, (msg) => { if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager); });
|
||||||
|
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
||||||
|
Mediator.Subscribe<DalamudLogoutMessage>(this, (_) => DalamudUtilOnLogOut());
|
||||||
|
|
||||||
|
Mediator.StartQueueProcessing();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
UnsubscribeAll();
|
||||||
|
|
||||||
|
DalamudUtilOnLogOut();
|
||||||
|
|
||||||
|
Logger.LogDebug("Halting MarePlugin");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DalamudUtilOnLogIn()
|
||||||
|
{
|
||||||
|
Logger?.LogDebug("Client login");
|
||||||
|
if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DalamudUtilOnLogOut()
|
||||||
|
{
|
||||||
|
Logger?.LogDebug("Client logout");
|
||||||
|
|
||||||
|
_runtimeServiceScope?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForPlayerAndLaunchCharacterManager()
|
||||||
|
{
|
||||||
|
while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await Task.Delay(100).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger?.LogDebug("Launching Managers");
|
||||||
|
|
||||||
|
_runtimeServiceScope?.Dispose();
|
||||||
|
_runtimeServiceScope = _serviceScopeFactory.CreateScope();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<UiService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<CommandManagerService>();
|
||||||
|
if (!_mareConfigService.Current.HasValidSetup() || !_serverConfigurationManager.HasValidConfig())
|
||||||
|
{
|
||||||
|
Mediator.Publish(new SwitchToIntroUiMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<CacheCreationService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||||
|
|
||||||
|
#if !DEBUG
|
||||||
|
if (_mareConfigService.Current.LogLevel != LogLevel.Information)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("Abnormal Log Level",
|
||||||
|
$"Your log level is set to '{_mareConfigService.Current.LogLevel}' which is not recommended for normal usage. Set it to '{LogLevel.Information}' in \"Mare Settings -> Debug\" unless instructed otherwise.",
|
||||||
|
MareConfiguration.Models.NotificationType.Error, TimeSpan.FromSeconds(15000)));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger?.LogCritical(ex, "Error during launch of managers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
MareSynchronos/MareSynchronos.csproj
Normal file
80
MareSynchronos/MareSynchronos.csproj
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Authors></Authors>
|
||||||
|
<Company></Company>
|
||||||
|
<Version>1.11.0</Version>
|
||||||
|
<Description></Description>
|
||||||
|
<Copyright></Copyright>
|
||||||
|
<PackageProjectUrl>https://github.com/Penumbra-Sync/client</PackageProjectUrl>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<CopyLocalLockfileAssemblies>true</CopyLocalLockfileAssemblies>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="PlayerData\Export\**" />
|
||||||
|
<EmbeddedResource Remove="PlayerData\Export\**" />
|
||||||
|
<None Remove="PlayerData\Export\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DalamudPackager" Version="12.0.0" />
|
||||||
|
<PackageReference Include="Downloader" Version="3.3.4" />
|
||||||
|
<PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" />
|
||||||
|
<PackageReference Include="Meziantou.Analyzer" Version="2.0.189">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||||
|
<PackageReference Include="Glamourer.Api" Version="2.4.0" />
|
||||||
|
<PackageReference Include="NReco.Logging.File" Version="1.2.2" />
|
||||||
|
<PackageReference Include="Penumbra.Api" Version="5.6.0" />
|
||||||
|
<PackageReference Include="Penumbra.String" Version="1.0.5" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||||
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ"))</SourceRevisionId>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="CheapLoc">
|
||||||
|
<HintPath>$(DalamudLibPath)CheapLoc.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="images\icon.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<EmbeddedResource Include="Localization\de.json" />
|
||||||
|
<EmbeddedResource Include="Localization\fr.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
13
MareSynchronos/MareSynchronos.json
Normal file
13
MareSynchronos/MareSynchronos.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"Author": "darkarchon",
|
||||||
|
"Name": "Mare Synchronos",
|
||||||
|
"Punchline": "Let others see you as you see yourself.",
|
||||||
|
"Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.",
|
||||||
|
"InternalName": "mareSynchronos",
|
||||||
|
"ApplicableVersion": "any",
|
||||||
|
"Tags": [
|
||||||
|
"customization"
|
||||||
|
],
|
||||||
|
"IconUrl": "https://raw.githubusercontent.com/Penumbra-Sync/client/main/MareSynchronos/images/logo.png",
|
||||||
|
"RepoUrl": "https://github.com/Penumbra-Sync/client"
|
||||||
|
}
|
||||||
76
MareSynchronos/PlayerData/Data/CharacterData.cs
Normal file
76
MareSynchronos/PlayerData/Data/CharacterData.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Data;
|
||||||
|
|
||||||
|
public class CharacterData
|
||||||
|
{
|
||||||
|
public Dictionary<ObjectKind, string> CustomizePlusScale { get; set; } = [];
|
||||||
|
public Dictionary<ObjectKind, HashSet<FileReplacement>> FileReplacements { get; set; } = [];
|
||||||
|
public Dictionary<ObjectKind, string> GlamourerString { get; set; } = [];
|
||||||
|
public string HeelsData { get; set; } = string.Empty;
|
||||||
|
public string HonorificData { get; set; } = string.Empty;
|
||||||
|
public string ManipulationString { get; set; } = string.Empty;
|
||||||
|
public string MoodlesData { get; set; } = string.Empty;
|
||||||
|
public string PetNamesData { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public void SetFragment(ObjectKind kind, CharacterDataFragment? fragment)
|
||||||
|
{
|
||||||
|
if (kind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
var playerFragment = (fragment as CharacterDataFragmentPlayer);
|
||||||
|
HeelsData = playerFragment?.HeelsData ?? string.Empty;
|
||||||
|
HonorificData = playerFragment?.HonorificData ?? string.Empty;
|
||||||
|
ManipulationString = playerFragment?.ManipulationString ?? string.Empty;
|
||||||
|
MoodlesData = playerFragment?.MoodlesData ?? string.Empty;
|
||||||
|
PetNamesData = playerFragment?.PetNamesData ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fragment is null)
|
||||||
|
{
|
||||||
|
CustomizePlusScale.Remove(kind);
|
||||||
|
FileReplacements.Remove(kind);
|
||||||
|
GlamourerString.Remove(kind);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CustomizePlusScale[kind] = fragment.CustomizePlusScale;
|
||||||
|
FileReplacements[kind] = fragment.FileReplacements;
|
||||||
|
GlamourerString[kind] = fragment.GlamourerString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public API.Data.CharacterData ToAPI()
|
||||||
|
{
|
||||||
|
Dictionary<ObjectKind, List<FileReplacementData>> fileReplacements =
|
||||||
|
FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap)
|
||||||
|
.GroupBy(f => f.Hash, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(g =>
|
||||||
|
{
|
||||||
|
return new FileReplacementData()
|
||||||
|
{
|
||||||
|
GamePaths = g.SelectMany(f => f.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
|
||||||
|
Hash = g.First().Hash,
|
||||||
|
};
|
||||||
|
}).ToList());
|
||||||
|
|
||||||
|
foreach (var item in FileReplacements)
|
||||||
|
{
|
||||||
|
var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto());
|
||||||
|
fileReplacements[item.Key].AddRange(fileSwapsToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new API.Data.CharacterData()
|
||||||
|
{
|
||||||
|
FileReplacements = fileReplacements,
|
||||||
|
GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value),
|
||||||
|
ManipulationData = ManipulationString,
|
||||||
|
HeelsData = HeelsData,
|
||||||
|
CustomizePlusData = CustomizePlusScale.ToDictionary(d => d.Key, d => d.Value),
|
||||||
|
HonorificData = HonorificData,
|
||||||
|
MoodlesData = MoodlesData,
|
||||||
|
PetNamesData = PetNamesData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
8
MareSynchronos/PlayerData/Data/CharacterDataFragment.cs
Normal file
8
MareSynchronos/PlayerData/Data/CharacterDataFragment.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.PlayerData.Data;
|
||||||
|
|
||||||
|
public class CharacterDataFragment
|
||||||
|
{
|
||||||
|
public string CustomizePlusScale { get; set; } = string.Empty;
|
||||||
|
public HashSet<FileReplacement> FileReplacements { get; set; } = [];
|
||||||
|
public string GlamourerString { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace MareSynchronos.PlayerData.Data;
|
||||||
|
|
||||||
|
public class CharacterDataFragmentPlayer : CharacterDataFragment
|
||||||
|
{
|
||||||
|
public string HeelsData { get; set; } = string.Empty;
|
||||||
|
public string HonorificData { get; set; } = string.Empty;
|
||||||
|
public string ManipulationString { get; set; } = string.Empty;
|
||||||
|
public string MoodlesData { get; set; } = string.Empty;
|
||||||
|
public string PetNamesData { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
40
MareSynchronos/PlayerData/Data/FileReplacement.cs
Normal file
40
MareSynchronos/PlayerData/Data/FileReplacement.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Data;
|
||||||
|
|
||||||
|
public partial class FileReplacement
|
||||||
|
{
|
||||||
|
public FileReplacement(string[] gamePaths, string filePath)
|
||||||
|
{
|
||||||
|
GamePaths = gamePaths.Select(g => g.Replace('\\', '/').ToLowerInvariant()).ToHashSet(StringComparer.Ordinal);
|
||||||
|
ResolvedPath = filePath.Replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<string> GamePaths { get; init; }
|
||||||
|
|
||||||
|
public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
public string Hash { get; set; } = string.Empty;
|
||||||
|
public bool IsFileSwap => !LocalPathRegex().IsMatch(ResolvedPath) && GamePaths.All(p => !LocalPathRegex().IsMatch(p));
|
||||||
|
public string ResolvedPath { get; init; }
|
||||||
|
|
||||||
|
public FileReplacementData ToFileReplacementDto()
|
||||||
|
{
|
||||||
|
return new FileReplacementData
|
||||||
|
{
|
||||||
|
GamePaths = [.. GamePaths],
|
||||||
|
Hash = Hash,
|
||||||
|
FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"HasReplacement:{HasFileReplacement},IsFileSwap:{IsFileSwap} - {string.Join(",", GamePaths)} => {ResolvedPath}";
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript)]
|
||||||
|
private static partial Regex LocalPathRegex();
|
||||||
|
}
|
||||||
47
MareSynchronos/PlayerData/Data/FileReplacementComparer.cs
Normal file
47
MareSynchronos/PlayerData/Data/FileReplacementComparer.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
namespace MareSynchronos.PlayerData.Data;
|
||||||
|
|
||||||
|
public class FileReplacementComparer : IEqualityComparer<FileReplacement>
|
||||||
|
{
|
||||||
|
private static readonly FileReplacementComparer _instance = new();
|
||||||
|
|
||||||
|
private FileReplacementComparer()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public static FileReplacementComparer Instance => _instance;
|
||||||
|
|
||||||
|
public bool Equals(FileReplacement? x, FileReplacement? y)
|
||||||
|
{
|
||||||
|
if (x == null || y == null) return false;
|
||||||
|
return x.ResolvedPath.Equals(y.ResolvedPath) && CompareLists(x.GamePaths, y.GamePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(FileReplacement obj)
|
||||||
|
{
|
||||||
|
return HashCode.Combine(obj.ResolvedPath.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CompareLists(HashSet<string> list1, HashSet<string> list2)
|
||||||
|
{
|
||||||
|
if (list1.Count != list2.Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (int i = 0; i < list1.Count; i++)
|
||||||
|
{
|
||||||
|
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source) where T : notnull
|
||||||
|
{
|
||||||
|
int hash = 0;
|
||||||
|
foreach (T element in source)
|
||||||
|
{
|
||||||
|
hash = unchecked(hash +
|
||||||
|
EqualityComparer<T>.Default.GetHashCode(element));
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Data;
|
||||||
|
|
||||||
|
public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData>
|
||||||
|
{
|
||||||
|
private static readonly FileReplacementDataComparer _instance = new();
|
||||||
|
|
||||||
|
private FileReplacementDataComparer()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public static FileReplacementDataComparer Instance => _instance;
|
||||||
|
|
||||||
|
public bool Equals(FileReplacementData? x, FileReplacementData? y)
|
||||||
|
{
|
||||||
|
if (x == null || y == null) return false;
|
||||||
|
return x.Hash.Equals(y.Hash) && CompareHashSets(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(FileReplacementData obj)
|
||||||
|
{
|
||||||
|
return HashCode.Combine(obj.Hash.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths), StringComparer.Ordinal.GetHashCode(obj.FileSwapPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CompareHashSets(HashSet<string> list1, HashSet<string> list2)
|
||||||
|
{
|
||||||
|
if (list1.Count != list2.Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (int i = 0; i < list1.Count; i++)
|
||||||
|
{
|
||||||
|
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source) where T : notnull
|
||||||
|
{
|
||||||
|
int hash = 0;
|
||||||
|
foreach (T element in source)
|
||||||
|
{
|
||||||
|
hash = unchecked(hash +
|
||||||
|
EqualityComparer<T>.Default.GetHashCode(element));
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
MareSynchronos/PlayerData/Data/PlayerChanges.cs
Normal file
14
MareSynchronos/PlayerData/Data/PlayerChanges.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace MareSynchronos.PlayerData.Pairs;
|
||||||
|
|
||||||
|
public enum PlayerChanges
|
||||||
|
{
|
||||||
|
ModFiles = 1,
|
||||||
|
ModManip = 2,
|
||||||
|
Glamourer = 3,
|
||||||
|
Customize = 4,
|
||||||
|
Heels = 5,
|
||||||
|
Honorific = 7,
|
||||||
|
ForcedRedraw = 8,
|
||||||
|
Moodles = 9,
|
||||||
|
PetNames = 10,
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI.Files;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
|
public class FileDownloadManagerFactory
|
||||||
|
{
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly FileCompactor _fileCompactor;
|
||||||
|
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
|
||||||
|
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_fileCompactor = fileCompactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileDownloadManager Create()
|
||||||
|
{
|
||||||
|
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
|
public class GameObjectHandlerFactory
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
|
|
||||||
|
public GameObjectHandlerFactory(ILoggerFactory loggerFactory, PerformanceCollectorService performanceCollectorService, MareMediator mareMediator,
|
||||||
|
DalamudUtilService dalamudUtilService)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_performanceCollectorService = performanceCollectorService;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
||||||
|
{
|
||||||
|
return await _dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(_loggerFactory.CreateLogger<GameObjectHandler>(),
|
||||||
|
_performanceCollectorService, _mareMediator, _dalamudUtilService, objectKind, getAddressFunc, isWatched)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
MareSynchronos/PlayerData/Factories/PairFactory.cs
Normal file
35
MareSynchronos/PlayerData/Factories/PairFactory.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
|
public class PairFactory
|
||||||
|
{
|
||||||
|
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
|
public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory,
|
||||||
|
MareMediator mareMediator, ServerConfigurationManager serverConfigurationManager)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_cachedPlayerFactory = cachedPlayerFactory;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair Create(UserFullPairDto userPairDto)
|
||||||
|
{
|
||||||
|
return new Pair(_loggerFactory.CreateLogger<Pair>(), userPairDto, _cachedPlayerFactory, _mareMediator, _serverConfigurationManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair Create(UserPairDto userPairDto)
|
||||||
|
{
|
||||||
|
return new Pair(_loggerFactory.CreateLogger<Pair>(), new(userPairDto.User, userPairDto.IndividualPairStatus, [], userPairDto.OwnPermissions, userPairDto.OtherPermissions),
|
||||||
|
_cachedPlayerFactory, _mareMediator, _serverConfigurationManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs
Normal file
52
MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
|
public class PairHandlerFactory
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
||||||
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
|
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
|
|
||||||
|
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
||||||
|
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||||
|
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||||
|
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
||||||
|
ServerConfigurationManager serverConfigManager)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
|
_hostApplicationLifetime = hostApplicationLifetime;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_serverConfigManager = serverConfigManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairHandler Create(Pair pair)
|
||||||
|
{
|
||||||
|
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
||||||
|
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||||
|
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
376
MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs
Normal file
376
MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Data;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using CharacterData = MareSynchronos.PlayerData.Data.CharacterData;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
|
public class PlayerDataFactory
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly ILogger<PlayerDataFactory> _logger;
|
||||||
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly TransientResourceManager _transientResourceManager;
|
||||||
|
|
||||||
|
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
|
||||||
|
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
|
||||||
|
PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_transientResourceManager = transientResourceManager;
|
||||||
|
_fileCacheManager = fileReplacementFactory;
|
||||||
|
_performanceCollector = performanceCollector;
|
||||||
|
_modelAnalyzer = modelAnalyzer;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!_ipcManager.Initialized)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerRelatedObject == null) return null;
|
||||||
|
|
||||||
|
bool pointerIsZero = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pointerIsZero = playerRelatedObject.Address == IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pointerIsZero = await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
pointerIsZero = true;
|
||||||
|
_logger.LogDebug("NullRef for {object}", playerRelatedObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not create data for {object}", playerRelatedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointerIsZero)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
|
||||||
|
{
|
||||||
|
return await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false);
|
||||||
|
}).ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Cancelled creating Character data for {object}", playerRelatedObject);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Failed to create {object} data", playerRelatedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||||
|
{
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||||
|
{
|
||||||
|
return ((Character*)playerPointer)->GameObject.DrawObject == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var objectKind = playerRelatedObject.ObjectKind;
|
||||||
|
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
||||||
|
|
||||||
|
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||||
|
|
||||||
|
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||||
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
||||||
|
int totalWaitTime = 10000;
|
||||||
|
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Character is null but it shouldn't be, waiting");
|
||||||
|
await Task.Delay(50, ct).ConfigureAwait(false);
|
||||||
|
totalWaitTime -= 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Dictionary<string, List<ushort>>? boneIndices =
|
||||||
|
objectKind != ObjectKind.Player
|
||||||
|
? null
|
||||||
|
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
DateTime start = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// penumbra call, it's currently broken
|
||||||
|
Dictionary<string, HashSet<string>>? resolvedPaths;
|
||||||
|
|
||||||
|
resolvedPaths = (await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false));
|
||||||
|
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data");
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
fragment.FileReplacements =
|
||||||
|
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
|
||||||
|
.Where(p => p.HasFileReplacement).ToHashSet();
|
||||||
|
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogDebug("== Static Replacements ==");
|
||||||
|
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
|
||||||
|
// or we get into redraw city for every change and nothing works properly
|
||||||
|
if (objectKind == ObjectKind.Pet)
|
||||||
|
{
|
||||||
|
foreach (var item in fragment.FileReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
||||||
|
{
|
||||||
|
if (_transientResourceManager.AddTransientResource(objectKind, item))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Marking static {item} for Pet as transient", item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("Clearing {count} Static Replacements for Pet", fragment.FileReplacements.Count);
|
||||||
|
fragment.FileReplacements.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
|
||||||
|
|
||||||
|
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
|
||||||
|
_transientResourceManager.ClearTransientPaths(objectKind, fragment.FileReplacements.SelectMany(c => c.GamePaths).ToList());
|
||||||
|
|
||||||
|
// get all remaining paths and resolve them
|
||||||
|
var transientPaths = ManageSemiTransientData(objectKind);
|
||||||
|
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_logger.LogDebug("== Transient Replacements ==");
|
||||||
|
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
|
fragment.FileReplacements.Add(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||||
|
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// make sure we only return data that actually has file replacements
|
||||||
|
fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
|
||||||
|
|
||||||
|
// gather up data from ipc
|
||||||
|
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
|
||||||
|
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
|
||||||
|
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
|
||||||
|
Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
|
||||||
|
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
|
||||||
|
var customizeScale = await getCustomizeData.ConfigureAwait(false);
|
||||||
|
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
|
||||||
|
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
var playerFragment = (fragment as CharacterDataFragmentPlayer)!;
|
||||||
|
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
|
||||||
|
|
||||||
|
playerFragment!.HonorificData = await getHonorificTitle.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
|
||||||
|
|
||||||
|
playerFragment!.HeelsData = await getHeelsOffset.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
|
||||||
|
|
||||||
|
playerFragment!.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
|
||||||
|
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
|
||||||
|
|
||||||
|
playerFragment!.PetNamesData = _ipcManager.PetNames.GetLocalNames();
|
||||||
|
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
|
||||||
|
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
|
||||||
|
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray());
|
||||||
|
foreach (var file in toCompute)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
|
||||||
|
}
|
||||||
|
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
|
||||||
|
if (removed > 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Removed {amount} of invalid files", removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException e)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(e, "Cancelled during player animation verification");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Failed to verify player animations, continuing without further verification");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
|
||||||
|
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterDataFragmentPlayer fragment, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (boneIndices == null) return;
|
||||||
|
|
||||||
|
foreach (var kvp in boneIndices)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (boneIndices.All(u => u.Value.Count == 0)) return;
|
||||||
|
|
||||||
|
int noValidationFailed = 0;
|
||||||
|
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
|
||||||
|
bool validationFailed = false;
|
||||||
|
if (skeletonIndices != null)
|
||||||
|
{
|
||||||
|
// 105 is the maximum vanilla skellington spoopy bone index
|
||||||
|
if (skeletonIndices.All(k => k.Value.Max() <= 105))
|
||||||
|
{
|
||||||
|
_logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||||
|
|
||||||
|
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
|
||||||
|
{
|
||||||
|
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
||||||
|
file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
|
||||||
|
validationFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationFailed)
|
||||||
|
{
|
||||||
|
noValidationFailed++;
|
||||||
|
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
|
||||||
|
fragment.FileReplacements.Remove(file);
|
||||||
|
foreach (var gamePath in file.GamePaths)
|
||||||
|
{
|
||||||
|
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noValidationFailed > 0)
|
||||||
|
{
|
||||||
|
_mareMediator.Publish(new NotificationMessage("Invalid Skeleton Setup",
|
||||||
|
$"Your client is attempting to send {noValidationFailed} animation files with invalid bone data. Those animation files have been removed from your sent data. " +
|
||||||
|
$"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).",
|
||||||
|
NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
|
||||||
|
{
|
||||||
|
var forwardPaths = forwardResolve.ToArray();
|
||||||
|
var reversePaths = reverseResolve.ToArray();
|
||||||
|
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
|
||||||
|
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||||
|
for (int i = 0; i < forwardPaths.Length; i++)
|
||||||
|
{
|
||||||
|
var filePath = forward[i].ToLowerInvariant();
|
||||||
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
|
{
|
||||||
|
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < reversePaths.Length; i++)
|
||||||
|
{
|
||||||
|
var filePath = reversePaths[i].ToLowerInvariant();
|
||||||
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
|
{
|
||||||
|
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<string> ManageSemiTransientData(ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
_transientResourceManager.PersistTransientResources(objectKind);
|
||||||
|
|
||||||
|
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
|
||||||
|
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
|
||||||
|
{
|
||||||
|
pathsToResolve.Add(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathsToResolve;
|
||||||
|
}
|
||||||
|
}
|
||||||
429
MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs
Normal file
429
MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||||
|
using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Handlers;
|
||||||
|
|
||||||
|
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly Func<IntPtr> _getAddress;
|
||||||
|
private readonly bool _isOwnedObject;
|
||||||
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private byte _classJob = 0;
|
||||||
|
private Task? _delayedZoningTask;
|
||||||
|
private bool _haltProcessing = false;
|
||||||
|
private CancellationTokenSource _zoningCts = new();
|
||||||
|
|
||||||
|
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
|
||||||
|
MareMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool ownedObject = true) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_performanceCollector = performanceCollector;
|
||||||
|
ObjectKind = objectKind;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_getAddress = () =>
|
||||||
|
{
|
||||||
|
_dalamudUtil.EnsureIsOnFramework();
|
||||||
|
return getAddress.Invoke();
|
||||||
|
};
|
||||||
|
_isOwnedObject = ownedObject;
|
||||||
|
Name = string.Empty;
|
||||||
|
|
||||||
|
if (ownedObject)
|
||||||
|
{
|
||||||
|
Mediator.Subscribe<TransientResourceChangedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_delayedZoningTask?.IsCompleted ?? true)
|
||||||
|
{
|
||||||
|
if (msg.Address != Address) return;
|
||||||
|
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||||
|
|
||||||
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||||
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||||
|
|
||||||
|
Mediator.Subscribe<CutsceneStartMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
_haltProcessing = true;
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
_haltProcessing = false;
|
||||||
|
ZoneSwitchEnd();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.Address == Address)
|
||||||
|
{
|
||||||
|
_haltProcessing = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.Address == Address)
|
||||||
|
{
|
||||||
|
_haltProcessing = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
||||||
|
|
||||||
|
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DrawCondition
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
ObjectZero,
|
||||||
|
DrawObjectZero,
|
||||||
|
RenderFlags,
|
||||||
|
ModelInSlotLoaded,
|
||||||
|
ModelFilesInSlotLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntPtr Address { get; private set; }
|
||||||
|
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
|
||||||
|
public byte Gender { get; private set; }
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public ObjectKind ObjectKind { get; }
|
||||||
|
public byte RaceId { get; private set; }
|
||||||
|
public byte TribeId { get; private set; }
|
||||||
|
private byte[] CustomizeData { get; set; } = new byte[26];
|
||||||
|
private IntPtr DrawObjectAddress { get; set; }
|
||||||
|
private byte[] EquipSlotData { get; set; } = new byte[40];
|
||||||
|
private ushort[] MainHandData { get; set; } = new ushort[3];
|
||||||
|
private ushort[] OffHandData { get; set; } = new ushort[3];
|
||||||
|
|
||||||
|
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
||||||
|
{
|
||||||
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
if (_haltProcessing) CheckAndUpdateObject();
|
||||||
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
|
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||||
|
{
|
||||||
|
act.Invoke(chara);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompareNameAndThrow(string name)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player name not equal to requested name, pointer invalid");
|
||||||
|
}
|
||||||
|
if (Address == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player pointer is zero, pointer invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
|
||||||
|
{
|
||||||
|
return _dalamudUtil.CreateGameObject(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
Address = IntPtr.Zero;
|
||||||
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
|
_haltProcessing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||||
|
{
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var owned = _isOwnedObject ? "Self" : "Other";
|
||||||
|
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void CheckAndUpdateObject()
|
||||||
|
{
|
||||||
|
var prevAddr = Address;
|
||||||
|
var prevDrawObj = DrawObjectAddress;
|
||||||
|
|
||||||
|
Address = _getAddress();
|
||||||
|
if (Address != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject;
|
||||||
|
DrawObjectAddress = drawObjAddr;
|
||||||
|
CurrentDrawCondition = DrawCondition.None;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
|
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
||||||
|
|
||||||
|
if (_haltProcessing) return;
|
||||||
|
|
||||||
|
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||||
|
bool addrDiff = Address != prevAddr;
|
||||||
|
|
||||||
|
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
var chara = (Character*)Address;
|
||||||
|
var name = chara->GameObject.NameString;
|
||||||
|
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal);
|
||||||
|
if (nameChange)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
bool equipDiff = false;
|
||||||
|
|
||||||
|
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
||||||
|
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
||||||
|
{
|
||||||
|
var classJob = chara->CharacterData.ClassJob;
|
||||||
|
if (classJob != _classJob)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||||
|
_classJob = classJob;
|
||||||
|
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)DrawObjectAddress)->Head);
|
||||||
|
|
||||||
|
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||||
|
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||||
|
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||||
|
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||||
|
|
||||||
|
if (equipDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
||||||
|
if (equipDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool customizeDiff = false;
|
||||||
|
|
||||||
|
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
||||||
|
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
||||||
|
{
|
||||||
|
var gender = ((Human*)DrawObjectAddress)->Customize.Sex;
|
||||||
|
var raceId = ((Human*)DrawObjectAddress)->Customize.Race;
|
||||||
|
var tribeId = ((Human*)DrawObjectAddress)->Customize.Tribe;
|
||||||
|
|
||||||
|
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||||
|
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
|
||||||
|
Gender = gender;
|
||||||
|
RaceId = raceId;
|
||||||
|
TribeId = tribeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data);
|
||||||
|
if (customizeDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
||||||
|
if (customizeDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
||||||
|
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (addrDiff || drawObjDiff)
|
||||||
|
{
|
||||||
|
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||||
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
|
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
||||||
|
{
|
||||||
|
bool hasChanges = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < customizeData.Length; i++)
|
||||||
|
{
|
||||||
|
var data = customizeData[i];
|
||||||
|
if (CustomizeData[i] != data)
|
||||||
|
{
|
||||||
|
CustomizeData[i] = data;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool CompareAndUpdateEquipByteData(byte* equipSlotData)
|
||||||
|
{
|
||||||
|
bool hasChanges = false;
|
||||||
|
for (int i = 0; i < EquipSlotData.Length; i++)
|
||||||
|
{
|
||||||
|
var data = equipSlotData[i];
|
||||||
|
if (EquipSlotData[i] != data)
|
||||||
|
{
|
||||||
|
EquipSlotData[i] = data;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
||||||
|
{
|
||||||
|
if ((nint)weapon == nint.Zero) return false;
|
||||||
|
bool hasChanges = false;
|
||||||
|
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
||||||
|
MainHandData[0] = weapon->ModelSetId;
|
||||||
|
hasChanges |= weapon->Variant != MainHandData[1];
|
||||||
|
MainHandData[1] = weapon->Variant;
|
||||||
|
hasChanges |= weapon->SecondaryId != MainHandData[2];
|
||||||
|
MainHandData[2] = weapon->SecondaryId;
|
||||||
|
return hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
||||||
|
{
|
||||||
|
if ((nint)weapon == nint.Zero) return false;
|
||||||
|
bool hasChanges = false;
|
||||||
|
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
||||||
|
OffHandData[0] = weapon->ModelSetId;
|
||||||
|
hasChanges |= weapon->Variant != OffHandData[1];
|
||||||
|
OffHandData[1] = weapon->Variant;
|
||||||
|
hasChanges |= weapon->SecondaryId != OffHandData[2];
|
||||||
|
OffHandData[2] = weapon->SecondaryId;
|
||||||
|
return hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FrameworkUpdate()
|
||||||
|
{
|
||||||
|
if (!_delayedZoningTask?.IsCompleted ?? false) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}"
|
||||||
|
+ $"+{Address.ToString("X")}", CheckAndUpdateObject);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsBeingDrawn()
|
||||||
|
{
|
||||||
|
if (_haltProcessing) CheckAndUpdateObject();
|
||||||
|
|
||||||
|
if (_dalamudUtil.IsAnythingDrawing)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, Global draw block", this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, CurrentDrawCondition);
|
||||||
|
return CurrentDrawCondition != DrawCondition.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||||
|
{
|
||||||
|
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||||
|
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
|
||||||
|
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0;
|
||||||
|
if (renderFlags) return DrawCondition.RenderFlags;
|
||||||
|
|
||||||
|
if (ObjectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
var modelInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelInSlotLoaded != 0);
|
||||||
|
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
|
||||||
|
var modelFilesInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelFilesInSlotLoaded != 0);
|
||||||
|
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DrawCondition.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ZoneSwitchEnd()
|
||||||
|
{
|
||||||
|
if (!_isOwnedObject) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_zoningCts?.CancelAfter(2500);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Zoning CTS cancel issue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ZoneSwitchStart()
|
||||||
|
{
|
||||||
|
if (!_isOwnedObject) return;
|
||||||
|
|
||||||
|
_zoningCts = new();
|
||||||
|
Logger.LogDebug("[{obj}] Starting Delay After Zoning", this);
|
||||||
|
_delayedZoningTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(120), _zoningCts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore cancelled
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
||||||
|
_zoningCts.Dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
718
MareSynchronos/PlayerData/Handlers/PairHandler.cs
Normal file
718
MareSynchronos/PlayerData/Handlers/PairHandler.cs
Normal file
@@ -0,0 +1,718 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Events;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using MareSynchronos.WebAPI.Files;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Handlers;
|
||||||
|
|
||||||
|
public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||||
|
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly FileDownloadManager _downloadManager;
|
||||||
|
private readonly FileCacheManager _fileDbManager;
|
||||||
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
|
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||||
|
private Guid _applicationId;
|
||||||
|
private Task? _applicationTask;
|
||||||
|
private CharacterData? _cachedData = null;
|
||||||
|
private GameObjectHandler? _charaHandler;
|
||||||
|
private readonly Dictionary<ObjectKind, Guid?> _customizeIds = [];
|
||||||
|
private CombatData? _dataReceivedInDowntime;
|
||||||
|
private CancellationTokenSource? _downloadCancellationTokenSource = new();
|
||||||
|
private bool _forceApplyMods = false;
|
||||||
|
private bool _isVisible;
|
||||||
|
private Guid _penumbraCollection;
|
||||||
|
private bool _redrawOnNextApplication = false;
|
||||||
|
|
||||||
|
public PairHandler(ILogger<PairHandler> logger, Pair pair,
|
||||||
|
GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||||
|
IpcManager ipcManager, FileDownloadManager transferManager,
|
||||||
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
|
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||||
|
FileCacheManager fileDbManager, MareMediator mediator,
|
||||||
|
PlayerPerformanceService playerPerformanceService,
|
||||||
|
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
Pair = pair;
|
||||||
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_downloadManager = transferManager;
|
||||||
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_lifetime = lifetime;
|
||||||
|
_fileDbManager = fileDbManager;
|
||||||
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_serverConfigManager = serverConfigManager;
|
||||||
|
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||||
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
|
_charaHandler?.Invalidate();
|
||||||
|
IsVisible = false;
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
if (!IsVisible && _charaHandler != null)
|
||||||
|
{
|
||||||
|
PlayerName = string.Empty;
|
||||||
|
_charaHandler.Dispose();
|
||||||
|
_charaHandler = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.GameObjectHandler == _charaHandler)
|
||||||
|
{
|
||||||
|
_redrawOnNextApplication = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<CombatOrPerformanceEndMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (IsVisible && _dataReceivedInDowntime != null)
|
||||||
|
{
|
||||||
|
ApplyCharacterData(_dataReceivedInDowntime.ApplicationId,
|
||||||
|
_dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced);
|
||||||
|
_dataReceivedInDowntime = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<CombatOrPerformanceStartMessage>(this, _ =>
|
||||||
|
{
|
||||||
|
_dataReceivedInDowntime = null;
|
||||||
|
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||||
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||||
|
});
|
||||||
|
|
||||||
|
LastAppliedDataBytes = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsVisible
|
||||||
|
{
|
||||||
|
get => _isVisible;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (_isVisible != value)
|
||||||
|
{
|
||||||
|
_isVisible = value;
|
||||||
|
string text = "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible");
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler),
|
||||||
|
EventSeverity.Informational, text)));
|
||||||
|
Mediator.Publish(new RefreshUiMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long LastAppliedDataBytes { get; private set; }
|
||||||
|
public Pair Pair { get; private set; }
|
||||||
|
public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero;
|
||||||
|
public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero
|
||||||
|
? uint.MaxValue
|
||||||
|
: ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_charaHandler!.Address)->EntityId;
|
||||||
|
public string? PlayerName { get; private set; }
|
||||||
|
public string PlayerNameHash => Pair.Ident;
|
||||||
|
|
||||||
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||||
|
{
|
||||||
|
if (_dalamudUtil.IsInCombatOrPerforming)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||||
|
"Cannot apply character data: you are in combat or performing music, deferring application")));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase);
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(isUploading: false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_charaHandler == null || (PlayerCharacter == IntPtr.Zero))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||||
|
"Cannot apply character data: Receiving Player is in an invalid state, deferring application")));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}",
|
||||||
|
applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero);
|
||||||
|
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
|
||||||
|
this, forceApplyCustomization, forceApplyMods: false)
|
||||||
|
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
|
||||||
|
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
|
||||||
|
_cachedData = characterData;
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetUploading(isUploading: false);
|
||||||
|
|
||||||
|
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, this, forceApplyCustomization, _forceApplyMods);
|
||||||
|
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA");
|
||||||
|
|
||||||
|
if (string.Equals(characterData.DataHash.Value, _cachedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal) && !forceApplyCustomization) return;
|
||||||
|
|
||||||
|
if (_dalamudUtil.IsInCutscene || _dalamudUtil.IsInGpose || !_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||||
|
"Cannot apply character data: you are in GPose, a Cutscene or Penumbra/Glamourer is not available")));
|
||||||
|
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while in cutscene/gpose or Penumbra/Glamourer unavailable, returning", applicationBase, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||||
|
"Applying Character Data")));
|
||||||
|
|
||||||
|
_forceApplyMods |= forceApplyCustomization;
|
||||||
|
|
||||||
|
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
|
||||||
|
|
||||||
|
if (_charaHandler != null && _forceApplyMods)
|
||||||
|
{
|
||||||
|
_forceApplyMods = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||||
|
{
|
||||||
|
player.Add(PlayerChanges.ForcedRedraw);
|
||||||
|
_redrawOnNextApplication = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||||
|
{
|
||||||
|
_pluginWarningNotificationManager.NotifyForMissingPlugins(Pair.UserData, PlayerName!, playerChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, this);
|
||||||
|
|
||||||
|
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Pair == null
|
||||||
|
? base.ToString() ?? string.Empty
|
||||||
|
: Pair.UserData.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != nint.Zero ? "HasChar" : "NoChar");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetUploading(bool isUploading = true)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Setting {this} uploading {uploading}", this, isUploading);
|
||||||
|
if (_charaHandler != null)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
SetUploading(isUploading: false);
|
||||||
|
var name = PlayerName;
|
||||||
|
Logger.LogDebug("Disposing {name} ({user})", name, Pair);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Guid applicationId = Guid.NewGuid();
|
||||||
|
_applicationCancellationTokenSource?.CancelDispose();
|
||||||
|
_applicationCancellationTokenSource = null;
|
||||||
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
|
_downloadCancellationTokenSource = null;
|
||||||
|
_downloadManager.Dispose();
|
||||||
|
_charaHandler?.Dispose();
|
||||||
|
_charaHandler = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair);
|
||||||
|
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||||
|
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).GetAwaiter().GetResult();
|
||||||
|
if (!IsVisible)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||||
|
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
cts.CancelAfter(TimeSpan.FromSeconds(60));
|
||||||
|
|
||||||
|
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}", applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
|
||||||
|
|
||||||
|
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error on disposal of {name}", name);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
PlayerName = null;
|
||||||
|
_cachedData = null;
|
||||||
|
Logger.LogDebug("Disposing {name} complete", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (PlayerCharacter == nint.Zero) return;
|
||||||
|
var ptr = PlayerCharacter;
|
||||||
|
|
||||||
|
var handler = changes.Key switch
|
||||||
|
{
|
||||||
|
ObjectKind.Player => _charaHandler!,
|
||||||
|
ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanionPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||||
|
ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMountPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||||
|
ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPetPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||||
|
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (handler.Address == nint.Zero)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||||
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||||
|
switch (change)
|
||||||
|
{
|
||||||
|
case PlayerChanges.Customize:
|
||||||
|
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
||||||
|
{
|
||||||
|
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||||
|
{
|
||||||
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||||
|
_customizeIds.Remove(changes.Key);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Heels:
|
||||||
|
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Honorific:
|
||||||
|
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Glamourer:
|
||||||
|
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
||||||
|
{
|
||||||
|
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Moodles:
|
||||||
|
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.PetNames:
|
||||||
|
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.ForcedRedraw:
|
||||||
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (handler != _charaHandler) handler.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
||||||
|
{
|
||||||
|
if (!updatedData.Any())
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles));
|
||||||
|
var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip));
|
||||||
|
|
||||||
|
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
|
var downloadToken = _downloadCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task? _pairDownloadTask;
|
||||||
|
|
||||||
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||||
|
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||||
|
{
|
||||||
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||||
|
|
||||||
|
if (updateModdedPaths)
|
||||||
|
{
|
||||||
|
int attempts = 0;
|
||||||
|
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||||
|
|
||||||
|
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||||
|
await _pairDownloadTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||||
|
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||||
|
$"Starting download for {toDownloadReplacements.Count} files")));
|
||||||
|
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
|
||||||
|
{
|
||||||
|
_downloadManager.ClearDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false));
|
||||||
|
|
||||||
|
await _pairDownloadTask.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (downloadToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||||
|
|
||||||
|
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var appToken = _applicationCancellationTokenSource?.Token;
|
||||||
|
while ((!_applicationTask?.IsCompleted ?? false)
|
||||||
|
&& !downloadToken.IsCancellationRequested
|
||||||
|
&& (!appToken?.IsCancellationRequested ?? false))
|
||||||
|
{
|
||||||
|
// block until current application is done
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
|
||||||
|
await Task.Delay(250).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) return;
|
||||||
|
|
||||||
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
|
var token = _applicationCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyCharacterDataAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
||||||
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_applicationId = Guid.NewGuid();
|
||||||
|
Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId);
|
||||||
|
|
||||||
|
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler);
|
||||||
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (updateModdedPaths)
|
||||||
|
{
|
||||||
|
// ensure collection is set
|
||||||
|
var objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection,
|
||||||
|
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
|
||||||
|
LastAppliedDataBytes = -1;
|
||||||
|
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
|
||||||
|
{
|
||||||
|
if (LastAppliedDataBytes == -1) LastAppliedDataBytes = 0;
|
||||||
|
|
||||||
|
LastAppliedDataBytes += path.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateManip)
|
||||||
|
{
|
||||||
|
await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, _applicationId, _penumbraCollection, charaData.ManipulationData).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
foreach (var kind in updatedData)
|
||||||
|
{
|
||||||
|
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
_cachedData = charaData;
|
||||||
|
|
||||||
|
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
|
||||||
|
{
|
||||||
|
IsVisible = false;
|
||||||
|
_forceApplyMods = true;
|
||||||
|
_cachedData = charaData;
|
||||||
|
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FrameworkUpdate()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(PlayerName))
|
||||||
|
{
|
||||||
|
var pc = _dalamudUtil.FindPlayerByNameHash(Pair.Ident);
|
||||||
|
if (pc == default((string, nint))) return;
|
||||||
|
Logger.LogDebug("One-Time Initializing {this}", this);
|
||||||
|
Initialize(pc.Name);
|
||||||
|
Logger.LogDebug("One-Time Initialized {this}", this);
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||||
|
$"Initializing User For Character {pc.Name}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_charaHandler?.Address != nint.Zero && !IsVisible)
|
||||||
|
{
|
||||||
|
Guid appData = Guid.NewGuid();
|
||||||
|
IsVisible = true;
|
||||||
|
if (_cachedData != null)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] {this} visibility changed, now: {visi}, cached data exists", appData, this, IsVisible);
|
||||||
|
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
ApplyCharacterData(appData, _cachedData!, forceApplyCustomization: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogTrace("{this} visibility changed, now: {visi}, no cached data exists", this, IsVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
||||||
|
{
|
||||||
|
IsVisible = false;
|
||||||
|
_charaHandler.Invalidate();
|
||||||
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
|
_downloadCancellationTokenSource = null;
|
||||||
|
Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Initialize(string name)
|
||||||
|
{
|
||||||
|
PlayerName = name;
|
||||||
|
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident), isWatched: false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_serverConfigManager.AutoPopulateNoteForUid(Pair.UserData.UID, name);
|
||||||
|
|
||||||
|
Mediator.Subscribe<HonorificReadyMessage>(this, async (_) =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return;
|
||||||
|
Logger.LogTrace("Reapplying Honorific data for {this}", this);
|
||||||
|
await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<PetNamesReadyMessage>(this, async (_) =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return;
|
||||||
|
Logger.LogTrace("Reapplying Pet Names data for {this}", this);
|
||||||
|
await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
_ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, _charaHandler.GetGameObject()!.ObjectIndex).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident);
|
||||||
|
if (address == nint.Zero) return;
|
||||||
|
|
||||||
|
Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, Pair.UserData.AliasOrUID, name, objectKind);
|
||||||
|
|
||||||
|
if (_customizeIds.TryGetValue(objectKind, out var customizeId))
|
||||||
|
{
|
||||||
|
_customizeIds.Remove(objectKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false);
|
||||||
|
tempHandler.CompareNameAndThrow(name);
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||||
|
tempHandler.CompareNameAndThrow(name);
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
|
||||||
|
tempHandler.CompareNameAndThrow(name);
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||||
|
tempHandler.CompareNameAndThrow(name);
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else if (objectKind == ObjectKind.MinionOrMount)
|
||||||
|
{
|
||||||
|
var minionOrMount = await _dalamudUtil.GetMinionOrMountAsync(address).ConfigureAwait(false);
|
||||||
|
if (minionOrMount != nint.Zero)
|
||||||
|
{
|
||||||
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||||
|
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (objectKind == ObjectKind.Pet)
|
||||||
|
{
|
||||||
|
var pet = await _dalamudUtil.GetPetAsync(address).ConfigureAwait(false);
|
||||||
|
if (pet != nint.Zero)
|
||||||
|
{
|
||||||
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||||
|
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (objectKind == ObjectKind.Companion)
|
||||||
|
{
|
||||||
|
var companion = await _dalamudUtil.GetCompanionAsync(address).ConfigureAwait(false);
|
||||||
|
if (companion != nint.Zero)
|
||||||
|
{
|
||||||
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||||
|
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FileReplacementData> TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token)
|
||||||
|
{
|
||||||
|
Stopwatch st = Stopwatch.StartNew();
|
||||||
|
ConcurrentBag<FileReplacementData> missingFiles = [];
|
||||||
|
moddedDictionary = [];
|
||||||
|
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
|
||||||
|
bool hasMigrationChanges = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList();
|
||||||
|
Parallel.ForEach(replacementList, new ParallelOptions()
|
||||||
|
{
|
||||||
|
CancellationToken = token,
|
||||||
|
MaxDegreeOfParallelism = 4
|
||||||
|
},
|
||||||
|
(item) =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
|
||||||
|
if (fileCache != null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension))
|
||||||
|
{
|
||||||
|
hasMigrationChanges = true;
|
||||||
|
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var gamePath in item.GamePaths)
|
||||||
|
{
|
||||||
|
outputDict[(gamePath, item.Hash)] = fileCache.ResolvedFilepath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Missing file: {hash}", item.Hash);
|
||||||
|
missingFiles.Add(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value);
|
||||||
|
|
||||||
|
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
|
||||||
|
{
|
||||||
|
foreach (var gamePath in item.GamePaths)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] Adding file swap for {path}: {fileSwap}", applicationBase, gamePath, item.FileSwapPath);
|
||||||
|
moddedDictionary[(gamePath, null)] = item.FileSwapPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||||
|
}
|
||||||
|
if (hasMigrationChanges) _fileDbManager.WriteOutFullCsv();
|
||||||
|
st.Stop();
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
|
||||||
|
return [.. missingFiles];
|
||||||
|
}
|
||||||
|
}
|
||||||
10
MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs
Normal file
10
MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace MareSynchronos.PlayerData.Pairs;
|
||||||
|
|
||||||
|
public record OptionalPluginWarning
|
||||||
|
{
|
||||||
|
public bool ShownHeelsWarning { get; set; } = false;
|
||||||
|
public bool ShownCustomizePlusWarning { get; set; } = false;
|
||||||
|
public bool ShownHonorificWarning { get; set; } = false;
|
||||||
|
public bool ShownMoodlesWarning { get; set; } = false;
|
||||||
|
public bool ShowPetNicknamesWarning { get; set; } = false;
|
||||||
|
}
|
||||||
257
MareSynchronos/PlayerData/Pairs/Pair.cs
Normal file
257
MareSynchronos/PlayerData/Pairs/Pair.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using Dalamud.Game.Gui.ContextMenu;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.API.Data.Extensions;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Pairs;
|
||||||
|
|
||||||
|
public class Pair
|
||||||
|
{
|
||||||
|
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||||
|
private readonly SemaphoreSlim _creationSemaphore = new(1);
|
||||||
|
private readonly ILogger<Pair> _logger;
|
||||||
|
private readonly MareMediator _mediator;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private CancellationTokenSource _applicationCts = new();
|
||||||
|
private OnlineUserIdentDto? _onlineUserIdentDto = null;
|
||||||
|
|
||||||
|
public Pair(ILogger<Pair> logger, UserFullPairDto userPair, PairHandlerFactory cachedPlayerFactory,
|
||||||
|
MareMediator mediator, ServerConfigurationManager serverConfigurationManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
UserPair = userPair;
|
||||||
|
_cachedPlayerFactory = cachedPlayerFactory;
|
||||||
|
_mediator = mediator;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null;
|
||||||
|
public IndividualPairStatus IndividualPairStatus => UserPair.IndividualPairStatus;
|
||||||
|
public bool IsDirectlyPaired => IndividualPairStatus != IndividualPairStatus.None;
|
||||||
|
public bool IsOneSidedPair => IndividualPairStatus == IndividualPairStatus.OneSided;
|
||||||
|
public bool IsOnline => CachedPlayer != null;
|
||||||
|
|
||||||
|
public bool IsPaired => IndividualPairStatus == IndividualPairStatus.Bidirectional || UserPair.Groups.Any();
|
||||||
|
public bool IsPaused => UserPair.OwnPermissions.IsPaused();
|
||||||
|
public bool IsVisible => CachedPlayer?.IsVisible ?? false;
|
||||||
|
public CharacterData? LastReceivedCharacterData { get; set; }
|
||||||
|
public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty;
|
||||||
|
public long LastAppliedDataBytes => CachedPlayer?.LastAppliedDataBytes ?? -1;
|
||||||
|
public long LastAppliedDataTris { get; set; } = -1;
|
||||||
|
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||||
|
public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty;
|
||||||
|
|
||||||
|
public UserData UserData => UserPair.User;
|
||||||
|
|
||||||
|
public UserFullPairDto UserPair { get; set; }
|
||||||
|
private PairHandler? CachedPlayer { get; set; }
|
||||||
|
|
||||||
|
public void AddContextMenu(IMenuOpenedArgs args)
|
||||||
|
{
|
||||||
|
if (CachedPlayer == null || (args.Target is not MenuTargetDefault target) || target.TargetObjectId != CachedPlayer.PlayerCharacterId || IsPaused) return;
|
||||||
|
|
||||||
|
SeStringBuilder seStringBuilder = new();
|
||||||
|
SeStringBuilder seStringBuilder2 = new();
|
||||||
|
SeStringBuilder seStringBuilder3 = new();
|
||||||
|
SeStringBuilder seStringBuilder4 = new();
|
||||||
|
var openProfileSeString = seStringBuilder.AddText("Open Profile").Build();
|
||||||
|
var reapplyDataSeString = seStringBuilder2.AddText("Reapply last data").Build();
|
||||||
|
var cyclePauseState = seStringBuilder3.AddText("Cycle pause state").Build();
|
||||||
|
var changePermissions = seStringBuilder4.AddText("Change Permissions").Build();
|
||||||
|
args.AddMenuItem(new MenuItem()
|
||||||
|
{
|
||||||
|
Name = openProfileSeString,
|
||||||
|
OnClicked = (a) => _mediator.Publish(new ProfileOpenStandaloneMessage(this)),
|
||||||
|
UseDefaultPrefix = false,
|
||||||
|
PrefixChar = 'M',
|
||||||
|
PrefixColor = 526
|
||||||
|
});
|
||||||
|
|
||||||
|
args.AddMenuItem(new MenuItem()
|
||||||
|
{
|
||||||
|
Name = reapplyDataSeString,
|
||||||
|
OnClicked = (a) => ApplyLastReceivedData(forced: true),
|
||||||
|
UseDefaultPrefix = false,
|
||||||
|
PrefixChar = 'M',
|
||||||
|
PrefixColor = 526
|
||||||
|
});
|
||||||
|
|
||||||
|
args.AddMenuItem(new MenuItem()
|
||||||
|
{
|
||||||
|
Name = changePermissions,
|
||||||
|
OnClicked = (a) => _mediator.Publish(new OpenPermissionWindow(this)),
|
||||||
|
UseDefaultPrefix = false,
|
||||||
|
PrefixChar = 'M',
|
||||||
|
PrefixColor = 526
|
||||||
|
});
|
||||||
|
|
||||||
|
args.AddMenuItem(new MenuItem()
|
||||||
|
{
|
||||||
|
Name = cyclePauseState,
|
||||||
|
OnClicked = (a) => _mediator.Publish(new CyclePauseMessage(UserData)),
|
||||||
|
UseDefaultPrefix = false,
|
||||||
|
PrefixChar = 'M',
|
||||||
|
PrefixColor = 526
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyData(OnlineUserCharaDataDto data)
|
||||||
|
{
|
||||||
|
_applicationCts = _applicationCts.CancelRecreate();
|
||||||
|
LastReceivedCharacterData = data.CharaData;
|
||||||
|
|
||||||
|
if (CachedPlayer == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Received Data for {uid} but CachedPlayer does not exist, waiting", data.User.UID);
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var timeoutCts = new CancellationTokenSource();
|
||||||
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(120));
|
||||||
|
var appToken = _applicationCts.Token;
|
||||||
|
using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, appToken);
|
||||||
|
while (CachedPlayer == null && !combined.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(250, combined.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!combined.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Applying delayed data for {uid}", data.User.UID);
|
||||||
|
ApplyLastReceivedData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyLastReceivedData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyLastReceivedData(bool forced = false)
|
||||||
|
{
|
||||||
|
if (CachedPlayer == null) return;
|
||||||
|
if (LastReceivedCharacterData == null) return;
|
||||||
|
|
||||||
|
CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_creationSemaphore.Wait();
|
||||||
|
|
||||||
|
if (CachedPlayer != null) return;
|
||||||
|
|
||||||
|
if (dto == null && _onlineUserIdentDto == null)
|
||||||
|
{
|
||||||
|
CachedPlayer?.Dispose();
|
||||||
|
CachedPlayer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dto != null)
|
||||||
|
{
|
||||||
|
_onlineUserIdentDto = dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedPlayer?.Dispose();
|
||||||
|
CachedPlayer = _cachedPlayerFactory.Create(this);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_creationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetNote()
|
||||||
|
{
|
||||||
|
return _serverConfigurationManager.GetNoteForUid(UserData.UID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetPlayerNameHash()
|
||||||
|
{
|
||||||
|
return CachedPlayer?.PlayerNameHash ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAnyConnection()
|
||||||
|
{
|
||||||
|
return UserPair.Groups.Any() || UserPair.IndividualPairStatus != IndividualPairStatus.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkOffline(bool wait = true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (wait)
|
||||||
|
_creationSemaphore.Wait();
|
||||||
|
LastReceivedCharacterData = null;
|
||||||
|
var player = CachedPlayer;
|
||||||
|
CachedPlayer = null;
|
||||||
|
player?.Dispose();
|
||||||
|
_onlineUserIdentDto = null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (wait)
|
||||||
|
_creationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetNote(string note)
|
||||||
|
{
|
||||||
|
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetIsUploading()
|
||||||
|
{
|
||||||
|
CachedPlayer?.SetUploading();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Removing not synced files");
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Nothing to remove");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool disableIndividualAnimations = (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations());
|
||||||
|
bool disableIndividualVFX = (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX());
|
||||||
|
bool disableIndividualSounds = (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds());
|
||||||
|
|
||||||
|
_logger.LogTrace("Disable: Sounds: {disableIndividualSounds}, Anims: {disableIndividualAnims}; " +
|
||||||
|
"VFX: {disableGroupSounds}",
|
||||||
|
disableIndividualSounds, disableIndividualAnimations, disableIndividualVFX);
|
||||||
|
|
||||||
|
if (disableIndividualAnimations || disableIndividualSounds || disableIndividualVFX)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}",
|
||||||
|
disableIndividualAnimations, disableIndividualSounds, disableIndividualVFX);
|
||||||
|
foreach (var objectKind in data.FileReplacements.Select(k => k.Key))
|
||||||
|
{
|
||||||
|
if (disableIndividualSounds)
|
||||||
|
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||||
|
.Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
if (disableIndividualAnimations)
|
||||||
|
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||||
|
.Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
if (disableIndividualVFX)
|
||||||
|
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||||
|
.Where(f => !f.GamePaths.Any(p => p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) || p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
408
MareSynchronos/PlayerData/Pairs/PairManager.cs
Normal file
408
MareSynchronos/PlayerData/Pairs/PairManager.cs
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Comparer;
|
||||||
|
using MareSynchronos.API.Data.Extensions;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.Services.Events;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Pairs;
|
||||||
|
|
||||||
|
public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<UserData, Pair> _allClientPairs = new(UserDataComparer.Instance);
|
||||||
|
private readonly ConcurrentDictionary<GroupData, GroupFullInfoDto> _allGroups = new(GroupDataComparer.Instance);
|
||||||
|
private readonly MareConfigService _configurationService;
|
||||||
|
private readonly IContextMenu _dalamudContextMenu;
|
||||||
|
private readonly PairFactory _pairFactory;
|
||||||
|
private Lazy<List<Pair>> _directPairsInternal;
|
||||||
|
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
||||||
|
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
|
||||||
|
|
||||||
|
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
||||||
|
MareConfigService configurationService, MareMediator mediator,
|
||||||
|
IContextMenu dalamudContextMenu) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_pairFactory = pairFactory;
|
||||||
|
_configurationService = configurationService;
|
||||||
|
_dalamudContextMenu = dalamudContextMenu;
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
||||||
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
||||||
|
_directPairsInternal = DirectPairsLazy();
|
||||||
|
_groupPairsInternal = GroupPairsLazy();
|
||||||
|
_pairsWithGroupsInternal = PairsWithGroupsLazy();
|
||||||
|
|
||||||
|
_dalamudContextMenu.OnMenuOpened += DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Pair> DirectPairs => _directPairsInternal.Value;
|
||||||
|
|
||||||
|
public Dictionary<GroupFullInfoDto, List<Pair>> GroupPairs => _groupPairsInternal.Value;
|
||||||
|
public Dictionary<GroupData, GroupFullInfoDto> Groups => _allGroups.ToDictionary(k => k.Key, k => k.Value);
|
||||||
|
public Pair? LastAddedUser { get; internal set; }
|
||||||
|
public Dictionary<Pair, List<GroupFullInfoDto>> PairsWithGroups => _pairsWithGroupsInternal.Value;
|
||||||
|
|
||||||
|
public void AddGroup(GroupFullInfoDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group] = dto;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddGroupPair(GroupPairFullInfoDto dto)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.ContainsKey(dto.User))
|
||||||
|
_allClientPairs[dto.User] = _pairFactory.Create(new UserFullPairDto(dto.User, API.Data.Enum.IndividualPairStatus.None,
|
||||||
|
[dto.Group.GID], dto.SelfToOtherPermissions, dto.OtherToSelfPermissions));
|
||||||
|
else _allClientPairs[dto.User].UserPair.Groups.Add(dto.GID);
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair? GetPairByUID(string uid)
|
||||||
|
{
|
||||||
|
var existingPair = _allClientPairs.FirstOrDefault(f => f.Key.UID == uid);
|
||||||
|
if (!Equals(existingPair, default(KeyValuePair<UserData, Pair>)))
|
||||||
|
{
|
||||||
|
return existingPair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddUserPair(UserFullPairDto dto)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.ContainsKey(dto.User))
|
||||||
|
{
|
||||||
|
_allClientPairs[dto.User] = _pairFactory.Create(dto);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus;
|
||||||
|
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddUserPair(UserPairDto dto, bool addToLastAddedUser = true)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.ContainsKey(dto.User))
|
||||||
|
{
|
||||||
|
_allClientPairs[dto.User] = _pairFactory.Create(dto);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
addToLastAddedUser = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus;
|
||||||
|
_allClientPairs[dto.User].UserPair.OwnPermissions = dto.OwnPermissions;
|
||||||
|
_allClientPairs[dto.User].UserPair.OtherPermissions = dto.OtherPermissions;
|
||||||
|
if (addToLastAddedUser)
|
||||||
|
LastAddedUser = _allClientPairs[dto.User];
|
||||||
|
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearPairs()
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Clearing all Pairs");
|
||||||
|
DisposePairs();
|
||||||
|
_allClientPairs.Clear();
|
||||||
|
_allGroups.Clear();
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Pair> GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.GetPlayerNameHash())).Select(p => p.Value).ToList();
|
||||||
|
|
||||||
|
public int GetVisibleUserCount() => _allClientPairs.Count(p => p.Value.IsVisible);
|
||||||
|
|
||||||
|
public List<UserData> GetVisibleUsers() => [.. _allClientPairs.Where(p => p.Value.IsVisible).Select(p => p.Key)];
|
||||||
|
|
||||||
|
public void MarkPairOffline(UserData user)
|
||||||
|
{
|
||||||
|
if (_allClientPairs.TryGetValue(user, out var pair))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ClearProfileDataMessage(pair.UserData));
|
||||||
|
pair.MarkOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
|
||||||
|
|
||||||
|
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||||
|
|
||||||
|
var pair = _allClientPairs[dto.User];
|
||||||
|
if (pair.HasCachedPlayer)
|
||||||
|
{
|
||||||
|
RecreateLazy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendNotif && _configurationService.Current.ShowOnlineNotifications
|
||||||
|
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.IsDirectlyPaired && !pair.IsOneSidedPair
|
||||||
|
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs)
|
||||||
|
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote())
|
||||||
|
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs))
|
||||||
|
{
|
||||||
|
string? note = pair.GetNote();
|
||||||
|
var msg = !string.IsNullOrEmpty(note)
|
||||||
|
? $"{note} ({pair.UserData.AliasOrUID}) is now online"
|
||||||
|
: $"{pair.UserData.AliasOrUID} is now online";
|
||||||
|
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pair.CreateCachedPlayer(dto);
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReceiveCharaData(OnlineUserCharaDataDto dto)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.TryGetValue(dto.User, out var pair)) throw new InvalidOperationException("No user found for " + dto.User);
|
||||||
|
|
||||||
|
Mediator.Publish(new EventMessage(new Event(pair.UserData, nameof(PairManager), EventSeverity.Informational, "Received Character Data")));
|
||||||
|
_allClientPairs[dto.User].ApplyData(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveGroup(GroupData data)
|
||||||
|
{
|
||||||
|
_allGroups.TryRemove(data, out _);
|
||||||
|
|
||||||
|
foreach (var item in _allClientPairs.ToList())
|
||||||
|
{
|
||||||
|
item.Value.UserPair.Groups.Remove(data.GID);
|
||||||
|
|
||||||
|
if (!item.Value.HasAnyConnection())
|
||||||
|
{
|
||||||
|
item.Value.MarkOffline();
|
||||||
|
_allClientPairs.TryRemove(item.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveGroupPair(GroupPairDto dto)
|
||||||
|
{
|
||||||
|
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||||
|
{
|
||||||
|
pair.UserPair.Groups.Remove(dto.Group.GID);
|
||||||
|
|
||||||
|
if (!pair.HasAnyConnection())
|
||||||
|
{
|
||||||
|
pair.MarkOffline();
|
||||||
|
_allClientPairs.TryRemove(dto.User, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveUserPair(UserDto dto)
|
||||||
|
{
|
||||||
|
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||||
|
{
|
||||||
|
pair.UserPair.IndividualPairStatus = API.Data.Enum.IndividualPairStatus.None;
|
||||||
|
|
||||||
|
if (!pair.HasAnyConnection())
|
||||||
|
{
|
||||||
|
pair.MarkOffline();
|
||||||
|
_allClientPairs.TryRemove(dto.User, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGroupInfo(GroupInfoDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group].Group = dto.Group;
|
||||||
|
_allGroups[dto.Group].Owner = dto.Owner;
|
||||||
|
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions;
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdatePairPermissions(UserPermissionsDto dto)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No such pair for " + dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto);
|
||||||
|
|
||||||
|
if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||||
|
}
|
||||||
|
|
||||||
|
pair.UserPair.OtherPermissions = dto.Permissions;
|
||||||
|
|
||||||
|
Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}",
|
||||||
|
pair.UserPair.OtherPermissions.IsPaused(),
|
||||||
|
pair.UserPair.OtherPermissions.IsDisableAnimations(),
|
||||||
|
pair.UserPair.OtherPermissions.IsDisableSounds(),
|
||||||
|
pair.UserPair.OtherPermissions.IsDisableVFX());
|
||||||
|
|
||||||
|
if (!pair.IsPaused)
|
||||||
|
pair.ApplyLastReceivedData();
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSelfPairPermissions(UserPermissionsDto dto)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No such pair for " + dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||||
|
}
|
||||||
|
|
||||||
|
pair.UserPair.OwnPermissions = dto.Permissions;
|
||||||
|
|
||||||
|
Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}",
|
||||||
|
pair.UserPair.OwnPermissions.IsPaused(),
|
||||||
|
pair.UserPair.OwnPermissions.IsDisableAnimations(),
|
||||||
|
pair.UserPair.OwnPermissions.IsDisableSounds(),
|
||||||
|
pair.UserPair.OwnPermissions.IsDisableVFX());
|
||||||
|
|
||||||
|
if (!pair.IsPaused)
|
||||||
|
pair.ApplyLastReceivedData();
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReceiveUploadStatus(UserDto dto)
|
||||||
|
{
|
||||||
|
if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible)
|
||||||
|
{
|
||||||
|
existingPair.SetIsUploading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group].GroupPairUserInfos[dto.UID] = dto.GroupUserInfo;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetGroupPermissions(GroupPermissionDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group].GroupPermissions = dto.Permissions;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetGroupStatusInfo(GroupPairUserInfoDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UpdateGroupPairPermissions(GroupPairUserPermissionDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UpdateIndividualPairStatus(UserIndividualPairStatusDto dto)
|
||||||
|
{
|
||||||
|
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||||
|
{
|
||||||
|
pair.UserPair.IndividualPairStatus = dto.IndividualPairStatus;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||||
|
|
||||||
|
DisposePairs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DalamudContextMenuOnOnOpenGameObjectContextMenu(Dalamud.Game.Gui.ContextMenu.IMenuOpenedArgs args)
|
||||||
|
{
|
||||||
|
if (args.MenuType == Dalamud.Game.Gui.ContextMenu.ContextMenuType.Inventory) return;
|
||||||
|
if (!_configurationService.Current.EnableRightClickMenus) return;
|
||||||
|
|
||||||
|
foreach (var pair in _allClientPairs.Where((p => p.Value.IsVisible)))
|
||||||
|
{
|
||||||
|
pair.Value.AddContextMenu(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Lazy<List<Pair>> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value)
|
||||||
|
.Where(k => k.IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None).ToList());
|
||||||
|
|
||||||
|
private void DisposePairs()
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Disposing all Pairs");
|
||||||
|
Parallel.ForEach(_allClientPairs, item =>
|
||||||
|
{
|
||||||
|
item.Value.MarkOffline(wait: false);
|
||||||
|
});
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> GroupPairsLazy()
|
||||||
|
{
|
||||||
|
return new Lazy<Dictionary<GroupFullInfoDto, List<Pair>>>(() =>
|
||||||
|
{
|
||||||
|
Dictionary<GroupFullInfoDto, List<Pair>> outDict = [];
|
||||||
|
foreach (var group in _allGroups)
|
||||||
|
{
|
||||||
|
outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.UserPair.Groups.Exists(g => GroupDataComparer.Instance.Equals(group.Key, new(g)))).ToList();
|
||||||
|
}
|
||||||
|
return outDict;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> PairsWithGroupsLazy()
|
||||||
|
{
|
||||||
|
return new Lazy<Dictionary<Pair, List<GroupFullInfoDto>>>(() =>
|
||||||
|
{
|
||||||
|
Dictionary<Pair, List<GroupFullInfoDto>> outDict = [];
|
||||||
|
|
||||||
|
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||||
|
{
|
||||||
|
outDict[pair] = _allGroups.Where(k => pair.UserPair.Groups.Contains(k.Key.GID, StringComparer.Ordinal)).Select(k => k.Value).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return outDict;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReapplyPairData()
|
||||||
|
{
|
||||||
|
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||||
|
{
|
||||||
|
pair.ApplyLastReceivedData(forced: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecreateLazy()
|
||||||
|
{
|
||||||
|
_directPairsInternal = DirectPairsLazy();
|
||||||
|
_groupPairsInternal = GroupPairsLazy();
|
||||||
|
_pairsWithGroupsInternal = PairsWithGroupsLazy();
|
||||||
|
Mediator.Publish(new RefreshUiMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
132
MareSynchronos/PlayerData/Pairs/VisibleUserDataDistributor.cs
Normal file
132
MareSynchronos/PlayerData/Pairs/VisibleUserDataDistributor.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using MareSynchronos.WebAPI.Files;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Pairs;
|
||||||
|
|
||||||
|
public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private CharacterData? _lastCreatedData;
|
||||||
|
private CharacterData? _uploadingCharacterData = null;
|
||||||
|
private readonly List<UserData> _previouslyVisiblePlayers = [];
|
||||||
|
private Task<CharacterData>? _fileUploadTask = null;
|
||||||
|
private readonly HashSet<UserData> _usersToPushDataTo = [];
|
||||||
|
private readonly SemaphoreSlim _pushDataSemaphore = new(1, 1);
|
||||||
|
private readonly CancellationTokenSource _runtimeCts = new();
|
||||||
|
|
||||||
|
|
||||||
|
public VisibleUserDataDistributor(ILogger<VisibleUserDataDistributor> logger, ApiController apiController, DalamudUtilService dalamudUtil,
|
||||||
|
PairManager pairManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_apiController = apiController;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_fileTransferManager = fileTransferManager;
|
||||||
|
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate());
|
||||||
|
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
var newData = msg.CharacterData;
|
||||||
|
if (_lastCreatedData == null || (!string.Equals(newData.DataHash.Value, _lastCreatedData.DataHash.Value, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
_lastCreatedData = newData;
|
||||||
|
Logger.LogTrace("Storing new data hash {hash}", newData.DataHash.Value);
|
||||||
|
PushToAllVisibleUsers(forced: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Data hash {hash} equal to stored data", newData.DataHash.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) => _previouslyVisiblePlayers.Clear());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_runtimeCts.Cancel();
|
||||||
|
_runtimeCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PushToAllVisibleUsers(bool forced = false)
|
||||||
|
{
|
||||||
|
foreach (var user in _pairManager.GetVisibleUsers())
|
||||||
|
{
|
||||||
|
_usersToPushDataTo.Add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_usersToPushDataTo.Count > 0)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Pushing data {hash} for {count} visible players", _lastCreatedData?.DataHash.Value ?? "UNKNOWN", _usersToPushDataTo.Count);
|
||||||
|
PushCharacterData(forced);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FrameworkOnUpdate()
|
||||||
|
{
|
||||||
|
if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return;
|
||||||
|
|
||||||
|
var allVisibleUsers = _pairManager.GetVisibleUsers();
|
||||||
|
var newVisibleUsers = allVisibleUsers.Except(_previouslyVisiblePlayers).ToList();
|
||||||
|
_previouslyVisiblePlayers.Clear();
|
||||||
|
_previouslyVisiblePlayers.AddRange(allVisibleUsers);
|
||||||
|
if (newVisibleUsers.Count == 0) return;
|
||||||
|
|
||||||
|
Logger.LogDebug("Scheduling character data push of {data} to {users}",
|
||||||
|
_lastCreatedData?.DataHash.Value ?? string.Empty,
|
||||||
|
string.Join(", ", newVisibleUsers.Select(k => k.AliasOrUID)));
|
||||||
|
foreach (var user in newVisibleUsers)
|
||||||
|
{
|
||||||
|
_usersToPushDataTo.Add(user);
|
||||||
|
}
|
||||||
|
PushCharacterData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PushCharacterData(bool forced = false)
|
||||||
|
{
|
||||||
|
if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
||||||
|
|
||||||
|
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
||||||
|
{
|
||||||
|
_uploadingCharacterData = _lastCreatedData.DeepClone();
|
||||||
|
Logger.LogDebug("Starting UploadTask for {hash}, Reason: TaskIsNull: {task}, TaskIsCompleted: {taskCpl}, Forced: {frc}",
|
||||||
|
_lastCreatedData.DataHash, _fileUploadTask == null, _fileUploadTask?.IsCompleted ?? false, forced);
|
||||||
|
_fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, [.. _usersToPushDataTo]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_fileUploadTask != null)
|
||||||
|
{
|
||||||
|
var dataToSend = await _fileUploadTask.ConfigureAwait(false);
|
||||||
|
await _pushDataSemaphore.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_usersToPushDataTo.Count == 0) return;
|
||||||
|
Logger.LogDebug("Pushing {data} to {users}", dataToSend.DataHash, string.Join(", ", _usersToPushDataTo.Select(k => k.AliasOrUID)));
|
||||||
|
await _apiController.PushCharacterData(dataToSend, [.. _usersToPushDataTo]).ConfigureAwait(false);
|
||||||
|
_usersToPushDataTo.Clear();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pushDataSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
245
MareSynchronos/PlayerData/Services/CacheCreationService.cs
Normal file
245
MareSynchronos/PlayerData/Services/CacheCreationService.cs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.PlayerData.Data;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Services;
|
||||||
|
|
||||||
|
public sealed class CacheCreationService : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim _cacheCreateLock = new(1);
|
||||||
|
private readonly HashSet<ObjectKind> _cachesToCreate = [];
|
||||||
|
private readonly PlayerDataFactory _characterDataFactory;
|
||||||
|
private readonly HashSet<ObjectKind> _currentlyCreating = [];
|
||||||
|
private readonly HashSet<ObjectKind> _debouncedObjectCache = [];
|
||||||
|
private readonly CharacterData _playerData = new();
|
||||||
|
private readonly Dictionary<ObjectKind, GameObjectHandler> _playerRelatedObjects = [];
|
||||||
|
private readonly CancellationTokenSource _runtimeCts = new();
|
||||||
|
private CancellationTokenSource _creationCts = new();
|
||||||
|
private CancellationTokenSource _debounceCts = new();
|
||||||
|
private bool _haltCharaDataCreation;
|
||||||
|
private bool _isZoning = false;
|
||||||
|
|
||||||
|
public CacheCreationService(ILogger<CacheCreationService> logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||||
|
PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_characterDataFactory = characterDataFactory;
|
||||||
|
|
||||||
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (msg) => _isZoning = true);
|
||||||
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (msg) => _isZoning = false);
|
||||||
|
|
||||||
|
Mediator.Subscribe<HaltCharaDataCreation>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_haltCharaDataCreation = !msg.Resume;
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<CreateCacheForObjectMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Received CreateCacheForObject for {handler}, updating", msg.ObjectToCreateFor);
|
||||||
|
AddCacheToCreate(msg.ObjectToCreateFor.ObjectKind);
|
||||||
|
});
|
||||||
|
|
||||||
|
_playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPtr, isWatched: true)
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
|
_playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMountPtr(), isWatched: true)
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
|
_playerRelatedObjects[ObjectKind.Pet] = gameObjectHandlerFactory.Create(ObjectKind.Pet, () => dalamudUtil.GetPetPtr(), isWatched: true)
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
|
_playerRelatedObjects[ObjectKind.Companion] = gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanionPtr(), isWatched: true)
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.GameObjectHandler == _playerRelatedObjects[ObjectKind.Player])
|
||||||
|
{
|
||||||
|
AddCacheToCreate(ObjectKind.Player);
|
||||||
|
AddCacheToCreate(ObjectKind.Pet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<ClearCacheForObjectMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.ObjectToCreateFor.ObjectKind == ObjectKind.Pet)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Received clear cache for {obj}, ignoring", msg.ObjectToCreateFor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logger.LogDebug("Clearing cache for {obj}", msg.ObjectToCreateFor);
|
||||||
|
AddCacheToCreate(msg.ObjectToCreateFor.ObjectKind);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<CustomizePlusMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_isZoning) return;
|
||||||
|
foreach (var item in _playerRelatedObjects
|
||||||
|
.Where(item => msg.Address == null
|
||||||
|
|| item.Value.Address == msg.Address).Select(k => k.Key))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Received CustomizePlus change, updating {obj}", item);
|
||||||
|
AddCacheToCreate(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<HeelsOffsetMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_isZoning) return;
|
||||||
|
Logger.LogDebug("Received Heels Offset change, updating player");
|
||||||
|
AddCacheToCreate();
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<GlamourerChangedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_isZoning) return;
|
||||||
|
var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address);
|
||||||
|
if (!default(KeyValuePair<ObjectKind, GameObjectHandler>).Equals(changedType))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Received GlamourerChangedMessage for {kind}", changedType);
|
||||||
|
AddCacheToCreate(changedType.Key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<HonorificMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_isZoning) return;
|
||||||
|
if (!string.Equals(msg.NewHonorificTitle, _playerData.HonorificData, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Received Honorific change, updating player");
|
||||||
|
AddCacheToCreate(ObjectKind.Player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<MoodlesMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_isZoning) return;
|
||||||
|
var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address);
|
||||||
|
if (!default(KeyValuePair<ObjectKind, GameObjectHandler>).Equals(changedType) && changedType.Key == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Received Moodles change, updating player");
|
||||||
|
AddCacheToCreate(ObjectKind.Player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<PetNamesMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_isZoning) return;
|
||||||
|
if (!string.Equals(msg.PetNicknamesData, _playerData.PetNamesData, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Received Pet Nicknames change, updating player");
|
||||||
|
AddCacheToCreate(ObjectKind.Player);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Received Penumbra Mod settings change, updating everything");
|
||||||
|
AddCacheToCreate(ObjectKind.Player);
|
||||||
|
AddCacheToCreate(ObjectKind.Pet);
|
||||||
|
AddCacheToCreate(ObjectKind.MinionOrMount);
|
||||||
|
AddCacheToCreate(ObjectKind.Companion);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, (msg) => ProcessCacheCreation());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
_playerRelatedObjects.Values.ToList().ForEach(p => p.Dispose());
|
||||||
|
_runtimeCts.Cancel();
|
||||||
|
_runtimeCts.Dispose();
|
||||||
|
_creationCts.Cancel();
|
||||||
|
_creationCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddCacheToCreate(ObjectKind kind = ObjectKind.Player)
|
||||||
|
{
|
||||||
|
_debounceCts.Cancel();
|
||||||
|
_debounceCts.Dispose();
|
||||||
|
_debounceCts = new();
|
||||||
|
var token = _debounceCts.Token;
|
||||||
|
_cacheCreateLock.Wait();
|
||||||
|
_debouncedObjectCache.Add(kind);
|
||||||
|
_cacheCreateLock.Release();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||||
|
Logger.LogTrace("Debounce complete, inserting objects to create for: {obj}", string.Join(", ", _debouncedObjectCache));
|
||||||
|
await _cacheCreateLock.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
foreach (var item in _debouncedObjectCache)
|
||||||
|
{
|
||||||
|
_cachesToCreate.Add(item);
|
||||||
|
}
|
||||||
|
_debouncedObjectCache.Clear();
|
||||||
|
_cacheCreateLock.Release();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessCacheCreation()
|
||||||
|
{
|
||||||
|
if (_isZoning || _haltCharaDataCreation) return;
|
||||||
|
|
||||||
|
if (_cachesToCreate.Count == 0) return;
|
||||||
|
|
||||||
|
if (_playerRelatedObjects.Any(p => p.Value.CurrentDrawCondition is
|
||||||
|
not (GameObjectHandler.DrawCondition.None or GameObjectHandler.DrawCondition.DrawObjectZero or GameObjectHandler.DrawCondition.ObjectZero)))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Waiting for draw to finish before executing cache creation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_creationCts.Cancel();
|
||||||
|
_creationCts.Dispose();
|
||||||
|
_creationCts = new();
|
||||||
|
_cacheCreateLock.Wait(_creationCts.Token);
|
||||||
|
var objectKindsToCreate = _cachesToCreate.ToList();
|
||||||
|
foreach (var creationObj in objectKindsToCreate)
|
||||||
|
{
|
||||||
|
_currentlyCreating.Add(creationObj);
|
||||||
|
}
|
||||||
|
_cachesToCreate.Clear();
|
||||||
|
_cacheCreateLock.Release();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_creationCts.Token, _runtimeCts.Token);
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), linkedCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Logger.LogDebug("Creating Caches for {objectKinds}", string.Join(", ", objectKindsToCreate));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<ObjectKind, CharacterDataFragment?> createdData = [];
|
||||||
|
foreach (var objectKind in _currentlyCreating)
|
||||||
|
{
|
||||||
|
createdData[objectKind] = await _characterDataFactory.BuildCharacterData(_playerRelatedObjects[objectKind], linkedCts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kvp in createdData)
|
||||||
|
{
|
||||||
|
_playerData.SetFragment(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
|
||||||
|
_currentlyCreating.Clear();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Cache Creation cancelled");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogCritical(ex, "Error during Cache Creation Processing");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Cache Creation complete");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
254
MareSynchronos/Plugin.cs
Normal file
254
MareSynchronos/Plugin.cs
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects;
|
||||||
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Interop;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.PlayerData.Services;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Events;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.UI;
|
||||||
|
using MareSynchronos.UI.Components;
|
||||||
|
using MareSynchronos.UI.Components.Popup;
|
||||||
|
using MareSynchronos.UI.Handlers;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using MareSynchronos.WebAPI.Files;
|
||||||
|
using MareSynchronos.WebAPI.SignalR;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NReco.Logging.File;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
|
using MareSynchronos.Services.CharaData;
|
||||||
|
using Dalamud.Game;
|
||||||
|
|
||||||
|
namespace MareSynchronos;
|
||||||
|
|
||||||
|
public sealed class Plugin : IDalamudPlugin
|
||||||
|
{
|
||||||
|
private readonly IHost _host;
|
||||||
|
|
||||||
|
public Plugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, IDataManager gameData,
|
||||||
|
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
||||||
|
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
||||||
|
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
||||||
|
ISigScanner sigScanner)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
||||||
|
Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName);
|
||||||
|
var traceDir = Path.Join(pluginInterface.ConfigDirectory.FullName, "tracelog");
|
||||||
|
if (!Directory.Exists(traceDir))
|
||||||
|
Directory.CreateDirectory(traceDir);
|
||||||
|
|
||||||
|
foreach (var file in Directory.EnumerateFiles(traceDir)
|
||||||
|
.Select(f => new FileInfo(f))
|
||||||
|
.OrderByDescending(f => f.LastWriteTimeUtc).Skip(9))
|
||||||
|
{
|
||||||
|
int attempts = 0;
|
||||||
|
bool deleted = false;
|
||||||
|
while (!deleted && attempts < 5)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file.Delete();
|
||||||
|
deleted = true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_host = new HostBuilder()
|
||||||
|
.UseContentRoot(pluginInterface.ConfigDirectory.FullName)
|
||||||
|
.ConfigureLogging(lb =>
|
||||||
|
{
|
||||||
|
lb.ClearProviders();
|
||||||
|
lb.AddDalamudLogging(pluginLog, gameData.HasModifiedGameDataFiles);
|
||||||
|
lb.AddFile(Path.Combine(traceDir, $"mare-trace-{DateTime.Now:yyyy-MM-dd-HH-mm-ss}.log"), (opt) =>
|
||||||
|
{
|
||||||
|
opt.Append = true;
|
||||||
|
opt.RollingFilesConvention = FileLoggerOptions.FileRollingConvention.Ascending;
|
||||||
|
opt.MinLevel = LogLevel.Trace;
|
||||||
|
opt.FileSizeLimitBytes = 50 * 1024 * 1024;
|
||||||
|
});
|
||||||
|
lb.SetMinimumLevel(LogLevel.Trace);
|
||||||
|
})
|
||||||
|
.ConfigureServices(collection =>
|
||||||
|
{
|
||||||
|
collection.AddSingleton(new WindowSystem("MareSynchronos"));
|
||||||
|
collection.AddSingleton<FileDialogManager>();
|
||||||
|
collection.AddSingleton(new Dalamud.Localization("MareSynchronos.Localization.", "", useEmbedded: true));
|
||||||
|
|
||||||
|
// add mare related singletons
|
||||||
|
collection.AddSingleton<MareMediator>();
|
||||||
|
collection.AddSingleton<FileCacheManager>();
|
||||||
|
collection.AddSingleton<ServerConfigurationManager>();
|
||||||
|
collection.AddSingleton<ApiController>();
|
||||||
|
collection.AddSingleton<PerformanceCollectorService>();
|
||||||
|
collection.AddSingleton<HubFactory>();
|
||||||
|
collection.AddSingleton<FileUploadManager>();
|
||||||
|
collection.AddSingleton<FileTransferOrchestrator>();
|
||||||
|
collection.AddSingleton<MarePlugin>();
|
||||||
|
collection.AddSingleton<MareProfileManager>();
|
||||||
|
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||||
|
collection.AddSingleton<FileDownloadManagerFactory>();
|
||||||
|
collection.AddSingleton<PairHandlerFactory>();
|
||||||
|
collection.AddSingleton<PairFactory>();
|
||||||
|
collection.AddSingleton<XivDataAnalyzer>();
|
||||||
|
collection.AddSingleton<CharacterAnalyzer>();
|
||||||
|
collection.AddSingleton<TokenProvider>();
|
||||||
|
collection.AddSingleton<PluginWarningNotificationService>();
|
||||||
|
collection.AddSingleton<FileCompactor>();
|
||||||
|
collection.AddSingleton<TagHandler>();
|
||||||
|
collection.AddSingleton<IdDisplayHandler>();
|
||||||
|
collection.AddSingleton<PlayerPerformanceService>();
|
||||||
|
collection.AddSingleton<TransientResourceManager>();
|
||||||
|
|
||||||
|
collection.AddSingleton<CharaDataManager>();
|
||||||
|
collection.AddSingleton<CharaDataFileHandler>();
|
||||||
|
collection.AddSingleton<CharaDataCharacterHandler>();
|
||||||
|
collection.AddSingleton<CharaDataNearbyManager>();
|
||||||
|
collection.AddSingleton<CharaDataGposeTogetherManager>();
|
||||||
|
|
||||||
|
collection.AddSingleton(s => new VfxSpawnManager(s.GetRequiredService<ILogger<VfxSpawnManager>>(),
|
||||||
|
gameInteropProvider, s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton((s) => new BlockedCharacterHandler(s.GetRequiredService<ILogger<BlockedCharacterHandler>>(), gameInteropProvider));
|
||||||
|
collection.AddSingleton((s) => new IpcProvider(s.GetRequiredService<ILogger<IpcProvider>>(),
|
||||||
|
pluginInterface,
|
||||||
|
s.GetRequiredService<CharaDataManager>(),
|
||||||
|
s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton<SelectPairForTagUi>();
|
||||||
|
collection.AddSingleton((s) => new EventAggregator(pluginInterface.ConfigDirectory.FullName,
|
||||||
|
s.GetRequiredService<ILogger<EventAggregator>>(), s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService<ILogger<DalamudUtilService>>(),
|
||||||
|
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
|
||||||
|
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<MareMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
|
||||||
|
s.GetRequiredService<MareConfigService>()));
|
||||||
|
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<MareConfigService>(),
|
||||||
|
s.GetRequiredService<MareMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>()));
|
||||||
|
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||||
|
s.GetRequiredService<MareConfigService>(), s.GetRequiredService<MareMediator>(), contextMenu));
|
||||||
|
collection.AddSingleton<RedrawManager>();
|
||||||
|
collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService<ILogger<IpcCallerPenumbra>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>(), s.GetRequiredService<RedrawManager>()));
|
||||||
|
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>(), s.GetRequiredService<RedrawManager>()));
|
||||||
|
collection.AddSingleton((s) => new IpcCallerCustomize(s.GetRequiredService<ILogger<IpcCallerCustomize>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton((s) => new IpcCallerHeels(s.GetRequiredService<ILogger<IpcCallerHeels>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton((s) => new IpcCallerHonorific(s.GetRequiredService<ILogger<IpcCallerHonorific>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton((s) => new IpcCallerMoodles(s.GetRequiredService<ILogger<IpcCallerMoodles>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService<ILogger<IpcCallerPetNames>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddSingleton((s) => new IpcCallerBrio(s.GetRequiredService<ILogger<IpcCallerBrio>>(), pluginInterface,
|
||||||
|
s.GetRequiredService<DalamudUtilService>()));
|
||||||
|
collection.AddSingleton((s) => new IpcManager(s.GetRequiredService<ILogger<IpcManager>>(),
|
||||||
|
s.GetRequiredService<MareMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
|
||||||
|
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
|
||||||
|
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
|
||||||
|
collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
|
||||||
|
s.GetRequiredService<MareMediator>(), s.GetRequiredService<DalamudUtilService>(),
|
||||||
|
notificationManager, chatGui, s.GetRequiredService<MareConfigService>()));
|
||||||
|
collection.AddSingleton((s) =>
|
||||||
|
{
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||||
|
return httpClient;
|
||||||
|
});
|
||||||
|
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<MareConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<ServerTagConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<TransientConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<XivDataStorageService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||||
|
collection.AddSingleton<ConfigurationMigrator>();
|
||||||
|
collection.AddSingleton<ConfigurationSaveService>();
|
||||||
|
|
||||||
|
collection.AddSingleton<HubFactory>();
|
||||||
|
|
||||||
|
// add scoped services
|
||||||
|
collection.AddScoped<DrawEntityFactory>();
|
||||||
|
collection.AddScoped<CacheMonitor>();
|
||||||
|
collection.AddScoped<UiFactory>();
|
||||||
|
collection.AddScoped<SelectTagForPairUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, JoinSyncshellUI>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, CreateSyncshellUI>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||||
|
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(),
|
||||||
|
s.GetRequiredService<MareMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
||||||
|
s.GetRequiredService<MareProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||||
|
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
|
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||||
|
collection.AddScoped<CacheCreationService>();
|
||||||
|
collection.AddScoped<PlayerDataFactory>();
|
||||||
|
collection.AddScoped<VisibleUserDataDistributor>();
|
||||||
|
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<MareConfigService>(),
|
||||||
|
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
||||||
|
s.GetRequiredService<UiFactory>(),
|
||||||
|
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareMediator>()));
|
||||||
|
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
|
||||||
|
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
|
||||||
|
s.GetRequiredService<MareMediator>(), s.GetRequiredService<MareConfigService>()));
|
||||||
|
collection.AddScoped((s) => new UiSharedService(s.GetRequiredService<ILogger<UiSharedService>>(), s.GetRequiredService<IpcManager>(), s.GetRequiredService<ApiController>(),
|
||||||
|
s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<MareConfigService>(), s.GetRequiredService<DalamudUtilService>(),
|
||||||
|
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
|
||||||
|
s.GetRequiredService<MareMediator>()));
|
||||||
|
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<PerformanceCollectorService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<DtrEntry>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_ = _host.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_host.StopAsync().GetAwaiter().GetResult();
|
||||||
|
_host.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
127
MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs
Normal file
127
MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly HashSet<HandledCharaDataEntry> _handledCharaData = [];
|
||||||
|
|
||||||
|
public IEnumerable<HandledCharaDataEntry> HandledCharaData => _handledCharaData;
|
||||||
|
|
||||||
|
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
|
||||||
|
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
|
||||||
|
IpcManager ipcManager)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
mediator.Subscribe<GposeEndMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
foreach (var chara in _handledCharaData)
|
||||||
|
{
|
||||||
|
RevertHandledChara(chara);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) => HandleCutsceneFrameworkUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCutsceneFrameworkUpdate()
|
||||||
|
{
|
||||||
|
if (!_dalamudUtilService.IsInGpose) return;
|
||||||
|
|
||||||
|
foreach (var entry in _handledCharaData.ToList())
|
||||||
|
{
|
||||||
|
var chara = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(entry.Name, onlyGposeCharacters: true);
|
||||||
|
if (chara is null)
|
||||||
|
{
|
||||||
|
RevertChara(entry.Name, entry.CustomizePlus).GetAwaiter().GetResult();
|
||||||
|
_handledCharaData.Remove(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
foreach (var chara in _handledCharaData)
|
||||||
|
{
|
||||||
|
RevertHandledChara(chara);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RevertChara(string name, Guid? cPlusId)
|
||||||
|
{
|
||||||
|
Guid applicationId = Guid.NewGuid();
|
||||||
|
await _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).ConfigureAwait(false);
|
||||||
|
if (cPlusId != null)
|
||||||
|
{
|
||||||
|
await _ipcManager.CustomizePlus.RevertByIdAsync(cPlusId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
using var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
|
||||||
|
() => _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.Address ?? IntPtr.Zero, false)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (handler.Address != nint.Zero)
|
||||||
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RevertHandledChara(string name)
|
||||||
|
{
|
||||||
|
var handled = _handledCharaData.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.Ordinal));
|
||||||
|
if (handled == null) return false;
|
||||||
|
_handledCharaData.Remove(handled);
|
||||||
|
await _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus)).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RevertHandledChara(HandledCharaDataEntry? handled)
|
||||||
|
{
|
||||||
|
if (handled == null) return Task.CompletedTask;
|
||||||
|
_handledCharaData.Remove(handled);
|
||||||
|
return _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
|
||||||
|
{
|
||||||
|
_handledCharaData.Add(handledCharaDataEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
|
||||||
|
{
|
||||||
|
foreach (var handledData in _handledCharaData)
|
||||||
|
{
|
||||||
|
if (newData.TryGetValue(handledData.MetaInfo.FullId, out var metaInfo) && metaInfo != null)
|
||||||
|
{
|
||||||
|
handledData.MetaInfo = metaInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GameObjectHandler?> TryCreateGameObjectHandler(string name, bool gPoseOnly = false)
|
||||||
|
{
|
||||||
|
var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
|
||||||
|
() => _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, gPoseOnly && _dalamudUtilService.IsInGpose)?.Address ?? IntPtr.Zero, false)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (handler.Address == nint.Zero) return null;
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GameObjectHandler?> TryCreateGameObjectHandler(int index)
|
||||||
|
{
|
||||||
|
var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
|
||||||
|
() => _dalamudUtilService.GetCharacterFromObjectTableByIndex(index)?.Address ?? IntPtr.Zero, false)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (handler.Address == nint.Zero) return null;
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
303
MareSynchronos/Services/CharaData/CharaDataFileHandler.cs
Normal file
303
MareSynchronos/Services/CharaData/CharaDataFileHandler.cs
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using K4os.Compression.LZ4.Legacy;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services.CharaData;
|
||||||
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using MareSynchronos.WebAPI.Files;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class CharaDataFileHandler : IDisposable
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly FileDownloadManager _fileDownloadManager;
|
||||||
|
private readonly FileUploadManager _fileUploadManager;
|
||||||
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
|
private readonly ILogger<CharaDataFileHandler> _logger;
|
||||||
|
private readonly MareCharaFileDataFactory _mareCharaFileDataFactory;
|
||||||
|
private readonly PlayerDataFactory _playerDataFactory;
|
||||||
|
private int _globalFileCounter = 0;
|
||||||
|
|
||||||
|
public CharaDataFileHandler(ILogger<CharaDataFileHandler> logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager,
|
||||||
|
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory)
|
||||||
|
{
|
||||||
|
_fileDownloadManager = fileDownloadManagerFactory.Create();
|
||||||
|
_logger = logger;
|
||||||
|
_fileUploadManager = fileUploadManager;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
_playerDataFactory = playerDataFactory;
|
||||||
|
_mareCharaFileDataFactory = new(fileCacheManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ComputeMissingFiles(CharaDataDownloadDto charaDataDownloadDto, out Dictionary<string, string> modPaths, out List<FileReplacementData> missingFiles)
|
||||||
|
{
|
||||||
|
modPaths = [];
|
||||||
|
missingFiles = [];
|
||||||
|
foreach (var file in charaDataDownloadDto.FileGamePaths)
|
||||||
|
{
|
||||||
|
var localCacheFile = _fileCacheManager.GetFileCacheByHash(file.HashOrFileSwap);
|
||||||
|
if (localCacheFile == null)
|
||||||
|
{
|
||||||
|
var existingFile = missingFiles.Find(f => string.Equals(f.Hash, file.HashOrFileSwap, StringComparison.Ordinal));
|
||||||
|
if (existingFile == null)
|
||||||
|
{
|
||||||
|
missingFiles.Add(new FileReplacementData()
|
||||||
|
{
|
||||||
|
Hash = file.HashOrFileSwap,
|
||||||
|
GamePaths = [file.GamePath]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingFile.GamePaths = existingFile.GamePaths.Concat([file.GamePath]).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
modPaths[file.GamePath] = localCacheFile.ResolvedFilepath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var swap in charaDataDownloadDto.FileSwaps)
|
||||||
|
{
|
||||||
|
modPaths[swap.GamePath] = swap.HashOrFileSwap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CharacterData?> CreatePlayerData()
|
||||||
|
{
|
||||||
|
var chara = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
|
||||||
|
if (_dalamudUtilService.IsInGpose)
|
||||||
|
{
|
||||||
|
chara = (IPlayerCharacter?)(await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(chara.Name.TextValue, _dalamudUtilService.IsInGpose).ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chara == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using var tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player,
|
||||||
|
() => _dalamudUtilService.GetCharacterFromObjectTableByIndex(chara.ObjectIndex)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false);
|
||||||
|
PlayerData.Data.CharacterData newCdata = new();
|
||||||
|
var fragment = await _playerDataFactory.BuildCharacterData(tempHandler, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
newCdata.SetFragment(ObjectKind.Player, fragment);
|
||||||
|
if (newCdata.FileReplacements.TryGetValue(ObjectKind.Player, out var playerData) && playerData != null)
|
||||||
|
{
|
||||||
|
foreach (var data in playerData.Select(g => g.GamePaths))
|
||||||
|
{
|
||||||
|
data.RemoveWhere(g => g.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| g.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| g.EndsWith(".scd", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (g.EndsWith(".avfx", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !g.Contains("/weapon/", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !g.Contains("/equipment/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|| (g.EndsWith(".atex", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !g.Contains("/weapon/", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !g.Contains("/equipment/", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
}
|
||||||
|
|
||||||
|
playerData.RemoveWhere(g => g.GamePaths.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newCdata.ToAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_fileDownloadManager.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DownloadFilesAsync(GameObjectHandler tempHandler, List<FileReplacementData> missingFiles, Dictionary<string, string> modPaths, CancellationToken token)
|
||||||
|
{
|
||||||
|
await _fileDownloadManager.InitiateDownloadList(tempHandler, missingFiles, token).ConfigureAwait(false);
|
||||||
|
await _fileDownloadManager.DownloadFiles(tempHandler, missingFiles, token).ConfigureAwait(false);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
foreach (var file in missingFiles.SelectMany(m => m.GamePaths, (FileEntry, GamePath) => (FileEntry.Hash, GamePath)))
|
||||||
|
{
|
||||||
|
var localFile = _fileCacheManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath;
|
||||||
|
if (localFile == null)
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("File not found locally.");
|
||||||
|
}
|
||||||
|
modPaths[file.GamePath] = localFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<(MareCharaFileHeader loadedCharaFile, long expectedLength)> LoadCharaFileHeader(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var unwrapped = File.OpenRead(filePath);
|
||||||
|
using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
|
||||||
|
using var reader = new BinaryReader(lz4Stream);
|
||||||
|
var loadedCharaFile = MareCharaFileHeader.FromBinaryReader(filePath, reader);
|
||||||
|
|
||||||
|
_logger.LogInformation("Read Mare Chara File");
|
||||||
|
_logger.LogInformation("Version: {ver}", (loadedCharaFile?.Version ?? -1));
|
||||||
|
long expectedLength = 0;
|
||||||
|
if (loadedCharaFile != null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Data");
|
||||||
|
foreach (var item in loadedCharaFile.CharaFileData.FileSwaps)
|
||||||
|
{
|
||||||
|
foreach (var gamePath in item.GamePaths)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Swap: {gamePath} => {fileSwapPath}", gamePath, item.FileSwapPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemNr = 0;
|
||||||
|
foreach (var item in loadedCharaFile.CharaFileData.Files)
|
||||||
|
{
|
||||||
|
itemNr++;
|
||||||
|
expectedLength += item.Length;
|
||||||
|
foreach (var gamePath in item.GamePaths)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("File {itemNr}: {gamePath} = {len}", itemNr, gamePath, item.Length.ToByteString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Expected length: {expected}", expectedLength.ToByteString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MCDF Header was null");
|
||||||
|
}
|
||||||
|
return Task.FromResult((loadedCharaFile, expectedLength));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Could not parse MCDF header of file {file}", filePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> McdfExtractFiles(MareCharaFileHeader? charaFileHeader, long expectedLength, List<string> extractedFiles)
|
||||||
|
{
|
||||||
|
if (charaFileHeader == null) return [];
|
||||||
|
|
||||||
|
using var lz4Stream = new LZ4Stream(File.OpenRead(charaFileHeader.FilePath), LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression);
|
||||||
|
using var reader = new BinaryReader(lz4Stream);
|
||||||
|
MareCharaFileHeader.AdvanceReaderToData(reader);
|
||||||
|
|
||||||
|
long totalRead = 0;
|
||||||
|
Dictionary<string, string> gamePathToFilePath = new(StringComparer.Ordinal);
|
||||||
|
foreach (var fileData in charaFileHeader.CharaFileData.Files)
|
||||||
|
{
|
||||||
|
var fileName = Path.Combine(_fileCacheManager.CacheFolder, "mare_" + _globalFileCounter++ + ".tmp");
|
||||||
|
extractedFiles.Add(fileName);
|
||||||
|
var length = fileData.Length;
|
||||||
|
var bufferSize = length;
|
||||||
|
using var fs = File.OpenWrite(fileName);
|
||||||
|
using var wr = new BinaryWriter(fs);
|
||||||
|
_logger.LogTrace("Reading {length} of {fileName}", length.ToByteString(), fileName);
|
||||||
|
var buffer = reader.ReadBytes(bufferSize);
|
||||||
|
wr.Write(buffer);
|
||||||
|
wr.Flush();
|
||||||
|
wr.Close();
|
||||||
|
if (buffer.Length == 0) throw new EndOfStreamException("Unexpected EOF");
|
||||||
|
foreach (var path in fileData.GamePaths)
|
||||||
|
{
|
||||||
|
gamePathToFilePath[path] = fileName;
|
||||||
|
_logger.LogTrace("{path} => {fileName} [{hash}]", path, fileName, fileData.Hash);
|
||||||
|
}
|
||||||
|
totalRead += length;
|
||||||
|
_logger.LogTrace("Read {read}/{expected} bytes", totalRead.ToByteString(), expectedLength.ToByteString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return gamePathToFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateCharaDataAsync(CharaDataExtendedUpdateDto updateDto)
|
||||||
|
{
|
||||||
|
var data = await CreatePlayerData().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (data != null)
|
||||||
|
{
|
||||||
|
var hasGlamourerData = data.GlamourerData.TryGetValue(ObjectKind.Player, out var playerDataString);
|
||||||
|
if (!hasGlamourerData) updateDto.GlamourerData = null;
|
||||||
|
else updateDto.GlamourerData = playerDataString;
|
||||||
|
|
||||||
|
var hasCustomizeData = data.CustomizePlusData.TryGetValue(ObjectKind.Player, out var customizeDataString);
|
||||||
|
if (!hasCustomizeData) updateDto.CustomizeData = null;
|
||||||
|
else updateDto.CustomizeData = customizeDataString;
|
||||||
|
|
||||||
|
updateDto.ManipulationData = data.ManipulationData;
|
||||||
|
|
||||||
|
var hasFiles = data.FileReplacements.TryGetValue(ObjectKind.Player, out var fileReplacements);
|
||||||
|
if (!hasFiles)
|
||||||
|
{
|
||||||
|
updateDto.FileGamePaths = [];
|
||||||
|
updateDto.FileSwaps = [];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
updateDto.FileGamePaths = [.. fileReplacements!.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))];
|
||||||
|
updateDto.FileSwaps = [.. fileReplacements!.Where(u => !string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.FileSwapPath, path))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task SaveCharaFileAsync(string description, string filePath)
|
||||||
|
{
|
||||||
|
var tempFilePath = filePath + ".tmp";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = await CreatePlayerData().ConfigureAwait(false);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
var mareCharaFileData = _mareCharaFileDataFactory.Create(description, data);
|
||||||
|
MareCharaFileHeader output = new(MareCharaFileHeader.CurrentVersion, mareCharaFileData);
|
||||||
|
|
||||||
|
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||||
|
using var lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression);
|
||||||
|
using var writer = new BinaryWriter(lz4);
|
||||||
|
output.WriteToStream(writer);
|
||||||
|
|
||||||
|
foreach (var item in output.CharaFileData.Files)
|
||||||
|
{
|
||||||
|
var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!;
|
||||||
|
_logger.LogDebug("Saving to MCDF: {hash}:{file}", item.Hash, file.ResolvedFilepath);
|
||||||
|
_logger.LogDebug("\tAssociated GamePaths:");
|
||||||
|
foreach (var path in item.GamePaths)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("\t{path}", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fsRead = File.OpenRead(file.ResolvedFilepath);
|
||||||
|
await using (fsRead.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
using var br = new BinaryReader(fsRead);
|
||||||
|
byte[] buffer = new byte[item.Length];
|
||||||
|
br.Read(buffer, 0, item.Length);
|
||||||
|
writer.Write(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.Flush();
|
||||||
|
await lz4.FlushAsync().ConfigureAwait(false);
|
||||||
|
await fs.FlushAsync().ConfigureAwait(false);
|
||||||
|
fs.Close();
|
||||||
|
File.Move(tempFilePath, filePath, true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failure Saving Mare Chara File, deleting output");
|
||||||
|
File.Delete(tempFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
||||||
|
{
|
||||||
|
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,696 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
using MareSynchronos.Interop;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData;
|
||||||
|
|
||||||
|
public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly IpcCallerBrio _brio;
|
||||||
|
private readonly SemaphoreSlim _charaDataCreationSemaphore = new(1, 1);
|
||||||
|
private readonly CharaDataFileHandler _charaDataFileHandler;
|
||||||
|
private readonly CharaDataManager _charaDataManager;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly Dictionary<string, GposeLobbyUserData> _usersInLobby = [];
|
||||||
|
private readonly VfxSpawnManager _vfxSpawnManager;
|
||||||
|
private (CharacterData ApiData, CharaDataDownloadDto Dto)? _lastCreatedCharaData;
|
||||||
|
private PoseData? _lastDeltaPoseData;
|
||||||
|
private PoseData? _lastFullPoseData;
|
||||||
|
private WorldData? _lastWorldData;
|
||||||
|
private CancellationTokenSource _lobbyCts = new();
|
||||||
|
private int _poseGenerationExecutions = 0;
|
||||||
|
|
||||||
|
public CharaDataGposeTogetherManager(ILogger<CharaDataGposeTogetherManager> logger, MareMediator mediator,
|
||||||
|
ApiController apiController, IpcCallerBrio brio, DalamudUtilService dalamudUtil, VfxSpawnManager vfxSpawnManager,
|
||||||
|
CharaDataFileHandler charaDataFileHandler, CharaDataManager charaDataManager) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
Mediator.Subscribe<GposeLobbyUserJoin>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnUserJoinLobby(msg.UserData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyUserLeave>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnUserLeaveLobby(msg.UserData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyReceiveCharaData>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnReceiveCharaData(msg.CharaDataDownloadDto);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyReceivePoseData>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnReceivePoseData(msg.UserData, msg.PoseData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GPoseLobbyReceiveWorldData>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnReceiveWorldData(msg.UserData, msg.WorldData);
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_usersInLobby.Count > 0 && !string.IsNullOrEmpty(CurrentGPoseLobbyId))
|
||||||
|
{
|
||||||
|
JoinGPoseLobby(CurrentGPoseLobbyId, isReconnecting: true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LeaveGPoseLobby();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GposeStartMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnEnterGpose();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<GposeEndMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnExitGpose();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnFrameworkUpdate();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
OnCutsceneFrameworkUpdate();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
LeaveGPoseLobby();
|
||||||
|
});
|
||||||
|
|
||||||
|
_apiController = apiController;
|
||||||
|
_brio = brio;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_vfxSpawnManager = vfxSpawnManager;
|
||||||
|
_charaDataFileHandler = charaDataFileHandler;
|
||||||
|
_charaDataManager = charaDataManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? CurrentGPoseLobbyId { get; private set; }
|
||||||
|
public string? LastGPoseLobbyId { get; private set; }
|
||||||
|
|
||||||
|
public IEnumerable<GposeLobbyUserData> UsersInLobby => _usersInLobby.Values;
|
||||||
|
|
||||||
|
public (bool SameMap, bool SameServer, bool SameEverything) IsOnSameMapAndServer(GposeLobbyUserData data)
|
||||||
|
{
|
||||||
|
return (data.Map.RowId == _lastWorldData?.LocationInfo.MapId, data.WorldData?.LocationInfo.ServerId == _lastWorldData?.LocationInfo.ServerId, data.WorldData?.LocationInfo == _lastWorldData?.LocationInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PushCharacterDownloadDto()
|
||||||
|
{
|
||||||
|
var playerData = await _charaDataFileHandler.CreatePlayerData().ConfigureAwait(false);
|
||||||
|
if (playerData == null) return;
|
||||||
|
if (!string.Equals(playerData.DataHash.Value, _lastCreatedCharaData?.ApiData.DataHash.Value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
List<GamePathEntry> filegamePaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
|
||||||
|
.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))];
|
||||||
|
List<GamePathEntry> fileSwapPaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
|
||||||
|
.Where(u => !string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.FileSwapPath, path))];
|
||||||
|
await _charaDataManager.UploadFiles([.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player]
|
||||||
|
.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))])
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
CharaDataDownloadDto charaDataDownloadDto = new($"GPOSELOBBY:{CurrentGPoseLobbyId}", new(_apiController.UID))
|
||||||
|
{
|
||||||
|
UpdatedDate = DateTime.UtcNow,
|
||||||
|
ManipulationData = playerData.ManipulationData,
|
||||||
|
CustomizeData = playerData.CustomizePlusData[API.Data.Enum.ObjectKind.Player],
|
||||||
|
FileGamePaths = filegamePaths,
|
||||||
|
FileSwaps = fileSwapPaths,
|
||||||
|
GlamourerData = playerData.GlamourerData[API.Data.Enum.ObjectKind.Player],
|
||||||
|
};
|
||||||
|
|
||||||
|
_lastCreatedCharaData = (playerData, charaDataDownloadDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
ForceResendOwnData();
|
||||||
|
|
||||||
|
if (_lastCreatedCharaData != null)
|
||||||
|
await _apiController.GposeLobbyPushCharacterData(_lastCreatedCharaData.Value.Dto)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CreateNewLobby()
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
ClearLobby();
|
||||||
|
CurrentGPoseLobbyId = await _apiController.GposeLobbyCreate().ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrEmpty(CurrentGPoseLobbyId))
|
||||||
|
{
|
||||||
|
_ = GposeWorldPositionBackgroundTask(_lobbyCts.Token);
|
||||||
|
_ = GposePoseDataBackgroundTask(_lobbyCts.Token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void JoinGPoseLobby(string joinLobbyId, bool isReconnecting = false)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var otherUsers = await _apiController.GposeLobbyJoin(joinLobbyId).ConfigureAwait(false);
|
||||||
|
ClearLobby();
|
||||||
|
if (otherUsers.Any())
|
||||||
|
{
|
||||||
|
LastGPoseLobbyId = string.Empty;
|
||||||
|
|
||||||
|
foreach (var user in otherUsers)
|
||||||
|
{
|
||||||
|
OnUserJoinLobby(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentGPoseLobbyId = joinLobbyId;
|
||||||
|
_ = GposeWorldPositionBackgroundTask(_lobbyCts.Token);
|
||||||
|
_ = GposePoseDataBackgroundTask(_lobbyCts.Token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LeaveGPoseLobby();
|
||||||
|
LastGPoseLobbyId = string.Empty;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void LeaveGPoseLobby()
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var left = await _apiController.GposeLobbyLeave().ConfigureAwait(false);
|
||||||
|
if (left)
|
||||||
|
{
|
||||||
|
if (_usersInLobby.Count != 0)
|
||||||
|
{
|
||||||
|
LastGPoseLobbyId = CurrentGPoseLobbyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearLobby(revertCharas: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
ClearLobby(revertCharas: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearLobby(bool revertCharas = false)
|
||||||
|
{
|
||||||
|
_lobbyCts.Cancel();
|
||||||
|
_lobbyCts.Dispose();
|
||||||
|
_lobbyCts = new();
|
||||||
|
CurrentGPoseLobbyId = string.Empty;
|
||||||
|
foreach (var user in _usersInLobby.ToDictionary())
|
||||||
|
{
|
||||||
|
if (revertCharas)
|
||||||
|
_charaDataManager.RevertChara(user.Value.HandledChara);
|
||||||
|
OnUserLeaveLobby(user.Value.UserData);
|
||||||
|
}
|
||||||
|
_usersInLobby.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateJsonFromPoseData(PoseData? poseData)
|
||||||
|
{
|
||||||
|
if (poseData == null) return "{}";
|
||||||
|
|
||||||
|
var node = new JsonObject();
|
||||||
|
node["Bones"] = new JsonObject();
|
||||||
|
foreach (var bone in poseData.Value.Bones)
|
||||||
|
{
|
||||||
|
node["Bones"]![bone.Key] = new JsonObject();
|
||||||
|
node["Bones"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["Bones"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["Bones"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
node["MainHand"] = new JsonObject();
|
||||||
|
foreach (var bone in poseData.Value.MainHand)
|
||||||
|
{
|
||||||
|
node["MainHand"]![bone.Key] = new JsonObject();
|
||||||
|
node["MainHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["MainHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["MainHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
node["OffHand"] = new JsonObject();
|
||||||
|
foreach (var bone in poseData.Value.OffHand)
|
||||||
|
{
|
||||||
|
node["OffHand"]![bone.Key] = new JsonObject();
|
||||||
|
node["OffHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["OffHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
node["OffHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.ToJsonString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PoseData CreatePoseDataFromJson(string json, PoseData? fullPoseData = null)
|
||||||
|
{
|
||||||
|
PoseData output = new();
|
||||||
|
output.Bones = new(StringComparer.Ordinal);
|
||||||
|
output.MainHand = new(StringComparer.Ordinal);
|
||||||
|
output.OffHand = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
float getRounded(string number)
|
||||||
|
{
|
||||||
|
return float.Round(float.Parse(number, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoneData createBoneData(JsonNode boneJson)
|
||||||
|
{
|
||||||
|
BoneData outputBoneData = new();
|
||||||
|
outputBoneData.Exists = true;
|
||||||
|
var posString = boneJson["Position"]!.ToString();
|
||||||
|
var pos = posString.Split(",", StringSplitOptions.TrimEntries);
|
||||||
|
outputBoneData.PositionX = getRounded(pos[0]);
|
||||||
|
outputBoneData.PositionY = getRounded(pos[1]);
|
||||||
|
outputBoneData.PositionZ = getRounded(pos[2]);
|
||||||
|
|
||||||
|
var scaString = boneJson["Scale"]!.ToString();
|
||||||
|
var sca = scaString.Split(",", StringSplitOptions.TrimEntries);
|
||||||
|
outputBoneData.ScaleX = getRounded(sca[0]);
|
||||||
|
outputBoneData.ScaleY = getRounded(sca[1]);
|
||||||
|
outputBoneData.ScaleZ = getRounded(sca[2]);
|
||||||
|
|
||||||
|
var rotString = boneJson["Rotation"]!.ToString();
|
||||||
|
var rot = rotString.Split(",", StringSplitOptions.TrimEntries);
|
||||||
|
outputBoneData.RotationX = getRounded(rot[0]);
|
||||||
|
outputBoneData.RotationY = getRounded(rot[1]);
|
||||||
|
outputBoneData.RotationZ = getRounded(rot[2]);
|
||||||
|
outputBoneData.RotationW = getRounded(rot[3]);
|
||||||
|
return outputBoneData;
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = JsonNode.Parse(json)!;
|
||||||
|
var bones = node["Bones"]!.AsObject();
|
||||||
|
foreach (var bone in bones)
|
||||||
|
{
|
||||||
|
string name = bone.Key;
|
||||||
|
var boneJson = bone.Value!.AsObject();
|
||||||
|
BoneData outputBoneData = createBoneData(boneJson);
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
{
|
||||||
|
if (fullPoseData.Value.Bones.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
|
||||||
|
{
|
||||||
|
output.Bones[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.Bones[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mainHand = node["MainHand"]!.AsObject();
|
||||||
|
foreach (var bone in mainHand)
|
||||||
|
{
|
||||||
|
string name = bone.Key;
|
||||||
|
var boneJson = bone.Value!.AsObject();
|
||||||
|
BoneData outputBoneData = createBoneData(boneJson);
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
{
|
||||||
|
if (fullPoseData.Value.MainHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
|
||||||
|
{
|
||||||
|
output.MainHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.MainHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var offhand = node["OffHand"]!.AsObject();
|
||||||
|
foreach (var bone in offhand)
|
||||||
|
{
|
||||||
|
string name = bone.Key;
|
||||||
|
var boneJson = bone.Value!.AsObject();
|
||||||
|
BoneData outputBoneData = createBoneData(boneJson);
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
{
|
||||||
|
if (fullPoseData.Value.OffHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData)
|
||||||
|
{
|
||||||
|
output.OffHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.OffHand[name] = outputBoneData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPoseData != null)
|
||||||
|
output.IsDelta = true;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GposePoseDataBackgroundTask(CancellationToken ct)
|
||||||
|
{
|
||||||
|
_lastFullPoseData = null;
|
||||||
|
_lastDeltaPoseData = null;
|
||||||
|
_poseGenerationExecutions = 0;
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), ct).ConfigureAwait(false);
|
||||||
|
if (!_dalamudUtil.IsInGpose) continue;
|
||||||
|
if (_usersInLobby.Count == 0) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chara = await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false);
|
||||||
|
if (_dalamudUtil.IsInGpose)
|
||||||
|
{
|
||||||
|
chara = (IPlayerCharacter?)(await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(chara.Name.TextValue, _dalamudUtil.IsInGpose).ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
if (chara == null || chara.Address == nint.Zero) continue;
|
||||||
|
|
||||||
|
var poseJson = await _brio.GetPoseAsync(chara.Address).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(poseJson)) continue;
|
||||||
|
|
||||||
|
var lastFullData = _poseGenerationExecutions++ >= 12 ? null : _lastFullPoseData;
|
||||||
|
lastFullData = _forceResendFullPose ? _lastFullPoseData : lastFullData;
|
||||||
|
|
||||||
|
var poseData = CreatePoseDataFromJson(poseJson, lastFullData);
|
||||||
|
if (!poseData.IsDelta)
|
||||||
|
{
|
||||||
|
_lastFullPoseData = poseData;
|
||||||
|
_lastDeltaPoseData = null;
|
||||||
|
_poseGenerationExecutions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deltaIsSame = _lastDeltaPoseData != null &&
|
||||||
|
(poseData.Bones.Keys.All(k => _lastDeltaPoseData.Value.Bones.ContainsKey(k)
|
||||||
|
&& poseData.Bones.Values.All(k => _lastDeltaPoseData.Value.Bones.ContainsValue(k))));
|
||||||
|
|
||||||
|
if (_forceResendFullPose || ((poseData.Bones.Any() || poseData.MainHand.Any() || poseData.OffHand.Any())
|
||||||
|
&& (!poseData.IsDelta || (poseData.IsDelta && !deltaIsSame))))
|
||||||
|
{
|
||||||
|
_forceResendFullPose = false;
|
||||||
|
await _apiController.GposeLobbyPushPoseData(poseData).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poseData.IsDelta)
|
||||||
|
_lastDeltaPoseData = poseData;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during Pose Data Generation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GposeWorldPositionBackgroundTask(CancellationToken ct)
|
||||||
|
{
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(_dalamudUtil.IsInGpose ? 10 : 1), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// if there are no players in lobby, don't do anything
|
||||||
|
if (_usersInLobby.Count == 0) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// get own player data
|
||||||
|
var player = (Dalamud.Game.ClientState.Objects.Types.ICharacter?)(await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false));
|
||||||
|
if (player == null) continue;
|
||||||
|
WorldData worldData;
|
||||||
|
if (_dalamudUtil.IsInGpose)
|
||||||
|
{
|
||||||
|
player = await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(player.Name.TextValue, true).ConfigureAwait(false);
|
||||||
|
if (player == null) continue;
|
||||||
|
worldData = (await _brio.GetTransformAsync(player.Address).ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var rotQuaternion = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), player.Rotation);
|
||||||
|
worldData = new()
|
||||||
|
{
|
||||||
|
PositionX = player.Position.X,
|
||||||
|
PositionY = player.Position.Y,
|
||||||
|
PositionZ = player.Position.Z,
|
||||||
|
RotationW = rotQuaternion.W,
|
||||||
|
RotationX = rotQuaternion.X,
|
||||||
|
RotationY = rotQuaternion.Y,
|
||||||
|
RotationZ = rotQuaternion.Z,
|
||||||
|
ScaleX = 1,
|
||||||
|
ScaleY = 1,
|
||||||
|
ScaleZ = 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false);
|
||||||
|
worldData.LocationInfo = loc;
|
||||||
|
|
||||||
|
if (_forceResendWorldData || worldData != _lastWorldData)
|
||||||
|
{
|
||||||
|
_forceResendWorldData = false;
|
||||||
|
await _apiController.GposeLobbyPushWorldData(worldData).ConfigureAwait(false);
|
||||||
|
_lastWorldData = worldData;
|
||||||
|
Logger.LogTrace("WorldData (gpose: {gpose}): {data}", _dalamudUtil.IsInGpose, worldData);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in _usersInLobby)
|
||||||
|
{
|
||||||
|
if (!entry.Value.HasWorldDataUpdate || _dalamudUtil.IsInGpose || entry.Value.WorldData == null) continue;
|
||||||
|
|
||||||
|
var entryWorldData = entry.Value.WorldData!.Value;
|
||||||
|
|
||||||
|
if (worldData.LocationInfo.MapId == entryWorldData.LocationInfo.MapId && worldData.LocationInfo.DivisionId == entryWorldData.LocationInfo.DivisionId
|
||||||
|
&& (worldData.LocationInfo.HouseId != entryWorldData.LocationInfo.HouseId
|
||||||
|
|| worldData.LocationInfo.WardId != entryWorldData.LocationInfo.WardId
|
||||||
|
|| entryWorldData.LocationInfo.ServerId != worldData.LocationInfo.ServerId))
|
||||||
|
{
|
||||||
|
if (entry.Value.SpawnedVfxId == null)
|
||||||
|
{
|
||||||
|
// spawn if it doesn't exist yet
|
||||||
|
entry.Value.LastWorldPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ);
|
||||||
|
entry.Value.SpawnedVfxId = await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.SpawnObject(entry.Value.LastWorldPosition.Value,
|
||||||
|
Quaternion.Identity, Vector3.One, 0.5f, 0.1f, 0.5f, 0.9f)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// move object via lerp if it does exist
|
||||||
|
var newPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ);
|
||||||
|
if (newPosition != entry.Value.LastWorldPosition)
|
||||||
|
{
|
||||||
|
entry.Value.UpdateStart = DateTime.UtcNow;
|
||||||
|
entry.Value.TargetWorldPosition = newPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(entry.Value.SpawnedVfxId)).ConfigureAwait(false);
|
||||||
|
entry.Value.SpawnedVfxId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during World Data Generation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCutsceneFrameworkUpdate()
|
||||||
|
{
|
||||||
|
foreach (var kvp in _usersInLobby)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(kvp.Value.AssociatedCharaName))
|
||||||
|
{
|
||||||
|
kvp.Value.Address = _dalamudUtil.GetGposeCharacterFromObjectTableByName(kvp.Value.AssociatedCharaName, true)?.Address ?? nint.Zero;
|
||||||
|
if (kvp.Value.Address == nint.Zero)
|
||||||
|
{
|
||||||
|
kvp.Value.AssociatedCharaName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kvp.Value.Address != nint.Zero && (kvp.Value.HasWorldDataUpdate || kvp.Value.HasPoseDataUpdate))
|
||||||
|
{
|
||||||
|
bool hadPoseDataUpdate = kvp.Value.HasPoseDataUpdate;
|
||||||
|
bool hadWorldDataUpdate = kvp.Value.HasWorldDataUpdate;
|
||||||
|
kvp.Value.HasPoseDataUpdate = false;
|
||||||
|
kvp.Value.HasWorldDataUpdate = false;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
if (hadPoseDataUpdate && kvp.Value.ApplicablePoseData != null)
|
||||||
|
{
|
||||||
|
await _brio.SetPoseAsync(kvp.Value.Address, CreateJsonFromPoseData(kvp.Value.ApplicablePoseData)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
if (hadWorldDataUpdate && kvp.Value.WorldData != null)
|
||||||
|
{
|
||||||
|
await _brio.ApplyTransformAsync(kvp.Value.Address, kvp.Value.WorldData.Value).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnterGpose()
|
||||||
|
{
|
||||||
|
ForceResendOwnData();
|
||||||
|
ResetOwnData();
|
||||||
|
foreach (var data in _usersInLobby.Values)
|
||||||
|
{
|
||||||
|
_ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(data.SpawnedVfxId));
|
||||||
|
data.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExitGpose()
|
||||||
|
{
|
||||||
|
ForceResendOwnData();
|
||||||
|
ResetOwnData();
|
||||||
|
foreach (var data in _usersInLobby.Values)
|
||||||
|
{
|
||||||
|
data.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private bool _forceResendFullPose = false;
|
||||||
|
private bool _forceResendWorldData = false;
|
||||||
|
|
||||||
|
private void ForceResendOwnData()
|
||||||
|
{
|
||||||
|
_forceResendFullPose = true;
|
||||||
|
_forceResendWorldData = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetOwnData()
|
||||||
|
{
|
||||||
|
_poseGenerationExecutions = 0;
|
||||||
|
_lastCreatedCharaData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate()
|
||||||
|
{
|
||||||
|
var frameworkTime = DateTime.UtcNow;
|
||||||
|
foreach (var kvp in _usersInLobby)
|
||||||
|
{
|
||||||
|
if (kvp.Value.SpawnedVfxId != null && kvp.Value.UpdateStart != null)
|
||||||
|
{
|
||||||
|
var secondsElasped = frameworkTime.Subtract(kvp.Value.UpdateStart.Value).TotalSeconds;
|
||||||
|
if (secondsElasped >= 1)
|
||||||
|
{
|
||||||
|
kvp.Value.LastWorldPosition = kvp.Value.TargetWorldPosition;
|
||||||
|
kvp.Value.TargetWorldPosition = null;
|
||||||
|
kvp.Value.UpdateStart = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var lerp = Vector3.Lerp(kvp.Value.LastWorldPosition ?? Vector3.One, kvp.Value.TargetWorldPosition ?? Vector3.One, (float)secondsElasped);
|
||||||
|
_vfxSpawnManager.MoveObject(kvp.Value.SpawnedVfxId.Value, lerp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReceiveCharaData(CharaDataDownloadDto charaDataDownloadDto)
|
||||||
|
{
|
||||||
|
if (!_usersInLobby.TryGetValue(charaDataDownloadDto.Uploader.UID, out var lobbyData))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lobbyData.CharaData = charaDataDownloadDto;
|
||||||
|
if (lobbyData.Address != nint.Zero && !string.IsNullOrEmpty(lobbyData.AssociatedCharaName))
|
||||||
|
{
|
||||||
|
_ = ApplyCharaData(lobbyData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ApplyCharaData(GposeLobbyUserData userData)
|
||||||
|
{
|
||||||
|
if (userData.CharaData == null || userData.Address == nint.Zero || string.IsNullOrEmpty(userData.AssociatedCharaName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _charaDataCreationSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _charaDataManager.ApplyCharaData(userData.CharaData!, userData.AssociatedCharaName).ConfigureAwait(false);
|
||||||
|
userData.LastAppliedCharaDataDate = userData.CharaData.UpdatedDate;
|
||||||
|
userData.HasPoseDataUpdate = true;
|
||||||
|
userData.HasWorldDataUpdate = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_charaDataCreationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _charaDataSpawnSemaphore = new(1, 1);
|
||||||
|
|
||||||
|
internal async Task SpawnAndApplyData(GposeLobbyUserData userData)
|
||||||
|
{
|
||||||
|
if (userData.CharaData == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _charaDataSpawnSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
userData.HasPoseDataUpdate = false;
|
||||||
|
userData.HasWorldDataUpdate = false;
|
||||||
|
var chara = await _charaDataManager.SpawnAndApplyData(userData.CharaData).ConfigureAwait(false);
|
||||||
|
if (chara == null) return;
|
||||||
|
userData.HandledChara = chara;
|
||||||
|
userData.AssociatedCharaName = chara.Name;
|
||||||
|
userData.HasPoseDataUpdate = true;
|
||||||
|
userData.HasWorldDataUpdate = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_charaDataSpawnSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReceivePoseData(UserData userData, PoseData poseData)
|
||||||
|
{
|
||||||
|
if (!_usersInLobby.TryGetValue(userData.UID, out var lobbyData))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poseData.IsDelta)
|
||||||
|
lobbyData.DeltaPoseData = poseData;
|
||||||
|
else
|
||||||
|
lobbyData.FullPoseData = poseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReceiveWorldData(UserData userData, WorldData worldData)
|
||||||
|
{
|
||||||
|
_usersInLobby[userData.UID].WorldData = worldData;
|
||||||
|
_ = _usersInLobby[userData.UID].SetWorldDataDescriptor(_dalamudUtil);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserJoinLobby(UserData userData)
|
||||||
|
{
|
||||||
|
if (_usersInLobby.ContainsKey(userData.UID))
|
||||||
|
OnUserLeaveLobby(userData);
|
||||||
|
_usersInLobby[userData.UID] = new(userData);
|
||||||
|
_ = PushCharacterDownloadDto();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUserLeaveLobby(UserData msg)
|
||||||
|
{
|
||||||
|
_usersInLobby.Remove(msg.UID, out var existingData);
|
||||||
|
if (existingData != default)
|
||||||
|
{
|
||||||
|
_ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(existingData.SpawnedVfxId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1035
MareSynchronos/Services/CharaData/CharaDataManager.cs
Normal file
1035
MareSynchronos/Services/CharaData/CharaDataManager.cs
Normal file
File diff suppressed because it is too large
Load Diff
296
MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
Normal file
296
MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.Interop;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services;
|
||||||
|
|
||||||
|
public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
public record NearbyCharaDataEntry
|
||||||
|
{
|
||||||
|
public float Direction { get; init; }
|
||||||
|
public float Distance { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly Dictionary<PoseEntryExtended, NearbyCharaDataEntry> _nearbyData = [];
|
||||||
|
private readonly Dictionary<PoseEntryExtended, Guid> _poseVfx = [];
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly CharaDataConfigService _charaDataConfigService;
|
||||||
|
private readonly Dictionary<UserData, List<CharaDataMetaInfoExtendedDto>> _metaInfoCache = [];
|
||||||
|
private readonly VfxSpawnManager _vfxSpawnManager;
|
||||||
|
private Task? _filterEntriesRunningTask;
|
||||||
|
private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null;
|
||||||
|
private DateTime _lastExecutionTime = DateTime.UtcNow;
|
||||||
|
private SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1);
|
||||||
|
public CharaDataNearbyManager(ILogger<CharaDataNearbyManager> logger, MareMediator mediator,
|
||||||
|
DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager,
|
||||||
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
|
CharaDataConfigService charaDataConfigService) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => HandleFrameworkUpdate());
|
||||||
|
mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, (_) => HandleFrameworkUpdate());
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_vfxSpawnManager = vfxSpawnManager;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
_charaDataConfigService = charaDataConfigService;
|
||||||
|
mediator.Subscribe<GposeStartMessage>(this, (_) => ClearAllVfx());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ComputeNearbyData { get; set; } = false;
|
||||||
|
|
||||||
|
public IDictionary<PoseEntryExtended, NearbyCharaDataEntry> NearbyData => _nearbyData;
|
||||||
|
|
||||||
|
public string UserNoteFilter { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public void UpdateSharedData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
|
||||||
|
{
|
||||||
|
_sharedDataUpdateSemaphore.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_metaInfoCache.Clear();
|
||||||
|
foreach (var kvp in newData)
|
||||||
|
{
|
||||||
|
if (kvp.Value == null) continue;
|
||||||
|
|
||||||
|
if (!_metaInfoCache.TryGetValue(kvp.Value.Uploader, out var list))
|
||||||
|
{
|
||||||
|
_metaInfoCache[kvp.Value.Uploader] = list = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_sharedDataUpdateSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetHoveredVfx(PoseEntryExtended? hoveredPose)
|
||||||
|
{
|
||||||
|
if (hoveredPose == null && _hoveredVfx == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (hoveredPose == null)
|
||||||
|
{
|
||||||
|
_vfxSpawnManager.DespawnObject(_hoveredVfx!.Value.VfxId);
|
||||||
|
_hoveredVfx = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hoveredVfx == null)
|
||||||
|
{
|
||||||
|
var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f);
|
||||||
|
if (vfxGuid != null)
|
||||||
|
_hoveredVfx = (vfxGuid.Value, hoveredPose);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hoveredPose != _hoveredVfx!.Value.Pose)
|
||||||
|
{
|
||||||
|
_vfxSpawnManager.DespawnObject(_hoveredVfx.Value.VfxId);
|
||||||
|
var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f);
|
||||||
|
if (vfxGuid != null)
|
||||||
|
_hoveredVfx = (vfxGuid.Value, hoveredPose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
ClearAllVfx();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float CalculateYawDegrees(Vector3 directionXZ)
|
||||||
|
{
|
||||||
|
// Calculate yaw angle in radians using Atan2 (X, Z)
|
||||||
|
float yawRadians = (float)Math.Atan2(-directionXZ.X, directionXZ.Z);
|
||||||
|
float yawDegrees = yawRadians * (180f / (float)Math.PI);
|
||||||
|
|
||||||
|
// Normalize to [0, 360)
|
||||||
|
if (yawDegrees < 0)
|
||||||
|
yawDegrees += 360f;
|
||||||
|
|
||||||
|
return yawDegrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float GetAngleToTarget(Vector3 cameraPosition, float cameraYawDegrees, Vector3 targetPosition)
|
||||||
|
{
|
||||||
|
// Step 4: Calculate the direction vector from camera to target
|
||||||
|
Vector3 directionToTarget = targetPosition - cameraPosition;
|
||||||
|
|
||||||
|
// Step 5: Project the directionToTarget onto the XZ plane (ignore Y)
|
||||||
|
Vector3 directionToTargetXZ = new Vector3(directionToTarget.X, 0, directionToTarget.Z);
|
||||||
|
|
||||||
|
// Handle the case where the target is directly above or below the camera
|
||||||
|
if (directionToTargetXZ.LengthSquared() < 1e-10f)
|
||||||
|
{
|
||||||
|
return 0; // Default direction
|
||||||
|
}
|
||||||
|
|
||||||
|
directionToTargetXZ = Vector3.Normalize(directionToTargetXZ);
|
||||||
|
|
||||||
|
// Step 6: Calculate the target's yaw angle
|
||||||
|
float targetYawDegrees = CalculateYawDegrees(directionToTargetXZ);
|
||||||
|
|
||||||
|
// Step 7: Calculate relative angle
|
||||||
|
float relativeAngle = targetYawDegrees - cameraYawDegrees;
|
||||||
|
if (relativeAngle < 0)
|
||||||
|
relativeAngle += 360f;
|
||||||
|
|
||||||
|
// Step 8: Map relative angle to ArrowDirection
|
||||||
|
return relativeAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float GetCameraYaw(Vector3 cameraPosition, Vector3 lookAtVector)
|
||||||
|
{
|
||||||
|
// Step 1: Calculate the direction vector from camera to LookAtPoint
|
||||||
|
Vector3 directionFacing = lookAtVector - cameraPosition;
|
||||||
|
|
||||||
|
// Step 2: Project the directionFacing onto the XZ plane (ignore Y)
|
||||||
|
Vector3 directionFacingXZ = new Vector3(directionFacing.X, 0, directionFacing.Z);
|
||||||
|
|
||||||
|
// Handle the case where the LookAtPoint is directly above or below the camera
|
||||||
|
if (directionFacingXZ.LengthSquared() < 1e-10f)
|
||||||
|
{
|
||||||
|
// Default to facing forward along the Z-axis if LookAtPoint is directly above or below
|
||||||
|
directionFacingXZ = new Vector3(0, 0, 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
directionFacingXZ = Vector3.Normalize(directionFacingXZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Calculate the camera's yaw angle based on directionFacingXZ
|
||||||
|
return (CalculateYawDegrees(directionFacingXZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearAllVfx()
|
||||||
|
{
|
||||||
|
foreach (var vfx in _poseVfx)
|
||||||
|
{
|
||||||
|
_vfxSpawnManager.DespawnObject(vfx.Value);
|
||||||
|
}
|
||||||
|
_poseVfx.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FilterEntriesAsync(Vector3 cameraPos, Vector3 cameraLookAt)
|
||||||
|
{
|
||||||
|
var previousPoses = _nearbyData.Keys.ToList();
|
||||||
|
_nearbyData.Clear();
|
||||||
|
|
||||||
|
var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false);
|
||||||
|
var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
|
var currentServer = player.CurrentWorld;
|
||||||
|
var playerPos = player.Position;
|
||||||
|
|
||||||
|
var cameraYaw = GetCameraYaw(cameraPos, cameraLookAt);
|
||||||
|
|
||||||
|
bool ignoreHousingLimits = _charaDataConfigService.Current.NearbyIgnoreHousingLimitations;
|
||||||
|
bool onlyCurrentServer = _charaDataConfigService.Current.NearbyOwnServerOnly;
|
||||||
|
bool showOwnData = _charaDataConfigService.Current.NearbyShowOwnData;
|
||||||
|
|
||||||
|
// initial filter on name
|
||||||
|
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|
||||||
|
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
|
||||||
|
.ToDictionary(k => k.Key, k => k.Value))
|
||||||
|
{
|
||||||
|
// filter all poses based on territory, that always must be correct
|
||||||
|
foreach (var pose in data.Value.Where(v => v.HasPoses && v.HasWorldData && (showOwnData || !v.IsOwnData))
|
||||||
|
.SelectMany(k => k.PoseExtended)
|
||||||
|
.Where(p => p.HasPoseData
|
||||||
|
&& p.HasWorldData
|
||||||
|
&& p.WorldData!.Value.LocationInfo.TerritoryId == ownLocation.TerritoryId)
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
var poseLocation = pose.WorldData!.Value.LocationInfo;
|
||||||
|
|
||||||
|
bool isInHousing = poseLocation.WardId != 0;
|
||||||
|
var distance = Vector3.Distance(playerPos, pose.Position);
|
||||||
|
if (distance > _charaDataConfigService.Current.NearbyDistanceFilter) continue;
|
||||||
|
|
||||||
|
|
||||||
|
bool addEntry = (!isInHousing && poseLocation.MapId == ownLocation.MapId
|
||||||
|
&& (!onlyCurrentServer || poseLocation.ServerId == currentServer.RowId))
|
||||||
|
|| (isInHousing
|
||||||
|
&& (((ignoreHousingLimits && !onlyCurrentServer)
|
||||||
|
|| (ignoreHousingLimits && onlyCurrentServer) && poseLocation.ServerId == currentServer.RowId)
|
||||||
|
|| poseLocation.ServerId == currentServer.RowId)
|
||||||
|
&& ((poseLocation.HouseId == 0 && poseLocation.DivisionId == ownLocation.DivisionId
|
||||||
|
&& (ignoreHousingLimits || poseLocation.WardId == ownLocation.WardId))
|
||||||
|
|| (poseLocation.HouseId > 0
|
||||||
|
&& (ignoreHousingLimits || (poseLocation.HouseId == ownLocation.HouseId && poseLocation.WardId == ownLocation.WardId && poseLocation.DivisionId == ownLocation.DivisionId && poseLocation.RoomId == ownLocation.RoomId)))
|
||||||
|
));
|
||||||
|
|
||||||
|
if (addEntry)
|
||||||
|
_nearbyData[pose] = new() { Direction = GetAngleToTarget(cameraPos, cameraYaw, pose.Position), Distance = distance };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombatOrPerforming)
|
||||||
|
await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void HandleFrameworkUpdate()
|
||||||
|
{
|
||||||
|
if (_lastExecutionTime.AddSeconds(0.5) > DateTime.UtcNow) return;
|
||||||
|
_lastExecutionTime = DateTime.UtcNow;
|
||||||
|
if (!ComputeNearbyData && !_charaDataConfigService.Current.NearbyShowAlways)
|
||||||
|
{
|
||||||
|
if (_nearbyData.Any())
|
||||||
|
_nearbyData.Clear();
|
||||||
|
if (_poseVfx.Any())
|
||||||
|
ClearAllVfx();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming)
|
||||||
|
ClearAllVfx();
|
||||||
|
|
||||||
|
var camera = CameraManager.Instance()->CurrentCamera;
|
||||||
|
Vector3 cameraPos = new(camera->Position.X, camera->Position.Y, camera->Position.Z);
|
||||||
|
Vector3 lookAt = new(camera->LookAtVector.X, camera->LookAtVector.Y, camera->LookAtVector.Z);
|
||||||
|
|
||||||
|
if (_filterEntriesRunningTask?.IsCompleted ?? true && _dalamudUtilService.IsLoggedIn)
|
||||||
|
_filterEntriesRunningTask = FilterEntriesAsync(cameraPos, lookAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
|
||||||
|
{
|
||||||
|
foreach (var data in _nearbyData.Keys)
|
||||||
|
{
|
||||||
|
if (_poseVfx.TryGetValue(data, out var _)) continue;
|
||||||
|
|
||||||
|
Guid? vfxGuid;
|
||||||
|
if (data.MetaInfo.IsOwnData)
|
||||||
|
{
|
||||||
|
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
|
||||||
|
}
|
||||||
|
if (vfxGuid != null)
|
||||||
|
{
|
||||||
|
_poseVfx[data] = vfxGuid.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var data in previousPoses.Except(_nearbyData.Keys))
|
||||||
|
{
|
||||||
|
if (_poseVfx.Remove(data, out var guid))
|
||||||
|
{
|
||||||
|
_vfxSpawnManager.DespawnObject(guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData
|
||||||
|
{
|
||||||
|
internal class CharaDataTogetherManager
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData;
|
||||||
|
|
||||||
|
public sealed class MareCharaFileDataFactory
|
||||||
|
{
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
|
||||||
|
public MareCharaFileDataFactory(FileCacheManager fileCacheManager)
|
||||||
|
{
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MareCharaFileData Create(string description, CharacterData characterCacheDto)
|
||||||
|
{
|
||||||
|
return new MareCharaFileData(_fileCacheManager, description, characterCacheDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
public sealed record CharaDataExtendedUpdateDto : CharaDataUpdateDto
|
||||||
|
{
|
||||||
|
private readonly CharaDataFullDto _charaDataFullDto;
|
||||||
|
|
||||||
|
public CharaDataExtendedUpdateDto(CharaDataUpdateDto dto, CharaDataFullDto charaDataFullDto) : base(dto)
|
||||||
|
{
|
||||||
|
_charaDataFullDto = charaDataFullDto;
|
||||||
|
_userList = charaDataFullDto.AllowedUsers.ToList();
|
||||||
|
_groupList = charaDataFullDto.AllowedGroups.ToList();
|
||||||
|
_poseList = charaDataFullDto.PoseData.Select(k => new PoseEntry(k.Id)
|
||||||
|
{
|
||||||
|
Description = k.Description,
|
||||||
|
PoseData = k.PoseData,
|
||||||
|
WorldData = k.WorldData
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharaDataUpdateDto BaseDto => new(Id)
|
||||||
|
{
|
||||||
|
AllowedUsers = AllowedUsers,
|
||||||
|
AllowedGroups = AllowedGroups,
|
||||||
|
AccessType = base.AccessType,
|
||||||
|
CustomizeData = base.CustomizeData,
|
||||||
|
Description = base.Description,
|
||||||
|
ExpiryDate = base.ExpiryDate,
|
||||||
|
FileGamePaths = base.FileGamePaths,
|
||||||
|
FileSwaps = base.FileSwaps,
|
||||||
|
GlamourerData = base.GlamourerData,
|
||||||
|
ShareType = base.ShareType,
|
||||||
|
ManipulationData = base.ManipulationData,
|
||||||
|
Poses = Poses
|
||||||
|
};
|
||||||
|
|
||||||
|
public new string ManipulationData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.ManipulationData ?? _charaDataFullDto.ManipulationData;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.ManipulationData = value;
|
||||||
|
if (string.Equals(base.ManipulationData, _charaDataFullDto.ManipulationData, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
base.ManipulationData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new string Description
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.Description ?? _charaDataFullDto.Description;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.Description = value;
|
||||||
|
if (string.Equals(base.Description, _charaDataFullDto.Description, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
base.Description = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new DateTime ExpiryDate
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.ExpiryDate ?? _charaDataFullDto.ExpiryDate;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
base.ExpiryDate = value;
|
||||||
|
if (Equals(base.ExpiryDate, _charaDataFullDto.ExpiryDate))
|
||||||
|
{
|
||||||
|
base.ExpiryDate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new AccessTypeDto AccessType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.AccessType ?? _charaDataFullDto.AccessType;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.AccessType = value;
|
||||||
|
|
||||||
|
if (Equals(base.AccessType, _charaDataFullDto.AccessType))
|
||||||
|
{
|
||||||
|
base.AccessType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new ShareTypeDto ShareType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.ShareType ?? _charaDataFullDto.ShareType;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.ShareType = value;
|
||||||
|
|
||||||
|
if (Equals(base.ShareType, _charaDataFullDto.ShareType))
|
||||||
|
{
|
||||||
|
base.ShareType = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new List<GamePathEntry>? FileGamePaths
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.FileGamePaths ?? _charaDataFullDto.FileGamePaths;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.FileGamePaths = value;
|
||||||
|
if (!(base.FileGamePaths ?? []).Except(_charaDataFullDto.FileGamePaths).Any()
|
||||||
|
&& !_charaDataFullDto.FileGamePaths.Except(base.FileGamePaths ?? []).Any())
|
||||||
|
{
|
||||||
|
base.FileGamePaths = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new List<GamePathEntry>? FileSwaps
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.FileSwaps ?? _charaDataFullDto.FileSwaps;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.FileSwaps = value;
|
||||||
|
if (!(base.FileSwaps ?? []).Except(_charaDataFullDto.FileSwaps).Any()
|
||||||
|
&& !_charaDataFullDto.FileSwaps.Except(base.FileSwaps ?? []).Any())
|
||||||
|
{
|
||||||
|
base.FileSwaps = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new string? GlamourerData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.GlamourerData ?? _charaDataFullDto.GlamourerData;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.GlamourerData = value;
|
||||||
|
if (string.Equals(base.GlamourerData, _charaDataFullDto.GlamourerData, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
base.GlamourerData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new string? CustomizeData
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return base.CustomizeData ?? _charaDataFullDto.CustomizeData;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
base.CustomizeData = value;
|
||||||
|
if (string.Equals(base.CustomizeData, _charaDataFullDto.CustomizeData, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
base.CustomizeData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<UserData> UserList => _userList;
|
||||||
|
private readonly List<UserData> _userList;
|
||||||
|
|
||||||
|
public IEnumerable<GroupData> GroupList => _groupList;
|
||||||
|
private readonly List<GroupData> _groupList;
|
||||||
|
|
||||||
|
public IEnumerable<PoseEntry> PoseList => _poseList;
|
||||||
|
private readonly List<PoseEntry> _poseList;
|
||||||
|
|
||||||
|
public void AddUserToList(string user)
|
||||||
|
{
|
||||||
|
_userList.Add(new(user, null));
|
||||||
|
UpdateAllowedUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddGroupToList(string group)
|
||||||
|
{
|
||||||
|
_groupList.Add(new(group, null));
|
||||||
|
UpdateAllowedGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAllowedUsers()
|
||||||
|
{
|
||||||
|
AllowedUsers = [.. _userList.Select(u => u.UID)];
|
||||||
|
if (!AllowedUsers.Except(_charaDataFullDto.AllowedUsers.Select(u => u.UID), StringComparer.Ordinal).Any()
|
||||||
|
&& !_charaDataFullDto.AllowedUsers.Select(u => u.UID).Except(AllowedUsers, StringComparer.Ordinal).Any())
|
||||||
|
{
|
||||||
|
AllowedUsers = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAllowedGroups()
|
||||||
|
{
|
||||||
|
AllowedGroups = [.. _groupList.Select(u => u.GID)];
|
||||||
|
if (!AllowedGroups.Except(_charaDataFullDto.AllowedGroups.Select(u => u.GID), StringComparer.Ordinal).Any()
|
||||||
|
&& !_charaDataFullDto.AllowedGroups.Select(u => u.GID).Except(AllowedGroups, StringComparer.Ordinal).Any())
|
||||||
|
{
|
||||||
|
AllowedGroups = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveUserFromList(string user)
|
||||||
|
{
|
||||||
|
_userList.RemoveAll(u => string.Equals(u.UID, user, StringComparison.Ordinal));
|
||||||
|
UpdateAllowedUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveGroupFromList(string group)
|
||||||
|
{
|
||||||
|
_groupList.RemoveAll(u => string.Equals(u.GID, group, StringComparison.Ordinal));
|
||||||
|
UpdateAllowedGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPose()
|
||||||
|
{
|
||||||
|
_poseList.Add(new PoseEntry(null));
|
||||||
|
UpdatePoseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePose(PoseEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.Id != null)
|
||||||
|
{
|
||||||
|
entry.Description = null;
|
||||||
|
entry.WorldData = null;
|
||||||
|
entry.PoseData = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_poseList.Remove(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePoseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdatePoseList()
|
||||||
|
{
|
||||||
|
Poses = [.. _poseList];
|
||||||
|
if (!Poses.Except(_charaDataFullDto.PoseData).Any() && !_charaDataFullDto.PoseData.Except(Poses).Any())
|
||||||
|
{
|
||||||
|
Poses = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetExpiry(bool expiring)
|
||||||
|
{
|
||||||
|
if (expiring)
|
||||||
|
{
|
||||||
|
var date = DateTime.UtcNow.AddDays(7);
|
||||||
|
SetExpiry(date.Year, date.Month, date.Day);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ExpiryDate = DateTime.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetExpiry(int year, int month, int day)
|
||||||
|
{
|
||||||
|
int daysInMonth = DateTime.DaysInMonth(year, month);
|
||||||
|
if (day > daysInMonth) day = 1;
|
||||||
|
ExpiryDate = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UndoChanges()
|
||||||
|
{
|
||||||
|
base.Description = null;
|
||||||
|
base.AccessType = null;
|
||||||
|
base.ShareType = null;
|
||||||
|
base.GlamourerData = null;
|
||||||
|
base.FileSwaps = null;
|
||||||
|
base.FileGamePaths = null;
|
||||||
|
base.CustomizeData = null;
|
||||||
|
base.ManipulationData = null;
|
||||||
|
AllowedUsers = null;
|
||||||
|
AllowedGroups = null;
|
||||||
|
Poses = null;
|
||||||
|
_poseList.Clear();
|
||||||
|
_poseList.AddRange(_charaDataFullDto.PoseData.Select(k => new PoseEntry(k.Id)
|
||||||
|
{
|
||||||
|
Description = k.Description,
|
||||||
|
PoseData = k.PoseData,
|
||||||
|
WorldData = k.WorldData
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RevertDeletion(PoseEntry pose)
|
||||||
|
{
|
||||||
|
if (pose.Id == null) return;
|
||||||
|
var oldPose = _charaDataFullDto.PoseData.Find(p => p.Id == pose.Id);
|
||||||
|
if (oldPose == null) return;
|
||||||
|
pose.Description = oldPose.Description;
|
||||||
|
pose.PoseData = oldPose.PoseData;
|
||||||
|
pose.WorldData = oldPose.WorldData;
|
||||||
|
UpdatePoseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool PoseHasChanges(PoseEntry pose)
|
||||||
|
{
|
||||||
|
if (pose.Id == null) return false;
|
||||||
|
var oldPose = _charaDataFullDto.PoseData.Find(p => p.Id == pose.Id);
|
||||||
|
if (oldPose == null) return false;
|
||||||
|
return !string.Equals(pose.Description, oldPose.Description, StringComparison.Ordinal)
|
||||||
|
|| !string.Equals(pose.PoseData, oldPose.PoseData, StringComparison.Ordinal)
|
||||||
|
|| pose.WorldData != oldPose.WorldData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasChanges =>
|
||||||
|
base.Description != null
|
||||||
|
|| base.ExpiryDate != null
|
||||||
|
|| base.AccessType != null
|
||||||
|
|| base.ShareType != null
|
||||||
|
|| AllowedUsers != null
|
||||||
|
|| AllowedGroups != null
|
||||||
|
|| base.GlamourerData != null
|
||||||
|
|| base.FileSwaps != null
|
||||||
|
|| base.FileGamePaths != null
|
||||||
|
|| base.CustomizeData != null
|
||||||
|
|| base.ManipulationData != null
|
||||||
|
|| Poses != null;
|
||||||
|
|
||||||
|
public bool IsAppearanceEqual =>
|
||||||
|
string.Equals(GlamourerData, _charaDataFullDto.GlamourerData, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(CustomizeData, _charaDataFullDto.CustomizeData, StringComparison.Ordinal)
|
||||||
|
&& FileGamePaths == _charaDataFullDto.FileGamePaths
|
||||||
|
&& FileSwaps == _charaDataFullDto.FileSwaps
|
||||||
|
&& string.Equals(ManipulationData, _charaDataFullDto.ManipulationData, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
public sealed record CharaDataFullExtendedDto : CharaDataFullDto
|
||||||
|
{
|
||||||
|
public CharaDataFullExtendedDto(CharaDataFullDto baseDto) : base(baseDto)
|
||||||
|
{
|
||||||
|
FullId = baseDto.Uploader.UID + ":" + baseDto.Id;
|
||||||
|
MissingFiles = new ReadOnlyCollection<GamePathEntry>(baseDto.OriginalFiles.Except(baseDto.FileGamePaths).ToList());
|
||||||
|
HasMissingFiles = MissingFiles.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FullId { get; set; }
|
||||||
|
public bool HasMissingFiles { get; init; }
|
||||||
|
public IReadOnlyCollection<GamePathEntry> MissingFiles { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
public sealed record CharaDataMetaInfoExtendedDto : CharaDataMetaInfoDto
|
||||||
|
{
|
||||||
|
private CharaDataMetaInfoExtendedDto(CharaDataMetaInfoDto baseMeta) : base(baseMeta)
|
||||||
|
{
|
||||||
|
FullId = baseMeta.Uploader.UID + ":" + baseMeta.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PoseEntryExtended> PoseExtended { get; private set; } = [];
|
||||||
|
public bool HasPoses => PoseExtended.Count != 0;
|
||||||
|
public bool HasWorldData => PoseExtended.Exists(p => p.HasWorldData);
|
||||||
|
public bool IsOwnData { get; private set; }
|
||||||
|
public string FullId { get; private set; }
|
||||||
|
|
||||||
|
public async static Task<CharaDataMetaInfoExtendedDto> Create(CharaDataMetaInfoDto baseMeta, DalamudUtilService dalamudUtilService, bool isOwnData = false)
|
||||||
|
{
|
||||||
|
CharaDataMetaInfoExtendedDto newDto = new(baseMeta);
|
||||||
|
|
||||||
|
foreach (var pose in newDto.PoseData)
|
||||||
|
{
|
||||||
|
newDto.PoseExtended.Add(await PoseEntryExtended.Create(pose, newDto, dalamudUtilService).ConfigureAwait(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
newDto.IsOwnData = isOwnData;
|
||||||
|
|
||||||
|
return newDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs
Normal file
174
MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
using Dalamud.Utility;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.CharaData;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
public sealed record GposeLobbyUserData(UserData UserData)
|
||||||
|
{
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
HasWorldDataUpdate = WorldData != null;
|
||||||
|
HasPoseDataUpdate = ApplicablePoseData != null;
|
||||||
|
SpawnedVfxId = null;
|
||||||
|
LastAppliedCharaDataDate = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldData? _worldData;
|
||||||
|
public WorldData? WorldData
|
||||||
|
{
|
||||||
|
get => _worldData; set
|
||||||
|
{
|
||||||
|
_worldData = value;
|
||||||
|
HasWorldDataUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasWorldDataUpdate { get; set; } = false;
|
||||||
|
|
||||||
|
private PoseData? _fullPoseData;
|
||||||
|
private PoseData? _deltaPoseData;
|
||||||
|
|
||||||
|
public PoseData? FullPoseData
|
||||||
|
{
|
||||||
|
get => _fullPoseData;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_fullPoseData = value;
|
||||||
|
ApplicablePoseData = CombinePoseData();
|
||||||
|
HasPoseDataUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PoseData? DeltaPoseData
|
||||||
|
{
|
||||||
|
get => _deltaPoseData;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_deltaPoseData = value;
|
||||||
|
ApplicablePoseData = CombinePoseData();
|
||||||
|
HasPoseDataUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PoseData? ApplicablePoseData { get; private set; }
|
||||||
|
public bool HasPoseDataUpdate { get; set; } = false;
|
||||||
|
public Guid? SpawnedVfxId { get; set; }
|
||||||
|
public Vector3? LastWorldPosition { get; set; }
|
||||||
|
public Vector3? TargetWorldPosition { get; set; }
|
||||||
|
public DateTime? UpdateStart { get; set; }
|
||||||
|
private CharaDataDownloadDto? _charaData;
|
||||||
|
public CharaDataDownloadDto? CharaData
|
||||||
|
{
|
||||||
|
get => _charaData; set
|
||||||
|
{
|
||||||
|
_charaData = value;
|
||||||
|
LastUpdatedCharaData = _charaData?.UpdatedDate ?? DateTime.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime LastUpdatedCharaData { get; private set; } = DateTime.MaxValue;
|
||||||
|
public DateTime LastAppliedCharaDataDate { get; set; } = DateTime.MinValue;
|
||||||
|
public nint Address { get; set; }
|
||||||
|
public string AssociatedCharaName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private PoseData? CombinePoseData()
|
||||||
|
{
|
||||||
|
if (DeltaPoseData == null && FullPoseData != null) return FullPoseData;
|
||||||
|
if (FullPoseData == null) return null;
|
||||||
|
|
||||||
|
PoseData output = FullPoseData!.Value.DeepClone();
|
||||||
|
PoseData delta = DeltaPoseData!.Value;
|
||||||
|
|
||||||
|
foreach (var bone in FullPoseData!.Value.Bones)
|
||||||
|
{
|
||||||
|
if (!delta.Bones.TryGetValue(bone.Key, out var data)) continue;
|
||||||
|
if (!data.Exists)
|
||||||
|
{
|
||||||
|
output.Bones.Remove(bone.Key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.Bones[bone.Key] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var bone in FullPoseData!.Value.MainHand)
|
||||||
|
{
|
||||||
|
if (!delta.MainHand.TryGetValue(bone.Key, out var data)) continue;
|
||||||
|
if (!data.Exists)
|
||||||
|
{
|
||||||
|
output.MainHand.Remove(bone.Key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.MainHand[bone.Key] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var bone in FullPoseData!.Value.OffHand)
|
||||||
|
{
|
||||||
|
if (!delta.OffHand.TryGetValue(bone.Key, out var data)) continue;
|
||||||
|
if (!data.Exists)
|
||||||
|
{
|
||||||
|
output.OffHand.Remove(bone.Key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.OffHand[bone.Key] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string WorldDataDescriptor { get; private set; } = string.Empty;
|
||||||
|
public Vector2 MapCoordinates { get; private set; }
|
||||||
|
public Lumina.Excel.Sheets.Map Map { get; private set; }
|
||||||
|
public HandledCharaDataEntry? HandledChara { get; set; }
|
||||||
|
|
||||||
|
public async Task SetWorldDataDescriptor(DalamudUtilService dalamudUtilService)
|
||||||
|
{
|
||||||
|
if (WorldData == null)
|
||||||
|
{
|
||||||
|
WorldDataDescriptor = "No World Data found";
|
||||||
|
}
|
||||||
|
|
||||||
|
var worldData = WorldData!.Value;
|
||||||
|
MapCoordinates = await dalamudUtilService.RunOnFrameworkThread(() =>
|
||||||
|
MapUtil.WorldToMap(new Vector2(worldData.PositionX, worldData.PositionY), dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
Map = dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map;
|
||||||
|
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.AppendLine("Server: " + dalamudUtilService.WorldData.Value[(ushort)worldData.LocationInfo.ServerId]);
|
||||||
|
sb.AppendLine("Territory: " + dalamudUtilService.TerritoryData.Value[worldData.LocationInfo.TerritoryId]);
|
||||||
|
sb.AppendLine("Map: " + dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].MapName);
|
||||||
|
|
||||||
|
if (worldData.LocationInfo.WardId != 0)
|
||||||
|
sb.AppendLine("Ward #: " + worldData.LocationInfo.WardId);
|
||||||
|
if (worldData.LocationInfo.DivisionId != 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("Subdivision: " + worldData.LocationInfo.DivisionId switch
|
||||||
|
{
|
||||||
|
1 => "No",
|
||||||
|
2 => "Yes",
|
||||||
|
_ => "-"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (worldData.LocationInfo.HouseId != 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("House #: " + (worldData.LocationInfo.HouseId == 100 ? "Apartments" : worldData.LocationInfo.HouseId.ToString()));
|
||||||
|
}
|
||||||
|
if (worldData.LocationInfo.RoomId != 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine("Apartment #: " + worldData.LocationInfo.RoomId);
|
||||||
|
}
|
||||||
|
sb.AppendLine("Coordinates: X: " + MapCoordinates.X.ToString("0.0", CultureInfo.InvariantCulture) + ", Y: " + MapCoordinates.Y.ToString("0.0", CultureInfo.InvariantCulture));
|
||||||
|
WorldDataDescriptor = sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace MareSynchronos.Services.CharaData.Models;
|
||||||
|
|
||||||
|
public sealed record HandledCharaDataEntry(string Name, bool IsSelf, Guid? CustomizePlus, CharaDataMetaInfoExtendedDto MetaInfo)
|
||||||
|
{
|
||||||
|
public CharaDataMetaInfoExtendedDto MetaInfo { get; set; } = MetaInfo;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user