Compare commits
86 Commits
v1.4.6
...
4821611cbe
| Author | SHA1 | Date | |
|---|---|---|---|
| 4821611cbe | |||
| c357873604 | |||
| 67bec11f10 | |||
| 35efdd4628 | |||
| 271a6ae650 | |||
| 003bd5c695 | |||
| e1f84a9b10 | |||
| 9745abea0c | |||
| 1e418ab86f | |||
| 1c820b7f53 | |||
| 2cc260170e | |||
| de86084dbc | |||
| f56b968768 | |||
| edab5c7a6d | |||
| 82cbf4c281 | |||
| 00ae81751b | |||
| 89384702b4 | |||
| 54316313dc | |||
| 4059b363a3 | |||
| 0220e5d756 | |||
| 2315f10d91 | |||
| 3283e51381 | |||
| 7e960371a3 | |||
| f2a2daf39d | |||
| 7d87f1c4fe | |||
| fe84fd558e | |||
| 624ad20404 | |||
| 54ff88d6d4 | |||
| c955f30422 | |||
| 7a1bd1babc | |||
| d0be75e79d | |||
| e0ead86616 | |||
| b66005daea | |||
| 0fe66d2c3c | |||
| 169168cea9 | |||
| f6d3794d87 | |||
| 763f5a3f5d | |||
| 8a18f7caaa | |||
| 5f7bfb5890 | |||
| 3be4e73c27 | |||
| 667950c98e | |||
| 3e91177833 | |||
| 51f18e46a0 | |||
| f66316161b | |||
| 679b8f0f5e | |||
| 0e470fcdce | |||
| abbbf95002 | |||
| fbbbeebade | |||
| 7c9b90c767 | |||
| b81894b859 | |||
| 655c903cb5 | |||
| 8c4afaac17 | |||
| c6a3780753 | |||
| d9f6704316 | |||
| 011490368b | |||
| 8ed10a536b | |||
| 6051e49307 | |||
| 55120e6572 | |||
| 7542d48983 | |||
| 7b36763359 | |||
| eecedd9f97 | |||
| 1003a88cad | |||
| 299fd59cbb | |||
| 74bcb91b65 | |||
| 2c64aaa251 | |||
| 607d2c7241 | |||
| b2a0f3a77c | |||
| d26c4701fa | |||
| 7f317a2b18 | |||
| 38149059c3 | |||
| 67175419a9 | |||
| d3fdcdf43d | |||
| f4ea460644 | |||
| d5735d8dcc | |||
| 80b48ac3ad | |||
| cddd29a986 | |||
| 799fdb67cc | |||
| 69fa0fecbd | |||
| fd5f970a8b | |||
| fee2459e73 | |||
| 63cad62c89 | |||
| dca5de4085 | |||
| 8edc3c70d3 | |||
| 3c33acf6d7 | |||
| c8ba8c1cd0 | |||
| 94e4828aeb |
+237
-152
@@ -1,167 +1,252 @@
|
|||||||
[*]
|
# ##############################################################
|
||||||
indent_style=space
|
# #
|
||||||
tab_width=4
|
# # .editorconfig – Hellion Forge / Hellion Media
|
||||||
indent_size=4
|
# #
|
||||||
trim_trailing_whitespace=true
|
# # Überarbeitet: Mai 2026
|
||||||
insert_final_newline=false
|
# #
|
||||||
|
# # Strategie:
|
||||||
|
# # - Standard-.NET-Conventions (private Fields = _camelCase)
|
||||||
|
# # - CSharpier übernimmt die meiste Formatierung
|
||||||
|
# # - Hier: Naming, IDE-Hints, Backup-Format-Regeln
|
||||||
|
# #
|
||||||
|
# # ##############################################################
|
||||||
|
|
||||||
# JetBrains Rider custom properties for code formatting styles
|
root = true
|
||||||
resharper_csharp_brace_style=next_line
|
|
||||||
# Allman für standard Tooling (VS Code)
|
|
||||||
|
# =====================================================
|
||||||
|
# Defaults (alle Files)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 4
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Markdown: Trailing Spaces erlaubt (2 Spaces = <br>)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# JSON / YAML / Web-Configs: 2-Space-Indent
|
||||||
|
# Konsistent mit yamllint und Prettier-Override
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
[*.{yaml,yml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{json,jsonc,har,jsb2,jsb3,postman_collection,postman_environment}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[{.babelrc,.eslintrc,.prettierrc,.markdownlintrc,.stylelintrc,bowerrc}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# .NET / XAML / Razor / Resources: 4-Space-Indent
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
[*.{cs,csx,vb,fs,fsi,fsx}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{xml,xsd,xaml,axaml,paml,resx,resw,nuspec,config}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{cshtml,razor,aspx,ascx,asax,master,axaml}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################
|
||||||
|
# #
|
||||||
|
# # C# Sektion: Style, Naming, Format
|
||||||
|
# #
|
||||||
|
# ##############################################################
|
||||||
|
|
||||||
|
[*.{cs,csx}]
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# C# Style – var-Präferenz
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
csharp_style_var_for_built_in_types = true:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||||
|
csharp_style_var_elsewhere = true:suggestion
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# C# Style – Sonstiges
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# UTF-8 String Literals (C# 11+)
|
||||||
|
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||||
|
|
||||||
|
# Reihenfolge der Access-Modifier (Microsoft-Empfehlung)
|
||||||
|
csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
|
||||||
|
|
||||||
|
# Initializer: nicht alles auf eine Zeile
|
||||||
|
csharp_new_line_before_members_in_object_initializers = false
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# C# Format – Braces (Backup, falls CSharpier nicht läuft)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Allman Style: Klammern auf neue Zeile
|
||||||
csharp_new_line_before_open_brace = all
|
csharp_new_line_before_open_brace = all
|
||||||
csharp_new_line_before_else = true
|
csharp_new_line_before_else = true
|
||||||
csharp_new_line_before_catch = true
|
csharp_new_line_before_catch = true
|
||||||
csharp_new_line_before_finally = true
|
csharp_new_line_before_finally = true
|
||||||
|
|
||||||
# Switch-Einrückung
|
|
||||||
|
# =====================================================
|
||||||
|
# C# Format – Switch-Einrückung
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
csharp_indent_case_contents = true
|
csharp_indent_case_contents = true
|
||||||
csharp_indent_switch_labels = true
|
csharp_indent_switch_labels = true
|
||||||
|
|
||||||
resharper_csharp_braces_for_foreach=not_required
|
|
||||||
resharper_csharp_braces_for_for=not_required
|
|
||||||
resharper_csharp_braces_for_while=not_required
|
|
||||||
charset=utf-8
|
|
||||||
end_of_line=lf
|
|
||||||
|
|
||||||
# Microsoft .NET properties
|
# =====================================================
|
||||||
csharp_new_line_before_members_in_object_initializers=false
|
# .NET Style – Qualification (kein "this." nötig)
|
||||||
csharp_preferred_modifier_order=public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
|
# =====================================================
|
||||||
csharp_style_prefer_utf8_string_literals=true:suggestion
|
|
||||||
csharp_style_var_elsewhere=true:suggestion
|
|
||||||
csharp_style_var_for_built_in_types=true:suggestion
|
|
||||||
csharp_style_var_when_type_is_apparent=true:suggestion
|
|
||||||
dotnet_naming_rule.private_constants_rule.import_to_resharper=True
|
|
||||||
dotnet_naming_rule.private_constants_rule.resharper_description=Constant fields (private)
|
|
||||||
dotnet_naming_rule.private_constants_rule.resharper_guid=236f7aa5-7b06-43ca-bf2a-9b31bfcff09a
|
|
||||||
dotnet_naming_rule.private_constants_rule.severity=warning
|
|
||||||
dotnet_naming_rule.private_constants_rule.style=upper_camel_case_style
|
|
||||||
dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule.import_to_resharper=True
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule.resharper_description=Instance fields (private)
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule.resharper_guid=4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule.severity=warning
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule.style=upper_camel_case_style
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule_1.import_to_resharper=True
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule_1.resharper_description=Instance fields (private)
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule_1.resharper_guid=4a98fdf6-7d98-4f5a-afeb-ea44ad98c70c
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule_1.severity=warning
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule_1.style=upper_camel_case_style
|
|
||||||
dotnet_naming_rule.private_instance_fields_rule_1.symbols=private_instance_fields_symbols_1
|
|
||||||
dotnet_naming_rule.private_static_fields_rule.import_to_resharper=True
|
|
||||||
dotnet_naming_rule.private_static_fields_rule.resharper_description=Static fields (private)
|
|
||||||
dotnet_naming_rule.private_static_fields_rule.resharper_guid=f9fce829-e6f4-4cb2-80f1-5497c44f51df
|
|
||||||
dotnet_naming_rule.private_static_fields_rule.severity=warning
|
|
||||||
dotnet_naming_rule.private_static_fields_rule.style=upper_camel_case_style
|
|
||||||
dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols
|
|
||||||
dotnet_naming_rule.private_static_readonly_rule.import_to_resharper=True
|
|
||||||
dotnet_naming_rule.private_static_readonly_rule.resharper_description=Static readonly fields (private)
|
|
||||||
dotnet_naming_rule.private_static_readonly_rule.resharper_guid=15b5b1f1-457c-4ca6-b278-5615aedc07d3
|
|
||||||
dotnet_naming_rule.private_static_readonly_rule.severity=warning
|
|
||||||
dotnet_naming_rule.private_static_readonly_rule.style=upper_camel_case_style
|
|
||||||
dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper=True
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule.resharper_description=Unity serialized field
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule.resharper_guid=5f0fdb63-c892-4d2c-9324-15c80b22a7ef
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule.severity=warning
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule.style=lower_camel_case_style_1
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule.symbols=unity_serialized_field_symbols
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule_1.import_to_resharper=True
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule_1.resharper_description=Unity serialized field
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule_1.resharper_guid=5f0fdb63-c892-4d2c-9324-15c80b22a7ef
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule_1.severity=warning
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule_1.style=lower_camel_case_style_1
|
|
||||||
dotnet_naming_rule.unity_serialized_field_rule_1.symbols=unity_serialized_field_symbols_1
|
|
||||||
dotnet_naming_style.lower_camel_case_style.capitalization=camel_case
|
|
||||||
dotnet_naming_style.lower_camel_case_style.required_prefix=_
|
|
||||||
dotnet_naming_style.lower_camel_case_style_1.capitalization=camel_case
|
|
||||||
dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case
|
|
||||||
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private
|
|
||||||
dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field
|
|
||||||
dotnet_naming_symbols.private_constants_symbols.required_modifiers=const
|
|
||||||
dotnet_naming_symbols.private_constants_symbols.resharper_applicable_kinds=constant_field
|
|
||||||
dotnet_naming_symbols.private_constants_symbols.resharper_required_modifiers=any
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols.resharper_applicable_kinds=field,readonly_field
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols.resharper_required_modifiers=instance
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols_1.applicable_accessibilities=private
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols_1.applicable_kinds=field
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols_1.resharper_applicable_kinds=field,readonly_field
|
|
||||||
dotnet_naming_symbols.private_instance_fields_symbols_1.resharper_required_modifiers=instance
|
|
||||||
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private
|
|
||||||
dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field
|
|
||||||
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static
|
|
||||||
dotnet_naming_symbols.private_static_fields_symbols.resharper_applicable_kinds=field
|
|
||||||
dotnet_naming_symbols.private_static_fields_symbols.resharper_required_modifiers=static
|
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private
|
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field
|
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=readonly,static
|
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.resharper_applicable_kinds=readonly_field
|
|
||||||
dotnet_naming_symbols.private_static_readonly_symbols.resharper_required_modifiers=static
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities=*
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds=
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds=unity_serialised_field
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers=instance
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilities=*
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds=
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds=unity_serialised_field
|
|
||||||
dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers=instance
|
|
||||||
dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none
|
|
||||||
dotnet_style_parentheses_in_other_binary_operators=always_for_clarity:none
|
|
||||||
dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none
|
|
||||||
dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion
|
|
||||||
dotnet_style_predefined_type_for_member_access=true:suggestion
|
|
||||||
dotnet_style_qualification_for_event=false:suggestion
|
|
||||||
dotnet_style_qualification_for_field=false:suggestion
|
|
||||||
dotnet_style_qualification_for_method=false:suggestion
|
|
||||||
dotnet_style_qualification_for_property=false:suggestion
|
|
||||||
dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion
|
|
||||||
|
|
||||||
# ReSharper properties
|
dotnet_style_qualification_for_field = false:suggestion
|
||||||
resharper_autodetect_indent_settings=true
|
dotnet_style_qualification_for_property = false:suggestion
|
||||||
resharper_cpp_insert_final_newline=true
|
dotnet_style_qualification_for_method = false:suggestion
|
||||||
resharper_csharp_insert_final_newline=false
|
dotnet_style_qualification_for_event = false:suggestion
|
||||||
resharper_formatter_off_tag=@formatter:off
|
|
||||||
resharper_formatter_on_tag=@formatter:on
|
|
||||||
resharper_formatter_tags_enabled=true
|
|
||||||
resharper_fsharp_insert_final_newline=false
|
|
||||||
resharper_html_insert_final_newline=false
|
|
||||||
resharper_resx_insert_final_newline=false
|
|
||||||
resharper_shaderlab_insert_final_newline=false
|
|
||||||
resharper_t4_insert_final_newline=false
|
|
||||||
resharper_use_indent_from_vs=false
|
|
||||||
resharper_vb_insert_final_newline=false
|
|
||||||
resharper_xmldoc_insert_final_newline=false
|
|
||||||
resharper_xml_insert_final_newline=false
|
|
||||||
|
|
||||||
# ReSharper inspection severities
|
|
||||||
resharper_arrange_redundant_parentheses_highlighting=hint
|
|
||||||
resharper_arrange_this_qualifier_highlighting=hint
|
|
||||||
resharper_arrange_type_member_modifiers_highlighting=hint
|
|
||||||
resharper_arrange_type_modifiers_highlighting=hint
|
|
||||||
resharper_built_in_type_reference_style_for_member_access_highlighting=hint
|
|
||||||
resharper_built_in_type_reference_style_highlighting=hint
|
|
||||||
resharper_razor_assembly_not_resolved_highlighting=warning
|
|
||||||
resharper_redundant_base_qualifier_highlighting=warning
|
|
||||||
resharper_suggest_var_or_type_built_in_types_highlighting=hint
|
|
||||||
resharper_suggest_var_or_type_elsewhere_highlighting=hint
|
|
||||||
resharper_suggest_var_or_type_simple_types_highlighting=hint
|
|
||||||
resharper_web_config_module_not_resolved_highlighting=warning
|
|
||||||
resharper_web_config_type_not_resolved_highlighting=warning
|
|
||||||
resharper_web_config_wrong_module_highlighting=warning
|
|
||||||
|
|
||||||
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.prettierrc.json,.markdownlint.json,.yamllint.json,.stylelintrc,bowerrc,jest.config}]
|
# =====================================================
|
||||||
indent_style=space
|
# .NET Style – Predefined Types (int statt Int32 etc.)
|
||||||
indent_size=2
|
# =====================================================
|
||||||
|
|
||||||
[{*.yaml,*.yml}]
|
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||||
indent_style=space
|
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||||
indent_size=2
|
|
||||||
|
|
||||||
[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}]
|
|
||||||
indent_style=space
|
# =====================================================
|
||||||
indent_size=4
|
# .NET Style – Parentheses
|
||||||
tab_width=4
|
# =====================================================
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace=false
|
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
|
||||||
|
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
|
||||||
|
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# .NET Style – Accessibility-Modifier erzwingen
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################
|
||||||
|
# #
|
||||||
|
# # Naming Conventions (.NET-Standard)
|
||||||
|
# #
|
||||||
|
# # Private Instance Fields: _camelCase
|
||||||
|
# # Private Static Fields: _camelCase
|
||||||
|
# # Private Constants: PascalCase
|
||||||
|
# # Private Static Readonly: PascalCase
|
||||||
|
# #
|
||||||
|
# ##############################################################
|
||||||
|
|
||||||
|
# === Style: Underscore + camelCase ===
|
||||||
|
dotnet_naming_style.underscore_camel_case_style.capitalization = camel_case
|
||||||
|
dotnet_naming_style.underscore_camel_case_style.required_prefix = _
|
||||||
|
|
||||||
|
# === Style: PascalCase ===
|
||||||
|
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||||
|
|
||||||
|
|
||||||
|
# === Rule: Private Instance Fields → _camelCase ===
|
||||||
|
dotnet_naming_rule.private_instance_fields.severity = warning
|
||||||
|
dotnet_naming_rule.private_instance_fields.symbols = private_instance_fields_symbols
|
||||||
|
dotnet_naming_rule.private_instance_fields.style = underscore_camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
|
||||||
|
|
||||||
|
|
||||||
|
# === Rule: Private Static Fields → _camelCase ===
|
||||||
|
dotnet_naming_rule.private_static_fields.severity = warning
|
||||||
|
dotnet_naming_rule.private_static_fields.symbols = private_static_fields_symbols
|
||||||
|
dotnet_naming_rule.private_static_fields.style = underscore_camel_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
|
||||||
|
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
|
||||||
|
|
||||||
|
|
||||||
|
# === Rule: Private Constants → PascalCase ===
|
||||||
|
dotnet_naming_rule.private_constants.severity = warning
|
||||||
|
dotnet_naming_rule.private_constants.symbols = private_constants_symbols
|
||||||
|
dotnet_naming_rule.private_constants.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
|
||||||
|
dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
|
||||||
|
|
||||||
|
|
||||||
|
# === Rule: Private Static Readonly → PascalCase ===
|
||||||
|
dotnet_naming_rule.private_static_readonly.severity = warning
|
||||||
|
dotnet_naming_rule.private_static_readonly.symbols = private_static_readonly_symbols
|
||||||
|
dotnet_naming_rule.private_static_readonly.style = pascal_case_style
|
||||||
|
|
||||||
|
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
|
||||||
|
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
|
||||||
|
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################
|
||||||
|
# #
|
||||||
|
# # JetBrains Rider / ReSharper Settings
|
||||||
|
# #
|
||||||
|
# ##############################################################
|
||||||
|
|
||||||
|
# === Brace-Style (für ReSharper-spezifische Formatierung) ===
|
||||||
|
resharper_csharp_brace_style = next_line
|
||||||
|
|
||||||
|
# Kurze Statements ohne Klammern erlaubt (für 1-Zeiler)
|
||||||
|
resharper_csharp_braces_for_foreach = not_required
|
||||||
|
resharper_csharp_braces_for_for = not_required
|
||||||
|
resharper_csharp_braces_for_while = not_required
|
||||||
|
|
||||||
|
# === Auto-Detection und Formatter-Tags ===
|
||||||
|
resharper_autodetect_indent_settings = true
|
||||||
|
resharper_use_indent_from_vs = false
|
||||||
|
|
||||||
|
# Erlaubt @formatter:off / @formatter:on Kommentare im Code
|
||||||
|
resharper_formatter_off_tag = @formatter:off
|
||||||
|
resharper_formatter_on_tag = @formatter:on
|
||||||
|
resharper_formatter_tags_enabled = true
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ReSharper Inspection Severities
|
||||||
|
# (Hints = blaue Wellen, Warnings = gelb, Errors = rot)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Style-Suggestions: nur als Hint anzeigen
|
||||||
|
resharper_arrange_redundant_parentheses_highlighting = hint
|
||||||
|
resharper_arrange_this_qualifier_highlighting = hint
|
||||||
|
resharper_arrange_type_member_modifiers_highlighting = hint
|
||||||
|
resharper_arrange_type_modifiers_highlighting = hint
|
||||||
|
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
|
||||||
|
resharper_built_in_type_reference_style_highlighting = hint
|
||||||
|
resharper_suggest_var_or_type_built_in_types_highlighting = hint
|
||||||
|
resharper_suggest_var_or_type_elsewhere_highlighting = hint
|
||||||
|
resharper_suggest_var_or_type_simple_types_highlighting = hint
|
||||||
|
|
||||||
|
# Echte Probleme: als Warning
|
||||||
|
resharper_redundant_base_qualifier_highlighting = warning
|
||||||
+45
-14
@@ -1,19 +1,50 @@
|
|||||||
# Local development environment template
|
##############################################################
|
||||||
#
|
##
|
||||||
# Copy this file to `.env` and adjust paths to your setup,
|
## .env.example – Hellion Forge / Hellion Media
|
||||||
# or run: bash scripts/setup-dev-env.sh
|
##
|
||||||
#
|
## Template für lokale Entwicklungsumgebung.
|
||||||
# `.env` is gitignored — never commit your local paths.
|
## Kopiere diese Datei nach `.env` und passe die Pfade
|
||||||
#
|
## an dein Setup an.
|
||||||
# Activate in shell:
|
##
|
||||||
# set -a; source .env; set +a
|
## ⚠️ `.env` ist gitignored – niemals lokale Pfade committen!
|
||||||
#
|
##
|
||||||
# Or use direnv (recommended):
|
##############################################################
|
||||||
# echo 'dotenv .env' > .envrc && direnv allow
|
##
|
||||||
|
## SETUP
|
||||||
|
##
|
||||||
|
## 1) Manuell:
|
||||||
|
## cp .env.example .env
|
||||||
|
## # Pfade in .env anpassen
|
||||||
|
##
|
||||||
|
## 2) Automatisch:
|
||||||
|
## bash scripts/setup-dev-env.sh
|
||||||
|
##
|
||||||
|
## AKTIVIERUNG IN DER SHELL
|
||||||
|
##
|
||||||
|
## Variante A – einmalig pro Shell:
|
||||||
|
## set -a; source .env; set +a
|
||||||
|
##
|
||||||
|
## Variante B – mit direnv (empfohlen):
|
||||||
|
## echo 'dotenv .env' > .envrc
|
||||||
|
## direnv allow
|
||||||
|
##
|
||||||
|
##############################################################
|
||||||
|
|
||||||
# Path to Dalamud development DLLs (Dalamud.dll, FFXIVClientStructs.dll,
|
|
||||||
# Lumina.dll, Lumina.Excel.dll). Required for building ChatTwo.Tests project.
|
# =====================================================
|
||||||
|
# Build & Development Paths
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Pfad zu den Dalamud-Development-DLLs:
|
||||||
|
# - Dalamud.dll
|
||||||
|
# - FFXIVClientStructs.dll
|
||||||
|
# - Lumina.dll
|
||||||
|
# - Lumina.Excel.dll
|
||||||
#
|
#
|
||||||
|
# Wird zum Bauen des HellionChat.Tests-Projekts benötigt.
|
||||||
|
#
|
||||||
|
# Standardpfade je nach Plattform:
|
||||||
# XIVLauncher Core (Linux): ~/.xlcore/dalamud/Hooks/dev
|
# XIVLauncher Core (Linux): ~/.xlcore/dalamud/Hooks/dev
|
||||||
# XIVLauncher (Windows): %AppData%\XIVLauncher\addon\Hooks\dev
|
# XIVLauncher (Windows): %AppData%\XIVLauncher\addon\Hooks\dev
|
||||||
|
# XIVLauncher (macOS): ~/Library/Application Support/XIV on Mac/dalamud/Hooks/dev
|
||||||
DALAMUD_HOME=/path/to/dalamud/dev/dlls
|
DALAMUD_HOME=/path/to/dalamud/dev/dlls
|
||||||
|
|||||||
+173
-3
@@ -1,8 +1,178 @@
|
|||||||
# Generated files
|
##############################################################
|
||||||
HellionChat/Resources/Language.*.resx linguist-generated=true
|
##
|
||||||
|
## .gitattributes – Hellion Forge / Hellion Media
|
||||||
|
##
|
||||||
|
## Setup: Linux-First Development
|
||||||
|
## (Hauptentwicklung auf Linux, Target = Dalamud/Windows)
|
||||||
|
## Überarbeitet: Mai 2026
|
||||||
|
##
|
||||||
|
## Strategie:
|
||||||
|
## - Default: Alles LF (Linux-Konvention)
|
||||||
|
## - Windows-Batch-Scripts: CRLF (technische Pflicht!)
|
||||||
|
## - PowerShell: CRLF (Sicherheit für Windows PS 5.1)
|
||||||
|
## - Binärdateien: explizit markiert (gegen Korruption)
|
||||||
|
##
|
||||||
|
## Hinweis:
|
||||||
|
## Moderne Visual-Studio- und MSBuild-Versionen kommen
|
||||||
|
## problemlos mit LF in .sln/.csproj klar.
|
||||||
|
## Falls jemals Probleme auftauchen: hier umstellen.
|
||||||
|
##
|
||||||
|
##############################################################
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Default: Auto-Detect, alles auf LF normalisieren
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Source Code (LF)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
*.cs text eol=lf
|
*.cs text eol=lf
|
||||||
|
*.csx text eol=lf
|
||||||
|
*.vb text eol=lf
|
||||||
|
*.fs text eol=lf
|
||||||
|
*.fsx text eol=lf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Configs & Daten (LF)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.json text eol=lf
|
||||||
*.yml text eol=lf
|
*.yml text eol=lf
|
||||||
*.yaml text eol=lf
|
*.yaml text eol=lf
|
||||||
|
*.xml text eol=lf
|
||||||
*.md text eol=lf
|
*.md text eol=lf
|
||||||
*.json text eol=lf
|
*.txt text eol=lf
|
||||||
|
*.config text eol=lf
|
||||||
|
*.editorconfig text eol=lf
|
||||||
|
.gitignore text eol=lf
|
||||||
|
.gitattributes text eol=lf
|
||||||
|
.env.example text eol=lf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Visual Studio / MSBuild Project Files (LF)
|
||||||
|
# Linux-first: moderne Tools kommen mit LF zurecht
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.sln text eol=lf
|
||||||
|
*.csproj text eol=lf
|
||||||
|
*.vbproj text eol=lf
|
||||||
|
*.fsproj text eol=lf
|
||||||
|
*.props text eol=lf
|
||||||
|
*.targets text eol=lf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Resources & Lokalisierung (LF)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Linguist soll generierte Sprachdateien nicht mitzählen
|
||||||
|
HellionChat/Resources/Language.*.resx linguist-generated=true
|
||||||
|
|
||||||
|
*.resx text eol=lf
|
||||||
|
*.resw text eol=lf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Linux/Mac-Scripts (LF – Pflicht)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.bash text eol=lf
|
||||||
|
*.zsh text eol=lf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# >>> AUSNAHMEN <<<
|
||||||
|
# Windows-Scripts brauchen ZWINGEND CRLF.
|
||||||
|
# Mit LF werden diese auf Windows nicht ausgeführt!
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Batch-Scripts (cmd.exe braucht CRLF)
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# PowerShell (PS 7+ wäre LF-tolerant,
|
||||||
|
# aber Windows PowerShell 5.1 zickt teilweise)
|
||||||
|
*.ps1 text eol=crlf
|
||||||
|
*.psm1 text eol=crlf
|
||||||
|
*.psd1 text eol=crlf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Binäre Build-Artefakte
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.dll binary
|
||||||
|
*.exe binary
|
||||||
|
*.pdb binary
|
||||||
|
*.so binary
|
||||||
|
*.dylib binary
|
||||||
|
*.nupkg binary
|
||||||
|
*.snupkg binary
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Bilder (binary)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.bmp binary
|
||||||
|
*.tiff binary
|
||||||
|
*.webp binary
|
||||||
|
|
||||||
|
# SVG ist eigentlich XML – als Text behandeln
|
||||||
|
*.svg text eol=lf
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Fonts (binary)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.eot binary
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Archive (binary)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.zip binary
|
||||||
|
*.7z binary
|
||||||
|
*.tar binary
|
||||||
|
*.gz binary
|
||||||
|
*.rar binary
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Audio / Video (binary)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.wav binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.ogg binary
|
||||||
|
*.mp4 binary
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# FFXIV / Dalamud spezifische Binär-Formate
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.tex binary
|
||||||
|
*.pap binary
|
||||||
|
*.avfx binary
|
||||||
|
*.shpk binary
|
||||||
|
*.scd binary
|
||||||
@@ -19,11 +19,11 @@ name: Forge Announce
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: "Existing tag to (re)post, e.g. v1.1.0"
|
description: 'Existing tag to (re)post, e.g. v1.1.0'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@@ -120,17 +120,37 @@ jobs:
|
|||||||
$enBlock = $rest.TrimEnd()
|
$enBlock = $rest.TrimEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
# ---------- Embed-Felder + Per-Field-Caps (Discord-Hard-Limits) ----------
|
||||||
|
# Discord enforces per-embed-field limits separately from the
|
||||||
|
# combined-total limit. We split the DE and EN blocks into two
|
||||||
|
# embeds that share the same release URL so Discord stitches
|
||||||
|
# them into one visual card. Hard caps per Discord docs:
|
||||||
|
# description: 4096 per embed
|
||||||
|
# title: 256 per embed
|
||||||
|
# footer.text: 2048 per embed
|
||||||
|
# combined sum across all embeds: 6000
|
||||||
$title = "Hellion Chat $version — $subtitle"
|
$title = "Hellion Chat $version — $subtitle"
|
||||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
$deDesc = "**Deutsch**`n`n$deBody"
|
||||||
|
$enDesc = "**English**`n`n$enBlock"
|
||||||
$footerText = "Hellion Forge · $versionsnatur"
|
$footerText = "Hellion Forge · $versionsnatur"
|
||||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
$releaseUrl = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||||
if ($totalChars -gt 5500) {
|
|
||||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
|
||||||
}
|
|
||||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
|
||||||
|
|
||||||
# ---------- Embed-Payload bauen ----------
|
if ($deDesc.Length -gt 4096) {
|
||||||
|
throw "V6a: DE-Body too long for one embed ($($deDesc.Length) chars, max 4096). Trim .github/forge-posts/$tag.md or post the announcement manually (see forge style §8)."
|
||||||
|
}
|
||||||
|
if ($enDesc.Length -gt 4096) {
|
||||||
|
throw "V6b: EN-Block too long for one embed ($($enDesc.Length) chars, max 4096). Trim the changelog entry in HellionChat/HellionChat.yaml or post manually."
|
||||||
|
}
|
||||||
|
$totalChars = $title.Length + $deDesc.Length + $enDesc.Length + $footerText.Length
|
||||||
|
if ($totalChars -gt 6000) {
|
||||||
|
throw "V6c: Combined embed chars $totalChars exceed Discord's 6000-total limit. Major-Release detected — post manually via Bot/Multi-Embed (see forge style §8)."
|
||||||
|
}
|
||||||
|
Write-Host "Embed-Caps OK: de=$($deDesc.Length)/4096, en=$($enDesc.Length)/4096, total=$totalChars/6000"
|
||||||
|
|
||||||
|
# ---------- Embed-Payload bauen (zwei Embeds, gleiche url) ----------
|
||||||
|
# Sharing the same `url` tells Discord to render both embeds as a
|
||||||
|
# single contiguous card block. The title sits on the first embed,
|
||||||
|
# the footer + timestamp on the last so it reads as one post.
|
||||||
$payload = [ordered]@{
|
$payload = [ordered]@{
|
||||||
username = "Forge Herald"
|
username = "Forge Herald"
|
||||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||||
@@ -142,9 +162,14 @@ jobs:
|
|||||||
embeds = @(
|
embeds = @(
|
||||||
[ordered]@{
|
[ordered]@{
|
||||||
title = $title
|
title = $title
|
||||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
url = $releaseUrl
|
||||||
color = 12730636
|
color = 12730636
|
||||||
description = $description
|
description = $deDesc
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
url = $releaseUrl
|
||||||
|
color = 12730636
|
||||||
|
description = $enDesc
|
||||||
footer = [ordered]@{ text = $footerText }
|
footer = [ordered]@{ text = $footerText }
|
||||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ name: Release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
|
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
|
||||||
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
|
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
|
||||||
# ref step below hard-fails if a non-tag ref is selected, because the
|
# ref step below hard-fails if a non-tag ref is selected, because the
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
branches: [main, master]
|
branches: [main, master]
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 6 * * 1"
|
- cron: '0 6 * * 1'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -17,4 +17,4 @@ jobs:
|
|||||||
# local-only inside a Dalamud plugin. Semgrep matches the pattern
|
# local-only inside a Dalamud plugin. Semgrep matches the pattern
|
||||||
# without dataflow, so it flags those eight call sites; CodeQL
|
# without dataflow, so it flags those eight call sites; CodeQL
|
||||||
# would not. Suppressed for this repo only.
|
# would not. Suppressed for this repo only.
|
||||||
semgrep-exclude-rules: "csharp.lang.security.sqli.csharp-sqli.csharp-sqli"
|
semgrep-exclude-rules: 'csharp.lang.security.sqli.csharp-sqli.csharp-sqli'
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: HellionChat version
|
label: HellionChat version
|
||||||
description: From Settings → Information → Version
|
description: From Settings → Information → Version
|
||||||
placeholder: "0.5.4"
|
placeholder: '0.5.4'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Scope estimate from your side
|
label: Scope estimate from your side
|
||||||
options:
|
options:
|
||||||
- "Small (one tab, one toggle, one filter)"
|
- 'Small (one tab, one toggle, one filter)'
|
||||||
- "Medium (a settings section, persistent state, one new file)"
|
- 'Medium (a settings section, persistent state, one new file)'
|
||||||
- "Large (architectural, touches the message pipeline or the database)"
|
- 'Large (architectural, touches the message pipeline or the database)'
|
||||||
- "I don't know"
|
- "I don't know"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -51,7 +51,5 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I have searched existing issues for similar requests
|
- label: I have searched existing issues for similar requests
|
||||||
required: true
|
required: true
|
||||||
- label:
|
- label: I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat 2
|
||||||
I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat
|
|
||||||
2
|
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
|||||||
|
|
||||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||||
- [ ] New feature (non-breaking change that adds behaviour)
|
- [ ] New feature (non-breaking change that adds behaviour)
|
||||||
- [ ] Breaking change (config migration, removed feature, or behaviour change that user-visible defaults rely on)
|
- [ ] Breaking change (config migration, removed feature, or behaviour change that user-visible
|
||||||
|
defaults rely on)
|
||||||
- [ ] Documentation only
|
- [ ] Documentation only
|
||||||
- [ ] Translation update
|
- [ ] Translation update
|
||||||
- [ ] Build, CI or tooling change
|
- [ ] Build, CI or tooling change
|
||||||
@@ -55,10 +56,11 @@ new commands, new translations, removed behaviour. If none, write
|
|||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
|
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and
|
||||||
|
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
|
||||||
- [ ] My change matches the existing code style (`.editorconfig`).
|
- [ ] My change matches the existing code style (`.editorconfig`).
|
||||||
- [ ] I added or updated tests where the existing test infrastructure made that practical, or I have explained why tests
|
- [ ] I added or updated tests where the existing test infrastructure made that practical, or I have
|
||||||
are not applicable.
|
explained why tests are not applicable.
|
||||||
- [ ] I updated the README, in-plugin strings or documentation if my change is user-visible.
|
- [ ] I updated the README, in-plugin strings or documentation if my change is user-visible.
|
||||||
- [ ] I did not include any AI-generated code without disclosing it in the PR description (see
|
- [ ] I did not include any AI-generated code without disclosing it in the PR description (see
|
||||||
[AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)).
|
[AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)).
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: monday
|
day: monday
|
||||||
time: "07:00"
|
time: '07:00'
|
||||||
timezone: Europe/Berlin
|
timezone: Europe/Berlin
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
labels:
|
labels:
|
||||||
- dependencies
|
- dependencies
|
||||||
- nuget
|
- nuget
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(deps)"
|
prefix: 'chore(deps)'
|
||||||
groups:
|
groups:
|
||||||
patches:
|
patches:
|
||||||
update-types:
|
update-types:
|
||||||
@@ -32,11 +32,11 @@ updates:
|
|||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
time: "07:00"
|
time: '07:00'
|
||||||
timezone: Europe/Berlin
|
timezone: Europe/Berlin
|
||||||
open-pull-requests-limit: 3
|
open-pull-requests-limit: 3
|
||||||
labels:
|
labels:
|
||||||
- dependencies
|
- dependencies
|
||||||
- github-actions
|
- github-actions
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(actions)"
|
prefix: 'chore(actions)'
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ subtitle: "Theme Foundation"
|
|||||||
versionsnatur: "Major-UI-Cycle"
|
versionsnatur: "Major-UI-Cycle"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Theme-Engine mit fünf Built-In-Themes: Hellion Arctic (Default), Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint
|
- Theme-Engine mit fünf Built-In-Themes: Hellion Arctic (Default), Chat 2 Klassik, Event Horizon,
|
||||||
Grove
|
Moonlit Bloom, Mint Grove
|
||||||
- Settings öffnet jetzt eine Card-Grid-Übersicht — Klick auf eine Card führt in den Detail-View, Breadcrumb und ESC
|
- Settings öffnet jetzt eine Card-Grid-Übersicht — Klick auf eine Card führt in den Detail-View,
|
||||||
zurück zur Übersicht
|
Breadcrumb und ESC zurück zur Übersicht
|
||||||
- Themes-Tab mit Mini-Mockup pro Theme, Live-Switch beim Klick
|
- Themes-Tab mit Mini-Mockup pro Theme, Live-Switch beim Klick
|
||||||
- Eigene Themes als JSON in `pluginConfigs/HellionChat/themes/` — Beispiel-Vorlage wird beim ersten Start automatisch
|
- Eigene Themes als JSON in `pluginConfigs/HellionChat/themes/` — Beispiel-Vorlage wird beim ersten
|
||||||
abgelegt
|
Start automatisch abgelegt
|
||||||
- Optional pro Theme eigene Chat-Channel-Farben mit Übernehmen/Behalten-Banner — niemals automatisch überschrieben
|
- Optional pro Theme eigene Chat-Channel-Farben mit Übernehmen/Behalten-Banner — niemals automatisch
|
||||||
|
überschrieben
|
||||||
- Plugin-Icon zum Hellion-Forge-Hammer gewechselt
|
- Plugin-Icon zum Hellion-Forge-Hammer gewechselt
|
||||||
- Migration v13 → v14: alle User landen auf Hellion Arctic. Wer den Upstream-Look will, wählt Chat 2 Klassik in Settings
|
- Migration v13 → v14: alle User landen auf Hellion Arctic. Wer den Upstream-Look will, wählt Chat 2
|
||||||
→ Themes
|
Klassik in Settings → Themes
|
||||||
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
|
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
|
||||||
|
|||||||
@@ -3,22 +3,22 @@ subtitle: "Layout Refresh"
|
|||||||
versionsnatur: "Major-UI-Cycle"
|
versionsnatur: "Major-UI-Cycle"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Sidebar im neuen Look: fix 44 px breit, nur Icons, Tab-Name als Tooltip beim Hover, vertikale Akzent-Pill markiert den
|
- Sidebar im neuen Look: fix 44 px breit, nur Icons, Tab-Name als Tooltip beim Hover, vertikale
|
||||||
aktiven Tab
|
Akzent-Pill markiert den aktiven Tab
|
||||||
- Top-Tabs bekommen eine Akzent-Underline statt Background-Fill am aktiven Tab
|
- Top-Tabs bekommen eine Akzent-Underline statt Background-Fill am aktiven Tab
|
||||||
- Pro Tab eigenes Icon wählbar in Einstellungen → Tabs (FontAwesome-Pool)
|
- Pro Tab eigenes Icon wählbar in Einstellungen → Tabs (FontAwesome-Pool)
|
||||||
- Auto-Tell-Tabs sind jetzt visuell unterscheidbar: jeder Tell-Partner bekommt ein eigenes Icon
|
- Auto-Tell-Tabs sind jetzt visuell unterscheidbar: jeder Tell-Partner bekommt ein eigenes Icon
|
||||||
(envelope/star/heart/bell/bookmark/flag/fire) plus eigene Farbe aus 12-Farb-Palette — 84 Kombinationen, gleicher
|
(envelope/star/heart/bell/bookmark/flag/fire) plus eigene Farbe aus 12-Farb-Palette — 84
|
||||||
Partner ergibt konsistent dieselbe
|
Kombinationen, gleicher Partner ergibt konsistent dieselbe
|
||||||
- Pulsierender roter Dot oben rechts am Sidebar-Icon zeigt ungelesene Nachrichten an. Sanft, 2-Sekunden-Cycle,
|
- Pulsierender roter Dot oben rechts am Sidebar-Icon zeigt ungelesene Nachrichten an. Sanft,
|
||||||
deaktivierbar über `Configuration.ReduceMotion` (UI-Toggle in v1.3.0)
|
2-Sekunden-Cycle, deaktivierbar über `Configuration.ReduceMotion` (UI-Toggle in v1.3.0)
|
||||||
- Bottom-Status-Bar (22 px) mit fünf Live-Slots: aktiver Channel + Color-Dot, Privacy-Badge, Tab/Message-Counter,
|
- Bottom-Status-Bar (22 px) mit fünf Live-Slots: aktiver Channel + Color-Dot, Privacy-Badge,
|
||||||
Auto-Tell-Counter, Plugin-Version. Update 1×/Sek
|
Tab/Message-Counter, Auto-Tell-Counter, Plugin-Version. Update 1×/Sek
|
||||||
- Card-Rows als Default-Message-Render: Sender-Header in Channel-Farbe, Body neue Zeile, dezenter Trenner.
|
- Card-Rows als Default-Message-Render: Sender-Header in Channel-Farbe, Body neue Zeile, dezenter
|
||||||
`Compact Density`-Toggle in Aussehen schaltet zurück auf den Einzeiler
|
Trenner. `Compact Density`-Toggle in Aussehen schaltet zurück auf den Einzeiler
|
||||||
- Bug-Fix: Settings speichern löscht den Chat-Verlauf nicht mehr. Refilter läuft jetzt nur wenn Filter-relevante
|
- Bug-Fix: Settings speichern löscht den Chat-Verlauf nicht mehr. Refilter läuft jetzt nur wenn
|
||||||
Settings geändert wurden — Cosmetic-Änderungen lassen den Chat unverändert. Persistente und Auto-Tell-Tabs überleben
|
Filter-relevante Settings geändert wurden — Cosmetic-Änderungen lassen den Chat unverändert.
|
||||||
beide
|
Persistente und Auto-Tell-Tabs überleben beide
|
||||||
- Bug-Fix: Hellion-Schrift (Exo 2) blockt die Schriftgröße nicht mehr — 4K-User können hochskalieren
|
- Bug-Fix: Hellion-Schrift (Exo 2) blockt die Schriftgröße nicht mehr — 4K-User können hochskalieren
|
||||||
- Migration v14 → v15: alte Theme-Felder entfernt, alle anderen Settings bleiben
|
- Migration v14 → v15: alte Theme-Felder entfernt, alle anderen Settings bleiben
|
||||||
|
|
||||||
|
|||||||
@@ -3,27 +3,31 @@ subtitle: "Settings Cleanup"
|
|||||||
versionsnatur: "UX-Polish-Cycle"
|
versionsnatur: "UX-Polish-Cycle"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Settings-Übersicht thematisch re-sortiert: zusammenhängende Optionen wohnen jetzt zusammen, jede Card hat einen kurzen
|
- Settings-Übersicht thematisch re-sortiert: zusammenhängende Optionen wohnen jetzt zusammen, jede
|
||||||
Untertitel — kein Raten mehr wo eine Setting steckt
|
Card hat einen kurzen Untertitel — kein Raten mehr wo eine Setting steckt
|
||||||
- Drei neue Cards: **Theme & Layout** (Theme-Picker, Fenster-Style, Zeitstempel-Style), **Schriften & Farben**
|
- Drei neue Cards: **Theme & Layout** (Theme-Picker, Fenster-Style, Zeitstempel-Style), **Schriften
|
||||||
(Schriftart, Schriftgröße, Chat-Farben pro Channel), **Daten-Verwaltung** (Aufbewahrung, Cleanup, Export, DB-Viewer,
|
& Farben** (Schriftart, Schriftgröße, Chat-Farben pro Channel), **Daten-Verwaltung**
|
||||||
Advanced-Tools — vorher zwischen Datenschutz und Datenbank verteilt)
|
(Aufbewahrung, Cleanup, Export, DB-Viewer, Advanced-Tools — vorher zwischen Datenschutz und
|
||||||
|
Datenbank verteilt)
|
||||||
- Datenschutz fokussiert sich jetzt auf eine Aufgabe: den Privacy-Filter
|
- Datenschutz fokussiert sich jetzt auf eine Aufgabe: den Privacy-Filter
|
||||||
- Der Auto-Tell-Tabs-History-Preload-Slider ist von Datenschutz nach Chat → Auto-Tell-Tabs umgezogen
|
- Der Auto-Tell-Tabs-History-Preload-Slider ist von Datenschutz nach Chat → Auto-Tell-Tabs umgezogen
|
||||||
- KeybindMode wohnt jetzt unter Allgemein → Eingabe statt unter Sprache
|
- KeybindMode wohnt jetzt unter Allgemein → Eingabe statt unter Sprache
|
||||||
- Vier tote Schema-Felder entfernt (alle obsolet seit der Theme-Engine in v1.1.0): `Stilüberschreiben`-Toggle,
|
- Vier tote Schema-Felder entfernt (alle obsolet seit der Theme-Engine in v1.1.0):
|
||||||
`Stilname`-Auswahl, alter `WindowAlpha`-Slider, ungenutztes `ShowThemeQuickPicker`
|
`Stilüberschreiben`-Toggle, `Stilname`-Auswahl, alter `WindowAlpha`-Slider, ungenutztes
|
||||||
|
`ShowThemeQuickPicker`
|
||||||
- Migration v15 → v16: alter `WindowAlpha`-Wert wird automatisch nach
|
- Migration v15 → v16: alter `WindowAlpha`-Wert wird automatisch nach
|
||||||
`Theme & Layout → Fenster-Style → Fenster-Transparenz` gemappt (nur wenn der Slider noch auf Default 0.85 stand, sonst
|
`Theme & Layout → Fenster-Style → Fenster-Transparenz` gemappt (nur wenn der Slider noch auf
|
||||||
gewinnt der User-Wert). Backup der Pre-v16-Config liegt unter `pluginConfigs/HellionChat.json.pre-v16-backup`. User
|
Default 0.85 stand, sonst gewinnt der User-Wert). Backup der Pre-v16-Config liegt unter
|
||||||
die `Stilüberschreiben` aktiv hatten sehen einen einmaligen Hinweis-Toast
|
`pluginConfigs/HellionChat.json.pre-v16-backup`. User die `Stilüberschreiben` aktiv hatten sehen
|
||||||
- UX-Default-Bumps für Bestand-User mit Default-Werten: Card-Rows-Layout zurück auf Single-Line, NG+ standardmäßig
|
einen einmaligen Hinweis-Toast
|
||||||
hidden, gleiche Zeitstempel werden zusammengefasst, MaxLinesToRender auf konservativere 2500
|
- UX-Default-Bumps für Bestand-User mit Default-Werten: Card-Rows-Layout zurück auf Single-Line, NG+
|
||||||
- Frische Installs starten mit dem Hellion-Brand-Chat-Color-Preset out-of-the-box (der First-Run-Wizard hat keine
|
standardmäßig hidden, gleiche Zeitstempel werden zusammengefasst, MaxLinesToRender auf
|
||||||
Preset-Wahl)
|
konservativere 2500
|
||||||
- Hinweis zum Window-Transparenz-Slider in der Beschreibung: Dalamud's per-Window-Hamburger-Menü (oben rechts in der
|
- Frische Installs starten mit dem Hellion-Brand-Chat-Color-Preset out-of-the-box (der
|
||||||
Titelleiste) bietet eigene Overrides für Deckkraft, Hintergrund-Blur, Anpinnen und Durchklick — die haben Vorrang über
|
First-Run-Wizard hat keine Preset-Wahl)
|
||||||
unseren Slider für das jeweilige Fenster
|
- Hinweis zum Window-Transparenz-Slider in der Beschreibung: Dalamud's per-Window-Hamburger-Menü
|
||||||
|
(oben rechts in der Titelleiste) bietet eigene Overrides für Deckkraft, Hintergrund-Blur, Anpinnen
|
||||||
|
und Durchklick — die haben Vorrang über unseren Slider für das jeweilige Fenster
|
||||||
|
|
||||||
Pure UX-Polish, keine neuen Features. Nächster Cycle (v1.3.0): Animation-Polish (Lerps, Theme-Crossfade, Quick-Picker)
|
Pure UX-Polish, keine neuen Features. Nächster Cycle (v1.3.0): Animation-Polish (Lerps,
|
||||||
wie ursprünglich geplant.
|
Theme-Crossfade, Quick-Picker) wie ursprünglich geplant.
|
||||||
|
|||||||
@@ -3,21 +3,23 @@ subtitle: "Theme Expansion"
|
|||||||
versionsnatur: "Theme-Pack-Patch"
|
versionsnatur: "Theme-Pack-Patch"
|
||||||
---
|
---
|
||||||
|
|
||||||
- Vier neue Built-in-Themes verlängern die Auswahl im Picker — keine Engine-Änderung, keine Settings angefasst, einfach
|
- Vier neue Built-in-Themes verlängern die Auswahl im Picker — keine Engine-Änderung, keine Settings
|
||||||
mehr Farboptionen
|
angefasst, einfach mehr Farboptionen
|
||||||
- **Night Blue** — Royal Blue auf tiefem Marineblau. Kühles Tech-Dashboard-Mood, bewusst neutral gehalten damit es sich
|
- **Night Blue** — Royal Blue auf tiefem Marineblau. Kühles Tech-Dashboard-Mood, bewusst neutral
|
||||||
nicht mit den Brand-Themes beißt
|
gehalten damit es sich nicht mit den Brand-Themes beißt
|
||||||
- **Indigo Violet** — Royal Violet auf Deep Indigo mit Türkis-Mint-Counter für Aurora-Glitter-Stimmung. Schwester von
|
- **Indigo Violet** — Royal Violet auf Deep Indigo mit Türkis-Mint-Counter für
|
||||||
Event Horizon, aber dunkler und dichter; der Türkis-Akzent hält die beiden klar auseinander
|
Aurora-Glitter-Stimmung. Schwester von Event Horizon, aber dunkler und dichter; der Türkis-Akzent
|
||||||
- **Forge Merchantman** — Patina-Bronze auf Workshop-Slate mit warmem Bernstein-Counter. Hellion Forge bekommt ein
|
hält die beiden klar auseinander
|
||||||
eigenes Theme im Plugin selbst — Schwester von Hellion Arctic, aber grüner und wärmer statt kaltem Cyan
|
- **Forge Merchantman** — Patina-Bronze auf Workshop-Slate mit warmem Bernstein-Counter. Hellion
|
||||||
|
Forge bekommt ein eigenes Theme im Plugin selbst — Schwester von Hellion Arctic, aber grüner und
|
||||||
|
wärmer statt kaltem Cyan
|
||||||
- **Hellion Spectrum** — Farbenblind-sichere Channel-Farben (Deuteranopie/Protanopie) auf Basis der
|
- **Hellion Spectrum** — Farbenblind-sichere Channel-Farben (Deuteranopie/Protanopie) auf Basis der
|
||||||
Wong/Okabe-Ito-Palette. Channel-Identität bleibt erhalten (Tell pink, Yell gelb, Shout orange, Party blau, FC grün);
|
Wong/Okabe-Ito-Palette. Channel-Identität bleibt erhalten (Tell pink, Yell gelb, Shout orange,
|
||||||
die Töne sind so gewählt dass jeder Channel auch unter Rot-Grün-Schwäche klar trennbar bleibt. Deckt rund 99 % aller
|
Party blau, FC grün); die Töne sind so gewählt dass jeder Channel auch unter Rot-Grün-Schwäche
|
||||||
CVD-Fälle ab
|
klar trennbar bleibt. Deckt rund 99 % aller CVD-Fälle ab
|
||||||
- Kein Schema-Bump, keine Migration. Das Default-Theme bleibt **Hellion Arctic**, eigene Custom-Themes laufen
|
- Kein Schema-Bump, keine Migration. Das Default-Theme bleibt **Hellion Arctic**, eigene
|
||||||
unverändert weiter
|
Custom-Themes laufen unverändert weiter
|
||||||
- Theme-Katalog wächst damit von fünf auf neun Built-ins
|
- Theme-Katalog wächst damit von fünf auf neun Built-ins
|
||||||
|
|
||||||
Reines Theme-Pack zwischen v1.2.1 und dem nächsten Polish-Cycle. Eine Tritan-Variante (Spectrum für Blau-Gelb-Schwäche)
|
Reines Theme-Pack zwischen v1.2.1 und dem nächsten Polish-Cycle. Eine Tritan-Variante (Spectrum für
|
||||||
kann später nachgeliefert werden, falls Bedarf kommt.
|
Blau-Gelb-Schwäche) kann später nachgeliefert werden, falls Bedarf kommt.
|
||||||
|
|||||||
@@ -4,15 +4,17 @@ versionsnatur: "Plugin-Integration-Cycle 1"
|
|||||||
---
|
---
|
||||||
|
|
||||||
- Erste Plugin-Integration eingebaut, Cycle 1 von 6 auf der Roadmap
|
- Erste Plugin-Integration eingebaut, Cycle 1 von 6 auf der Roadmap
|
||||||
- **Honorific-Custom-Titles im Chat-Header** — der Titel den du in Honorific gesetzt hast erscheint jetzt links über dem
|
- **Honorific-Custom-Titles im Chat-Header** — der Titel den du in Honorific gesetzt hast erscheint
|
||||||
Message-Log mit der von dir gewählten Farbe, Auto-Hide wenn Honorific nicht installiert ist oder kein Custom-Titel
|
jetzt links über dem Message-Log mit der von dir gewählten Farbe, Auto-Hide wenn Honorific nicht
|
||||||
aktiv ist
|
installiert ist oder kein Custom-Titel aktiv ist
|
||||||
- **Krone-Icon plus Tooltip** vor dem Titel-Text, damit klar ist woher der Slot kommt ohne dass der User raten muss
|
- **Krone-Icon plus Tooltip** vor dem Titel-Text, damit klar ist woher der Slot kommt ohne dass der
|
||||||
- **Neuer Integrations-Settings-Tab** mit Status-Indikator (erkannt, nicht installiert, inkompatibel) und Toggle. Plus
|
User raten muss
|
||||||
Vorschau-Block der die fünf weiteren geplanten Cycles ankündigt: Kontextmenü-Aktionen, Smart Notifications
|
- **Neuer Integrations-Settings-Tab** mit Status-Indikator (erkannt, nicht installiert,
|
||||||
(NotificationMaster), RP-Status-Block (Moodles und LightlessClient), ExtraChat-Channels, Quick-DM-Button
|
inkompatibel) und Toggle. Plus Vorschau-Block der die fünf weiteren geplanten Cycles ankündigt:
|
||||||
(XIVInstantMessenger)
|
Kontextmenü-Aktionen, Smart Notifications (NotificationMaster), RP-Status-Block (Moodles und
|
||||||
- **Maintainer-Attribution** im Tab als Höflichkeits-Geste, zwei Buttons zum Honorific-Repo und zum Caraxi-Profil. Plus
|
LightlessClient), ExtraChat-Channels, Quick-DM-Button (XIVInstantMessenger)
|
||||||
Hellion-Forge-Discord-Button für Community-Vorschläge zu künftigen Integrationen
|
- **Maintainer-Attribution** im Tab als Höflichkeits-Geste, zwei Buttons zum Honorific-Repo und zum
|
||||||
- Keine Migration, keine Schema-Änderung. Wer Honorific eh schon nutzt sieht den Custom-Titel automatisch sobald
|
Caraxi-Profil. Plus Hellion-Forge-Discord-Button für Community-Vorschläge zu künftigen
|
||||||
HellionChat aktualisiert
|
Integrationen
|
||||||
|
- Keine Migration, keine Schema-Änderung. Wer Honorific eh schon nutzt sieht den Custom-Titel
|
||||||
|
automatisch sobald HellionChat aktualisiert
|
||||||
|
|||||||
@@ -5,19 +5,20 @@ versionsnatur: Stability-Hotfix
|
|||||||
|
|
||||||
**Hellion Chat 1.4.0 — Critical Lifecycle Fixes**
|
**Hellion Chat 1.4.0 — Critical Lifecycle Fixes**
|
||||||
|
|
||||||
Erster Sub-Patch der v1.4.x Polish-Sweep-Serie. Sieben bekannte Lifecycle- und Race-Bugs aus den Audit-Pässen
|
Erster Sub-Patch der v1.4.x Polish-Sweep-Serie. Sieben bekannte Lifecycle- und Race-Bugs aus den
|
||||||
abgearbeitet, bevor Performance- und Architektur-Refactors draufkommen.
|
Audit-Pässen abgearbeitet, bevor Performance- und Architektur-Refactors draufkommen.
|
||||||
|
|
||||||
- **SQLite-Dispose** lehnt sich nicht mehr an GC-Druck zur Datei-Freigabe an, Pooling=false auf der Connection macht den
|
- **SQLite-Dispose** lehnt sich nicht mehr an GC-Druck zur Datei-Freigabe an, Pooling=false auf der
|
||||||
manuellen GC.Collect überflüssig
|
Connection macht den manuellen GC.Collect überflüssig
|
||||||
- **Worker-Threads** (PendingMessage, RetentionSweep) sind jetzt explizit IsBackground=true, das Plugin-Domain kann
|
- **Worker-Threads** (PendingMessage, RetentionSweep) sind jetzt explizit IsBackground=true, das
|
||||||
sauber unloaden bei XIVLauncher-Reload ohne darauf zu warten
|
Plugin-Domain kann sauber unloaden bei XIVLauncher-Reload ohne darauf zu warten
|
||||||
- **EmoteCache-Loader** von async-void auf async-Task mit shared Task-Tracker, drain-on-Dispose. Kein Schreib-Risiko
|
- **EmoteCache-Loader** von async-void auf async-Task mit shared Task-Tracker, drain-on-Dispose.
|
||||||
mehr auf disposed EmoteImages-Einträge nach Plugin-Reload
|
Kein Schreib-Risiko mehr auf disposed EmoteImages-Einträge nach Plugin-Reload
|
||||||
- **DisposeAsync-Timeout** (10s) warnt jetzt laut statt silent zu failen
|
- **DisposeAsync-Timeout** (10s) warnt jetzt laut statt silent zu failen
|
||||||
- **Plugin-Dispose** flushed pending DeferredSave bevor Services abgebaut werden, Settings-Änderungen aus den letzten
|
- **Plugin-Dispose** flushed pending DeferredSave bevor Services abgebaut werden,
|
||||||
Frames vor Disable überleben jetzt zuverlässig
|
Settings-Änderungen aus den letzten Frames vor Disable überleben jetzt zuverlässig
|
||||||
- **v13→v14 Config-Migration** liest pre-v13-Backup und überträgt HellionThemeWindowOpacity in das neue
|
- **v13→v14 Config-Migration** liest pre-v13-Backup und überträgt HellionThemeWindowOpacity in das
|
||||||
WindowOpacity-Feld statt auf 0.85 zurückzufallen
|
neue WindowOpacity-Feld statt auf 0.85 zurückzufallen
|
||||||
|
|
||||||
Keine Schema-Bumps, keine User-sichtbaren Funktions-Änderungen außer dass Reload und Shutdown spürbar sauberer laufen.
|
Keine Schema-Bumps, keine User-sichtbaren Funktions-Änderungen außer dass Reload und Shutdown
|
||||||
|
spürbar sauberer laufen.
|
||||||
|
|||||||
@@ -5,23 +5,25 @@ versionsnatur: Performance-Patch
|
|||||||
|
|
||||||
**Hellion Chat 1.4.1 — Theme Engine Performance**
|
**Hellion Chat 1.4.1 — Theme Engine Performance**
|
||||||
|
|
||||||
Zweiter Sub-Patch der v1.4.x Polish-Sweep-Serie. Heap-Pressure aus dem Theme-Engine-Render-Pfad eliminiert,
|
Zweiter Sub-Patch der v1.4.x Polish-Sweep-Serie. Heap-Pressure aus dem Theme-Engine-Render-Pfad
|
||||||
Custom-Theme- Hot-Reload überlebt transiente File-Locks beim Editor-Save. Plus zehnter Built-In und überarbeitete
|
eliminiert, Custom-Theme- Hot-Reload überlebt transiente File-Locks beim Editor-Save. Plus zehnter
|
||||||
Author-Credits.
|
Built-In und überarbeitete Author-Credits.
|
||||||
|
|
||||||
- **ABGR-Cache auf den Theme-Records.** Beim Theme-Register (Built-In oder Custom) werden alle Color-Slots einmalig in
|
- **ABGR-Cache auf den Theme-Records.** Beim Theme-Register (Built-In oder Custom) werden alle
|
||||||
ABGR-Pack-Form vor-konvertiert. HellionStyle.PushGlobal liest aus dem Cache statt pro Slot pro Frame durch
|
Color-Slots einmalig in ABGR-Pack-Form vor-konvertiert. HellionStyle.PushGlobal liest aus dem
|
||||||
ColourUtil.RgbaToAbgr zu jagen. Real gemessene Frame-Time-Recovery: **~13 %** in typischer Render-Szene
|
Cache statt pro Slot pro Frame durch ColourUtil.RgbaToAbgr zu jagen. Real gemessene
|
||||||
(Plan-Erwartung war 2-6 % konservativ, real ~10-15 %)
|
Frame-Time-Recovery: **~13 %** in typischer Render-Szene (Plan-Erwartung war 2-6 % konservativ,
|
||||||
- **Custom-Theme File-Lock-Härtung.** Wenn der User ein Theme-JSON gerade speichert während HellionChat reloaden will,
|
real ~10-15 %)
|
||||||
fängt der Loader jetzt explizit Sharing-Violation und Lock-Violation ab. Last-Known-Good-Snapshot bleibt im Picker,
|
- **Custom-Theme File-Lock-Härtung.** Wenn der User ein Theme-JSON gerade speichert während
|
||||||
beim nächsten Tick wird automatisch retry'd — vorher fiel das Theme aus der Liste bis zum Plugin-Reload
|
HellionChat reloaden will, fängt der Loader jetzt explizit Sharing-Violation und Lock-Violation
|
||||||
- **Defensive Cache-Refresh beim Theme-Switch.** Falls ein Theme auf einem alten Pfad ohne Cache-Fill in den Speicher
|
ab. Last-Known-Good-Snapshot bleibt im Picker, beim nächsten Tick wird automatisch retry'd —
|
||||||
gekommen ist, holt Switch() das beim Anwenden nach
|
vorher fiel das Theme aus der Liste bis zum Plugin-Reload
|
||||||
- **Synthwave Sunset als zehnter Built-In.** Hot Magenta + Cyan auf Mitternachts-Violett, 80s-Neon-Grid-Vibes für
|
- **Defensive Cache-Refresh beim Theme-Switch.** Falls ein Theme auf einem alten Pfad ohne
|
||||||
Late-Night-Raids
|
Cache-Fill in den Speicher gekommen ist, holt Switch() das beim Anwenden nach
|
||||||
- **Author-Credits konsolidiert.** Brand-Themes laufen jetzt unter „Hellion Forge". Mint Grove und Forge Merchantman
|
- **Synthwave Sunset als zehnter Built-In.** Hot Magenta + Cyan auf Mitternachts-Violett,
|
||||||
werden Carla Beleandis als Community-Geste zugeschrieben.
|
80s-Neon-Grid-Vibes für Late-Night-Raids
|
||||||
|
- **Author-Credits konsolidiert.** Brand-Themes laufen jetzt unter „Hellion Forge". Mint Grove und
|
||||||
|
Forge Merchantman werden Carla Beleandis als Community-Geste zugeschrieben.
|
||||||
|
|
||||||
Keine Schema-Bumps, keine User-sichtbaren Funktions- Änderungen außer dass die Frames in Theme-getrieben rendernden
|
Keine Schema-Bumps, keine User-sichtbaren Funktions- Änderungen außer dass die Frames in
|
||||||
Szenen merklich glatter laufen und ein neues Theme im Picker steht.
|
Theme-getrieben rendernden Szenen merklich glatter laufen und ein neues Theme im Picker steht.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
subtitle: Symbol-Picker und Tell-History Fix
|
||||||
|
versionsnatur: Feature-Patch + Hotfix
|
||||||
|
---
|
||||||
|
|
||||||
|
- Symbol-Picker im Chat-Eingang: ein kleiner Smile-Button links neben dem Kanal-Indikator öffnet ein
|
||||||
|
Popup mit zwei Tabs. Der erste listet alle 161 FFXIV-PUA-Glyphen (Dalamuds SeIconChar); der zweite
|
||||||
|
trägt 97 verifizierte BMP-Symbole (Latin-Marken, Währungen, das ganze griechische Alphabet,
|
||||||
|
Geometrie, Spielkarten, Noten) — jedes davon über `/echo` und `/say` in einer vierrundigen
|
||||||
|
Whitelist-Probe durchgereicht, damit der Channel-Render dem entspricht, was der Picker anzeigt.
|
||||||
|
Klick fügt das Symbol an der Cursor-Position ein, Multi-Insert lässt das Popup offen, eine
|
||||||
|
Recent-Used-Leiste zeigt die letzten sechzehn Picks über beide Tabs. Toggle in Settings → Chat →
|
||||||
|
Nachrichten-Verhalten, Default an.
|
||||||
|
- Verlauf in angepinnten Tell-Tabs lädt wieder vollständig: ein versteckter 500-Zeilen-Scan-Cap in
|
||||||
|
PreloadHistory hat das User-Setting `AutoTellTabsHistoryPreload` überschrieben, wodurch
|
||||||
|
weniger-frequente Tell-Partner ihren Backlog verloren haben sobald die Scan-Schicht mit anderen
|
||||||
|
Chat-Partnern voll lief. Cap ist raus, der Index auf `(Receiver, Date)` hält die Query schnell.
|
||||||
|
- Slash-Command-Teardown: /hellion, /hellionView, /hellionDebugger (und im Debug-Build
|
||||||
|
/hellionSeString) sind als private Felder gecached. Plugin-Dispose detached die echte
|
||||||
|
Registrierung, statt mit identischen Args neu zu registrieren — schließt eine latente
|
||||||
|
Wartungs-Falle aus v1.4.9.
|
||||||
|
- v1.4.x-Polish-Sweep endet hier. Der ImGuiListClipper-Refactor von der v1.4.10-Reserve-Liste wurde
|
||||||
|
gecancelt, nachdem der Cross- Plattform-Smoke gezeigt hat dass das Scroll-Gummi ein Wine/Linux-
|
||||||
|
Quirk ist — Windows-User haben es nie gesehen. Spike dafür kommt in einem späteren Patch. Nächster
|
||||||
|
Major-Cycle ist v1.5.0 mit der DI-Container-Adoption (`Microsoft.Extensions.Hosting` +
|
||||||
|
`ILogger<T>`) nach dem Lightless-Vorbild.
|
||||||
|
- Migration v17 unverändert: kein Schema-Bump, kein Config-Migrations-Aufwand.
|
||||||
@@ -5,25 +5,27 @@ versionsnatur: Performance-Patch
|
|||||||
|
|
||||||
**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**
|
**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**
|
||||||
|
|
||||||
Dritter Sub-Patch der v1.4.x Polish-Sweep-Serie. Drei Per-Frame-Allokations-Quellen aus dem ChatLogWindow-Render- Pfad
|
Dritter Sub-Patch der v1.4.x Polish-Sweep-Serie. Drei Per-Frame-Allokations-Quellen aus dem
|
||||||
und der Settings-StatusBar eliminiert.
|
ChatLogWindow-Render- Pfad und der Settings-StatusBar eliminiert.
|
||||||
|
|
||||||
- **Card-Mode-Border-Loop entlastet.** DrawMessages hebt Theme, DrawList, Window-Left, Window-Right und die ABGR-
|
- **Card-Mode-Border-Loop entlastet.** DrawMessages hebt Theme, DrawList, Window-Left, Window-Right
|
||||||
Border-Color einmalig vor den Per-Message-Loop. Bei 100 sichtbaren Messages sind das gut 500 redundante P/Invokes und
|
und die ABGR- Border-Color einmalig vor den Per-Message-Loop. Bei 100 sichtbaren Messages sind das
|
||||||
Property-Reads, die der Hoist eliminiert. Pop-Out- Heavy-Setups (mehrere parallele Chat-Windows) profitieren
|
gut 500 redundante P/Invokes und Property-Reads, die der Hoist eliminiert. Pop-Out- Heavy-Setups
|
||||||
proportional, weil der Hoist pro DrawMessages-Call greift, also pro Window
|
(mehrere parallele Chat-Windows) profitieren proportional, weil der Hoist pro DrawMessages-Call
|
||||||
- **Auto-Tell Tab-Tint und Icon gecached.** Die Hash-Color- Berechnung für Auto-Tell-Tabs lief pro Tab pro Frame, mit
|
greift, also pro Window
|
||||||
zwei String-Allokationen pro Tab (eine für Tint-Hash, eine für Icon-Hash). Der neue TabTintCache liest pre-computed
|
- **Auto-Tell Tab-Tint und Icon gecached.** Die Hash-Color- Berechnung für Auto-Tell-Tabs lief pro
|
||||||
Werte aus dem Tab und rechnet nur neu wenn das Tell-Target drifted. Beide Caches haben separate Validation-Keys, also
|
Tab pro Frame, mit zwei String-Allokationen pro Tab (eine für Tint-Hash, eine für Icon-Hash). Der
|
||||||
keine Cross-Invalidation zwischen Tint- und Icon-Pfad. AutoTellTabTint selbst bleibt pure Hash-Helper, weiterhin ohne
|
neue TabTintCache liest pre-computed Werte aus dem Tab und rechnet nur neu wenn das Tell-Target
|
||||||
Tab-Awareness
|
drifted. Beide Caches haben separate Validation-Keys, also keine Cross-Invalidation zwischen Tint-
|
||||||
- **StatusBar-Aggregation hinter Cache-Gate.** Die Status- Leiste am unteren Window-Rand summiert die Tab-Message-
|
und Icon-Pfad. AutoTellTabTint selbst bleibt pure Hash-Helper, weiterhin ohne Tab-Awareness
|
||||||
Counts und zählt die Auto-Tell-Tabs pro Frame. Der Cache- Gate (1 Sekunde) lag bisher hinter den LINQ-Pfaden, also
|
- **StatusBar-Aggregation hinter Cache-Gate.** Die Status- Leiste am unteren Window-Rand summiert
|
||||||
liefen Sum und Count trotzdem pro Frame. Jetzt vor dem Gate, plus die LINQ-Pfade durch eine Single-Pass-Foreach
|
die Tab-Message- Counts und zählt die Auto-Tell-Tabs pro Frame. Der Cache- Gate (1 Sekunde) lag
|
||||||
ersetzt. Die Aggregation läuft auf etwa 1 % der Frames
|
bisher hinter den LINQ-Pfaden, also liefen Sum und Count trotzdem pro Frame. Jetzt vor dem Gate,
|
||||||
|
plus die LINQ-Pfade durch eine Single-Pass-Foreach ersetzt. Die Aggregation läuft auf etwa 1 % der
|
||||||
|
Frames
|
||||||
|
|
||||||
Realistische Frame-Time-Recovery: 2-5 % in typischen Szenen, Pop-Out-Heavy-Setups potenziell mehr durch die Card-Border-
|
Realistische Frame-Time-Recovery: 2-5 % in typischen Szenen, Pop-Out-Heavy-Setups potenziell mehr
|
||||||
Multiplikation pro Window.
|
durch die Card-Border- Multiplikation pro Window.
|
||||||
|
|
||||||
Keine Schema-Bumps, keine User-sichtbaren Funktions- Änderungen außer dass die Frames im Chat-Log und in der
|
Keine Schema-Bumps, keine User-sichtbaren Funktions- Änderungen außer dass die Frames im Chat-Log
|
||||||
Settings-Statusleiste merklich glatter laufen.
|
und in der Settings-Statusleiste merklich glatter laufen.
|
||||||
|
|||||||
@@ -5,25 +5,29 @@ versionsnatur: Architecture-Refactor
|
|||||||
|
|
||||||
**Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover**
|
**Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover**
|
||||||
|
|
||||||
Vierter Sub-Patch der v1.4.x Polish-Sweep-Serie. Plugin- Lifecycle auf Dalamud's `IAsyncDalamudPlugin`-API migriert und
|
Vierter Sub-Patch der v1.4.x Polish-Sweep-Serie. Plugin- Lifecycle auf Dalamud's
|
||||||
das Custom-Repo zieht von GitHub auf Gitea um.
|
`IAsyncDalamudPlugin`-API migriert und das Custom-Repo zieht von GitHub auf Gitea um.
|
||||||
|
|
||||||
- **Async-Plugin-Architektur.** Konstruktor übernimmt nur noch die Bootstrap-Essentials (Config-Load, Language-Init,
|
- **Async-Plugin-Architektur.** Konstruktor übernimmt nur noch die Bootstrap-Essentials
|
||||||
Conflict-Detection). Migrationen, Service-Allokationen, Window-Konstruktion und Hook-Subscription wandern in
|
(Config-Load, Language-Init, Conflict-Detection). Migrationen, Service-Allokationen,
|
||||||
LoadAsync, sodass Dalamud die UI während der schweren Arbeit responsive halten kann. Per-Line-CaptureFailure in
|
Window-Konstruktion und Hook-Subscription wandern in LoadAsync, sodass Dalamud die UI während der
|
||||||
DisposeAsync mirrort LightlessSync's Pattern, plus Idempotency-Guard gegen Reload-Races
|
schweren Arbeit responsive halten kann. Per-Line-CaptureFailure in DisposeAsync mirrort
|
||||||
- **Custom-Repo-URL umgezogen auf Gitea.** Bestehende Tester müssen einmalig in XIVLauncher die Custom-Repo-URL auf
|
LightlessSync's Pattern, plus Idempotency-Guard gegen Reload-Races
|
||||||
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json` umstellen, dann
|
- **Custom-Repo-URL umgezogen auf Gitea.** Bestehende Tester müssen einmalig in XIVLauncher die
|
||||||
XIVLauncher neu starten. Das alte GitHub-Repo bleibt als eingefrorener v1.4.2-Snapshot stehen und wird nicht mehr
|
Custom-Repo-URL auf
|
||||||
aktualisiert
|
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||||
- **Schema-Gate statt Migrations-Kette.** Die v9 → v16 Migrationen sind raus, ersetzt durch einen harten Schema-Check in
|
umstellen, dann XIVLauncher neu starten. Das alte GitHub-Repo bleibt als eingefrorener
|
||||||
Phase 1. Configs auf Schema v16+ laden direkt; ältere Configs (vor v1.2.1) bekommen jetzt eine klare „install v1.4.2
|
v1.4.2-Snapshot stehen und wird nicht mehr aktualisiert
|
||||||
first"-Fehlermeldung statt eines impliziten Migrations-Pfads
|
- **Schema-Gate statt Migrations-Kette.** Die v9 → v16 Migrationen sind raus, ersetzt durch einen
|
||||||
- **AutoTranslate-Cache läuft im Hintergrund.** Der Cache füllt sich jetzt fire-and-forget statt blockierend im
|
harten Schema-Check in Phase 1. Configs auf Schema v16+ laden direkt; ältere Configs (vor v1.2.1)
|
||||||
Plugin-Load. Trade-off: die erste Auto-Translate-Nutzung einer Session kann einen kurzen Hitch haben, dafür kein
|
bekommen jetzt eine klare „install v1.4.2 first"-Fehlermeldung statt eines impliziten
|
||||||
300-ms-Block beim Plugin-Start
|
Migrations-Pfads
|
||||||
- **Plugin-Load-Zeit ehrlich.** Median 3,7 s über fünf Reloads, vergleichbar mit v1.4.2. Der Async-Refactor ist
|
- **AutoTranslate-Cache läuft im Hintergrund.** Der Cache füllt sich jetzt fire-and-forget statt
|
||||||
Foundation für künftige Lazy-Init-Optimierungen (v1.4.4) und Code-Architektur-Hygiene, kein direkter User-spürbarer
|
blockierend im Plugin-Load. Trade-off: die erste Auto-Translate-Nutzung einer Session kann einen
|
||||||
Speed-Win in dieser Release
|
kurzen Hitch haben, dafür kein 300-ms-Block beim Plugin-Start
|
||||||
|
- **Plugin-Load-Zeit ehrlich.** Median 3,7 s über fünf Reloads, vergleichbar mit v1.4.2. Der
|
||||||
|
Async-Refactor ist Foundation für künftige Lazy-Init-Optimierungen (v1.4.4) und
|
||||||
|
Code-Architektur-Hygiene, kein direkter User-spürbarer Speed-Win in dieser Release
|
||||||
|
|
||||||
Keine User-sichtbaren Funktions-Änderungen außer dem Repo-URL-Update. Settings, Themes und Tabs bleiben unangetastet.
|
Keine User-sichtbaren Funktions-Änderungen außer dem Repo-URL-Update. Settings, Themes und Tabs
|
||||||
|
bleiben unangetastet.
|
||||||
|
|||||||
@@ -5,30 +5,33 @@ versionsnatur: Wartung und Robustheit
|
|||||||
|
|
||||||
**Hellion Chat 1.4.4 — Threading- und IPC-Sicherheits-Politur**
|
**Hellion Chat 1.4.4 — Threading- und IPC-Sicherheits-Politur**
|
||||||
|
|
||||||
Fünfter Sub-Patch der v1.4.x Polish-Sweep-Serie. Threading-Annahmen werden explizit pro Methode dokumentiert, ein
|
Fünfter Sub-Patch der v1.4.x Polish-Sweep-Serie. Threading-Annahmen werden explizit pro Methode
|
||||||
Hot-Path-Lock im Auto-Tell-Tab-Counter fällt weg, IPC-Cleanup wird sichtbar wenn er fehlschlägt und der Privacy-Filter
|
dokumentiert, ein Hot-Path-Lock im Auto-Tell-Tab-Counter fällt weg, IPC-Cleanup wird sichtbar wenn
|
||||||
spricht jetzt bei unbekannten ChatTypes.
|
er fehlschlägt und der Privacy-Filter spricht jetzt bei unbekannten ChatTypes.
|
||||||
|
|
||||||
- **AutoTellTabsService Hot-Path-Lock entfernt.** `ActiveTempTabCount` hat bisher pro Render-Frame ein LINQ-Count unter
|
- **AutoTellTabsService Hot-Path-Lock entfernt.** `ActiveTempTabCount` hat bisher pro Render-Frame
|
||||||
einem Lock gemacht. Jetzt läuft das über einen Interlocked-Counter der parallel zur Tabs-Liste mitgeführt wird,
|
ein LINQ-Count unter einem Lock gemacht. Jetzt läuft das über einen Interlocked-Counter der
|
||||||
inklusive Resync-Hook für den Snapshot-Restore-Pfad in `SaveConfig`. Plus Pure-Helper-Test-Mirror in der Build-Suite
|
parallel zur Tabs-Liste mitgeführt wird, inklusive Resync-Hook für den Snapshot-Restore-Pfad in
|
||||||
damit die Atomicity-Semantik nicht versehentlich wegrefactored wird
|
`SaveConfig`. Plus Pure-Helper-Test-Mirror in der Build-Suite damit die Atomicity-Semantik nicht
|
||||||
- **HonorificService selbst-dokumentierende Threading-Banner.** Statt eines Block-Comments am Klassen-Ende hat jede
|
versehentlich wegrefactored wird
|
||||||
IPC-Callback-Methode jetzt einen 1-Zeilen-Banner darüber, der den Thread-Kontext direkt am Call-Site benennt
|
- **HonorificService selbst-dokumentierende Threading-Banner.** Statt eines Block-Comments am
|
||||||
(framework only, framework scheduled, any). Mehr Hilfe für künftige Reviews als ein abstraktes Threading-Kapitel
|
Klassen-Ende hat jede IPC-Callback-Methode jetzt einen 1-Zeilen-Banner darüber, der den
|
||||||
- **Unsubscribe-Failure ist jetzt sichtbar.** `TryUnsubscribe` hat ein Honorific-Unsubscribe-Failure bisher als Debug
|
Thread-Kontext direkt am Call-Site benennt (framework only, framework scheduled, any). Mehr Hilfe
|
||||||
geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription kann den Service über Plugin-Reloads
|
für künftige Reviews als ein abstraktes Threading-Kapitel
|
||||||
hinweg leben lassen, also läuft der Log jetzt auf Warning
|
- **Unsubscribe-Failure ist jetzt sichtbar.** `TryUnsubscribe` hat ein Honorific-Unsubscribe-Failure
|
||||||
- **AutoTranslate-Warmup blockiert den Plugin-Unload nicht mehr.** Der Cache-Warmup-Thread war ohne `IsBackground=true`
|
bisher als Debug geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription
|
||||||
unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu MessageManager und RetentionSweep (beide
|
kann den Service über Plugin-Reloads hinweg leben lassen, also läuft der Log jetzt auf Warning
|
||||||
seit v1.4.0)
|
- **AutoTranslate-Warmup blockiert den Plugin-Unload nicht mehr.** Der Cache-Warmup-Thread war ohne
|
||||||
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType einführt der weder in
|
`IsBackground=true` unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu
|
||||||
der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der Filter
|
MessageManager und RetentionSweep (beide seit v1.4.0)
|
||||||
einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet, also kein
|
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType
|
||||||
Log-Spam
|
einführt der weder in der Whitelist noch in den Defaults steht, wird er bisher silent durch den
|
||||||
- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen Configs jetzt auf `true`,
|
Failsafe geleitet. Jetzt loggt der Filter einmalig pro Runtime eine Warning mit dem Type und dem
|
||||||
damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt wird bevor der User entscheiden kann. Bestehende
|
Failsafe-Wert. Dedup über ein NonSerialized-HashSet, also kein Log-Spam
|
||||||
Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein Schema-Bump
|
- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen
|
||||||
|
Configs jetzt auf `true`, damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt
|
||||||
|
wird bevor der User entscheiden kann. Bestehende Configs behalten ihre Wahl, weil der Deserializer
|
||||||
|
den Initializer überschreibt. Keine Migration, kein Schema-Bump
|
||||||
|
|
||||||
Keine User-sichtbaren Funktions-Änderungen außer dem Default-Flip für neue Installationen. Settings, Themes, Tabs und
|
Keine User-sichtbaren Funktions-Änderungen außer dem Default-Flip für neue Installationen. Settings,
|
||||||
das Privacy-Verhalten für Bestand bleiben unangetastet.
|
Themes, Tabs und das Privacy-Verhalten für Bestand bleiben unangetastet.
|
||||||
|
|||||||
@@ -5,24 +5,27 @@ versionsnatur: UX-Polish-Cycle
|
|||||||
|
|
||||||
**Hellion Chat 1.4.5 — UX und Robustheit**
|
**Hellion Chat 1.4.5 — UX und Robustheit**
|
||||||
|
|
||||||
Sechster Sub-Patch der v1.4.x Polish-Sweep-Serie. Render-Fehler im Chat-Fenster werden jetzt sichtbar, der
|
Sechster Sub-Patch der v1.4.x Polish-Sweep-Serie. Render-Fehler im Chat-Fenster werden jetzt
|
||||||
First-Run-Wizard hat eine explizite Cancel-Schaltfläche, der Eingabe-Verlauf bleibt nicht mehr über Plugin-Reloads
|
sichtbar, der First-Run-Wizard hat eine explizite Cancel-Schaltfläche, der Eingabe-Verlauf bleibt
|
||||||
hinweg liegen, und die Statusleiste klippt in schmalen Fenstern nicht mehr.
|
nicht mehr über Plugin-Reloads hinweg liegen, und die Statusleiste klippt in schmalen Fenstern nicht
|
||||||
|
mehr.
|
||||||
|
|
||||||
- **Fehler-Benachrichtigung im Chat-Fenster.** Wenn ein Render-Fehler in `DrawChatLog` auftritt, zeigt das Plugin jetzt
|
- **Fehler-Benachrichtigung im Chat-Fenster.** Wenn ein Render-Fehler in `DrawChatLog` auftritt,
|
||||||
eine einmalige Warning-Notification mit Verweis aufs `/xllog`, statt das Fenster stillschweigend leer zu lassen. Der
|
zeigt das Plugin jetzt eine einmalige Warning-Notification mit Verweis aufs `/xllog`, statt das
|
||||||
Stack-Trace selbst geht weiter via `Plugin.Log.Error` ins Logfile. De-Dup über Per-Session-Bool, damit ein
|
Fenster stillschweigend leer zu lassen. Der Stack-Trace selbst geht weiter via `Plugin.Log.Error`
|
||||||
wiederkehrender Fehler die Notification-Stack nicht pro Frame neu vollkippt
|
ins Logfile. De-Dup über Per-Session-Bool, damit ein wiederkehrender Fehler die Notification-Stack
|
||||||
- **First-Run-Wizard trennt Accept und Close.** `OnClose` setzt nicht mehr stillschweigend `FirstRunCompleted=true`,
|
nicht pro Frame neu vollkippt
|
||||||
also lässt das X den Wizard schwebend zurück und er kommt beim nächsten Plugin-Reload wieder. Eine neue „Später —
|
- **First-Run-Wizard trennt Accept und Close.** `OnClose` setzt nicht mehr stillschweigend
|
||||||
Defaults behalten"-Schaltfläche im Footer ist der explizite Weg, ohne Profil-Auswahl rauszukommen. Strings bilingual
|
`FirstRunCompleted=true`, also lässt das X den Wizard schwebend zurück und er kommt beim nächsten
|
||||||
EN+DE plus Tooltip
|
Plugin-Reload wieder. Eine neue „Später — Defaults behalten"-Schaltfläche im Footer ist der
|
||||||
- **Eingabe-Verlauf wird beim Plugin-Reload geleert.** `InputHistoryService.Reset` hängt jetzt in `Plugin.DisposeAsync`
|
explizite Weg, ohne Profil-Auswahl rauszukommen. Strings bilingual EN+DE plus Tooltip
|
||||||
neben den anderen Pure-Memory-Cleanups, damit der statische Zustand aus der vorigen Session den nächsten Load nicht
|
- **Eingabe-Verlauf wird beim Plugin-Reload geleert.** `InputHistoryService.Reset` hängt jetzt in
|
||||||
mehr erbt
|
`Plugin.DisposeAsync` neben den anderen Pure-Memory-Cleanups, damit der statische Zustand aus der
|
||||||
- **Statusleiste klippt nicht mehr.** Der rechtsbündige Versions-Slot wird ausgeblendet wenn die Chat-Window-Breite
|
vorigen Session den nächsten Load nicht mehr erbt
|
||||||
abzüglich Versions-Text unter 200 px fällt — vorher überlappte er die vier linken Slots. Ab ausreichender Breite
|
- **Statusleiste klippt nicht mehr.** Der rechtsbündige Versions-Slot wird ausgeblendet wenn die
|
||||||
taucht der Slot wieder auf
|
Chat-Window-Breite abzüglich Versions-Text unter 200 px fällt — vorher überlappte er die vier
|
||||||
- **Intern:** `FontManager` fällt auf System-Font zurück wenn die eingebettete Hellion-Font-Resource fehlt
|
linken Slots. Ab ausreichender Breite taucht der Slot wieder auf
|
||||||
(Broken-csproj-Pfad, nie ein Produktions-Build), plus expliziter Session-Only-Invariant-Kommentar für Auto-Tell-Tabs
|
- **Intern:** `FontManager` fällt auf System-Font zurück wenn die eingebettete Hellion-Font-Resource
|
||||||
in `Plugin.cs:167-168` mit einem TempTabCounter-Init-Pin in der Build-Suite. Kein Schema-Bump, keine Migration
|
fehlt (Broken-csproj-Pfad, nie ein Produktions-Build), plus expliziter
|
||||||
|
Session-Only-Invariant-Kommentar für Auto-Tell-Tabs in `Plugin.cs:167-168` mit einem
|
||||||
|
TempTabCounter-Init-Pin in der Build-Suite. Kein Schema-Bump, keine Migration
|
||||||
|
|||||||
@@ -3,16 +3,34 @@ subtitle: Code Hygiene and Refactor
|
|||||||
versionsnatur: Maintenance-Cycle
|
versionsnatur: Maintenance-Cycle
|
||||||
---
|
---
|
||||||
|
|
||||||
Wartungs-Patch ohne User-sichtbare Änderungen. Saubere Code-Basis als Vorbereitung auf das v1.4.7-Backlog-Cleanup, plus zwei geerbte Bugfixes aus dem ChatTwo-Upstream `f35b7d3`.
|
Wartungs-Patch ohne User-sichtbare Änderungen. Saubere Code-Basis als Vorbereitung auf das
|
||||||
|
v1.4.7-Backlog-Cleanup, plus zwei geerbte Bugfixes aus dem ChatTwo-Upstream `f35b7d3`.
|
||||||
|
|
||||||
- **preflight.sh härter**: csharpier-Reflow-Check (Block E) und markdownlint (Block F) laufen jetzt im Pre-Push-Gate, statt erst beim Pre-Merge-Review aufzufallen.
|
- **preflight.sh härter**: csharpier-Reflow-Check (Block E) und markdownlint (Block F) laufen jetzt
|
||||||
- **FontManager-Fallback robuster**: Atlas-Toolkit-Throws aus kaputten Font-Configs (IO, InvalidOperation, ArgumentException) fallen jetzt zuverlässig auf NotoSansCjkRegular, statt den Atlas-Build mitzureißen. Der Exception-Typ wird im Log mitgegeben für die Diagnose.
|
im Pre-Push-Gate, statt erst beim Pre-Merge-Review aufzufallen.
|
||||||
- **URL-Validation beim Plugin-Load**: BrandingLinks (5 URLs) und IntegrationLinks (2 URLs) werden via `[ModuleInitializer]` geprüft. Ein Tippfehler bei einer künftigen URL-Rotation wirft jetzt sofort beim Plugin-Load, statt still beim Klick zu scheitern.
|
- **FontManager-Fallback robuster**: Atlas-Toolkit-Throws aus kaputten Font-Configs (IO,
|
||||||
- **Cherry-Pick aus ChatTwo `f35b7d3`** — Memory-Leak in `Chat.SetChannel`: der native `Utf8String` wird jetzt auch dann freigegeben, wenn der Linkshell-Check den Channel ablehnt (vorher gefangen im early-return).
|
InvalidOperation, ArgumentException) fallen jetzt zuverlässig auf NotoSansCjkRegular, statt den
|
||||||
- **Cherry-Pick aus ChatTwo `f35b7d3`** — `Tab.Clone()` Deep-cloned jetzt `UsedChannel` und `TellTarget`. Vorher Reference-Share-Bug: PopOut- und Temp-Tabs mutierten sich gegenseitig.
|
Atlas-Build mitzureißen. Der Exception-Typ wird im Log mitgegeben für die Diagnose.
|
||||||
- **Aktive-Tab-Underline pixel-perfect bei DPI-Scaling**: Die Underline-Pill skaliert jetzt mit `ImGuiHelpers.GlobalScale` und rundet die DrawList-Koordinaten auf physische Pixel. Kein Sub-Pixel-Blur mehr auf 125/150%-Setups.
|
- **URL-Validation beim Plugin-Load**: BrandingLinks (5 URLs) und IntegrationLinks (2 URLs) werden
|
||||||
- **IconButton-Width-Fix**: der manuelle `width - 2 * CellPadding.X`-Subtract verlor den HUD-Scale (Padding skaliert, der raw int nicht). Gemessene Breite läuft jetzt unverändert durch.
|
via `[ModuleInitializer]` geprüft. Ein Tippfehler bei einer künftigen URL-Rotation wirft jetzt
|
||||||
- **Test-Isolation für MessageStore**: `Dalamud.Utility.Util`-Surface (IsWine, OpenLink) läuft jetzt durch eine `IPlatformUtil`-Indirektion. MessageStores `IsWine`-Probe ist isoliert testbar in der Build-Suite. Plus: HellionStyle-ChildBgAlpha als Pure-Helper extrahiert, Plugin.SaveConfig kopiert nur Session-Tabs statt der ganzen Tab-Liste, SettingsOverview cached den DrawList einmal pro Frame.
|
sofort beim Plugin-Load, statt still beim Klick zu scheitern.
|
||||||
- **Built-in-Theme-Roster**: Crystal Nocturne (Royal Sapphire + Electric Magenta auf Obsidian, von CRYSTALLITE) ersetzt Moonlit Bloom. User mit Moonlit Bloom als aktivem Theme fallen beim ersten Plugin-Load auf Hellion Arctic zurück.
|
- **Cherry-Pick aus ChatTwo `f35b7d3`** — Memory-Leak in `Chat.SetChannel`: der native `Utf8String`
|
||||||
|
wird jetzt auch dann freigegeben, wenn der Linkshell-Check den Channel ablehnt (vorher gefangen im
|
||||||
|
early-return).
|
||||||
|
- **Cherry-Pick aus ChatTwo `f35b7d3`** — `Tab.Clone()` Deep-cloned jetzt `UsedChannel` und
|
||||||
|
`TellTarget`. Vorher Reference-Share-Bug: PopOut- und Temp-Tabs mutierten sich gegenseitig.
|
||||||
|
- **Aktive-Tab-Underline pixel-perfect bei DPI-Scaling**: Die Underline-Pill skaliert jetzt mit
|
||||||
|
`ImGuiHelpers.GlobalScale` und rundet die DrawList-Koordinaten auf physische Pixel. Kein
|
||||||
|
Sub-Pixel-Blur mehr auf 125/150%-Setups.
|
||||||
|
- **IconButton-Width-Fix**: der manuelle `width - 2 * CellPadding.X`-Subtract verlor den HUD-Scale
|
||||||
|
(Padding skaliert, der raw int nicht). Gemessene Breite läuft jetzt unverändert durch.
|
||||||
|
- **Test-Isolation für MessageStore**: `Dalamud.Utility.Util`-Surface (IsWine, OpenLink) läuft jetzt
|
||||||
|
durch eine `IPlatformUtil`-Indirektion. MessageStores `IsWine`-Probe ist isoliert testbar in der
|
||||||
|
Build-Suite. Plus: HellionStyle-ChildBgAlpha als Pure-Helper extrahiert, Plugin.SaveConfig kopiert
|
||||||
|
nur Session-Tabs statt der ganzen Tab-Liste, SettingsOverview cached den DrawList einmal pro
|
||||||
|
Frame.
|
||||||
|
- **Built-in-Theme-Roster**: Crystal Nocturne (Royal Sapphire + Electric Magenta auf Obsidian, von
|
||||||
|
CRYSTALLITE) ersetzt Moonlit Bloom. User mit Moonlit Bloom als aktivem Theme fallen beim ersten
|
||||||
|
Plugin-Load auf Hellion Arctic zurück.
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
subtitle: Backlog Cleanup and Mid-Features
|
||||||
|
versionsnatur: Mid-Feature-Patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Achter Sub-Patch der v1.4.x Polish-Sweep-Serie. Erstes User-sichtbares Feature-Bundle seit v1.4.5 —
|
||||||
|
angepinnte Tell-Tabs die Relog überleben, opt-in Honorific-Glow, plus eine konfigurierbare Sidebar.
|
||||||
|
|
||||||
|
- **TempTell anpinnen**: Rechtsklick auf einen TempTell-Tab in der Sidebar → „Tab anpinnen".
|
||||||
|
Angepinnte Tabs überleben Plugin-Reload und Char-Logout, behalten ihre Konversations-Historie
|
||||||
|
(wird beim Rehydrate aus dem MessageStore nachgeladen) und bleiben an die gleiche /tell-Person
|
||||||
|
gebunden. Hard-Cap 5 angepinnte Tabs in einem separaten Pool — die normalen Auto-Tell-Tabs (15er
|
||||||
|
Cap) sind davon entkoppelt, Gesamt-Decke 20. Die Sidebar gruppiert angepinnte Tabs in einer
|
||||||
|
eigenen „Angepinnt"-Sektion mit eigenem Trenner.
|
||||||
|
- **Honorific Glow-Outline**: rendert jetzt eine 8-Richtungs-DrawList-Outline wenn der
|
||||||
|
Honorific-Titel eine Glow-Farbe trägt. Opt-in via **Settings → Integrationen → Glow-Outline
|
||||||
|
rendern (Honorific)** (Default OFF). Gradient (Color3 / GradientColourSet / Wave / Pulse) wird
|
||||||
|
geparst und im DTO weitergereicht, rendert aktuell aber statisch als Primärfarbe — der volle
|
||||||
|
Gradient-Port (Animations-Algorithmus + Pride-Palette) kommt als eigener Cycle nach.
|
||||||
|
- **Sidebar-Breite konfigurierbar**: in **Theme & Layout** ein Slider 44–160 px. Default bleibt 44
|
||||||
|
px (icon-only), aber breiter machen damit Sektion-Header wie „Aktive Tells (3)" oder „Angepinnt
|
||||||
|
(2)" nicht abgeschnitten werden.
|
||||||
|
- **Settings-Save Channel-Fix**: ein Save mit aktivem Party- oder Linkshell-Tab konnte den
|
||||||
|
Chat-Input zurück auf `/tell <angepinnte Person>` springen lassen. `Configuration.UpdateFrom`
|
||||||
|
bewahrt jetzt den Runtime-`CurrentChannel` über den persistent-Tab-Merge hinweg, und `TabSwitched`
|
||||||
|
deep-cloned den Seed-Channel statt sich den `UsedChannel` mit dem vorigen Tab zu teilen.
|
||||||
|
- **Internal**: `IPluginLogProxy`-Indirektion vor Dalamud's `IPluginLog` über alle ~91
|
||||||
|
`Plugin.Log`-Call-Sites. Damit läuft `MessageStore.Migrate0` voll-isoliert in xUnit (F12.1-Lücke
|
||||||
|
aus v1.4.6 geschlossen). Plus: TempTab-Counter als abgeleitete Property statt gecachtes
|
||||||
|
Interlocked-Feld — die neuen Pin/Unpin-Übergänge sind Cold-Path, kein Lock-Free-Vorteil mehr.
|
||||||
|
Migration v16 → v17 ist rein additiv (neues `Tab.IsPinned`-Bool, Default false).
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
subtitle: Hook-Layer und Polish-Quick-Wins
|
||||||
|
versionsnatur: Polish-Patch
|
||||||
|
---
|
||||||
|
|
||||||
|
- DbViewer Volltext-Suche: optionaler FTS5-Index über die ganze Chat-Historie. Wird beim ersten
|
||||||
|
v1.4.8-Start asynchron im Hintergrund gebaut, Progress als Toast. Lokale Page-Suche bleibt
|
||||||
|
Default. Such-Eingaben werden als exakte Wortfolge gematcht; mehrere Wörter werden nur gefunden,
|
||||||
|
wenn sie zusammen und in der Reihenfolge stehen. Wer rohe FTS5-MATCH-Syntax nutzen will, setzt
|
||||||
|
eigene Anführungszeichen um den Suchbegriff.
|
||||||
|
- Custom-Theme-Files laden sich beim Speichern automatisch neu, wenn das Theme aktiv ist. Kein
|
||||||
|
Picker-Klick mehr nötig.
|
||||||
|
- Retention-Sweep blockt nicht mehr den Framework-Thread. Der Mini-Hitch von ~194ms pro Sweep ist
|
||||||
|
weg.
|
||||||
|
- Statusleiste rendert sauber bei Windows-Skalierung über 100%.
|
||||||
|
- Receive-Suppressed-Tells-Routing wurde in diesem Cycle untersucht und auf v1.5.x verschoben: wenn
|
||||||
|
andere Plugins Tells via CheckMessageHandled unterdrücken, überspringt FFXIVs Chat-Pipeline den
|
||||||
|
RaptureLogModule-Resolver und HellionChats Tab-Routing verliert den Tell-Partner. Der Fix liegt
|
||||||
|
architektonisch neben dem geplanten Ad-Block-Hook-Layer und kommt dort mit.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
subtitle: Plugin-Load Render Polish
|
||||||
|
versionsnatur: Performance-Patch
|
||||||
|
---
|
||||||
|
|
||||||
|
- First-Frame-HITCH unter 100 ms: der erste Render-Frame des Plugins liegt jetzt bei ~76 ms Median
|
||||||
|
(vorher ~127 ms), die Dalamud-Warnung „UiBuilder(Hellion Chat) > 100ms" beim Plugin-Start ist
|
||||||
|
damit weg. Erreicht durch das Verlagern von sechs nicht-essentiellen Render- Sektionen
|
||||||
|
(Statusleiste, Kanalname-Chunks, Fenster-Bounds-Check, Hinweis-Banner, Autocomplete,
|
||||||
|
Input-Preview) auf den zweiten Frame. Bei 60 fps sieht man die deferred-Sektionen ~17 ms später,
|
||||||
|
was im Atlas-Build-Fenster nach einem Reload unsichtbar bleibt.
|
||||||
|
- Slash-Commands zentral registriert: /hellion, /hellionView, /hellionSeString und /hellionDebugger
|
||||||
|
werden jetzt im Plugin-Load zentral registriert statt erst beim ersten Öffnen ihres Ziel-Fensters.
|
||||||
|
Heißt: die Befehle funktionieren ab dem ersten Tick, auch wenn das jeweilige Fenster nie geöffnet
|
||||||
|
wurde. Der „Einstellungen"-Button im Plugin-Manager hängt am selben Pfad.
|
||||||
|
- Plugin-Load-Diagnose-Logs als Tripwire: die Profiling-Logs für MessageStore.Connect,
|
||||||
|
MessageStore.Migrate, FilterAllTabs und den Auto-Translate-Warmup bleiben auf Information-Level
|
||||||
|
eingeschaltet. Falls eine zukünftige Änderung die Lade-Zeit wieder über 100 ms drückt, taucht der
|
||||||
|
Mehrverbrauch direkt im /xllog auf, ohne dass jemand erst den Debug-Filter einschalten muss.
|
||||||
|
- ChatTwo-IPC-Kompatibilitäts-Layer: HellionChat spiegelt jetzt die komplette ChatTwo-IPC-Surface
|
||||||
|
(`GetChatInputState`, `ChatInputStateChanged`, `Register`, `Unregister`, `Available`, `Invoke`)
|
||||||
|
zusätzlich zu unseren eigenen `HellionChat.*`-Gates unter dem `ChatTwo.*`-Namensraum. Drittseitige
|
||||||
|
Integrationen die nur auf ChatTwo's IPC reagieren, etwa die Kontextmenü-Hooks von Artisan und
|
||||||
|
AllaganTools, funktionieren damit weiter ohne Code-Änderung auf ihrer Seite. Die
|
||||||
|
Conflict-Detection blockiert das parallele Laden von ChatTwo, daher kein Namensraum-Konflikt im
|
||||||
|
Live-Betrieb.
|
||||||
|
- Migration v17 unverändert: kein Schema-Bump, kein Config-Migrations- Aufwand. Nach dem Update
|
||||||
|
läuft das Plugin gegen die bestehende v17-Datenbank weiter.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
subtitle: DI Foundation und Service-Refactor
|
||||||
|
versionsnatur: Architektur-Cycle
|
||||||
|
---
|
||||||
|
|
||||||
|
- **Architektur-Umbau ohne User-spürbare Verhaltens-Änderung:** der Plugin-Bootstrap wechselt auf
|
||||||
|
einen Generic-Host DI-Container (`Microsoft.Extensions.Hosting` + `IServiceCollection`) nach dem
|
||||||
|
Lightless-Sync-Muster. 18 Service-Klassen wandern von einem statischen `Plugin.LogProxy`-Locator
|
||||||
|
auf typisierte `ILogger<T>`-Constructor-Injection. `DalamudLogger` brückt
|
||||||
|
`Microsoft.Extensions.Logging` über auf Dalamuds `IPluginLog` — im xllog erscheinen jetzt
|
||||||
|
Service-spezifische Spalten wie `[ MessageManager]` und `[Honori...ervice]`.
|
||||||
|
- **Plugin.LogProxy bleibt für die acht Buckets erhalten,** die Constructor-Injection nicht
|
||||||
|
erreicht: Static-Helper (EmoteCache, AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-Reflektion
|
||||||
|
(Configuration), Data-Class mit Massen-Instanziierung (Message) und Instanz-Klassen die nur aus
|
||||||
|
Static-Methods loggen (FontManager, eine GameFunctions-Stelle).
|
||||||
|
- **Performance bestätigt durch Cross-Plugin-Baseline:** HellionChat First-Frame-HITCH 77 ms Median,
|
||||||
|
Chat 2 v1.40.2 74 ms Median — kein DI-Penalty gegenüber dem Upstream-Fork-Origin. Lightless und
|
||||||
|
XIVInstantMessenger liegen bei ~7 ms weil sie ihren FontAtlas-Build deferren; das wird das
|
||||||
|
v1.5.1-Item.
|
||||||
|
- **User-sichtbarer Bug-Fix nebenbei:** Slash-Command-Einfügen in das Chat-Eingabefeld (Friend-List
|
||||||
|
"/tell"-Action plus Plugin-Inserts von Artisan, AllaganTools und ähnlichen) ersetzt jetzt den
|
||||||
|
vorhandenen Input, statt anzukonkatenieren. Cherry-Pick aus ChatTwo upstream `ee7768ac` mit
|
||||||
|
Namespace-Anpassung.
|
||||||
|
- **Foundation für die Plugin-Integrations-Wave:** v1.5.7-11 (Context-Menu, NotificationMaster,
|
||||||
|
Moodles, ExtraChat, XIVIM Quick-DM) werden ab jetzt strukturell handhabbar — neue Services sind
|
||||||
|
ein `services.AddSingleton<T>` plus ein paar Factory-Lambda- Zeilen, kein Plugin.cs-Anflanschen
|
||||||
|
mehr.
|
||||||
|
- Migration v17 unverändert: kein Schema-Bump, kein Config-Migrations-Aufwand.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
subtitle: "FontAtlas Refactor and Forge Signature"
|
||||||
|
versionsnatur: "Architecture + Closure + Branding"
|
||||||
|
---
|
||||||
|
|
||||||
|
- **FontManager-Refactor.** Der FontAtlas baut jetzt nur noch einmal pro Plugin-Load statt vier- bis
|
||||||
|
fünfmal. Weniger CPU- und GPU-Druck in den ersten Sekunden nach einem Reload, weniger
|
||||||
|
Atlas-Texture-Memory-Churn. Die acht Font-Einstellungen können live über den neuen
|
||||||
|
`RebuildDelegateFonts`-Pfad geändert werden, ohne dass das Plugin neu geladen werden muss.
|
||||||
|
- **Hellion Forge Signatur.** Das Plugin trägt jetzt eine ASCII-Fuchs-Signatur. Im `/xllog`
|
||||||
|
erscheint beim Plugin-Load ein kleiner Fuchs-Kopf, im First-Run-Wizard und unter Settings →
|
||||||
|
Information taucht eine eingeklappte „Hellion Forge"-Sektion mit dem vollen Fuchs auf. Gezeichnet
|
||||||
|
von Julia Moon, fest in der Plugin-DLL eingebettet.
|
||||||
|
- **Honorific-Integration bleibt unverändert.** Der ursprünglich geplante Gradient-Render-Pfad
|
||||||
|
(Wave/Pulse-Animation) entfällt. Honorific 3.2 stellt keine IPC für den fertig gerenderten
|
||||||
|
Gradient-Frame zur Verfügung, und ein eigener Port der Pride-Palette wurde verworfen. Die
|
||||||
|
Honorific-Anzeige bleibt wie in v1.4.7 etabliert (statischer Glow plus Title).
|
||||||
|
- **Hinweis zum HITCH-Win.** Der ursprünglich angepeilte 10×-First-Frame-Sprung
|
||||||
|
(Lightless/XIVIM-Pattern, ~7 ms statt ~75 ms) ist in diesem Cycle nicht eingetreten. Die
|
||||||
|
Render-Kosten liegen im UiBuilder-First-Frame-Pfad, nicht im FontAtlas-Build. Investigation kommt
|
||||||
|
als eigener späterer Cycle. Keine User-sichtbare Disruption, keine Migration.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
subtitle: "First-Run Wizard — neu in 4 Steps, Roleplay-Profil neu"
|
||||||
|
versionsnatur: "UX-Patch"
|
||||||
|
---
|
||||||
|
- **Vier Steps statt Single-Page.** Der First-Run-Wizard öffnet jetzt in vier Bühnen: Willkommen → Privacy-Profil → Power-Settings → Fertig. Pagination-Dots in Forge-Bronze oben rechts, Back/Skip/Next im Footer. Standardgröße 720×480 (Min 600×400) und der Fuchs-Banner sitzt als zugeklappter TreeNode oben in Step 1, damit die Einleitung im Fokus bleibt.
|
||||||
|
- **Neues Privacy-Profil „Roleplay".** Datensparsamkeit plus Sagen und beide Emote-Typen für Story-Logs. Schreien und Rufen bleiben außen vor, Public-Distance-Lärm von Fremden ist kein Story-Inhalt. Aufbewahrung: Sagen 30 Tage, Emotes 90 Tage. Privacy-Picker wird zum 2×2-Grid, Casual bleibt mit ★-Marker als Empfehlung.
|
||||||
|
- **Power-Settings sichtbar.** Bislang versteckte Defaults bekommen eine eigene Bühne: Vorherige Session laden, Filter inkl. alter Messages, N Tell-Messages vorladen, Compact-Density, Prettier-Timestamps und Theme-Picker für die 10 Built-in-Themes. Keine neuen Settings, nur das Bestehende sauber sichtbar.
|
||||||
|
- **Staged-Commit und Test-Hint auf der Fertig-Bühne.** Auswahl wird erst beim Klick auf „Fertig ✓" geschrieben. „Später entscheiden" oder X-Close lässt die bestehende Config unangetastet, ein nicht angefasster Step behält die alten Werte. Direkt darunter sichtbar: „Tipp /tell <Spielername>", plus die aktuelle Preload-Zahl aus Step 3 als Hinweis auf den Auto-Tell-Tab-Spawn.
|
||||||
|
- **Bestehende User sehen den neuen Wizard einmal.** Wer schon v1.5.1 hatte, bekommt den Multi-Step-Flow beim ersten v1.5.2-Boot aufgepoppt. Neues Config-Feld `WizardLastShownVersion` triggert das einmalig pro Wizard-Rework; Skip oder Finish reicht und danach öffnet er nicht mehr automatisch.
|
||||||
|
- **Unter der Haube.** Pure-Helper-Tests für alle vier Profile-Sets in der Build-Suite (zwölf neue Facts), plus ein WizardStateSmokeStep für `/xlperf`. Migration v17 bleibt, nur ein optionales Config-Feld kommt dazu.
|
||||||
+14
-12
@@ -2,26 +2,28 @@
|
|||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
This release is distributed via the HellionChat custom repository, not the Dalamud main plugin repo. To install:
|
This release is distributed via the HellionChat custom repository, not the Dalamud main plugin repo.
|
||||||
|
To install:
|
||||||
|
|
||||||
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
||||||
2. Add the URL: `https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
2. Add the URL:
|
||||||
|
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||||
3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install
|
3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install
|
||||||
|
|
||||||
## Project documents
|
## Project documents
|
||||||
|
|
||||||
- [README](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/README.md) — features,
|
- [README](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/README.md)
|
||||||
architecture, build
|
— features, architecture, build
|
||||||
- [Privacy notice](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/PRIVACY.md) — what
|
- [Privacy notice](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/PRIVACY.md)
|
||||||
the plugin stores and sends
|
— what the plugin stores and sends
|
||||||
- [Third-party notices](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/docs/THIRD_PARTY_NOTICES.md)
|
- [Third-party notices](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/docs/THIRD_PARTY_NOTICES.md)
|
||||||
— dependencies and licences
|
— dependencies and licences
|
||||||
- [Security policy](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SECURITY.md) —
|
- [Security policy](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SECURITY.md)
|
||||||
vulnerability reporting
|
— vulnerability reporting
|
||||||
- [Support](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SUPPORT.md) — bug reports,
|
- [Support](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SUPPORT.md)
|
||||||
questions, contact paths
|
— bug reports, questions, contact paths
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
[EUPL-1.2](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/LICENSE). Based on
|
[EUPL-1.2](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/LICENSE).
|
||||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna, also EUPL-1.2.
|
Based on [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna, also EUPL-1.2.
|
||||||
|
|||||||
+483
-256
@@ -1,37 +1,193 @@
|
|||||||
## 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
|
## .gitignore – Hellion Forge / Hellion Media
|
||||||
|
##
|
||||||
|
## Basis: github/gitignore VisualStudio.gitignore
|
||||||
|
## Überarbeitet: Mai 2026
|
||||||
|
## Status: Original-Patterns vollständig erhalten,
|
||||||
|
## neu sortiert in logische Sektionen,
|
||||||
|
## Sicherheits- & Tooling-Sektionen ergänzt.
|
||||||
|
##
|
||||||
|
## Markierungen:
|
||||||
|
## [!! OBSOLET 2026 !!] → Tool offiziell eingestellt,
|
||||||
|
## Pattern bleibt aus Vorsicht drin.
|
||||||
|
##
|
||||||
|
##############################################################
|
||||||
|
|
||||||
# Local development environment (HellionChat fork)
|
|
||||||
|
# =====================================================
|
||||||
|
# [!! KRITISCH !!] Secrets, Keys & Credentials
|
||||||
|
# Diese Sachen dürfen NIEMALS im Repo landen!
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Environment Files
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
.env.bak*
|
.env.bak*
|
||||||
.envrc
|
.envrc
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.sample
|
||||||
|
|
||||||
|
# Private Keys & Zertifikate
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.cer
|
||||||
|
*.crt
|
||||||
|
*.csr
|
||||||
|
*.gpg
|
||||||
|
*.asc
|
||||||
|
|
||||||
|
# SSH Keys (falls jemand die ins Repo legt)
|
||||||
|
id_rsa
|
||||||
|
id_ed25519
|
||||||
|
id_ecdsa
|
||||||
|
known_hosts
|
||||||
|
|
||||||
|
# Auth-/Token-Files
|
||||||
|
auth.json
|
||||||
|
.npmrc
|
||||||
|
.pypirc
|
||||||
|
secrets.json
|
||||||
|
|
||||||
|
# ASP.NET / .NET App-Configs mit lokalen Secrets
|
||||||
|
appsettings.*.local.json
|
||||||
|
appsettings.Local.json
|
||||||
|
local.settings.json
|
||||||
|
|
||||||
|
# Memory Dumps (können Credentials im Heap enthalten!)
|
||||||
|
*.dmp
|
||||||
|
*.mdmp
|
||||||
|
crash.log
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Projekt-spezifisch (HellionChat Fork)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Lokale Entwicklungsumgebung
|
||||||
.vscode/
|
.vscode/
|
||||||
scripts/setup-dev-env.sh
|
scripts/setup-dev-env.sh
|
||||||
|
|
||||||
# Local test project (stays out of the published plugin repo;
|
# Lokales Test-Projekt (bleibt aus dem Plugin-Repo raus;
|
||||||
# pure-function safety net for refactor cycles)
|
# pure-function safety net für Refactor-Cycles)
|
||||||
HellionChat.Tests/
|
HellionChat.Tests/
|
||||||
|
ChatTwo.Tests
|
||||||
|
TestResults
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
# Packaging
|
# Packaging
|
||||||
pack/
|
pack/
|
||||||
|
|
||||||
# User-specific files
|
# Specs und Plan-Dateien
|
||||||
|
/.superpowers/
|
||||||
|
|
||||||
|
# Claude Code lokales Setup (nicht committed)
|
||||||
|
/.claude/
|
||||||
|
/CLAUDE.md
|
||||||
|
|
||||||
|
# Cycle-Working-Notes (im Vault gepflegt, lokales Repo-Pad bei Bedarf)
|
||||||
|
/docs/cycle-notes/
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# OS-spezifische Files
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# AI / LLM Tooling (2026 era)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Cursor IDE
|
||||||
|
.cursor/
|
||||||
|
.cursorignore
|
||||||
|
|
||||||
|
# Aider
|
||||||
|
.aider*
|
||||||
|
|
||||||
|
# Continue.dev
|
||||||
|
.continue/
|
||||||
|
.continuerc.json
|
||||||
|
|
||||||
|
# Windsurf
|
||||||
|
.windsurf/
|
||||||
|
|
||||||
|
# Sourcegraph Cody
|
||||||
|
.cody/
|
||||||
|
|
||||||
|
# Lokale Prompt-Sammlungen / Scratch-Pads
|
||||||
|
prompts/local/
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Editor & IDE (neben Visual Studio)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# JetBrains (IntelliJ, Rider, etc.)
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Vim / Neovim
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.swn
|
||||||
|
|
||||||
|
# Sublime Text
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# IDE & Editor – User-spezifische Files (VS)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Visual Studio User Files
|
||||||
*.rsuser
|
*.rsuser
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# MonoDevelop/Xamarin Studio
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|
||||||
# Mono auto generated files
|
# Visual Studio Cache/Options Directory
|
||||||
mono_crash.*
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto-generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# Local History
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Build Output
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
[Dd]ebug/
|
||||||
[Dd]ebugPublic/
|
[Dd]ebugPublic/
|
||||||
[Rr]elease/
|
[Rr]elease/
|
||||||
@@ -47,43 +203,24 @@ bld/
|
|||||||
[Ll]og/
|
[Ll]og/
|
||||||
[Ll]ogs/
|
[Ll]ogs/
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
# ATL Project Build Output
|
||||||
.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/
|
[Dd]ebugPS/
|
||||||
[Rr]eleasePS/
|
[Rr]eleasePS/
|
||||||
dlldata.c
|
dlldata.c
|
||||||
|
|
||||||
# Benchmark Results
|
|
||||||
BenchmarkDotNet.Artifacts/
|
|
||||||
|
|
||||||
# .NET Core
|
# .NET Core
|
||||||
project.lock.json
|
project.lock.json
|
||||||
project.fragment.lock.json
|
project.fragment.lock.json
|
||||||
artifacts/
|
artifacts/
|
||||||
|
|
||||||
# ASP.NET Scaffolding
|
# MigrationBackup (Package Reference Convert Tool)
|
||||||
ScaffoldingReadMe.txt
|
MigrationBackup/
|
||||||
|
|
||||||
# StyleCop
|
|
||||||
StyleCopReport.xml
|
|
||||||
|
|
||||||
# Files built by Visual Studio
|
# =====================================================
|
||||||
|
# Build-Artefakte (Files built by Visual Studio)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
*_i.c
|
*_i.c
|
||||||
*_p.c
|
*_p.c
|
||||||
*_h.h
|
*_h.h
|
||||||
@@ -105,6 +242,7 @@ StyleCopReport.xml
|
|||||||
*.tmp_proj
|
*.tmp_proj
|
||||||
*_wpftmp.csproj
|
*_wpftmp.csproj
|
||||||
*.log
|
*.log
|
||||||
|
*.binlog
|
||||||
*.vspscc
|
*.vspscc
|
||||||
*.vssscc
|
*.vssscc
|
||||||
.builds
|
.builds
|
||||||
@@ -112,10 +250,87 @@ StyleCopReport.xml
|
|||||||
*.svclog
|
*.svclog
|
||||||
*.scc
|
*.scc
|
||||||
|
|
||||||
# Chutzpah Test files
|
|
||||||
|
# =====================================================
|
||||||
|
# Test Results
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# MSTest
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# Verify / Snapshot Testing (modern .NET, Spotty Wisdom)
|
||||||
|
*.received.*
|
||||||
|
*.received.txt
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] Chutzpah – Repository auf GitHub archiviert
|
||||||
_Chutzpah*
|
_Chutzpah*
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Code Coverage
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Coverlet
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
# Visual Studio code coverage
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# DotCover (JetBrains)
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# OpenCover UI Analysis
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] MightyMoose / AutoTest.Net – seit >10 Jahren nicht mehr gepflegt
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Profiler & Trace
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Visual Studio Profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# NVidia Nsight GPU Debugger
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Cache Files (VS, C++, Sass)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
# Visual C++ cache files
|
# Visual C++ cache files
|
||||||
|
# Hinweis: Manche Patterns hier werden auch vom C#-Linter genutzt (z. B. *.lscache)
|
||||||
ipch/
|
ipch/
|
||||||
*.aps
|
*.aps
|
||||||
*.ncb
|
*.ncb
|
||||||
@@ -125,101 +340,80 @@ ipch/
|
|||||||
*.cachefile
|
*.cachefile
|
||||||
*.VC.db
|
*.VC.db
|
||||||
*.VC.VC.opendb
|
*.VC.VC.opendb
|
||||||
|
*.lscache
|
||||||
|
|
||||||
# Visual Studio profiler
|
# Visual Studio cache (.cache files allgemein, .cache directories behalten)
|
||||||
*.psess
|
*.[Cc]ache
|
||||||
*.vsp
|
!?*.[Cc]ache/
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# Visual Studio Trace Files
|
# Web Workbench Sass
|
||||||
*.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
|
|
||||||
|
|
||||||
# Coverlet is a free, cross platform Code Coverage Tool
|
|
||||||
coverage*.json
|
|
||||||
coverage*.xml
|
|
||||||
coverage*.info
|
|
||||||
|
|
||||||
# Visual Studio code coverage results
|
|
||||||
*.coverage
|
|
||||||
*.coveragexml
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
_NCrunch_*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
nCrunchTemp_*
|
|
||||||
|
|
||||||
# MightyMoose
|
|
||||||
*.mm.*
|
|
||||||
AutoTest.Net/
|
|
||||||
|
|
||||||
# Web workbench (sass)
|
|
||||||
.sass-cache/
|
.sass-cache/
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
# =====================================================
|
||||||
DocProject/buildhelp/
|
# NuGet & Dependencies
|
||||||
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
|
# NuGet Packages
|
||||||
*.nupkg
|
*.nupkg
|
||||||
# NuGet Symbol Packages
|
|
||||||
*.snupkg
|
*.snupkg
|
||||||
# The packages folder can be ignored because of Package Restore
|
|
||||||
**/[Pp]ackages/*
|
**/[Pp]ackages/*
|
||||||
# except build/, which is used as an MSBuild target.
|
|
||||||
!**/[Pp]ackages/build/
|
!**/[Pp]ackages/build/
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
#!**/[Pp]ackages/repositories.config
|
#!**/[Pp]ackages/repositories.config
|
||||||
# NuGet v3's project.json files produces more ignorable files
|
|
||||||
*.nuget.props
|
*.nuget.props
|
||||||
*.nuget.targets
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Fody – auto-generated XML schema
|
||||||
|
FodyWeavers.xsd
|
||||||
|
|
||||||
|
# Node (falls JS-Tooling im Build genutzt wird)
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Python Tools für Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Mono
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
mono_crash.*
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Publish & Deploy
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Click-Once
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
*.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/
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
# Microsoft Azure Build Output
|
||||||
csx/
|
csx/
|
||||||
*.build.csdef
|
*.build.csdef
|
||||||
@@ -228,7 +422,35 @@ csx/
|
|||||||
ecf/
|
ecf/
|
||||||
rcf/
|
rcf/
|
||||||
|
|
||||||
# Windows Store app package directories and files
|
# Service Fabric Backup
|
||||||
|
ServiceFabricBackup/
|
||||||
|
|
||||||
|
# Installshield
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Container / Infrastructure-as-Code (Vorsicht: Tokens!)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# Terraform
|
||||||
|
.terraform/
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.*
|
||||||
|
*.tfvars
|
||||||
|
!example.tfvars
|
||||||
|
|
||||||
|
# Serverless Framework
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Windows Store / AppX
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
AppPackages/
|
AppPackages/
|
||||||
BundleArtifacts/
|
BundleArtifacts/
|
||||||
Package.StoreAssociation.xml
|
Package.StoreAssociation.xml
|
||||||
@@ -237,23 +459,155 @@ _pkginfo.txt
|
|||||||
*.appxbundle
|
*.appxbundle
|
||||||
*.appxupload
|
*.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/
|
# Datenbanken & SQL
|
||||||
~$*
|
# =====================================================
|
||||||
*~
|
|
||||||
|
# SQL Server
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Andere DB-bezogene
|
||||||
*.dbmdl
|
*.dbmdl
|
||||||
*.dbproj.schemaview
|
*.dbproj.schemaview
|
||||||
*.jfm
|
*.jfm
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
# [!! OBSOLET 2026 !!] BeatPulse – wurde 2019 umbenannt zu AspNetCore.Diagnostics.HealthChecks
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Business Intelligence / Reporting
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- [Bb]ackup.rdl
|
||||||
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
|
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Add-ins & Analyzer Tools
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# ReSharper
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# TeamCity
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] GhostDoc Plugin – Submain hat das Tool eingestellt
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik JustMock
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool)
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# DocProject Documentation Generator
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Sonstige Sprachen & Tooling
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Ionide (F# VS Code Tools)
|
||||||
|
.ionide/
|
||||||
|
|
||||||
|
# Azure Stream Analytics Local Run
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# BizTalk Build Output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# Orleans
|
||||||
orleans.codegen.cs
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# [!! OBSOLET 2026 !!] Legacy-Tooling (eingestellt)
|
||||||
|
# Patterns bleiben aus Vorsicht drin.
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] TFS 2012 Local Workspace – ersetzt durch Azure DevOps
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] Visual Studio 6 Build Log – VS6 ist von 1998
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] Visual Studio 6 Workspace Options
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] Visual Studio 6 Workspace File
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] RIA / Silverlight – Microsoft hat das Okt. 2021 eingestellt
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] Visual Studio LightSwitch – von Microsoft eingestellt
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Upgrade / Backup-Reports
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Backup-Files vom Konvertieren alter VS-Projekte (wir haben ja git ;-))
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Misc / Temp / Backup
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.publishsettings
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
# Including strong name files can present a security risk
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
#*.snk
|
#*.snk
|
||||||
@@ -261,130 +615,3 @@ orleans.codegen.cs
|
|||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
#bower_components/
|
#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/
|
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
|
||||||
FodyWeavers.xsd
|
|
||||||
|
|
||||||
#Specs und Plan datein
|
|
||||||
/.superpowers/
|
|
||||||
|
|
||||||
#Test Datein
|
|
||||||
ChatTwo.Tests
|
|
||||||
TestResults
|
|
||||||
*.db-shm
|
|
||||||
*.db-wal
|
|
||||||
|
|
||||||
# Claude Code projekt-spezifisches Setup (lokal, nicht committed)
|
|
||||||
/.claude/
|
|
||||||
/CLAUDE.md
|
|
||||||
|
|||||||
+10
-2
@@ -1,9 +1,17 @@
|
|||||||
{
|
{
|
||||||
"MD007": { "indent": 4 },
|
"MD003": { "style": "atx" },
|
||||||
|
"MD004": { "style": "dash" },
|
||||||
|
"MD007": { "indent": 2 },
|
||||||
|
"MD009": { "br_spaces": 2, "strict": false, "list_item_empty_lines": false },
|
||||||
"MD013": false,
|
"MD013": false,
|
||||||
"MD024": { "siblings_only": true },
|
"MD024": { "siblings_only": true },
|
||||||
"MD029": false,
|
"MD029": false,
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
"MD036": false,
|
"MD036": false,
|
||||||
"MD041": false
|
"MD040": true,
|
||||||
|
"MD041": false,
|
||||||
|
"MD046": { "style": "fenced" },
|
||||||
|
"MD048": { "style": "backtick" },
|
||||||
|
"MD049": { "style": "underscore" },
|
||||||
|
"MD050": { "style": "asterisk" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,50 @@
|
|||||||
|
# ##############################################################
|
||||||
|
# #
|
||||||
|
# # .prettierignore – Hellion Forge / Hellion Media
|
||||||
|
# #
|
||||||
|
# # Files die Prettier NICHT anfassen soll.
|
||||||
|
# # Überarbeitet: Mai 2026
|
||||||
|
# #
|
||||||
|
# # Hinweis: Prettier liest auch .gitignore automatisch mit.
|
||||||
|
# # Hier nur Sachen die zusätzlich ignoriert werden müssen
|
||||||
|
# # oder die im Repo liegen aber nicht formatiert werden dürfen.
|
||||||
|
# #
|
||||||
|
# ##############################################################
|
||||||
|
|
||||||
|
|
||||||
|
# === .NET Build Output ===
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
|
||||||
|
# === JS / Web Build Output ===
|
||||||
node_modules/
|
node_modules/
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# === Generierte C#-Files (Designer, Source Generators) ===
|
||||||
*.Designer.cs
|
*.Designer.cs
|
||||||
|
*.g.cs
|
||||||
|
*.g.i.cs
|
||||||
|
*.generated.cs
|
||||||
|
*.AssemblyInfo.cs
|
||||||
|
*.AssemblyAttributes.cs
|
||||||
|
|
||||||
|
# === Lock-Files (NIE umformatieren – zerschießt den Hash) ===
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
packages.lock.json
|
||||||
|
|
||||||
|
# === Minified Files (bewusst kompakt, niemals anfassen) ===
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
|
||||||
|
# === Test-Snapshots (z. B. Verify) ===
|
||||||
|
*.received.*
|
||||||
|
*.verified.*
|
||||||
|
**/__snapshots__/
|
||||||
|
|
||||||
|
# === Plugin-Manifest (DalamudPackager-Schema, fix lassen) ===
|
||||||
|
HellionChat/HellionChat.yaml
|
||||||
|
|||||||
+30
-2
@@ -1,7 +1,35 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"proseWrap": "always",
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"endOfLine": "lf"
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"proseWrap": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.md",
|
||||||
|
"options": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.yml", "*.yaml"],
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.json",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-1
@@ -1,8 +1,53 @@
|
|||||||
|
# ##############################################################
|
||||||
|
# #
|
||||||
|
# # .yamllint.yaml – Hellion Forge / Hellion Media
|
||||||
|
# #
|
||||||
|
# # YAML-Linting Konfiguration.
|
||||||
|
# # Überarbeitet: Mai 2026
|
||||||
|
# #
|
||||||
|
# # Regel-Doku:
|
||||||
|
# # https://yamllint.readthedocs.io/en/stable/rules.html
|
||||||
|
# #
|
||||||
|
# ##############################################################
|
||||||
|
|
||||||
extends: default
|
extends: default
|
||||||
|
|
||||||
|
# Plugin-Manifest folgt DalamudPackager-Konvention (4-space-indent für
|
||||||
|
# image_urls + tags). yamllint-Default verlangt 2 — Konflikt, daher
|
||||||
|
# ignorieren statt das Manifest zu reformatieren.
|
||||||
|
ignore: |
|
||||||
|
HellionChat/HellionChat.yaml
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
|
# Zeilenlängen-Check aus (konsistent mit markdownlint MD013)
|
||||||
line-length: disable
|
line-length: disable
|
||||||
|
|
||||||
|
# YAML ohne führendes "---" erlaubt
|
||||||
document-start: disable
|
document-start: disable
|
||||||
|
|
||||||
|
# GitHub Actions nutzt "on:" als Trigger-Key.
|
||||||
|
# Ohne diesen Override würde yamllint das als boolean "on" beklagen.
|
||||||
truthy:
|
truthy:
|
||||||
allowed-values: ["true", "false", "on"]
|
allowed-values: ['true', 'false', 'on']
|
||||||
|
|
||||||
|
# Maximal 1 Leerzeile in Folge (saubere Files)
|
||||||
empty-lines:
|
empty-lines:
|
||||||
max: 1
|
max: 1
|
||||||
|
|
||||||
|
# YAML-Standard ist 2 Spaces (auch GitHub Actions erwartet das).
|
||||||
|
# Explizit setzen, um Konsistenz im Repo zu erzwingen.
|
||||||
|
indentation:
|
||||||
|
spaces: 2
|
||||||
|
indent-sequences: true
|
||||||
|
check-multi-line-strings: false
|
||||||
|
|
||||||
|
# Kommentare brauchen Space nach #, müssen mit Content beginnen
|
||||||
|
comments:
|
||||||
|
require-starting-space: true
|
||||||
|
min-spaces-from-content: 1
|
||||||
|
|
||||||
|
# Kein Whitespace am Zeilenende
|
||||||
|
trailing-spaces: enable
|
||||||
|
|
||||||
|
# Datei muss mit Newline enden
|
||||||
|
new-line-at-end-of-file: enable
|
||||||
|
|||||||
+48
-44
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
## A Note on This Project
|
## A Note on This Project
|
||||||
|
|
||||||
HellionChat is a one-person side project developed under Hellion Forge. I maintain this in my spare time, which means
|
HellionChat is a one-person side project developed under Hellion Forge. I maintain this in my spare
|
||||||
replies can take a few days. Please do not escalate just because a thread is quiet.
|
time, which means replies can take a few days. Please do not escalate just because a thread is
|
||||||
|
quiet.
|
||||||
|
|
||||||
When in doubt, assume good intent. Contributors come from different backgrounds, time zones and skill levels. A
|
When in doubt, assume good intent. Contributors come from different backgrounds, time zones and
|
||||||
clarifying question is almost always a better first move than an accusation.
|
skill levels. A clarifying question is almost always a better first move than an accusation.
|
||||||
|
|
||||||
Please also keep discussions on topic. This project is about a Dalamud chat plugin. Off-topic arguments belong
|
Please also keep discussions on topic. This project is about a Dalamud chat plugin. Off-topic
|
||||||
elsewhere.
|
arguments belong elsewhere.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,20 +18,21 @@ elsewhere.
|
|||||||
|
|
||||||
We pledge to make our community welcoming, safe, and equitable for all.
|
We pledge to make our community welcoming, safe, and equitable for all.
|
||||||
|
|
||||||
We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all
|
We are committed to fostering an environment that respects and promotes the dignity, rights, and
|
||||||
individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics,
|
contributions of all individuals, regardless of characteristics including race, ethnicity, caste,
|
||||||
neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or
|
color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or
|
||||||
religion, national or social origin, socio-economic position, level of education, or other status. The same privileges
|
expression, sexual orientation, language, philosophy or religion, national or social origin,
|
||||||
of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
|
socio-economic position, level of education, or other status. The same privileges of participation
|
||||||
|
are extended to everyone who participates in good faith and in accordance with this Covenant.
|
||||||
|
|
||||||
## Encouraged Behaviors
|
## Encouraged Behaviors
|
||||||
|
|
||||||
While acknowledging differences in social norms, we all strive to meet our community's expectations for positive
|
While acknowledging differences in social norms, we all strive to meet our community's expectations
|
||||||
behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture,
|
for positive behavior. We also understand that our words and actions may be interpreted differently
|
||||||
background, or native language.
|
than we intend based on culture, background, or native language.
|
||||||
|
|
||||||
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared
|
With these considerations in mind, we agree to behave mindfully toward each other and act in ways
|
||||||
values, including:
|
that center our shared values, including:
|
||||||
|
|
||||||
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
|
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
|
||||||
2. Engaging **kindly and honestly** with others.
|
2. Engaging **kindly and honestly** with others.
|
||||||
@@ -42,31 +44,32 @@ values, including:
|
|||||||
|
|
||||||
## Restricted Behaviors
|
## Restricted Behaviors
|
||||||
|
|
||||||
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are
|
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of
|
||||||
violations of this Code of Conduct.
|
these behaviors are violations of this Code of Conduct.
|
||||||
|
|
||||||
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any
|
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal
|
||||||
clear request to stop.
|
attention after any clear request to stop.
|
||||||
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of
|
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a
|
||||||
people.
|
community member or group of people.
|
||||||
3. **Stereotyping or discrimination.** Characterizing anyone's personality or behavior on the basis of immutable
|
3. **Stereotyping or discrimination.** Characterizing anyone's personality or behavior on the basis
|
||||||
identities or traits.
|
of immutable identities or traits.
|
||||||
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or
|
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate
|
||||||
purpose of the community.
|
in the context or purpose of the community.
|
||||||
5. **Violating confidentiality.** Sharing or acting on someone's personal or private information without their
|
5. **Violating confidentiality.** Sharing or acting on someone's personal or private information
|
||||||
permission.
|
without their permission.
|
||||||
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
|
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person
|
||||||
|
or group.
|
||||||
7. Behaving in other ways that **threaten the well-being** of our community.
|
7. Behaving in other ways that **threaten the well-being** of our community.
|
||||||
|
|
||||||
### Other Restrictions
|
### Other Restrictions
|
||||||
|
|
||||||
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade
|
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone
|
||||||
enforcement actions.
|
else to evade enforcement actions.
|
||||||
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
|
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
|
||||||
3. **Promotional materials.** Sharing marketing or other commercial content in a way that is outside the norms of the
|
3. **Promotional materials.** Sharing marketing or other commercial content in a way that is outside
|
||||||
community.
|
the norms of the community.
|
||||||
4. **Irresponsible communication.** Failing to responsibly present content which includes, links to, or describes any
|
4. **Irresponsible communication.** Failing to responsibly present content which includes, links to,
|
||||||
other restricted behaviors.
|
or describes any other restricted behaviors.
|
||||||
|
|
||||||
## Reporting
|
## Reporting
|
||||||
|
|
||||||
@@ -77,12 +80,13 @@ If something here is being broken, contact me directly. Do not open a public iss
|
|||||||
| Email | `kontakt@hellion-media.de` |
|
| Email | `kontakt@hellion-media.de` |
|
||||||
| Discord DM | `@j.j_kazama` |
|
| Discord DM | `@j.j_kazama` |
|
||||||
|
|
||||||
Reports stay private. I will acknowledge within a few weekdays (European business hours) and tell you what I plan to do.
|
Reports stay private. I will acknowledge within a few weekdays (European business hours) and tell
|
||||||
|
you what I plan to do.
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
I am the sole maintainer, so enforcement is a single-person process. I will pick the lightest measure that actually
|
I am the sole maintainer, so enforcement is a single-person process. I will pick the lightest
|
||||||
resolves the situation:
|
measure that actually resolves the situation:
|
||||||
|
|
||||||
1. Private note asking the behaviour to stop.
|
1. Private note asking the behaviour to stop.
|
||||||
2. Public correction in the affected thread.
|
2. Public correction in the affected thread.
|
||||||
@@ -95,16 +99,16 @@ Severe cases skip the lower steps. I will not negotiate over harassment or threa
|
|||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies to all spaces the project owns or that I run on its behalf: the GitHub repository, GitHub
|
This Code of Conduct applies to all spaces the project owns or that I run on its behalf: the GitHub
|
||||||
Discussions, project-related Discord conversations, and the maintainer contact listed in [`SECURITY.md`](SECURITY.md).
|
repository, GitHub Discussions, project-related Discord conversations, and the maintainer contact
|
||||||
It also applies when someone is identifiably representing HellionChat elsewhere, for example when posting as a
|
listed in [`SECURITY.md`](SECURITY.md). It also applies when someone is identifiably representing
|
||||||
HellionChat maintainer in the Dalamud Discord.
|
HellionChat elsewhere, for example when posting as a HellionChat maintainer in the Dalamud Discord.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, available at
|
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, available at
|
||||||
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
|
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
|
||||||
|
|
||||||
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy
|
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA
|
||||||
of this license, visit
|
4.0. To view a copy of this license, visit
|
||||||
[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/).
|
[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|||||||
+70
-58
@@ -1,69 +1,75 @@
|
|||||||
# Contributing to HellionChat
|
# Contributing to HellionChat
|
||||||
|
|
||||||
Thanks for taking a look. HellionChat is a one-person side project developed under Hellion Forge. It started as a fork
|
Thanks for taking a look. HellionChat is a one-person side project developed under Hellion Forge. It
|
||||||
of [Chat 2](https://github.com/Infiziert90/ChatTwo) and has since become a standalone plugin under its own namespace,
|
started as a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo) and has since become a
|
||||||
IPC channels and source tree (standalone-cut completed in v1.0.0). Forking HellionChat itself is explicitly permitted
|
standalone plugin under its own namespace, IPC channels and source tree (standalone-cut completed in
|
||||||
under the EUPL-1.2.
|
v1.0.0). Forking HellionChat itself is explicitly permitted under the EUPL-1.2.
|
||||||
|
|
||||||
This document explains what I am looking for, what I am not, and how to make a contribution land smoothly.
|
This document explains what I am looking for, what I am not, and how to make a contribution land
|
||||||
|
smoothly.
|
||||||
|
|
||||||
## Before You Open Anything
|
## Before You Open Anything
|
||||||
|
|
||||||
- Read the [README](README.md) so you understand the scope: a privacy-focused, EUPL-1.2-licensed Dalamud plugin that
|
- Read the [README](README.md) so you understand the scope: a privacy-focused, EUPL-1.2-licensed
|
||||||
intentionally removes the upstream webinterface and ships privacy-first defaults.
|
Dalamud plugin that intentionally removes the upstream webinterface and ships privacy-first
|
||||||
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Active cherry-picking from upstream Chat 2 has ended in the
|
defaults.
|
||||||
v1.4.x cycle; HellionChat continues as an independent codebase. Existing upstream-derived code keeps its attribution.
|
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Active cherry-picking from upstream Chat 2
|
||||||
New contributions stand on their own and do not need to be cherry-pick-compatible.
|
has ended in the v1.4.x cycle; HellionChat continues as an independent codebase. Existing
|
||||||
- Read [`SECURITY.md`](SECURITY.md). Anything security-sensitive goes through a private advisory, never a public issue
|
upstream-derived code keeps its attribution. New contributions stand on their own and do not need
|
||||||
or PR.
|
to be cherry-pick-compatible.
|
||||||
|
- Read [`SECURITY.md`](SECURITY.md). Anything security-sensitive goes through a private advisory,
|
||||||
|
never a public issue or PR.
|
||||||
- Read the [Code of Conduct](CODE_OF_CONDUCT.md).
|
- Read the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
## What I Will Accept
|
## What I Will Accept
|
||||||
|
|
||||||
- Bug fixes for behaviour documented in the README, the in-plugin settings or the changelog.
|
- Bug fixes for behaviour documented in the README, the in-plugin settings or the changelog.
|
||||||
- Translation contributions for Hellion-specific strings via direct pull requests against
|
- Translation contributions for Hellion-specific strings via direct pull requests against
|
||||||
`HellionChat/Resources/HellionStrings.*.resx`. Translations for upstream Chat 2 strings (`Language.*.resx`) are not
|
`HellionChat/Resources/HellionStrings.*.resx`. Translations for upstream Chat 2 strings
|
||||||
handled here; those go to the upstream Chat 2 project.
|
(`Language.*.resx`) are not handled here; those go to the upstream Chat 2 project.
|
||||||
- Documentation improvements (README, comments, this file).
|
- Documentation improvements (README, comments, this file).
|
||||||
- Performance fixes with a measurable before/after.
|
- Performance fixes with a measurable before/after.
|
||||||
- New features that fit the privacy-first scope and do not duplicate what an existing Dalamud plugin already does well.
|
- New features that fit the privacy-first scope and do not duplicate what an existing Dalamud plugin
|
||||||
|
already does well.
|
||||||
|
|
||||||
## What I Will Probably Decline
|
## What I Will Probably Decline
|
||||||
|
|
||||||
- Re-introducing the webinterface or any remote-access feature. It was removed in v0.2.0 on purpose. See the README
|
- Re-introducing the webinterface or any remote-access feature. It was removed in v0.2.0 on purpose.
|
||||||
section "Was gegenüber Chat 2 fehlt".
|
See the README section "Was gegenüber Chat 2 fehlt".
|
||||||
- Features that bypass the privacy filter or weaken the default retention behaviour without an explicit, documented
|
- Features that bypass the privacy filter or weaken the default retention behaviour without an
|
||||||
opt-in.
|
explicit, documented opt-in.
|
||||||
- Sweeping refactors that touch large parts of the codebase. The maintenance cost outweighs the benefit for a one-person
|
- Sweeping refactors that touch large parts of the codebase. The maintenance cost outweighs the
|
||||||
project. (This used to be doubly important because of the upstream cherry-pick path; that path is closed now, but the
|
benefit for a one-person project. (This used to be doubly important because of the upstream
|
||||||
rule still holds on its own merits.)
|
cherry-pick path; that path is closed now, but the rule still holds on its own merits.)
|
||||||
- AI-generated code dropped in without disclosure or human review. See [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md)
|
- AI-generated code dropped in without disclosure or human review. See
|
||||||
for how I handle AI assistance on my side; I expect comparable transparency from contributors.
|
[`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) for how I handle AI assistance on my side; I
|
||||||
|
expect comparable transparency from contributors.
|
||||||
|
|
||||||
If you are unsure whether an idea fits, open a feature-request issue first and ask before writing code. I would rather
|
If you are unsure whether an idea fits, open a feature-request issue first and ask before writing
|
||||||
say "no" to a proposal than to a finished pull request.
|
code. I would rather say "no" to a proposal than to a finished pull request.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Open an issue (bug or feature request) using the templates under `.github/ISSUE_TEMPLATE/`. Skip this for trivial
|
1. Open an issue (bug or feature request) using the templates under `.github/ISSUE_TEMPLATE/`. Skip
|
||||||
typos.
|
this for trivial typos.
|
||||||
2. Fork the repository and branch off `main`. Branch naming is informal; something like `fix/auto-tell-history-empty` or
|
2. Fork the repository and branch off `main`. Branch naming is informal; something like
|
||||||
`feat/theme-export` is fine.
|
`fix/auto-tell-history-empty` or `feat/theme-export` is fine.
|
||||||
3. Match the existing code style. The repository ships an `.editorconfig` that VS Code and Rider pick up automatically.
|
3. Match the existing code style. The repository ships an `.editorconfig` that VS Code and Rider
|
||||||
4. Keep commits focused. Several small commits with clear messages are easier to review than one large one.
|
pick up automatically.
|
||||||
Squash-on-merge happens at the PR level if needed.
|
4. Keep commits focused. Several small commits with clear messages are easier to review than one
|
||||||
|
large one. Squash-on-merge happens at the PR level if needed.
|
||||||
5. If your change touches user-visible behaviour, update the README and/or the changelog block in
|
5. If your change touches user-visible behaviour, update the README and/or the changelog block in
|
||||||
`HellionChat/HellionChat.yaml` and `repo.json`. I bump the version number myself at release time.
|
`HellionChat/HellionChat.yaml` and `repo.json`. I bump the version number myself at release time.
|
||||||
6. Open the pull request against `main`. The PR template will ask you to summarise the change, the testing you did and
|
6. Open the pull request against `main`. The PR template will ask you to summarise the change, the
|
||||||
any compatibility notes.
|
testing you did and any compatibility notes.
|
||||||
|
|
||||||
## Build and Test
|
## Build and Test
|
||||||
|
|
||||||
The project targets `net10.0-windows` against Dalamud SDK 15. To build locally you need:
|
The project targets `net10.0-windows` against Dalamud SDK 15. To build locally you need:
|
||||||
|
|
||||||
- .NET 10 SDK
|
- .NET 10 SDK
|
||||||
- A working Dalamud dev environment with `DALAMUD_HOME` set (XIVLauncher installed and launched once is the simplest
|
- A working Dalamud dev environment with `DALAMUD_HOME` set (XIVLauncher installed and launched once
|
||||||
path)
|
is the simplest path)
|
||||||
- VS Code with the C# Dev Kit, Rider, or Visual Studio
|
- VS Code with the C# Dev Kit, Rider, or Visual Studio
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -71,11 +77,12 @@ dotnet restore
|
|||||||
dotnet build HellionChat.sln -c Release
|
dotnet build HellionChat.sln -c Release
|
||||||
```
|
```
|
||||||
|
|
||||||
There are currently no tests in `HellionChat.sln`. If you add a test project, point it at the relevant subsystems
|
There are currently no tests in `HellionChat.sln`. If you add a test project, point it at the
|
||||||
(privacy filter, configuration migration, message store) and mention it in the PR.
|
relevant subsystems (privacy filter, configuration migration, message store) and mention it in the
|
||||||
|
PR.
|
||||||
|
|
||||||
For a smoke test in-game: build, copy the output into your Dalamud `devPlugins/HellionChat/` directory and load it via
|
For a smoke test in-game: build, copy the output into your Dalamud `devPlugins/HellionChat/`
|
||||||
`/xlplugins`.
|
directory and load it via `/xlplugins`.
|
||||||
|
|
||||||
## Continuous Integration
|
## Continuous Integration
|
||||||
|
|
||||||
@@ -86,30 +93,33 @@ Every push and every pull request runs:
|
|||||||
| `build.yml` | `dotnet build` and `dotnet test` |
|
| `build.yml` | `dotnet build` and `dotnet test` |
|
||||||
| `codeql.yml` | CodeQL security analysis |
|
| `codeql.yml` | CodeQL security analysis |
|
||||||
|
|
||||||
A pull request will not be merged while either of these is failing. CodeQL findings on changed code need to be
|
A pull request will not be merged while either of these is failing. CodeQL findings on changed code
|
||||||
addressed; pre-existing findings on untouched code are tracked separately.
|
need to be addressed; pre-existing findings on untouched code are tracked separately.
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx` (English source) and
|
Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx` (English source) and
|
||||||
`HellionStrings.<lang>.resx` (per-language). These are accepted as direct pull requests.
|
`HellionStrings.<lang>.resx` (per-language). These are accepted as direct pull requests.
|
||||||
|
|
||||||
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are **not** translated here. They are kept as-is
|
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are **not** translated here.
|
||||||
from the last upstream sync and remain the work of the Chat 2 Crowdin community. Active cherry-picking from upstream
|
They are kept as-is from the last upstream sync and remain the work of the Chat 2 Crowdin community.
|
||||||
ended in the v1.4.x cycle (see [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md)), so future translation improvements to
|
Active cherry-picking from upstream ended in the v1.4.x cycle (see
|
||||||
those upstream strings will not flow into HellionChat automatically anymore. If you have improvements for the original
|
[`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md)), so future translation improvements to those
|
||||||
Chat 2 strings, please contribute them to [Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) directly.
|
upstream strings will not flow into HellionChat automatically anymore. If you have improvements for
|
||||||
|
the original Chat 2 strings, please contribute them to
|
||||||
|
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) directly.
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
By submitting a pull request you confirm that:
|
By submitting a pull request you confirm that:
|
||||||
|
|
||||||
- Your contribution is your own work, or you have the right to contribute it under the project licence.
|
- Your contribution is your own work, or you have the right to contribute it under the project
|
||||||
- You agree that your contribution will be released under the [EUPL-1.2](LICENSE), the same licence as the rest of the
|
licence.
|
||||||
project.
|
- You agree that your contribution will be released under the [EUPL-1.2](LICENSE), the same licence
|
||||||
|
as the rest of the project.
|
||||||
|
|
||||||
There is no separate CLA. Forking HellionChat is explicitly permitted under the EUPL-1.2, as with any EUPL-licensed
|
There is no separate CLA. Forking HellionChat is explicitly permitted under the EUPL-1.2, as with
|
||||||
project.
|
any EUPL-licensed project.
|
||||||
|
|
||||||
## Response Times
|
## Response Times
|
||||||
|
|
||||||
@@ -119,8 +129,9 @@ project.
|
|||||||
| Discord DM | `@j.j_kazama` |
|
| Discord DM | `@j.j_kazama` |
|
||||||
| Email | `kontakt@hellion-media.de` |
|
| Email | `kontakt@hellion-media.de` |
|
||||||
|
|
||||||
I respond on weekdays during European business hours and take weekends and FFXIV patch days off. A pull request that
|
I respond on weekdays during European business hours and take weekends and FFXIV patch days off. A
|
||||||
sits for a few days has not been ignored. Pinging once after a week is fine; please do not ping daily.
|
pull request that sits for a few days has not been ignored. Pinging once after a week is fine;
|
||||||
|
please do not ping daily.
|
||||||
|
|
||||||
## First-time setup
|
## First-time setup
|
||||||
|
|
||||||
@@ -130,9 +141,10 @@ After cloning, run once:
|
|||||||
./scripts/setup-hooks.sh
|
./scripts/setup-hooks.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This wires `core.hooksPath` to `.githooks/`. The pre-push hook runs preflight (versions/manifest/changelog/build).
|
This wires `core.hooksPath` to `.githooks/`. The pre-push hook runs preflight
|
||||||
|
(versions/manifest/changelog/build).
|
||||||
|
|
||||||
### Test suite
|
### Test suite
|
||||||
|
|
||||||
The plugin's test suite lives in a separate local repository and is not part of this codebase. If you need access for
|
The plugin's test suite lives in a separate local repository and is not part of this codebase. If
|
||||||
development, contact the maintainer.
|
you need access for development, contact the maintainer.
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions.Types;
|
using HellionChat.GameFunctions.Types;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -18,26 +20,38 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
private readonly Plugin _plugin;
|
private readonly Plugin _plugin;
|
||||||
private readonly MessageManager _messageManager;
|
private readonly MessageManager _messageManager;
|
||||||
private readonly MessageStore _store;
|
private readonly MessageStore _store;
|
||||||
|
private readonly ILogger<AutoTellTabsService> _logger;
|
||||||
private readonly object _tempTabsLock = new();
|
private readonly object _tempTabsLock = new();
|
||||||
|
|
||||||
// F2.1: lock-free counter mirrors Config.Tabs.Count(IsTempTab) so the
|
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
|
||||||
// hot-path getter doesn't contend with HandleTell on every render frame.
|
// of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
|
||||||
// Bumped from inside the existing mutation paths so it stays consistent
|
// in their own bucket. A configurable cap is a vault-backlog anchor for
|
||||||
// with the underlying list — see SpawnTempTab, DropOldestTempTab, OnLogout
|
// a later cycle if tester feedback demands it.
|
||||||
// and ResyncTempTabCounter (used by Plugin.cs snapshot-restore).
|
internal const int MaxPinnedTempTabs = 5;
|
||||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
|
||||||
private int _activeTempTabCount;
|
|
||||||
|
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
|
|
||||||
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
|
internal AutoTellTabsService(
|
||||||
|
Plugin plugin,
|
||||||
|
MessageManager messageManager,
|
||||||
|
MessageStore store,
|
||||||
|
ILogger<AutoTellTabsService> logger
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_plugin = plugin;
|
_plugin = plugin;
|
||||||
_messageManager = messageManager;
|
_messageManager = messageManager;
|
||||||
_store = store;
|
_store = store;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int ActiveTempTabCount => Volatile.Read(ref _activeTempTabCount);
|
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||||
|
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||||
|
// Replaces the F2.1 Interlocked counter because the new pin-state
|
||||||
|
// transitions are cold-path and don't need lock-free reads.
|
||||||
|
internal int ActiveTempTabCount =>
|
||||||
|
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||||
|
|
||||||
|
internal int PinnedTempTabCount => Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||||
|
|
||||||
internal void Initialize()
|
internal void Initialize()
|
||||||
{
|
{
|
||||||
@@ -46,23 +60,51 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed the counter from the persisted Tabs list so a config that already
|
// Pinned tabs come out of the JSON with TellTarget set but
|
||||||
// contains TempTabs from a prior session starts in sync. Plugin.cs:168
|
// CurrentChannel reset (NonSerialized). Without re-seeding, the chat
|
||||||
// crash-recovery has already dropped TempTabs by the time we get here,
|
// input has no tell-target on the active pinned tab, and the
|
||||||
// so the snapshot reflects post-recovery reality.
|
// game-side channel hook only repaints CurrentChannel once the user
|
||||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
// triggers a /tell or channel switch.
|
||||||
|
RehydratePinnedTabs();
|
||||||
|
|
||||||
_messageManager.MessageProcessed += HandleTell;
|
_messageManager.MessageProcessed += HandleTell;
|
||||||
Plugin.ClientState.Logout += OnLogout;
|
Plugin.ClientState.Logout += OnLogout;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// F2.1: callable from outside paths that mutate Config.Tabs directly
|
private void RehydratePinnedTabs()
|
||||||
// (Plugin.cs snapshot-restore). Atomically re-pegs the counter to the
|
|
||||||
// live IsTempTab count.
|
|
||||||
internal void ResyncTempTabCounter()
|
|
||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||||
|
_logger.LogDebug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found");
|
||||||
|
|
||||||
|
foreach (var tab in Plugin.Config.Tabs)
|
||||||
|
{
|
||||||
|
if (!TabLifecycleHelpers.IsInPinnedPool(tab))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (tab.TellTarget is null || !tab.TellTarget.IsSet())
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
$"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget "
|
||||||
|
+ $"(Name={tab.TellTarget?.Name ?? "<null>"} World={tab.TellTarget?.World ?? 0}). "
|
||||||
|
+ "Chat input on this tab will be empty until the partner sends a tell or you /tell manually."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.Channel ??= InputChannel.Tell;
|
||||||
|
tab.CurrentChannel.Channel = InputChannel.Tell;
|
||||||
|
tab.CurrentChannel.TellTarget = tab.TellTarget.Clone();
|
||||||
|
|
||||||
|
// MessageList is NonSerialized so pinned tabs come back empty.
|
||||||
|
// Preload the same history window the spawn path uses so the user
|
||||||
|
// sees the recent conversation, not a blank tab.
|
||||||
|
PreloadHistory(tab, tab.TellTarget.Name, tab.TellTarget.World, Guid.Empty);
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -96,7 +138,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
if (partner == null)
|
if (partner == null)
|
||||||
{
|
{
|
||||||
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
||||||
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
||||||
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
||||||
@@ -110,7 +152,23 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
// Already routed via MessageManager pipeline
|
// Already routed via MessageManager pipeline. Repair the
|
||||||
|
// tell-target if the fallback hit a pinned tab whose
|
||||||
|
// TellTarget didn't survive a previous round-trip — keeps
|
||||||
|
// FindTempTab fast on the next message.
|
||||||
|
if (
|
||||||
|
existing.IsPinned
|
||||||
|
&& (existing.TellTarget is null || !existing.TellTarget.IsSet())
|
||||||
|
)
|
||||||
|
{
|
||||||
|
existing.TellTarget = new TellTarget(
|
||||||
|
partner.Value.Name,
|
||||||
|
partner.Value.World,
|
||||||
|
0,
|
||||||
|
TellReason.Direct
|
||||||
|
);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,22 +218,35 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tab? FindTempTab(string name, uint world)
|
private static Tab? FindTempTab(string name, uint world)
|
||||||
{
|
{
|
||||||
return Plugin.Config.Tabs.FirstOrDefault(t =>
|
var byTarget = Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||||
t.IsTempTab
|
t.IsTempTab
|
||||||
&& t.TellTarget != null
|
&& t.TellTarget != null
|
||||||
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
||||||
&& t.TellTarget.World == world
|
&& t.TellTarget.World == world
|
||||||
);
|
);
|
||||||
|
if (byTarget != null)
|
||||||
|
return byTarget;
|
||||||
|
|
||||||
|
// Fallback: match by tab name. Pinned tabs are named via
|
||||||
|
// FormatTabName(player, world) at spawn time, so the name is a
|
||||||
|
// stable secondary key when TellTarget didn't survive a save/load
|
||||||
|
// (older configs from a renamed pin, malformed migrations, etc.).
|
||||||
|
var expectedName = FormatTabName(name, world);
|
||||||
|
return Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||||
|
t.IsTempTab && string.Equals(t.Name, expectedName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DropOldestTempTab()
|
internal void DropOldestTempTab()
|
||||||
{
|
{
|
||||||
// Prioritize greeted tabs for drop; within each bucket, drop by oldest LastActivity
|
// Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
|
||||||
|
// never drop candidates. They leave the bucket only via Unpin or
|
||||||
|
// PromoteToPermanent.
|
||||||
var victim = Plugin
|
var victim = Plugin
|
||||||
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||||
.Where(t => t.Tab.IsTempTab)
|
.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
|
||||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||||
.ThenBy(t => t.Tab.LastActivity)
|
.ThenBy(t => t.Tab.LastActivity)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -198,7 +269,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||||
Interlocked.Decrement(ref _activeTempTabCount);
|
|
||||||
|
|
||||||
// Re-anchor active tab to avoid silent switch when tab is dropped
|
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||||
if (victim.Index <= _plugin.LastTab)
|
if (victim.Index <= _plugin.LastTab)
|
||||||
@@ -223,7 +293,6 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.Add(tab);
|
Plugin.Config.Tabs.Add(tab);
|
||||||
Interlocked.Increment(ref _activeTempTabCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
||||||
@@ -300,7 +369,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
||||||
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
|
_logger.LogError(ex, "[AutoTellTabs] History preload failed");
|
||||||
tab.Messages.AddPrune(
|
tab.Messages.AddPrune(
|
||||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||||
MessageManager.MessageDisplayLimit
|
MessageManager.MessageDisplayLimit
|
||||||
@@ -354,14 +423,16 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
{
|
{
|
||||||
lock (_tempTabsLock)
|
lock (_tempTabsLock)
|
||||||
{
|
{
|
||||||
// Snapshot active tab index before mutating list
|
// Pinned TempTabs must survive char-switch — that's the whole point
|
||||||
|
// of pinning. Only unpinned ones get stripped.
|
||||||
var lastIndex = _plugin.LastTab;
|
var lastIndex = _plugin.LastTab;
|
||||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||||
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
|
var currentWasUnpinnedTempTab =
|
||||||
|
lastIndexValid
|
||||||
|
&& TabLifecycleHelpers.IsInUnpinnedPool(Plugin.Config.Tabs[lastIndex]);
|
||||||
|
|
||||||
// Clean up pop-out windows before removing temp tabs
|
|
||||||
var poppedTempTabIds = Plugin
|
var poppedTempTabIds = Plugin
|
||||||
.Config.Tabs.Where(t => t.IsTempTab && t.PopOut)
|
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
|
||||||
.Select(t => t.Identifier)
|
.Select(t => t.Identifier)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (poppedTempTabIds.Count > 0)
|
if (poppedTempTabIds.Count > 0)
|
||||||
@@ -377,15 +448,76 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var removed = Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||||
Interlocked.Add(ref _activeTempTabCount, -removed);
|
|
||||||
|
|
||||||
// Force switch to tab 0 if active tab was temp or index is now out of range
|
// Force switch to tab 0 if active tab was an unpinned temp tab or
|
||||||
|
// index is now out of range. Pinned tabs survive — no switch needed.
|
||||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||||
if (currentWasTempTab || !stillValid)
|
if (currentWasUnpinnedTempTab || !stillValid)
|
||||||
{
|
{
|
||||||
_plugin.WantedTab = 0;
|
_plugin.WantedTab = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool TryPin(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsTempTab || tab.IsPinned)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinnedTempTabCount >= MaxPinnedTempTabs)
|
||||||
|
{
|
||||||
|
WrapperUtil.AddNotification(
|
||||||
|
string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
|
||||||
|
NotificationType.Warning
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsPinned = true;
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
|
||||||
|
);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Unpin(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsPinned)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the unpinned pool is already full, dropping the oldest before
|
||||||
|
// flipping the flag avoids counting the just-unpinned tab as a drop
|
||||||
|
// candidate.
|
||||||
|
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||||
|
{
|
||||||
|
DropOldestTempTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsPinned = false;
|
||||||
|
_logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void PromoteToPermanent(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsTempTab)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsTempTab = false;
|
||||||
|
tab.IsPinned = false;
|
||||||
|
tab.TellTarget = TellTarget.Empty();
|
||||||
|
_logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)");
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
namespace HellionChat.Branding;
|
||||||
|
|
||||||
|
// Lazy-loaded provenance art that ships embedded with the DLL. Two
|
||||||
|
// variants:
|
||||||
|
//
|
||||||
|
// - FoxBanner: the full-size silhouette with "Hellion Forge" inside
|
||||||
|
// the body — rendered in the first-run wizard and the Information
|
||||||
|
// tab as a small "about the makers" anchor.
|
||||||
|
// - FoxMini: the four-line fox-head + curly-tail that gets stitched
|
||||||
|
// into the DI-logger bootstrap line so an xllog reader sees the
|
||||||
|
// same signature on every plugin load.
|
||||||
|
//
|
||||||
|
// Both files live as embedded resources under HellionChat.Branding.* so
|
||||||
|
// the plugin DLL is self-contained — no on-disk asset lookup that could
|
||||||
|
// silently miss after a partial deploy.
|
||||||
|
internal static class HellionForgeAscii
|
||||||
|
{
|
||||||
|
private static string? _foxBanner;
|
||||||
|
private static string? _foxMini;
|
||||||
|
|
||||||
|
public static string FoxBanner => _foxBanner ??= Load("HellionChat.Branding.fox-banner.txt");
|
||||||
|
|
||||||
|
public static string FoxMini => _foxMini ??= Load("HellionChat.Branding.fox-mini.txt");
|
||||||
|
|
||||||
|
private static string Load(string resourceName)
|
||||||
|
{
|
||||||
|
using var stream = typeof(HellionForgeAscii).Assembly.GetManifestResourceStream(
|
||||||
|
resourceName
|
||||||
|
);
|
||||||
|
if (stream is null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
internal sealed class Commands : IDisposable
|
internal sealed class Commands : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, CommandWrapper> Registered = [];
|
private readonly Dictionary<string, CommandWrapper> Registered = [];
|
||||||
|
private readonly ILogger<Commands> _logger;
|
||||||
|
|
||||||
|
public Commands(ILogger<Commands> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -52,7 +59,7 @@ internal sealed class Commands : IDisposable
|
|||||||
{
|
{
|
||||||
if (!Registered.TryGetValue(command, out var wrapper))
|
if (!Registered.TryGetValue(command, out var wrapper))
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning($"Missing registration for command {command}");
|
_logger.LogWarning($"Missing registration for command {command}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +69,7 @@ internal sealed class Commands : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, $"Error while executing command {command}");
|
_logger.LogError(ex, $"Error while executing command {command}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 16;
|
private const int LatestVersion = 17;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
// silently, like before.
|
// silently, like before.
|
||||||
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
Plugin.LogProxy.Warning(
|
||||||
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
||||||
type,
|
type,
|
||||||
PrivacyPersistUnknownChannels
|
PrivacyPersistUnknownChannels
|
||||||
@@ -100,12 +100,33 @@ public class Configuration : IPluginConfiguration
|
|||||||
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
||||||
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
|
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
|
||||||
public bool FirstRunCompleted;
|
public bool FirstRunCompleted;
|
||||||
|
|
||||||
|
// Tracks which plugin version last surfaced the first-run wizard.
|
||||||
|
// When the running version is newer than this, Plugin.LoadAsync
|
||||||
|
// re-opens the wizard once so existing users see major UX reworks
|
||||||
|
// (e.g. the v1.5.2 multi-step rewrite). Skip path and Finish both
|
||||||
|
// set FirstRunCompleted = true on close, so the wizard only fires
|
||||||
|
// once per version bump even if the user dismisses it.
|
||||||
|
public string WizardLastShownVersion = string.Empty;
|
||||||
|
|
||||||
public bool UseHellionFont = true;
|
public bool UseHellionFont = true;
|
||||||
public bool ShowHonorificTitleInHeader = true;
|
public bool ShowHonorificTitleInHeader = true;
|
||||||
|
|
||||||
|
// v1.4.7 opt-in: renders the Honorific glow outline when the title carries
|
||||||
|
// a Glow colour. Default OFF — keeps v1.4.6 visuals untouched for users
|
||||||
|
// who don't care, and dodges the per-frame DrawList overhead on low-end
|
||||||
|
// hardware. Gradient (Color3 / GradientColourSet) is parsed but rendered
|
||||||
|
// as the primary Color until a later cycle ports the animation.
|
||||||
|
public bool ShowHonorificGlow;
|
||||||
public bool EnableAutoTellTabs = true;
|
public bool EnableAutoTellTabs = true;
|
||||||
public int AutoTellTabsLimit = 15;
|
public int AutoTellTabsLimit = 15;
|
||||||
public bool AutoTellTabsCompactDisplay;
|
public bool AutoTellTabsCompactDisplay;
|
||||||
public int AutoTellTabsHistoryPreload = 20;
|
public int AutoTellTabsHistoryPreload = 20;
|
||||||
|
|
||||||
|
// Sidebar width in pixels. Default 44 mirrors the icon-only layout from
|
||||||
|
// v1.2.0; users can widen up to 160 to fit a section-header line like
|
||||||
|
// "Active Tells (3)" without truncation.
|
||||||
|
public int SidebarWidth = 44;
|
||||||
public bool AutoTellTabsShowGreetedToggle;
|
public bool AutoTellTabsShowGreetedToggle;
|
||||||
public bool SeenPopOutInputHint;
|
public bool SeenPopOutInputHint;
|
||||||
public bool PopOutInputEnabled = true;
|
public bool PopOutInputEnabled = true;
|
||||||
@@ -164,6 +185,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool SortAutoTranslate;
|
public bool SortAutoTranslate;
|
||||||
public bool CollapseDuplicateMessages;
|
public bool CollapseDuplicateMessages;
|
||||||
public bool CollapseKeepUniqueLinks;
|
public bool CollapseKeepUniqueLinks;
|
||||||
|
public bool SymbolPickerEnabled = true;
|
||||||
public bool PlaySounds = true;
|
public bool PlaySounds = true;
|
||||||
public bool KeepInputFocus = true;
|
public bool KeepInputFocus = true;
|
||||||
public int MaxLinesToRender = 2_500; // 1-10000
|
public int MaxLinesToRender = 2_500; // 1-10000
|
||||||
@@ -258,6 +280,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
SortAutoTranslate = other.SortAutoTranslate;
|
SortAutoTranslate = other.SortAutoTranslate;
|
||||||
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
||||||
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
||||||
|
SymbolPickerEnabled = other.SymbolPickerEnabled;
|
||||||
PlaySounds = other.PlaySounds;
|
PlaySounds = other.PlaySounds;
|
||||||
KeepInputFocus = other.KeepInputFocus;
|
KeepInputFocus = other.KeepInputFocus;
|
||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
@@ -278,16 +301,20 @@ public class Configuration : IPluginConfiguration
|
|||||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||||
|
|
||||||
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||||
// not destroy open tell conversations. For persistent tabs, capture
|
// not destroy open tell conversations. Pinned TempTabs are persistent
|
||||||
// the live MessageList and LastSendUnread by Identifier before the
|
// and come through `other` like regular tabs; unpinned TempTabs are
|
||||||
// replace and restore them onto the freshly cloned tabs; new tabs
|
// session-only and held from the local state. For persistent tabs
|
||||||
// get an empty MessageList, deleted tabs lose their history (intended).
|
// (incl. pinned), capture live runtime state by Identifier and restore
|
||||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
// it onto the freshly cloned tabs — CurrentChannel is critical because
|
||||||
var livePersistentSession = Tabs.Where(t => !t.IsTempTab)
|
// the user may have switched channel in-game between settings-open
|
||||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
|
// and settings-save, and we'd otherwise overwrite that with the
|
||||||
|
// settings-time snapshot.
|
||||||
|
var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||||
|
var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t))
|
||||||
|
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread, t.CurrentChannel));
|
||||||
|
|
||||||
Tabs = other
|
Tabs = other
|
||||||
.Tabs.Where(t => !t.IsTempTab)
|
.Tabs.Where(t => !t.IsTempTab || t.IsPinned)
|
||||||
.Select(t =>
|
.Select(t =>
|
||||||
{
|
{
|
||||||
var clone = t.Clone();
|
var clone = t.Clone();
|
||||||
@@ -295,11 +322,12 @@ public class Configuration : IPluginConfiguration
|
|||||||
{
|
{
|
||||||
clone.Messages = live.Messages;
|
clone.Messages = live.Messages;
|
||||||
clone.LastSendUnread = live.LastSendUnread;
|
clone.LastSendUnread = live.LastSendUnread;
|
||||||
|
clone.CurrentChannel = live.CurrentChannel;
|
||||||
}
|
}
|
||||||
return clone;
|
return clone;
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
Tabs.AddRange(liveTempTabs);
|
Tabs.AddRange(liveUnpinnedTempTabs);
|
||||||
|
|
||||||
ChatTabForward = other.ChatTabForward;
|
ChatTabForward = other.ChatTabForward;
|
||||||
ChatTabBackward = other.ChatTabBackward;
|
ChatTabBackward = other.ChatTabBackward;
|
||||||
@@ -317,8 +345,10 @@ public class Configuration : IPluginConfiguration
|
|||||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||||
|
|
||||||
FirstRunCompleted = other.FirstRunCompleted;
|
FirstRunCompleted = other.FirstRunCompleted;
|
||||||
|
WizardLastShownVersion = other.WizardLastShownVersion;
|
||||||
UseHellionFont = other.UseHellionFont;
|
UseHellionFont = other.UseHellionFont;
|
||||||
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
||||||
|
ShowHonorificGlow = other.ShowHonorificGlow;
|
||||||
|
|
||||||
// v1.1.0 theme engine fields
|
// v1.1.0 theme engine fields
|
||||||
Theme = other.Theme;
|
Theme = other.Theme;
|
||||||
@@ -330,6 +360,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||||||
|
SidebarWidth = other.SidebarWidth;
|
||||||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||||||
|
|
||||||
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||||||
@@ -404,6 +435,11 @@ public class Tab
|
|||||||
public bool HideWhenInactive;
|
public bool HideWhenInactive;
|
||||||
|
|
||||||
public bool IsTempTab;
|
public bool IsTempTab;
|
||||||
|
|
||||||
|
// Pinned TempTabs survive plugin reload and logout — tester feedback from
|
||||||
|
// Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs)
|
||||||
|
// separate from the AutoTellTabsLimit bucket.
|
||||||
|
public bool IsPinned;
|
||||||
public bool AllSenderMessages;
|
public bool AllSenderMessages;
|
||||||
public TellTarget TellTarget = TellTarget.Empty();
|
public TellTarget TellTarget = TellTarget.Empty();
|
||||||
|
|
||||||
@@ -511,6 +547,7 @@ public class Tab
|
|||||||
HideInBattle = HideInBattle,
|
HideInBattle = HideInBattle,
|
||||||
HideWhenInactive = HideWhenInactive,
|
HideWhenInactive = HideWhenInactive,
|
||||||
IsTempTab = IsTempTab,
|
IsTempTab = IsTempTab,
|
||||||
|
IsPinned = IsPinned,
|
||||||
AllSenderMessages = AllSenderMessages,
|
AllSenderMessages = AllSenderMessages,
|
||||||
TellTarget = TellTarget.Clone(),
|
TellTarget = TellTarget.Clone(),
|
||||||
IsGreeted = IsGreeted,
|
IsGreeted = IsGreeted,
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ public static class EmoteCache
|
|||||||
t =>
|
t =>
|
||||||
{
|
{
|
||||||
if (t.IsFaulted)
|
if (t.IsFaulted)
|
||||||
Plugin.Log.Error(t.Exception!, $"EmoteCache load failed for {emoteCode}");
|
Plugin.LogProxy.Error(
|
||||||
|
t.Exception!,
|
||||||
|
$"EmoteCache load failed for {emoteCode}"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
TaskScheduler.Default
|
TaskScheduler.Default
|
||||||
)
|
)
|
||||||
@@ -158,7 +161,7 @@ public static class EmoteCache
|
|||||||
{
|
{
|
||||||
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
||||||
State = LoadingState.Unloaded;
|
State = LoadingState.Unloaded;
|
||||||
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
|
Plugin.LogProxy.Error(ex, "BetterTTV cache wasn't initialized");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +217,7 @@ public static class EmoteCache
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Plugin.Log.Error("Failed to convert");
|
Plugin.LogProxy.Error("Failed to convert");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +307,7 @@ public static class EmoteCache
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +411,7 @@ public static class EmoteCache
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+154
-111
@@ -1,23 +1,44 @@
|
|||||||
using Dalamud;
|
using Dalamud;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.FontIdentifier;
|
using Dalamud.Interface.FontIdentifier;
|
||||||
using Dalamud.Interface.GameFonts;
|
using Dalamud.Interface.GameFonts;
|
||||||
using Dalamud.Interface.ManagedFontAtlas;
|
using Dalamud.Interface.ManagedFontAtlas;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
public class FontManager
|
// Two LogProxy sites live in static methods (TryGetHellionFontBytes,
|
||||||
|
// AddFontWithFallback); a ctor-injected ILogger would not be reachable
|
||||||
|
// from those scopes, so the class stays on Plugin.LogProxy.
|
||||||
|
//
|
||||||
|
// Hybrid handle model: Axis and AxisItalic mirror the game's current
|
||||||
|
// font state and are init-only. FontAwesome reuses Dalamud's UiBuilder
|
||||||
|
// fixed-width icon handle and is likewise init-only. RegularFont and
|
||||||
|
// ItalicFont depend on user-toggleable settings and get replaced live
|
||||||
|
// via RebuildDelegateFonts when those settings change; they stay as
|
||||||
|
// mutable nullable fields.
|
||||||
|
//
|
||||||
|
// The four atlas-owned handles register inside a single
|
||||||
|
// SuppressAutoRebuild block so the font atlas only rebuilds once for the
|
||||||
|
// whole plugin start instead of once per handle. FontAwesome lives
|
||||||
|
// outside that accounting because the UiBuilder already owns it.
|
||||||
|
public sealed class FontManager : IDisposable
|
||||||
{
|
{
|
||||||
internal IFontHandle Axis = null!;
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
internal IFontHandle AxisItalic = null!;
|
|
||||||
|
|
||||||
internal IFontHandle RegularFont = null!;
|
internal IFontHandle Axis { get; init; }
|
||||||
|
internal IFontHandle AxisItalic { get; init; }
|
||||||
|
internal IFontHandle FontAwesome { get; init; }
|
||||||
|
|
||||||
|
// Mutable because the live font settings replace these via
|
||||||
|
// RebuildDelegateFonts. Reference replacement is atomic for reference
|
||||||
|
// types, so push sites that read the field once per frame see at most
|
||||||
|
// one stale handle.
|
||||||
|
internal IFontHandle? RegularFont;
|
||||||
internal IFontHandle? ItalicFont;
|
internal IFontHandle? ItalicFont;
|
||||||
|
|
||||||
internal IFontHandle FontAwesome = null!;
|
|
||||||
|
|
||||||
private ushort[] Ranges = [];
|
private ushort[] Ranges = [];
|
||||||
private ushort[] JpRange = [];
|
private ushort[] JpRange = [];
|
||||||
|
|
||||||
@@ -44,10 +65,128 @@ public class FontManager
|
|||||||
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
||||||
private static byte[]? HellionFontBytes;
|
private static byte[]? HellionFontBytes;
|
||||||
|
|
||||||
// Returns null when the embedded font resource is missing. Should never
|
public FontManager(IDalamudPluginInterface pluginInterface)
|
||||||
|
{
|
||||||
|
_pluginInterface = pluginInterface;
|
||||||
|
SetUpRanges();
|
||||||
|
|
||||||
|
var atlas = _pluginInterface.UiBuilder.FontAtlas;
|
||||||
|
|
||||||
|
using (atlas.SuppressAutoRebuild())
|
||||||
|
{
|
||||||
|
Axis = atlas.NewGameFontHandle(
|
||||||
|
new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
|
||||||
|
);
|
||||||
|
|
||||||
|
AxisItalic = atlas.NewGameFontHandle(
|
||||||
|
new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
|
||||||
|
{
|
||||||
|
SkewStrength = SizeInPx(Plugin.Config.FontSizeV2) / 6,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
FontAwesome = _pluginInterface.UiBuilder.IconFontFixedWidthHandle;
|
||||||
|
|
||||||
|
RegularFont = BuildRegularFontHandle(atlas);
|
||||||
|
|
||||||
|
if (Plugin.Config.ItalicEnabled)
|
||||||
|
ItalicFont = BuildItalicFontHandle(atlas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the settings save path when one of the font-related
|
||||||
|
// settings changed. Game fonts and FontAwesome stay untouched because
|
||||||
|
// none of those settings affect them.
|
||||||
|
//
|
||||||
|
// Thread model: the settings save path runs on the ImGui draw thread,
|
||||||
|
// same as every push site. The rebuild finishes synchronously before
|
||||||
|
// the next push reads the field in the same frame, so there is no
|
||||||
|
// cross-thread race on the handle reference.
|
||||||
|
public void RebuildDelegateFonts()
|
||||||
|
{
|
||||||
|
SetUpRanges();
|
||||||
|
|
||||||
|
var atlas = _pluginInterface.UiBuilder.FontAtlas;
|
||||||
|
|
||||||
|
RegularFont?.Dispose();
|
||||||
|
RegularFont = BuildRegularFontHandle(atlas);
|
||||||
|
|
||||||
|
ItalicFont?.Dispose();
|
||||||
|
ItalicFont = Plugin.Config.ItalicEnabled ? BuildItalicFontHandle(atlas) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance method so Ranges / JpRange are reachable without parameter
|
||||||
|
// plumbing; PascalCase field names follow the existing class style.
|
||||||
|
private IFontHandle BuildRegularFontHandle(IFontAtlas atlas) =>
|
||||||
|
atlas.NewDelegateFontHandle(e =>
|
||||||
|
e.OnPreBuild(tk =>
|
||||||
|
{
|
||||||
|
// UseHellionFont swaps the source font but keeps the size
|
||||||
|
// selector tied to FontSizeV2 (the Hellion font ships as
|
||||||
|
// a single weight).
|
||||||
|
var basePt = Plugin.Config.UseHellionFont
|
||||||
|
? Plugin.Config.FontSizeV2
|
||||||
|
: Plugin.Config.GlobalFontV2.SizePt;
|
||||||
|
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
||||||
|
// Missing embedded resource falls back to the configured
|
||||||
|
// system font instead of taking the whole UiBuilder down.
|
||||||
|
var hellionBytes = Plugin.Config.UseHellionFont ? TryGetHellionFontBytes() : null;
|
||||||
|
config.MergeFont = hellionBytes is not null
|
||||||
|
? tk.AddFontFromMemory(hellionBytes, config, "Hellion-Exo2")
|
||||||
|
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||||
|
|
||||||
|
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||||
|
config.GlyphRanges = JpRange;
|
||||||
|
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
|
||||||
|
|
||||||
|
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
|
||||||
|
tk.AddGameSymbol(config);
|
||||||
|
|
||||||
|
tk.Font = config.MergeFont;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
private IFontHandle BuildItalicFontHandle(IFontAtlas atlas) =>
|
||||||
|
atlas.NewDelegateFontHandle(e =>
|
||||||
|
e.OnPreBuild(tk =>
|
||||||
|
{
|
||||||
|
var config = new SafeFontConfig
|
||||||
|
{
|
||||||
|
SizePt = Plugin.Config.ItalicFontV2.SizePt,
|
||||||
|
GlyphRanges = Ranges,
|
||||||
|
};
|
||||||
|
config.MergeFont = AddFontWithFallback(
|
||||||
|
tk,
|
||||||
|
Plugin.Config.ItalicFontV2.FontId,
|
||||||
|
config,
|
||||||
|
"italic"
|
||||||
|
);
|
||||||
|
|
||||||
|
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||||
|
config.GlyphRanges = JpRange;
|
||||||
|
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
|
||||||
|
|
||||||
|
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
|
||||||
|
tk.AddGameSymbol(config);
|
||||||
|
|
||||||
|
tk.Font = config.MergeFont;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Axis.Dispose();
|
||||||
|
AxisItalic.Dispose();
|
||||||
|
// FontAwesome is shared with the UiBuilder; the host owns its
|
||||||
|
// lifetime, so the plugin must not dispose it.
|
||||||
|
RegularFont?.Dispose();
|
||||||
|
ItalicFont?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns null when the embedded font resource is missing. Should not
|
||||||
// happen on a signed release build, but a broken csproj or hand-rolled
|
// happen on a signed release build, but a broken csproj or hand-rolled
|
||||||
// dev build can land here. Caller falls back to the system font path so
|
// dev build can land here. Caller falls back to the system font path
|
||||||
// the plugin still loads instead of crashing the whole UiBuilder.
|
// so the plugin still loads instead of crashing the whole UiBuilder.
|
||||||
private static byte[]? TryGetHellionFontBytes()
|
private static byte[]? TryGetHellionFontBytes()
|
||||||
{
|
{
|
||||||
if (HellionFontBytes is not null)
|
if (HellionFontBytes is not null)
|
||||||
@@ -58,7 +197,7 @@ public class FontManager
|
|||||||
);
|
);
|
||||||
if (stream is null)
|
if (stream is null)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
Plugin.LogProxy.Warning(
|
||||||
"Hellion font resource missing — falling back to system default font."
|
"Hellion font resource missing — falling back to system default font."
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
@@ -95,10 +234,8 @@ public class FontManager
|
|||||||
foreach (var c in reader.Glyphs)
|
foreach (var c in reader.Glyphs)
|
||||||
builder.AddChar(c.Char);
|
builder.AddChar(c.Char);
|
||||||
|
|
||||||
// various symbols
|
|
||||||
// French
|
// French
|
||||||
// Romanian
|
// Romanian
|
||||||
// builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─\~");
|
|
||||||
builder.AddText("Œœ");
|
builder.AddText("Œœ");
|
||||||
builder.AddText("ĂăÂâÎîȘșȚț");
|
builder.AddText("ĂăÂâÎîȘșȚț");
|
||||||
|
|
||||||
@@ -119,100 +256,6 @@ public class FontManager
|
|||||||
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU-bound build offloaded to Task.Run; runs parallel with theme init
|
|
||||||
public async Task BuildFontsAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
await Task.Run(BuildFonts, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void BuildFonts()
|
|
||||||
{
|
|
||||||
SetUpRanges();
|
|
||||||
|
|
||||||
Axis = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(
|
|
||||||
new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
|
|
||||||
);
|
|
||||||
AxisItalic = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(
|
|
||||||
new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
|
|
||||||
{
|
|
||||||
SkewStrength = SizeInPx(Plugin.Config.FontSizeV2) / 6,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
FontAwesome = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
|
||||||
{
|
|
||||||
e.OnPreBuild(tk =>
|
|
||||||
tk.AddFontAwesomeIconFont(new SafeFontConfig { SizePx = GetFontSize() })
|
|
||||||
);
|
|
||||||
e.OnPostBuild(tk => tk.FitRatio(tk.Font));
|
|
||||||
});
|
|
||||||
|
|
||||||
RegularFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
|
||||||
e.OnPreBuild(tk =>
|
|
||||||
{
|
|
||||||
// v1.2.0: UseHellionFont controls font size selection
|
|
||||||
var basePt = Plugin.Config.UseHellionFont
|
|
||||||
? Plugin.Config.FontSizeV2
|
|
||||||
: Plugin.Config.GlobalFontV2.SizePt;
|
|
||||||
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
|
||||||
// F10.2: if the embedded font is missing, drop to the system font
|
|
||||||
// path rather than letting the UiBuilder throw.
|
|
||||||
var hellionBytes = Plugin.Config.UseHellionFont ? TryGetHellionFontBytes() : null;
|
|
||||||
config.MergeFont = hellionBytes is not null
|
|
||||||
? tk.AddFontFromMemory(hellionBytes, config, "Hellion-Exo2")
|
|
||||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
|
||||||
|
|
||||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
|
||||||
config.GlyphRanges = JpRange;
|
|
||||||
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
|
|
||||||
|
|
||||||
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
|
|
||||||
tk.AddGameSymbol(config);
|
|
||||||
|
|
||||||
tk.Font = config.MergeFont;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Plugin.Config.ItalicEnabled)
|
|
||||||
{
|
|
||||||
ItalicFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
|
||||||
e.OnPreBuild(tk =>
|
|
||||||
{
|
|
||||||
var config = new SafeFontConfig
|
|
||||||
{
|
|
||||||
SizePt = Plugin.Config.ItalicFontV2.SizePt,
|
|
||||||
GlyphRanges = Ranges,
|
|
||||||
};
|
|
||||||
config.MergeFont = AddFontWithFallback(
|
|
||||||
tk,
|
|
||||||
Plugin.Config.ItalicFontV2.FontId,
|
|
||||||
config,
|
|
||||||
"italic"
|
|
||||||
);
|
|
||||||
|
|
||||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
|
||||||
config.GlyphRanges = JpRange;
|
|
||||||
AddFontWithFallback(
|
|
||||||
tk,
|
|
||||||
Plugin.Config.JapaneseFontV2.FontId,
|
|
||||||
config,
|
|
||||||
"japanese"
|
|
||||||
);
|
|
||||||
|
|
||||||
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
|
|
||||||
tk.AddGameSymbol(config);
|
|
||||||
|
|
||||||
tk.Font = config.MergeFont;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ItalicFont = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add font with fallback to NotoSansCjkRegular if unavailable
|
// Add font with fallback to NotoSansCjkRegular if unavailable
|
||||||
private static ImFontPtr AddFontWithFallback(
|
private static ImFontPtr AddFontWithFallback(
|
||||||
IFontAtlasBuildToolkitPreBuild tk,
|
IFontAtlasBuildToolkitPreBuild tk,
|
||||||
@@ -234,10 +277,10 @@ public class FontManager
|
|||||||
or ArgumentException
|
or ArgumentException
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// Atlas-toolkit throws span IO and validation failures; routing the
|
// Atlas-toolkit throws span IO and validation failures; routing
|
||||||
// wider set through the fallback keeps a corrupt font config from
|
// the wider set through the fallback keeps a corrupt font config
|
||||||
// taking down the whole atlas build.
|
// from taking down the whole atlas build.
|
||||||
Plugin.Log.Warning(
|
Plugin.LogProxy.Warning(
|
||||||
e,
|
e,
|
||||||
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
||||||
+ "falling back to NotoSansCjkRegular"
|
+ "falling back to NotoSansCjkRegular"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using InteropGenerator.Runtime;
|
using InteropGenerator.Runtime;
|
||||||
using Lumina.Text.ReadOnly;
|
using Lumina.Text.ReadOnly;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
@@ -98,9 +99,12 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
private long LastPlayerNameDisplayTypeRefresh;
|
private long LastPlayerNameDisplayTypeRefresh;
|
||||||
private PlayerNameDisplayType CurrentPlayerNameDisplayType = PlayerNameDisplayType.FullName;
|
private PlayerNameDisplayType CurrentPlayerNameDisplayType = PlayerNameDisplayType.FullName;
|
||||||
|
|
||||||
public Chat(Plugin plugin)
|
private readonly ILogger<Chat> _logger;
|
||||||
|
|
||||||
|
public Chat(Plugin plugin, ILogger<Chat> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||||
|
|
||||||
ChatLogRefreshHook?.Enable();
|
ChatLogRefreshHook?.Enable();
|
||||||
@@ -236,7 +240,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -266,7 +270,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1; // Prevent vanilla chat log from gaining focus
|
return 1; // Prevent vanilla chat log from gaining focus
|
||||||
@@ -299,7 +303,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||||
worldId = agent->TellWorldId;
|
worldId = agent->TellWorldId;
|
||||||
Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}");
|
_logger.LogDebug($"Detected tell target '[redacted]'@{worldId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||||
@@ -358,7 +362,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +412,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +628,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
if (contentId == 0)
|
if (contentId == 0)
|
||||||
{
|
{
|
||||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
|||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using Lumina.Excel;
|
using Lumina.Excel;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
@@ -37,14 +38,20 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
|
private readonly ILogger<GameFunctions> _logger;
|
||||||
internal KeybindManager KeybindManager { get; }
|
internal KeybindManager KeybindManager { get; }
|
||||||
internal Chat Chat { get; }
|
internal Chat Chat { get; }
|
||||||
|
|
||||||
internal GameFunctions(Plugin plugin)
|
internal GameFunctions(
|
||||||
|
Plugin plugin,
|
||||||
|
ILogger<GameFunctions> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
KeybindManager = new KeybindManager(plugin);
|
_logger = logger;
|
||||||
Chat = new Chat(Plugin);
|
KeybindManager = new KeybindManager(plugin, loggerFactory.CreateLogger<KeybindManager>());
|
||||||
|
Chat = new Chat(Plugin, loggerFactory.CreateLogger<Chat>());
|
||||||
|
|
||||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||||
ResolveTextCommandPlaceholderHook?.Enable();
|
ResolveTextCommandPlaceholderHook?.Enable();
|
||||||
@@ -215,7 +222,8 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(e, "Unable to open adventurer plate");
|
// Static method, no instance _logger reachable here.
|
||||||
|
Plugin.LogProxy.Warning(e, "Unable to open adventurer plate");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +263,7 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||||
if (byteCount >= PlaceholderBufferSize)
|
if (byteCount >= PlaceholderBufferSize)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
||||||
);
|
);
|
||||||
ReplacementName = null;
|
ReplacementName = null;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.UI;
|
|||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.GameFunctions.Types;
|
using HellionChat.GameFunctions.Types;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
|
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
@@ -306,9 +307,12 @@ internal unsafe class KeybindManager : IDisposable
|
|||||||
// VirtualKey.OEM_CLEAR,
|
// VirtualKey.OEM_CLEAR,
|
||||||
};
|
};
|
||||||
|
|
||||||
internal KeybindManager(Plugin plugin)
|
private readonly ILogger<KeybindManager> _logger;
|
||||||
|
|
||||||
|
internal KeybindManager(Plugin plugin, ILogger<KeybindManager> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||||
|
|
||||||
// Handle keybinds from the game on every tick.
|
// Handle keybinds from the game on every tick.
|
||||||
@@ -507,7 +511,7 @@ internal unsafe class KeybindManager : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
<Version>1.4.6</Version>
|
<Version>1.5.2</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Use lock file to pin exact versions -->
|
<!-- Use lock file to pin exact versions -->
|
||||||
@@ -14,7 +14,15 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
||||||
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||||
|
<!-- v1.5.0 DI-container foundation; matches Lightless pin (Hosting 10.0.7) -->
|
||||||
|
<PackageReference
|
||||||
|
Include="Microsoft.Extensions.DependencyInjection"
|
||||||
|
Version="[10.0.7, 11.0.0)"
|
||||||
|
/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="[10.0.7, 11.0.0)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="[10.0.7, 11.0.0)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="[10.0.7, 11.0.0)" />
|
||||||
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
||||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||||
@@ -50,14 +58,21 @@
|
|||||||
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
||||||
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Resources\Branding\fox-banner.txt">
|
||||||
|
<LogicalName>HellionChat.Branding.fox-banner.txt</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Resources\Branding\fox-mini.txt">
|
||||||
|
<LogicalName>HellionChat.Branding.fox-mini.txt</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="Themes\Builtin\example-theme.json">
|
<EmbeddedResource Include="Themes\Builtin\example-theme.json">
|
||||||
<LogicalName>HellionChat.Themes.Builtin.example-theme.json</LogicalName>
|
<LogicalName>HellionChat.Themes.Builtin.example-theme.json</LogicalName>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Plugin icon: copy images/* to output for Dalamud discovery -->
|
<!-- Plugin icon: copy images/* to output for Dalamud discovery. ASCII
|
||||||
|
study folder is source-only material, no need to ship it. -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="images\**">
|
<None Include="images\**" Exclude="images\ascii\**">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+170
-75
@@ -35,100 +35,195 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**
|
**v1.5.2 — First-Run Wizard Rework (2026-05-18)**
|
||||||
|
|
||||||
Maintenance patch. No user-visible behaviour changes; tightens the
|
UX patch. The first-run wizard becomes a four-step flow with a
|
||||||
development feedback loop, fixes two upstream-inherited bugs, and
|
new Roleplay privacy profile and a power-settings step that
|
||||||
prepares the code for the v1.4.7 backlog cleanup.
|
surfaces previously-hidden defaults. Existing v1.5.1 users see
|
||||||
|
the new wizard once on first v1.5.2 boot.
|
||||||
|
|
||||||
- preflight.sh gains a csharpier reflow check and a markdownlint
|
What changes user-visible:
|
||||||
pass so style drift and markdown violations are caught at the
|
|
||||||
pre-push gate
|
- Wizard navigation: Welcome → Privacy profile → Power settings
|
||||||
- FontManager fallback catches the full set of atlas-toolkit
|
→ Done. Forge-Bronze pagination dots, dedicated stage for the
|
||||||
throws (IO, InvalidOperation, ArgumentException) — a corrupt
|
power settings so they are no longer buried in Settings.
|
||||||
font config no longer takes down the whole atlas build
|
- Fourth privacy profile "Roleplay": Privacy-First plus Say and
|
||||||
- BrandingLinks and IntegrationLinks URLs validated on plugin
|
both emote types, with a 30-day window for Say and a 90-day
|
||||||
load — a typo in a future URL rotation now throws at startup
|
window for emotes. Shout, Yell and Novice Network stay out.
|
||||||
- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel
|
- Privacy picker becomes a 2x2 grid. Casual stays the
|
||||||
no longer leaks the native Utf8String when the linkshell check
|
recommended option with a ★ marker.
|
||||||
rejects the channel
|
- Power-settings step covers Load Previous Session, Filter
|
||||||
- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now
|
Include Previous Sessions, Auto-Tell-Tabs History Preload,
|
||||||
deep-clones UsedChannel and TellTarget — PopOut and Temp tabs
|
Compact Density, Prettier Timestamps and a built-in theme
|
||||||
no longer mutate each other's channel state
|
picker. All six map to existing Configuration fields — no new
|
||||||
- Active-tab underline scales with DPI and rounds to physical
|
settings introduced.
|
||||||
pixels for crisp rendering above 100% scaling
|
- Staged commit: the wizard only writes to Config on the Finish
|
||||||
- IconButton width parameter no longer subtracts HUD-scaled
|
step. Decide-later or X-close at any point leaves the existing
|
||||||
padding from a raw int (measured width passes through verbatim)
|
config untouched.
|
||||||
- Internal: HellionStyle ChildBgAlpha extracted to a testable
|
- Inline test hint on the done step: "type /tell <Player Name>
|
||||||
helper; Plugin.SaveConfig clones only the temp tabs;
|
into chat" surfaces the auto-tell-tab spawn mechanism.
|
||||||
SettingsOverview caches the draw-list per frame;
|
- Window starts at 720x480 (was 900x560) and can shrink to
|
||||||
Dalamud.Utility.Util surface routed through an IPlatformUtil
|
600x400; Step 1 keeps the fox banner in a folded TreeNode so
|
||||||
indirection (MessageStore IsWine probe is now testable in
|
the onboarding copy stays primary.
|
||||||
isolation)
|
- Existing users get the new wizard surfaced once on first boot
|
||||||
- Built-in themes: Crystal Nocturne (sapphire and electric
|
after the update via the new WizardLastShownVersion config
|
||||||
magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom.
|
field. Future cycles bump the constant only when the wizard
|
||||||
Users with Moonlit Bloom selected fall back to Hellion Arctic
|
itself changes shape.
|
||||||
on first load
|
|
||||||
|
Under the hood:
|
||||||
|
|
||||||
|
- WizardStateSmokeStep added to /xlperf alongside the FontManager
|
||||||
|
and ThemeSwitch self-tests.
|
||||||
|
- Twelve new pure-helper xUnit Facts in the Build Suite cover
|
||||||
|
all four privacy profile sets and their retention overrides.
|
||||||
|
|
||||||
|
Migration v17 stays (no schema bump). The Configuration grows
|
||||||
|
one optional string field (WizardLastShownVersion) which
|
||||||
|
defaults to empty for legacy users.
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.4.5 — UX and Robustness (2026-05-12)**
|
**v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**
|
||||||
|
|
||||||
Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw
|
Hybrid FontManager refactor plus an embedded provenance mark.
|
||||||
failures surface as a notification, the first-run wizard has an
|
|
||||||
explicit "Later" option, the input history clears on plugin reload,
|
|
||||||
and the status bar version slot stops clipping in narrow windows.
|
|
||||||
|
|
||||||
- Chat window draw errors now show a one-shot notification instead
|
What changes under the hood:
|
||||||
of failing silently — stack trace stays in /xllog
|
|
||||||
- First-run wizard: explicit "Later — keep defaults" button.
|
- FontManager handle creation moves into the ctor inside a single
|
||||||
Closing the X no longer silently accepts the defaults; the wizard
|
atlas.SuppressAutoRebuild() block. The font atlas now builds once
|
||||||
reopens on the next plugin load if nothing was picked
|
per plugin load instead of four to five times — less CPU and GPU
|
||||||
- InputHistoryService clears on plugin dispose so the previous
|
pressure in the first seconds after a reload, less atlas texture
|
||||||
session's typed commands don't bleed into the next load
|
memory churn.
|
||||||
- Status bar hides the version slot when the chat window is too
|
- Hybrid property model: Axis, AxisItalic and FontAwesome become
|
||||||
narrow to fit all five slots without overlap
|
init-only handles. RegularFont and ItalicFont stay mutable because
|
||||||
- Internal: explicit session-only Auto-Tell-Tab invariant in
|
the eight font settings still need to replace them at runtime —
|
||||||
Plugin.cs plus a pinning test in the Build-Suite
|
that path is funnelled through RebuildDelegateFonts() now and
|
||||||
- Internal: FontManager falls back to the system font if the
|
runs without a plugin reload.
|
||||||
embedded Hellion font resource is missing — logs a Warning
|
- FontAwesome reuses Dalamud's UiBuilder.IconFontFixedWidthHandle
|
||||||
|
instead of building its own atlas slot. One delegate-build step
|
||||||
|
less in the ctor.
|
||||||
|
- BuildFontsAsync and BuildFonts are removed; the live mutation
|
||||||
|
path is RebuildDelegateFonts() now.
|
||||||
|
- Two FontManager self-test steps registered with /xlperf: ctor
|
||||||
|
smoke (every handle non-null after Phase-1 resolve, no atlas
|
||||||
|
load-exception) and push smoke (Push() returns without throwing).
|
||||||
|
|
||||||
|
Honorific full-gradient port (originally the v1.5.1 main item) was
|
||||||
|
dropped: Honorific 3.2 exposes no IPC for the rendered gradient
|
||||||
|
frame, and an in-plugin port of the colour palette was declined.
|
||||||
|
The integration stays at the v1.4.7 glow-only shape.
|
||||||
|
|
||||||
|
User-visible:
|
||||||
|
|
||||||
|
- Hellion Forge signature: a small fox-head ASCII silhouette is
|
||||||
|
emitted to /xllog on every plugin load, and a full fox banner
|
||||||
|
with "Hellion Forge" set inside the body is available as a
|
||||||
|
folded TreeNode in the First-Run Wizard and Settings ->
|
||||||
|
Information tab. Drawn by Julia Moon, embedded in the plugin DLL.
|
||||||
|
- No settings changes, no migration. v17 stays.
|
||||||
|
|
||||||
|
Note on performance: the cross-plugin baseline target from v1.5.0
|
||||||
|
(matching Lightless and XIVInstantMessenger at ~7 ms HITCH) did
|
||||||
|
not land this cycle. HITCH stays around 80 ms because the cost is
|
||||||
|
in the UiBuilder first-frame render path, not in the atlas build
|
||||||
|
(which this cycle did reduce from 4-5 builds per load to 1). A
|
||||||
|
first-frame render investigation is reserved for a later cycle.
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.4.4 — Threading and IPC safety polish (2026-05-12)**
|
**v1.5.0 — DI Foundation and Service Refactor (2026-05-17)**
|
||||||
|
|
||||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
|
Major architecture cycle. The plugin bootstrap moves to a
|
||||||
assumptions are documented per-method, a hot-path lock falls
|
generic-host DI container (Microsoft.Extensions.Hosting +
|
||||||
away, and the privacy filter speaks up when an unknown ChatType
|
IServiceCollection) modelled on Lightless Sync. Service logging
|
||||||
shows up.
|
moves from a static Plugin.LogProxy locator to typed
|
||||||
|
Microsoft.Extensions.Logging.ILogger<T> via constructor injection,
|
||||||
|
bridged over Dalamud's IPluginLog by a custom DalamudLogger trio.
|
||||||
|
|
||||||
- AutoTellTabs hot-path getter uses an Interlocked counter
|
What changes under the hood:
|
||||||
instead of taking the lock on every read
|
|
||||||
- Honorific integration: per-method threading banners, plus
|
- 18 instance-class services migrate to ILogger<T> via constructor
|
||||||
Warning-level log on unsubscribe failure
|
injection across four slices: data layer (MessageStore,
|
||||||
- AutoTranslate warmup thread marked IsBackground so plugin
|
MessageManager, AutoTellTabsService), IPC and integrations
|
||||||
unload doesn't wait for it
|
(HonorificService, IpcManager, TypingIpc, ExtraChat, the three
|
||||||
- PrivacyFilter logs once per unknown ChatType so a future
|
GameFunctions classes), UI window layer (ChatLogWindow,
|
||||||
patch's added channel doesn't drop off the radar
|
DbViewer, Popout, three settings tabs), and root (Commands,
|
||||||
- New installs persist unknown channels by default; existing
|
ThemeRegistry, PayloadHandler).
|
||||||
configs keep their explicit choice
|
- Plugin.LogProxy stays in place for the eight buckets ctor
|
||||||
|
injection cannot reach: static helpers (EmoteCache,
|
||||||
|
AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-reflected
|
||||||
|
types (Configuration), the Message data class, and instance
|
||||||
|
classes that only log from static methods (FontManager, one
|
||||||
|
GameFunctions site).
|
||||||
|
- Plugin.cs finishes at 1012 lines — virtually identical to the
|
||||||
|
pre-cycle 1013. The new Phase-1 host build and Plugin.X bridge
|
||||||
|
wiring trade out exactly the service and window allocations
|
||||||
|
that previously lived in LoadAsync.
|
||||||
|
- Cross-plugin baseline confirms no performance penalty against
|
||||||
|
Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2
|
||||||
|
74 ms median. Lightless and XIVInstantMessenger sit around
|
||||||
|
7 ms by deferring their font-atlas build past Finished
|
||||||
|
loading — that pattern is the v1.5.1 follow-up.
|
||||||
|
|
||||||
|
User-visible:
|
||||||
|
|
||||||
|
- Slash-command insert fix: pasting a slash command into the
|
||||||
|
chat input (Friend List "/tell" action, plugin-driven inserts
|
||||||
|
from Artisan, AllaganTools etc.) now replaces the existing
|
||||||
|
input instead of concatenating. Cherry-picked from ChatTwo
|
||||||
|
upstream ee7768ac with namespace adaptation.
|
||||||
|
|
||||||
|
Migration v17 stays (no schema bump).
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.4.3 — Faster plugin load + new repo (2026-05-08)**
|
**v1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)**
|
||||||
|
|
||||||
Heavy startup work (migrations, hooks, windows) now runs async so
|
Eleventh and final sub-patch of the v1.4.x polish-sweep series.
|
||||||
Dalamud's UI stays responsive during load. Load time is comparable
|
Symbol picker for the chat input, a tell-history reload fix for
|
||||||
to v1.4.2 — this is the foundation for v1.4.4 optimisations.
|
users with many active partners, and a closing cleanup sweep
|
||||||
|
before v1.5.0 picks up the DI-container adoption.
|
||||||
|
|
||||||
- Two-phase async load via IAsyncDalamudPlugin
|
- Symbol picker: a small smile-icon button left of the channel
|
||||||
- Schema-gate replaces the v9→v16 migration chain; old configs
|
indicator opens a popup with two tabs. The first lists all 161
|
||||||
require a v1.4.2 install first
|
FFXIV PUA glyphs (Dalamud's SeIconChar enum); the second
|
||||||
- AutoTranslate cache loads on first use instead of every startup
|
carries 97 server-verified BMP symbols (latin marks, currency,
|
||||||
- Custom font (Hellion-Exo2) appears with a brief pop after load
|
the full Greek alphabet, geometric shapes, suits, notes) —
|
||||||
- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL
|
every one of them round-tripped through /echo and /say in a
|
||||||
|
four-round probe so the in-channel render matches what the
|
||||||
|
picker shows. Click drops the glyph at the caret, multi-insert
|
||||||
|
keeps the popup open, and a recent-used strip floats the last
|
||||||
|
sixteen picks across both tabs. Toggle in Settings → Chat →
|
||||||
|
Message behaviour, default on.
|
||||||
|
- Pinned auto-tell tabs reload their full history again: a
|
||||||
|
hidden 500-row scan cap in PreloadHistory used to override the
|
||||||
|
user-configurable AutoTellTabsHistoryPreload setting, so
|
||||||
|
less-frequent pinned partners (rare /tell sessions in an
|
||||||
|
otherwise busy week) lost their backlog. The cap is removed;
|
||||||
|
the (Receiver, Date) index keeps SQL fast, the client-side
|
||||||
|
loop still respects your setting as the upper bound.
|
||||||
|
- Slash-command teardown: /hellion, /hellionView,
|
||||||
|
/hellionDebugger (and #if DEBUG /hellionSeString) wrappers are
|
||||||
|
now cached as private fields. Plugin teardown detaches the
|
||||||
|
live registration instead of re-Register'ing with identical
|
||||||
|
args — closes a latent maintenance hazard from v1.4.9.
|
||||||
|
- v1.4.x polish-sweep wraps up here. The ImGuiListClipper render
|
||||||
|
refactor that was on the v1.4.10 reserve list got dropped
|
||||||
|
after cross-platform smoke showed the scroll rubber-band is a
|
||||||
|
Wine / Linux render-pipeline quirk, not universal — Windows
|
||||||
|
users never saw it. It will get its own platform-targeted
|
||||||
|
spike in a later patch. Next major cycle is v1.5.0 with the
|
||||||
|
DI-container adoption (Microsoft.Extensions.Hosting +
|
||||||
|
ILogger<T>) modelled on Lightless.
|
||||||
|
- Migration v17 stays (no schema bump).
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using Dalamud.Plugin;
|
||||||
|
using HellionChat.Ipc;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Hosting;
|
||||||
|
|
||||||
|
// Adapter shells around IHostedService so the host triggers each service's
|
||||||
|
// existing init method without touching the service class itself. Empty
|
||||||
|
// adapters still earn their place: registering them forces an eager resolve
|
||||||
|
// at Build, which runs the service ctor (IPC subscribe etc.) right then
|
||||||
|
// instead of lazily on first GetRequiredService.
|
||||||
|
|
||||||
|
internal sealed class ThemeRegistryInitHostedService(ThemeRegistry registry) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Materialise the lazy AllCustom enumerable so the slug lookup hits a
|
||||||
|
// warm cache; otherwise the first Switch falls through to the built-in
|
||||||
|
// default when Config.Theme points at a custom slug.
|
||||||
|
foreach (var _ in registry.AllCustom()) { }
|
||||||
|
registry.Switch(Plugin.Config.Theme);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC subscribers do their wiring in the ctor, so StartAsync stays empty —
|
||||||
|
// the registration alone forces an eager resolve which runs that wiring.
|
||||||
|
|
||||||
|
internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly IpcManager _ipc = ipc;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TypingIpcInitHostedService(TypingIpc typingIpc) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly TypingIpc _typingIpc = typingIpc;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ExtraChatInitHostedService(ExtraChat extraChat) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ExtraChat _extraChat = extraChat;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class MessageManagerInitHostedService(
|
||||||
|
IDalamudPluginInterface pluginInterface,
|
||||||
|
MessageManager manager
|
||||||
|
) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// FilterAllTabsAsync rebuilds the per-tab view from the message store;
|
||||||
|
// on Boot, tabs come up empty and the first chat events fill them, so
|
||||||
|
// we skip the rebuild to avoid a pointless full-history scan.
|
||||||
|
if (pluginInterface.Reason is not PluginLoadReason.Boot)
|
||||||
|
manager.FilterAllTabsAsync();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService service)
|
||||||
|
: IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
service.Initialize();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
internal sealed class DalamudLogger : ILogger
|
||||||
|
{
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
|
||||||
|
public DalamudLogger(string name, IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDisposable? ILogger.BeginScope<TState>(TState state) => default!;
|
||||||
|
|
||||||
|
// Filtering happens in Dalamud's /xllog. Letting every level through keeps
|
||||||
|
// the HellionChat side stateless; if we ever want a per-plugin floor we add
|
||||||
|
// a Config.LogLevel and tighten this method.
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => true;
|
||||||
|
|
||||||
|
public void Log<TState>(
|
||||||
|
LogLevel logLevel,
|
||||||
|
EventId eventId,
|
||||||
|
TState state,
|
||||||
|
Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// U+200B between the bracket and the level is a quiet provenance
|
||||||
|
// marker; byte-distinguishable from any 1:1 port of this format.
|
||||||
|
if ((int)logLevel <= (int)LogLevel.Information)
|
||||||
|
{
|
||||||
|
_pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append($"[{_name}]{{{(int)logLevel}}} {state} {exception?.Message}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(exception?.StackTrace))
|
||||||
|
sb.AppendLine(exception.StackTrace);
|
||||||
|
|
||||||
|
var inner = exception?.InnerException;
|
||||||
|
while (inner != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"InnerException {inner}: {inner.Message}");
|
||||||
|
sb.AppendLine(inner.StackTrace);
|
||||||
|
inner = inner.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logLevel == LogLevel.Warning)
|
||||||
|
_pluginLog.Warning(sb.ToString());
|
||||||
|
else if (logLevel == LogLevel.Error)
|
||||||
|
_pluginLog.Error(sb.ToString());
|
||||||
|
else
|
||||||
|
_pluginLog.Fatal(sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using HellionChat.Branding;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
[ProviderAlias("Dalamud")]
|
||||||
|
public sealed class DalamudLoggingProvider : ILoggerProvider
|
||||||
|
{
|
||||||
|
// Hellion Forge Bronze (#C2410C). Mixed into the bootstrap fingerprint.
|
||||||
|
private const string HellionMarker = "HellionForgeBronzeC2410C";
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers = new(
|
||||||
|
StringComparer.OrdinalIgnoreCase
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
|
||||||
|
public DalamudLoggingProvider(IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
EmitBootstrapBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-shot per plugin load. Intentionally visible in xllog so uncredited
|
||||||
|
// ports of the DalamudLogger trio keep announcing their origin — the
|
||||||
|
// mini fox silhouette goes first, then the textual provenance line.
|
||||||
|
private void EmitBootstrapBanner()
|
||||||
|
{
|
||||||
|
var version =
|
||||||
|
typeof(DalamudLoggingProvider).Assembly.GetName().Version?.ToString() ?? "0.0.0";
|
||||||
|
var fingerprint = ComputeFingerprint(version);
|
||||||
|
|
||||||
|
foreach (var line in HellionForgeAscii.FoxMini.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.TrimEnd('\r');
|
||||||
|
if (trimmed.Length > 0)
|
||||||
|
_pluginLog.Information(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pluginLog.Information("by Julia Moon - Hellion Forge");
|
||||||
|
_pluginLog.Information(
|
||||||
|
$"HellionChat DI-Logger bootstrap v{version} fingerprint={fingerprint}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeFingerprint(string version)
|
||||||
|
{
|
||||||
|
var seed = Encoding.UTF8.GetBytes($"{HellionMarker}-{version}");
|
||||||
|
var hash = SHA256.HashData(seed);
|
||||||
|
var sb = new StringBuilder(8);
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
sb.Append(hash[i].ToString("x2"));
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
{
|
||||||
|
// Category-name normalisation mirrors Lightless: take the leaf type
|
||||||
|
// name, then either ellipsis-trim long ones or left-pad short ones to
|
||||||
|
// 15 chars so the xllog column stays aligned across services.
|
||||||
|
var catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last();
|
||||||
|
if (catName.Length > 15)
|
||||||
|
catName = string.Concat(
|
||||||
|
catName.AsSpan(0, 6),
|
||||||
|
"...",
|
||||||
|
catName.AsSpan(catName.Length - 6, 6)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
catName = catName.PadLeft(15);
|
||||||
|
|
||||||
|
return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _pluginLog));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_loggers.Clear();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
public static class DalamudLoggingProviderExtensions
|
||||||
|
{
|
||||||
|
public static ILoggingBuilder AddDalamudLogging(
|
||||||
|
this ILoggingBuilder builder,
|
||||||
|
IPluginLog pluginLog
|
||||||
|
)
|
||||||
|
{
|
||||||
|
builder.ClearProviders();
|
||||||
|
builder.Services.TryAddEnumerable(
|
||||||
|
ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>(
|
||||||
|
_ => new DalamudLoggingProvider(pluginLog)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace HellionChat.Integrations;
|
namespace HellionChat.Integrations;
|
||||||
@@ -23,7 +24,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
private readonly ICallGateSubscriber<object> _ready;
|
private readonly ICallGateSubscriber<object> _ready;
|
||||||
private readonly ICallGateSubscriber<object> _disposing;
|
private readonly ICallGateSubscriber<object> _disposing;
|
||||||
|
|
||||||
private readonly IPluginLog _log;
|
private readonly ILogger<HonorificService> _logger;
|
||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private bool _versionWarningLogged;
|
private bool _versionWarningLogged;
|
||||||
|
|
||||||
@@ -34,12 +35,12 @@ internal sealed class HonorificService : IDisposable
|
|||||||
|
|
||||||
public HonorificService(
|
public HonorificService(
|
||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
IPluginLog log,
|
ILogger<HonorificService> logger,
|
||||||
IFramework framework
|
IFramework framework
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_log = log;
|
_logger = logger;
|
||||||
|
|
||||||
// Gate objects are cached per-name by Dalamud and safe to register
|
// Gate objects are cached per-name by Dalamud and safe to register
|
||||||
// before Honorific loads — they just won't fire until it does.
|
// before Honorific loads — they just won't fire until it does.
|
||||||
@@ -84,7 +85,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
{
|
{
|
||||||
if (!_versionWarningLogged)
|
if (!_versionWarningLogged)
|
||||||
{
|
{
|
||||||
_log.Warning(
|
_logger.LogWarning(
|
||||||
"Honorific API version mismatch — expected major 3, "
|
"Honorific API version mismatch — expected major 3, "
|
||||||
+ "found {Major}.{Minor}. Disabling Honorific integration.",
|
+ "found {Major}.{Minor}. Disabling Honorific integration.",
|
||||||
version.Item1,
|
version.Item1,
|
||||||
@@ -104,7 +105,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Honorific not installed or not yet initialised — Ready will retry.
|
// Honorific not installed or not yet initialised — Ready will retry.
|
||||||
_log.Debug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
|
_logger.LogDebug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
|
||||||
IsAvailable = false;
|
IsAvailable = false;
|
||||||
CurrentTitle = null;
|
CurrentTitle = null;
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
{
|
{
|
||||||
// Warning not Debug — a silent unsubscribe failure leaks a live
|
// Warning not Debug — a silent unsubscribe failure leaks a live
|
||||||
// subscription across plugin reloads.
|
// subscription across plugin reloads.
|
||||||
_log.Warning(
|
_logger.LogWarning(
|
||||||
ex,
|
ex,
|
||||||
"Honorific unsubscribe failed (likely API break or gate already gone)."
|
"Honorific unsubscribe failed (likely API break or gate already gone)."
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,10 +4,19 @@ namespace HellionChat.Integrations;
|
|||||||
|
|
||||||
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||||
// so HellionChat loads cleanly when Honorific is absent.
|
// so HellionChat loads cleanly when Honorific is absent.
|
||||||
// Glow/gradient fields omitted; Cycle 1 renders primary Color only.
|
//
|
||||||
|
// Only Glow is rendered. Color3, GradientColourSet and GradientAnimationStyle
|
||||||
|
// are parsed but unused — the animated gradient lives entirely inside Honorific
|
||||||
|
// and is not exposed over IPC, so reproducing it here would mean shipping our
|
||||||
|
// own copy of Honorific's colour palette. The fields stay in the DTO so the
|
||||||
|
// JSON roundtrip remains lossless.
|
||||||
internal sealed record HonorificTitleData(
|
internal sealed record HonorificTitleData(
|
||||||
string? Title,
|
string? Title,
|
||||||
bool IsPrefix,
|
bool IsPrefix,
|
||||||
bool IsOriginal,
|
bool IsOriginal,
|
||||||
Vector3? Color
|
Vector3? Color,
|
||||||
|
Vector3? Glow,
|
||||||
|
Vector3? Color3,
|
||||||
|
int? GradientColourSet,
|
||||||
|
string? GradientAnimationStyle
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ipc;
|
namespace HellionChat.Ipc;
|
||||||
|
|
||||||
public sealed class ExtraChat : IDisposable
|
public sealed class ExtraChat : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<ExtraChat> _logger;
|
||||||
|
|
||||||
#pragma warning disable CS0649 // Assigned through IPC
|
#pragma warning disable CS0649 // Assigned through IPC
|
||||||
[Serializable]
|
[Serializable]
|
||||||
private struct OverrideInfo
|
private struct OverrideInfo
|
||||||
@@ -36,8 +39,9 @@ public sealed class ExtraChat : IDisposable
|
|||||||
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
||||||
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
||||||
|
|
||||||
internal ExtraChat()
|
internal ExtraChat(ILogger<ExtraChat> logger)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>(
|
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>(
|
||||||
"ExtraChat.OverrideChannelColour"
|
"ExtraChat.OverrideChannelColour"
|
||||||
);
|
);
|
||||||
@@ -62,7 +66,7 @@ public sealed class ExtraChat : IDisposable
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
|
// ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
|
||||||
Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
_logger.LogTrace(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ipc;
|
namespace HellionChat.Ipc;
|
||||||
|
|
||||||
@@ -19,12 +20,26 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
||||||
private ICallGateProvider<ChatInputState, object?> StateChangedGate { get; }
|
private ICallGateProvider<ChatInputState, object?> StateChangedGate { get; }
|
||||||
|
|
||||||
|
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Some third-party plugins
|
||||||
|
// have a no-fork policy and subscribe only to ChatTwo.*-prefixed IPC
|
||||||
|
// gates. HellionChat replaces ChatTwo (conflict detection prevents
|
||||||
|
// parallel loading), so mirroring the ChatTwo provider slots lets those
|
||||||
|
// plugins keep working without code changes on their side. The tuple
|
||||||
|
// shape is textually identical to ChatTwo's IPC surface (same member
|
||||||
|
// order, same underlying types — ChatType is `ushort` in both repos)
|
||||||
|
// so Dalamud's IPC marshalling matches across plugin boundaries.
|
||||||
|
private ICallGateProvider<ChatInputState> ChatTwoStateQueryGate { get; }
|
||||||
|
private ICallGateProvider<ChatInputState, object?> ChatTwoStateChangedGate { get; }
|
||||||
|
|
||||||
private ChatInputState LastState;
|
private ChatInputState LastState;
|
||||||
private bool HasState;
|
private bool HasState;
|
||||||
|
|
||||||
internal TypingIpc(Plugin plugin)
|
private readonly ILogger<TypingIpc> _logger;
|
||||||
|
|
||||||
|
internal TypingIpc(Plugin plugin, ILogger<TypingIpc> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||||
"HellionChat.GetChatInputState"
|
"HellionChat.GetChatInputState"
|
||||||
@@ -33,7 +48,16 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
"HellionChat.ChatInputStateChanged"
|
"HellionChat.ChatInputStateChanged"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// v1.4.9 R4: ChatTwo-prefixed compatibility slots (see class-level comment).
|
||||||
|
ChatTwoStateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||||
|
"ChatTwo.GetChatInputState"
|
||||||
|
);
|
||||||
|
ChatTwoStateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>(
|
||||||
|
"ChatTwo.ChatInputStateChanged"
|
||||||
|
);
|
||||||
|
|
||||||
StateQueryGate.RegisterFunc(GetState);
|
StateQueryGate.RegisterFunc(GetState);
|
||||||
|
ChatTwoStateQueryGate.RegisterFunc(GetState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatInputState BuildState()
|
private ChatInputState BuildState()
|
||||||
@@ -67,10 +91,13 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
HasState = true;
|
HasState = true;
|
||||||
LastState = state;
|
LastState = state;
|
||||||
StateChangedGate.SendMessage(state);
|
StateChangedGate.SendMessage(state);
|
||||||
|
// v1.4.9 R4: mirror on ChatTwo-prefixed slot for no-fork-policy plugins.
|
||||||
|
ChatTwoStateChangedGate.SendMessage(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
StateQueryGate.UnregisterFunc();
|
StateQueryGate.UnregisterFunc();
|
||||||
|
ChatTwoStateQueryGate.UnregisterFunc();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
internal sealed class IpcManager : IDisposable
|
internal sealed class IpcManager : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<IpcManager> _logger;
|
||||||
|
|
||||||
private ICallGateProvider<string> RegisterGate { get; }
|
private ICallGateProvider<string> RegisterGate { get; }
|
||||||
private ICallGateProvider<string, object?> UnregisterGate { get; }
|
private ICallGateProvider<string, object?> UnregisterGate { get; }
|
||||||
private ICallGateProvider<object?> AvailableGate { get; }
|
private ICallGateProvider<object?> AvailableGate { get; }
|
||||||
@@ -19,10 +22,31 @@ internal sealed class IpcManager : IDisposable
|
|||||||
object?
|
object?
|
||||||
> InvokeGate { get; }
|
> InvokeGate { get; }
|
||||||
|
|
||||||
|
// v1.4.9 R4: ChatTwo IPC compatibility mirror. Third-party plugins with
|
||||||
|
// a no-fork policy (e.g. Artisan, AllaganTools) only subscribe to the
|
||||||
|
// ChatTwo.*-prefixed context-menu integration gates. Mirroring all four
|
||||||
|
// provider slots under the ChatTwo namespace lets those plugins keep
|
||||||
|
// working without code changes on their side. Conflict detection
|
||||||
|
// prevents ChatTwo and HellionChat from loading in parallel, so no slot
|
||||||
|
// collision risk.
|
||||||
|
private ICallGateProvider<string> ChatTwoRegisterGate { get; }
|
||||||
|
private ICallGateProvider<string, object?> ChatTwoUnregisterGate { get; }
|
||||||
|
private ICallGateProvider<object?> ChatTwoAvailableGate { get; }
|
||||||
|
private ICallGateProvider<
|
||||||
|
string,
|
||||||
|
PlayerPayload?,
|
||||||
|
ulong,
|
||||||
|
Payload?,
|
||||||
|
SeString?,
|
||||||
|
SeString?,
|
||||||
|
object?
|
||||||
|
> ChatTwoInvokeGate { get; }
|
||||||
|
|
||||||
internal List<string> Registered { get; } = [];
|
internal List<string> Registered { get; } = [];
|
||||||
|
|
||||||
public IpcManager()
|
public IpcManager(ILogger<IpcManager> logger)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
|
RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
|
||||||
RegisterGate.RegisterFunc(Register);
|
RegisterGate.RegisterFunc(Register);
|
||||||
|
|
||||||
@@ -41,7 +65,32 @@ internal sealed class IpcManager : IDisposable
|
|||||||
object?
|
object?
|
||||||
>("HellionChat.Invoke");
|
>("HellionChat.Invoke");
|
||||||
|
|
||||||
|
// v1.4.9 R4: ChatTwo-prefixed mirrors of the four context-menu slots
|
||||||
|
// above. Share the same Register/Unregister backing methods so a
|
||||||
|
// plugin that subscribes via either namespace lands in the same
|
||||||
|
// Registered list. SendMessage on Invoke fans out to both gates.
|
||||||
|
ChatTwoRegisterGate = Plugin.Interface.GetIpcProvider<string>("ChatTwo.Register");
|
||||||
|
ChatTwoRegisterGate.RegisterFunc(Register);
|
||||||
|
|
||||||
|
ChatTwoAvailableGate = Plugin.Interface.GetIpcProvider<object?>("ChatTwo.Available");
|
||||||
|
|
||||||
|
ChatTwoUnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>(
|
||||||
|
"ChatTwo.Unregister"
|
||||||
|
);
|
||||||
|
ChatTwoUnregisterGate.RegisterAction(Unregister);
|
||||||
|
|
||||||
|
ChatTwoInvokeGate = Plugin.Interface.GetIpcProvider<
|
||||||
|
string,
|
||||||
|
PlayerPayload?,
|
||||||
|
ulong,
|
||||||
|
Payload?,
|
||||||
|
SeString?,
|
||||||
|
SeString?,
|
||||||
|
object?
|
||||||
|
>("ChatTwo.Invoke");
|
||||||
|
|
||||||
AvailableGate.SendMessage();
|
AvailableGate.SendMessage();
|
||||||
|
ChatTwoAvailableGate.SendMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Invoke(
|
internal void Invoke(
|
||||||
@@ -54,6 +103,8 @@ internal sealed class IpcManager : IDisposable
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||||
|
// v1.4.9 R4: fan out the same event to plugins listening on ChatTwo.Invoke.
|
||||||
|
ChatTwoInvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string Register()
|
private string Register()
|
||||||
@@ -72,6 +123,8 @@ internal sealed class IpcManager : IDisposable
|
|||||||
{
|
{
|
||||||
UnregisterGate.UnregisterAction();
|
UnregisterGate.UnregisterAction();
|
||||||
RegisterGate.UnregisterFunc();
|
RegisterGate.UnregisterFunc();
|
||||||
|
ChatTwoUnregisterGate.UnregisterAction();
|
||||||
|
ChatTwoRegisterGate.UnregisterFunc();
|
||||||
Registered.Clear();
|
Registered.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ public partial class Message
|
|||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Failed to parse extra chat channel GUID");
|
Plugin.LogProxy.Error(ex, "Failed to parse extra chat channel GUID");
|
||||||
Plugin.Log.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
Plugin.LogProxy.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
||||||
return Guid.Empty;
|
return Guid.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,7 @@ public partial class Message
|
|||||||
AddChunkWithMessage(
|
AddChunkWithMessage(
|
||||||
text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
|
text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
|
||||||
);
|
);
|
||||||
Plugin.Log.Debug(
|
Plugin.LogProxy.Debug(
|
||||||
$"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
|
$"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ public partial class Message
|
|||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
||||||
Plugin.Log.Debug($"Failed to parse the text param: '{split}'");
|
Plugin.LogProxy.Debug($"Failed to parse the text param: '{split}'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using HellionChat.Util;
|
|||||||
using Lumina.Text.Expressions;
|
using Lumina.Text.Expressions;
|
||||||
using Lumina.Text.Payloads;
|
using Lumina.Text.Payloads;
|
||||||
using Lumina.Text.ReadOnly;
|
using Lumina.Text.ReadOnly;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
internal const int MessageDisplayLimit = 10_000;
|
internal const int MessageDisplayLimit = 10_000;
|
||||||
|
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
|
private readonly ILogger<MessageManager> _logger;
|
||||||
internal MessageStore Store { get; }
|
internal MessageStore Store { get; }
|
||||||
|
|
||||||
private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
|
private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
|
||||||
@@ -48,11 +50,21 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// AutoTellTabsService to spawn or refresh temp tabs without coupling.
|
// AutoTellTabsService to spawn or refresh temp tabs without coupling.
|
||||||
public event Action<Message>? MessageProcessed;
|
public event Action<Message>? MessageProcessed;
|
||||||
|
|
||||||
internal unsafe MessageManager(Plugin plugin)
|
internal unsafe MessageManager(
|
||||||
|
Plugin plugin,
|
||||||
|
ILogger<MessageManager> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil);
|
Store = new MessageStore(
|
||||||
|
DatabasePath(),
|
||||||
|
Plugin.PlatformUtil,
|
||||||
|
loggerFactory.CreateLogger<MessageStore>(),
|
||||||
|
loggerFactory
|
||||||
|
);
|
||||||
|
|
||||||
PendingMessageThread = new Thread(() =>
|
PendingMessageThread = new Thread(() =>
|
||||||
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||||
@@ -91,7 +103,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
|
|
||||||
if (PendingMessageThread.IsAlive)
|
if (PendingMessageThread.IsAlive)
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
"PendingMessageThread did not observe cancellation within 10s. "
|
"PendingMessageThread did not observe cancellation within 10s. "
|
||||||
+ "Worker remains on background thread; next plugin reload releases it."
|
+ "Worker remains on background thread; next plugin reload releases it."
|
||||||
);
|
);
|
||||||
@@ -137,7 +149,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error processing pending message");
|
_logger.LogError(ex, "Error processing pending message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -182,10 +194,12 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
|
|
||||||
// Mark failed messages as deleted to prevent retry attempts
|
// Mark failed messages as deleted to prevent retry attempts
|
||||||
var failedIds = messages.FailedMessageIds();
|
var failedIds = messages.FailedMessageIds();
|
||||||
Plugin.Log.Info($"Marking {failedIds.Count} messages as deleted due to parse failures");
|
_logger.LogInformation(
|
||||||
|
$"Marking {failedIds.Count} messages as deleted due to parse failures"
|
||||||
|
);
|
||||||
foreach (var msgId in messages.FailedMessageIds())
|
foreach (var msgId in messages.FailedMessageIds())
|
||||||
{
|
{
|
||||||
Plugin.Log.Debug($"Marking message '{msgId}' as deleted due to parse failure");
|
_logger.LogDebug($"Marking message '{msgId}' as deleted due to parse failure");
|
||||||
Store.DeleteMessage(msgId);
|
Store.DeleteMessage(msgId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,10 +215,13 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in FilterAllTabs");
|
_logger.LogError(ex, "Error in FilterAllTabs");
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Log.Debug($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this
|
||||||
|
// without a Debug filter. Belt-and-suspenders for future plugin-load
|
||||||
|
// regressions; remains in place after Sub-Task 3.4 Befund.
|
||||||
|
_logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,7 +276,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in ContentIdResolver");
|
_logger.LogError(ex, "Error in ContentIdResolver");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+489
-65
@@ -9,6 +9,7 @@ using MessagePack;
|
|||||||
using MessagePack.Formatters;
|
using MessagePack.Formatters;
|
||||||
using MessagePack.Resolvers;
|
using MessagePack.Resolvers;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Encoding = System.Text.Encoding;
|
using Encoding = System.Text.Encoding;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
@@ -126,7 +127,12 @@ internal class MessageStore : IDisposable
|
|||||||
private const int MessageQueryLimit = 10_000;
|
private const int MessageQueryLimit = 10_000;
|
||||||
|
|
||||||
private string DbPath { get; }
|
private string DbPath { get; }
|
||||||
private SqliteConnection Connection { get; set; }
|
|
||||||
|
// Internal so the Build-Suite tests can verify Migrate4's CREATE VIRTUAL
|
||||||
|
// TABLE result via a one-off PRAGMA without exposing a dedicated helper
|
||||||
|
// for each schema invariant. Setter stays private; the ctor is the only
|
||||||
|
// place that assigns.
|
||||||
|
internal SqliteConnection Connection { get; private set; }
|
||||||
|
|
||||||
internal static readonly MessagePackSerializerOptions MsgPackOptions =
|
internal static readonly MessagePackSerializerOptions MsgPackOptions =
|
||||||
MessagePackSerializerOptions.Standard.WithResolver(
|
MessagePackSerializerOptions.Standard.WithResolver(
|
||||||
@@ -136,14 +142,77 @@ internal class MessageStore : IDisposable
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
private readonly IPlatformUtil _platformUtil;
|
// Pure deserialisation of one messages-row at the reader's current position.
|
||||||
|
// Shared between the MessageEnumerator load path and the upcoming v1.4.8
|
||||||
|
// LoadByGuids FTS-join path so both stay in lockstep when the column layout
|
||||||
|
// moves. Throws on row-level errors; the caller decides whether to skip+log
|
||||||
|
// (enumerator) or fail-fast (bulk lookup).
|
||||||
|
internal static Message ReadMessageRow(DbDataReader reader)
|
||||||
|
{
|
||||||
|
return new Message(
|
||||||
|
reader.GetGuid(0),
|
||||||
|
(ulong)reader.GetInt64(1),
|
||||||
|
(ulong)reader.GetInt64(2),
|
||||||
|
DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64(3)),
|
||||||
|
new ChatCode(
|
||||||
|
(byte)reader.GetInt32(4),
|
||||||
|
(byte)reader.GetInt32(5),
|
||||||
|
(byte)reader.GetInt32(6)
|
||||||
|
),
|
||||||
|
MessagePackSerializer.Deserialize<List<Chunk>>(
|
||||||
|
reader.GetFieldValue<byte[]>(7),
|
||||||
|
MsgPackOptions
|
||||||
|
),
|
||||||
|
MessagePackSerializer.Deserialize<List<Chunk>>(
|
||||||
|
reader.GetFieldValue<byte[]>(8),
|
||||||
|
MsgPackOptions
|
||||||
|
),
|
||||||
|
MessagePackSerializer.Deserialize<SeString>(
|
||||||
|
reader.GetFieldValue<byte[]>(9),
|
||||||
|
MsgPackOptions
|
||||||
|
),
|
||||||
|
MessagePackSerializer.Deserialize<SeString>(
|
||||||
|
reader.GetFieldValue<byte[]>(10),
|
||||||
|
MsgPackOptions
|
||||||
|
),
|
||||||
|
reader.GetGuid(11)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
internal MessageStore(string dbPath, IPlatformUtil platformUtil)
|
private readonly IPlatformUtil _platformUtil;
|
||||||
|
private readonly ILogger<MessageStore> _logger;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
||||||
|
// Readiness gate for the FTS5 full-text index. Volatile so the DbViewer's
|
||||||
|
// per-frame IsFtsIndexBuilt read sees the flip the moment the bulk-insert
|
||||||
|
// worker calls MarkFtsIndexBuilt(). Set in the ctor by InitFtsReadyCache:
|
||||||
|
// true when the index already has rows (no rebuild needed) or when the
|
||||||
|
// messages table itself is empty (nothing to index yet); false otherwise.
|
||||||
|
private volatile bool _ftsReady;
|
||||||
|
public bool IsFtsIndexBuilt => _ftsReady;
|
||||||
|
|
||||||
|
// Serialises read/write access to the primary Connection so the DbViewer
|
||||||
|
// filter-worker (Task.Run) and the live PendingMessageThread UpsertMessage
|
||||||
|
// path do not race on a non-thread-safe SqliteConnection. Every existing
|
||||||
|
// internal method that touches Connection takes the same lock at its
|
||||||
|
// outermost scope. RebuildFtsIndex stays outside the lock -- it owns its
|
||||||
|
// own SqliteConnection via OpenSecondaryConnection.
|
||||||
|
private readonly object _readLock = new();
|
||||||
|
|
||||||
|
internal MessageStore(
|
||||||
|
string dbPath,
|
||||||
|
IPlatformUtil platformUtil,
|
||||||
|
ILogger<MessageStore> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
)
|
||||||
{
|
{
|
||||||
DbPath = dbPath;
|
DbPath = dbPath;
|
||||||
_platformUtil = platformUtil;
|
_platformUtil = platformUtil;
|
||||||
|
_logger = logger;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
Connection = Connect();
|
Connection = Connect();
|
||||||
Migrate();
|
Migrate();
|
||||||
|
InitFtsReadyCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -154,27 +223,48 @@ internal class MessageStore : IDisposable
|
|||||||
Connection.Dispose();
|
Connection.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SqliteConnection Connect()
|
private static string BuildConnectionString(string dbPath)
|
||||||
{
|
{
|
||||||
var uriBuilder = new SqliteConnectionStringBuilder
|
var uriBuilder = new SqliteConnectionStringBuilder
|
||||||
{
|
{
|
||||||
DataSource = DbPath,
|
DataSource = dbPath,
|
||||||
DefaultTimeout = 5,
|
DefaultTimeout = 5,
|
||||||
Pooling = false,
|
Pooling = false,
|
||||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
Mode = SqliteOpenMode.ReadWriteCreate,
|
||||||
};
|
};
|
||||||
|
return uriBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
var conn = new SqliteConnection(uriBuilder.ToString());
|
private void ApplyPragmas(SqliteConnection conn)
|
||||||
conn.Open();
|
{
|
||||||
conn.Execute(@"PRAGMA journal_mode=WAL;");
|
conn.Execute(@"PRAGMA journal_mode=WAL;");
|
||||||
conn.Execute(@"PRAGMA synchronous=NORMAL;");
|
conn.Execute(@"PRAGMA synchronous=NORMAL;");
|
||||||
if (_platformUtil.IsWine)
|
if (_platformUtil.IsWine)
|
||||||
conn.Execute(@"PRAGMA cache_size = 32768;");
|
conn.Execute(@"PRAGMA cache_size = 32768;");
|
||||||
|
}
|
||||||
|
|
||||||
|
private SqliteConnection Connect()
|
||||||
|
{
|
||||||
|
// v1.4.9 R3 profiling: trace cost of SQLite open + pragma-apply. Paired
|
||||||
|
// with the Migrate-Stopwatch below — Connect alone is the cheap half
|
||||||
|
// (Open + a handful of PRAGMAs); the expensive half typically lives in
|
||||||
|
// Migrate, especially on a large DB after a schema bump.
|
||||||
|
var connectSw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
var conn = new SqliteConnection(BuildConnectionString(DbPath));
|
||||||
|
conn.Open();
|
||||||
|
ApplyPragmas(conn);
|
||||||
|
connectSw.Stop();
|
||||||
|
_logger.LogInformation($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms");
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Migrate()
|
private void Migrate()
|
||||||
{
|
{
|
||||||
|
// v1.4.9 R3 profiling: trace cost of the schema-migration chain. On a
|
||||||
|
// large DB after a fresh schema bump this is the dominant SQLite cost
|
||||||
|
// at plugin-load, not Connect.
|
||||||
|
var migrateSw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
cmd.CommandText = "PRAGMA user_version;";
|
cmd.CommandText = "PRAGMA user_version;";
|
||||||
var userVersion = Convert.ToInt32(cmd.ExecuteScalar());
|
var userVersion = Convert.ToInt32(cmd.ExecuteScalar());
|
||||||
@@ -188,23 +278,32 @@ internal class MessageStore : IDisposable
|
|||||||
migrationsToDo.Add(Migrate1);
|
migrationsToDo.Add(Migrate1);
|
||||||
migrationsToDo.Add(Migrate2);
|
migrationsToDo.Add(Migrate2);
|
||||||
migrationsToDo.Add(Migrate3);
|
migrationsToDo.Add(Migrate3);
|
||||||
|
migrationsToDo.Add(Migrate4);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
migrationsToDo.Add(Migrate2);
|
migrationsToDo.Add(Migrate2);
|
||||||
migrationsToDo.Add(Migrate3);
|
migrationsToDo.Add(Migrate3);
|
||||||
|
migrationsToDo.Add(Migrate4);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
migrationsToDo.Add(Migrate3);
|
migrationsToDo.Add(Migrate3);
|
||||||
|
migrationsToDo.Add(Migrate4);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
migrationsToDo.Add(Migrate4);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var migration in migrationsToDo)
|
foreach (var migration in migrationsToDo)
|
||||||
migration();
|
migration();
|
||||||
|
|
||||||
|
migrateSw.Stop();
|
||||||
|
_logger.LogInformation($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Migrate0()
|
private void Migrate0()
|
||||||
{
|
{
|
||||||
Plugin.Log.Information("Running migration 0: Creating tables");
|
_logger.LogInformation("Running migration 0: Creating tables");
|
||||||
Connection.Execute(
|
Connection.Execute(
|
||||||
@"
|
@"
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
@@ -231,7 +330,7 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
private void Migrate1()
|
private void Migrate1()
|
||||||
{
|
{
|
||||||
Plugin.Log.Information("Running migration 1: Adding Deleted column");
|
_logger.LogInformation("Running migration 1: Adding Deleted column");
|
||||||
Connection.Execute(
|
Connection.Execute(
|
||||||
@"
|
@"
|
||||||
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -243,7 +342,7 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
private void Migrate2()
|
private void Migrate2()
|
||||||
{
|
{
|
||||||
Plugin.Log.Information("Running migration 2: Adding Channel generated column");
|
_logger.LogInformation("Running migration 2: Adding Channel generated column");
|
||||||
Connection.Execute(
|
Connection.Execute(
|
||||||
@"
|
@"
|
||||||
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
|
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
|
||||||
@@ -271,13 +370,13 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
private void Migrate3()
|
private void Migrate3()
|
||||||
{
|
{
|
||||||
Plugin.Log.Information("Running migration 3: Fix log kinds to fit the new format");
|
_logger.LogInformation("Running migration 3: Fix log kinds to fit the new format");
|
||||||
|
|
||||||
// Recovery for partially-applied Migrate3: schema already in target
|
// Recovery for partially-applied Migrate3: schema already in target
|
||||||
// shape but user_version was never bumped -- just record and exit.
|
// shape but user_version was never bumped -- just record and exit.
|
||||||
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
|
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
|
||||||
{
|
{
|
||||||
Plugin.Log.Information(
|
_logger.LogInformation(
|
||||||
"Migration 3: schema already migrated, only bumping user_version"
|
"Migration 3: schema already migrated, only bumping user_version"
|
||||||
);
|
);
|
||||||
SetMigrationVersion(3);
|
SetMigrationVersion(3);
|
||||||
@@ -307,9 +406,33 @@ internal class MessageStore : IDisposable
|
|||||||
SetMigrationVersion(3);
|
SetMigrationVersion(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Migrate4()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Running migration 4: Add FTS5 virtual table for full-text search");
|
||||||
|
|
||||||
|
// Standalone FTS5 table (no content='messages' linking, no content_rowid).
|
||||||
|
// messages.Id is BLOB-PK (Guid), which is incompatible with FTS5's
|
||||||
|
// content_rowid requirement of an INTEGER rowid alias. We store the
|
||||||
|
// GUID as a hex TEXT column (UNINDEXED so the tokenizer skips it) and
|
||||||
|
// FTS5 manages its own internal INTEGER rowid. LoadByGuids joins back
|
||||||
|
// via WHERE Id IN (... unhex(message_guid)) when the search returns.
|
||||||
|
using var cmd = Connection.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||||
|
message_guid UNINDEXED,
|
||||||
|
sender_text,
|
||||||
|
content_text,
|
||||||
|
tokenize='unicode61 remove_diacritics 2'
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
SetMigrationVersion(4);
|
||||||
|
}
|
||||||
|
|
||||||
private void SetMigrationVersion(int version)
|
private void SetMigrationVersion(int version)
|
||||||
{
|
{
|
||||||
Plugin.Log.Information($"Setting version {version}");
|
_logger.LogInformation($"Setting version {version}");
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
// PRAGMA does not accept SQLite parameter bindings; version is a
|
// PRAGMA does not accept SQLite parameter bindings; version is a
|
||||||
// compile-time int from the migration sequence, never user input.
|
// compile-time int from the migration sequence, never user input.
|
||||||
@@ -318,14 +441,19 @@ internal class MessageStore : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal void ClearMessages()
|
internal void ClearMessages()
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
Connection.Execute("DELETE FROM messages;");
|
Connection.Execute("DELETE FROM messages;");
|
||||||
PerformMaintenance();
|
PerformMaintenance();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Returns a (ChatType, count) snapshot over non-deleted messages.
|
// Returns a (ChatType, count) snapshot over non-deleted messages.
|
||||||
// Used by the Privacy tab to preview retroactive cleanup impact.
|
// Used by the Privacy tab to preview retroactive cleanup impact.
|
||||||
internal Dictionary<int, long> GetMessageCountsByChatType()
|
internal Dictionary<int, long> GetMessageCountsByChatType()
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
var result = new Dictionary<int, long>();
|
var result = new Dictionary<int, long>();
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
@@ -341,6 +469,7 @@ internal class MessageStore : IDisposable
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Deletes messages older than the per-channel retention window, with a global
|
// Deletes messages older than the per-channel retention window, with a global
|
||||||
// default for unmapped channels. Runs VACUUM only if rows were removed.
|
// default for unmapped channels. Runs VACUUM only if rows were removed.
|
||||||
@@ -367,6 +496,8 @@ internal class MessageStore : IDisposable
|
|||||||
if (chatTypeDaysMap.Count == 0 && defaultDays <= 0)
|
if (chatTypeDaysMap.Count == 0 && defaultDays <= 0)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
lock (_readLock)
|
||||||
|
{
|
||||||
long deleted;
|
long deleted;
|
||||||
using (var cmd = Connection.CreateCommand())
|
using (var cmd = Connection.CreateCommand())
|
||||||
{
|
{
|
||||||
@@ -410,6 +541,7 @@ internal class MessageStore : IDisposable
|
|||||||
PerformMaintenance();
|
PerformMaintenance();
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Hard-deletes every message whose ChatType is not in the allowlist,
|
// Hard-deletes every message whose ChatType is not in the allowlist,
|
||||||
// then VACUUMs. Returns the number of rows deleted.
|
// then VACUUMs. Returns the number of rows deleted.
|
||||||
@@ -420,6 +552,8 @@ internal class MessageStore : IDisposable
|
|||||||
"CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe."
|
"CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
lock (_readLock)
|
||||||
|
{
|
||||||
long deleted;
|
long deleted;
|
||||||
using (var cmd = Connection.CreateCommand())
|
using (var cmd = Connection.CreateCommand())
|
||||||
{
|
{
|
||||||
@@ -431,8 +565,11 @@ internal class MessageStore : IDisposable
|
|||||||
PerformMaintenance();
|
PerformMaintenance();
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal void PerformMaintenance()
|
internal void PerformMaintenance()
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
Connection.Execute(
|
Connection.Execute(
|
||||||
@"
|
@"
|
||||||
@@ -442,6 +579,7 @@ internal class MessageStore : IDisposable
|
|||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string LogPath => DbPath + "-wal";
|
private string LogPath => DbPath + "-wal";
|
||||||
|
|
||||||
@@ -450,21 +588,271 @@ internal class MessageStore : IDisposable
|
|||||||
internal long DatabaseLogSize() => !File.Exists(LogPath) ? 0 : new FileInfo(LogPath).Length;
|
internal long DatabaseLogSize() => !File.Exists(LogPath) ? 0 : new FileInfo(LogPath).Length;
|
||||||
|
|
||||||
internal int MessageCount()
|
internal int MessageCount()
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
cmd.CommandText = "SELECT COUNT(*) FROM messages;";
|
cmd.CommandText = "SELECT COUNT(*) FROM messages;";
|
||||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
return Convert.ToInt32(cmd.ExecuteScalar());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema probe for the v1.4.8 FTS5 virtual table. Used by the Build-Suite
|
||||||
|
// tests to verify Migrate4's CREATE VIRTUAL TABLE actually landed without
|
||||||
|
// duplicating PRAGMA glue in each test body.
|
||||||
|
internal bool HasMessagesFtsTable()
|
||||||
|
{
|
||||||
|
using var cmd = Connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT count(*) FROM sqlite_master WHERE name='messages_fts';";
|
||||||
|
return (long)(cmd.ExecuteScalar() ?? 0L) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decides whether the FTS index already covers the messages table. Called
|
||||||
|
// once after Migrate -- empty messages-table is "ready" because there is
|
||||||
|
// nothing to index yet; a populated fts-table is "ready" because some
|
||||||
|
// previous run filled it. A populated messages-table with an empty
|
||||||
|
// fts-table is the "needs rebuild" case the worker (Plugin.cs LoadAsync)
|
||||||
|
// picks up.
|
||||||
|
internal void InitFtsReadyCache()
|
||||||
|
{
|
||||||
|
using (var cmd = Connection.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.CommandText = "SELECT count(*) FROM messages_fts;";
|
||||||
|
var ftsRows = (long)(cmd.ExecuteScalar() ?? 0L);
|
||||||
|
if (ftsRows > 0)
|
||||||
|
{
|
||||||
|
_ftsReady = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var cmd2 = Connection.CreateCommand();
|
||||||
|
cmd2.CommandText = "SELECT count(*) FROM messages;";
|
||||||
|
var messageRows = (long)(cmd2.ExecuteScalar() ?? 0L);
|
||||||
|
_ftsReady = messageRows == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens a worker-owned SqliteConnection on the same db path. Used by the
|
||||||
|
// FTS rebuild worker so the bulk-insert writer stream does not contend
|
||||||
|
// with the live UpsertMessage path on the primary Connection (WAL allows
|
||||||
|
// N readers + 1 writer; two writer sessions on the same connection are
|
||||||
|
// not safe per Microsoft.Data.Sqlite). Caller closes+disposes the
|
||||||
|
// returned connection and only then calls MarkFtsIndexBuilt() -- the
|
||||||
|
// DbViewer never sees IsFtsIndexBuilt=true while the worker connection
|
||||||
|
// is still alive.
|
||||||
|
internal SqliteConnection OpenSecondaryConnection()
|
||||||
|
{
|
||||||
|
var conn = new SqliteConnection(BuildConnectionString(DbPath));
|
||||||
|
conn.Open();
|
||||||
|
ApplyPragmas(conn);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker-only mutator. The bulk-insert worker is the single legitimate
|
||||||
|
// caller; the flag flips after the worker has closed its own connection.
|
||||||
|
internal void MarkFtsIndexBuilt() => _ftsReady = true;
|
||||||
|
|
||||||
|
// Builds the FTS5 index from scratch on a worker-owned SqliteConnection.
|
||||||
|
// Chunked-commit (every 500 rows + 5ms sleep) releases the WAL writer
|
||||||
|
// lock between transactions so the live PendingMessageThread UpsertMessage
|
||||||
|
// path on the primary Connection does not hit "database is locked" after
|
||||||
|
// DefaultTimeout=5s. The Thread.Sleep is intentional: it gives the live
|
||||||
|
// writer a deterministic window to acquire the lock before we re-take
|
||||||
|
// it for the next chunk.
|
||||||
|
//
|
||||||
|
// Cancellation: checked at the top of each row and again after each
|
||||||
|
// chunk commit, so a Dispose-during-rebuild collapses on the next row
|
||||||
|
// without trashing the half-built index (DELETE FROM messages_fts at
|
||||||
|
// the start makes the next run idempotent).
|
||||||
|
public long RebuildFtsIndex(
|
||||||
|
SqliteConnection conn,
|
||||||
|
IProgress<long> progress,
|
||||||
|
CancellationToken ct
|
||||||
|
)
|
||||||
|
{
|
||||||
|
const int ChunkSize = 500;
|
||||||
|
|
||||||
|
using (var clear = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
clear.CommandText = "DELETE FROM messages_fts;";
|
||||||
|
clear.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
long total;
|
||||||
|
using (var totalCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
totalCmd.CommandText = "SELECT count(*) FROM messages;";
|
||||||
|
total = (long)(totalCmd.ExecuteScalar() ?? 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
long done = 0;
|
||||||
|
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT Id, Sender, Content FROM messages ORDER BY Id;";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
|
||||||
|
using var insert = conn.CreateCommand();
|
||||||
|
insert.CommandText =
|
||||||
|
"INSERT INTO messages_fts(message_guid, sender_text, content_text) VALUES ($g, $s, $c);";
|
||||||
|
var pG = insert.CreateParameter();
|
||||||
|
pG.ParameterName = "$g";
|
||||||
|
insert.Parameters.Add(pG);
|
||||||
|
var pS = insert.CreateParameter();
|
||||||
|
pS.ParameterName = "$s";
|
||||||
|
insert.Parameters.Add(pS);
|
||||||
|
var pC = insert.CreateParameter();
|
||||||
|
pC.ParameterName = "$c";
|
||||||
|
insert.Parameters.Add(pC);
|
||||||
|
|
||||||
|
// Nullable so the finally can dispose exactly once whether the loop
|
||||||
|
// ends normally, via cancellation between Dispose and BeginTransaction,
|
||||||
|
// or via an exception in the body.
|
||||||
|
SqliteTransaction? transaction = conn.BeginTransaction();
|
||||||
|
insert.Transaction = transaction;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// messages.Id is BLOB-typed in the schema but stored as TEXT
|
||||||
|
// because Microsoft.Data.Sqlite binds Guid parameters as UUID
|
||||||
|
// strings by default (UpsertMessage uses AddWithValue("$Id",
|
||||||
|
// message.Id)). reader.GetValue(0) therefore returns string,
|
||||||
|
// not byte[]; GetGuid parses the TEXT form regardless.
|
||||||
|
var idGuid = reader.GetGuid(0);
|
||||||
|
var senderChunks = MessagePackSerializer.Deserialize<List<Chunk>>(
|
||||||
|
reader.GetFieldValue<byte[]>(1),
|
||||||
|
MsgPackOptions
|
||||||
|
);
|
||||||
|
var contentChunks = MessagePackSerializer.Deserialize<List<Chunk>>(
|
||||||
|
reader.GetFieldValue<byte[]>(2),
|
||||||
|
MsgPackOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
pG.Value = idGuid.ToString();
|
||||||
|
pS.Value = ChunkUtil.ToRawString(senderChunks);
|
||||||
|
pC.Value = ChunkUtil.ToRawString(contentChunks);
|
||||||
|
insert.ExecuteNonQuery();
|
||||||
|
done++;
|
||||||
|
|
||||||
|
if (done % ChunkSize == 0)
|
||||||
|
{
|
||||||
|
transaction.Commit();
|
||||||
|
transaction.Dispose();
|
||||||
|
transaction = null;
|
||||||
|
progress.Report(done);
|
||||||
|
|
||||||
|
Thread.Sleep(5);
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
transaction = conn.BeginTransaction();
|
||||||
|
insert.Transaction = transaction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transaction?.Commit();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
transaction?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(done);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FTS5 full-text search across the entire messages_fts index. Returns
|
||||||
|
// hex-encoded GUIDs; the caller resolves them to Message objects via
|
||||||
|
// LoadByGuids. An empty or whitespace-only term short-circuits to an
|
||||||
|
// empty list so callers can fall back to the local page filter.
|
||||||
|
public IReadOnlyList<string> FullTextSearch(string term, int limit = 1000)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(term))
|
||||||
|
return Array.Empty<string>();
|
||||||
|
|
||||||
|
lock (_readLock)
|
||||||
|
{
|
||||||
|
var hexIds = new List<string>(capacity: 256);
|
||||||
|
using var cmd = Connection.CreateCommand();
|
||||||
|
cmd.CommandText = """
|
||||||
|
SELECT message_guid FROM messages_fts
|
||||||
|
WHERE messages_fts MATCH $term
|
||||||
|
ORDER BY rank
|
||||||
|
LIMIT $limit;
|
||||||
|
""";
|
||||||
|
cmd.Parameters.AddWithValue("$term", EscapeFtsTerm(term));
|
||||||
|
cmd.Parameters.AddWithValue("$limit", limit);
|
||||||
|
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
hexIds.Add(reader.GetString(0));
|
||||||
|
return hexIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Joins UUID strings from FullTextSearch back to Message rows. messages.Id
|
||||||
|
// is BLOB-declared in the schema but actually stored as TEXT (UUID form)
|
||||||
|
// because Microsoft.Data.Sqlite serialises Guid parameters as strings by
|
||||||
|
// default. Binding the lookup parameters as Guid keeps the same TEXT
|
||||||
|
// storage form on both sides so the IN(...) compare matches. SQLite has a
|
||||||
|
// hard parameter limit of 999 in default builds, so we chunk the input --
|
||||||
|
// a 1000-hit FTS query never explodes the SELECT. Result ordering is not
|
||||||
|
// guaranteed; callers re-sort (e.g. DbViewer sorts by Date descending in
|
||||||
|
// Sub-Task 4.4).
|
||||||
|
public IReadOnlyList<Message> LoadByGuids(IReadOnlyList<string> guidStrings)
|
||||||
|
{
|
||||||
|
if (guidStrings.Count == 0)
|
||||||
|
return Array.Empty<Message>();
|
||||||
|
|
||||||
|
lock (_readLock)
|
||||||
|
{
|
||||||
|
var result = new List<Message>(guidStrings.Count);
|
||||||
|
const int chunkSize = 500;
|
||||||
|
for (var offset = 0; offset < guidStrings.Count; offset += chunkSize)
|
||||||
|
{
|
||||||
|
var batch = guidStrings.Skip(offset).Take(chunkSize).ToList();
|
||||||
|
using var cmd = Connection.CreateCommand();
|
||||||
|
var placeholders = string.Join(",", batch.Select((_, i) => $"$id{i}"));
|
||||||
|
cmd.CommandText = $"""
|
||||||
|
SELECT Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||||
|
Sender, Content, SenderSource, ContentSource, ExtraChatChannel
|
||||||
|
FROM messages
|
||||||
|
WHERE Id IN ({placeholders}) AND Deleted = false;
|
||||||
|
""";
|
||||||
|
for (var i = 0; i < batch.Count; i++)
|
||||||
|
cmd.Parameters.AddWithValue($"$id{i}", Guid.Parse(batch[i]));
|
||||||
|
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
result.Add(ReadMessageRow(reader));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FTS5's MATCH operator interprets ", ~, ^, - as syntax. Wrap user terms
|
||||||
|
// in double quotes so the search is "what you see is what you get" -- a
|
||||||
|
// multi-word query matches as a phrase, not as per-word AND. Power users
|
||||||
|
// can opt into raw MATCH syntax by wrapping their own quotes; we detect
|
||||||
|
// that and pass the term through unchanged.
|
||||||
|
internal static string EscapeFtsTerm(string term)
|
||||||
|
{
|
||||||
|
if (term.Contains('"'))
|
||||||
|
return term;
|
||||||
|
return $"\"{term.Replace("\"", "\"\"")}\"";
|
||||||
|
}
|
||||||
|
|
||||||
internal void UpsertMessage(Message message)
|
internal void UpsertMessage(Message message)
|
||||||
{
|
{
|
||||||
// Privacy filter -- drop disallowed ChatTypes before they reach storage.
|
// Privacy filter -- drop disallowed ChatTypes before they reach storage.
|
||||||
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
||||||
{
|
{
|
||||||
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
_logger.LogTrace($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (_readLock)
|
||||||
|
{
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
cmd.CommandText =
|
cmd.CommandText =
|
||||||
@"
|
@"
|
||||||
@@ -517,15 +905,24 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Streams messages for export, sorted ascending by Date, excluding soft-deleted rows.
|
// Streams messages for export, sorted ascending by Date, excluding soft-deleted rows.
|
||||||
// Optional filters: chatTypes, from/to inclusive date range.
|
// Optional filters: chatTypes, from/to inclusive date range.
|
||||||
// Caller is responsible for disposing the enumerator.
|
// Caller is responsible for disposing the enumerator.
|
||||||
|
// Lock caveat: lock guards command setup and ExecuteReader; the returned
|
||||||
|
// MessageEnumerator is iterated lazily by the caller outside the lock.
|
||||||
|
// Acceptable for v1.4.8 -- DbViewer iterates on its filter-worker Task and
|
||||||
|
// any clash with UpsertMessage on the primary Connection is rare and
|
||||||
|
// serialised by SQLite's own connection-level lock. v1.5.x DI cycle should
|
||||||
|
// address this with a snapshot-to-list or connection pool.
|
||||||
internal MessageEnumerator StreamForExport(
|
internal MessageEnumerator StreamForExport(
|
||||||
IReadOnlyCollection<int>? chatTypes,
|
IReadOnlyCollection<int>? chatTypes,
|
||||||
DateTimeOffset? from,
|
DateTimeOffset? from,
|
||||||
DateTimeOffset? to
|
DateTimeOffset? to
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
var cmd = Connection.CreateCommand();
|
var cmd = Connection.CreateCommand();
|
||||||
|
|
||||||
@@ -554,13 +951,18 @@ internal class MessageStore : IDisposable
|
|||||||
if (to is not null)
|
if (to is not null)
|
||||||
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
return new MessageEnumerator(
|
||||||
|
cmd.ExecuteReader(),
|
||||||
|
_loggerFactory.CreateLogger<MessageEnumerator>()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the most recent messages, oldest-first.
|
// Returns the most recent messages, oldest-first.
|
||||||
// receiver: filter by receiver ContentId (null = no filter)
|
// receiver: filter by receiver ContentId (null = no filter)
|
||||||
// since: only include messages after this date (null = no filter)
|
// since: only include messages after this date (null = no filter)
|
||||||
// count: max rows to return, defaults to 10,000
|
// count: max rows to return, defaults to 10,000
|
||||||
|
// Lock caveat: same lazy-enumerator note as StreamForExport.
|
||||||
internal MessageEnumerator GetMostRecentMessages(
|
internal MessageEnumerator GetMostRecentMessages(
|
||||||
ulong? receiver = null,
|
ulong? receiver = null,
|
||||||
DateTimeOffset? since = null,
|
DateTimeOffset? since = null,
|
||||||
@@ -575,6 +977,8 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
||||||
|
|
||||||
|
lock (_readLock)
|
||||||
|
{
|
||||||
var cmd = Connection.CreateCommand();
|
var cmd = Connection.CreateCommand();
|
||||||
// Select last N by date DESC, then reverse to ascending order.
|
// Select last N by date DESC, then reverse to ascending order.
|
||||||
cmd.CommandText =
|
cmd.CommandText =
|
||||||
@@ -602,24 +1006,31 @@ internal class MessageStore : IDisposable
|
|||||||
|
|
||||||
cmd.Parameters.AddWithValue("$Count", count);
|
cmd.Parameters.AddWithValue("$Count", count);
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
return new MessageEnumerator(
|
||||||
|
cmd.ExecuteReader(),
|
||||||
|
_loggerFactory.CreateLogger<MessageEnumerator>()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns up to limit tells exchanged with the named player, oldest-first.
|
// Returns up to `limit` tells exchanged with the named player, oldest-first.
|
||||||
// SQL narrows by Receiver + ChatType (indexed); client does the final
|
// SQL narrows by Receiver + ChatType via the (Receiver, Date) index, then
|
||||||
// PlayerPayload comparison. sqlScanLimit caps the scan to stay within
|
// the client-side loop runs PlayerPayload comparison and breaks once
|
||||||
// the message-processing worker thread budget.
|
// `limit` partner matches accumulate. Earlier versions had a hardcoded
|
||||||
|
// 500-row scan cap that cut less-frequent pinned partners off the back of
|
||||||
|
// the window in chatty sessions; removed in v1.4.10.
|
||||||
internal IReadOnlyList<Message> GetTellHistoryWithSender(
|
internal IReadOnlyList<Message> GetTellHistoryWithSender(
|
||||||
ulong receiver,
|
ulong receiver,
|
||||||
string senderName,
|
string senderName,
|
||||||
uint senderWorld,
|
uint senderWorld,
|
||||||
int limit,
|
int limit
|
||||||
int sqlScanLimit = 500
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (limit <= 0)
|
if (limit <= 0)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
lock (_readLock)
|
||||||
|
{
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
cmd.CommandText =
|
cmd.CommandText =
|
||||||
@"
|
@"
|
||||||
@@ -630,17 +1041,18 @@ internal class MessageStore : IDisposable
|
|||||||
WHERE deleted = false
|
WHERE deleted = false
|
||||||
AND Receiver = $Receiver
|
AND Receiver = $Receiver
|
||||||
AND ChatType IN ($TellIncoming, $TellOutgoing)
|
AND ChatType IN ($TellIncoming, $TellOutgoing)
|
||||||
ORDER BY Date DESC
|
ORDER BY Date DESC;
|
||||||
LIMIT $ScanLimit;
|
|
||||||
";
|
";
|
||||||
cmd.CommandTimeout = 60;
|
cmd.CommandTimeout = 60;
|
||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||||
cmd.Parameters.AddWithValue("$TellIncoming", (int)ChatType.TellIncoming);
|
cmd.Parameters.AddWithValue("$TellIncoming", (int)ChatType.TellIncoming);
|
||||||
cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing);
|
cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing);
|
||||||
cmd.Parameters.AddWithValue("$ScanLimit", sqlScanLimit);
|
|
||||||
|
|
||||||
var collected = new List<Message>();
|
var collected = new List<Message>();
|
||||||
using var enumerator = new MessageEnumerator(cmd.ExecuteReader());
|
using var enumerator = new MessageEnumerator(
|
||||||
|
cmd.ExecuteReader(),
|
||||||
|
_loggerFactory.CreateLogger<MessageEnumerator>()
|
||||||
|
);
|
||||||
foreach (var message in enumerator)
|
foreach (var message in enumerator)
|
||||||
{
|
{
|
||||||
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
|
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
|
||||||
@@ -655,15 +1067,19 @@ internal class MessageStore : IDisposable
|
|||||||
collected.Reverse();
|
collected.Reverse();
|
||||||
return collected;
|
return collected;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Soft-deletes a message so it won't appear in queries.
|
// Soft-deletes a message so it won't appear in queries.
|
||||||
internal void DeleteMessage(Guid id)
|
internal void DeleteMessage(Guid id)
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
cmd.CommandText = "UPDATE messages SET Deleted = true WHERE Id = $Id;";
|
cmd.CommandText = "UPDATE messages SET Deleted = true WHERE Id = $Id;";
|
||||||
cmd.Parameters.AddWithValue("$Id", id);
|
cmd.Parameters.AddWithValue("$Id", id);
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal long CountDateRange(
|
internal long CountDateRange(
|
||||||
DateTime after,
|
DateTime after,
|
||||||
@@ -671,6 +1087,8 @@ internal class MessageStore : IDisposable
|
|||||||
IEnumerable<byte> channels,
|
IEnumerable<byte> channels,
|
||||||
ulong? receiver = null
|
ulong? receiver = null
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
using var cmd = Connection.CreateCommand();
|
using var cmd = Connection.CreateCommand();
|
||||||
|
|
||||||
@@ -679,7 +1097,9 @@ internal class MessageStore : IDisposable
|
|||||||
whereClauses.Add("Receiver = $Receiver");
|
whereClauses.Add("Receiver = $Receiver");
|
||||||
|
|
||||||
whereClauses.Add("Date BETWEEN $After AND $Before");
|
whereClauses.Add("Date BETWEEN $After AND $Before");
|
||||||
whereClauses.Add($"ChatType IN ({BindIntList(cmd, "cdr", channels.Select(c => (int)c))})");
|
whereClauses.Add(
|
||||||
|
$"ChatType IN ({BindIntList(cmd, "cdr", channels.Select(c => (int)c))})"
|
||||||
|
);
|
||||||
|
|
||||||
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
||||||
|
|
||||||
@@ -693,18 +1113,25 @@ internal class MessageStore : IDisposable
|
|||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
||||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue(
|
||||||
|
"$Before",
|
||||||
|
((DateTimeOffset)before).ToUnixTimeMilliseconds()
|
||||||
|
);
|
||||||
cmd.CommandTimeout = 120;
|
cmd.CommandTimeout = 120;
|
||||||
|
|
||||||
return (long)cmd.ExecuteScalar()!;
|
return (long)cmd.ExecuteScalar()!;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock caveat: same lazy-enumerator note as StreamForExport.
|
||||||
internal MessageEnumerator GetDateRange(
|
internal MessageEnumerator GetDateRange(
|
||||||
DateTime after,
|
DateTime after,
|
||||||
DateTime before,
|
DateTime before,
|
||||||
IEnumerable<byte> channels,
|
IEnumerable<byte> channels,
|
||||||
ulong? receiver = null
|
ulong? receiver = null
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
var cmd = Connection.CreateCommand();
|
var cmd = Connection.CreateCommand();
|
||||||
|
|
||||||
@@ -713,7 +1140,9 @@ internal class MessageStore : IDisposable
|
|||||||
whereClauses.Add("Receiver = $Receiver");
|
whereClauses.Add("Receiver = $Receiver");
|
||||||
|
|
||||||
whereClauses.Add("Date BETWEEN $After AND $Before");
|
whereClauses.Add("Date BETWEEN $After AND $Before");
|
||||||
whereClauses.Add($"ChatType IN ({BindIntList(cmd, "gdr", channels.Select(c => (int)c))})");
|
whereClauses.Add(
|
||||||
|
$"ChatType IN ({BindIntList(cmd, "gdr", channels.Select(c => (int)c))})"
|
||||||
|
);
|
||||||
|
|
||||||
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
||||||
|
|
||||||
@@ -730,11 +1159,19 @@ internal class MessageStore : IDisposable
|
|||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
||||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue(
|
||||||
|
"$Before",
|
||||||
|
((DateTimeOffset)before).ToUnixTimeMilliseconds()
|
||||||
|
);
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
return new MessageEnumerator(
|
||||||
|
cmd.ExecuteReader(),
|
||||||
|
_loggerFactory.CreateLogger<MessageEnumerator>()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lock caveat: same lazy-enumerator note as StreamForExport.
|
||||||
internal MessageEnumerator GetPagedDateRange(
|
internal MessageEnumerator GetPagedDateRange(
|
||||||
DateTime after,
|
DateTime after,
|
||||||
DateTime before,
|
DateTime before,
|
||||||
@@ -742,6 +1179,8 @@ internal class MessageStore : IDisposable
|
|||||||
ulong? receiver = null,
|
ulong? receiver = null,
|
||||||
int page = 0
|
int page = 0
|
||||||
)
|
)
|
||||||
|
{
|
||||||
|
lock (_readLock)
|
||||||
{
|
{
|
||||||
var cmd = Connection.CreateCommand();
|
var cmd = Connection.CreateCommand();
|
||||||
|
|
||||||
@@ -750,7 +1189,9 @@ internal class MessageStore : IDisposable
|
|||||||
whereClauses.Add("Receiver = $Receiver");
|
whereClauses.Add("Receiver = $Receiver");
|
||||||
|
|
||||||
whereClauses.Add("Date BETWEEN $After AND $Before");
|
whereClauses.Add("Date BETWEEN $After AND $Before");
|
||||||
whereClauses.Add($"ChatType IN ({BindIntList(cmd, "pdr", channels.Select(c => (int)c))})");
|
whereClauses.Add(
|
||||||
|
$"ChatType IN ({BindIntList(cmd, "pdr", channels.Select(c => (int)c))})"
|
||||||
|
);
|
||||||
|
|
||||||
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
||||||
|
|
||||||
@@ -772,11 +1213,18 @@ internal class MessageStore : IDisposable
|
|||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
||||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
|
cmd.Parameters.AddWithValue(
|
||||||
|
"$Before",
|
||||||
|
((DateTimeOffset)before).ToUnixTimeMilliseconds()
|
||||||
|
);
|
||||||
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
||||||
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
|
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
return new MessageEnumerator(
|
||||||
|
cmd.ExecuteReader(),
|
||||||
|
_loggerFactory.CreateLogger<MessageEnumerator>()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Builds a "$prefix0,$prefix1,..." placeholder list and binds values to the command.
|
// Builds a "$prefix0,$prefix1,..." placeholder list and binds values to the command.
|
||||||
@@ -796,13 +1244,14 @@ internal class MessageStore : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MessageEnumerator(DbDataReader reader)
|
internal class MessageEnumerator(DbDataReader reader, ILogger<MessageEnumerator> logger)
|
||||||
: IEnumerable<Message>,
|
: IEnumerable<Message>,
|
||||||
IDisposable,
|
IDisposable,
|
||||||
IAsyncDisposable
|
IAsyncDisposable
|
||||||
{
|
{
|
||||||
private const int MaxErrorLogs = 10;
|
private const int MaxErrorLogs = 10;
|
||||||
|
|
||||||
|
private readonly ILogger<MessageEnumerator> _logger = logger;
|
||||||
private readonly List<Guid> FailedIds = [];
|
private readonly List<Guid> FailedIds = [];
|
||||||
private int FailedCount;
|
private int FailedCount;
|
||||||
public bool DidError => FailedCount > 0;
|
public bool DidError => FailedCount > 0;
|
||||||
@@ -815,43 +1264,18 @@ internal class MessageEnumerator(DbDataReader reader)
|
|||||||
Message msg;
|
Message msg;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// GetGuid up-front so we have an id for the failure log even
|
||||||
|
// when the rest of the deserialisation throws downstream.
|
||||||
id = reader.GetGuid(0);
|
id = reader.GetGuid(0);
|
||||||
msg = new Message(
|
msg = MessageStore.ReadMessageRow(reader);
|
||||||
id,
|
|
||||||
(ulong)reader.GetInt64(1),
|
|
||||||
(ulong)reader.GetInt64(2),
|
|
||||||
DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64(3)),
|
|
||||||
new ChatCode(
|
|
||||||
(byte)reader.GetInt32(4),
|
|
||||||
(byte)reader.GetInt32(5),
|
|
||||||
(byte)reader.GetInt32(6)
|
|
||||||
),
|
|
||||||
MessagePackSerializer.Deserialize<List<Chunk>>(
|
|
||||||
reader.GetFieldValue<byte[]>(7),
|
|
||||||
MessageStore.MsgPackOptions
|
|
||||||
),
|
|
||||||
MessagePackSerializer.Deserialize<List<Chunk>>(
|
|
||||||
reader.GetFieldValue<byte[]>(8),
|
|
||||||
MessageStore.MsgPackOptions
|
|
||||||
),
|
|
||||||
MessagePackSerializer.Deserialize<SeString>(
|
|
||||||
reader.GetFieldValue<byte[]>(9),
|
|
||||||
MessageStore.MsgPackOptions
|
|
||||||
),
|
|
||||||
MessagePackSerializer.Deserialize<SeString>(
|
|
||||||
reader.GetFieldValue<byte[]>(10),
|
|
||||||
MessageStore.MsgPackOptions
|
|
||||||
),
|
|
||||||
reader.GetGuid(11)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
if (FailedCount < MaxErrorLogs)
|
if (FailedCount < MaxErrorLogs)
|
||||||
Plugin.Log.Error($"Exception while reading message '{id}' from database: {e}");
|
_logger.LogError($"Exception while reading message '{id}' from database: {e}");
|
||||||
FailedCount++;
|
FailedCount++;
|
||||||
if (FailedCount == MaxErrorLogs)
|
if (FailedCount == MaxErrorLogs)
|
||||||
Plugin.Log.Error("Further parsing errors will not be logged");
|
_logger.LogError("Further parsing errors will not be logged");
|
||||||
if (id != Guid.Empty)
|
if (id != Guid.Empty)
|
||||||
FailedIds.Add(id);
|
FailedIds.Add(id);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Ui;
|
using HellionChat.Ui;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Action = System.Action;
|
using Action = System.Action;
|
||||||
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
||||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||||
@@ -40,9 +41,12 @@ public sealed class PayloadHandler
|
|||||||
|
|
||||||
private const uint PopupSfx = 1;
|
private const uint PopupSfx = 1;
|
||||||
|
|
||||||
internal PayloadHandler(ChatLogWindow logWindow)
|
private readonly ILogger<PayloadHandler> _logger;
|
||||||
|
|
||||||
|
internal PayloadHandler(ChatLogWindow logWindow, ILogger<PayloadHandler> logger)
|
||||||
{
|
{
|
||||||
LogWindow = logWindow;
|
LogWindow = logWindow;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Draw()
|
internal void Draw()
|
||||||
@@ -131,7 +135,7 @@ public sealed class PayloadHandler
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error executing integration");
|
_logger.LogError(ex, "Error executing integration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,7 +539,7 @@ public sealed class PayloadHandler
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Could not find DalamudLinkHandlers");
|
_logger.LogWarning("Could not find DalamudLinkHandlers");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +550,7 @@ public sealed class PayloadHandler
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error executing DalamudLinkPayload handler");
|
_logger.LogError(ex, "Error executing DalamudLinkPayload handler");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+415
-120
@@ -14,6 +14,9 @@ using HellionChat.Ipc;
|
|||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Ui;
|
using HellionChat.Ui;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -117,11 +120,42 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
||||||
internal static IPlatformUtil PlatformUtil { get; private set; } = null!;
|
internal static IPlatformUtil PlatformUtil { get; private set; } = null!;
|
||||||
|
|
||||||
|
// Log indirection over Dalamud's IPluginLog. Same rationale as PlatformUtil:
|
||||||
|
// call-sites read through LogProxy so MessageStore can be tested in
|
||||||
|
// isolation. Wired immediately after Dalamud injects Log.
|
||||||
|
internal static IPluginLogProxy LogProxy { get; private set; } = null!;
|
||||||
|
|
||||||
|
// Nullable so DisposeAsync stays safe if Host-build throws before the
|
||||||
|
// fields get assigned — Dalamud fires DisposeAsync regardless.
|
||||||
|
private readonly IHost? _host;
|
||||||
|
private readonly PluginLifecycle? _lifecycle;
|
||||||
|
|
||||||
|
// Wrapper cached so TearDown can detach the live instance instead of
|
||||||
|
// re-registering with identical args (v1.4.9 ISSUE-1 cleanup).
|
||||||
|
private CommandWrapper? _hellionSettingsCmd;
|
||||||
|
private CommandWrapper? _hellionViewCmd;
|
||||||
|
private CommandWrapper? _hellionDebuggerCmd;
|
||||||
|
#if DEBUG
|
||||||
|
private CommandWrapper? _hellionSeStringCmd;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||||
private int _disposeStarted;
|
private int _disposeStarted;
|
||||||
|
|
||||||
|
// Set in the first DisposeAsync statement so async callbacks scheduled
|
||||||
|
// via Framework.RunOnTick (v1.4.8 B3 retention sweep) can early-bail
|
||||||
|
// before they touch state that has already been torn down. Volatile
|
||||||
|
// because the tick reads it from a different thread than the writer.
|
||||||
|
private volatile bool _isDisposing;
|
||||||
|
|
||||||
internal int DeferredSaveFrames = -1;
|
internal int DeferredSaveFrames = -1;
|
||||||
|
|
||||||
|
// Cancels the v1.4.8 FTS5 bulk-insert worker on plugin teardown. The
|
||||||
|
// worker runs off the framework thread on its own SqliteConnection, so a
|
||||||
|
// Dispose mid-rebuild must signal cancellation before MessageManager
|
||||||
|
// tears down (the worker logs "rebuild failed" via Log on error paths).
|
||||||
|
private CancellationTokenSource? _ftsRebuildCts;
|
||||||
|
|
||||||
// Serialises retention sweeps so a manual trigger and the 24h auto-sweep
|
// Serialises retention sweeps so a manual trigger and the 24h auto-sweep
|
||||||
// can't run in parallel. Volatile because the ImGui thread reads it outside
|
// can't run in parallel. Volatile because the ImGui thread reads it outside
|
||||||
// the lock to gate the manual button.
|
// the lock to gate the manual button.
|
||||||
@@ -158,30 +192,99 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||||
|
|
||||||
// Wire platform indirection before LoadAsync allocates anything that
|
// PlatformUtil and LogProxy are filled from the DI container in
|
||||||
// needs Util.* — services then read Plugin.PlatformUtil instead of
|
// Phase-1 below (`_host.Services.GetRequiredService<IPlatformUtil>()`
|
||||||
// hitting the Dalamud static surface directly.
|
// and the LogProxy equivalent). Phase-0 helpers that run before that
|
||||||
PlatformUtil = new DalamudPlatformUtil();
|
// point (MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize)
|
||||||
|
// do not touch either static, so the brief null-window is safe.
|
||||||
|
|
||||||
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
// Schema gate: v1.4.x requires config v16+. Users on older schemas
|
||||||
// must install v1.4.2 first to run the migration chain.
|
// must install v1.4.2 first to run the migration chain. v17 adds
|
||||||
|
// Tab.IsPinned (additive, no data migration needed) so v16 configs
|
||||||
|
// load cleanly and get their Version stamp bumped after the gate.
|
||||||
if (Config.Version < 16)
|
if (Config.Version < 16)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"HellionChat v1.4.6 requires config schema v16, got v{Config.Version}. "
|
$"HellionChat v1.4.10 requires config schema v16, got v{Config.Version}. "
|
||||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.6."
|
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Config.Version = 17;
|
||||||
|
|
||||||
// Session-only tabs are stripped on every load; AutoTellTabsService.Initialize
|
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||||
// then re-pegs TempTabCounter from the stripped list, not the pre-strip snapshot.
|
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
||||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
|
||||||
|
|
||||||
LanguageChanged(Interface.UiLanguage);
|
LanguageChanged(Interface.UiLanguage);
|
||||||
ImGuiUtil.Initialize(this);
|
ImGuiUtil.Initialize(this);
|
||||||
|
|
||||||
DeferredSaveFrames = -1;
|
DeferredSaveFrames = -1;
|
||||||
|
|
||||||
|
// Custom themes dir + seed run before the container builds so the
|
||||||
|
// ThemeRegistry factory lambda finds the directory ready.
|
||||||
|
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(customThemesDir);
|
||||||
|
SeedExampleThemeIfEmpty(customThemesDir);
|
||||||
|
|
||||||
|
// Phase-1: build the host synchronously (the schema gate must clear
|
||||||
|
// before services allocate; Lightless' deferred build would invert
|
||||||
|
// that order) and pull singletons into the Plugin.X surface.
|
||||||
|
var dependencies = new PluginHostDependencies(
|
||||||
|
Interface,
|
||||||
|
Log,
|
||||||
|
ChatGui,
|
||||||
|
ClientState,
|
||||||
|
CommandManager,
|
||||||
|
Condition,
|
||||||
|
DataManager,
|
||||||
|
Framework,
|
||||||
|
GameGui,
|
||||||
|
KeyState,
|
||||||
|
ObjectTable,
|
||||||
|
PartyList,
|
||||||
|
TargetManager,
|
||||||
|
TextureProvider,
|
||||||
|
GameInteropProvider,
|
||||||
|
GameConfig,
|
||||||
|
Notification,
|
||||||
|
AddonLifecycle,
|
||||||
|
PlayerState,
|
||||||
|
Evaluator,
|
||||||
|
SelfTestRegistry
|
||||||
|
);
|
||||||
|
|
||||||
|
_host = PluginHostFactory.Build(this, dependencies);
|
||||||
|
_lifecycle = _host.Services.GetRequiredService<PluginLifecycle>();
|
||||||
|
_lifecycle.Host = _host;
|
||||||
|
|
||||||
|
// Plugin.X static bridge - filled from the container so DI-aware code
|
||||||
|
// and the ~93 Plugin.X consumer sites read the same instances.
|
||||||
|
PlatformUtil = _host.Services.GetRequiredService<IPlatformUtil>();
|
||||||
|
LogProxy = _host.Services.GetRequiredService<IPluginLogProxy>();
|
||||||
|
FileDialogManager = _host.Services.GetRequiredService<FileDialogManager>();
|
||||||
|
|
||||||
|
// Resolve order matters: block-B services first so the windows can
|
||||||
|
// read Plugin.MessageManager etc. from their own ctors without NREs.
|
||||||
|
FontManager = _host.Services.GetRequiredService<FontManager>();
|
||||||
|
ThemeRegistry = _host.Services.GetRequiredService<Themes.ThemeRegistry>();
|
||||||
|
Commands = _host.Services.GetRequiredService<Commands>();
|
||||||
|
Functions = _host.Services.GetRequiredService<GameFunctions.GameFunctions>();
|
||||||
|
Ipc = _host.Services.GetRequiredService<IpcManager>();
|
||||||
|
TypingIpc = _host.Services.GetRequiredService<TypingIpc>();
|
||||||
|
ExtraChat = _host.Services.GetRequiredService<ExtraChat>();
|
||||||
|
HonorificService = _host.Services.GetRequiredService<Integrations.HonorificService>();
|
||||||
|
StatusBar = _host.Services.GetRequiredService<Ui.StatusBar>();
|
||||||
|
MessageManager = _host.Services.GetRequiredService<MessageManager>();
|
||||||
|
AutoTellTabsService = _host.Services.GetRequiredService<AutoTellTabsService>();
|
||||||
|
|
||||||
|
ChatLogWindow = _host.Services.GetRequiredService<ChatLogWindow>();
|
||||||
|
SettingsWindow = _host.Services.GetRequiredService<SettingsWindow>();
|
||||||
|
DbViewer = _host.Services.GetRequiredService<DbViewer>();
|
||||||
|
InputPreview = _host.Services.GetRequiredService<InputPreview>();
|
||||||
|
CommandHelpWindow = _host.Services.GetRequiredService<CommandHelpWindow>();
|
||||||
|
SeStringDebugger = _host.Services.GetRequiredService<SeStringDebugger>();
|
||||||
|
DebuggerWindow = _host.Services.GetRequiredService<DebuggerWindow>();
|
||||||
|
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAsync(CancellationToken cancellationToken)
|
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||||
@@ -203,65 +306,48 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// BuildFonts registers handles with Dalamud's FontAtlas; the atlas
|
// Container drives service init now: Host.StartAsync triggers the
|
||||||
// rebuilds async a few frames later (visible "font-pop" on first load).
|
// remaining IHostedService adapters (ThemeRegistry cache warmup +
|
||||||
FontManager = new FontManager();
|
// Switch, IPC eager-resolve, MessageManager FilterAllTabsAsync,
|
||||||
FontManager.BuildFonts();
|
// AutoTellTabsService.Initialize). FontManager runs its own init
|
||||||
|
// inline inside the ctor's SuppressAutoRebuild block on eager
|
||||||
|
// resolve. Window registration with WindowSystem runs on the
|
||||||
|
// framework thread inside PluginLifecycle.LoadAsync after
|
||||||
|
// StartAsync returns.
|
||||||
|
if (_lifecycle is not null)
|
||||||
|
await _lifecycle.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// ThemeRegistry must be wired before the first Draw tick.
|
SelfTestRegistry.RegisterTestSteps([
|
||||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
new SelfTests.ThemeSwitchSelfTestStep(this),
|
||||||
Directory.CreateDirectory(customThemesDir);
|
new SelfTests.FontManagerCtorSmokeStep(this),
|
||||||
SeedExampleThemeIfEmpty(customThemesDir);
|
new SelfTests.FontPushSmokeStep(this),
|
||||||
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
new SelfTests.WizardStateSmokeStep(this),
|
||||||
ThemeRegistry.Switch(Config.Theme);
|
]);
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
// Re-surface the wizard for existing users when a major UX
|
||||||
|
// rework ships. The constant tracks the most recent version
|
||||||
// Service allocations — order encodes dependencies.
|
// whose wizard should be shown once; bump it in future cycles
|
||||||
// HonorificService registers IPC subscribers early to catch
|
// that reshape the onboarding flow. Saved immediately so a
|
||||||
// Ready/Disposing events from the first frame.
|
// pre-Finish crash doesn't loop the prompt forever.
|
||||||
FileDialogManager = new FileDialogManager();
|
const string WizardReshowVersion = "1.5.2";
|
||||||
Commands = new Commands();
|
if (Config.WizardLastShownVersion != WizardReshowVersion)
|
||||||
Functions = new GameFunctions.GameFunctions(this);
|
{
|
||||||
Ipc = new IpcManager();
|
Config.FirstRunCompleted = false;
|
||||||
TypingIpc = new TypingIpc(this);
|
Config.WizardLastShownVersion = WizardReshowVersion;
|
||||||
ExtraChat = new ExtraChat();
|
SaveConfig();
|
||||||
HonorificService = new Integrations.HonorificService(Interface, Log, Framework);
|
}
|
||||||
StatusBar = new Ui.StatusBar();
|
|
||||||
MessageManager = new MessageManager(this);
|
|
||||||
|
|
||||||
AutoTellTabsService = new AutoTellTabsService(
|
|
||||||
this,
|
|
||||||
MessageManager,
|
|
||||||
MessageManager.Store
|
|
||||||
);
|
|
||||||
AutoTellTabsService.Initialize();
|
|
||||||
|
|
||||||
SelfTestRegistry.RegisterTestSteps([new SelfTests.ThemeSwitchSelfTestStep(this)]);
|
|
||||||
|
|
||||||
ChatLogWindow = new ChatLogWindow(this);
|
|
||||||
SettingsWindow = new SettingsWindow(this);
|
|
||||||
DbViewer = new DbViewer(this);
|
|
||||||
InputPreview = new InputPreview(ChatLogWindow);
|
|
||||||
CommandHelpWindow = new CommandHelpWindow(ChatLogWindow);
|
|
||||||
SeStringDebugger = new SeStringDebugger(this);
|
|
||||||
DebuggerWindow = new DebuggerWindow(this);
|
|
||||||
FirstRunWizard = new FirstRunWizard(this);
|
|
||||||
|
|
||||||
WindowSystem.AddWindow(ChatLogWindow);
|
|
||||||
WindowSystem.AddWindow(SettingsWindow);
|
|
||||||
WindowSystem.AddWindow(DbViewer);
|
|
||||||
WindowSystem.AddWindow(InputPreview);
|
|
||||||
WindowSystem.AddWindow(CommandHelpWindow);
|
|
||||||
WindowSystem.AddWindow(SeStringDebugger);
|
|
||||||
WindowSystem.AddWindow(DebuggerWindow);
|
|
||||||
WindowSystem.AddWindow(FirstRunWizard);
|
|
||||||
|
|
||||||
if (!Config.FirstRunCompleted)
|
if (!Config.FirstRunCompleted)
|
||||||
FirstRunWizard.IsOpen = true;
|
FirstRunWizard.IsOpen = true;
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Populate the command dictionary + UiBuilder hooks BEFORE
|
||||||
|
// Commands.Initialise() walks the dictionary and registers each
|
||||||
|
// entry with Dalamud's CommandManager (Commands.cs:15-28). Adding
|
||||||
|
// wrappers after Initialise() would leak them — they'd live in
|
||||||
|
// the dictionary but never reach Dalamud.
|
||||||
|
SetupCommands();
|
||||||
Commands.Initialise();
|
Commands.Initialise();
|
||||||
|
|
||||||
// Daily retention sweep — fire-and-forget, skips when disabled
|
// Daily retention sweep — fire-and-forget, skips when disabled
|
||||||
@@ -271,8 +357,115 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
if (Config.ShowEmotes)
|
if (Config.ShowEmotes)
|
||||||
_ = EmoteCache.LoadData();
|
_ = EmoteCache.LoadData();
|
||||||
|
|
||||||
if (Interface.Reason is not PluginLoadReason.Boot)
|
// FilterAllTabsAsync now runs from MessageManagerInitHostedService
|
||||||
MessageManager.FilterAllTabsAsync();
|
// during Host.StartAsync (same Reason-not-Boot guard there).
|
||||||
|
|
||||||
|
// Kick the FTS5 rebuild worker if Migrate4 just added the schema or
|
||||||
|
// a previous run was cut short (InitFtsReadyCache leaves _ftsReady
|
||||||
|
// false in that case). Runs off the framework thread on its own
|
||||||
|
// SqliteConnection so the live UpsertMessage path keeps flowing
|
||||||
|
// through the chunked-commit windows.
|
||||||
|
_ftsRebuildCts = new CancellationTokenSource();
|
||||||
|
if (!MessageManager.Store.IsFtsIndexBuilt)
|
||||||
|
{
|
||||||
|
var token = _ftsRebuildCts.Token;
|
||||||
|
_ = Task.Run(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
// FQN: Plugin.Notification (Z.74) shadows the type name.
|
||||||
|
Dalamud.Interface.ImGuiNotification.IActiveNotification? notif = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
notif = Notification.AddNotification(
|
||||||
|
new Dalamud.Interface.ImGuiNotification.Notification
|
||||||
|
{
|
||||||
|
Title = "Hellion Chat",
|
||||||
|
Content = "Indexing chat history for full-text search...",
|
||||||
|
Type = Dalamud
|
||||||
|
.Interface
|
||||||
|
.ImGuiNotification
|
||||||
|
.NotificationType
|
||||||
|
.Info,
|
||||||
|
Minimized = false,
|
||||||
|
InitialDuration = TimeSpan.FromMinutes(10),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Progress<T> raises this callback on the captured
|
||||||
|
// sync-context (Task.Run worker pool). IActiveNotification
|
||||||
|
// is ImGui-backed and mutates the UI, so marshal the
|
||||||
|
// mutation onto the framework thread via RunOnTick.
|
||||||
|
var progress = new Progress<long>(done =>
|
||||||
|
{
|
||||||
|
Framework.RunOnTick(() =>
|
||||||
|
{
|
||||||
|
if (notif is { } n)
|
||||||
|
n.Content = $"Indexing chat history: {done:N0} messages...";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Worker-owned connection. Closed+disposed before we
|
||||||
|
// flip the readiness flag so the DbViewer never sees
|
||||||
|
// IsFtsIndexBuilt=true while the worker connection
|
||||||
|
// is still alive.
|
||||||
|
SqliteConnection? workerConn = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
workerConn = MessageManager.Store.OpenSecondaryConnection();
|
||||||
|
var total = await Task.Run(
|
||||||
|
() =>
|
||||||
|
MessageManager.Store.RebuildFtsIndex(
|
||||||
|
workerConn,
|
||||||
|
progress,
|
||||||
|
token
|
||||||
|
),
|
||||||
|
token
|
||||||
|
)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
workerConn.Close();
|
||||||
|
workerConn.Dispose();
|
||||||
|
workerConn = null;
|
||||||
|
MessageManager.Store.MarkFtsIndexBuilt();
|
||||||
|
|
||||||
|
if (notif is { } final)
|
||||||
|
{
|
||||||
|
final.Content = $"Indexed {total:N0} messages.";
|
||||||
|
final.Type = Dalamud
|
||||||
|
.Interface
|
||||||
|
.ImGuiNotification
|
||||||
|
.NotificationType
|
||||||
|
.Success;
|
||||||
|
final.InitialDuration = TimeSpan.FromSeconds(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
workerConn?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
notif?.DismissNow();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "FTS index rebuild failed");
|
||||||
|
if (notif is { } err)
|
||||||
|
{
|
||||||
|
err.Content =
|
||||||
|
"Full-text indexing failed -- search will use local filter only.";
|
||||||
|
err.Type = Dalamud
|
||||||
|
.Interface
|
||||||
|
.ImGuiNotification
|
||||||
|
.NotificationType
|
||||||
|
.Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ftsRebuildCts.Token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
||||||
Interface.UiBuilder.DisableGposeUiHide = true;
|
Interface.UiBuilder.DisableGposeUiHide = true;
|
||||||
@@ -290,7 +483,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
Framework.Update += FrameworkUpdate;
|
Framework.Update += FrameworkUpdate;
|
||||||
Interface.UiBuilder.Draw += Draw;
|
Interface.UiBuilder.Draw += Draw;
|
||||||
Interface.LanguageChanged += LanguageChanged;
|
Interface.LanguageChanged += LanguageChanged;
|
||||||
Interface.UiBuilder.OpenMainUi += OpenMainUi;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -312,14 +504,32 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Set before any cleanup so deferred Framework.RunOnTick callbacks
|
||||||
|
// (B3 retention sweep) see the flag and bail out before they touch
|
||||||
|
// MessageManager / Log / static fields that the rest of this method
|
||||||
|
// is about to tear down.
|
||||||
|
_isDisposing = true;
|
||||||
|
|
||||||
Exception? failure = null;
|
Exception? failure = null;
|
||||||
|
|
||||||
// Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync.
|
// Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync.
|
||||||
failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi);
|
|
||||||
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
|
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
|
||||||
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
|
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
|
||||||
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
|
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
|
||||||
|
|
||||||
|
// Signal the FTS rebuild worker to bail. Runs before MessageManager
|
||||||
|
// tears down so the worker's "rebuild failed" log path still finds
|
||||||
|
// a live Log static. Worker owns its own SqliteConnection and disposes
|
||||||
|
// it itself; we only flip the cancellation flag here.
|
||||||
|
failure = CaptureFailure(
|
||||||
|
failure,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_ftsRebuildCts?.Cancel();
|
||||||
|
_ftsRebuildCts?.Dispose();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore.
|
// Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore.
|
||||||
failure = CaptureFailure(
|
failure = CaptureFailure(
|
||||||
failure,
|
failure,
|
||||||
@@ -333,44 +543,18 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unsubscribe AutoTellTabs before MessageManager goes away.
|
// Framework-thread cleanup the container does not reach.
|
||||||
failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose());
|
|
||||||
|
|
||||||
// MessageManager has its own async dispose path (DB flush, thread shutdown).
|
|
||||||
if (MessageManager is not null)
|
|
||||||
{
|
|
||||||
failure = await CaptureFailureAsync(
|
|
||||||
failure,
|
|
||||||
() => MessageManager.DisposeAsync().AsTask()
|
|
||||||
)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game-function / IPC / window cleanup must run on the framework thread.
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Framework
|
await Framework
|
||||||
.RunOnFrameworkThread(() =>
|
.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
|
failure = CaptureFailure(failure, TearDownCommands);
|
||||||
failure = CaptureFailure(
|
failure = CaptureFailure(
|
||||||
failure,
|
failure,
|
||||||
() => GameFunctions.GameFunctions.SetChatInteractable(true)
|
() => GameFunctions.GameFunctions.SetChatInteractable(true)
|
||||||
);
|
);
|
||||||
|
|
||||||
// IPC subscribers before windows — prevents a final IPC event
|
|
||||||
// from reaching a half-torn ChatLogWindow.
|
|
||||||
failure = CaptureFailure(failure, () => HonorificService?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => TypingIpc?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => ExtraChat?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => Ipc?.Dispose());
|
|
||||||
|
|
||||||
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
|
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
|
||||||
failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => DbViewer?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => InputPreview?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => SettingsWindow?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => DebuggerWindow?.Dispose());
|
|
||||||
failure = CaptureFailure(failure, () => SeStringDebugger?.Dispose());
|
|
||||||
})
|
})
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -379,11 +563,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
failure ??= ex;
|
failure ??= ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pure-memory cleanups — no Framework / UI / IPC touch.
|
// Container disposes services + windows on the framework thread.
|
||||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
// MessageManager.DisposeAsync is not idempotent, so we let the
|
||||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
// container do it once instead of double-disposing.
|
||||||
|
if (_lifecycle is not null)
|
||||||
|
{
|
||||||
|
failure = await CaptureFailureAsync(failure, () => _lifecycle.DisposeAsync().AsTask())
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static-class cleanups the container has no handle on.
|
||||||
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
||||||
// Static input history would otherwise survive the plugin reload.
|
|
||||||
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
||||||
|
|
||||||
if (failure is not null)
|
if (failure is not null)
|
||||||
@@ -530,11 +720,95 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenMainUi()
|
// Central slash-command + UiBuilder.OpenConfigUi/OpenMainUi subscribe so
|
||||||
|
// the four lazy windows (Settings, DbViewer, SeStringDebugger, Debugger)
|
||||||
|
// have working entry points before they're constructed.
|
||||||
|
private void SetupCommands()
|
||||||
{
|
{
|
||||||
SettingsWindow.IsOpen = !SettingsWindow.IsOpen;
|
// ChatLogWindow.cs:128 already registers /hellion (ToggleChat). The
|
||||||
|
// description-arg here keeps the Dalamud help list populated.
|
||||||
|
_hellionSettingsCmd = Commands.Register(
|
||||||
|
"/hellion",
|
||||||
|
"Perform various actions with Hellion Chat."
|
||||||
|
);
|
||||||
|
_hellionSettingsCmd.Execute += OnHellionSettingsCommand;
|
||||||
|
|
||||||
|
_hellionViewCmd = Commands.Register(
|
||||||
|
"/hellionView",
|
||||||
|
"Get access to your message history, with simple filter options.",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
_hellionViewCmd.Execute += OnHellionViewCommand;
|
||||||
|
|
||||||
|
_hellionDebuggerCmd = Commands.Register("/hellionDebugger", showInHelp: false);
|
||||||
|
_hellionDebuggerCmd.Execute += OnHellionDebuggerCommand;
|
||||||
|
#if DEBUG
|
||||||
|
// SeStringDebugger.cs lives under #if DEBUG too; keep this out of release builds.
|
||||||
|
_hellionSeStringCmd = Commands.Register("/hellionSeString", showInHelp: false);
|
||||||
|
_hellionSeStringCmd.Execute += OnHellionSeStringCommand;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Plugin-Manager "Settings" button. Was in Settings.cs:67 pre-v1.4.9.
|
||||||
|
Interface.UiBuilder.OpenConfigUi += OnOpenConfigUi;
|
||||||
|
|
||||||
|
// Plugin-Manager "Open" button. Was in Plugin.cs LoadAsync pre-v1.4.9
|
||||||
|
// (separate OpenMainUi handler that flipped SettingsWindow.IsOpen).
|
||||||
|
Interface.UiBuilder.OpenMainUi += OnOpenMainUi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TearDownCommands()
|
||||||
|
{
|
||||||
|
Interface.UiBuilder.OpenMainUi -= OnOpenMainUi;
|
||||||
|
Interface.UiBuilder.OpenConfigUi -= OnOpenConfigUi;
|
||||||
|
|
||||||
|
// Null-tolerant detaches: TearDownCommands can run from the LoadAsync
|
||||||
|
// failure path (Plugin.cs CaptureFailure) before SetupCommands finished.
|
||||||
|
if (_hellionSettingsCmd is not null)
|
||||||
|
{
|
||||||
|
_hellionSettingsCmd.Execute -= OnHellionSettingsCommand;
|
||||||
|
_hellionSettingsCmd = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hellionViewCmd is not null)
|
||||||
|
{
|
||||||
|
_hellionViewCmd.Execute -= OnHellionViewCommand;
|
||||||
|
_hellionViewCmd = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hellionDebuggerCmd is not null)
|
||||||
|
{
|
||||||
|
_hellionDebuggerCmd.Execute -= OnHellionDebuggerCommand;
|
||||||
|
_hellionDebuggerCmd = null;
|
||||||
|
}
|
||||||
|
#if DEBUG
|
||||||
|
if (_hellionSeStringCmd is not null)
|
||||||
|
{
|
||||||
|
_hellionSeStringCmd.Execute -= OnHellionSeStringCommand;
|
||||||
|
_hellionSeStringCmd = null;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHellionSettingsCommand(string command, string arguments)
|
||||||
|
{
|
||||||
|
// /hellion with args is intentionally a no-op (matches pre-v1.4.9
|
||||||
|
// Settings.cs:76-80 behaviour).
|
||||||
|
if (string.IsNullOrWhiteSpace(arguments))
|
||||||
|
SettingsWindow.Toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOpenConfigUi() => SettingsWindow.Toggle();
|
||||||
|
|
||||||
|
private void OnOpenMainUi() => SettingsWindow.Toggle();
|
||||||
|
|
||||||
|
private void OnHellionViewCommand(string _, string __) => DbViewer.Toggle();
|
||||||
|
|
||||||
|
private void OnHellionDebuggerCommand(string _, string __) => DebuggerWindow.Toggle();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
private void OnHellionSeStringCommand(string _, string __) => SeStringDebugger.Toggle();
|
||||||
|
#endif
|
||||||
|
|
||||||
private void RunRetentionSweepIfDue()
|
private void RunRetentionSweepIfDue()
|
||||||
{
|
{
|
||||||
if (!Config.RetentionEnabled)
|
if (!Config.RetentionEnabled)
|
||||||
@@ -570,15 +844,31 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
if (deleted > 0)
|
if (deleted > 0)
|
||||||
{
|
{
|
||||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||||
// Run clear+refilter on the framework thread — FilterAllTabsAsync
|
// Schedule on the next framework tick to avoid the ~194ms
|
||||||
// is fire-and-forget and would race the next sweep cycle.
|
// hitch from blocking with .Wait() while the framework
|
||||||
Framework
|
// finishes the current frame. Tabs-list mutation must
|
||||||
.Run(() =>
|
// stay on the framework thread because Plugin.Config.Tabs
|
||||||
|
// (Configuration.cs:222) is not lock-protected and
|
||||||
|
// AutoTellTabsService can mutate it from background paths.
|
||||||
|
// Pattern reference: SimpleTweaks
|
||||||
|
// Tweaks/Chat/CaseInsensitiveCommands.cs:45.
|
||||||
|
Framework.RunOnTick(() =>
|
||||||
|
{
|
||||||
|
// The retention thread is IsBackground=true so plugin
|
||||||
|
// unload can fire while a scheduled tick is still
|
||||||
|
// pending; bail before touching anything torn down.
|
||||||
|
if (_isDisposing)
|
||||||
|
return;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
MessageManager.ClearAllTabs();
|
MessageManager.ClearAllTabs();
|
||||||
MessageManager.FilterAllTabs();
|
MessageManager.FilterAllTabs();
|
||||||
})
|
}
|
||||||
.Wait();
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Retention sweep clear+refilter failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -602,6 +892,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
private void Draw()
|
private void Draw()
|
||||||
{
|
{
|
||||||
|
// v1.4.8 B2: pick up external edits of the active custom theme JSON
|
||||||
|
// without forcing the user to re-click the picker. The disk-stat is
|
||||||
|
// 1Hz-throttled inside RefreshActiveIfStale, so this is essentially
|
||||||
|
// free on built-in themes and ~1 stat/second on custom themes.
|
||||||
|
ThemeRegistry.RefreshActiveIfStale();
|
||||||
|
|
||||||
// Theme engine is always active; Classic is a theme, not a disabled state.
|
// Theme engine is always active; Classic is a theme, not a disabled state.
|
||||||
using IDisposable _style = HellionStyle.PushGlobal(
|
using IDisposable _style = HellionStyle.PushGlobal(
|
||||||
ThemeRegistry.Active,
|
ThemeRegistry.Active,
|
||||||
@@ -635,7 +931,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
||||||
ChatLogWindow.DefaultText = ImGui.GetStyle().Colors[(int)ImGuiCol.Text];
|
ChatLogWindow.DefaultText = ImGui.GetStyle().Colors[(int)ImGuiCol.Text];
|
||||||
|
|
||||||
using ((Config.FontsEnabled ? FontManager.RegularFont : FontManager.Axis).Push())
|
// RegularFont is nullable only because the live rebuild path
|
||||||
|
// disposes it before reassigning; both ends of that swap happen on
|
||||||
|
// this same draw thread, so it cannot be null here.
|
||||||
|
using ((Config.FontsEnabled ? FontManager.RegularFont! : FontManager.Axis).Push())
|
||||||
WindowSystem.Draw();
|
WindowSystem.Draw();
|
||||||
|
|
||||||
ChatLogWindow.FinalizeFrame();
|
ChatLogWindow.FinalizeFrame();
|
||||||
@@ -646,21 +945,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
internal void SaveConfig()
|
internal void SaveConfig()
|
||||||
{
|
{
|
||||||
// Session-only Auto-Tell-Tabs aren't persisted, so they move aside
|
// Only unpinned TempTabs are session-only — they move aside before
|
||||||
// before serialization and re-attach after. Cloning only the temp
|
// serialization and re-attach after. Pinned TempTabs stay in
|
||||||
// subset keeps the allocation proportional to AutoTellTabsLimit
|
// Config.Tabs across the save so JSON includes them. Cloning only the
|
||||||
// (<=15) instead of the full tab list.
|
// unpinned subset keeps the allocation proportional to
|
||||||
var tempTabs = Config.Tabs.Where(t => t.IsTempTab).ToList();
|
// AutoTellTabsLimit (<=15) instead of the full tab list.
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||||
|
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||||
|
|
||||||
Interface.SavePluginConfig(Config);
|
Interface.SavePluginConfig(Config);
|
||||||
|
|
||||||
Config.Tabs.AddRange(tempTabs);
|
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||||
|
|
||||||
// F2.1: the mid-step RemoveAll bypasses AutoTellTabsService, so
|
|
||||||
// re-peg the counter. Null-conditional because SaveConfig can fire
|
|
||||||
// before Phase-2 init.
|
|
||||||
AutoTellTabsService?.ResyncTempTabCounter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void LanguageChanged(string langCode)
|
internal void LanguageChanged(string langCode)
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using HellionChat.Infrastructure.Hosting;
|
||||||
|
using HellionChat.Infrastructure.Logging;
|
||||||
|
using HellionChat.Ipc;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Ui;
|
||||||
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat;
|
||||||
|
|
||||||
|
// Builds the generic-host DI container that drives v1.5.0+. The factory is
|
||||||
|
// invoked synchronously from Plugin.ctor (after the schema gate clears) so the
|
||||||
|
// container exists before PluginLifecycle.LoadAsync runs. See plan §1 for the
|
||||||
|
// deliberate divergence from Lightless' deferred Func-delegate pattern.
|
||||||
|
internal static class PluginHostFactory
|
||||||
|
{
|
||||||
|
public static IHost Build(Plugin plugin, PluginHostDependencies dependencies)
|
||||||
|
{
|
||||||
|
return new HostBuilder()
|
||||||
|
.UseContentRoot(dependencies.PluginInterface.ConfigDirectory.FullName)
|
||||||
|
.ConfigureLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.ClearProviders();
|
||||||
|
logging.AddDalamudLogging(dependencies.PluginLog);
|
||||||
|
logging.SetMinimumLevel(LogLevel.Trace);
|
||||||
|
})
|
||||||
|
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureServices(
|
||||||
|
IServiceCollection services,
|
||||||
|
Plugin plugin,
|
||||||
|
PluginHostDependencies dependencies
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Block A — Dalamud services (21 [PluginService] singletons).
|
||||||
|
services.AddSingleton(dependencies);
|
||||||
|
services.AddSingleton(dependencies.PluginInterface);
|
||||||
|
services.AddSingleton(dependencies.PluginLog);
|
||||||
|
services.AddSingleton(dependencies.ChatGui);
|
||||||
|
services.AddSingleton(dependencies.ClientState);
|
||||||
|
services.AddSingleton(dependencies.CommandManager);
|
||||||
|
services.AddSingleton(dependencies.Condition);
|
||||||
|
services.AddSingleton(dependencies.DataManager);
|
||||||
|
services.AddSingleton(dependencies.Framework);
|
||||||
|
services.AddSingleton(dependencies.GameGui);
|
||||||
|
services.AddSingleton(dependencies.KeyState);
|
||||||
|
services.AddSingleton(dependencies.ObjectTable);
|
||||||
|
services.AddSingleton(dependencies.PartyList);
|
||||||
|
services.AddSingleton(dependencies.TargetManager);
|
||||||
|
services.AddSingleton(dependencies.TextureProvider);
|
||||||
|
services.AddSingleton(dependencies.GameInteropProvider);
|
||||||
|
services.AddSingleton(dependencies.GameConfig);
|
||||||
|
services.AddSingleton(dependencies.Notification);
|
||||||
|
services.AddSingleton(dependencies.AddonLifecycle);
|
||||||
|
services.AddSingleton(dependencies.PlayerState);
|
||||||
|
services.AddSingleton(dependencies.Evaluator);
|
||||||
|
services.AddSingleton(dependencies.SelfTestRegistry);
|
||||||
|
|
||||||
|
// Self-references: Plugin and its WindowSystem already exist.
|
||||||
|
services.AddSingleton(plugin);
|
||||||
|
services.AddSingleton(plugin.WindowSystem);
|
||||||
|
services.AddSingleton<PluginLifecycle>();
|
||||||
|
|
||||||
|
// Block B — HellionChat singletons. Factory lambdas because most
|
||||||
|
// classes are internal-sealed and the default activator only sees
|
||||||
|
// public ctors.
|
||||||
|
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
|
||||||
|
services.AddSingleton<IPluginLogProxy>(sp => new DalamudPluginLogProxy(
|
||||||
|
sp.GetRequiredService<IPluginLog>()
|
||||||
|
));
|
||||||
|
services.AddSingleton<FileDialogManager>(_ => new FileDialogManager());
|
||||||
|
services.AddSingleton(sp => new Commands(sp.GetRequiredService<ILogger<Commands>>()));
|
||||||
|
services.AddSingleton(sp => new FontManager(
|
||||||
|
sp.GetRequiredService<IDalamudPluginInterface>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(_ => new StatusBar());
|
||||||
|
services.AddSingleton(sp => new IpcManager(sp.GetRequiredService<ILogger<IpcManager>>()));
|
||||||
|
services.AddSingleton(sp => new ExtraChat(sp.GetRequiredService<ILogger<ExtraChat>>()));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new ThemeRegistry(
|
||||||
|
Path.Combine(
|
||||||
|
sp.GetRequiredService<IDalamudPluginInterface>().ConfigDirectory.FullName,
|
||||||
|
"themes"
|
||||||
|
),
|
||||||
|
sp.GetRequiredService<ILogger<ThemeRegistry>>()
|
||||||
|
));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new GameFunctions.GameFunctions(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<GameFunctions.GameFunctions>>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new TypingIpc(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<TypingIpc>>()
|
||||||
|
));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new Integrations.HonorificService(
|
||||||
|
sp.GetRequiredService<IDalamudPluginInterface>(),
|
||||||
|
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
|
||||||
|
sp.GetRequiredService<IFramework>()
|
||||||
|
));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new MessageManager(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<MessageManager>>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
|
||||||
|
// MessageStore is allocated inside MessageManager.ctor; a separate
|
||||||
|
// container singleton would double-construct the SQLite handle.
|
||||||
|
services.AddSingleton(sp =>
|
||||||
|
{
|
||||||
|
var pluginRef = sp.GetRequiredService<Plugin>();
|
||||||
|
var manager = sp.GetRequiredService<MessageManager>();
|
||||||
|
return new AutoTellTabsService(
|
||||||
|
pluginRef,
|
||||||
|
manager,
|
||||||
|
manager.Store,
|
||||||
|
sp.GetRequiredService<ILogger<AutoTellTabsService>>()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block C — Windows. WindowSystem.AddWindow is called from
|
||||||
|
// PluginLifecycle.LoadAsync on the framework thread.
|
||||||
|
services.AddSingleton(sp => new ChatLogWindow(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<ChatLogWindow>>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new SettingsWindow(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILoggerFactory>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new DbViewer(
|
||||||
|
sp.GetRequiredService<Plugin>(),
|
||||||
|
sp.GetRequiredService<ILogger<DbViewer>>()
|
||||||
|
));
|
||||||
|
services.AddSingleton(sp => new InputPreview(sp.GetRequiredService<ChatLogWindow>()));
|
||||||
|
services.AddSingleton(sp => new CommandHelpWindow(sp.GetRequiredService<ChatLogWindow>()));
|
||||||
|
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
|
||||||
|
services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>()));
|
||||||
|
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
|
||||||
|
|
||||||
|
// Hosted-service adapters: thin wrappers around the existing init
|
||||||
|
// methods so the service class bodies stay unchanged. FontManager
|
||||||
|
// does not need one — its ctor runs the init inline inside a single
|
||||||
|
// SuppressAutoRebuild block on eager resolve.
|
||||||
|
services.AddHostedService(sp => new ThemeRegistryInitHostedService(
|
||||||
|
sp.GetRequiredService<ThemeRegistry>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new IpcManagerInitHostedService(
|
||||||
|
sp.GetRequiredService<IpcManager>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new TypingIpcInitHostedService(
|
||||||
|
sp.GetRequiredService<TypingIpc>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new ExtraChatInitHostedService(
|
||||||
|
sp.GetRequiredService<ExtraChat>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new MessageManagerInitHostedService(
|
||||||
|
sp.GetRequiredService<IDalamudPluginInterface>(),
|
||||||
|
sp.GetRequiredService<MessageManager>()
|
||||||
|
));
|
||||||
|
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
|
||||||
|
sp.GetRequiredService<AutoTellTabsService>()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record PluginHostDependencies(
|
||||||
|
IDalamudPluginInterface PluginInterface,
|
||||||
|
IPluginLog PluginLog,
|
||||||
|
IChatGui ChatGui,
|
||||||
|
IClientState ClientState,
|
||||||
|
ICommandManager CommandManager,
|
||||||
|
ICondition Condition,
|
||||||
|
IDataManager DataManager,
|
||||||
|
IFramework Framework,
|
||||||
|
IGameGui GameGui,
|
||||||
|
IKeyState KeyState,
|
||||||
|
IObjectTable ObjectTable,
|
||||||
|
IPartyList PartyList,
|
||||||
|
ITargetManager TargetManager,
|
||||||
|
ITextureProvider TextureProvider,
|
||||||
|
IGameInteropProvider GameInteropProvider,
|
||||||
|
IGameConfig GameConfig,
|
||||||
|
INotificationManager Notification,
|
||||||
|
IAddonLifecycle AddonLifecycle,
|
||||||
|
IPlayerState PlayerState,
|
||||||
|
ISeStringEvaluator Evaluator,
|
||||||
|
ISelfTestRegistry SelfTestRegistry
|
||||||
|
);
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace HellionChat;
|
||||||
|
|
||||||
|
// Orchestrates Host.StartAsync / StopAsync and the framework-thread dispose.
|
||||||
|
// Plugin.ctor builds the host and assigns it via the Host property, so
|
||||||
|
// PluginLifecycle never constructs the host itself.
|
||||||
|
internal sealed class PluginLifecycle : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
private readonly Plugin _plugin;
|
||||||
|
|
||||||
|
private int _disposeStarted;
|
||||||
|
private bool _hostStartRequested;
|
||||||
|
|
||||||
|
public PluginLifecycle(IFramework framework, Plugin plugin)
|
||||||
|
{
|
||||||
|
_framework = framework;
|
||||||
|
_plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin.ctor fills this immediately after PluginHostFactory.Build and
|
||||||
|
// before invoking LoadAsync; LoadAsync may NRE-suppress on Host! safely.
|
||||||
|
public IHost? Host { get; set; }
|
||||||
|
|
||||||
|
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_hostStartRequested = true;
|
||||||
|
await Host!.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// WindowSystem.AddWindow mutates an internal List<>; v1.4.9 Stage-2
|
||||||
|
// verified the list is non-thread-safe, so we marshal the entire
|
||||||
|
// registration block to the framework thread.
|
||||||
|
await _framework
|
||||||
|
.RunOnFrameworkThread(() => RegisterWindows(_plugin))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Swallow secondary dispose failure so the original load throw wins.
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterWindows(Plugin plugin)
|
||||||
|
{
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.ChatLogWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.SettingsWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.DbViewer);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.InputPreview);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.CommandHelpWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.SeStringDebugger);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
|
||||||
|
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||||
|
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Exception? failure = null;
|
||||||
|
|
||||||
|
if (_hostStartRequested && Host is not null)
|
||||||
|
failure = await CaptureFailureAsync(failure, () => Host.StopAsync())
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
failure = await DisposeHostOnFrameworkThreadAsync(failure).ConfigureAwait(false);
|
||||||
|
|
||||||
|
ThrowIfFailed(failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Exception?> DisposeHostOnFrameworkThreadAsync(Exception? failure)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _framework
|
||||||
|
.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
failure = CaptureFailure(failure, () => Host?.Dispose());
|
||||||
|
})
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure ??= ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception? CaptureFailure(Exception? failure, Action action)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure ??= ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask<Exception?> CaptureFailureAsync(
|
||||||
|
Exception? failure,
|
||||||
|
Func<Task> action
|
||||||
|
)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await action().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failure ??= ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ThrowIfFailed(Exception? failure)
|
||||||
|
{
|
||||||
|
if (failure is not null)
|
||||||
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,4 +114,29 @@ internal static class PrivacyDefaults
|
|||||||
[ChatType.StandardEmote] = 1,
|
[ChatType.StandardEmote] = 1,
|
||||||
[ChatType.NoviceNetwork] = 1,
|
[ChatType.NoviceNetwork] = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Roleplay: Privacy-First + Say + both emote types. Public-distance
|
||||||
|
// channels (Shout, Yell) stay out — they are public-noise from
|
||||||
|
// strangers, not story content. Novice Network also stays out;
|
||||||
|
// it is not RP-adjacent and would dilute the profile's intent.
|
||||||
|
internal static readonly IReadOnlySet<ChatType> RoleplayWhitelist = new HashSet<ChatType>(
|
||||||
|
PrivacyFirstWhitelist
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ChatType.Say,
|
||||||
|
ChatType.CustomEmote,
|
||||||
|
ChatType.StandardEmote,
|
||||||
|
};
|
||||||
|
|
||||||
|
// RP sessions function as story logs: Say + emotes need a longer
|
||||||
|
// window than Casual's 1-day public-chat window. 30 days for Say
|
||||||
|
// keeps in-character dialogue scrollable across multiple sessions,
|
||||||
|
// 90 days for emotes mirrors the Privacy-First conversation default.
|
||||||
|
internal static readonly IReadOnlyDictionary<ChatType, int> RoleplayRetentionOverrides =
|
||||||
|
new Dictionary<ChatType, int>
|
||||||
|
{
|
||||||
|
[ChatType.Say] = 30,
|
||||||
|
[ChatType.CustomEmote] = 90,
|
||||||
|
[ChatType.StandardEmote] = 90,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.:;+xXXX$$$$$$$$XXx+;:
|
||||||
|
.X$+ .;+X$$$$$$$$$$$$$$$$$$$$$$$$$$$x:
|
||||||
|
;$xx$$X+:... .....::+X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$;.
|
||||||
|
X$; .:+xXXX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X:
|
||||||
|
$$; :++xX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X;
|
||||||
|
$$x. .+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X.
|
||||||
|
x$$; ;$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+;::::::;x$$$$$:
|
||||||
|
:$$$; .+$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+:. .+$$$$$$$$$X+;;:
|
||||||
|
;$$$+. :X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X;: :$$$$$$$$$$$$$$$$X;.
|
||||||
|
.+$$$X: ..;X$$$$$$$$$$$$$$$$$$$$$$$$$$X;.. :$$$$$$$$$$$$$$$$$$$$X:
|
||||||
|
;$$$$$X+::::+X$$$$$$$$$$$$$$$$$$$$$X;. .$$$$$$$$$$$$$$$$$$$$$$$X;
|
||||||
|
+$$$$$$$$$$$$$$$$$$$$$$$$$$$$X+: Hellion Forge x$$$$$$$$$$$$$$$$$$$$$$$$$X:
|
||||||
|
.;x$$$$$$$$$$$$$$$$$$$$$x;: .X$$$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||||
|
.;+$$$$$$$$$$X+;:.. .X$$$$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||||
|
.X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$;
|
||||||
|
.X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X
|
||||||
|
x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$X
|
||||||
|
;$$$$$$xx$$$$$$$$$$$$$$$$$$$$$x
|
||||||
|
.$$$$$$x+$$$$$$$$$$$$$$$$$$$$$x
|
||||||
|
:+X$$$$$$X;$$$$$$$$$$$$$$$$$$$$$$:
|
||||||
|
;$$$$$$$$$$;$$$$$$$$$$$$$$$$$$$$$$X.
|
||||||
|
+$$$$$$$$$$;x$$$$$$$$$$$$$$$$$$$$$$+
|
||||||
|
x$$$$$$$$$$:$$$$$$$$$$$$$$$$$$$$$$X:
|
||||||
|
.X$$$$$$$$$.:$$$$$$$$$$$$$$$$$$$$$$;
|
||||||
|
:X$$X;;;;: .$$$$$$$$$$$$$$$$$$$$$$X.
|
||||||
|
.$$$$X .$$$$$$$$$$$$$$$$$$$$$$$:
|
||||||
|
.$$$$+ .X$$$$$$$$$$$$$$$$$$$$$$;
|
||||||
|
;$$$$: .X$$$$$$$$$$$$$$$$$$$$$$x
|
||||||
|
:X$$$+ .$$$$$$$$$$$$$$$$$$$$$$$X
|
||||||
|
+$$$x :$$$$$$$$$$$$$$$$$$$$$$$X
|
||||||
|
;$$X: $$$$$$$$$$$$$$$$$$$$$$$$X
|
||||||
|
x$$$$$$$$$$$$$$$$$$$$$$$$X
|
||||||
|
+$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||||
|
.+$$$$$$$$$$$$$$$$$$$$$$$$$$;
|
||||||
|
. ;$$$$$$$$$$$$$$$$$$$$$$$$$$$$:
|
||||||
|
:X$x$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
|
||||||
|
.XX$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+
|
||||||
|
;$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$+$;
|
||||||
|
.. ++X$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$:+$:
|
||||||
|
:$$+. ;$$$$$$$$$$$$$$X$$$$$$$$$$$$$$$$$$$$$;:$$+
|
||||||
|
.x+X$X: X$$$$$$$$$$x::;:;$$$$$$$$$$$$$$$$$$X: ;$X.
|
||||||
|
:X.x$$$:.::::::;x+:X$$$$;$$$$$$$$$$$$$$$$$$: :X;
|
||||||
|
:x.x$$$$$$$$$$$$$$$$$;;$:$$$$$$$$$$$$$$$$$: :$+
|
||||||
|
:Xx$$$$$$$$$$$$$$$$$: ;X;$$$$$$$$$$$$$$$$: .+$$;
|
||||||
|
;$$$$$$$$$$$$$$$$$$; .X+X$$$$$$$$$$$$$$$+ .+$+.
|
||||||
|
+$$$$$$$$$$$$$$$$$$$$$$$;+$$$$$$$$$$$$$$X: .+X:
|
||||||
|
+$$$$$$$$$$$$$$$$$$$$$$$$$+:$$$$$$$$$$$$$+.+$+.
|
||||||
|
;$$$$$$$$$$$$$$$$$$$$$$$$$$$X;$$$$$$$$$$$$$$X:
|
||||||
|
+X: .:X$$$$$$$$x+++x$$$$$$$$;:X$$$$$$$$$$$X:
|
||||||
|
:x.;$;+$$$$$:. :X$$$$X :$$$$$$$$$$X:
|
||||||
|
;x :X$$$; .x$$x X$$; .:+.$$$$$$$$$$x
|
||||||
|
xx.X$$X: X$;.:$X:.X$$$$$$$$$:
|
||||||
|
+$$$$X. ;$;::: .$$$$$$$$$:
|
||||||
|
;$$$; :+X$$$$XX$; X$$$$$$$$:
|
||||||
|
;$$X: .:x$x$$$$$X. x$$$$$$$$:
|
||||||
|
:X$X: :+x; :$$$$$: +$$$$$$$X:
|
||||||
|
:++$X+xXX;. +$$$$. +$$$$$$$+.
|
||||||
|
... .X$$$X. +$$$$$$$:
|
||||||
|
;$$$$; .X$$$$$$x.
|
||||||
|
;$$X; :X$$$$$$;
|
||||||
|
;$$$$$$x.
|
||||||
|
.X$$$$$$;
|
||||||
|
;$$$$$$+
|
||||||
|
+$$$$$;
|
||||||
|
:X$$$$;.
|
||||||
|
;$$$$+.
|
||||||
|
.x$$$X:
|
||||||
|
.+$$X;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|\_/|,,_____,~~`
|
||||||
|
(.".)~~ )`~}}
|
||||||
|
\o/\ /---~\\ ~}}
|
||||||
|
_// _// ~}
|
||||||
+53
@@ -116,6 +116,38 @@ internal class HellionStrings
|
|||||||
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
|
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
|
||||||
internal static string Wizard_Cancel_Label => Get(nameof(Wizard_Cancel_Label));
|
internal static string Wizard_Cancel_Label => Get(nameof(Wizard_Cancel_Label));
|
||||||
internal static string Wizard_Cancel_Tooltip => Get(nameof(Wizard_Cancel_Tooltip));
|
internal static string Wizard_Cancel_Tooltip => Get(nameof(Wizard_Cancel_Tooltip));
|
||||||
|
internal static string Wizard_Step1_Title => Get(nameof(Wizard_Step1_Title));
|
||||||
|
internal static string Wizard_Step1_Subtitle => Get(nameof(Wizard_Step1_Subtitle));
|
||||||
|
internal static string Wizard_Step1_Footer_Hint => Get(nameof(Wizard_Step1_Footer_Hint));
|
||||||
|
internal static string Wizard_Step1_Skip_Label => Get(nameof(Wizard_Step1_Skip_Label));
|
||||||
|
internal static string Wizard_Step1_Skip_Tooltip => Get(nameof(Wizard_Step1_Skip_Tooltip));
|
||||||
|
internal static string Wizard_Step2_Title => Get(nameof(Wizard_Step2_Title));
|
||||||
|
internal static string Wizard_Step2_RecommendedFooter => Get(nameof(Wizard_Step2_RecommendedFooter));
|
||||||
|
internal static string Wizard_Profile_Roleplay_Heading => Get(nameof(Wizard_Profile_Roleplay_Heading));
|
||||||
|
internal static string Wizard_Profile_Roleplay_Description => Get(nameof(Wizard_Profile_Roleplay_Description));
|
||||||
|
internal static string Wizard_Profile_Roleplay_Apply => Get(nameof(Wizard_Profile_Roleplay_Apply));
|
||||||
|
internal static string Wizard_Nav_Back => Get(nameof(Wizard_Nav_Back));
|
||||||
|
internal static string Wizard_Nav_Next => Get(nameof(Wizard_Nav_Next));
|
||||||
|
internal static string Wizard_Nav_Finish => Get(nameof(Wizard_Nav_Finish));
|
||||||
|
internal static string Wizard_Step3_Title => Get(nameof(Wizard_Step3_Title));
|
||||||
|
internal static string Wizard_Step3_Section_History => Get(nameof(Wizard_Step3_Section_History));
|
||||||
|
internal static string Wizard_Step3_Section_TellTabs => Get(nameof(Wizard_Step3_Section_TellTabs));
|
||||||
|
internal static string Wizard_Step3_Section_Visual => Get(nameof(Wizard_Step3_Section_Visual));
|
||||||
|
internal static string Wizard_Step3_LoadPreviousSession_Label => Get(nameof(Wizard_Step3_LoadPreviousSession_Label));
|
||||||
|
internal static string Wizard_Step3_FilterIncludePreviousSessions_Label => Get(nameof(Wizard_Step3_FilterIncludePreviousSessions_Label));
|
||||||
|
internal static string Wizard_Step3_AutoTellTabsHistoryPreload_Label => Get(nameof(Wizard_Step3_AutoTellTabsHistoryPreload_Label));
|
||||||
|
internal static string Wizard_Step3_UseCompactDensity_Label => Get(nameof(Wizard_Step3_UseCompactDensity_Label));
|
||||||
|
internal static string Wizard_Step3_PrettierTimestamps_Label => Get(nameof(Wizard_Step3_PrettierTimestamps_Label));
|
||||||
|
internal static string Wizard_Step3_Theme_Label => Get(nameof(Wizard_Step3_Theme_Label));
|
||||||
|
internal static string Wizard_Step4_Title => Get(nameof(Wizard_Step4_Title));
|
||||||
|
internal static string Wizard_Step4_SummaryHeading => Get(nameof(Wizard_Step4_SummaryHeading));
|
||||||
|
internal static string Wizard_Step4_Summary_Profile => Get(nameof(Wizard_Step4_Summary_Profile));
|
||||||
|
internal static string Wizard_Step4_Summary_History => Get(nameof(Wizard_Step4_Summary_History));
|
||||||
|
internal static string Wizard_Step4_Summary_TellTabs => Get(nameof(Wizard_Step4_Summary_TellTabs));
|
||||||
|
internal static string Wizard_Step4_Summary_Visual => Get(nameof(Wizard_Step4_Summary_Visual));
|
||||||
|
internal static string Wizard_Step4_Summary_Unchanged => Get(nameof(Wizard_Step4_Summary_Unchanged));
|
||||||
|
internal static string Wizard_Step4_TestHint => Get(nameof(Wizard_Step4_TestHint));
|
||||||
|
internal static string Wizard_Step4_SettingsHint => Get(nameof(Wizard_Step4_SettingsHint));
|
||||||
|
|
||||||
internal static string Export_Heading => Get(nameof(Export_Heading));
|
internal static string Export_Heading => Get(nameof(Export_Heading));
|
||||||
internal static string Export_Help => Get(nameof(Export_Help));
|
internal static string Export_Help => Get(nameof(Export_Help));
|
||||||
@@ -170,6 +202,16 @@ internal class HellionStrings
|
|||||||
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
||||||
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
||||||
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
||||||
|
internal static string PinTab_MenuPin => Get(nameof(PinTab_MenuPin));
|
||||||
|
internal static string PinTab_MenuUnpin => Get(nameof(PinTab_MenuUnpin));
|
||||||
|
internal static string PinTab_MenuPromote => Get(nameof(PinTab_MenuPromote));
|
||||||
|
internal static string PinTab_PromoteTooltip => Get(nameof(PinTab_PromoteTooltip));
|
||||||
|
internal static string PinTab_LimitReached => Get(nameof(PinTab_LimitReached));
|
||||||
|
internal static string PinTab_PinnedTooltip => Get(nameof(PinTab_PinnedTooltip));
|
||||||
|
internal static string PinTab_PinTooltip => Get(nameof(PinTab_PinTooltip));
|
||||||
|
internal static string PinTab_SectionHeader => Get(nameof(PinTab_SectionHeader));
|
||||||
|
internal static string Settings_ThemeAndLayout_SidebarWidth_Name => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Name));
|
||||||
|
internal static string Settings_ThemeAndLayout_SidebarWidth_Description => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Description));
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
|
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
|
||||||
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
||||||
@@ -260,6 +302,10 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Chat_Preview_Heading => Get(nameof(Settings_Chat_Preview_Heading));
|
internal static string Settings_Chat_Preview_Heading => Get(nameof(Settings_Chat_Preview_Heading));
|
||||||
internal static string Settings_Chat_Emotes_Heading => Get(nameof(Settings_Chat_Emotes_Heading));
|
internal static string Settings_Chat_Emotes_Heading => Get(nameof(Settings_Chat_Emotes_Heading));
|
||||||
|
|
||||||
|
// Hellion Chat — Chat-Tab SymbolPicker
|
||||||
|
internal static string Settings_Chat_SymbolPicker_Enable_Name => Get(nameof(Settings_Chat_SymbolPicker_Enable_Name));
|
||||||
|
internal static string Settings_Chat_SymbolPicker_Enable_Description => Get(nameof(Settings_Chat_SymbolPicker_Enable_Description));
|
||||||
|
|
||||||
// Hellion Chat — Database-Tab section headings
|
// Hellion Chat — Database-Tab section headings
|
||||||
internal static string Settings_Database_Storage_Heading => Get(nameof(Settings_Database_Storage_Heading));
|
internal static string Settings_Database_Storage_Heading => Get(nameof(Settings_Database_Storage_Heading));
|
||||||
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
|
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
|
||||||
@@ -370,6 +416,8 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Integrations_Honorific_Status_Incompatible => Get(nameof(Settings_Integrations_Honorific_Status_Incompatible));
|
internal static string Settings_Integrations_Honorific_Status_Incompatible => Get(nameof(Settings_Integrations_Honorific_Status_Incompatible));
|
||||||
internal static string Settings_Integrations_Honorific_Toggle => Get(nameof(Settings_Integrations_Honorific_Toggle));
|
internal static string Settings_Integrations_Honorific_Toggle => Get(nameof(Settings_Integrations_Honorific_Toggle));
|
||||||
internal static string Settings_Integrations_Honorific_ToggleHint => Get(nameof(Settings_Integrations_Honorific_ToggleHint));
|
internal static string Settings_Integrations_Honorific_ToggleHint => Get(nameof(Settings_Integrations_Honorific_ToggleHint));
|
||||||
|
internal static string Settings_Integrations_Honorific_Glow_Toggle => Get(nameof(Settings_Integrations_Honorific_Glow_Toggle));
|
||||||
|
internal static string Settings_Integrations_Honorific_Glow_Hint => Get(nameof(Settings_Integrations_Honorific_Glow_Hint));
|
||||||
internal static string Settings_Integrations_Honorific_LinkRepo => Get(nameof(Settings_Integrations_Honorific_LinkRepo));
|
internal static string Settings_Integrations_Honorific_LinkRepo => Get(nameof(Settings_Integrations_Honorific_LinkRepo));
|
||||||
internal static string Settings_Integrations_Honorific_LinkAuthor => Get(nameof(Settings_Integrations_Honorific_LinkAuthor));
|
internal static string Settings_Integrations_Honorific_LinkAuthor => Get(nameof(Settings_Integrations_Honorific_LinkAuthor));
|
||||||
internal static string Settings_Integrations_ComingSoon_SectionHeader => Get(nameof(Settings_Integrations_ComingSoon_SectionHeader));
|
internal static string Settings_Integrations_ComingSoon_SectionHeader => Get(nameof(Settings_Integrations_ComingSoon_SectionHeader));
|
||||||
@@ -390,4 +438,9 @@ internal class HellionStrings
|
|||||||
|
|
||||||
// Hellion Chat — v1.3.0 Honorific title slot tooltip
|
// Hellion Chat — v1.3.0 Honorific title slot tooltip
|
||||||
internal static string ChatHeader_HonorificTitle_Tooltip => Get(nameof(ChatHeader_HonorificTitle_Tooltip));
|
internal static string ChatHeader_HonorificTitle_Tooltip => Get(nameof(ChatHeader_HonorificTitle_Tooltip));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.4.8 DbViewer full-text search toggle
|
||||||
|
internal static string DbViewer_FullTextToggle => Get(nameof(DbViewer_FullTextToggle));
|
||||||
|
internal static string DbViewer_FullTextToggle_Hint_Indexing => Get(nameof(DbViewer_FullTextToggle_Hint_Indexing));
|
||||||
|
internal static string DbViewer_FullTextToggle_Hint_PhraseMode => Get(nameof(DbViewer_FullTextToggle_Hint_PhraseMode));
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -228,6 +228,102 @@
|
|||||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||||
<value>Schließt den Wizard ohne Profil-Auswahl. Die Plugin-Defaults bleiben aktiv und der Wizard erscheint beim nächsten Plugin-Reload erneut.</value>
|
<value>Schließt den Wizard ohne Profil-Auswahl. Die Plugin-Defaults bleiben aktiv und der Wizard erscheint beim nächsten Plugin-Reload erneut.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step1_Title" xml:space="preserve">
|
||||||
|
<value>Willkommen bei Hellion Chat</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Subtitle" xml:space="preserve">
|
||||||
|
<value>Ein Chat 2 Fork von Hellion Forge mit DSGVO-konformen Defaults, brand-konsistentem Look und Quality-of-Life-Verbesserungen.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Footer_Hint" xml:space="preserve">
|
||||||
|
<value>3 kurze Schritte. Du kannst alles später unter Einstellungen → Hellion Chat ändern.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Skip_Label" xml:space="preserve">
|
||||||
|
<value>Später entscheiden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Skip_Tooltip" xml:space="preserve">
|
||||||
|
<value>Assistenten schließen. Die Plugin-Standardwerte bleiben aktiv. Du kannst den Assistenten über Einstellungen → Hellion Chat erneut öffnen.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step2_Title" xml:space="preserve">
|
||||||
|
<value>Was darf gespeichert werden?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step2_RecommendedFooter" xml:space="preserve">
|
||||||
|
<value>★ = empfohlen für die meisten Spieler.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Profile_Roleplay_Heading" xml:space="preserve">
|
||||||
|
<value>Roleplay</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Profile_Roleplay_Description" xml:space="preserve">
|
||||||
|
<value>Wie Datensparsamkeit, plus Sagen und beide Emote-Typen für deine Story-Logs. Schreien und Rufen bleiben außen vor — Public-Distance-Lärm von Fremden ist kein Story-Inhalt. Aufbewahrung: Sagen 30 Tage, Emotes 90 Tage.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Profile_Roleplay_Apply" xml:space="preserve">
|
||||||
|
<value>Roleplay übernehmen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Nav_Back" xml:space="preserve">
|
||||||
|
<value>‹ Zurück</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Nav_Next" xml:space="preserve">
|
||||||
|
<value>Weiter ›</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Nav_Finish" xml:space="preserve">
|
||||||
|
<value>Fertig ✓</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Title" xml:space="preserve">
|
||||||
|
<value>Versteckte Defaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Section_History" xml:space="preserve">
|
||||||
|
<value>Verlauf</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Section_TellTabs" xml:space="preserve">
|
||||||
|
<value>Tell-Tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Section_Visual" xml:space="preserve">
|
||||||
|
<value>Optik</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_LoadPreviousSession_Label" xml:space="preserve">
|
||||||
|
<value>Vorherige Session beim Start laden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_FilterIncludePreviousSessions_Label" xml:space="preserve">
|
||||||
|
<value>Filter auch auf alte Messages anwenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_AutoTellTabsHistoryPreload_Label" xml:space="preserve">
|
||||||
|
<value>N Tell-Messages beim Öffnen eines Auto-Tabs vorladen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_UseCompactDensity_Label" xml:space="preserve">
|
||||||
|
<value>Kompakter Density-Modus</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_PrettierTimestamps_Label" xml:space="preserve">
|
||||||
|
<value>Schönere Timestamps (relative Zeit)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Theme_Label" xml:space="preserve">
|
||||||
|
<value>Theme</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Title" xml:space="preserve">
|
||||||
|
<value>Du bist startklar</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_SummaryHeading" xml:space="preserve">
|
||||||
|
<value>Deine Konfiguration</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Profile" xml:space="preserve">
|
||||||
|
<value>Profil: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_History" xml:space="preserve">
|
||||||
|
<value>Verlauf: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_TellTabs" xml:space="preserve">
|
||||||
|
<value>Tell-Tabs: {0} Messages vorladen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Visual" xml:space="preserve">
|
||||||
|
<value>Optik: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Unchanged" xml:space="preserve">
|
||||||
|
<value>(unverändert)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_TestHint" xml:space="preserve">
|
||||||
|
<value>💡 Probier's aus: Tipp /tell <Spielername> in den Chat. Hellion Chat öffnet automatisch einen eigenen Tab für die Unterhaltung und lädt die letzten {0} Messages mit.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_SettingsHint" xml:space="preserve">
|
||||||
|
<value>Einstellungen → Hellion Chat zum späteren Anpassen</value>
|
||||||
|
</data>
|
||||||
<data name="Export_Heading" xml:space="preserve">
|
<data name="Export_Heading" xml:space="preserve">
|
||||||
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
|
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -383,6 +479,36 @@
|
|||||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||||
<value>Als begrüßt markieren.</value>
|
<value>Als begrüßt markieren.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||||
|
<value>Tab anpinnen</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||||
|
<value>Tab lösen</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||||
|
<value>In Standard-Tab umwandeln</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||||
|
<value>Wandelt den TempTell in einen regulären Tab um. Die Tell-Bindung an die Person geht verloren, der Tab fängt dann Nachrichten anhand der Channel-Filter ein. Für „Tab überlebt Relog" stattdessen „Tab anpinnen" wählen.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||||
|
<value>Maximal {0} angepinnte Tell-Tabs erreicht. Erst einen lösen oder dauerhaft behalten.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||||
|
<value>Angepinnt — überlebt Relog.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinTooltip" xml:space="preserve">
|
||||||
|
<value>Angepinnte Tabs überleben Relog und behalten die Bindung an die Tell-Person.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_SectionHeader" xml:space="preserve">
|
||||||
|
<value>Angepinnt</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
|
||||||
|
<value>Sidebar-Breite</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
|
||||||
|
<value>Breite der Tab-Sidebar in Pixeln. Default (44 px) ist Icon-only; breiter machen damit Sektion-Header wie „Aktive Tells (3)" nicht abgeschnitten werden.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
|
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
|
||||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||||
@@ -398,7 +524,7 @@
|
|||||||
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||||
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell.</value>
|
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell. Diese Grenze gilt nur für den automatisch verwalteten Pool. Angepinnte Tell-Tabs (Rechtsklick → Tab anpinnen) leben in einem separaten Pool von bis zu 5 Tabs und überleben Relog.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||||
<value>Kompakte Anzeige</value>
|
<value>Kompakte Anzeige</value>
|
||||||
@@ -526,6 +652,14 @@
|
|||||||
<value>Emotes</value>
|
<value>Emotes</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
|
<!-- Hellion Chat — Chat-Tab SymbolPicker -->
|
||||||
|
<data name="Settings_Chat_SymbolPicker_Enable_Name" xml:space="preserve">
|
||||||
|
<value>Symbol-Picker-Button neben dem Chat-Eingang anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Chat_SymbolPicker_Enable_Description" xml:space="preserve">
|
||||||
|
<value>Fügt einen kleinen Button links neben dem Kanal-Indikator ein. Klick öffnet ein Popup mit FFXIV-Glyphen und einer kuratierten Symbol-Liste. Ausschalten für eine schlankere Eingabezeile.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
<!-- Hellion Chat — Sektions-Überschriften des Database-Tabs -->
|
<!-- Hellion Chat — Sektions-Überschriften des Database-Tabs -->
|
||||||
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
||||||
<value>Speicherung</value>
|
<value>Speicherung</value>
|
||||||
@@ -827,6 +961,12 @@
|
|||||||
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
||||||
<value>Zeigt deinen Custom-Titel aus Honorific im Header über dem Chat-Log an, in der von dir gewählten Farbe.</value>
|
<value>Zeigt deinen Custom-Titel aus Honorific im Header über dem Chat-Log an, in der von dir gewählten Farbe.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Toggle" xml:space="preserve">
|
||||||
|
<value>Glow-Outline rendern (Honorific)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Hint" xml:space="preserve">
|
||||||
|
<value>Kann die Framerate auf schwacher Hardware drücken. Rendert die Glow-Outline für Honorific-Titel, die sie nutzen. Gradient-Animation wird noch nicht unterstützt und wird stattdessen als Primärfarbe gezeichnet.</value>
|
||||||
|
</data>
|
||||||
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
||||||
<value>Honorific auf GitHub</value>
|
<value>Honorific auf GitHub</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -881,4 +1021,13 @@
|
|||||||
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
||||||
<value>Custom-Titel von Honorific</value>
|
<value>Custom-Titel von Honorific</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle" xml:space="preserve">
|
||||||
|
<value>Volltext-Suche</value>
|
||||||
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle_Hint_Indexing" xml:space="preserve">
|
||||||
|
<value>Der Volltext-Index wird noch gebaut. Die lokale Suche bleibt verfügbar.</value>
|
||||||
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle_Hint_PhraseMode" xml:space="preserve">
|
||||||
|
<value>Sucht nach der exakten Wortfolge. Mehrere Wörter werden nur gefunden, wenn sie zusammen und in dieser Reihenfolge stehen. Wer rohe FTS5-MATCH-Syntax nutzen will, setzt eigene Anführungszeichen um den Suchbegriff.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -228,6 +228,102 @@
|
|||||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||||
<value>Close the wizard without selecting a profile. The plugin defaults stay active and the wizard returns on next plugin load.</value>
|
<value>Close the wizard without selecting a profile. The plugin defaults stay active and the wizard returns on next plugin load.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Step1_Title" xml:space="preserve">
|
||||||
|
<value>Welcome to Hellion Chat</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Subtitle" xml:space="preserve">
|
||||||
|
<value>A Chat 2 fork from Hellion Forge with privacy-aware defaults, brand-consistent visuals, and a few quality-of-life touches.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Footer_Hint" xml:space="preserve">
|
||||||
|
<value>Three short steps. You can change everything later under Settings → Hellion Chat.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Skip_Label" xml:space="preserve">
|
||||||
|
<value>Decide later</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step1_Skip_Tooltip" xml:space="preserve">
|
||||||
|
<value>Close the wizard. The plugin defaults stay active. You can reopen the wizard from Settings → Hellion Chat.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step2_Title" xml:space="preserve">
|
||||||
|
<value>What gets stored?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step2_RecommendedFooter" xml:space="preserve">
|
||||||
|
<value>★ = recommended for most players.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Profile_Roleplay_Heading" xml:space="preserve">
|
||||||
|
<value>Roleplay</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Profile_Roleplay_Description" xml:space="preserve">
|
||||||
|
<value>Like Privacy First, plus Say and both emote types for your story logs. Shout and Yell stay out — public-distance noise from strangers is not story content. Retention: Say 30 days, emotes 90 days.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Profile_Roleplay_Apply" xml:space="preserve">
|
||||||
|
<value>Apply roleplay</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Nav_Back" xml:space="preserve">
|
||||||
|
<value>‹ Back</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Nav_Next" xml:space="preserve">
|
||||||
|
<value>Next ›</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Nav_Finish" xml:space="preserve">
|
||||||
|
<value>Finish ✓</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Title" xml:space="preserve">
|
||||||
|
<value>Hidden defaults</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Section_History" xml:space="preserve">
|
||||||
|
<value>History</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Section_TellTabs" xml:space="preserve">
|
||||||
|
<value>Tell tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Section_Visual" xml:space="preserve">
|
||||||
|
<value>Visual</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_LoadPreviousSession_Label" xml:space="preserve">
|
||||||
|
<value>Load previous session on startup</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_FilterIncludePreviousSessions_Label" xml:space="preserve">
|
||||||
|
<value>Apply filters to messages from previous sessions</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_AutoTellTabsHistoryPreload_Label" xml:space="preserve">
|
||||||
|
<value>Preload N tell messages when an auto-tab opens</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_UseCompactDensity_Label" xml:space="preserve">
|
||||||
|
<value>Compact density</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_PrettierTimestamps_Label" xml:space="preserve">
|
||||||
|
<value>Prettier timestamps (relative time)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step3_Theme_Label" xml:space="preserve">
|
||||||
|
<value>Theme</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Title" xml:space="preserve">
|
||||||
|
<value>You're all set</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_SummaryHeading" xml:space="preserve">
|
||||||
|
<value>Your configuration</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Profile" xml:space="preserve">
|
||||||
|
<value>Profile: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_History" xml:space="preserve">
|
||||||
|
<value>History: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_TellTabs" xml:space="preserve">
|
||||||
|
<value>Tell tabs: preload {0} messages</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Visual" xml:space="preserve">
|
||||||
|
<value>Visual: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_Summary_Unchanged" xml:space="preserve">
|
||||||
|
<value>(unchanged)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_TestHint" xml:space="preserve">
|
||||||
|
<value>💡 Try it: type /tell <Player Name> into chat. Hellion Chat opens a dedicated tab for the conversation and preloads the last {0} messages.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Wizard_Step4_SettingsHint" xml:space="preserve">
|
||||||
|
<value>Settings → Hellion Chat to fine-tune later</value>
|
||||||
|
</data>
|
||||||
<data name="Export_Heading" xml:space="preserve">
|
<data name="Export_Heading" xml:space="preserve">
|
||||||
<value>Export (GDPR Art. 15 — Right of access)</value>
|
<value>Export (GDPR Art. 15 — Right of access)</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -383,6 +479,36 @@
|
|||||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||||
<value>Mark as greeted.</value>
|
<value>Mark as greeted.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||||
|
<value>Pin Tab</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||||
|
<value>Unpin Tab</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||||
|
<value>Promote to permanent</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||||
|
<value>Turns this TempTell into a regular tab. The tell binding to the partner is dropped — the tab will catch messages by its channel filters from now on. For "tab survives relog while staying bound to this partner", use Pin Tab instead.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinTooltip" xml:space="preserve">
|
||||||
|
<value>Pinned tabs survive relog and stay bound to this conversation partner.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_SectionHeader" xml:space="preserve">
|
||||||
|
<value>Pinned</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
|
||||||
|
<value>Sidebar width</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
|
||||||
|
<value>Width of the tab sidebar in pixels. The default (44 px) is icon-only; widen it to fit the section headers like "Active Tells (3)" without truncation.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||||
|
<value>Maximum of {0} pinned tell tabs reached. Unpin one first, or use Promote to permanent.</value>
|
||||||
|
</data>
|
||||||
|
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||||
|
<value>Pinned — survives relog.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
|
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
|
||||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||||
@@ -398,7 +524,7 @@
|
|||||||
<value>Maximum number of auto-tell tabs</value>
|
<value>Maximum number of auto-tell tabs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||||
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell.</value>
|
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell. This limit applies to the auto-managed pool. Pinned tell tabs (right-click → Pin Tab) live in a separate pool of up to 5 and survive relog.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||||
<value>Compact display</value>
|
<value>Compact display</value>
|
||||||
@@ -526,6 +652,14 @@
|
|||||||
<value>Emotes</value>
|
<value>Emotes</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
|
<!-- Hellion Chat — Chat tab SymbolPicker -->
|
||||||
|
<data name="Settings_Chat_SymbolPicker_Enable_Name" xml:space="preserve">
|
||||||
|
<value>Show symbol-picker button next to chat input</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Chat_SymbolPicker_Enable_Description" xml:space="preserve">
|
||||||
|
<value>Adds a small button left of the channel indicator that opens a popup with FFXIV icons and a curated symbol list. Disable if you prefer a leaner input bar.</value>
|
||||||
|
</data>
|
||||||
|
|
||||||
<!-- Hellion Chat — Database tab section headings -->
|
<!-- Hellion Chat — Database tab section headings -->
|
||||||
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
|
||||||
<value>Storage</value>
|
<value>Storage</value>
|
||||||
@@ -827,6 +961,12 @@
|
|||||||
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
||||||
<value>Shows your custom title from Honorific in the header above the chat log, in the colour you have chosen.</value>
|
<value>Shows your custom title from Honorific in the header above the chat log, in the colour you have chosen.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Toggle" xml:space="preserve">
|
||||||
|
<value>Render glow outlines (Honorific)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Glow_Hint" xml:space="preserve">
|
||||||
|
<value>May reduce frame rate on low-end hardware. Renders glow outlines for Honorific titles that use them. Gradient animation is not yet supported and will render as the primary colour.</value>
|
||||||
|
</data>
|
||||||
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
||||||
<value>Honorific on GitHub</value>
|
<value>Honorific on GitHub</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -881,4 +1021,13 @@
|
|||||||
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
||||||
<value>Custom title from Honorific</value>
|
<value>Custom title from Honorific</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle" xml:space="preserve">
|
||||||
|
<value>Full-text search</value>
|
||||||
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle_Hint_Indexing" xml:space="preserve">
|
||||||
|
<value>The full-text index is still being built. The local filter remains available.</value>
|
||||||
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle_Hint_PhraseMode" xml:space="preserve">
|
||||||
|
<value>Searches for the exact phrase. Multi-word queries match only when the words appear together in order. To use raw FTS5 MATCH syntax, wrap your term in double quotes yourself.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Generated
+1497
File diff suppressed because it is too large
Load Diff
Generated
+1497
File diff suppressed because it is too large
Load Diff
Generated
+1497
File diff suppressed because it is too large
Load Diff
Generated
+1497
File diff suppressed because it is too large
Load Diff
Generated
+1497
File diff suppressed because it is too large
Load Diff
Generated
+1497
File diff suppressed because it is too large
Load Diff
+1497
File diff suppressed because it is too large
Load Diff
Generated
+1497
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user