Compare commits
49 Commits
96510e7292
...
2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
d777dc599f
|
|||
|
e3d9300ca3
|
|||
|
699678f641
|
|||
|
7a391e6253
|
|||
|
620ebf9195
|
|||
|
8cc4f34c55
|
|||
|
513845b811
|
|||
|
84586cac3d
|
|||
|
b4108c7803
|
|||
|
d891dceb28
|
|||
|
89fa1a999f
|
|||
|
1f6e86ec2d
|
|||
|
d225a3844a
|
|||
|
d4a46910f9
|
|||
|
b59a579f56
|
|||
|
7706ef1fa7
|
|||
|
fca730557e
|
|||
|
6572fdcc27
|
|||
|
bf770f19d9
|
|||
|
78089a9fc7
|
|||
|
3c81e1f243
|
|||
|
0808266887
|
|||
|
a2071b9c05
|
|||
|
612e7c88a2
|
|||
|
1755b5cb54
|
|||
|
4a388dcfa9
|
|||
|
a0957715a5
|
|||
|
04a8ee3186
|
|||
|
b79a51748f
|
|||
|
95d9f65068
|
|||
|
a70968d30c
|
|||
|
6ebb73040b
|
|||
|
46f2443824
|
|||
|
eeab8354b6
|
|||
|
b5d8f288f9
|
|||
| 3c2dab4d21 | |||
| edb49f710a | |||
| 9ff21dc341 | |||
| 17962a37b3 | |||
| 4495177f02 | |||
| 14bb5c14f7 | |||
| 6101686a33 | |||
| bc6cde48de | |||
| 9ce213f949 | |||
| cd5bf3f06f | |||
| 52eaa0cf95 | |||
| 6edb90f4d7 | |||
| 4688043f6a | |||
| 5da6047759 |
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
|
||||||
357
.gitignore
vendored
Normal file
357
.gitignore
vendored
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
## 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
|
||||||
|
.idea
|
||||||
|
qodana.yaml
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
.DS_Store
|
||||||
|
MareSynchronos/.DS_Store
|
||||||
|
*.zip
|
||||||
|
UmbraServer_extracted/
|
||||||
|
NuGet.config
|
||||||
|
Directory.Build.props
|
||||||
|
|
||||||
|
# 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/
|
||||||
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[submodule "MareAPI"]
|
||||||
|
path = MareAPI
|
||||||
|
url = ssh://git@git.umbra-sync.net:1222/Keda/UmbraAPI.git
|
||||||
|
branch = main
|
||||||
|
[submodule "Penumbra.Api"]
|
||||||
|
path = Penumbra.Api
|
||||||
|
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||||
|
branch = main
|
||||||
|
[submodule "Glamourer.Api"]
|
||||||
|
path = Glamourer.Api
|
||||||
|
url = https://github.com/Ottermandias/Glamourer.Api.git
|
||||||
|
branch = main
|
||||||
1
Glamourer.Api
Submodule
1
Glamourer.Api
Submodule
Submodule Glamourer.Api added at 59a7ab5fa9
21
LICENSE_MIT
Normal file
21
LICENSE_MIT
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.
|
||||||
1
MareAPI
Submodule
1
MareAPI
Submodule
Submodule MareAPI added at d105d20507
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
|
||||||
120
MareSynchronos/.editorconfig
Normal file
120
MareSynchronos/.editorconfig
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
|
||||||
|
[*.{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 = 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
|
||||||
|
|
||||||
|
# MA0048: File name must match type name
|
||||||
|
dotnet_diagnostic.MA0048.severity = silent
|
||||||
881
MareSynchronos/FileCache/CacheMonitor.cs
Normal file
881
MareSynchronos/FileCache/CacheMonitor.cs
Normal file
@@ -0,0 +1,881 @@
|
|||||||
|
using System;
|
||||||
|
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;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
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);
|
||||||
|
StartSubstWatcher(_fileDbManager.SubstFolder);
|
||||||
|
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);
|
||||||
|
StartSubstWatcher(_fileDbManager.SubstFolder);
|
||||||
|
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);
|
||||||
|
StartSubstWatcher(_fileDbManager.SubstFolder);
|
||||||
|
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, StrongBox<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)
|
||||||
|
{
|
||||||
|
HaltScanLocks.TryAdd(source, new(0));
|
||||||
|
Interlocked.Increment(ref HaltScanLocks[source].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
private readonly Dictionary<string, WatcherChange> _substChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public void StopMonitoring()
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Stopping monitoring of Penumbra and Mare storage folders");
|
||||||
|
MareWatcher?.Dispose();
|
||||||
|
SubstWatcher?.Dispose();
|
||||||
|
PenumbraWatcher?.Dispose();
|
||||||
|
MareWatcher = null;
|
||||||
|
SubstWatcher = null;
|
||||||
|
PenumbraWatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StorageisNTFS { get; private set; } = false;
|
||||||
|
|
||||||
|
public void StartMareWatcher(string? snowPath)
|
||||||
|
{
|
||||||
|
MareWatcher?.Dispose();
|
||||||
|
if (string.IsNullOrEmpty(snowPath) || !Directory.Exists(snowPath))
|
||||||
|
{
|
||||||
|
MareWatcher = null;
|
||||||
|
Logger.LogWarning("Umbra file path is not set, cannot start the FSW for Umbra.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
|
||||||
|
StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Logger.LogInformation("Umbra Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
|
||||||
|
|
||||||
|
Logger.LogDebug("Initializing Mare FSW on {path}", snowPath);
|
||||||
|
MareWatcher = new()
|
||||||
|
{
|
||||||
|
Path = snowPath,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartSubstWatcher(string? substPath)
|
||||||
|
{
|
||||||
|
SubstWatcher?.Dispose();
|
||||||
|
if (string.IsNullOrEmpty(substPath))
|
||||||
|
{
|
||||||
|
SubstWatcher = null;
|
||||||
|
Logger.LogWarning("Umbra file path is not set, cannot start the FSW for Umbra.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(substPath))
|
||||||
|
Directory.CreateDirectory(substPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Could not create subst directory at {path}.", substPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("Initializing Subst FSW on {path}", substPath);
|
||||||
|
SubstWatcher = new()
|
||||||
|
{
|
||||||
|
Path = substPath,
|
||||||
|
InternalBufferSize = 8388608,
|
||||||
|
NotifyFilter = NotifyFilters.CreationTime
|
||||||
|
| NotifyFilters.LastWrite
|
||||||
|
| NotifyFilters.FileName
|
||||||
|
| NotifyFilters.DirectoryName
|
||||||
|
| NotifyFilters.Size,
|
||||||
|
Filter = "*.*",
|
||||||
|
IncludeSubdirectories = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
SubstWatcher.Deleted += SubstWatcher_FileChanged;
|
||||||
|
SubstWatcher.Created += SubstWatcher_FileChanged;
|
||||||
|
SubstWatcher.EnableRaisingEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MareWatcher_FileChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Umbra FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||||
|
|
||||||
|
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
lock (_mareChanges)
|
||||||
|
{
|
||||||
|
_mareChanges[e.FullPath] = new(e.ChangeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = MareWatcherExecution();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubstWatcher_FileChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Subst FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||||
|
|
||||||
|
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
|
lock (_substChanges)
|
||||||
|
{
|
||||||
|
_substChanges[e.FullPath] = new(e.ChangeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = SubstWatcherExecution();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
private CancellationTokenSource _substFswCts = new();
|
||||||
|
public FileSystemWatcher? PenumbraWatcher { get; private set; }
|
||||||
|
public FileSystemWatcher? MareWatcher { get; private set; }
|
||||||
|
public FileSystemWatcher? SubstWatcher { 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.Value > 0));
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_mareChanges)
|
||||||
|
{
|
||||||
|
foreach (var key in changes.Keys)
|
||||||
|
{
|
||||||
|
_mareChanges.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleChanges(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubstWatcherExecution()
|
||||||
|
{
|
||||||
|
_substFswCts = _substFswCts.CancelRecreate();
|
||||||
|
var token = _substFswCts.Token;
|
||||||
|
var delay = TimeSpan.FromSeconds(5);
|
||||||
|
Dictionary<string, WatcherChange> changes;
|
||||||
|
lock (_substChanges)
|
||||||
|
changes = _substChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
} while (HaltScanLocks.Any(f => f.Value.Value > 0));
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_substChanges)
|
||||||
|
{
|
||||||
|
foreach (var key in changes.Keys)
|
||||||
|
{
|
||||||
|
_substChanges.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleChanges(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearSubstStorage()
|
||||||
|
{
|
||||||
|
var substDir = _fileDbManager.SubstFolder;
|
||||||
|
var allSubstFiles = Directory.GetFiles(substDir, "*.*", SearchOption.TopDirectoryOnly)
|
||||||
|
.Where(f =>
|
||||||
|
{
|
||||||
|
var val = f.Split('\\')[^1];
|
||||||
|
return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40
|
||||||
|
|| val.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
if (SubstWatcher != null)
|
||||||
|
SubstWatcher.EnableRaisingEvents = false;
|
||||||
|
|
||||||
|
Dictionary<string, WatcherChange> changes = _substChanges.ToDictionary(t => t.Key, t => new WatcherChange(WatcherChangeTypes.Deleted, t.Key), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var file in allSubstFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleChanges(changes);
|
||||||
|
|
||||||
|
if (SubstWatcher != null)
|
||||||
|
SubstWatcher.EnableRaisingEvents = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteSubstOriginals()
|
||||||
|
{
|
||||||
|
var cacheDir = _configService.Current.CacheFolder;
|
||||||
|
var substDir = _fileDbManager.SubstFolder;
|
||||||
|
var allSubstFiles = Directory.GetFiles(substDir, "*.*", SearchOption.TopDirectoryOnly)
|
||||||
|
.Where(f =>
|
||||||
|
{
|
||||||
|
var val = f.Split('\\')[^1];
|
||||||
|
return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40
|
||||||
|
|| val.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var substFile in allSubstFiles)
|
||||||
|
{
|
||||||
|
var cacheFile = Path.Join(cacheDir, Path.GetFileName(substFile));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(cacheFile))
|
||||||
|
File.Delete(cacheFile);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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)
|
||||||
|
.Concat(Directory.EnumerateFiles(_fileDbManager.SubstFolder))
|
||||||
|
.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 substDir = _fileDbManager.SubstFolder;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
HaltScanLocks.TryAdd(source, new(0));
|
||||||
|
Interlocked.Decrement(ref HaltScanLocks[source].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_scanCancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
_scanCancellationTokenSource.Dispose();
|
||||||
|
PenumbraWatcher?.Dispose();
|
||||||
|
MareWatcher?.Dispose();
|
||||||
|
SubstWatcher?.Dispose();
|
||||||
|
TryCancelAndDispose(_penumbraFswCts);
|
||||||
|
TryCancelAndDispose(_mareFswCts);
|
||||||
|
TryCancelAndDispose(_substFswCts);
|
||||||
|
TryCancelAndDispose(_periodicCalculationTokenSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCancelAndDispose(CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FullFileScan(CancellationToken ct)
|
||||||
|
{
|
||||||
|
TotalFiles = 1;
|
||||||
|
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||||
|
bool penDirExists = true;
|
||||||
|
bool cacheDirExists = true;
|
||||||
|
var substDir = _fileDbManager.SubstFolder;
|
||||||
|
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("Umbra Cache directory is not set or does not exist.");
|
||||||
|
}
|
||||||
|
if (!penDirExists || !cacheDirExists)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(substDir))
|
||||||
|
Directory.CreateDirectory(substDir);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Could not create subst directory at {path}.", substDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.Concat(Directory.GetFiles(substDir, "*.*", 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 = [];
|
||||||
|
Lock 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)
|
||||||
|
{
|
||||||
|
if (cachePath.StartsWith(substDir, StringComparison.Ordinal))
|
||||||
|
_ = _fileDbManager.CreateSubstEntry(cachePath);
|
||||||
|
else
|
||||||
|
_ = _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);
|
||||||
|
StartSubstWatcher(_fileDbManager.SubstFolder);
|
||||||
|
StartPenumbraWatcher(penumbraDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
MareSynchronos/FileCache/FileCacheEntity.cs
Normal file
30
MareSynchronos/FileCache/FileCacheEntity.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#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 bool IsSubstEntry => PrefixedFilePath.StartsWith(FileCacheManager.SubstPrefix, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
553
MareSynchronos/FileCache/FileCacheManager.cs
Normal file
553
MareSynchronos/FileCache/FileCacheManager.cs
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
using Dalamud.Utility;
|
||||||
|
using K4os.Compression.LZ4.Streams;
|
||||||
|
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}";
|
||||||
|
public const string SubstPrefix = "{subst}";
|
||||||
|
public const string SubstPath = "subst";
|
||||||
|
public string CacheFolder => _configService.Current.CacheFolder;
|
||||||
|
public string SubstFolder => CacheFolder.IsNullOrEmpty() ? string.Empty : CacheFolder.ToLowerInvariant().TrimEnd('\\') + "\\" + SubstPath;
|
||||||
|
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 Lock _fileWriteLock = new();
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
|
|
||||||
|
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, string? hash = null)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
if (hash != null)
|
||||||
|
return CreateFileCacheEntity(fi, prefixedPath, hash);
|
||||||
|
else
|
||||||
|
return CreateFileCacheEntity(fi, prefixedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileCacheEntity? CreateSubstEntry(string path)
|
||||||
|
{
|
||||||
|
FileInfo fi = new(path);
|
||||||
|
if (!fi.Exists) return null;
|
||||||
|
_logger.LogTrace("Creating substitute entry for {path}", path);
|
||||||
|
var fullName = fi.FullName.ToLowerInvariant();
|
||||||
|
if (!fullName.Contains(SubstFolder, StringComparison.Ordinal)) return null;
|
||||||
|
string prefixedPath = fullName.Replace(SubstFolder, SubstPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
var fakeHash = Path.GetFileNameWithoutExtension(fi.FullName).ToUpperInvariant();
|
||||||
|
var result = CreateFileCacheEntity(fi, prefixedPath, fakeHash);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && !c.IsSubstEntry) : 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;
|
||||||
|
if (fileCache.IsSubstEntry) continue;
|
||||||
|
|
||||||
|
_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 {expectedHash}", 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 string GetSubstFilePath(string hash, string extension)
|
||||||
|
{
|
||||||
|
return Path.Combine(SubstFolder, hash + "." + extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
|
||||||
|
{
|
||||||
|
var fileCache = GetFileCacheByHash(fileHash)!;
|
||||||
|
using var fs = File.OpenRead(fileCache.ResolvedFilepath);
|
||||||
|
var ms = new MemoryStream(64 * 1024);
|
||||||
|
using var encstream = LZ4Stream.Encode(ms, new LZ4EncoderSettings(){CompressionLevel=K4os.Compression.LZ4.LZ4Level.L09_HC});
|
||||||
|
await fs.CopyToAsync(encstream, uploadToken).ConfigureAwait(false);
|
||||||
|
encstream.Close();
|
||||||
|
fileCache.CompressedSize = encstream.Length;
|
||||||
|
return (fileHash, ms.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileCacheEntity? GetFileCacheByHash(string hash, bool preferSubst = false)
|
||||||
|
{
|
||||||
|
var caches = GetFileCachesByHash(hash);
|
||||||
|
if (preferSubst && caches.Subst != null)
|
||||||
|
return caches.Subst;
|
||||||
|
return caches.Penumbra ?? caches.Cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (FileCacheEntity? Penumbra, FileCacheEntity? Cache, FileCacheEntity? Subst) GetFileCachesByHash(string hash)
|
||||||
|
{
|
||||||
|
(FileCacheEntity? Penumbra, FileCacheEntity? Cache, FileCacheEntity? Subst) result = (null, null, null);
|
||||||
|
if (_fileCaches.TryGetValue(hash, out var hashes))
|
||||||
|
{
|
||||||
|
result.Penumbra = hashes.Where(p => p.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault();
|
||||||
|
result.Cache = hashes.Where(p => p.PrefixedFilePath.StartsWith(CachePrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault();
|
||||||
|
result.Subst = hashes.Where(p => p.PrefixedFilePath.StartsWith(SubstPrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(SubstFolder, SubstPrefix, 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)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (dict.TryGetValue(entry.Value, out var entity))
|
||||||
|
{
|
||||||
|
var validatedCache = GetValidatedFileCache(entity);
|
||||||
|
result.Add(entry.Key, validatedCache);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (entry.Value.StartsWith(PenumbraPrefix, StringComparison.Ordinal))
|
||||||
|
result.Add(entry.Key, CreateFileEntry(entry.Key));
|
||||||
|
else if (entry.Value.StartsWith(SubstPrefix, StringComparison.Ordinal))
|
||||||
|
result.Add(entry.Key, CreateSubstEntry(entry.Key));
|
||||||
|
else if (entry.Value.StartsWith(CachePrefix, StringComparison.Ordinal))
|
||||||
|
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 _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
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(SubstPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(SubstPrefix, SubstFolder, 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 Umbra. After, reload Umbra 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
250
MareSynchronos/FileCache/FileCompactor.cs
Normal file
250
MareSynchronos/FileCache/FileCompactor.cs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
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 WofFileCompressionInfoV1 _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 WofFileCompressionInfoV1
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RenameAndCompact(string filePath, string originalFilePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(originalFilePath, filePath);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// File already exists
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 WofFileCompressionInfoV1 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct WofFileCompressionInfoV1
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
313
MareSynchronos/FileCache/TransientResourceManager.cs
Normal file
313
MareSynchronos/FileCache/TransientResourceManager.cs
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Data;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.FileCache;
|
||||||
|
|
||||||
|
public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly Lock _cacheAdditionLock = new();
|
||||||
|
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
||||||
|
private readonly TransientConfigService _configurationService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly string[] _fileTypesToHandle = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"];
|
||||||
|
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||||
|
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
||||||
|
|
||||||
|
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<ClassJobChangedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (_playerRelatedPointers.Contains(msg.GameObjectHandler))
|
||||||
|
{
|
||||||
|
DalamudUtil_ClassJobChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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 string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||||
|
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
||||||
|
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_semiTransientResources == null)
|
||||||
|
{
|
||||||
|
_semiTransientResources = new();
|
||||||
|
_semiTransientResources.TryAdd(ObjectKind.Player, new HashSet<string>(StringComparer.Ordinal));
|
||||||
|
if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var gamePaths))
|
||||||
|
{
|
||||||
|
int restored = 0;
|
||||||
|
foreach (var gamePath in gamePaths)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(gamePath)) continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Loaded persistent transient resource {path}", gamePath);
|
||||||
|
SemiTransientResources[ObjectKind.Player].Add(gamePath);
|
||||||
|
restored++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error during loading persistent transient resource {path}", gamePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _semiTransientResources;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private ConcurrentDictionary<IntPtr, HashSet<string>> TransientResources { get; } = new();
|
||||||
|
|
||||||
|
public void CleanUpSemiTransientResources(ObjectKind objectKind, List<FileReplacement>? fileReplacement = null)
|
||||||
|
{
|
||||||
|
if (SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
if (fileReplacement == null)
|
||||||
|
{
|
||||||
|
value.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList())
|
||||||
|
{
|
||||||
|
value.RemoveWhere(p => string.Equals(p, replacement, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<string> GetSemiTransientResources(ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
if (SemiTransientResources.TryGetValue(objectKind, out var result))
|
||||||
|
{
|
||||||
|
return result ?? new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetTransientResources(IntPtr gameObject)
|
||||||
|
{
|
||||||
|
if (TransientResources.TryGetValue(gameObject, out var result))
|
||||||
|
{
|
||||||
|
return [.. result];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
SemiTransientResources[objectKind] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TransientResources.TryGetValue(gameObject, out var resources))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transientResources = resources.ToList();
|
||||||
|
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
|
||||||
|
foreach (var gamePath in transientResources)
|
||||||
|
{
|
||||||
|
value.Add(gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements))
|
||||||
|
{
|
||||||
|
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements.Where(f => !string.IsNullOrEmpty(f)).ToHashSet(StringComparer.Ordinal);
|
||||||
|
_configurationService.Save();
|
||||||
|
}
|
||||||
|
TransientResources[gameObject].Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AddSemiTransientResource(ObjectKind objectKind, string item)
|
||||||
|
{
|
||||||
|
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
SemiTransientResources[objectKind] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Add(item.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ClearTransientPaths(IntPtr ptr, List<string> list)
|
||||||
|
{
|
||||||
|
if (TransientResources.TryGetValue(ptr, 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.LogInformation("Removed {removed} previously existing transient paths", removed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TransientResources.Clear();
|
||||||
|
SemiTransientResources.Clear();
|
||||||
|
if (SemiTransientResources.TryGetValue(ObjectKind.Player, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = value;
|
||||||
|
_configurationService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DalamudUtil_ClassJobChanged()
|
||||||
|
{
|
||||||
|
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value?.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DalamudUtil_FrameworkUpdate()
|
||||||
|
{
|
||||||
|
_cachedFrameAddresses = _cachedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.CurrentAddress(), c => c.ObjectKind));
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Clear();
|
||||||
|
}
|
||||||
|
foreach (var item in TransientResources.Where(item => !_dalamudUtil.IsGameObjectPresent(item.Key)).Select(i => i.Key).ToList())
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Object not present anymore: {addr}", item.ToString("X"));
|
||||||
|
TransientResources.TryRemove(item, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||||
|
{
|
||||||
|
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||||
|
var gameObject = 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
|
||||||
|
if (!_fileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore files not belonging to anything player related
|
||||||
|
if (!_cachedFrameAddresses.TryGetValue(gameObject, out var objectKind))
|
||||||
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TransientResources.TryGetValue(gameObject, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
TransientResources[gameObject] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Contains(replacedGamePath) ||
|
||||||
|
SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var thing = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObject);
|
||||||
|
value.Add(replacedGamePath);
|
||||||
|
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, thing?.ToString() ?? gameObject.ToString("X"), filePath);
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
_sendTransientCts?.Cancel();
|
||||||
|
_sendTransientCts?.Dispose();
|
||||||
|
_sendTransientCts = new();
|
||||||
|
var token = _sendTransientCts.Token;
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false);
|
||||||
|
Mediator.Publish(new TransientResourceChangedMessage(gameObject));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RemoveTransientResource(ObjectKind objectKind, string path)
|
||||||
|
{
|
||||||
|
if (SemiTransientResources.TryGetValue(objectKind, out var resources))
|
||||||
|
{
|
||||||
|
resources.RemoveWhere(f => string.Equals(path, f, StringComparison.OrdinalIgnoreCase));
|
||||||
|
_configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = resources;
|
||||||
|
_configurationService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CancellationTokenSource _sendTransientCts = new();
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
MareSynchronos/Interop/DalamudLogger.cs
Normal file
55
MareSynchronos/Interop/DalamudLogger.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public DalamudLogger(string name, MareConfigService mareConfigService, IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => 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;
|
||||||
|
|
||||||
|
if ((int)logLevel <= (int)LogLevel.Information)
|
||||||
|
_pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.Append($"[{_name}]{{{(int)logLevel}}} {state}: {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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
MareSynchronos/Interop/DalamudLoggingProvider.cs
Normal file
44
MareSynchronos/Interop/DalamudLoggingProvider.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public DalamudLoggingProvider(MareConfigService mareConfigService, IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_loggers.Clear();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs
Normal file
20
MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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)
|
||||||
|
{
|
||||||
|
builder.ClearProviders();
|
||||||
|
|
||||||
|
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>
|
||||||
|
(b => new DalamudLoggingProvider(b.GetRequiredService<MareConfigService>(), pluginLog)));
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
333
MareSynchronos/Interop/GameChatHooks.cs
Normal file
333
MareSynchronos/Interop/GameChatHooks.cs
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Memory;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.Shell;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop;
|
||||||
|
|
||||||
|
public record ChatChannelOverride
|
||||||
|
{
|
||||||
|
public string ChannelName { get; set; } = string.Empty;
|
||||||
|
public Action<byte[]>? ChatMessageHandler { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe sealed class GameChatHooks : IDisposable
|
||||||
|
{
|
||||||
|
// Based on https://git.anna.lgbt/anna/ExtraChat/src/branch/main/client/ExtraChat/GameFunctions.cs
|
||||||
|
|
||||||
|
private readonly ILogger<GameChatHooks> _logger;
|
||||||
|
private readonly Action<int, byte[]> _ssCommandHandler;
|
||||||
|
|
||||||
|
#region signatures
|
||||||
|
#pragma warning disable CS0649
|
||||||
|
// I do not know what kind of black magic this function performs
|
||||||
|
// Client::UI::Misc::PronounModule::???
|
||||||
|
[Signature("E8 ?? ?? ?? ?? 44 88 74 24 ?? 4C 8D 45")]
|
||||||
|
private readonly delegate* unmanaged<PronounModule*, Utf8String*, byte, Utf8String*> _processStringStep2;
|
||||||
|
|
||||||
|
// Component::Shell::ShellCommandModule::ExecuteCommandInner
|
||||||
|
private delegate void SendMessageDelegate(ShellCommandModule* module, Utf8String* message, UIModule* uiModule);
|
||||||
|
[Signature(
|
||||||
|
"E8 ?? ?? ?? ?? FE 87 ?? ?? ?? ?? C7 87",
|
||||||
|
DetourName = nameof(SendMessageDetour)
|
||||||
|
)]
|
||||||
|
private Hook<SendMessageDelegate>? SendMessageHook { get; init; }
|
||||||
|
|
||||||
|
// Client::UI::Shell::RaptureShellModule::SetChatChannel
|
||||||
|
private delegate void SetChatChannelDelegate(RaptureShellModule* module, uint channel);
|
||||||
|
[Signature(
|
||||||
|
"E8 ?? ?? ?? ?? 33 C0 EB ?? 85 D2",
|
||||||
|
DetourName = nameof(SetChatChannelDetour)
|
||||||
|
)]
|
||||||
|
private Hook<SetChatChannelDelegate>? SetChatChannelHook { get; init; }
|
||||||
|
|
||||||
|
// Component::Shell::ShellCommandModule::ChangeChannelName
|
||||||
|
private delegate byte* ChangeChannelNameDelegate(AgentChatLog* agent);
|
||||||
|
[Signature(
|
||||||
|
"E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6",
|
||||||
|
DetourName = nameof(ChangeChannelNameDetour)
|
||||||
|
)]
|
||||||
|
private Hook<ChangeChannelNameDelegate>? ChangeChannelNameHook { get; init; }
|
||||||
|
|
||||||
|
// Client::UI::Agent::AgentChatLog::???
|
||||||
|
private delegate byte ShouldDoNameLookupDelegate(AgentChatLog* agent);
|
||||||
|
[Signature(
|
||||||
|
"48 89 5C 24 ?? 57 48 83 EC ?? 48 8B D9 40 32 FF 48 8B 49 ?? ?? ?? ?? FF 50",
|
||||||
|
DetourName = nameof(ShouldDoNameLookupDetour)
|
||||||
|
)]
|
||||||
|
private Hook<ShouldDoNameLookupDelegate>? ShouldDoNameLookupHook { get; init; }
|
||||||
|
|
||||||
|
// Temporary chat channel change (via hotkey)
|
||||||
|
// Client::UI::Shell::RaptureShellModule::???
|
||||||
|
private delegate ulong TempChatChannelDelegate(RaptureShellModule* module, uint x, uint y, ulong z);
|
||||||
|
[Signature(
|
||||||
|
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 49 8B F9 41 8B F0",
|
||||||
|
DetourName = nameof(TempChatChannelDetour)
|
||||||
|
)]
|
||||||
|
private Hook<TempChatChannelDelegate>? TempChatChannelHook { get; init; }
|
||||||
|
|
||||||
|
// Temporary tell target change (via hotkey)
|
||||||
|
// Client::UI::Shell::RaptureShellModule::SetContextTellTargetInForay
|
||||||
|
private delegate ulong TempTellTargetDelegate(RaptureShellModule* module, ulong a, ulong b, ulong c, ushort d, ulong e, ulong f, ushort g);
|
||||||
|
[Signature(
|
||||||
|
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 41 0F B7 F9",
|
||||||
|
DetourName = nameof(TempTellTargetDetour)
|
||||||
|
)]
|
||||||
|
private Hook<TempTellTargetDelegate>? TempTellTargetHook { get; init; }
|
||||||
|
|
||||||
|
// Called every frame while the chat bar is not focused
|
||||||
|
private delegate void UnfocusTickDelegate(RaptureShellModule* module);
|
||||||
|
[Signature(
|
||||||
|
"40 53 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 48 8B D9 0F 84 ?? ?? ?? ?? 48 8D 91",
|
||||||
|
DetourName = nameof(UnfocusTickDetour)
|
||||||
|
)]
|
||||||
|
private Hook<UnfocusTickDelegate>? UnfocusTickHook { get; init; }
|
||||||
|
#pragma warning restore CS0649
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private ChatChannelOverride? _chatChannelOverride;
|
||||||
|
private ChatChannelOverride? _chatChannelOverrideTempBuffer;
|
||||||
|
private bool _shouldForceNameLookup = false;
|
||||||
|
|
||||||
|
private DateTime _nextMessageIsReply = DateTime.UnixEpoch;
|
||||||
|
|
||||||
|
public ChatChannelOverride? ChatChannelOverride
|
||||||
|
{
|
||||||
|
get => _chatChannelOverride;
|
||||||
|
set {
|
||||||
|
_chatChannelOverride = value;
|
||||||
|
_shouldForceNameLookup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StashChatChannel()
|
||||||
|
{
|
||||||
|
if (_chatChannelOverride != null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Stashing chat channel");
|
||||||
|
_chatChannelOverrideTempBuffer = _chatChannelOverride;
|
||||||
|
ChatChannelOverride = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnstashChatChannel()
|
||||||
|
{
|
||||||
|
if (_chatChannelOverrideTempBuffer != null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Unstashing chat channel");
|
||||||
|
ChatChannelOverride = _chatChannelOverrideTempBuffer;
|
||||||
|
_chatChannelOverrideTempBuffer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameChatHooks(ILogger<GameChatHooks> logger, IGameInteropProvider gameInteropProvider, Action<int, byte[]> ssCommandHandler)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_ssCommandHandler = ssCommandHandler;
|
||||||
|
|
||||||
|
logger.LogInformation("Initializing GameChatHooks");
|
||||||
|
gameInteropProvider.InitializeFromAttributes(this);
|
||||||
|
|
||||||
|
SendMessageHook?.Enable();
|
||||||
|
SetChatChannelHook?.Enable();
|
||||||
|
ChangeChannelNameHook?.Enable();
|
||||||
|
ShouldDoNameLookupHook?.Enable();
|
||||||
|
TempChatChannelHook?.Enable();
|
||||||
|
TempTellTargetHook?.Enable();
|
||||||
|
UnfocusTickHook?.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SendMessageHook?.Dispose();
|
||||||
|
SetChatChannelHook?.Dispose();
|
||||||
|
ChangeChannelNameHook?.Dispose();
|
||||||
|
ShouldDoNameLookupHook?.Dispose();
|
||||||
|
TempChatChannelHook?.Dispose();
|
||||||
|
TempTellTargetHook?.Dispose();
|
||||||
|
UnfocusTickHook?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ProcessChatMessage(Utf8String* message)
|
||||||
|
{
|
||||||
|
var pronounModule = UIModule.Instance()->GetPronounModule();
|
||||||
|
var chatString1 = pronounModule->ProcessString(message, true);
|
||||||
|
var chatString2 = _processStringStep2(pronounModule, chatString1, 1);
|
||||||
|
return MemoryHelper.ReadRaw((nint)chatString2->StringPtr.Value, chatString2->Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendMessageDetour(ShellCommandModule* thisPtr, Utf8String* message, UIModule* uiModule)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var messageLength = message->Length;
|
||||||
|
var messageSpan = message->AsSpan();
|
||||||
|
|
||||||
|
bool isCommand = false;
|
||||||
|
bool isReply = false;
|
||||||
|
|
||||||
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Check if chat input begins with a command (or auto-translated command)
|
||||||
|
// Or if we think we're being called to send text via the /r command
|
||||||
|
if (_nextMessageIsReply >= utcNow)
|
||||||
|
{
|
||||||
|
isCommand = true;
|
||||||
|
}
|
||||||
|
else if (messageLength == 0 || messageSpan[0] == (byte)'/' || !messageSpan.ContainsAnyExcept((byte)' '))
|
||||||
|
{
|
||||||
|
isCommand = true;
|
||||||
|
if (messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/r ")) || messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/reply ")))
|
||||||
|
isReply = true;
|
||||||
|
}
|
||||||
|
else if (messageSpan[0] == (byte)0x02) /* Payload.START_BYTE */
|
||||||
|
{
|
||||||
|
var payload = Payload.Decode(new BinaryReader(new UnmanagedMemoryStream(message->StringPtr, message->BufSize))) as AutoTranslatePayload;
|
||||||
|
|
||||||
|
// Auto-translate text begins with /
|
||||||
|
if (payload != null && payload.Text.Length > 2 && payload.Text[2] == '/')
|
||||||
|
{
|
||||||
|
isCommand = true;
|
||||||
|
if (payload.Text[2..].StartsWith("/r ", StringComparison.Ordinal) || payload.Text[2..].StartsWith("/reply ", StringComparison.Ordinal))
|
||||||
|
isReply = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When using /r the game will set a flag and then call this function a second time
|
||||||
|
// The next call to this function will be raw text intended for the IM recipient
|
||||||
|
// This flag's validity is time-limited as a fail-safe
|
||||||
|
if (isReply)
|
||||||
|
_nextMessageIsReply = utcNow + TimeSpan.FromMilliseconds(100);
|
||||||
|
|
||||||
|
// If it is a command, check if it begins with /ss first so we can handle the message directly
|
||||||
|
// Letting Dalamud handle the commands causes all of the special payloads to be dropped
|
||||||
|
if (isCommand && messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/ss")))
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||||
|
{
|
||||||
|
var cmdString = $"/ss{i} ";
|
||||||
|
if (messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes(cmdString)))
|
||||||
|
{
|
||||||
|
var ssChatBytes = ProcessChatMessage(message);
|
||||||
|
ssChatBytes = ssChatBytes.Skip(cmdString.Length).ToArray();
|
||||||
|
_ssCommandHandler?.Invoke(i, ssChatBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not a command, or no override is set, then call the original chat handler
|
||||||
|
if (isCommand || _chatChannelOverride == null)
|
||||||
|
{
|
||||||
|
SendMessageHook!.OriginalDisposeSafe(thisPtr, message, uiModule);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, the text is to be sent to the emulated chat channel handler
|
||||||
|
// The chat input string is rendered in to a payload for display first
|
||||||
|
var chatBytes = ProcessChatMessage(message);
|
||||||
|
|
||||||
|
if (chatBytes.Length > 0)
|
||||||
|
_chatChannelOverride.ChatMessageHandler?.Invoke(chatBytes);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Exception thrown during SendMessageDetour");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetChatChannelDetour(RaptureShellModule* module, uint channel)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_chatChannelOverride != null)
|
||||||
|
{
|
||||||
|
_chatChannelOverride = null;
|
||||||
|
_shouldForceNameLookup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Exception thrown during SetChatChannelDetour");
|
||||||
|
}
|
||||||
|
|
||||||
|
SetChatChannelHook!.OriginalDisposeSafe(module, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TempChatChannelDetour(RaptureShellModule* module, uint x, uint y, ulong z)
|
||||||
|
{
|
||||||
|
var result = TempChatChannelHook!.OriginalDisposeSafe(module, x, y, z);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
|
StashChatChannel();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ulong TempTellTargetDetour(RaptureShellModule* module, ulong a, ulong b, ulong c, ushort d, ulong e, ulong f, ushort g)
|
||||||
|
{
|
||||||
|
var result = TempTellTargetHook!.OriginalDisposeSafe(module, a, b, c, d, e, f, g);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
|
StashChatChannel();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnfocusTickDetour(RaptureShellModule* module)
|
||||||
|
{
|
||||||
|
UnfocusTickHook!.OriginalDisposeSafe(module);
|
||||||
|
UnstashChatChannel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte* ChangeChannelNameDetour(AgentChatLog* agent)
|
||||||
|
{
|
||||||
|
var originalResult = ChangeChannelNameHook!.OriginalDisposeSafe(agent);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Replace the chat channel name on the UI if active
|
||||||
|
if (_chatChannelOverride != null)
|
||||||
|
{
|
||||||
|
agent->ChannelLabel.SetString(_chatChannelOverride.ChannelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Exception thrown during ChangeChannelNameDetour");
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte ShouldDoNameLookupDetour(AgentChatLog* agent)
|
||||||
|
{
|
||||||
|
var originalResult = ShouldDoNameLookupHook!.OriginalDisposeSafe(agent);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Force the chat channel name to update when required
|
||||||
|
if (_shouldForceNameLookup)
|
||||||
|
{
|
||||||
|
_shouldForceNameLookup = false;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Exception thrown during ShouldDoNameLookupDetour");
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
270
MareSynchronos/Interop/GameModel/MdlFile.cs
Normal file
270
MareSynchronos/Interop/GameModel/MdlFile.cs
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
using Lumina.Data;
|
||||||
|
using Lumina.Extensions;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
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;
|
||||||
|
public uint[] IndexOffset;
|
||||||
|
|
||||||
|
public uint[] VertexBufferSize;
|
||||||
|
public uint[] IndexBufferSize;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
VertexOffset = Array.Empty<uint>();
|
||||||
|
IndexOffset = Array.Empty<uint>();
|
||||||
|
VertexBufferSize = Array.Empty<uint>();
|
||||||
|
IndexBufferSize = Array.Empty<uint>();
|
||||||
|
VertexDeclarations = Array.Empty<VertexDeclarationStruct>();
|
||||||
|
ElementIds = Array.Empty<ElementIdStruct>();
|
||||||
|
Meshes = Array.Empty<MeshStruct>();
|
||||||
|
BoneBoundingBoxes = Array.Empty<BoundingBoxStruct>();
|
||||||
|
Lods = Array.Empty<LodStruct>();
|
||||||
|
ExtraLods = Array.Empty<ExtraLodStruct>();
|
||||||
|
|
||||||
|
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.HasFlag(ModelFlags2.ExtraLodEnabled)
|
||||||
|
? 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
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();
|
||||||
|
}
|
||||||
145
MareSynchronos/Interop/Ipc/IpcCallerBrio.cs
Normal file
145
MareSynchronos/Interop/Ipc/IpcCallerBrio.cs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
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);
|
||||||
|
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
253
MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs
Normal file
253
MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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 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 _pluginLoaded;
|
||||||
|
private Version _pluginVersion;
|
||||||
|
|
||||||
|
private bool _shownGlamourerUnavailable = false;
|
||||||
|
private readonly uint LockCode = 0x626E7579;
|
||||||
|
|
||||||
|
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;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_redrawManager = redrawManager;
|
||||||
|
|
||||||
|
var plugin = PluginWatcherService.GetInitialPluginState(pi, "Glamourer");
|
||||||
|
|
||||||
|
_pluginLoaded = plugin?.IsLoaded ?? false;
|
||||||
|
_pluginVersion = plugin?.Version ?? new(0, 0, 0, 0);
|
||||||
|
|
||||||
|
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "Glamourer", (msg) =>
|
||||||
|
{
|
||||||
|
_pluginLoaded = msg.IsLoaded;
|
||||||
|
_pluginVersion = msg.Version;
|
||||||
|
CheckAPI();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = _pluginLoaded && _pluginVersion >= new Version(1, 0, 6, 1);
|
||||||
|
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 Umbra. 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 allowImmediate = false)
|
||||||
|
{
|
||||||
|
if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return;
|
||||||
|
|
||||||
|
// Call immediately if possible
|
||||||
|
if (allowImmediate && _dalamudUtil.IsOnFrameworkThread && !await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var gameObj = await _dalamudUtil.CreateGameObjectAsync(handler.Address).ConfigureAwait(false);
|
||||||
|
if (gameObj is ICharacter chara)
|
||||||
|
{
|
||||||
|
logger.LogDebug("[{appid}] Calling on IPC: GlamourerApplyAll", applicationId);
|
||||||
|
_glamourerApplyAll!.Invoke(customization, chara.ObjectIndex, LockCode);
|
||||||
|
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: GlamourerUnlock", 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 void RevertNow(ILogger logger, Guid applicationId, int objectIndex)
|
||||||
|
{
|
||||||
|
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||||
|
logger.LogTrace("[{applicationId}] Immediately reverting object index {objId}", applicationId, objectIndex);
|
||||||
|
_glamourerRevert.Invoke(objectIndex, LockCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RevertByNameNow(ILogger logger, Guid applicationId, string name)
|
||||||
|
{
|
||||||
|
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||||
|
logger.LogTrace("[{applicationId}] Immediately reverting {name}", applicationId, name);
|
||||||
|
_glamourerRevertByName.Invoke(name, LockCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: >= 0 };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
APIAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs
Normal file
135
MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
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: >= 0 };
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
string title = _honorificGetLocalCharacterTitle.InvokeFunc();
|
||||||
|
return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title));
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
44
MareSynchronos/Interop/Ipc/IpcCallerMare.cs
Normal file
44
MareSynchronos/Interop/Ipc/IpcCallerMare.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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 IpcCallerMare : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ICallGateSubscriber<List<nint>> _mareHandledGameAddresses;
|
||||||
|
private readonly List<nint> _emptyList = [];
|
||||||
|
|
||||||
|
private bool _pluginLoaded;
|
||||||
|
|
||||||
|
public IpcCallerMare(ILogger<IpcCallerMare> logger, IDalamudPluginInterface pi, MareMediator mediator) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_mareHandledGameAddresses = pi.GetIpcSubscriber<List<nint>>("MareSynchronos.GetHandledAddresses");
|
||||||
|
|
||||||
|
_pluginLoaded = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false;
|
||||||
|
|
||||||
|
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "MareSynchronos", (msg) =>
|
||||||
|
{
|
||||||
|
_pluginLoaded = msg.IsLoaded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool APIAvailable { get; private set; } = false;
|
||||||
|
|
||||||
|
// Must be called on framework thread
|
||||||
|
public IReadOnlyList<nint> GetHandledGameAddresses()
|
||||||
|
{
|
||||||
|
if (!_pluginLoaded) return _emptyList;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _mareHandledGameAddresses.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return _emptyList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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.GetStatusManagerByPtrV2");
|
||||||
|
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
|
||||||
|
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
|
||||||
|
|
||||||
|
_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() == 3;
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
366
MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs
Normal file
366
MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
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 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;
|
||||||
|
|
||||||
|
private bool _pluginLoaded;
|
||||||
|
private Version _pluginVersion;
|
||||||
|
|
||||||
|
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||||
|
MareMediator mareMediator, RedrawManager redrawManager) : base(logger, mareMediator)
|
||||||
|
{
|
||||||
|
_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);
|
||||||
|
|
||||||
|
var plugin = PluginWatcherService.GetInitialPluginState(pi, "Penumbra");
|
||||||
|
|
||||||
|
_pluginLoaded = plugin?.IsLoaded ?? false;
|
||||||
|
_pluginVersion = plugin?.Version ?? new(0, 0, 0, 0);
|
||||||
|
|
||||||
|
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "Penumbra", (msg) =>
|
||||||
|
{
|
||||||
|
_pluginLoaded = msg.IsLoaded;
|
||||||
|
_pluginVersion = msg.Version;
|
||||||
|
CheckAPI();
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
penumbraAvailable = _pluginLoaded && _pluginVersion >= new Version(1, 5, 1, 0);
|
||||||
|
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 Umbra. 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(() =>
|
||||||
|
{
|
||||||
|
Guid collId;
|
||||||
|
var collName = "ElfSync_" + uid;
|
||||||
|
PenumbraApiEc penEC = _penumbraCreateNamedTemporaryCollection.Invoke(uid, collName, out collId);
|
||||||
|
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
|
||||||
|
if (penEC != PenumbraApiEc.Success)
|
||||||
|
{
|
||||||
|
logger.LogError("Failed to create temporary collection for {collName} with error code {penEC}. Please include this line in any error reports", collName, penEC);
|
||||||
|
return Guid.Empty;
|
||||||
|
}
|
||||||
|
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 void RedrawNow(ILogger logger, Guid applicationId, int objectIndex)
|
||||||
|
{
|
||||||
|
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
||||||
|
logger.LogTrace("[{applicationId}] Immediately redrawing object index {objId}", applicationId, objectIndex);
|
||||||
|
_penumbraRedraw.Invoke(objectIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.OnReady");
|
||||||
|
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.OnDisposing");
|
||||||
|
_apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion");
|
||||||
|
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.IsEnabled");
|
||||||
|
|
||||||
|
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.OnPlayerDataChanged");
|
||||||
|
_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: 4, Item2: >= 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
MareSynchronos/Interop/Ipc/IpcManager.cs
Normal file
68
MareSynchronos/Interop/Ipc/IpcManager.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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 int _stateCheckCounter = -1;
|
||||||
|
|
||||||
|
private void PeriodicApiStateCheck()
|
||||||
|
{
|
||||||
|
// Stagger API checks
|
||||||
|
if (++_stateCheckCounter > 8)
|
||||||
|
_stateCheckCounter = 0;
|
||||||
|
int i = _stateCheckCounter;
|
||||||
|
if (i == 0) Penumbra.CheckAPI();
|
||||||
|
if (i == 1) Penumbra.CheckModDirectory();
|
||||||
|
if (i == 2) Glamourer.CheckAPI();
|
||||||
|
if (i == 3) Heels.CheckAPI();
|
||||||
|
if (i == 4) CustomizePlus.CheckAPI();
|
||||||
|
if (i == 5) Honorific.CheckAPI();
|
||||||
|
if (i == 6) Moodles.CheckAPI();
|
||||||
|
if (i == 7) PetNames.CheckAPI();
|
||||||
|
if (i == 8) Brio.CheckAPI();
|
||||||
|
}
|
||||||
|
}
|
||||||
225
MareSynchronos/Interop/Ipc/IpcProvider.cs
Normal file
225
MareSynchronos/Interop/Ipc/IpcProvider.cs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using System;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
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 MareConfigService _mareConfig;
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
private ICallGateProvider<string, IGameObject, bool>? _loadFileProviderMare;
|
||||||
|
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProviderMare;
|
||||||
|
private ICallGateProvider<List<nint>>? _handledGameAddressesMare;
|
||||||
|
|
||||||
|
private bool _marePluginEnabled = false;
|
||||||
|
private bool _impersonating = false;
|
||||||
|
private DateTime _unregisterTime = DateTime.UtcNow;
|
||||||
|
private CancellationTokenSource? _registerDelayCts = new();
|
||||||
|
|
||||||
|
public bool MarePluginEnabled => _marePluginEnabled;
|
||||||
|
public bool ImpersonationActive => _impersonating;
|
||||||
|
|
||||||
|
public MareMediator Mediator { get; init; }
|
||||||
|
|
||||||
|
public IpcProvider(ILogger<IpcProvider> logger, IDalamudPluginInterface pi, MareConfigService mareConfig,
|
||||||
|
CharaDataManager charaDataManager, MareMediator mareMediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_pi = pi;
|
||||||
|
_mareConfig = mareConfig;
|
||||||
|
_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);
|
||||||
|
});
|
||||||
|
|
||||||
|
_marePluginEnabled = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false;
|
||||||
|
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "MareSynchronos", p => {
|
||||||
|
_marePluginEnabled = p.IsLoaded;
|
||||||
|
HandleMareImpersonation(automatic: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Starting IpcProvider Service");
|
||||||
|
_loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("ElfSync.LoadMcdf");
|
||||||
|
_loadFileProvider.RegisterFunc(LoadMcdf);
|
||||||
|
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("UmbraSync.LoadMcdfAsync");
|
||||||
|
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
|
||||||
|
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("UmbraSync.GetHandledAddresses");
|
||||||
|
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
|
||||||
|
|
||||||
|
_loadFileProviderMare = _pi.GetIpcProvider<string, IGameObject, bool>("MareSynchronos.LoadMcdf");
|
||||||
|
_loadFileAsyncProviderMare = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("MareSynchronos.LoadMcdfAsync");
|
||||||
|
_handledGameAddressesMare = _pi.GetIpcProvider<List<nint>>("MareSynchronos.GetHandledAddresses");
|
||||||
|
HandleMareImpersonation(automatic: true);
|
||||||
|
|
||||||
|
_logger.LogInformation("Started IpcProviderService");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleMareImpersonation(bool automatic = false)
|
||||||
|
{
|
||||||
|
if (_marePluginEnabled)
|
||||||
|
{
|
||||||
|
if (_impersonating)
|
||||||
|
{
|
||||||
|
_loadFileProviderMare?.UnregisterFunc();
|
||||||
|
_loadFileAsyncProviderMare?.UnregisterFunc();
|
||||||
|
_handledGameAddressesMare?.UnregisterFunc();
|
||||||
|
_impersonating = false;
|
||||||
|
_unregisterTime = DateTime.UtcNow;
|
||||||
|
_logger.LogDebug("Unregistered MareSynchronos API");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_mareConfig.Current.MareAPI)
|
||||||
|
{
|
||||||
|
var cancelToken = EnsureFreshCts(ref _registerDelayCts).Token;
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
// Wait before registering to reduce the chance of a race condition
|
||||||
|
if (automatic)
|
||||||
|
await Task.Delay(5000);
|
||||||
|
|
||||||
|
if (cancelToken.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_marePluginEnabled)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Not registering MareSynchronos API: Mare plugin is loaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadFileProviderMare?.RegisterFunc(LoadMcdf);
|
||||||
|
_loadFileAsyncProviderMare?.RegisterFunc(LoadMcdfAsync);
|
||||||
|
_handledGameAddressesMare?.RegisterFunc(GetHandledAddresses);
|
||||||
|
_impersonating = true;
|
||||||
|
_logger.LogDebug("Registered MareSynchronos API");
|
||||||
|
}, cancelToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EnsureFreshCts(ref _registerDelayCts);
|
||||||
|
if (_impersonating)
|
||||||
|
{
|
||||||
|
_loadFileProviderMare?.UnregisterFunc();
|
||||||
|
_loadFileAsyncProviderMare?.UnregisterFunc();
|
||||||
|
_handledGameAddressesMare?.UnregisterFunc();
|
||||||
|
_impersonating = false;
|
||||||
|
_unregisterTime = DateTime.UtcNow;
|
||||||
|
_logger.LogDebug("Unregistered MareSynchronos API");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Stopping IpcProvider Service");
|
||||||
|
_loadFileProvider?.UnregisterFunc();
|
||||||
|
_loadFileAsyncProvider?.UnregisterFunc();
|
||||||
|
_handledGameAddresses?.UnregisterFunc();
|
||||||
|
|
||||||
|
TryCancel(_registerDelayCts);
|
||||||
|
if (_impersonating)
|
||||||
|
{
|
||||||
|
_loadFileProviderMare?.UnregisterFunc();
|
||||||
|
_loadFileAsyncProviderMare?.UnregisterFunc();
|
||||||
|
_handledGameAddressesMare?.UnregisterFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.UnsubscribeAll(this);
|
||||||
|
CancelAndDispose(ref _registerDelayCts);
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (!_impersonating)
|
||||||
|
{
|
||||||
|
if ((DateTime.UtcNow - _unregisterTime).TotalSeconds >= 1.0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetHandledAddresses called when it should not be registered");
|
||||||
|
_handledGameAddressesMare?.UnregisterFunc();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
TryCancel(cts);
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCancel(CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
MareSynchronos/Interop/Ipc/RedrawManager.cs
Normal file
94
MareSynchronos/Interop/Ipc/RedrawManager.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using System;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.Interop.Ipc;
|
||||||
|
|
||||||
|
public class RedrawManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly ConcurrentDictionary<nint, bool> _penumbraRedrawRequests = [];
|
||||||
|
private CancellationTokenSource? _disposalCts = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
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, EnsureFreshCts(ref _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()
|
||||||
|
{
|
||||||
|
EnsureFreshCts(ref _disposalCts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref _disposalCts);
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CancellationTokenSource EnsureFreshCts(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
CancelAndDispose(ref cts);
|
||||||
|
cts = new CancellationTokenSource();
|
||||||
|
return cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CancelAndDispose(ref CancellationTokenSource? cts)
|
||||||
|
{
|
||||||
|
if (cts == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
cts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
MareSynchronos/Interop/VfxSpawnManager.cs
Normal file
203
MareSynchronos/Interop/VfxSpawnManager.cs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
#region signatures
|
||||||
|
#pragma warning disable CS0649
|
||||||
|
[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;
|
||||||
|
#pragma warning restore CS0649
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
MareSynchronos/MareConfiguration/ConfigurationMigrator.cs
Normal file
82
MareSynchronos/MareConfiguration/ConfigurationMigrator.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using MareSynchronos.WebAPI;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, MareConfigService mareConfig) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||||
|
private readonly MareConfigService _mareConfig = mareConfig;
|
||||||
|
|
||||||
|
public void Migrate()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = _mareConfig.ConfigurationPath;
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("EnableAutoSyncDiscovery", out var enableAutoSync))
|
||||||
|
{
|
||||||
|
var val = enableAutoSync.GetBoolean();
|
||||||
|
if (_mareConfig.Current.EnableAutoDetectDiscovery != val)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.EnableAutoDetectDiscovery = val;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AllowAutoSyncPairRequests", out var allowAutoSync))
|
||||||
|
{
|
||||||
|
var val = allowAutoSync.GetBoolean();
|
||||||
|
if (_mareConfig.Current.AllowAutoDetectPairRequests != val)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AllowAutoDetectPairRequests = val;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AutoSyncMaxDistanceMeters", out var maxDistSync) && maxDistSync.TryGetInt32(out var md))
|
||||||
|
{
|
||||||
|
if (_mareConfig.Current.AutoDetectMaxDistanceMeters != md)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AutoDetectMaxDistanceMeters = md;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.TryGetProperty("AutoSyncMuteMinutes", out var muteSync) && muteSync.TryGetInt32(out var mm))
|
||||||
|
{
|
||||||
|
if (_mareConfig.Current.AutoDetectMuteMinutes != mm)
|
||||||
|
{
|
||||||
|
_mareConfig.Current.AutoDetectMuteMinutes = mm;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Migrated config: AutoSync -> AutoDetect fields");
|
||||||
|
_mareConfig.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Configuration migration failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs
Normal file
141
MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
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 = Activator.CreateInstance<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,20 @@
|
|||||||
|
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 NearbyMaxWisps { get; set; } = 20;
|
||||||
|
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,95 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
using MareSynchronos.UI;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class MareConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public int ExpectedTOSVersion = 2;
|
||||||
|
public int AcceptedTOSVersion { get; set; } = 0;
|
||||||
|
public bool AcceptedAgreement { get; set; } = false;
|
||||||
|
public string CacheFolder { get; set; } = string.Empty;
|
||||||
|
public bool DisableOptionalPluginWarnings { get; set; } = false;
|
||||||
|
public bool EnableDtrEntry { get; set; } = true;
|
||||||
|
public int DtrStyle { get; set; } = 0;
|
||||||
|
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: 0x8D37C0u);
|
||||||
|
public bool UseNameColors { get; set; } = false;
|
||||||
|
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
|
||||||
|
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
|
||||||
|
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 bool LogEvents { get; set; } = true;
|
||||||
|
public bool HoldCombatApplication { get; set; } = false;
|
||||||
|
public double MaxLocalCacheInGiB { get; set; } = 100;
|
||||||
|
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||||
|
public bool OpenPopupOnAdd { get; set; } = true;
|
||||||
|
public int ParallelDownloads { get; set; } = 10;
|
||||||
|
public bool EnableDownloadQueue { get; set; } = false;
|
||||||
|
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||||
|
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||||
|
[Obsolete] 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; } = false;
|
||||||
|
public bool ShowSyncshellUsersInVisible { get; set; } = true;
|
||||||
|
[Obsolete] public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false;
|
||||||
|
public bool ShowCharacterNames { get; set; } = true;
|
||||||
|
public bool ShowOfflineUsersSeparately { get; set; } = true;
|
||||||
|
public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true;
|
||||||
|
public bool GroupUpSyncshells { get; set; } = true;
|
||||||
|
public bool SerialApplication { get; set; } = false;
|
||||||
|
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 string LastChangelogVersionSeen { get; set; } = string.Empty;
|
||||||
|
public bool DefaultDisableSounds { get; set; } = false;
|
||||||
|
public bool DefaultDisableAnimations { get; set; } = false;
|
||||||
|
public bool DefaultDisableVfx { get; set; } = false;
|
||||||
|
public Dictionary<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public bool EnableAutoDetectDiscovery { get; set; } = true;
|
||||||
|
public bool AllowAutoDetectPairRequests { get; set; } = true;
|
||||||
|
public int AutoDetectMaxDistanceMeters { get; set; } = 40;
|
||||||
|
public int AutoDetectMuteMinutes { get; set; } = 5;
|
||||||
|
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 int Version { get; set; } = 1;
|
||||||
|
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
||||||
|
|
||||||
|
public bool DisableSyncshellChat { get; set; } = false;
|
||||||
|
public int ChatColor { get; set; } = 0; // 0 means "use plugin default"
|
||||||
|
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
|
||||||
|
public bool ExtraChatAPI { get; set; } = false;
|
||||||
|
public bool ExtraChatTags { get; set; } = false;
|
||||||
|
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
|
||||||
|
public bool TypingIndicatorShowOnPartyList { get; set; } = true;
|
||||||
|
public bool TypingIndicatorEnabled { get; set; } = true;
|
||||||
|
public bool TypingIndicatorShowSelf { get; set; } = true;
|
||||||
|
public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
|
||||||
|
|
||||||
|
public bool MareAPI { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class NotificationsConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public List<StoredNotification> Notifications { get; set; } = new();
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class PlayerPerformanceConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
|
||||||
|
public bool NotifyAutoPauseDirectPairs { get; set; } = true;
|
||||||
|
public bool NotifyAutoPauseGroupPairs { get; set; } = true;
|
||||||
|
public bool ShowSelfAnalysisWarnings { get; set; } = true;
|
||||||
|
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
|
||||||
|
public int TrisAutoPauseThresholdThousands { get; set; } = 400;
|
||||||
|
public bool IgnoreDirectPairs { get; set; } = true;
|
||||||
|
public TextureShrinkMode TextureShrinkMode { get; set; } = TextureShrinkMode.Default;
|
||||||
|
public bool TextureShrinkDeleteOriginal { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class RemoteConfigCache : IMareConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
public ulong Timestamp { get; set; } = 0;
|
||||||
|
public string Origin { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset? LastModified { get; set; } = null;
|
||||||
|
public string ETag { get; set; } = string.Empty;
|
||||||
|
public JsonObject Configuration { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerBlockConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerBlockStorage> ServerBlocks { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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.UmbraServer, ServerUri = ApiController.UmbraServiceUri } },
|
||||||
|
};
|
||||||
|
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerTagConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class SyncshellConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerShellStorage> ServerShellStorage { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class TransientConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, HashSet<string>> PlayerPersistentTransientCache { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class UidNotesConfig : IMareConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, ServerNotesStorage> ServerNotes { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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, (uint Mip0Size, int MipCount, ushort Width, ushort Height)> TexDictionary { 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class StoredNotification
|
||||||
|
{
|
||||||
|
public string Category { get; set; } = string.Empty; // name of enum NotificationCategory
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -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,8 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerBlockStorage
|
||||||
|
{
|
||||||
|
public List<string> Whitelist { get; set; } = new();
|
||||||
|
public List<string> Blacklist { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerNotesStorage
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> GidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, string> UidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, string> UidLastSeenNames { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ServerShellStorage
|
||||||
|
{
|
||||||
|
public Dictionary<string, ShellConfig> GidShellConfig { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
11
MareSynchronos/MareConfiguration/Models/ServerStorage.cs
Normal file
11
MareSynchronos/MareConfiguration/Models/ServerStorage.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
10
MareSynchronos/MareConfiguration/Models/ShellConfig.cs
Normal file
10
MareSynchronos/MareConfiguration/Models/ShellConfig.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ShellConfig
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public int ShellNumber { get; set; }
|
||||||
|
public int Color { get; set; } = 0; // 0 means "default to the global setting"
|
||||||
|
public int LogKind { get; set; } = 0; // 0 means "default to the global setting"
|
||||||
|
}
|
||||||
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
13
MareSynchronos/MareConfiguration/Models/SyncOverrideEntry.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class SyncOverrideEntry
|
||||||
|
{
|
||||||
|
public bool? DisableSounds { get; set; }
|
||||||
|
public bool? DisableAnimations { get; set; }
|
||||||
|
public bool? DisableVfx { get; set; }
|
||||||
|
|
||||||
|
public bool IsEmpty => DisableSounds is null && DisableAnimations is null && DisableVfx is null;
|
||||||
|
}
|
||||||
10
MareSynchronos/MareConfiguration/Models/TextureShrinkMode.cs
Normal file
10
MareSynchronos/MareConfiguration/Models/TextureShrinkMode.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
public enum TextureShrinkMode
|
||||||
|
{
|
||||||
|
Never,
|
||||||
|
Default,
|
||||||
|
DefaultHiRes,
|
||||||
|
Always,
|
||||||
|
AlwaysHiRes
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace MareSynchronos.MareConfiguration.Models;
|
||||||
|
|
||||||
|
public enum TypingIndicatorBubbleSize
|
||||||
|
{
|
||||||
|
Small,
|
||||||
|
Medium,
|
||||||
|
Large
|
||||||
|
}
|
||||||
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,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class NotificationsConfigService : ConfigurationServiceBase<NotificationsConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "notifications.json";
|
||||||
|
|
||||||
|
public NotificationsConfigService(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;
|
||||||
|
}
|
||||||
11
MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs
Normal file
11
MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class RemoteConfigCacheService : ConfigurationServiceBase<RemoteConfigCache>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "remotecache.json";
|
||||||
|
|
||||||
|
public RemoteConfigCacheService(string configDir) : base(configDir) { }
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
14
MareSynchronos/MareConfiguration/ServerBlockConfigService.cs
Normal file
14
MareSynchronos/MareConfiguration/ServerBlockConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class ServerBlockConfigService : ConfigurationServiceBase<ServerBlockConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "blocks.json";
|
||||||
|
|
||||||
|
public ServerBlockConfigService(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/SyncshellConfigService.cs
Normal file
14
MareSynchronos/MareConfiguration/SyncshellConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using MareSynchronos.MareConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace MareSynchronos.MareConfiguration;
|
||||||
|
|
||||||
|
public class SyncshellConfigService : ConfigurationServiceBase<SyncshellConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "syncshells.json";
|
||||||
|
|
||||||
|
public SyncshellConfigService(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;
|
||||||
|
}
|
||||||
175
MareSynchronos/MarePlugin.cs
Normal file
175
MareSynchronos/MarePlugin.cs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
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 MareSynchronos.Interop;
|
||||||
|
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}.{rev}", "Umbra Sync", version.Major, version.Minor, version.Build, version.Revision);
|
||||||
|
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(MarePlugin), Services.Events.EventSeverity.Informational,
|
||||||
|
$"Starting Umbra Sync {version.Major}.{version.Minor}.{version.Build}.{version.Revision}")));
|
||||||
|
|
||||||
|
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<OnlinePlayerManager>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<SyncDefaultsService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatTypingDetectionService>();
|
||||||
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
|
||||||
|
var characterAnalyzer = _runtimeServiceScope.ServiceProvider.GetRequiredService<CharacterAnalyzer>();
|
||||||
|
_ = characterAnalyzer.ComputeAnalysis(print: false);
|
||||||
|
|
||||||
|
#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 \"Umbra Settings -> Debug\" unless instructed otherwise.",
|
||||||
|
MareConfiguration.Models.NotificationType.Error, TimeSpan.FromSeconds(15000)));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger?.LogCritical(ex, "Error during launch of managers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
MareSynchronos/MareSynchronos.csproj
Normal file
65
MareSynchronos/MareSynchronos.csproj
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
|
||||||
|
<PropertyGroup>
|
||||||
|
<AssemblyName>UmbraSync</AssemblyName>
|
||||||
|
<RootNamespace>UmbraSync</RootNamespace>
|
||||||
|
<Version>0.1.9.9</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="PlayerData\Export\**" />
|
||||||
|
<EmbeddedResource Remove="PlayerData\Export\**" />
|
||||||
|
<None Remove="PlayerData\Export\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Chaos.NaCl.Standard" Version="1.0.0" />
|
||||||
|
<PackageReference Include="Downloader" Version="3.3.4" />
|
||||||
|
<PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" />
|
||||||
|
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" />
|
||||||
|
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||||
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="Exists('.\Penumbra.Api\Penumbra.Api.csproj')">
|
||||||
|
<ProjectReference Include=".\Penumbra.Api\Penumbra.Api.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="!Exists('.\Penumbra.Api\Penumbra.Api.csproj')">
|
||||||
|
<PackageReference Include="Penumbra.Api" Version="5.12.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="Exists('.\Glamourer.Api\Glamourer.Api.csproj')">
|
||||||
|
<ProjectReference Include=".\Glamourer.Api\Glamourer.Api.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="!Exists('.\Glamourer.Api\Glamourer.Api.csproj')">
|
||||||
|
<PackageReference Include="Glamourer.Api" Version="2.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<SourceRevisionId>build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ"))</SourceRevisionId>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DisablePackageVulnerabilityAnalysis>true</DisablePackageVulnerabilityAnalysis>
|
||||||
|
<NoWarn>$(NoWarn);NU1900</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
50
MareSynchronos/PlayerData/Data/CharacterData.cs
Normal file
50
MareSynchronos/PlayerData/Data/CharacterData.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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 PetNamesData { get; set; } = string.Empty;
|
||||||
|
public string MoodlesData { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
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,
|
||||||
|
PetNamesData = PetNamesData,
|
||||||
|
MoodlesData = MoodlesData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
42
MareSynchronos/PlayerData/Data/FileReplacement.cs
Normal file
42
MareSynchronos/PlayerData/Data/FileReplacement.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable MA0009
|
||||||
|
[GeneratedRegex(@"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript)]
|
||||||
|
private static partial Regex LocalPathRegex();
|
||||||
|
#pragma warning restore MA0009
|
||||||
|
}
|
||||||
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,33 @@
|
|||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
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 MareConfigService _mareConfigService;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
|
||||||
|
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_fileCompactor = fileCompactor;
|
||||||
|
_mareConfigService = mareConfigService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileDownloadManager Create()
|
||||||
|
{
|
||||||
|
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, _mareConfigService);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs
Normal file
30
MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
|
public class PairAnalyzerFactory
|
||||||
|
{
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
private readonly MareMediator _mareMediator;
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||||
|
|
||||||
|
public PairAnalyzerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator,
|
||||||
|
FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_modelAnalyzer = modelAnalyzer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairAnalyzer Create(Pair pair)
|
||||||
|
{
|
||||||
|
return new PairAnalyzer(_loggerFactory.CreateLogger<PairAnalyzer>(), pair, _mareMediator,
|
||||||
|
_fileCacheManager, _modelAnalyzer);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
MareSynchronos/PlayerData/Factories/PairFactory.cs
Normal file
33
MareSynchronos/PlayerData/Factories/PairFactory.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
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 MareConfigService _mareConfig;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
|
public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory,
|
||||||
|
MareMediator mareMediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_cachedPlayerFactory = cachedPlayerFactory;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_mareConfig = mareConfig;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair Create(UserData userData)
|
||||||
|
{
|
||||||
|
return new Pair(_loggerFactory.CreateLogger<Pair>(), userData, _cachedPlayerFactory, _mareMediator, _mareConfig, _serverConfigurationManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs
Normal file
57
MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Factories;
|
||||||
|
|
||||||
|
public class PairHandlerFactory
|
||||||
|
{
|
||||||
|
private readonly MareConfigService _configService;
|
||||||
|
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 PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
|
private readonly PairAnalyzerFactory _pairAnalyzerFactory;
|
||||||
|
private readonly VisibilityService _visibilityService;
|
||||||
|
|
||||||
|
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
||||||
|
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||||
|
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||||
|
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
|
||||||
|
PairAnalyzerFactory pairAnalyzerFactory,
|
||||||
|
MareConfigService configService, VisibilityService visibilityService)
|
||||||
|
{
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
|
_hostApplicationLifetime = hostApplicationLifetime;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_mareMediator = mareMediator;
|
||||||
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_pairAnalyzerFactory = pairAnalyzerFactory;
|
||||||
|
_configService = configService;
|
||||||
|
_visibilityService = visibilityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairHandler Create(Pair pair)
|
||||||
|
{
|
||||||
|
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
|
||||||
|
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||||
|
_fileCacheManager, _mareMediator, _playerPerformanceService, _configService, _visibilityService);
|
||||||
|
}
|
||||||
|
}
|
||||||
365
MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs
Normal file
365
MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
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 static readonly string[] _allowedExtensionsForGamePaths = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"];
|
||||||
|
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 BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!_ipcManager.Initialized)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerRelatedObject == null) return;
|
||||||
|
|
||||||
|
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);
|
||||||
|
previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind);
|
||||||
|
previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind);
|
||||||
|
previousData.CustomizePlusScale.Remove(playerRelatedObject.ObjectKind);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousFileReplacements = previousData.FileReplacements.ToDictionary(d => d.Key, d => d.Value);
|
||||||
|
var previousGlamourerData = previousData.GlamourerString.ToDictionary(d => d.Key, d => d.Value);
|
||||||
|
var previousCustomize = previousData.CustomizePlusScale.ToDictionary(d => d.Key, d => d.Value);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
|
||||||
|
{
|
||||||
|
await CreateCharacterData(previousData, playerRelatedObject, token).ConfigureAwait(false);
|
||||||
|
}).ConfigureAwait(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Cancelled creating Character data for {object}", playerRelatedObject);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Failed to create {object} data", playerRelatedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousData.FileReplacements = previousFileReplacements;
|
||||||
|
previousData.GlamourerString = previousGlamourerData;
|
||||||
|
previousData.CustomizePlusScale = previousCustomize;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<CharacterData> CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token)
|
||||||
|
{
|
||||||
|
var objectKind = playerRelatedObject.ObjectKind;
|
||||||
|
|
||||||
|
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||||
|
|
||||||
|
if (!previousData.FileReplacements.TryGetValue(objectKind, out HashSet<FileReplacement>? value))
|
||||||
|
{
|
||||||
|
previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
previousData.CustomizePlusScale.Remove(objectKind);
|
||||||
|
|
||||||
|
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||||
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).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, token).ConfigureAwait(false);
|
||||||
|
totalWaitTime -= 50;
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
|
||||||
|
previousData.FileReplacements[objectKind] =
|
||||||
|
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
|
||||||
|
.Where(p => p.HasFileReplacement).ToHashSet();
|
||||||
|
previousData.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !_allowedExtensionsForGamePaths.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||||
|
|
||||||
|
_logger.LogDebug("== Static Replacements ==");
|
||||||
|
foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Persisting {item}", item);
|
||||||
|
_transientResourceManager.AddSemiTransientResource(objectKind, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_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(playerRelatedObject.Address, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList());
|
||||||
|
|
||||||
|
// get all remaining paths and resolve them
|
||||||
|
var transientPaths = ManageSemiTransientData(objectKind, playerRelatedObject.Address);
|
||||||
|
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);
|
||||||
|
previousData.FileReplacements[objectKind].Add(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||||
|
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. previousData.FileReplacements[objectKind]]);
|
||||||
|
|
||||||
|
// make sure we only return data that actually has file replacements
|
||||||
|
foreach (var item in previousData.FileReplacements)
|
||||||
|
{
|
||||||
|
previousData.FileReplacements[item.Key] = new HashSet<FileReplacement>(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gather up data from ipc
|
||||||
|
previousData.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
|
||||||
|
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();
|
||||||
|
previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]);
|
||||||
|
var customizeScale = await getCustomizeData.ConfigureAwait(false);
|
||||||
|
previousData.CustomizePlusScale[playerRelatedObject.ObjectKind] = customizeScale ?? string.Empty;
|
||||||
|
_logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale[playerRelatedObject.ObjectKind]);
|
||||||
|
previousData.HonorificData = await getHonorificTitle.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Honorific is now: {data}", previousData.HonorificData);
|
||||||
|
previousData.HeelsData = await getHeelsOffset.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Heels is now: {heels}", previousData.HeelsData);
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
previousData.PetNamesData = _ipcManager.PetNames.GetLocalNames();
|
||||||
|
_logger.LogDebug("Pet Nicknames is now: {petnames}", previousData.PetNamesData);
|
||||||
|
previousData.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousData.FileReplacements.TryGetValue(objectKind, out HashSet<FileReplacement>? fileReplacements))
|
||||||
|
{
|
||||||
|
var toCompute = 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)
|
||||||
|
{
|
||||||
|
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
|
||||||
|
}
|
||||||
|
var removed = fileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
|
||||||
|
if (removed > 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Removed {amount} of invalid files", removed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await VerifyPlayerAnimationBones(boneIndices, previousData, objectKind).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
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 previousData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterData previousData, ObjectKind objectKind)
|
||||||
|
{
|
||||||
|
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 previousData.FileReplacements[objectKind].Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
previousData.FileReplacements[objectKind].Remove(file);
|
||||||
|
foreach (var gamePath in file.GamePaths)
|
||||||
|
{
|
||||||
|
_transientResourceManager.RemoveTransientResource(objectKind, 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, IntPtr charaPointer)
|
||||||
|
{
|
||||||
|
_transientResourceManager.PersistTransientResources(charaPointer, objectKind);
|
||||||
|
|
||||||
|
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
|
||||||
|
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
|
||||||
|
{
|
||||||
|
pathsToResolve.Add(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathsToResolve;
|
||||||
|
}
|
||||||
|
}
|
||||||
487
MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs
Normal file
487
MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly Func<IntPtr> _getAddress;
|
||||||
|
private readonly bool _isOwnedObject;
|
||||||
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private CancellationTokenSource? _clearCts = new();
|
||||||
|
private Task? _delayedZoningTask;
|
||||||
|
private bool _haltProcessing = false;
|
||||||
|
private bool _ignoreSendAfterRedraw = false;
|
||||||
|
private int _ptrNullCounter = 0;
|
||||||
|
private byte _classJob = 0;
|
||||||
|
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;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
_ignoreSendAfterRedraw = true;
|
||||||
|
await Task.Delay(500).ConfigureAwait(false);
|
||||||
|
_ignoreSendAfterRedraw = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
||||||
|
|
||||||
|
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum DrawCondition
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
DrawObjectZero,
|
||||||
|
RenderFlags,
|
||||||
|
ModelInSlotLoaded,
|
||||||
|
ModelFilesInSlotLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte RaceId { get; private set; }
|
||||||
|
public byte Gender { get; private set; }
|
||||||
|
public byte TribeId { get; private set; }
|
||||||
|
|
||||||
|
public IntPtr Address { get; private set; }
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public ObjectKind ObjectKind { get; }
|
||||||
|
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 (IsBeingDrawn()) 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 IntPtr CurrentAddress()
|
||||||
|
{
|
||||||
|
_dalamudUtil.EnsureIsOnFramework();
|
||||||
|
return _getAddress.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_ptrNullCounter = 0;
|
||||||
|
var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject;
|
||||||
|
DrawObjectAddress = drawObjAddr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_haltProcessing) return;
|
||||||
|
|
||||||
|
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||||
|
bool addrDiff = Address != prevAddr;
|
||||||
|
|
||||||
|
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
if (_clearCts != null)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{this}] Cancelling Clear Task", this);
|
||||||
|
_clearCts.CancelDispose();
|
||||||
|
_clearCts = null;
|
||||||
|
}
|
||||||
|
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 && !_ignoreSendAfterRedraw) // send the message out immediately and cancel out, no reason to continue if not self
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
|
Mediator.Publish(new CharacterChangedMessage(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)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
|
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||||
|
{
|
||||||
|
_clearCts?.CancelDispose();
|
||||||
|
_clearCts = new();
|
||||||
|
var token = _clearCts.Token;
|
||||||
|
_ = Task.Run(() => ClearAsync(token), token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClearAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{this}] Running Clear Task", this);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||||
|
Logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this);
|
||||||
|
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||||
|
_clearCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 unsafe IntPtr GetDrawObjUnsafe(nint curPtr)
|
||||||
|
{
|
||||||
|
return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->DrawObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsBeingDrawn()
|
||||||
|
{
|
||||||
|
var curPtr = _getAddress();
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr: {ptr}", this, curPtr.ToString("X"));
|
||||||
|
|
||||||
|
if (curPtr == IntPtr.Zero && _ptrNullCounter < 2)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, counter is {cnt}", this, _ptrNullCounter);
|
||||||
|
_ptrNullCounter++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curPtr == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, returning", this);
|
||||||
|
|
||||||
|
Address = IntPtr.Zero;
|
||||||
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
|
throw new ArgumentNullException($"CurPtr for {this} turned ZERO");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dalamudUtil.IsAnythingDrawing)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, Global draw block", this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawObj = GetDrawObjUnsafe(curPtr);
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, DrawObjPtr: {ptr}", this, drawObj.ToString("X"));
|
||||||
|
var isDrawn = IsBeingDrawnUnsafe(drawObj, curPtr);
|
||||||
|
Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, isDrawn);
|
||||||
|
return isDrawn != DrawCondition.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe DrawCondition IsBeingDrawnUnsafe(IntPtr drawObj, IntPtr curPtr)
|
||||||
|
{
|
||||||
|
var drawObjZero = drawObj == IntPtr.Zero;
|
||||||
|
if (drawObjZero) return DrawCondition.DrawObjectZero;
|
||||||
|
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->RenderFlags) != 0x0;
|
||||||
|
if (renderFlags) return DrawCondition.RenderFlags;
|
||||||
|
|
||||||
|
if (ObjectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
var modelInSlotLoaded = (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0);
|
||||||
|
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
|
||||||
|
var modelFilesInSlotLoaded = (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0);
|
||||||
|
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
|
||||||
|
return DrawCondition.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DrawCondition.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ZoneSwitchEnd()
|
||||||
|
{
|
||||||
|
if (!_isOwnedObject || _haltProcessing) return;
|
||||||
|
|
||||||
|
_clearCts?.Cancel();
|
||||||
|
_clearCts?.Dispose();
|
||||||
|
_clearCts = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_zoningCts?.CancelAfter(2500);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Zoning CTS cancel issue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ZoneSwitchStart()
|
||||||
|
{
|
||||||
|
if (!_isOwnedObject || _haltProcessing) 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
886
MareSynchronos/PlayerData/Handlers/PairHandler.cs
Normal file
886
MareSynchronos/PlayerData/Handlers/PairHandler.cs
Normal file
@@ -0,0 +1,886 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.FileCache;
|
||||||
|
using MareSynchronos.Interop.Ipc;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Pairs;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Events;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
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 MareConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly FileDownloadManager _downloadManager;
|
||||||
|
private readonly FileCacheManager _fileDbManager;
|
||||||
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
|
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||||
|
private readonly VisibilityService _visibilityService;
|
||||||
|
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 _deferred = Guid.Empty;
|
||||||
|
private Guid _penumbraCollection = Guid.Empty;
|
||||||
|
private bool _redrawOnNextApplication = false;
|
||||||
|
|
||||||
|
public PairHandler(ILogger<PairHandler> logger, Pair pair, PairAnalyzer pairAnalyzer,
|
||||||
|
GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||||
|
IpcManager ipcManager, FileDownloadManager transferManager,
|
||||||
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
|
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||||
|
FileCacheManager fileDbManager, MareMediator mediator,
|
||||||
|
PlayerPerformanceService playerPerformanceService,
|
||||||
|
MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
Pair = pair;
|
||||||
|
PairAnalyzer = pairAnalyzer;
|
||||||
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
_ipcManager = ipcManager;
|
||||||
|
_downloadManager = transferManager;
|
||||||
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_fileDbManager = fileDbManager;
|
||||||
|
_playerPerformanceService = playerPerformanceService;
|
||||||
|
_configService = configService;
|
||||||
|
_visibilityService = visibilityService;
|
||||||
|
|
||||||
|
_visibilityService.StartTracking(Pair.Ident);
|
||||||
|
|
||||||
|
Mediator.SubscribeKeyed<PlayerVisibilityMessage>(this, Pair.Ident, (msg) => UpdateVisibility(msg.IsVisible, msg.Invalidate));
|
||||||
|
|
||||||
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
|
_charaHandler?.Invalidate();
|
||||||
|
IsVisible = false;
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) =>
|
||||||
|
{
|
||||||
|
_penumbraCollection = Guid.Empty;
|
||||||
|
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, _ =>
|
||||||
|
{
|
||||||
|
if (_configService.Current.HoldCombatApplication)
|
||||||
|
{
|
||||||
|
_dataReceivedInDowntime = null;
|
||||||
|
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||||
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<RecalculatePerformanceMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.UID != null && !msg.UID.Equals(Pair.UserData.UID, StringComparison.Ordinal)) return;
|
||||||
|
Logger.LogDebug("Recalculating performance for {uid}", Pair.UserData.UID);
|
||||||
|
pair.ApplyLastReceivedData(forced: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long LastAppliedDataBytes { get; private set; }
|
||||||
|
public Pair Pair { get; private init; }
|
||||||
|
public PairAnalyzer PairAnalyzer { get; private init; }
|
||||||
|
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 (_configService.Current.HoldCombatApplication && _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))
|
||||||
|
{
|
||||||
|
if (_deferred != Guid.Empty)
|
||||||
|
{
|
||||||
|
_isVisible = false;
|
||||||
|
_visibilityService.StopTracking(Pair.Ident);
|
||||||
|
_visibilityService.StartTracking(Pair.Ident);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
||||||
|
// Ensure that this deferred application actually occurs by forcing visibiltiy to re-proc
|
||||||
|
// Set _deferred as a silencing flag to avoid spamming logs once per frame with failed applications
|
||||||
|
_isVisible = false;
|
||||||
|
_deferred = applicationBase;
|
||||||
|
_visibilityService.StopTracking(Pair.Ident);
|
||||||
|
_visibilityService.StartTracking(Pair.Ident);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_deferred = Guid.Empty;
|
||||||
|
|
||||||
|
SetUploading(isUploading: false);
|
||||||
|
|
||||||
|
if (Pair.IsDownloadBlocked)
|
||||||
|
{
|
||||||
|
var reasons = string.Join(", ", Pair.HoldDownloadReasons);
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||||
|
$"Not applying character data: {reasons}")));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons);
|
||||||
|
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;
|
||||||
|
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!disposing) return;
|
||||||
|
|
||||||
|
_visibilityService.StopTracking(Pair.Ident);
|
||||||
|
|
||||||
|
SetUploading(isUploading: false);
|
||||||
|
var name = PlayerName;
|
||||||
|
Logger.LogDebug("Disposing {name} ({user})", name, Pair);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Guid applicationId = Guid.NewGuid();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User")));
|
||||||
|
}
|
||||||
|
|
||||||
|
UndoApplicationAsync(applicationId).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
_applicationCancellationTokenSource?.Dispose();
|
||||||
|
_applicationCancellationTokenSource = null;
|
||||||
|
_downloadCancellationTokenSource?.Dispose();
|
||||||
|
_downloadCancellationTokenSource = null;
|
||||||
|
_charaHandler?.Dispose();
|
||||||
|
_charaHandler = null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error on disposal of {name}", name);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
PlayerName = null;
|
||||||
|
_cachedData = null;
|
||||||
|
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, null));
|
||||||
|
Logger.LogDebug("Disposing {name} complete", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UndoApplication(Guid applicationId = default)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () => {
|
||||||
|
await UndoApplicationAsync(applicationId).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UndoApplicationAsync(Guid applicationId = default)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"Undoing application of {Pair.UserPair}");
|
||||||
|
var name = PlayerName;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (applicationId == default)
|
||||||
|
applicationId = Guid.NewGuid();
|
||||||
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
||||||
|
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
||||||
|
|
||||||
|
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||||
|
if (_penumbraCollection != Guid.Empty)
|
||||||
|
{
|
||||||
|
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false);
|
||||||
|
_penumbraCollection = Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair);
|
||||||
|
if (!IsVisible)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||||
|
await _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
{
|
||||||
|
await RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Error on undoing application of {name}", 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.GetCompanion(ptr), isWatched: false).ConfigureAwait(false),
|
||||||
|
ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), isWatched: false).ConfigureAwait(false),
|
||||||
|
ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPet(ptr), isWatched: false).ConfigureAwait(false),
|
||||||
|
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
|
||||||
|
};
|
||||||
|
|
||||||
|
async Task processApplication(IEnumerable<PlayerChanges> changeList)
|
||||||
|
{
|
||||||
|
foreach (var change in changeList)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("[{applicationId}{ft}] Processing {change} for {handler}", applicationId, _dalamudUtil.IsOnFrameworkThread ? "*" : "", 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, allowImmediate: true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.PetNames:
|
||||||
|
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.Moodles:
|
||||||
|
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlayerChanges.ForcedRedraw:
|
||||||
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
if (_configService.Current.SerialApplication)
|
||||||
|
{
|
||||||
|
var serialChangeList = changes.Value.Where(p => p <= PlayerChanges.ForcedRedraw).OrderBy(p => (int)p);
|
||||||
|
var asyncChangeList = changes.Value.Where(p => p > PlayerChanges.ForcedRedraw).OrderBy(p => (int)p);
|
||||||
|
await _dalamudUtil.RunOnFrameworkThread(async () => await processApplication(serialChangeList).ConfigureAwait(false)).ConfigureAwait(false);
|
||||||
|
await Task.Run(async () => await processApplication(asyncChangeList).ConfigureAwait(false), CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ = processApplication(changes.Value.OrderBy(p => (int)p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
_ = Task.Run(async () => {
|
||||||
|
await 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)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync", applicationBase);
|
||||||
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||||
|
|
||||||
|
if (updateModdedPaths)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync > updateModdedPaths", applicationBase);
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1);
|
||||||
|
_downloadManager.ClearDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false), downloadToken);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Mediator.Publish(new HaltScanMessage(nameof(PlayerPerformanceService.ShrinkTextures)));
|
||||||
|
if (await _playerPerformanceService.ShrinkTextures(this, charaData, downloadToken).ConfigureAwait(false))
|
||||||
|
_ = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ResumeScanMessage(nameof(PlayerPerformanceService.ShrinkTextures)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool exceedsThreshold = !await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (exceedsThreshold)
|
||||||
|
Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1);
|
||||||
|
else
|
||||||
|
Pair.UnholdApplication("IndividualPerformanceThreshold");
|
||||||
|
|
||||||
|
if (exceedsThreshold)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] Not applying due to performance thresholds", applicationBase);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pair.IsApplicationBlocked)
|
||||||
|
{
|
||||||
|
var reasons = string.Join(", ", Pair.HoldApplicationReasons);
|
||||||
|
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||||
|
$"Not applying character data: {reasons}")));
|
||||||
|
Logger.LogTrace("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
ushort objIndex = ushort.MaxValue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_applicationId = Guid.NewGuid();
|
||||||
|
Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId);
|
||||||
|
|
||||||
|
if (_penumbraCollection == Guid.Empty)
|
||||||
|
{
|
||||||
|
if (objIndex == ushort.MaxValue)
|
||||||
|
objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
|
||||||
|
_penumbraCollection = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, Pair.UserData.UID).ConfigureAwait(false);
|
||||||
|
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (objIndex == ushort.MaxValue)
|
||||||
|
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;
|
||||||
|
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, 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;
|
||||||
|
Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, charaData));
|
||||||
|
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateVisibility(bool nowVisible, bool invalidate = false)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(PlayerName))
|
||||||
|
{
|
||||||
|
var pc = _dalamudUtil.FindPlayerByNameHash(Pair.Ident);
|
||||||
|
if (pc.ObjectId == 0) 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}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This was triggered by the character becoming handled by Mare, so unapply everything
|
||||||
|
// There seems to be a good chance that this races Mare and then crashes
|
||||||
|
if (!nowVisible && invalidate)
|
||||||
|
{
|
||||||
|
bool wasVisible = IsVisible;
|
||||||
|
IsVisible = false;
|
||||||
|
_charaHandler?.Invalidate();
|
||||||
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
|
_downloadCancellationTokenSource = null;
|
||||||
|
if (wasVisible)
|
||||||
|
Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible);
|
||||||
|
Logger.LogDebug("Invalidating {this}", this);
|
||||||
|
UndoApplication();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsVisible && nowVisible)
|
||||||
|
{
|
||||||
|
// This is deferred application attempt, avoid any log output
|
||||||
|
if (_deferred != Guid.Empty)
|
||||||
|
{
|
||||||
|
_isVisible = true;
|
||||||
|
_ = Task.Run(() =>
|
||||||
|
{
|
||||||
|
ApplyCharacterData(_deferred, _cachedData!, forceApplyCustomization: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
IsVisible = true;
|
||||||
|
Mediator.Publish(new PairHandlerVisibleMessage(this));
|
||||||
|
if (_cachedData != null)
|
||||||
|
{
|
||||||
|
Guid appData = Guid.NewGuid();
|
||||||
|
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 (IsVisible && !nowVisible)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
|
||||||
|
Mediator.Subscribe<HonorificReadyMessage>(this, msg =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return;
|
||||||
|
Logger.LogTrace("Reapplying Honorific data for {this}", this);
|
||||||
|
_ = Task.Run(async () => await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false), CancellationToken.None);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<PetNamesReadyMessage>(this, msg =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return;
|
||||||
|
Logger.LogTrace("Reapplying Pet Names data for {this}", this);
|
||||||
|
_ = Task.Run(async () => await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false), CancellationToken.None);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Pet Nicknames for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
|
||||||
|
Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||||
|
await _ipcManager.Moodles.RevertStatusAsync(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, preferSubst: true);
|
||||||
|
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 (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
75
MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs
Normal file
75
MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
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 OnlinePlayerManager : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
|
private readonly HashSet<PairHandler> _newVisiblePlayers = [];
|
||||||
|
private readonly PairManager _pairManager;
|
||||||
|
private CharacterData? _lastSentData;
|
||||||
|
|
||||||
|
public OnlinePlayerManager(ILogger<OnlinePlayerManager> logger, ApiController apiController, DalamudUtilService dalamudUtil,
|
||||||
|
PairManager pairManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_apiController = apiController;
|
||||||
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_pairManager = pairManager;
|
||||||
|
_fileTransferManager = fileTransferManager;
|
||||||
|
Mediator.Subscribe<PlayerChangedMessage>(this, (_) => PlayerManagerOnPlayerHasChanged());
|
||||||
|
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate());
|
||||||
|
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
var newData = msg.CharacterData;
|
||||||
|
if (_lastSentData == null || (!string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Pushing data for visible players");
|
||||||
|
_lastSentData = newData;
|
||||||
|
PushCharacterData(_pairManager.GetVisibleUsers());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Not sending data for {hash}", newData.DataHash.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (msg) => _newVisiblePlayers.Add(msg.Player));
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushCharacterData(_pairManager.GetVisibleUsers()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FrameworkOnUpdate()
|
||||||
|
{
|
||||||
|
if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return;
|
||||||
|
|
||||||
|
if (!_newVisiblePlayers.Any()) return;
|
||||||
|
var newVisiblePlayers = _newVisiblePlayers.ToList();
|
||||||
|
_newVisiblePlayers.Clear();
|
||||||
|
Logger.LogTrace("Has new visible players, pushing character data");
|
||||||
|
PushCharacterData(newVisiblePlayers.Select(c => c.Pair.UserData).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayerManagerOnPlayerHasChanged()
|
||||||
|
{
|
||||||
|
PushCharacterData(_pairManager.GetVisibleUsers());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PushCharacterData(List<UserData> visiblePlayers)
|
||||||
|
{
|
||||||
|
if (visiblePlayers.Any() && _lastSentData != null)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var dataToSend = await _fileTransferManager.UploadFiles(_lastSentData.DeepClone(), visiblePlayers).ConfigureAwait(false);
|
||||||
|
await _apiController.PushCharacterData(dataToSend, visiblePlayers).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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 ShowPetNicknamesWarning { get; set; } = false;
|
||||||
|
public bool ShownMoodlesWarning { get; set; } = false;
|
||||||
|
}
|
||||||
385
MareSynchronos/PlayerData/Pairs/Pair.cs
Normal file
385
MareSynchronos/PlayerData/Pairs/Pair.cs
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
using Dalamud.Game.Gui.ContextMenu;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using MareSynchronos.API.Data;
|
||||||
|
using MareSynchronos.API.Data.Comparer;
|
||||||
|
using MareSynchronos.API.Data.Enum;
|
||||||
|
using MareSynchronos.API.Data.Extensions;
|
||||||
|
using MareSynchronos.API.Dto.Group;
|
||||||
|
using MareSynchronos.API.Dto.User;
|
||||||
|
using MareSynchronos.MareConfiguration;
|
||||||
|
using MareSynchronos.PlayerData.Factories;
|
||||||
|
using MareSynchronos.PlayerData.Handlers;
|
||||||
|
using MareSynchronos.Services;
|
||||||
|
using MareSynchronos.Services.Mediator;
|
||||||
|
using MareSynchronos.Services.ServerConfiguration;
|
||||||
|
using MareSynchronos.Utils;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace MareSynchronos.PlayerData.Pairs;
|
||||||
|
|
||||||
|
public class Pair : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||||
|
private readonly SemaphoreSlim _creationSemaphore = new(1);
|
||||||
|
private readonly ILogger<Pair> _logger;
|
||||||
|
private readonly MareConfigService _mareConfig;
|
||||||
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private CancellationTokenSource _applicationCts = new();
|
||||||
|
private OnlineUserIdentDto? _onlineUserIdentDto = null;
|
||||||
|
|
||||||
|
public Pair(ILogger<Pair> logger, UserData userData, PairHandlerFactory cachedPlayerFactory,
|
||||||
|
MareMediator mediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager)
|
||||||
|
: base(logger, mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_cachedPlayerFactory = cachedPlayerFactory;
|
||||||
|
_mareConfig = mareConfig;
|
||||||
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
|
||||||
|
UserData = userData;
|
||||||
|
|
||||||
|
Mediator.SubscribeKeyed<HoldPairApplicationMessage>(this, UserData.UID, (msg) => HoldApplication(msg.Source));
|
||||||
|
Mediator.SubscribeKeyed<UnholdPairApplicationMessage>(this, UserData.UID, (msg) => UnholdApplication(msg.Source));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<GroupFullInfoDto, GroupPairFullInfoDto> GroupPair { get; set; } = new(GroupDtoComparer.Instance);
|
||||||
|
public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null;
|
||||||
|
public bool IsOnline => CachedPlayer != null;
|
||||||
|
|
||||||
|
public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused()
|
||||||
|
: GroupPair.All(p => p.Key.GroupUserPermissions.IsPaused() || p.Value.GroupUserPermissions.IsPaused());
|
||||||
|
|
||||||
|
// Download locks apply earlier in the process than Application locks
|
||||||
|
private ConcurrentDictionary<string, int> HoldDownloadLocks { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
private ConcurrentDictionary<string, int> HoldApplicationLocks { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public bool IsDownloadBlocked => HoldDownloadLocks.Any(f => f.Value > 0);
|
||||||
|
public bool IsApplicationBlocked => HoldApplicationLocks.Any(f => f.Value > 0) || IsDownloadBlocked;
|
||||||
|
|
||||||
|
public IEnumerable<string> HoldDownloadReasons => HoldDownloadLocks.Keys;
|
||||||
|
public IEnumerable<string> HoldApplicationReasons => Enumerable.Concat(HoldDownloadLocks.Keys, HoldApplicationLocks.Keys);
|
||||||
|
|
||||||
|
public bool IsVisible => CachedPlayer?.IsVisible ?? false;
|
||||||
|
public CharacterData? LastReceivedCharacterData { get; set; }
|
||||||
|
public string? PlayerName => GetPlayerName();
|
||||||
|
public uint PlayerCharacterId => GetPlayerCharacterId();
|
||||||
|
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 PairAnalyzer? PairAnalyzer => CachedPlayer?.PairAnalyzer;
|
||||||
|
|
||||||
|
public UserData UserData { get; init; }
|
||||||
|
|
||||||
|
public UserPairDto? 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;
|
||||||
|
|
||||||
|
void Add(string name, Action<IMenuItemClickedArgs>? action)
|
||||||
|
{
|
||||||
|
args.AddMenuItem(new MenuItem()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
OnClicked = action,
|
||||||
|
PrefixColor = 526,
|
||||||
|
PrefixChar = 'S'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isBlocked = IsApplicationBlocked;
|
||||||
|
bool isBlacklisted = _serverConfigurationManager.IsUidBlacklisted(UserData.UID);
|
||||||
|
bool isWhitelisted = _serverConfigurationManager.IsUidWhitelisted(UserData.UID);
|
||||||
|
|
||||||
|
Add("Open Profile", _ => Mediator.Publish(new ProfileOpenStandaloneMessage(this)));
|
||||||
|
|
||||||
|
if (!isBlocked && !isBlacklisted)
|
||||||
|
Add("Always Block Modded Appearance", _ => {
|
||||||
|
_serverConfigurationManager.AddBlacklistUid(UserData.UID);
|
||||||
|
HoldApplication("Blacklist", maxValue: 1);
|
||||||
|
ApplyLastReceivedData(forced: true);
|
||||||
|
});
|
||||||
|
else if (isBlocked && !isWhitelisted)
|
||||||
|
Add("Always Allow Modded Appearance", _ => {
|
||||||
|
_serverConfigurationManager.AddWhitelistUid(UserData.UID);
|
||||||
|
UnholdApplication("Blacklist", skipApplication: true);
|
||||||
|
ApplyLastReceivedData(forced: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isWhitelisted)
|
||||||
|
Add("Remove from Whitelist", _ => {
|
||||||
|
_serverConfigurationManager.RemoveWhitelistUid(UserData.UID);
|
||||||
|
ApplyLastReceivedData(forced: true);
|
||||||
|
});
|
||||||
|
else if (isBlacklisted)
|
||||||
|
Add("Remove from Blacklist", _ => {
|
||||||
|
_serverConfigurationManager.RemoveBlacklistUid(UserData.UID);
|
||||||
|
UnholdApplication("Blacklist", skipApplication: true);
|
||||||
|
ApplyLastReceivedData(forced: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
Add("Reapply last data", _ => ApplyLastReceivedData(forced: true));
|
||||||
|
|
||||||
|
if (UserPair != null)
|
||||||
|
{
|
||||||
|
Add("Change Permissions", _ => Mediator.Publish(new OpenPermissionWindow(this)));
|
||||||
|
Add("Cycle pause state", _ => Mediator.Publish(new CyclePauseMessage(UserData)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (IsDownloadBlocked) return;
|
||||||
|
|
||||||
|
if (_serverConfigurationManager.IsUidBlacklisted(UserData.UID))
|
||||||
|
HoldApplication("Blacklist", maxValue: 1);
|
||||||
|
|
||||||
|
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? GetPlayerName()
|
||||||
|
{
|
||||||
|
if (CachedPlayer != null && CachedPlayer.PlayerName != null)
|
||||||
|
return CachedPlayer.PlayerName;
|
||||||
|
else
|
||||||
|
return _serverConfigurationManager.GetNameForUid(UserData.UID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint GetPlayerCharacterId()
|
||||||
|
{
|
||||||
|
if (CachedPlayer != null)
|
||||||
|
return CachedPlayer.PlayerCharacterId;
|
||||||
|
return uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetNoteOrName()
|
||||||
|
{
|
||||||
|
string? note = GetNote();
|
||||||
|
if (_mareConfig.Current.ShowCharacterNames || IsVisible)
|
||||||
|
return note ?? GetPlayerName();
|
||||||
|
else
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetPairSortKey()
|
||||||
|
{
|
||||||
|
string? noteOrName = GetNoteOrName();
|
||||||
|
|
||||||
|
if (noteOrName != null)
|
||||||
|
return $"0{noteOrName}";
|
||||||
|
else
|
||||||
|
return $"9{UserData.AliasOrUID}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetPlayerNameHash()
|
||||||
|
{
|
||||||
|
return CachedPlayer?.PlayerNameHash ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAnyConnection()
|
||||||
|
{
|
||||||
|
return UserPair != null || GroupPair.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_applicationCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
_applicationCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetNote(string note)
|
||||||
|
{
|
||||||
|
_serverConfigurationManager.SetNoteForUid(UserData.UID, note);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetIsUploading()
|
||||||
|
{
|
||||||
|
CachedPlayer?.SetUploading();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HoldApplication(string source, int maxValue = int.MaxValue)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Holding {UserData.UID} for reason: {source}");
|
||||||
|
bool wasHeld = IsApplicationBlocked;
|
||||||
|
HoldApplicationLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1));
|
||||||
|
if (!wasHeld)
|
||||||
|
CachedPlayer?.UndoApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnholdApplication(string source, bool skipApplication = false)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}");
|
||||||
|
bool wasHeld = IsApplicationBlocked;
|
||||||
|
HoldApplicationLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1));
|
||||||
|
HoldApplicationLocks.TryRemove(new(source, 0));
|
||||||
|
if (!skipApplication && wasHeld && !IsApplicationBlocked)
|
||||||
|
ApplyLastReceivedData(forced: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HoldDownloads(string source, int maxValue = int.MaxValue)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Holding {UserData.UID} for reason: {source}");
|
||||||
|
bool wasHeld = IsApplicationBlocked;
|
||||||
|
HoldDownloadLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1));
|
||||||
|
if (!wasHeld)
|
||||||
|
CachedPlayer?.UndoApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnholdDownloads(string source, bool skipApplication = false)
|
||||||
|
{
|
||||||
|
_logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}");
|
||||||
|
bool wasHeld = IsApplicationBlocked;
|
||||||
|
HoldDownloadLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1));
|
||||||
|
HoldDownloadLocks.TryRemove(new(source, 0));
|
||||||
|
if (!skipApplication && wasHeld && !IsApplicationBlocked)
|
||||||
|
ApplyLastReceivedData(forced: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Removing not synced files");
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Nothing to remove");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ActiveGroupPairs = GroupPair.Where(p => !p.Value.GroupUserPermissions.IsPaused() && !p.Key.GroupUserPermissions.IsPaused()).ToList();
|
||||||
|
|
||||||
|
bool disableIndividualAnimations = UserPair != null && (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations());
|
||||||
|
bool disableIndividualVFX = UserPair != null && (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX());
|
||||||
|
bool disableGroupAnimations = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations());
|
||||||
|
|
||||||
|
bool disableAnimations = (UserPair != null && disableIndividualAnimations) || (UserPair == null && disableGroupAnimations);
|
||||||
|
|
||||||
|
bool disableIndividualSounds = UserPair != null && (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds());
|
||||||
|
bool disableGroupSounds = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableSounds() || pair.Key.GroupPermissions.IsDisableSounds() || pair.Key.GroupUserPermissions.IsDisableSounds());
|
||||||
|
bool disableGroupVFX = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableVFX() || pair.Key.GroupPermissions.IsDisableVFX() || pair.Key.GroupUserPermissions.IsDisableVFX());
|
||||||
|
|
||||||
|
bool disableSounds = (UserPair != null && disableIndividualSounds) || (UserPair == null && disableGroupSounds);
|
||||||
|
bool disableVFX = (UserPair != null && disableIndividualVFX) || (UserPair == null && disableGroupVFX);
|
||||||
|
|
||||||
|
_logger.LogTrace("Disable: Sounds: {disableSounds}, Anims: {disableAnimations}, VFX: {disableVFX}",
|
||||||
|
disableSounds, disableAnimations, disableVFX);
|
||||||
|
|
||||||
|
if (disableAnimations || disableSounds || disableVFX)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}",
|
||||||
|
disableAnimations, disableSounds, disableVFX);
|
||||||
|
foreach (var objectKind in data.FileReplacements.Select(k => k.Key))
|
||||||
|
{
|
||||||
|
if (disableSounds)
|
||||||
|
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||||
|
.Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
if (disableAnimations)
|
||||||
|
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||||
|
.Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
if (disableVFX)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
420
MareSynchronos/PlayerData/Pairs/PairManager.cs
Normal file
420
MareSynchronos/PlayerData/Pairs/PairManager.cs
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
_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 void AddGroup(GroupFullInfoDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group] = dto;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddGroupPair(GroupPairFullInfoDto dto, bool isInitialLoad = false)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.ContainsKey(dto.User))
|
||||||
|
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
||||||
|
|
||||||
|
var group = _allGroups[dto.Group];
|
||||||
|
_allClientPairs[dto.User].GroupPair[group] = dto;
|
||||||
|
RecreateLazy();
|
||||||
|
|
||||||
|
if (!isInitialLoad)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ApplyDefaultGroupPermissionsMessage(dto));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair? GetPairByUID(string uid)
|
||||||
|
{
|
||||||
|
var existingPair = _allClientPairs.FirstOrDefault(f => uid.Equals(f.Key.UID, StringComparison.Ordinal));
|
||||||
|
if (!Equals(existingPair, default(KeyValuePair<UserData, Pair>)))
|
||||||
|
{
|
||||||
|
return existingPair.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddUserPair(UserPairDto dto, bool addToLastAddedUser = true)
|
||||||
|
{
|
||||||
|
if (!_allClientPairs.ContainsKey(dto.User))
|
||||||
|
{
|
||||||
|
_allClientPairs[dto.User] = _pairFactory.Create(dto.User);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
addToLastAddedUser = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_allClientPairs[dto.User].UserPair = dto;
|
||||||
|
if (addToLastAddedUser)
|
||||||
|
LastAddedUser = _allClientPairs[dto.User];
|
||||||
|
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||||
|
RecreateLazy();
|
||||||
|
|
||||||
|
if (addToLastAddedUser)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new ApplyDefaultPairPermissionsMessage(dto));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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).ToList();
|
||||||
|
|
||||||
|
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.UserPair != null
|
||||||
|
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs)
|
||||||
|
&& (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote())
|
||||||
|
|| !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs))
|
||||||
|
{
|
||||||
|
string? note = pair.GetNoteOrName();
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).Where(grpPair => GroupDataComparer.Instance.Equals(grpPair.Group, data)).ToList())
|
||||||
|
{
|
||||||
|
_allClientPairs[item.Key].GroupPair.Remove(grpPair);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_allClientPairs[item.Key].HasAnyConnection() && _allClientPairs.TryRemove(item.Key, out var pair))
|
||||||
|
{
|
||||||
|
pair.MarkOffline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveGroupPair(GroupPairDto dto)
|
||||||
|
{
|
||||||
|
if (_allClientPairs.TryGetValue(dto.User, out var pair))
|
||||||
|
{
|
||||||
|
var group = _allGroups[dto.Group];
|
||||||
|
pair.GroupPair.Remove(group);
|
||||||
|
|
||||||
|
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 = null;
|
||||||
|
|
||||||
|
if (!pair.HasAnyConnection())
|
||||||
|
{
|
||||||
|
pair.MarkOffline();
|
||||||
|
_allClientPairs.TryRemove(dto.User, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGroupInfo(GroupInfoDto dto)
|
||||||
|
{
|
||||||
|
if (!_allGroups.TryGetValue(dto.Group, out var groupInfo))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupInfo.Group = dto.Group;
|
||||||
|
groupInfo.Owner = dto.Owner;
|
||||||
|
groupInfo.GroupPermissions = dto.GroupPermissions;
|
||||||
|
groupInfo.IsTemporary = dto.IsTemporary;
|
||||||
|
groupInfo.ExpiresAt = dto.ExpiresAt;
|
||||||
|
|
||||||
|
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()
|
||||||
|
|| pair.UserPair.OtherPermissions.IsPaired() != dto.Permissions.IsPaired())
|
||||||
|
{
|
||||||
|
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 == null) throw new InvalidOperationException("No direct pair for " + dto);
|
||||||
|
|
||||||
|
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused()
|
||||||
|
|| pair.UserPair.OwnPermissions.IsPaired() != dto.Permissions.IsPaired())
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var group = _allGroups[dto.Group];
|
||||||
|
_allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto)
|
||||||
|
{
|
||||||
|
var group = _allGroups[dto.Group];
|
||||||
|
var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions;
|
||||||
|
_allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions;
|
||||||
|
if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations()
|
||||||
|
|| prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds()
|
||||||
|
|| prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX())
|
||||||
|
{
|
||||||
|
_allClientPairs[dto.User].ApplyLastReceivedData();
|
||||||
|
}
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetGroupPermissions(GroupPermissionDto dto)
|
||||||
|
{
|
||||||
|
var prevPermissions = _allGroups[dto.Group].GroupPermissions;
|
||||||
|
_allGroups[dto.Group].GroupPermissions = dto.Permissions;
|
||||||
|
if (prevPermissions.IsDisableAnimations() != dto.Permissions.IsDisableAnimations()
|
||||||
|
|| prevPermissions.IsDisableSounds() != dto.Permissions.IsDisableSounds()
|
||||||
|
|| prevPermissions.IsDisableVFX() != dto.Permissions.IsDisableVFX())
|
||||||
|
{
|
||||||
|
RecreateLazy();
|
||||||
|
var group = _allGroups[dto.Group];
|
||||||
|
GroupPairs[group].ForEach(p => p.ApplyLastReceivedData());
|
||||||
|
}
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetGroupStatusInfo(GroupPairUserInfoDto dto)
|
||||||
|
{
|
||||||
|
_allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo;
|
||||||
|
RecreateLazy();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetGroupUserPermissions(GroupPairUserPermissionDto dto)
|
||||||
|
{
|
||||||
|
var prevPermissions = _allGroups[dto.Group].GroupUserPermissions;
|
||||||
|
_allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions;
|
||||||
|
if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations()
|
||||||
|
|| prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds()
|
||||||
|
|| prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX())
|
||||||
|
{
|
||||||
|
RecreateLazy();
|
||||||
|
var group = _allGroups[dto.Group];
|
||||||
|
GroupPairs[group].ForEach(p => p.ApplyLastReceivedData());
|
||||||
|
}
|
||||||
|
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.UserPair != null).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 = new();
|
||||||
|
foreach (var group in _allGroups)
|
||||||
|
{
|
||||||
|
outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user