Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 1d88cb4c42 | |||
| c5fe69f0d3 | |||
| b46d3ad0a8 | |||
| e33cf0dcb9 | |||
| 0d016aaa5d | |||
| 5b972238bb | |||
| 7ac1eb3fd4 | |||
| db48f27842 | |||
| f8b5c14509 | |||
| 28e4b30cd6 | |||
| 4510c1e404 | |||
| 6b44f549b4 | |||
| ae1436b103 | |||
| 2684c31f10 | |||
| bdd64cad07 | |||
| 28ea2fa553 | |||
| dd597fca44 | |||
| b9d3ff8f26 | |||
| df3d5d78d6 | |||
| 2e057ce6c4 | |||
| e5dbc333fa | |||
| d0ec94c3e6 | |||
| cafb6faa39 | |||
| b8d289a847 | |||
| f16d8f5c78 | |||
| eabb39ba86 | |||
| b489ac946c | |||
| 8d9151c74a | |||
| 4ecbaf2a4b | |||
| 3e4601a0c8 |
+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
|
||||||
+47
-16
@@ -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
|
||||||
#
|
#
|
||||||
# XIVLauncher Core (Linux): ~/.xlcore/dalamud/Hooks/dev
|
# Wird zum Bauen des HellionChat.Tests-Projekts benötigt.
|
||||||
# XIVLauncher (Windows): %AppData%\XIVLauncher\addon\Hooks\dev
|
#
|
||||||
|
# Standardpfade je nach Plattform:
|
||||||
|
# XIVLauncher Core (Linux): ~/.xlcore/dalamud/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
|
||||||
|
|||||||
+177
-7
@@ -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
|
||||||
*.cs text eol=lf
|
|
||||||
*.yml text eol=lf
|
|
||||||
*.yaml text eol=lf
|
# =====================================================
|
||||||
*.md text eol=lf
|
# Source Code (LF)
|
||||||
*.json 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
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.xml text eol=lf
|
||||||
|
*.md 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
|
||||||
+27
-27
@@ -11,43 +11,43 @@ name: Build
|
|||||||
# Dalamud SDK 15 uses on Linux).
|
# Dalamud SDK 15 uses on Linux).
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# Minimum permissions for a build-only workflow: read the repo, nothing
|
# Minimum permissions for a build-only workflow: read the repo, nothing
|
||||||
# else. Closes the CodeQL "Workflow does not contain permissions" alert
|
# else. Closes the CodeQL "Workflow does not contain permissions" alert
|
||||||
# and matches the principle-of-least-privilege the security guide
|
# and matches the principle-of-least-privilege the security guide
|
||||||
# recommends for workflows that don't push or create releases.
|
# recommends for workflows that don't push or create releases.
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build (Release)
|
name: Build (Release)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: 10.0.x
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
- name: Download Dalamud staging
|
- name: Download Dalamud staging
|
||||||
run: |
|
run: |
|
||||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||||
mkdir -p "$hooks"
|
mkdir -p "$hooks"
|
||||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||||
unzip -oq dalamud.zip -d "$hooks"
|
unzip -oq dalamud.zip -d "$hooks"
|
||||||
|
|
||||||
- name: Restore
|
- name: Restore
|
||||||
run: dotnet restore HellionChat/HellionChat.csproj
|
run: dotnet restore HellionChat/HellionChat.csproj
|
||||||
|
|
||||||
- name: Build (Release)
|
- name: Build (Release)
|
||||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||||
|
|||||||
+215
-190
@@ -17,209 +17,234 @@ name: Forge Announce
|
|||||||
# (issue titles, PR bodies, commit messages, etc.) flows into run-steps.
|
# (issue titles, PR bodies, commit messages, etc.) flows into run-steps.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
announce:
|
announce:
|
||||||
name: Post changelog to Hellion Forge
|
name: Post changelog to Hellion Forge
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret
|
# The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret
|
||||||
# on Gitea (Settings → Actions → Secrets). Repo-level secrets are in
|
# on Gitea (Settings → Actions → Secrets). Repo-level secrets are in
|
||||||
# scope for every job by default, no environment: declaration needed.
|
# scope for every job by default, no environment: declaration needed.
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# On push:tags github.ref points at the tag commit; on workflow_dispatch
|
# On push:tags github.ref points at the tag commit; on workflow_dispatch
|
||||||
# the user supplies the tag explicitly. Always check out that tag so
|
# the user supplies the tag explicitly. Always check out that tag so
|
||||||
# the yaml + forge-posts file are read from the tagged tree, not main.
|
# the yaml + forge-posts file are read from the tagged tree, not main.
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||||
|
|
||||||
# Build embed-payload as a JSON file on disk. PowerShell-Core (pwsh)
|
# Build embed-payload as a JSON file on disk. PowerShell-Core (pwsh)
|
||||||
# ships pre-installed on ubuntu-latest so we get the same scripting
|
# ships pre-installed on ubuntu-latest so we get the same scripting
|
||||||
# patterns release.yml uses on windows-latest. Tag is read via env: to
|
# patterns release.yml uses on windows-latest. Tag is read via env: to
|
||||||
# treat it as a string variable rather than inline shell text, and
|
# treat it as a string variable rather than inline shell text, and
|
||||||
# validated against the semver regex before any interpolation.
|
# validated against the semver regex before any interpolation.
|
||||||
- name: Build embed payload
|
- name: Build embed payload
|
||||||
id: build
|
id: build
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
$tag = $env:TAG_NAME
|
$tag = $env:TAG_NAME
|
||||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
throw "V1: Refusing to announce non-semver tag: $tag"
|
throw "V1: Refusing to announce non-semver tag: $tag"
|
||||||
|
}
|
||||||
|
$version = $tag.Substring(1)
|
||||||
|
|
||||||
|
# ---------- Forge-Post-Datei lesen ----------
|
||||||
|
$forgePath = ".github/forge-posts/$tag.md"
|
||||||
|
if (-not (Test-Path $forgePath)) {
|
||||||
|
throw "V2: Forge-Post-Datei für $tag fehlt unter .github/forge-posts/. Datei vor dem Tag anlegen, dann Tag re-pushen oder workflow_dispatch."
|
||||||
|
}
|
||||||
|
$forgeRaw = Get-Content -Path $forgePath -Raw
|
||||||
|
|
||||||
|
# Frontmatter (--- … ---) am Datei-Anfang
|
||||||
|
if ($forgeRaw -notmatch '(?s)\A---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)\z') {
|
||||||
|
throw "V3: Frontmatter (---) fehlt oder ist defekt in $forgePath"
|
||||||
|
}
|
||||||
|
$fmText = $matches[1]
|
||||||
|
$deBody = $matches[2].Trim()
|
||||||
|
|
||||||
|
$subtitle = $null
|
||||||
|
$versionsnatur = $null
|
||||||
|
foreach ($line in ($fmText -split "`r?`n")) {
|
||||||
|
if ($line -match '^subtitle:\s*"?([^"]*)"?\s*$') { $subtitle = $matches[1] }
|
||||||
|
if ($line -match '^versionsnatur:\s*"?([^"]*)"?\s*$') { $versionsnatur = $matches[1] }
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subtitle)) { throw "V3: Frontmatter-Feld 'subtitle' fehlt in $forgePath" }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($versionsnatur)) { throw "V3: Frontmatter-Feld 'versionsnatur' fehlt in $forgePath" }
|
||||||
|
if ($subtitle.Length -gt 60) { throw "V4: Frontmatter-Feld 'subtitle' überschreitet Limit ($($subtitle.Length) Char, max 60)" }
|
||||||
|
if ($versionsnatur.Length -gt 40) { throw "V4: Frontmatter-Feld 'versionsnatur' überschreitet Limit ($($versionsnatur.Length) Char, max 40)" }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($deBody)) { throw "V3: DE-Body fehlt in $forgePath" }
|
||||||
|
|
||||||
|
# ---------- EN-Block aus HellionChat.yaml ziehen ----------
|
||||||
|
# 1:1 Pattern aus release.yml — gleicher Header-Marker, gleiches
|
||||||
|
# Trailer-Verhalten. Bei Drift die zwei Workflows synchron halten.
|
||||||
|
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||||
|
$raw = Get-Content -Path $yamlPath -Raw
|
||||||
|
$marker = "changelog: |-"
|
||||||
|
$idx = $raw.IndexOf($marker)
|
||||||
|
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
||||||
|
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||||
|
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||||
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
|
}) -join "`n"
|
||||||
|
|
||||||
|
$header = "**v$version "
|
||||||
|
$start = $changelogBody.IndexOf($header)
|
||||||
|
if ($start -lt 0) {
|
||||||
|
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
||||||
|
}
|
||||||
|
$rest = $changelogBody.Substring($start)
|
||||||
|
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||||
|
$trailer = $rest.IndexOf("`n`n---")
|
||||||
|
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||||
|
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||||
|
} elseif ($trailer -ge 0) {
|
||||||
|
$enBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||||
|
} else {
|
||||||
|
$enBlock = $rest.TrimEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 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"
|
||||||
|
$deDesc = "**Deutsch**`n`n$deBody"
|
||||||
|
$enDesc = "**English**`n`n$enBlock"
|
||||||
|
$footerText = "Hellion Forge · $versionsnatur"
|
||||||
|
$releaseUrl = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||||
|
|
||||||
|
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]@{
|
||||||
|
username = "Forge Herald"
|
||||||
|
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||||
|
content = "<@&1500489631555260446>"
|
||||||
|
allowed_mentions = [ordered]@{
|
||||||
|
parse = @()
|
||||||
|
roles = @("1500489631555260446")
|
||||||
|
}
|
||||||
|
embeds = @(
|
||||||
|
[ordered]@{
|
||||||
|
title = $title
|
||||||
|
url = $releaseUrl
|
||||||
|
color = 12730636
|
||||||
|
description = $deDesc
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
url = $releaseUrl
|
||||||
|
color = 12730636
|
||||||
|
description = $enDesc
|
||||||
|
footer = [ordered]@{ text = $footerText }
|
||||||
|
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||||
}
|
}
|
||||||
$version = $tag.Substring(1)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
# ---------- Forge-Post-Datei lesen ----------
|
$payloadJson = $payload | ConvertTo-Json -Depth 8 -Compress
|
||||||
$forgePath = ".github/forge-posts/$tag.md"
|
# Ausgabe-Datei ohne trailing newline für sauberes curl --data-binary @-
|
||||||
if (-not (Test-Path $forgePath)) {
|
[System.IO.File]::WriteAllText("$PWD/embed-payload.json", $payloadJson, [System.Text.UTF8Encoding]::new($false))
|
||||||
throw "V2: Forge-Post-Datei für $tag fehlt unter .github/forge-posts/. Datei vor dem Tag anlegen, dann Tag re-pushen oder workflow_dispatch."
|
|
||||||
}
|
|
||||||
$forgeRaw = Get-Content -Path $forgePath -Raw
|
|
||||||
|
|
||||||
# Frontmatter (--- … ---) am Datei-Anfang
|
Write-Host "Payload size: $($payloadJson.Length) chars"
|
||||||
if ($forgeRaw -notmatch '(?s)\A---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)\z') {
|
Write-Host "Embed title: $title"
|
||||||
throw "V3: Frontmatter (---) fehlt oder ist defekt in $forgePath"
|
Write-Host "Embed footer: $footerText"
|
||||||
}
|
|
||||||
$fmText = $matches[1]
|
|
||||||
$deBody = $matches[2].Trim()
|
|
||||||
|
|
||||||
$subtitle = $null
|
# POST to the Hellion Forge changelog webhook. curl from PowerShell-Core
|
||||||
$versionsnatur = $null
|
# so we can pipe the payload via stdin (--data-binary @-) and keep
|
||||||
foreach ($line in ($fmText -split "`r?`n")) {
|
# secrets out of process arg lists. One retry on 5xx, hard fail on 4xx.
|
||||||
if ($line -match '^subtitle:\s*"?([^"]*)"?\s*$') { $subtitle = $matches[1] }
|
- name: POST to Hellion Forge webhook
|
||||||
if ($line -match '^versionsnatur:\s*"?([^"]*)"?\s*$') { $versionsnatur = $matches[1] }
|
shell: pwsh
|
||||||
}
|
env:
|
||||||
if ([string]::IsNullOrWhiteSpace($subtitle)) { throw "V3: Frontmatter-Feld 'subtitle' fehlt in $forgePath" }
|
DISCORD_FORGE_WEBHOOK: ${{ secrets.DISCORD_FORGE_WEBHOOK }}
|
||||||
if ([string]::IsNullOrWhiteSpace($versionsnatur)) { throw "V3: Frontmatter-Feld 'versionsnatur' fehlt in $forgePath" }
|
run: |
|
||||||
if ($subtitle.Length -gt 60) { throw "V4: Frontmatter-Feld 'subtitle' überschreitet Limit ($($subtitle.Length) Char, max 60)" }
|
if ([string]::IsNullOrEmpty($env:DISCORD_FORGE_WEBHOOK)) {
|
||||||
if ($versionsnatur.Length -gt 40) { throw "V4: Frontmatter-Feld 'versionsnatur' überschreitet Limit ($($versionsnatur.Length) Char, max 40)" }
|
throw "V7: DISCORD_FORGE_WEBHOOK secret is empty. Check Settings → Environments → Webhook."
|
||||||
if ([string]::IsNullOrWhiteSpace($deBody)) { throw "V3: DE-Body fehlt in $forgePath" }
|
}
|
||||||
|
|
||||||
# ---------- EN-Block aus HellionChat.yaml ziehen ----------
|
$payloadFile = "$PWD/embed-payload.json"
|
||||||
# 1:1 Pattern aus release.yml — gleicher Header-Marker, gleiches
|
if (-not (Test-Path $payloadFile)) {
|
||||||
# Trailer-Verhalten. Bei Drift die zwei Workflows synchron halten.
|
throw "Embed payload file missing — previous step did not produce embed-payload.json"
|
||||||
$yamlPath = "HellionChat/HellionChat.yaml"
|
}
|
||||||
$raw = Get-Content -Path $yamlPath -Raw
|
|
||||||
$marker = "changelog: |-"
|
|
||||||
$idx = $raw.IndexOf($marker)
|
|
||||||
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
|
||||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
|
||||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
|
||||||
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
|
||||||
}) -join "`n"
|
|
||||||
|
|
||||||
$header = "**v$version "
|
$maxAttempts = 2
|
||||||
$start = $changelogBody.IndexOf($header)
|
$attempt = 0
|
||||||
if ($start -lt 0) {
|
while ($attempt -lt $maxAttempts) {
|
||||||
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
$attempt++
|
||||||
}
|
Write-Host "POST attempt $attempt of $maxAttempts"
|
||||||
$rest = $changelogBody.Substring($start)
|
$tmpResp = "$PWD/.webhook-response"
|
||||||
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
$tmpHeaders = "$PWD/.webhook-headers"
|
||||||
$trailer = $rest.IndexOf("`n`n---")
|
# --silent suppresses progress; --show-error prints errors so
|
||||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
# the workflow log shows what happened. -w prints HTTP status
|
||||||
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
# to stdout for inspection. -o captures body for diagnosis,
|
||||||
} elseif ($trailer -ge 0) {
|
# -D captures headers.
|
||||||
$enBlock = $rest.Substring(0, $trailer).TrimEnd()
|
$rawStatus = Get-Content $payloadFile -Raw |
|
||||||
} else {
|
curl --silent --show-error `
|
||||||
$enBlock = $rest.TrimEnd()
|
--header 'Content-Type: application/json' `
|
||||||
}
|
--data-binary '@-' `
|
||||||
|
-D $tmpHeaders `
|
||||||
|
-o $tmpResp `
|
||||||
|
-w '%{http_code}' `
|
||||||
|
"$env:DISCORD_FORGE_WEBHOOK"
|
||||||
|
$status = [int]$rawStatus
|
||||||
|
Write-Host "HTTP status: $status"
|
||||||
|
|
||||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
if ($status -ge 200 -and $status -lt 300) {
|
||||||
$title = "Hellion Chat $version — $subtitle"
|
Write-Host "Forge announce POST succeeded."
|
||||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
exit 0
|
||||||
$footerText = "Hellion Forge · $versionsnatur"
|
}
|
||||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
|
||||||
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 ----------
|
$bodySnippet = ""
|
||||||
$payload = [ordered]@{
|
if (Test-Path $tmpResp) {
|
||||||
username = "Forge Herald"
|
$bodySnippet = (Get-Content $tmpResp -Raw -ErrorAction SilentlyContinue)
|
||||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
if ($bodySnippet.Length -gt 500) { $bodySnippet = $bodySnippet.Substring(0, 500) + " …" }
|
||||||
content = "<@&1500489631555260446>"
|
}
|
||||||
allowed_mentions = [ordered]@{
|
|
||||||
parse = @()
|
|
||||||
roles = @("1500489631555260446")
|
|
||||||
}
|
|
||||||
embeds = @(
|
|
||||||
[ordered]@{
|
|
||||||
title = $title
|
|
||||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
|
||||||
color = 12730636
|
|
||||||
description = $description
|
|
||||||
footer = [ordered]@{ text = $footerText }
|
|
||||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
$payloadJson = $payload | ConvertTo-Json -Depth 8 -Compress
|
if ($status -ge 400 -and $status -lt 500) {
|
||||||
# Ausgabe-Datei ohne trailing newline für sauberes curl --data-binary @-
|
# E2: 4xx is permanent — webhook revoked, channel deleted,
|
||||||
[System.IO.File]::WriteAllText("$PWD/embed-payload.json", $payloadJson, [System.Text.UTF8Encoding]::new($false))
|
# payload malformed. No retry.
|
||||||
|
throw "E2: Discord-Webhook returned permanent $status. Body: $bodySnippet"
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "Payload size: $($payloadJson.Length) chars"
|
# E1: 5xx (or transport-level fail with status 0) — wait + retry once
|
||||||
Write-Host "Embed title: $title"
|
if ($attempt -lt $maxAttempts) {
|
||||||
Write-Host "Embed footer: $footerText"
|
Write-Host "Transient $status — sleeping 30s before retry."
|
||||||
|
Start-Sleep -Seconds 30
|
||||||
# POST to the Hellion Forge changelog webhook. curl from PowerShell-Core
|
} else {
|
||||||
# so we can pipe the payload via stdin (--data-binary @-) and keep
|
throw "E1: Discord-Webhook returned transient $status after $maxAttempts attempts. Body: $bodySnippet"
|
||||||
# secrets out of process arg lists. One retry on 5xx, hard fail on 4xx.
|
}
|
||||||
- name: POST to Hellion Forge webhook
|
}
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
DISCORD_FORGE_WEBHOOK: ${{ secrets.DISCORD_FORGE_WEBHOOK }}
|
|
||||||
run: |
|
|
||||||
if ([string]::IsNullOrEmpty($env:DISCORD_FORGE_WEBHOOK)) {
|
|
||||||
throw "V7: DISCORD_FORGE_WEBHOOK secret is empty. Check Settings → Environments → Webhook."
|
|
||||||
}
|
|
||||||
|
|
||||||
$payloadFile = "$PWD/embed-payload.json"
|
|
||||||
if (-not (Test-Path $payloadFile)) {
|
|
||||||
throw "Embed payload file missing — previous step did not produce embed-payload.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
$maxAttempts = 2
|
|
||||||
$attempt = 0
|
|
||||||
while ($attempt -lt $maxAttempts) {
|
|
||||||
$attempt++
|
|
||||||
Write-Host "POST attempt $attempt of $maxAttempts"
|
|
||||||
$tmpResp = "$PWD/.webhook-response"
|
|
||||||
$tmpHeaders = "$PWD/.webhook-headers"
|
|
||||||
# --silent suppresses progress; --show-error prints errors so
|
|
||||||
# the workflow log shows what happened. -w prints HTTP status
|
|
||||||
# to stdout for inspection. -o captures body for diagnosis,
|
|
||||||
# -D captures headers.
|
|
||||||
$rawStatus = Get-Content $payloadFile -Raw |
|
|
||||||
curl --silent --show-error `
|
|
||||||
--header 'Content-Type: application/json' `
|
|
||||||
--data-binary '@-' `
|
|
||||||
-D $tmpHeaders `
|
|
||||||
-o $tmpResp `
|
|
||||||
-w '%{http_code}' `
|
|
||||||
"$env:DISCORD_FORGE_WEBHOOK"
|
|
||||||
$status = [int]$rawStatus
|
|
||||||
Write-Host "HTTP status: $status"
|
|
||||||
|
|
||||||
if ($status -ge 200 -and $status -lt 300) {
|
|
||||||
Write-Host "Forge announce POST succeeded."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
$bodySnippet = ""
|
|
||||||
if (Test-Path $tmpResp) {
|
|
||||||
$bodySnippet = (Get-Content $tmpResp -Raw -ErrorAction SilentlyContinue)
|
|
||||||
if ($bodySnippet.Length -gt 500) { $bodySnippet = $bodySnippet.Substring(0, 500) + " …" }
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status -ge 400 -and $status -lt 500) {
|
|
||||||
# E2: 4xx is permanent — webhook revoked, channel deleted,
|
|
||||||
# payload malformed. No retry.
|
|
||||||
throw "E2: Discord-Webhook returned permanent $status. Body: $bodySnippet"
|
|
||||||
}
|
|
||||||
|
|
||||||
# E1: 5xx (or transport-level fail with status 0) — wait + retry once
|
|
||||||
if ($attempt -lt $maxAttempts) {
|
|
||||||
Write-Host "Transient $status — sleeping 30s before retry."
|
|
||||||
Start-Sleep -Seconds 30
|
|
||||||
} else {
|
|
||||||
throw "E1: Discord-Webhook returned transient $status after $maxAttempts attempts. Body: $bodySnippet"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+142
-142
@@ -17,167 +17,167 @@ name: Release
|
|||||||
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...
|
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...
|
||||||
|
|
||||||
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
|
||||||
# release-action reads GITHUB_REF directly and rejects anything that
|
# release-action reads GITHUB_REF directly and rejects anything that
|
||||||
# does not start with refs/tags/.
|
# does not start with refs/tags/.
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Build and attach release ZIP
|
name: Build and attach release ZIP
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# release-action@main reads GITHUB_REF directly (its action.yml
|
# release-action@main reads GITHUB_REF directly (its action.yml
|
||||||
# does not declare a tag_name input). Validate up-front so manual
|
# does not declare a tag_name input). Validate up-front so manual
|
||||||
# dispatches from a branch ref fail loud here instead of burning
|
# dispatches from a branch ref fail loud here instead of burning
|
||||||
# a full build before the final step errors out with "ref X is
|
# a full build before the final step errors out with "ref X is
|
||||||
# not a tag".
|
# not a tag".
|
||||||
- name: Validate tag ref
|
- name: Validate tag ref
|
||||||
run: |
|
run: |
|
||||||
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
|
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
|
||||||
echo "::error::Release workflow must run on a v*.X.Y tag ref, got ${GITHUB_REF}"
|
echo "::error::Release workflow must run on a v*.X.Y tag ref, got ${GITHUB_REF}"
|
||||||
echo "::error::Push a tag, or pick the tag (not main) in the workflow_dispatch Ref dropdown."
|
echo "::error::Push a tag, or pick the tag (not main) in the workflow_dispatch Ref dropdown."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: 10.0.x
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
- name: Download Dalamud staging
|
- name: Download Dalamud staging
|
||||||
run: |
|
run: |
|
||||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||||
mkdir -p "$hooks"
|
mkdir -p "$hooks"
|
||||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||||
unzip -oq dalamud.zip -d "$hooks"
|
unzip -oq dalamud.zip -d "$hooks"
|
||||||
|
|
||||||
- name: Build (Release)
|
- name: Build (Release)
|
||||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
|
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
|
||||||
|
|
||||||
- name: Locate latest.zip
|
- name: Locate latest.zip
|
||||||
id: locate
|
id: locate
|
||||||
run: |
|
run: |
|
||||||
zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
|
zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
|
||||||
if [ -z "$zip" ]; then
|
if [ -z "$zip" ]; then
|
||||||
echo "latest.zip not found under HellionChat/bin/Release" >&2
|
echo "latest.zip not found under HellionChat/bin/Release" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Found: $zip"
|
echo "Found: $zip"
|
||||||
echo "path=$zip" >> "$GITHUB_OUTPUT"
|
echo "path=$zip" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Build a release body from the matching changelog block in
|
# Build a release body from the matching changelog block in
|
||||||
# HellionChat.yaml plus a static install / docs footer. Fails the
|
# HellionChat.yaml plus a static install / docs footer. Fails the
|
||||||
# workflow if no block exists for the tagged version, which is the
|
# workflow if no block exists for the tagged version, which is the
|
||||||
# automated counterpart to the "yaml + repo.json + release body
|
# automated counterpart to the "yaml + repo.json + release body
|
||||||
# kept in sync" rule.
|
# kept in sync" rule.
|
||||||
#
|
#
|
||||||
# GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
|
# GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
|
||||||
# tag value is treated as a PowerShell variable, not as inline shell
|
# tag value is treated as a PowerShell variable, not as inline shell
|
||||||
# text. The strict regex below rejects anything that is not a clean
|
# text. The strict regex below rejects anything that is not a clean
|
||||||
# semver tag before it is used to build a string.
|
# semver tag before it is used to build a string.
|
||||||
- name: Generate release body
|
- name: Generate release body
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
# github.ref_name is the tag because Validate tag ref above
|
# github.ref_name is the tag because Validate tag ref above
|
||||||
# already enforced refs/tags/v*. Read via env: so the value
|
# already enforced refs/tags/v*. Read via env: so the value
|
||||||
# is a PowerShell variable, not inline shell text, and gets
|
# is a PowerShell variable, not inline shell text, and gets
|
||||||
# re-validated against the semver regex below.
|
# re-validated against the semver regex below.
|
||||||
TAG_NAME: ${{ github.ref_name }}
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
$tag = $env:TAG_NAME
|
$tag = $env:TAG_NAME
|
||||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
throw "Refusing to generate release body for non-semver tag: $tag"
|
throw "Refusing to generate release body for non-semver tag: $tag"
|
||||||
}
|
}
|
||||||
$version = $tag.Substring(1)
|
$version = $tag.Substring(1)
|
||||||
|
|
||||||
$yamlPath = "HellionChat/HellionChat.yaml"
|
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||||
$raw = Get-Content -Path $yamlPath -Raw
|
$raw = Get-Content -Path $yamlPath -Raw
|
||||||
|
|
||||||
$marker = "changelog: |-"
|
$marker = "changelog: |-"
|
||||||
$idx = $raw.IndexOf($marker)
|
$idx = $raw.IndexOf($marker)
|
||||||
if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }
|
if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }
|
||||||
|
|
||||||
# changelog: is the last top-level key in the manifest, so
|
# changelog: is the last top-level key in the manifest, so
|
||||||
# everything after the marker is the literal block. Strip the
|
# everything after the marker is the literal block. Strip the
|
||||||
# 4-space yaml indent (prettier convention) from each line.
|
# 4-space yaml indent (prettier convention) from each line.
|
||||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||||
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
}) -join "`n"
|
}) -join "`n"
|
||||||
|
|
||||||
# Subblock convention: "**vX.Y.Z — <subtitle> (<date>)**"
|
# Subblock convention: "**vX.Y.Z — <subtitle> (<date>)**"
|
||||||
# matches verify-changelog-sync.sh and slim-rule grep.
|
# matches verify-changelog-sync.sh and slim-rule grep.
|
||||||
$header = "**v$version "
|
$header = "**v$version "
|
||||||
$start = $changelogBody.IndexOf($header)
|
$start = $changelogBody.IndexOf($header)
|
||||||
if ($start -lt 0) {
|
if ($start -lt 0) {
|
||||||
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
||||||
}
|
}
|
||||||
|
|
||||||
$rest = $changelogBody.Substring($start)
|
$rest = $changelogBody.Substring($start)
|
||||||
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||||
$trailer = $rest.IndexOf("`n`n---")
|
$trailer = $rest.IndexOf("`n`n---")
|
||||||
|
|
||||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||||
$currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
$currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||||
} elseif ($trailer -ge 0) {
|
} elseif ($trailer -ge 0) {
|
||||||
$currentBlock = $rest.Substring(0, $trailer).TrimEnd()
|
$currentBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||||
} else {
|
} else {
|
||||||
$currentBlock = $rest.TrimEnd()
|
$currentBlock = $rest.TrimEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Static install / docs / licence footer is maintained as a
|
# Static install / docs / licence footer is maintained as a
|
||||||
# separate file so the workflow YAML stays clean (no embedded
|
# separate file so the workflow YAML stays clean (no embedded
|
||||||
# heredoc that would have to be indented under the run-block).
|
# heredoc that would have to be indented under the run-block).
|
||||||
$footerPath = ".github/release-footer.md"
|
$footerPath = ".github/release-footer.md"
|
||||||
if (-not (Test-Path $footerPath)) {
|
if (-not (Test-Path $footerPath)) {
|
||||||
throw "Release footer template not found: $footerPath"
|
throw "Release footer template not found: $footerPath"
|
||||||
}
|
}
|
||||||
$footer = Get-Content -Path $footerPath -Raw
|
$footer = Get-Content -Path $footerPath -Raw
|
||||||
|
|
||||||
$body = $currentBlock + "`n" + $footer
|
$body = $currentBlock + "`n" + $footer
|
||||||
$body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline
|
$body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline
|
||||||
|
|
||||||
Write-Host "Generated release body for $tag :"
|
Write-Host "Generated release body for $tag :"
|
||||||
Write-Host "----------------------------------------"
|
Write-Host "----------------------------------------"
|
||||||
Write-Host $body
|
Write-Host $body
|
||||||
Write-Host "----------------------------------------"
|
Write-Host "----------------------------------------"
|
||||||
|
|
||||||
# release-action@main only declares files/title/body/pre_release/
|
# release-action@main only declares files/title/body/pre_release/
|
||||||
# draft/api_key/insecure as inputs (see its action.yml). It silently
|
# draft/api_key/insecure as inputs (see its action.yml). It silently
|
||||||
# ignores anything else, including body_path and tag_name. The tag
|
# ignores anything else, including body_path and tag_name. The tag
|
||||||
# itself comes from GITHUB_REF, the body must be passed inline via
|
# itself comes from GITHUB_REF, the body must be passed inline via
|
||||||
# body:, so we re-emit release-body.md as a step output first.
|
# body:, so we re-emit release-body.md as a step output first.
|
||||||
- name: Expose release body for release-action
|
- name: Expose release body for release-action
|
||||||
id: body
|
id: body
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
{
|
{
|
||||||
echo 'content<<RELEASE_BODY_EOF'
|
echo 'content<<RELEASE_BODY_EOF'
|
||||||
cat release-body.md
|
cat release-body.md
|
||||||
echo 'RELEASE_BODY_EOF'
|
echo 'RELEASE_BODY_EOF'
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Gitea-native release action. Creates the release if the tag has no
|
# Gitea-native release action. Creates the release if the tag has no
|
||||||
# release yet, or updates the existing one with latest.zip attached
|
# release yet, or updates the existing one with latest.zip attached
|
||||||
# and the generated body. The auto-injected GITHUB_TOKEN on Gitea
|
# and the generated body. The auto-injected GITHUB_TOKEN on Gitea
|
||||||
# Actions has Gitea-API scope and is sufficient for release write.
|
# Actions has Gitea-API scope and is sufficient for release write.
|
||||||
- name: Attach to Gitea release
|
- name: Attach to Gitea release
|
||||||
uses: https://gitea.com/actions/release-action@main
|
uses: https://gitea.com/actions/release-action@main
|
||||||
with:
|
with:
|
||||||
files: ${{ steps.locate.outputs.path }}
|
files: ${{ steps.locate.outputs.path }}
|
||||||
body: ${{ steps.body.outputs.content }}
|
body: ${{ steps.body.outputs.content }}
|
||||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
name: Security
|
name: Security
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
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:
|
||||||
scan:
|
scan:
|
||||||
uses: JonKazama-Hellion/security-workflows/.gitea/workflows/security-scan.yml@main
|
uses: JonKazama-Hellion/security-workflows/.gitea/workflows/security-scan.yml@main
|
||||||
with:
|
with:
|
||||||
# MessageStore.cs uses string-interpolation in CommandText for table
|
# MessageStore.cs uses string-interpolation in CommandText for table
|
||||||
# names and clause-joins that come from internal code constants, not
|
# names and clause-joins that come from internal code constants, not
|
||||||
# user input. Values are bound via SqlParameter, the SQL surface is
|
# user input. Values are bound via SqlParameter, the SQL surface is
|
||||||
# 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'
|
||||||
|
|||||||
@@ -1,73 +1,73 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Something in HellionChat is broken or behaves wrong
|
description: Something in HellionChat is broken or behaves wrong
|
||||||
labels:
|
labels:
|
||||||
- bug
|
- bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for reporting. Please fill in the fields below so I can
|
Thanks for reporting. Please fill in the fields below so I can
|
||||||
reproduce the issue. If this is a security issue, stop here and
|
reproduce the issue. If this is a security issue, stop here and
|
||||||
report it privately to [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D)
|
report it privately to [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D)
|
||||||
instead.
|
instead.
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
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
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
options:
|
||||||
|
- Windows (XIVLauncher)
|
||||||
|
- Linux (XIVLauncher Core)
|
||||||
|
- macOS (XIVLauncher Core / wine)
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened
|
||||||
|
description: Plain description, no log dumps yet
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: What you expected
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: How to reproduce
|
||||||
|
description: Step-by-step from "open settings" or "log in" through to the broken behaviour
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Relevant /xllog excerpt
|
||||||
|
description: Filter for "HellionChat" if the log is huge
|
||||||
|
render: text
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirm
|
||||||
|
attributes:
|
||||||
|
label: Pre-flight
|
||||||
|
options:
|
||||||
|
- label: I am running the latest version of HellionChat
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have searched existing issues for duplicates
|
||||||
- type: dropdown
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: Platform
|
|
||||||
options:
|
|
||||||
- Windows (XIVLauncher)
|
|
||||||
- Linux (XIVLauncher Core)
|
|
||||||
- macOS (XIVLauncher Core / wine)
|
|
||||||
- Other
|
|
||||||
validations:
|
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: what-happened
|
|
||||||
attributes:
|
|
||||||
label: What happened
|
|
||||||
description: Plain description, no log dumps yet
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: What you expected
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: steps
|
|
||||||
attributes:
|
|
||||||
label: How to reproduce
|
|
||||||
description: Step-by-step from "open settings" or "log in" through to the broken behaviour
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: log
|
|
||||||
attributes:
|
|
||||||
label: Relevant /xllog excerpt
|
|
||||||
description: Filter for "HellionChat" if the log is huge
|
|
||||||
render: text
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: confirm
|
|
||||||
attributes:
|
|
||||||
label: Pre-flight
|
|
||||||
options:
|
|
||||||
- label: I am running the latest version of HellionChat
|
|
||||||
required: true
|
|
||||||
- label: I have searched existing issues for duplicates
|
|
||||||
required: true
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Security vulnerability
|
- name: Security vulnerability
|
||||||
url: mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
url: mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||||
about: Do not open a public issue for security problems. Report by e-mail instead.
|
about: Do not open a public issue for security problems. Report by e-mail instead.
|
||||||
|
|
||||||
- name: Upstream Chat 2 issue
|
- name: Upstream Chat 2 issue
|
||||||
url: https://github.com/Infiziert90/ChatTwo/issues
|
url: https://github.com/Infiziert90/ChatTwo/issues
|
||||||
about:
|
about:
|
||||||
If the issue exists in upstream Chat 2 too, please report it there so the original maintainers see it as well.
|
If the issue exists in upstream Chat 2 too, please report it there so the original maintainers see it as well.
|
||||||
|
|
||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.com/users/j.j_kazama
|
url: https://discord.com/users/j.j_kazama
|
||||||
about: Quick questions, casual feedback. Bug reports still go through the issue tracker for tracking.
|
about: Quick questions, casual feedback. Bug reports still go through the issue tracker for tracking.
|
||||||
|
|||||||
@@ -1,57 +1,55 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest a feature or enhancement for HellionChat
|
description: Suggest a feature or enhancement for HellionChat
|
||||||
labels:
|
labels:
|
||||||
- enhancement
|
- enhancement
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for the suggestion. HellionChat focuses on privacy by
|
Thanks for the suggestion. HellionChat focuses on privacy by
|
||||||
default and a small, well-scoped feature set. Suggestions that
|
default and a small, well-scoped feature set. Suggestions that
|
||||||
align with that scope are easier to accept than ones that pull
|
align with that scope are easier to accept than ones that pull
|
||||||
the plugin toward "do everything".
|
the plugin toward "do everything".
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: What problem are you trying to solve
|
label: What problem are you trying to solve
|
||||||
description: The user-side problem, not the proposed solution yet
|
description: The user-side problem, not the proposed solution yet
|
||||||
validations:
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: What you would like HellionChat to do
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives you have considered
|
||||||
|
description: Other plugins, manual workarounds, settings combinations
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: scope
|
||||||
|
attributes:
|
||||||
|
label: Scope estimate from your side
|
||||||
|
options:
|
||||||
|
- 'Small (one tab, one toggle, one filter)'
|
||||||
|
- 'Medium (a settings section, persistent state, one new file)'
|
||||||
|
- 'Large (architectural, touches the message pipeline or the database)'
|
||||||
|
- "I don't know"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirm
|
||||||
|
attributes:
|
||||||
|
label: Pre-flight
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues for similar requests
|
||||||
required: true
|
required: true
|
||||||
|
- label: I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat 2
|
||||||
- type: textarea
|
|
||||||
id: solution
|
|
||||||
attributes:
|
|
||||||
label: What you would like HellionChat to do
|
|
||||||
validations:
|
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: Alternatives you have considered
|
|
||||||
description: Other plugins, manual workarounds, settings combinations
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: scope
|
|
||||||
attributes:
|
|
||||||
label: Scope estimate from your side
|
|
||||||
options:
|
|
||||||
- "Small (one tab, one toggle, one filter)"
|
|
||||||
- "Medium (a settings section, persistent state, one new file)"
|
|
||||||
- "Large (architectural, touches the message pipeline or the database)"
|
|
||||||
- "I don't know"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: confirm
|
|
||||||
attributes:
|
|
||||||
label: Pre-flight
|
|
||||||
options:
|
|
||||||
- label: I have searched existing issues for similar requests
|
|
||||||
required: true
|
|
||||||
- label:
|
|
||||||
I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat
|
|
||||||
2
|
|
||||||
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)).
|
||||||
|
|||||||
+38
-38
@@ -1,42 +1,42 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
# NuGet package updates for the plugin project. Weekly cadence keeps the
|
# NuGet package updates for the plugin project. Weekly cadence keeps the
|
||||||
# noise down while still catching transitive security advisories within
|
# noise down while still catching transitive security advisories within
|
||||||
# a few days of disclosure.
|
# a few days of disclosure.
|
||||||
- package-ecosystem: nuget
|
- package-ecosystem: nuget
|
||||||
directory: /HellionChat
|
directory: /HellionChat
|
||||||
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:
|
||||||
- patch
|
- patch
|
||||||
minor:
|
minor:
|
||||||
update-types:
|
update-types:
|
||||||
- minor
|
- minor
|
||||||
|
|
||||||
# GitHub Actions versions in .github/workflows. Lower cadence because
|
# GitHub Actions versions in .github/workflows. Lower cadence because
|
||||||
# Action releases ship less frequently and are usually safe to defer
|
# Action releases ship less frequently and are usually safe to defer
|
||||||
# for a month.
|
# for a month.
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
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,31 +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
|
für künftige Reviews als ein abstraktes Threading-Kapitel
|
||||||
Plugin-Reloads 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
|
kann den Service über Plugin-Reloads hinweg leben lassen, also läuft der Log jetzt auf Warning
|
||||||
(beide 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
|
`IsBackground=true` unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu
|
||||||
in der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der
|
MessageManager und RetentionSweep (beide seit v1.4.0)
|
||||||
Filter einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet,
|
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType
|
||||||
also kein 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.
|
Failsafe-Wert. Dedup über ein NonSerialized-HashSet, also kein Log-Spam
|
||||||
Bestehende Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein
|
- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen
|
||||||
Schema-Bump
|
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.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
subtitle: UX und Robustheit
|
||||||
|
versionsnatur: UX-Polish-Cycle
|
||||||
|
---
|
||||||
|
|
||||||
|
**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 First-Run-Wizard hat eine explizite Cancel-Schaltfläche, der Eingabe-Verlauf bleibt
|
||||||
|
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 eine einmalige Warning-Notification mit Verweis aufs `/xllog`, statt das
|
||||||
|
Fenster stillschweigend leer zu lassen. Der Stack-Trace selbst geht weiter via `Plugin.Log.Error`
|
||||||
|
ins Logfile. De-Dup über Per-Session-Bool, damit ein wiederkehrender Fehler die Notification-Stack
|
||||||
|
nicht pro Frame neu vollkippt
|
||||||
|
- **First-Run-Wizard trennt Accept und Close.** `OnClose` setzt nicht mehr stillschweigend
|
||||||
|
`FirstRunCompleted=true`, also lässt das X den Wizard schwebend zurück und er kommt beim nächsten
|
||||||
|
Plugin-Reload wieder. Eine neue „Später — Defaults behalten"-Schaltfläche im Footer ist der
|
||||||
|
explizite Weg, ohne Profil-Auswahl rauszukommen. Strings bilingual EN+DE plus Tooltip
|
||||||
|
- **Eingabe-Verlauf wird beim Plugin-Reload geleert.** `InputHistoryService.Reset` hängt jetzt in
|
||||||
|
`Plugin.DisposeAsync` neben den anderen Pure-Memory-Cleanups, damit der statische Zustand aus der
|
||||||
|
vorigen Session den nächsten Load nicht mehr erbt
|
||||||
|
- **Statusleiste klippt nicht mehr.** Der rechtsbündige Versions-Slot wird ausgeblendet wenn die
|
||||||
|
Chat-Window-Breite abzüglich Versions-Text unter 200 px fällt — vorher überlappte er die vier
|
||||||
|
linken Slots. Ab ausreichender Breite taucht der Slot wieder auf
|
||||||
|
- **Intern:** `FontManager` fällt auf System-Font zurück wenn die eingebettete Hellion-Font-Resource
|
||||||
|
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
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
subtitle: Code Hygiene and Refactor
|
||||||
|
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`.
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **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).
|
||||||
@@ -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.
|
||||||
|
|||||||
+456
-229
@@ -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,50 +459,29 @@ _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
|
||||||
~$*
|
# =====================================================
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.dbproj.schemaview
|
|
||||||
*.jfm
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
orleans.codegen.cs
|
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
# SQL Server
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
|
||||||
#*.snk
|
|
||||||
|
|
||||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
|
||||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
|
||||||
#bower_components/
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
|
||||||
# because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
ServiceFabricBackup/
|
|
||||||
*.rptproj.bak
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
*.mdf
|
*.mdf
|
||||||
*.ldf
|
*.ldf
|
||||||
*.ndf
|
*.ndf
|
||||||
|
|
||||||
# Business Intelligence projects
|
# Andere DB-bezogene
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] BeatPulse – wurde 2019 umbenannt zu AspNetCore.Diagnostics.HealthChecks
|
||||||
|
healthchecksdb
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Business Intelligence / Reporting
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
*.rdl.data
|
*.rdl.data
|
||||||
*.bim.layout
|
*.bim.layout
|
||||||
*.bim_*.settings
|
*.bim_*.settings
|
||||||
@@ -288,27 +489,97 @@ ServiceFabricBackup/
|
|||||||
*- [Bb]ackup.rdl
|
*- [Bb]ackup.rdl
|
||||||
*- [Bb]ackup ([0-9]).rdl
|
*- [Bb]ackup ([0-9]).rdl
|
||||||
*- [Bb]ackup ([0-9][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
|
# Microsoft Fakes
|
||||||
FakesAssemblies/
|
FakesAssemblies/
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
# [!! OBSOLET 2026 !!] GhostDoc Plugin – Submain hat das Tool eingestellt
|
||||||
*.GhostDoc.xml
|
*.GhostDoc.xml
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
# Tabs Studio
|
||||||
.ntvs_analysis.dat
|
*.tss
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# [!! 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
|
*.plg
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
# [!! OBSOLET 2026 !!] Visual Studio 6 Workspace Options
|
||||||
*.opt
|
*.opt
|
||||||
|
|
||||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
# [!! OBSOLET 2026 !!] Visual Studio 6 Workspace File
|
||||||
*.vbw
|
*.vbw
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
# [!! OBSOLET 2026 !!] RIA / Silverlight – Microsoft hat das Okt. 2021 eingestellt
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# [!! OBSOLET 2026 !!] Visual Studio LightSwitch – von Microsoft eingestellt
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
@@ -316,75 +587,31 @@ node_modules/
|
|||||||
**/*.Server/ModelManifest.xml
|
**/*.Server/ModelManifest.xml
|
||||||
_Pvt_Extensions
|
_Pvt_Extensions
|
||||||
|
|
||||||
# Paket dependency manager
|
|
||||||
.paket/paket.exe
|
|
||||||
paket-files/
|
|
||||||
|
|
||||||
# FAKE - F# Make
|
# =====================================================
|
||||||
.fake/
|
# Upgrade / Backup-Reports
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
# CodeRush personal settings
|
# Backup-Files vom Konvertieren alter VS-Projekte (wir haben ja git ;-))
|
||||||
.cr/personal
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Cake - Uncomment if you are using it
|
# =====================================================
|
||||||
# tools/**
|
# Misc / Temp / Backup
|
||||||
# !tools/packages.config
|
# =====================================================
|
||||||
|
|
||||||
# Tabs Studio
|
ClientBin/
|
||||||
*.tss
|
~$*
|
||||||
|
*~
|
||||||
|
*.publishsettings
|
||||||
|
|
||||||
# Telerik's JustMock configuration file
|
# Including strong name files can present a security risk
|
||||||
*.jmconfig
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
# BizTalk build output
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
*.btp.cs
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
*.btm.cs
|
#bower_components/
|
||||||
*.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
|
|
||||||
+12
-2
@@ -1,7 +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 },
|
||||||
"MD029": false,
|
"MD029": false,
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
"MD041": false
|
"MD036": false,
|
||||||
|
"MD040": true,
|
||||||
|
"MD041": false,
|
||||||
|
"MD046": { "style": "fenced" },
|
||||||
|
"MD048": { "style": "backtick" },
|
||||||
|
"MD049": { "style": "underscore" },
|
||||||
|
"MD050": { "style": "asterisk" }
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-1
@@ -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/
|
||||||
*.Designer.cs
|
dist/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# === Generierte C#-Files (Designer, Source Generators) ===
|
||||||
|
*.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-6
@@ -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:
|
||||||
line-length: disable
|
# Zeilenlängen-Check aus (konsistent mit markdownlint MD013)
|
||||||
document-start: disable
|
line-length: disable
|
||||||
truthy:
|
|
||||||
allowed-values: ["true", "false", "on"]
|
# YAML ohne führendes "---" erlaubt
|
||||||
empty-lines:
|
document-start: disable
|
||||||
max: 1
|
|
||||||
|
# GitHub Actions nutzt "on:" als Trigger-Key.
|
||||||
|
# Ohne diesen Override würde yamllint das als boolean "on" beklagen.
|
||||||
|
truthy:
|
||||||
|
allowed-values: ['true', 'false', 'on']
|
||||||
|
|
||||||
|
# Maximal 1 Leerzeile in Folge (saubere Files)
|
||||||
|
empty-lines:
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Branding;
|
namespace HellionChat.Branding;
|
||||||
|
|
||||||
// Centralised — a future invite/URL rotation only touches this file.
|
// Centralised — a future invite/URL rotation only touches this file.
|
||||||
@@ -9,4 +12,22 @@ internal static class BrandingLinks
|
|||||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
||||||
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
||||||
public const string HellionMediaWebsite = "https://hellion-media.de/de";
|
public const string HellionMediaWebsite = "https://hellion-media.de/de";
|
||||||
|
|
||||||
|
// CA2255 warns against [ModuleInitializer] in library code, but Dalamud
|
||||||
|
// loads the plugin DLL directly so the module-init pass is the right hook
|
||||||
|
// for a one-shot URL sanity check at plugin load.
|
||||||
|
#pragma warning disable CA2255
|
||||||
|
[ModuleInitializer]
|
||||||
|
#pragma warning restore CA2255
|
||||||
|
internal static void ValidateUrls()
|
||||||
|
{
|
||||||
|
UrlValidation.ValidateAll(
|
||||||
|
nameof(BrandingLinks),
|
||||||
|
HellionForgeDiscordInvite,
|
||||||
|
HellionForgeGitea,
|
||||||
|
HellionChatRepo,
|
||||||
|
HellionForgeWebsite,
|
||||||
|
HellionMediaWebsite
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -500,7 +536,7 @@ public class Tab
|
|||||||
Opacity = Opacity,
|
Opacity = Opacity,
|
||||||
Identifier = Identifier,
|
Identifier = Identifier,
|
||||||
InputDisabled = InputDisabled,
|
InputDisabled = InputDisabled,
|
||||||
CurrentChannel = CurrentChannel,
|
CurrentChannel = CurrentChannel.Clone(),
|
||||||
CanMove = CanMove,
|
CanMove = CanMove,
|
||||||
CanResize = CanResize,
|
CanResize = CanResize,
|
||||||
IndependentHide = IndependentHide,
|
IndependentHide = IndependentHide,
|
||||||
@@ -511,8 +547,9 @@ public class Tab
|
|||||||
HideInBattle = HideInBattle,
|
HideInBattle = HideInBattle,
|
||||||
HideWhenInactive = HideWhenInactive,
|
HideWhenInactive = HideWhenInactive,
|
||||||
IsTempTab = IsTempTab,
|
IsTempTab = IsTempTab,
|
||||||
|
IsPinned = IsPinned,
|
||||||
AllSenderMessages = AllSenderMessages,
|
AllSenderMessages = AllSenderMessages,
|
||||||
TellTarget = TellTarget.From(TellTarget),
|
TellTarget = TellTarget.Clone(),
|
||||||
IsGreeted = IsGreeted,
|
IsGreeted = IsGreeted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -690,6 +727,29 @@ public class UsedChannel
|
|||||||
{
|
{
|
||||||
Channel = channel;
|
Channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||||
|
// - Deep-clone the UsedChannel so Tab.Clone() no longer shares
|
||||||
|
// channel state (incl. TellTarget) with its origin Tab. Previously
|
||||||
|
// a reference copy: PopOut and Temp tabs mutated each other.
|
||||||
|
// - Name is intentionally a reference copy (matches upstream); it
|
||||||
|
// gets reassigned on every channel switch anyway.
|
||||||
|
// TEST-MIRROR: ../../Hellion Build test/_Helpers/UsedChannelCloneTests.cs
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
public UsedChannel Clone()
|
||||||
|
{
|
||||||
|
return new UsedChannel
|
||||||
|
{
|
||||||
|
Channel = Channel,
|
||||||
|
Name = Name,
|
||||||
|
TellTarget = TellTarget?.Clone(),
|
||||||
|
|
||||||
|
UseTempChannel = UseTempChannel,
|
||||||
|
TempChannel = TempChannel,
|
||||||
|
TempTellTarget = TempTellTarget?.Clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+174
-108
@@ -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,16 +65,144 @@ 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;
|
||||||
|
|
||||||
private static byte[] GetHellionFontBytes()
|
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
|
||||||
|
// dev build can land here. Caller falls back to the system font path
|
||||||
|
// so the plugin still loads instead of crashing the whole UiBuilder.
|
||||||
|
private static byte[]? TryGetHellionFontBytes()
|
||||||
{
|
{
|
||||||
if (HellionFontBytes is not null)
|
if (HellionFontBytes is not null)
|
||||||
return HellionFontBytes;
|
return HellionFontBytes;
|
||||||
|
|
||||||
using var stream =
|
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
|
||||||
typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
|
"HellionFont.ttf"
|
||||||
?? throw new FileNotFoundException(
|
);
|
||||||
"Hellion font resource not embedded in the assembly"
|
if (stream is null)
|
||||||
|
{
|
||||||
|
Plugin.LogProxy.Warning(
|
||||||
|
"Hellion font resource missing — falling back to system default font."
|
||||||
);
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
stream.CopyTo(ms);
|
stream.CopyTo(ms);
|
||||||
HellionFontBytes = ms.ToArray();
|
HellionFontBytes = ms.ToArray();
|
||||||
@@ -85,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("ĂăÂâÎîȘșȚț");
|
||||||
|
|
||||||
@@ -109,97 +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 };
|
|
||||||
config.MergeFont = Plugin.Config.UseHellionFont
|
|
||||||
? tk.AddFontFromMemory(GetHellionFontBytes(), 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,
|
||||||
@@ -213,11 +269,21 @@ public class FontManager
|
|||||||
return fontId.AddToBuildToolkit(tk, config);
|
return fontId.AddToBuildToolkit(tk, config);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
|
when (e
|
||||||
|
is FileNotFoundException
|
||||||
|
or DirectoryNotFoundException
|
||||||
|
or IOException
|
||||||
|
or InvalidOperationException
|
||||||
|
or ArgumentException
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
// Atlas-toolkit throws span IO and validation failures; routing
|
||||||
|
// the wider set through the fallback keeps a corrupt font config
|
||||||
|
// from taking down the whole atlas build.
|
||||||
|
Plugin.LogProxy.Warning(
|
||||||
e,
|
e,
|
||||||
$"Configured {slot} font unavailable, falling back to NotoSansCjkRegular"
|
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
||||||
|
+ "falling back to NotoSansCjkRegular"
|
||||||
);
|
);
|
||||||
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
|
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
|
||||||
return fallback.AddToBuildToolkit(tk, config);
|
return fallback.AddToBuildToolkit(tk, config);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,16 +427,24 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if channel is valid (non-linkshell or existing linkshell)
|
// ---------------------------------------------------------------
|
||||||
internal static bool ValidAnyLinkshell(InputChannel channel)
|
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||||
|
// - Renamed ValidAnyLinkshell -> IsChannelOrExistingLinkshell. The
|
||||||
|
// name now states intent: returns true for any non-linkshell
|
||||||
|
// channel, or a linkshell index that actually exists.
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
internal static bool IsChannelOrExistingLinkshell(InputChannel channel)
|
||||||
{
|
{
|
||||||
var idx = channel.LinkshellIndex();
|
var idx = channel.LinkshellIndex();
|
||||||
if (idx == uint.MaxValue || channel.IsExtraChatLinkshell())
|
if (idx == uint.MaxValue || channel.IsExtraChatLinkshell())
|
||||||
return true;
|
return true;
|
||||||
if (channel.IsLinkshell() && ValidLinkshell(idx))
|
|
||||||
return true;
|
if (channel.IsLinkshell())
|
||||||
if (channel.IsCrossLinkshell() && ValidCrossLinkshell(idx))
|
return ValidLinkshell(idx);
|
||||||
return true;
|
|
||||||
|
if (channel.IsCrossLinkshell())
|
||||||
|
return ValidCrossLinkshell(idx);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,12 +543,17 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
if (idx == uint.MaxValue)
|
if (idx == uint.MaxValue)
|
||||||
idx = 0;
|
idx = 0;
|
||||||
|
|
||||||
if (!ValidAnyLinkshell(channel))
|
// ---------------------------------------------------------------
|
||||||
return;
|
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||||
|
// - Wrap ChangeChatChannel in the validity check instead of
|
||||||
|
// early-returning. The previous early return skipped Dtor and
|
||||||
|
// leaked the native Utf8String allocated a few lines above.
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
if (IsChannelOrExistingLinkshell(channel))
|
||||||
|
RaptureShellModule
|
||||||
|
.Instance()
|
||||||
|
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||||
|
|
||||||
RaptureShellModule
|
|
||||||
.Instance()
|
|
||||||
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
|
||||||
target->Dtor(true);
|
target->Dtor(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,5 +40,11 @@ public class TellTarget
|
|||||||
|
|
||||||
public static TellTarget Empty() => new(string.Empty, 0, 0, TellReason.Direct);
|
public static TellTarget Empty() => new(string.Empty, 0, 0, TellReason.Direct);
|
||||||
|
|
||||||
public static TellTarget From(TellTarget t) => new(t.Name, t.World, t.ContentId, t.Reason);
|
// ---------------------------------------------------------------
|
||||||
|
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||||
|
// - Replaced static From(t) with an instance-style Clone() so call
|
||||||
|
// sites read like a copy operation, not a factory.
|
||||||
|
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/TellTargetCloneTests.cs
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
public TellTarget Clone() => new(Name, World, ContentId, Reason);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.4</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 -->
|
||||||
@@ -15,6 +15,14 @@
|
|||||||
<!-- 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.7" />
|
||||||
|
<!-- 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>
|
||||||
|
|||||||
+177
-31
@@ -35,49 +35,195 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
**v1.4.4 — Threading and IPC safety polish (2026-05-12)**
|
**v1.5.2 — First-Run Wizard Rework (2026-05-18)**
|
||||||
|
|
||||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
|
UX patch. The first-run wizard becomes a four-step flow with a
|
||||||
assumptions are documented per-method, a hot-path lock falls
|
new Roleplay privacy profile and a power-settings step that
|
||||||
away, and the privacy filter speaks up when an unknown ChatType
|
surfaces previously-hidden defaults. Existing v1.5.1 users see
|
||||||
shows up.
|
the new wizard once on first v1.5.2 boot.
|
||||||
|
|
||||||
- AutoTellTabs hot-path getter uses an Interlocked counter
|
What changes user-visible:
|
||||||
instead of taking the lock on every read
|
|
||||||
- Honorific integration: per-method threading banners, plus
|
- Wizard navigation: Welcome → Privacy profile → Power settings
|
||||||
Warning-level log on unsubscribe failure
|
→ Done. Forge-Bronze pagination dots, dedicated stage for the
|
||||||
- AutoTranslate warmup thread marked IsBackground so plugin
|
power settings so they are no longer buried in Settings.
|
||||||
unload doesn't wait for it
|
- Fourth privacy profile "Roleplay": Privacy-First plus Say and
|
||||||
- PrivacyFilter logs once per unknown ChatType so a future
|
both emote types, with a 30-day window for Say and a 90-day
|
||||||
patch's added channel doesn't drop off the radar
|
window for emotes. Shout, Yell and Novice Network stay out.
|
||||||
- New installs persist unknown channels by default; existing
|
- Privacy picker becomes a 2x2 grid. Casual stays the
|
||||||
configs keep their explicit choice
|
recommended option with a ★ marker.
|
||||||
|
- Power-settings step covers Load Previous Session, Filter
|
||||||
|
Include Previous Sessions, Auto-Tell-Tabs History Preload,
|
||||||
|
Compact Density, Prettier Timestamps and a built-in theme
|
||||||
|
picker. All six map to existing Configuration fields — no new
|
||||||
|
settings introduced.
|
||||||
|
- Staged commit: the wizard only writes to Config on the Finish
|
||||||
|
step. Decide-later or X-close at any point leaves the existing
|
||||||
|
config untouched.
|
||||||
|
- Inline test hint on the done step: "type /tell <Player Name>
|
||||||
|
into chat" surfaces the auto-tell-tab spawn mechanism.
|
||||||
|
- Window starts at 720x480 (was 900x560) and can shrink to
|
||||||
|
600x400; Step 1 keeps the fox banner in a folded TreeNode so
|
||||||
|
the onboarding copy stays primary.
|
||||||
|
- Existing users get the new wizard surfaced once on first boot
|
||||||
|
after the update via the new WizardLastShownVersion config
|
||||||
|
field. Future cycles bump the constant only when the wizard
|
||||||
|
itself changes shape.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.4.3 — Faster plugin load + new repo (2026-05-08)**
|
**v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**
|
||||||
|
|
||||||
Heavy startup work (migrations, hooks, windows) now runs async so
|
Hybrid FontManager refactor plus an embedded provenance mark.
|
||||||
Dalamud's UI stays responsive during load. Load time is comparable
|
|
||||||
to v1.4.2 — this is the foundation for v1.4.4 optimisations.
|
|
||||||
|
|
||||||
- Two-phase async load via IAsyncDalamudPlugin
|
What changes under the hood:
|
||||||
- Schema-gate replaces the v9→v16 migration chain; old configs
|
|
||||||
require a v1.4.2 install first
|
- FontManager handle creation moves into the ctor inside a single
|
||||||
- AutoTranslate cache loads on first use instead of every startup
|
atlas.SuppressAutoRebuild() block. The font atlas now builds once
|
||||||
- Custom font (Hellion-Exo2) appears with a brief pop after load
|
per plugin load instead of four to five times — less CPU and GPU
|
||||||
- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL
|
pressure in the first seconds after a reload, less atlas texture
|
||||||
|
memory churn.
|
||||||
|
- Hybrid property model: Axis, AxisItalic and FontAwesome become
|
||||||
|
init-only handles. RegularFont and ItalicFont stay mutable because
|
||||||
|
the eight font settings still need to replace them at runtime —
|
||||||
|
that path is funnelled through RebuildDelegateFonts() now and
|
||||||
|
runs without a plugin reload.
|
||||||
|
- 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.2 — Smoother frames in the chat log**
|
**v1.5.0 — DI Foundation and Service Refactor (2026-05-17)**
|
||||||
|
|
||||||
Per-frame allocations in the chat-log render path eliminated.
|
Major architecture cycle. The plugin bootstrap moves to a
|
||||||
2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.
|
generic-host DI container (Microsoft.Extensions.Hosting +
|
||||||
|
IServiceCollection) modelled on Lightless Sync. Service logging
|
||||||
|
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.
|
||||||
|
|
||||||
- Card-mode: theme/border invariants hoisted out of the per-message loop
|
What changes under the hood:
|
||||||
- Auto-tell tab tint and icon cached per tab
|
|
||||||
- Status bar aggregation runs on ~1% of frames instead of every frame
|
- 18 instance-class services migrate to ILogger<T> via constructor
|
||||||
|
injection across four slices: data layer (MessageStore,
|
||||||
|
MessageManager, AutoTellTabsService), IPC and integrations
|
||||||
|
(HonorificService, IpcManager, TypingIpc, ExtraChat, the three
|
||||||
|
GameFunctions classes), UI window layer (ChatLogWindow,
|
||||||
|
DbViewer, Popout, three settings tabs), and root (Commands,
|
||||||
|
ThemeRegistry, PayloadHandler).
|
||||||
|
- 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.10 — Symbol-Picker and Tell-History Fix (2026-05-16)**
|
||||||
|
|
||||||
|
Eleventh and final sub-patch of the v1.4.x polish-sweep series.
|
||||||
|
Symbol picker for the chat input, a tell-history reload fix for
|
||||||
|
users with many active partners, and a closing cleanup sweep
|
||||||
|
before v1.5.0 picks up the DI-container adoption.
|
||||||
|
|
||||||
|
- Symbol picker: a small smile-icon button left of the channel
|
||||||
|
indicator opens a popup with two tabs. The first lists all 161
|
||||||
|
FFXIV PUA glyphs (Dalamud's SeIconChar enum); the second
|
||||||
|
carries 97 server-verified BMP symbols (latin marks, currency,
|
||||||
|
the full Greek alphabet, geometric shapes, suits, notes) —
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace HellionChat;
|
|||||||
|
|
||||||
// Shared input history for all ChatInputBars (main and pop-out windows).
|
// Shared input history for all ChatInputBars (main and pop-out windows).
|
||||||
// Push deduplicates: existing entries are moved to the end when re-added.
|
// Push deduplicates: existing entries are moved to the end when re-added.
|
||||||
|
// TEST-MIRROR: ../../Hellion Build test/Util/InputHistoryServiceTests.cs
|
||||||
public static class InputHistoryService
|
public static class InputHistoryService
|
||||||
{
|
{
|
||||||
private const int MaxSize = 30;
|
private const int MaxSize = 30;
|
||||||
@@ -41,4 +42,12 @@ public static class InputHistoryService
|
|||||||
return null;
|
return null;
|
||||||
return _entries[cursor];
|
return _entries[cursor];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin reload doesn't reset static state automatically. Plugin.DisposeAsync
|
||||||
|
// calls this so the next load starts with an empty history instead of
|
||||||
|
// inheriting the previous session's entries.
|
||||||
|
public static void Reset()
|
||||||
|
{
|
||||||
|
_entries.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,3 +1,6 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Integrations;
|
namespace HellionChat.Integrations;
|
||||||
|
|
||||||
// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
|
// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
|
||||||
@@ -5,4 +8,13 @@ internal static class IntegrationLinks
|
|||||||
{
|
{
|
||||||
public const string HonorificRepo = "https://github.com/Caraxi/Honorific";
|
public const string HonorificRepo = "https://github.com/Caraxi/Honorific";
|
||||||
public const string HonorificAuthor = "https://github.com/Caraxi";
|
public const string HonorificAuthor = "https://github.com/Caraxi";
|
||||||
|
|
||||||
|
// See BrandingLinks.ValidateUrls for the CA2255 rationale.
|
||||||
|
#pragma warning disable CA2255
|
||||||
|
[ModuleInitializer]
|
||||||
|
#pragma warning restore CA2255
|
||||||
|
internal static void ValidateUrls()
|
||||||
|
{
|
||||||
|
UrlValidation.ValidateAll(nameof(IntegrationLinks), HonorificRepo, HonorificAuthor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+680
-254
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+421
-111
@@ -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;
|
||||||
|
|
||||||
@@ -113,11 +116,46 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||||
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
||||||
|
|
||||||
|
// Platform indirection over Dalamud.Utility.Util. Wired in Phase-1 ctor so
|
||||||
|
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
||||||
|
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.
|
||||||
@@ -154,23 +192,99 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||||
|
|
||||||
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
// PlatformUtil and LogProxy are filled from the DI container in
|
||||||
// must install v1.4.2 first to run the migration chain.
|
// Phase-1 below (`_host.Services.GetRequiredService<IPlatformUtil>()`
|
||||||
|
// and the LogProxy equivalent). Phase-0 helpers that run before that
|
||||||
|
// 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
|
||||||
|
// 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.4 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.4."
|
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.10."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Config.Version = 17;
|
||||||
|
|
||||||
// Drop session-only Auto-Tell-Tabs that a previous crash may have persisted.
|
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
||||||
|
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
|
||||||
|
|
||||||
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)
|
||||||
@@ -192,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
|
||||||
@@ -260,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;
|
||||||
@@ -279,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
|
||||||
{
|
{
|
||||||
@@ -301,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,
|
||||||
@@ -322,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);
|
||||||
}
|
}
|
||||||
@@ -368,10 +563,18 @@ 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());
|
||||||
|
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
||||||
|
|
||||||
if (failure is not null)
|
if (failure is not null)
|
||||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||||
@@ -517,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)
|
||||||
@@ -557,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
|
||||||
{
|
{
|
||||||
@@ -589,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,
|
||||||
@@ -622,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();
|
||||||
@@ -633,19 +945,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
internal void SaveConfig()
|
internal void SaveConfig()
|
||||||
{
|
{
|
||||||
// Strip session-only Auto-Tell-Tabs before serialization; restore after.
|
// Only unpinned TempTabs are session-only — they move aside before
|
||||||
var snapshot = Config.Tabs.ToList();
|
// serialization and re-attach after. Pinned TempTabs stay in
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
// Config.Tabs across the save so JSON includes them. Cloning only the
|
||||||
|
// unpinned subset keeps the allocation proportional to
|
||||||
|
// AutoTellTabsLimit (<=15) instead of the full tab list.
|
||||||
|
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||||
|
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||||
|
|
||||||
Interface.SavePluginConfig(Config);
|
Interface.SavePluginConfig(Config);
|
||||||
|
|
||||||
Config.Tabs.Clear();
|
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||||
Config.Tabs.AddRange(snapshot);
|
|
||||||
|
|
||||||
// F2.1: snapshot-restore preserves IsTempTab tabs but 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/\ /---~\\ ~}}
|
||||||
|
_// _// ~}
|
||||||
+55
@@ -114,6 +114,40 @@ internal class HellionStrings
|
|||||||
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
|
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
|
||||||
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
|
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
|
||||||
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_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));
|
||||||
@@ -168,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));
|
||||||
@@ -258,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));
|
||||||
@@ -368,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));
|
||||||
@@ -388,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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,108 @@
|
|||||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||||
<value>Wizard erneut zeigen</value>
|
<value>Wizard erneut zeigen</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Cancel_Label" xml:space="preserve">
|
||||||
|
<value>Später — Defaults behalten</value>
|
||||||
|
</data>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
@@ -377,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">
|
||||||
@@ -392,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>
|
||||||
@@ -520,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>
|
||||||
@@ -821,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>
|
||||||
@@ -875,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>
|
||||||
|
|||||||
@@ -222,6 +222,108 @@
|
|||||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||||
<value>Show wizard again</value>
|
<value>Show wizard again</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Wizard_Cancel_Label" xml:space="preserve">
|
||||||
|
<value>Later — keep defaults</value>
|
||||||
|
</data>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
@@ -377,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">
|
||||||
@@ -392,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>
|
||||||
@@ -520,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>
|
||||||
@@ -821,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>
|
||||||
@@ -875,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>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// Verifies the FontManager came out of the DI container with every
|
||||||
|
// handle attached and no atlas-load failure on any of them. ItalicFont
|
||||||
|
// is allowed to be null when ItalicEnabled is off.
|
||||||
|
internal sealed class FontManagerCtorSmokeStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
public FontManagerCtorSmokeStep(Plugin plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - FontManager ctor smoke";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var fm = this.plugin.FontManager;
|
||||||
|
if (fm is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("Plugin.FontManager is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.Axis is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("Axis handle is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.AxisItalic is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("AxisItalic handle is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.FontAwesome is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("FontAwesome handle is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.RegularFont is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("RegularFont handle is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Plugin.Config.ItalicEnabled && fm.ItalicFont is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("ItalicEnabled is on but ItalicFont handle is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.Axis.LoadException is { } e1)
|
||||||
|
{
|
||||||
|
ImGui.Text($"Axis load exception: {e1.Message}");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.AxisItalic.LoadException is { } e2)
|
||||||
|
{
|
||||||
|
ImGui.Text($"AxisItalic load exception: {e2.Message}");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.FontAwesome.LoadException is { } e3)
|
||||||
|
{
|
||||||
|
ImGui.Text($"FontAwesome load exception: {e3.Message}");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.RegularFont.LoadException is { } e4)
|
||||||
|
{
|
||||||
|
ImGui.Text($"RegularFont load exception: {e4.Message}");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fm.ItalicFont?.LoadException is { } e5)
|
||||||
|
{
|
||||||
|
ImGui.Text($"ItalicFont load exception: {e5.Message}");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// Push-safety smoke: IFontHandle.Push() is contracted safe regardless
|
||||||
|
// of the Available state, so this step proves the ctor-built handles
|
||||||
|
// can be pushed even right after plugin load. Self-test steps run on
|
||||||
|
// the framework thread via the xlperf path, so the push call itself
|
||||||
|
// stays main-thread-safe.
|
||||||
|
internal sealed class FontPushSmokeStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
public FontPushSmokeStep(Plugin plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - FontManager push smoke";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var fm = this.plugin.FontManager;
|
||||||
|
if (fm?.RegularFont is null || fm.FontAwesome is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("RegularFont or FontAwesome missing - see FontManager ctor smoke");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (fm.RegularFont.Push()) { }
|
||||||
|
using (fm.FontAwesome.Push()) { }
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
ImGui.Text($"Push threw: {e.GetType().Name}: {e.Message}");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp() { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
using HellionChat.Code;
|
||||||
|
using HellionChat.Ui;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// Drives the FirstRunWizard state machine through every step and
|
||||||
|
// commits a no-op pending state (Variant 1), then re-runs picking
|
||||||
|
// Roleplay on Step 2 and skipping Step 3 (Variant 2). Verifies
|
||||||
|
// that the staged-commit path does not throw under any combination
|
||||||
|
// of Pending* values and that CommitPending leaves Config in a
|
||||||
|
// readable shape. Variant 2's Roleplay commit would normally
|
||||||
|
// mutate the six PrivacyFilter / Retention fields ApplyRoleplay
|
||||||
|
// touches, so the step snapshots them before Variant 2 runs and
|
||||||
|
// CleanUp() restores them — the self-test stays idempotent across
|
||||||
|
// repeated /xlperf runs and does not overwrite an active privacy
|
||||||
|
// profile.
|
||||||
|
internal sealed class WizardStateSmokeStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
// Snapshot slots for the six Configuration fields ApplyRoleplay
|
||||||
|
// writes in Variant 2. Populated right before Variant 2 mutates
|
||||||
|
// Config, consumed by CleanUp(). Reference-typed snapshots
|
||||||
|
// (HashSet, Dictionary) capture the existing slot by reference,
|
||||||
|
// which is safe because ApplyRoleplay reassigns the slot with
|
||||||
|
// a fresh instance instead of mutating in place.
|
||||||
|
private bool? snapshotPrivacyFilterEnabled;
|
||||||
|
private HashSet<ChatType>? snapshotPrivacyPersistChannels;
|
||||||
|
private bool? snapshotPrivacyPersistUnknownChannels;
|
||||||
|
private bool? snapshotRetentionEnabled;
|
||||||
|
private int? snapshotRetentionDefaultDays;
|
||||||
|
private Dictionary<ChatType, int>? snapshotRetentionPerChannelDays;
|
||||||
|
|
||||||
|
public WizardStateSmokeStep(Plugin plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - FirstRunWizard state smoke";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var wizard = this.plugin.FirstRunWizard;
|
||||||
|
if (wizard is null)
|
||||||
|
{
|
||||||
|
ImGui.Text("Plugin.FirstRunWizard is null");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Variant 1: no-op CommitPending. Walks the state machine and
|
||||||
|
// verifies the empty-pending write-back path does not throw.
|
||||||
|
wizard.TestOnly_AdvanceTo(1);
|
||||||
|
wizard.TestOnly_AdvanceTo(2);
|
||||||
|
wizard.TestOnly_AdvanceTo(3);
|
||||||
|
wizard.TestOnly_AdvanceTo(4);
|
||||||
|
wizard.CommitPending();
|
||||||
|
|
||||||
|
// Variant 2: skip Step 3 explicitly. Picks Roleplay on Step 2,
|
||||||
|
// jumps straight to Step 4 (no Step-3 entry → no seed for
|
||||||
|
// LoadPreviousSession / FilterIncludePreviousSessions), commits,
|
||||||
|
// and asserts the two coupled history toggles remained on their
|
||||||
|
// pre-test value. Pins the null-semantics from Spec Z.176 so a
|
||||||
|
// regression in CommitPending that started writing seeded
|
||||||
|
// recommendations unconditionally would surface here.
|
||||||
|
// CommitPending → ApplyRoleplay overwrites six privacy /
|
||||||
|
// retention fields, so snapshot them first and let CleanUp
|
||||||
|
// restore them after the assert. Keeps /xlperf idempotent.
|
||||||
|
this.snapshotPrivacyFilterEnabled = Plugin.Config.PrivacyFilterEnabled;
|
||||||
|
this.snapshotPrivacyPersistChannels = Plugin.Config.PrivacyPersistChannels;
|
||||||
|
this.snapshotPrivacyPersistUnknownChannels = Plugin
|
||||||
|
.Config
|
||||||
|
.PrivacyPersistUnknownChannels;
|
||||||
|
this.snapshotRetentionEnabled = Plugin.Config.RetentionEnabled;
|
||||||
|
this.snapshotRetentionDefaultDays = Plugin.Config.RetentionDefaultDays;
|
||||||
|
this.snapshotRetentionPerChannelDays = Plugin.Config.RetentionPerChannelDays;
|
||||||
|
|
||||||
|
var loadPrevBefore = Plugin.Config.LoadPreviousSession;
|
||||||
|
var filterPrevBefore = Plugin.Config.FilterIncludePreviousSessions;
|
||||||
|
wizard.TestOnly_AdvanceTo(2);
|
||||||
|
wizard.TestOnly_SetPendingProfile(FirstRunWizard.PrivacyProfile.Roleplay);
|
||||||
|
wizard.TestOnly_AdvanceTo(4);
|
||||||
|
wizard.CommitPending();
|
||||||
|
if (Plugin.Config.LoadPreviousSession != loadPrevBefore)
|
||||||
|
{
|
||||||
|
ImGui.Text("Skip-Step-3 path overwrote LoadPreviousSession");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
if (Plugin.Config.FilterIncludePreviousSessions != filterPrevBefore)
|
||||||
|
{
|
||||||
|
ImGui.Text("Skip-Step-3 path overwrote FilterIncludePreviousSessions");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ImGui.Text($"Wizard state smoke threw: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp()
|
||||||
|
{
|
||||||
|
// Restore the six Variant-2 snapshots so back-to-back /xlperf
|
||||||
|
// runs don't drift the active privacy profile. If Variant 2
|
||||||
|
// never ran (Variant 1 threw early), the slots stay null and
|
||||||
|
// restore is a no-op. After restore the slots are nulled so a
|
||||||
|
// future RunStep starts fresh.
|
||||||
|
if (this.snapshotPrivacyFilterEnabled is { } privacyFilter)
|
||||||
|
Plugin.Config.PrivacyFilterEnabled = privacyFilter;
|
||||||
|
if (this.snapshotPrivacyPersistChannels is { } persistChannels)
|
||||||
|
Plugin.Config.PrivacyPersistChannels = persistChannels;
|
||||||
|
if (this.snapshotPrivacyPersistUnknownChannels is { } persistUnknown)
|
||||||
|
Plugin.Config.PrivacyPersistUnknownChannels = persistUnknown;
|
||||||
|
if (this.snapshotRetentionEnabled is { } retentionEnabled)
|
||||||
|
Plugin.Config.RetentionEnabled = retentionEnabled;
|
||||||
|
if (this.snapshotRetentionDefaultDays is { } retentionDays)
|
||||||
|
Plugin.Config.RetentionDefaultDays = retentionDays;
|
||||||
|
if (this.snapshotRetentionPerChannelDays is { } retentionPolicy)
|
||||||
|
Plugin.Config.RetentionPerChannelDays = retentionPolicy;
|
||||||
|
|
||||||
|
this.snapshotPrivacyFilterEnabled = null;
|
||||||
|
this.snapshotPrivacyPersistChannels = null;
|
||||||
|
this.snapshotPrivacyPersistUnknownChannels = null;
|
||||||
|
this.snapshotRetentionEnabled = null;
|
||||||
|
this.snapshotRetentionDefaultDays = null;
|
||||||
|
this.snapshotRetentionPerChannelDays = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class CrystalNocturne
|
||||||
|
{
|
||||||
|
public const string Slug = "crystal-nocturne";
|
||||||
|
|
||||||
|
public static Theme Build() =>
|
||||||
|
new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Crystal Nocturne",
|
||||||
|
Author: "CRYSTALLITE",
|
||||||
|
Description: "Royal sapphire and electric magenta over obsidian — a nocturne for the crystal-lit dance floor.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#1D4ED8"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#3B82F6"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#93C5FD"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#3B82F699"),
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#A21CAF"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#D946EF"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#F0ABFC"),
|
||||||
|
Identity: ColourUtil.HexToRgba("#3B82F6"),
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#08070F"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#11101F"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#1C1A33"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#262340"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#332D55"),
|
||||||
|
Border: ColourUtil.HexToRgba("#D946EF55"),
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#F5F3FF"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#A5A0C0"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#4B4763"),
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#10B981"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#F43F5E"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#FACC15"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#3B82F6")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 2f,
|
||||||
|
ChildRounding: 1f,
|
||||||
|
PopupRounding: 2f,
|
||||||
|
FrameRounding: 1f,
|
||||||
|
GrabRounding: 1f,
|
||||||
|
TabRounding: 1f,
|
||||||
|
ScrollbarRounding: 2f,
|
||||||
|
WindowBorderSize: 1f,
|
||||||
|
FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(
|
||||||
|
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Crystal Nocturne — sapphire-blue identity for party/team channels,
|
||||||
|
// accent-magenta for tells, with mint/peach accents on linkshells
|
||||||
|
// so the eight LS slots stay individually distinguishable on the
|
||||||
|
// dark obsidian background.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F5F3FF"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FACC15"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0B090"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#93C5FD"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#10B981"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#93C5FD"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#10B981"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FACC15"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#93C5FD"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#F0B090"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A5A0C0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#D946EF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#3B82F6"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#F0B090"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#F0B090"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A5A0C0"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
using HellionChat.Util;
|
|
||||||
|
|
||||||
namespace HellionChat.Themes.Builtin;
|
|
||||||
|
|
||||||
internal static class MoonlitBloom
|
|
||||||
{
|
|
||||||
public const string Slug = "moonlit-bloom";
|
|
||||||
|
|
||||||
public static Theme Build() =>
|
|
||||||
new(
|
|
||||||
Slug: Slug,
|
|
||||||
Name: "Moonlit Bloom",
|
|
||||||
Author: "Hellion Forge",
|
|
||||||
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
|
|
||||||
Colors: new ThemeColors(
|
|
||||||
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
|
|
||||||
Primary: ColourUtil.HexToRgba("#E374E8"),
|
|
||||||
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
|
|
||||||
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
|
|
||||||
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
|
|
||||||
Accent: ColourUtil.HexToRgba("#9CCB7C"),
|
|
||||||
AccentLight: ColourUtil.HexToRgba("#B6E297"),
|
|
||||||
Identity: ColourUtil.HexToRgba("#E374E8"),
|
|
||||||
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
|
|
||||||
ChildBg: ColourUtil.HexToRgba("#15122B"),
|
|
||||||
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
|
|
||||||
Surface: ColourUtil.HexToRgba("#28224A"),
|
|
||||||
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
|
|
||||||
Border: ColourUtil.HexToRgba("#E374E844"),
|
|
||||||
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
|
|
||||||
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
|
|
||||||
TextDim: ColourUtil.HexToRgba("#554B6E"),
|
|
||||||
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
|
|
||||||
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
|
|
||||||
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
|
||||||
StatusInfo: ColourUtil.HexToRgba("#6278FF")
|
|
||||||
),
|
|
||||||
Layout: new ThemeLayout(
|
|
||||||
WindowRounding: 6f,
|
|
||||||
ChildRounding: 5f,
|
|
||||||
PopupRounding: 5f,
|
|
||||||
FrameRounding: 4f,
|
|
||||||
GrabRounding: 4f,
|
|
||||||
TabRounding: 4f,
|
|
||||||
ScrollbarRounding: 4f,
|
|
||||||
WindowBorderSize: 1f,
|
|
||||||
FrameBorderSize: 1f
|
|
||||||
),
|
|
||||||
Typography: new ThemeTypography(),
|
|
||||||
IsBuiltIn: true,
|
|
||||||
ChatColors: new ThemeChatColors(
|
|
||||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
|
||||||
{
|
|
||||||
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
|
|
||||||
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
|
|
||||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
|
|
||||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
|
|
||||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
|
||||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
|
|
||||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
|
|
||||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
|
|
||||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
|
||||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
|
||||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
|
|
||||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
|
|
||||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
|
|
||||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
|
||||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
|
||||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
using HellionChat.Themes.Builtin;
|
using HellionChat.Themes.Builtin;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Themes;
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
public sealed class ThemeRegistry
|
public sealed class ThemeRegistry
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<ThemeRegistry>? _logger;
|
||||||
|
|
||||||
public const string DefaultSlug = HellionArctic.Slug;
|
public const string DefaultSlug = HellionArctic.Slug;
|
||||||
|
|
||||||
|
// 1Hz throttle for the v1.4.8 B2 auto-refresh-on-active path. The
|
||||||
|
// Plugin.Draw hook calls RefreshActiveIfStale every frame, but the
|
||||||
|
// actual File.GetLastWriteTimeUtc disk-stat only runs once per second
|
||||||
|
// -- 60fps would otherwise mean 3600 stats/min on the same path (more
|
||||||
|
// on Wine). Same idiom as the StatusBar 1Hz cache.
|
||||||
|
private const long ActiveStampPollIntervalMs = 1000;
|
||||||
|
|
||||||
private readonly Dictionary<string, Theme> _builtIns;
|
private readonly Dictionary<string, Theme> _builtIns;
|
||||||
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(
|
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(
|
||||||
StringComparer.OrdinalIgnoreCase
|
StringComparer.OrdinalIgnoreCase
|
||||||
@@ -13,19 +23,33 @@ public sealed class ThemeRegistry
|
|||||||
private readonly string? _customThemesDir;
|
private readonly string? _customThemesDir;
|
||||||
private Theme _active;
|
private Theme _active;
|
||||||
|
|
||||||
public ThemeRegistry(string? customThemesDir = null)
|
// v1.4.8 B2: source path of the currently active custom theme. Captured
|
||||||
|
// at Switch() time so RefreshActiveIfStale does not have to reconstruct
|
||||||
|
// a filename from the slug -- custom theme filenames are not required
|
||||||
|
// to match the slug they declare in the JSON body. Null when the active
|
||||||
|
// theme is built-in or no custom-themes directory is configured.
|
||||||
|
private string? _activeCustomPath;
|
||||||
|
private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs;
|
||||||
|
private DateTime _lastActiveStamp = DateTime.MinValue;
|
||||||
|
|
||||||
|
public ThemeRegistry(string? customThemesDir = null, ILogger<ThemeRegistry>? logger = null)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
|
// Insertion order drives the Theme-Picker grid layout (3 columns).
|
||||||
|
// Row 1: blue family. Row 2: purple to magenta family.
|
||||||
|
// Row 3: green / warm / classic. Row 4: Synthwave Sunset as a
|
||||||
|
// retro bonus on its own line.
|
||||||
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ HellionArctic.Slug, HellionArctic.Build() },
|
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||||
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
||||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
|
||||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
|
||||||
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
|
||||||
{ NightBlue.Slug, NightBlue.Build() },
|
{ NightBlue.Slug, NightBlue.Build() },
|
||||||
|
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||||
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
||||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
{ CrystalNocturne.Slug, CrystalNocturne.Build() },
|
||||||
{ MintGrove.Slug, MintGrove.Build() },
|
{ MintGrove.Slug, MintGrove.Build() },
|
||||||
|
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||||
|
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||||
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,7 +68,9 @@ public sealed class ThemeRegistry
|
|||||||
if (_builtIns.TryGetValue(slug, out var b))
|
if (_builtIns.TryGetValue(slug, out var b))
|
||||||
return b;
|
return b;
|
||||||
|
|
||||||
var custom = LoadCustomBySlug(slug);
|
// Discard the source path here; Switch is the only call-site that
|
||||||
|
// needs to remember it for the auto-refresh hook.
|
||||||
|
var custom = LoadCustomBySlug(slug, out _);
|
||||||
if (custom != null)
|
if (custom != null)
|
||||||
return custom;
|
return custom;
|
||||||
|
|
||||||
@@ -55,12 +81,70 @@ public sealed class ThemeRegistry
|
|||||||
|
|
||||||
public IEnumerable<Theme> AllCustom() => RefreshCustomCache();
|
public IEnumerable<Theme> AllCustom() => RefreshCustomCache();
|
||||||
|
|
||||||
|
// Built-in-first to match Get(slug)'s lookup order. A user theme JSON
|
||||||
|
// that declares the same slug as a built-in is ignored deliberately --
|
||||||
|
// having Switch prefer custom and Get prefer built-in would produce
|
||||||
|
// a state where _active and Get(_active.Slug) disagree.
|
||||||
public void Switch(string slug)
|
public void Switch(string slug)
|
||||||
{
|
{
|
||||||
var theme = Get(slug);
|
if (_builtIns.TryGetValue(slug, out var builtin))
|
||||||
// Defensive — ensures any future theme source always gets a populated cache.
|
{
|
||||||
theme.RecomputeAbgrCache();
|
_active = builtin;
|
||||||
_active = theme;
|
_active.RecomputeAbgrCache();
|
||||||
|
_activeCustomPath = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var customTheme = LoadCustomBySlug(slug, out var customPath);
|
||||||
|
if (customTheme is not null)
|
||||||
|
{
|
||||||
|
_active = customTheme;
|
||||||
|
// Defensive — ensures any future theme source always gets a populated cache.
|
||||||
|
_active.RecomputeAbgrCache();
|
||||||
|
_activeCustomPath = customPath;
|
||||||
|
// Force a first-tick reload-check after the switch so the stamp
|
||||||
|
// baseline is established on the next RefreshActiveIfStale call.
|
||||||
|
_lastActiveStamp = DateTime.MinValue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: neither built-in nor custom matched. Drop to default
|
||||||
|
// and clear the active custom path so RefreshActiveIfStale stays idle.
|
||||||
|
_active = _builtIns[DefaultSlug];
|
||||||
|
_active.RecomputeAbgrCache();
|
||||||
|
_activeCustomPath = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1Hz-throttled disk-stat on the currently active custom theme file.
|
||||||
|
// When the file's LastWriteTime moves forward (editor save), reload the
|
||||||
|
// theme via Get() so the user sees the edit immediately without
|
||||||
|
// re-selecting in the picker. Built-in themes short-circuit; custom
|
||||||
|
// themes without an _activeCustomPath (e.g. Switch fell to default)
|
||||||
|
// short-circuit too.
|
||||||
|
public void RefreshActiveIfStale()
|
||||||
|
{
|
||||||
|
var now = Environment.TickCount64;
|
||||||
|
if (now - _lastActiveStampCheckMs < ActiveStampPollIntervalMs)
|
||||||
|
return;
|
||||||
|
_lastActiveStampCheckMs = now;
|
||||||
|
|
||||||
|
if (_active.IsBuiltIn)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var path = _activeCustomPath;
|
||||||
|
if (path is null || !File.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var stamp = File.GetLastWriteTimeUtc(path);
|
||||||
|
if (!ThemeStampDiff.IsStale(_lastActiveStamp, stamp))
|
||||||
|
return;
|
||||||
|
_lastActiveStamp = stamp;
|
||||||
|
|
||||||
|
// Get() re-runs RefreshCustomCache which picks up the new content
|
||||||
|
// (the cache keys by path + LastWriteTime, so a mtime bump invalidates).
|
||||||
|
// RecomputeAbgrCache happens inside RefreshCustomCache on cache miss.
|
||||||
|
var reloaded = Get(_active.Slug);
|
||||||
|
_active = reloaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION.
|
// 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION.
|
||||||
@@ -73,18 +157,30 @@ public sealed class ThemeRegistry
|
|||||||
return code == 0x80070020u || code == 0x80070021u;
|
return code == 0x80070020u || code == 0x80070021u;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom themes are loaded lazily, cached by LastWriteTime.
|
// Slug -> Theme lookup with the source path as an out-param so the
|
||||||
// A changed JSON is reloaded on the next lookup.
|
// Switch path can remember which file backs the active custom theme.
|
||||||
private Theme? LoadCustomBySlug(string slug)
|
// Pure reverse-lookup over the existing _customCache: that cache is
|
||||||
|
// already Path -> (Theme, Stamp), so iterating it costs nothing,
|
||||||
|
// avoids a re-parse of every JSON, and keeps the parse logic (and
|
||||||
|
// the recoverable-file-lock recovery) confined to RefreshCustomCache.
|
||||||
|
// The cache must be warm before this runs; Plugin.LoadAsync triggers
|
||||||
|
// a one-time warm-up via AllCustom() before the first Switch call.
|
||||||
|
private Theme? LoadCustomBySlug(string slug, out string? sourcePath)
|
||||||
{
|
{
|
||||||
|
sourcePath = null;
|
||||||
if (_customThemesDir is null)
|
if (_customThemesDir is null)
|
||||||
return null;
|
return null;
|
||||||
if (!Directory.Exists(_customThemesDir))
|
if (!Directory.Exists(_customThemesDir))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
foreach (var theme in RefreshCustomCache())
|
foreach (var kvp in _customCache)
|
||||||
if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
{
|
||||||
return theme;
|
if (string.Equals(kvp.Value.Theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
sourcePath = kvp.Key;
|
||||||
|
return kvp.Value.Theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +210,7 @@ public sealed class ThemeRegistry
|
|||||||
catch (Exception ex) when (IsRecoverableFileLock(ex))
|
catch (Exception ex) when (IsRecoverableFileLock(ex))
|
||||||
{
|
{
|
||||||
// Editor mid-save: keep last known good, retry on next refresh.
|
// Editor mid-save: keep last known good, retry on next refresh.
|
||||||
Plugin.Log.Debug(
|
_logger?.LogDebug(
|
||||||
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
|
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
|
||||||
);
|
);
|
||||||
if (cached.Theme is not null)
|
if (cached.Theme is not null)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Pure stale-check for the v1.4.8 B2 theme-auto-refresh-on-active path.
|
||||||
|
// Lives in a free helper class so the Build-Suite can exercise the diff
|
||||||
|
// rules without instantiating ThemeRegistry (which touches the Dalamud
|
||||||
|
// log proxy and the filesystem). The rules:
|
||||||
|
// - DateTime.MinValue on the current stat means we could not read the
|
||||||
|
// file -- hold the last known good (return false).
|
||||||
|
// - Equal stamps mean no change since we last saw it.
|
||||||
|
// - Any other difference, including the first observation where lastSeen
|
||||||
|
// is MinValue, counts as stale and triggers a reload.
|
||||||
|
internal static class ThemeStampDiff
|
||||||
|
{
|
||||||
|
public static bool IsStale(System.DateTime lastSeen, System.DateTime current)
|
||||||
|
{
|
||||||
|
if (current == System.DateTime.MinValue)
|
||||||
|
return false;
|
||||||
|
return current != lastSeen;
|
||||||
|
}
|
||||||
|
}
|
||||||
+344
-55
@@ -22,6 +22,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using Lumina.Extensions;
|
using Lumina.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
private readonly CommandWrapper _clearHellionCommand;
|
private readonly CommandWrapper _clearHellionCommand;
|
||||||
private readonly CommandWrapper _hellionCommand;
|
private readonly CommandWrapper _hellionCommand;
|
||||||
|
private readonly SymbolPicker _symbolPicker;
|
||||||
|
|
||||||
internal bool ScreenshotMode;
|
internal bool ScreenshotMode;
|
||||||
private string Salt { get; }
|
private string Salt { get; }
|
||||||
@@ -90,13 +92,26 @@ public sealed class ChatLogWindow : Window
|
|||||||
private bool PlayedClosingSound = true;
|
private bool PlayedClosingSound = true;
|
||||||
private bool DrewThisFrame;
|
private bool DrewThisFrame;
|
||||||
|
|
||||||
|
// One-shot guard so a recurring draw failure doesn't spam the
|
||||||
|
// notification stack frame-by-frame. Resets only on next plugin reload.
|
||||||
|
private bool NotifiedDrawFailure;
|
||||||
|
|
||||||
private long FrameTime; // set every frame
|
private long FrameTime; // set every frame
|
||||||
internal long LastActivityTime = Environment.TickCount64;
|
internal long LastActivityTime = Environment.TickCount64;
|
||||||
|
|
||||||
internal ChatLogWindow(Plugin plugin)
|
private readonly ILogger<ChatLogWindow> _logger;
|
||||||
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
||||||
|
internal ChatLogWindow(
|
||||||
|
Plugin plugin,
|
||||||
|
ILogger<ChatLogWindow> logger,
|
||||||
|
ILoggerFactory loggerFactory
|
||||||
|
)
|
||||||
: base($"{Plugin.PluginName}###chat2")
|
: base($"{Plugin.PluginName}###chat2")
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
|
_loggerFactory = loggerFactory;
|
||||||
Salt = new Random().Next().ToString();
|
Salt = new Random().Next().ToString();
|
||||||
|
|
||||||
Size = new Vector2(500, 250);
|
Size = new Vector2(500, 250);
|
||||||
@@ -109,8 +124,10 @@ public sealed class ChatLogWindow : Window
|
|||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
// AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow.
|
// AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow.
|
||||||
|
|
||||||
PayloadHandler = new PayloadHandler(this);
|
PayloadHandler = new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>());
|
||||||
HandlerLender = new Lender<PayloadHandler>(() => new PayloadHandler(this));
|
HandlerLender = new Lender<PayloadHandler>(() =>
|
||||||
|
new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>())
|
||||||
|
);
|
||||||
|
|
||||||
SetUpTextCommandChannels();
|
SetUpTextCommandChannels();
|
||||||
SetUpAllCommands();
|
SetUpAllCommands();
|
||||||
@@ -125,6 +142,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
_clearHellionCommand.Execute += ClearLog;
|
_clearHellionCommand.Execute += ClearLog;
|
||||||
_hellionCommand.Execute += ToggleChat;
|
_hellionCommand.Execute += ToggleChat;
|
||||||
|
|
||||||
|
_symbolPicker = new SymbolPicker();
|
||||||
|
|
||||||
Plugin.ClientState.Login += Login;
|
Plugin.ClientState.Login += Login;
|
||||||
Plugin.ClientState.Logout += Logout;
|
Plugin.ClientState.Logout += Logout;
|
||||||
|
|
||||||
@@ -185,11 +204,28 @@ public sealed class ChatLogWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Cherry-picked from ChatTwo upstream ee7768ac (Infiziert90, 2026-05-16)
|
||||||
|
// - Replace the chat input when args.AddIfNotPresent / args.Input starts
|
||||||
|
// with a slash. Vanilla actions like the Friend List "/tell" entry and
|
||||||
|
// other plugins push slash commands through these args; appending them
|
||||||
|
// to existing text would produce inputs like "test/tell user@world".
|
||||||
|
// ---------------------------------------------------------------
|
||||||
if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent))
|
if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent))
|
||||||
Chat += args.AddIfNotPresent;
|
{
|
||||||
|
if (args.AddIfNotPresent.StartsWith('/'))
|
||||||
|
Chat = args.AddIfNotPresent;
|
||||||
|
else
|
||||||
|
Chat += args.AddIfNotPresent;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Input != null)
|
if (args.Input != null)
|
||||||
Chat += args.Input;
|
{
|
||||||
|
if (args.Input.StartsWith('/'))
|
||||||
|
Chat = args.Input;
|
||||||
|
else
|
||||||
|
Chat += args.Input;
|
||||||
|
}
|
||||||
|
|
||||||
var (info, reason, target) = (args.ChannelSwitchInfo, args.TellReason, args.TellTarget);
|
var (info, reason, target) = (args.ChannelSwitchInfo, args.TellReason, args.TellTarget);
|
||||||
|
|
||||||
@@ -268,9 +304,12 @@ public sealed class ChatLogWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetChannel == null || !GameFunctions.Chat.ValidAnyLinkshell(targetChannel.Value))
|
if (
|
||||||
|
targetChannel == null
|
||||||
|
|| !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -324,11 +363,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
case "hide":
|
case "hide":
|
||||||
CurrentHideState = HideState.User;
|
CurrentHideState = HideState.User;
|
||||||
Plugin.Log.Verbose("HideState: → User (chat hide command)");
|
_logger.LogTrace("HideState: → User (chat hide command)");
|
||||||
break;
|
break;
|
||||||
case "show":
|
case "show":
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose("HideState: → None (chat show command)");
|
_logger.LogTrace("HideState: → None (chat show command)");
|
||||||
break;
|
break;
|
||||||
case "toggle":
|
case "toggle":
|
||||||
CurrentHideState = CurrentHideState switch
|
CurrentHideState = CurrentHideState switch
|
||||||
@@ -338,7 +377,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
HideState.None => HideState.User,
|
HideState.None => HideState.User,
|
||||||
_ => CurrentHideState,
|
_ => CurrentHideState,
|
||||||
};
|
};
|
||||||
Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
_logger.LogTrace($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,8 +451,9 @@ public sealed class ChatLogWindow : Window
|
|||||||
// The hint banner renders before this block so ImGui already accounts for it.
|
// The hint banner renders before this block so ImGui already accounts for it.
|
||||||
height -= ImGui.GetFrameHeightWithSpacing();
|
height -= ImGui.GetFrameHeightWithSpacing();
|
||||||
|
|
||||||
// Status bar at the window bottom reserves 22px + 2px spacing.
|
// StatusBar.Height now bakes in its own DPI-aware 2px spacer, so the
|
||||||
height -= StatusBar.Height + 2;
|
// window reservation is just Height -- no extra +2 (v1.4.8 B1).
|
||||||
|
height -= StatusBar.Height;
|
||||||
|
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
@@ -434,11 +474,24 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
private void TabSwitched(Tab newTab, Tab previousTab)
|
private void TabSwitched(Tab newTab, Tab previousTab)
|
||||||
{
|
{
|
||||||
// Use the fixed channel if set by the user, or set it to the current tabs channel if this tab wasn't accessed before
|
// Use the fixed channel if set by the user. Otherwise, if the new tab
|
||||||
|
// has no channel state yet (fresh from JSON, never selected this
|
||||||
|
// session), seed from the previous tab — but deep-clone so we don't
|
||||||
|
// share TellTarget with the previous tab. Without the clone, a later
|
||||||
|
// /tell on the new tab would mutate the pinned tab's TellTarget and
|
||||||
|
// the Party/Linkshell channel would pop back to the pinned tell-mark.
|
||||||
if (newTab.Channel is not null)
|
if (newTab.Channel is not null)
|
||||||
|
{
|
||||||
newTab.CurrentChannel.Channel = newTab.Channel.Value;
|
newTab.CurrentChannel.Channel = newTab.Channel.Value;
|
||||||
|
}
|
||||||
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
|
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
|
||||||
newTab.CurrentChannel = previousTab.CurrentChannel;
|
{
|
||||||
|
newTab.CurrentChannel = previousTab.CurrentChannel.Clone();
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' "
|
||||||
|
+ $"(Channel={newTab.CurrentChannel.Channel}, TellTarget={newTab.CurrentChannel.TellTarget?.ToTargetString() ?? "null"})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
SetChannel(newTab.CurrentChannel.Channel);
|
SetChannel(newTab.CurrentChannel.Channel);
|
||||||
}
|
}
|
||||||
@@ -462,14 +515,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
Plugin.Log.Verbose("HideState: None → Battle");
|
_logger.LogTrace("HideState: None → Battle");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the chat is hidden because of battle, we reset it here
|
// If the chat is hidden because of battle, we reset it here
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose("HideState: Battle → None");
|
_logger.LogTrace("HideState: Battle → None");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||||
@@ -482,7 +535,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (Plugin.Functions.Chat.CheckHideFlags())
|
if (Plugin.Functions.Chat.CheckHideFlags())
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
Plugin.Log.Verbose("HideState: None → Cutscene");
|
_logger.LogTrace("HideState: None → Cutscene");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +546,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
&& !Plugin.GposeActive
|
&& !Plugin.GposeActive
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
_logger.LogTrace($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,14 +554,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (CurrentHideState == HideState.Cutscene && Activate)
|
if (CurrentHideState == HideState.Cutscene && Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
_logger.LogTrace("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the user hid the chat and is now activating chat, reset the hide state
|
// if the user hid the chat and is now activating chat, reset the hide state
|
||||||
if (CurrentHideState == HideState.User && Activate)
|
if (CurrentHideState == HideState.User && Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose("HideState: User → None (activate)");
|
_logger.LogTrace("HideState: User → None (activate)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -615,6 +668,15 @@ public sealed class ChatLogWindow : Window
|
|||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.4.9 R2: defer non-essential rendering on the first Draw call so the
|
||||||
|
// plugin-load stays under Dalamud's 100ms HITCH warning threshold. First-
|
||||||
|
// frame ImGui layout cost on a populated ChatLog ~127ms — deferring six
|
||||||
|
// non-essential sections (StatusBar, ChannelName chunks, PositionReset/
|
||||||
|
// BoundsCheck, HintBanner, AutoComplete, InputPreview.CalculatePreview)
|
||||||
|
// shaves ~33ms down to ~94ms. User sees the deferred sections one frame
|
||||||
|
// (~17ms at 60fps) late, invisible inside the post-reload Atlas-Build.
|
||||||
|
private bool _firstFrameDone;
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
DrewThisFrame = true;
|
DrewThisFrame = true;
|
||||||
@@ -622,15 +684,39 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
DrawChatLog();
|
DrawChatLog();
|
||||||
AddPopOutsToDraw();
|
AddPopOutsToDraw();
|
||||||
DrawAutoComplete();
|
|
||||||
|
// v1.4.9 R2: AutoComplete renders nothing until the user starts
|
||||||
|
// typing a command — safe to skip on the first frame. ~6ms.
|
||||||
|
if (_firstFrameDone)
|
||||||
|
DrawAutoComplete();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error drawing Chat Log window");
|
_logger.LogError(ex, "Error drawing Chat Log window");
|
||||||
|
if (!NotifiedDrawFailure)
|
||||||
|
{
|
||||||
|
Plugin.Notification.AddNotification(
|
||||||
|
new Dalamud.Interface.ImGuiNotification.Notification
|
||||||
|
{
|
||||||
|
Title = "Hellion Chat",
|
||||||
|
Content = "A drawing error occurred. Check /xllog for details.",
|
||||||
|
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||||
|
InitialDuration = TimeSpan.FromSeconds(20),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
NotifiedDrawFailure = true;
|
||||||
|
}
|
||||||
// Prevent recurring draw failures from constantly trying to grab
|
// Prevent recurring draw failures from constantly trying to grab
|
||||||
// input focus, which breaks every other ImGui window.
|
// input focus, which breaks every other ImGui window.
|
||||||
Activate = false;
|
Activate = false;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Flag flips after the first Draw completes (success or caught
|
||||||
|
// exception). Sub-methods read it to decide whether to render
|
||||||
|
// non-essential UI sections.
|
||||||
|
_firstFrameDone = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsChatMode =>
|
private static bool IsChatMode =>
|
||||||
@@ -646,18 +732,25 @@ public sealed class ChatLogWindow : Window
|
|||||||
LastWindowSize = currentSize;
|
LastWindowSize = currentSize;
|
||||||
LastWindowPos = ImGui.GetWindowPos();
|
LastWindowPos = ImGui.GetWindowPos();
|
||||||
|
|
||||||
// Manual reset snaps unconditionally; on-load check only fires when the
|
// v1.4.9 R2: skip the bounds-check chain on the first frame. The
|
||||||
// stored position has no overlap with any visible viewport.
|
// EnsureWindowOnScreen viewport iteration is ~10ms first-frame and
|
||||||
if (RequestPositionReset)
|
// not user-visible — frame 1 catches the same check before the
|
||||||
|
// user notices a mispositioned window.
|
||||||
|
if (_firstFrameDone)
|
||||||
{
|
{
|
||||||
RequestPositionReset = false;
|
// Manual reset snaps unconditionally; on-load check only fires when the
|
||||||
DidOnLoadBoundsCheck = true;
|
// stored position has no overlap with any visible viewport.
|
||||||
ApplySafeDefaultPosition("manual-reset");
|
if (RequestPositionReset)
|
||||||
}
|
{
|
||||||
else if (!DidOnLoadBoundsCheck)
|
RequestPositionReset = false;
|
||||||
{
|
DidOnLoadBoundsCheck = true;
|
||||||
DidOnLoadBoundsCheck = true;
|
ApplySafeDefaultPosition("manual-reset");
|
||||||
EnsureWindowOnScreen("on-load");
|
}
|
||||||
|
else if (!DidOnLoadBoundsCheck)
|
||||||
|
{
|
||||||
|
DidOnLoadBoundsCheck = true;
|
||||||
|
EnsureWindowOnScreen("on-load");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resized)
|
if (resized)
|
||||||
@@ -666,12 +759,17 @@ public sealed class ChatLogWindow : Window
|
|||||||
LastViewport = ImGui.GetWindowViewport().Handle;
|
LastViewport = ImGui.GetWindowViewport().Handle;
|
||||||
WasDocked = ImGui.IsWindowDocked();
|
WasDocked = ImGui.IsWindowDocked();
|
||||||
|
|
||||||
if (IsChatMode && Plugin.InputPreview.IsDrawable)
|
// v1.4.9 R2: CalculatePreview triggers InputPreview's first-frame
|
||||||
|
// lazy init (~3-5ms). User-typing-driven, safe to defer one frame.
|
||||||
|
if (_firstFrameDone && IsChatMode && Plugin.InputPreview.IsDrawable)
|
||||||
Plugin.InputPreview.CalculatePreview();
|
Plugin.InputPreview.CalculatePreview();
|
||||||
|
|
||||||
// Render the hint banner first so it sits above the tab area at full
|
// Render the hint banner first so it sits above the tab area at full
|
||||||
// window width. ImGui accounts for its height automatically.
|
// window width. ImGui accounts for its height automatically.
|
||||||
DrawV061HintBannerIfNeeded();
|
// v1.4.9 R2: skip on first frame (~3-5ms layout cost). The banner
|
||||||
|
// is a v0.6.1 migration notice that returns the same result frame 1.
|
||||||
|
if (_firstFrameDone)
|
||||||
|
DrawV061HintBannerIfNeeded();
|
||||||
|
|
||||||
if (Plugin.Config.SidebarTabView)
|
if (Plugin.Config.SidebarTabView)
|
||||||
DrawTabSidebar();
|
DrawTabSidebar();
|
||||||
@@ -726,6 +824,40 @@ public sealed class ChatLogWindow : Window
|
|||||||
)
|
)
|
||||||
inputColour = ecColour;
|
inputColour = ecColour;
|
||||||
|
|
||||||
|
// Symbol-picker trigger sits left of the channel indicator. ImRaii.Popup
|
||||||
|
// inside DrawAndConsume pins to the last rendered item, so the call MUST
|
||||||
|
// run immediately after this IconButton — placing it after the channel
|
||||||
|
// picker below would pin the popup under the wrong widget.
|
||||||
|
if (Plugin.Config.SymbolPickerEnabled)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
ImGuiUtil.IconButton(
|
||||||
|
FontAwesomeIcon.Smile,
|
||||||
|
"symbol-picker-trigger",
|
||||||
|
"Insert symbol or FFXIV icon"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_symbolPicker.OpenPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DrawAndConsume runs unconditionally; with the button hidden the popup
|
||||||
|
// can't open, so the call is a no-op. Splice path stays outside the
|
||||||
|
// guard for the same reason.
|
||||||
|
var insertedSymbol = _symbolPicker.DrawAndConsume();
|
||||||
|
if (insertedSymbol is not null)
|
||||||
|
{
|
||||||
|
// Same cursor-aware splice idiom as the AutoComplete commit path at
|
||||||
|
// ChatLogWindow.cs:2487-2493. Clamp because CursorPos can drift if
|
||||||
|
// the user mutates Chat while the popup is open.
|
||||||
|
var pos = Math.Clamp(CursorPos, 0, Chat.Length);
|
||||||
|
Chat = Chat[..pos] + insertedSymbol + Chat[pos..];
|
||||||
|
Activate = true;
|
||||||
|
ActivatePos = pos + insertedSymbol.Length;
|
||||||
|
}
|
||||||
|
if (Plugin.Config.SymbolPickerEnabled)
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
var beforeIcon = ImGui.GetCursorPos();
|
var beforeIcon = ImGui.GetCursorPos();
|
||||||
|
|
||||||
var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
|
var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
|
||||||
@@ -904,7 +1036,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
// v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog,
|
// v1.2.0 — Bottom-Status-Bar. Letzter Render-Step in DrawChatLog,
|
||||||
// damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen.
|
// damit alle Zeilen-Operationen davor keine Layout-Sprünge auslösen.
|
||||||
Plugin.StatusBar.Draw(Plugin);
|
// v1.4.9 R2: skip on the first frame; ~12ms of first-frame layout
|
||||||
|
// cost. User sees the StatusBar 1 frame (~17ms at 60fps) later
|
||||||
|
// which is hidden inside the post-reload Atlas-Build window.
|
||||||
|
if (_firstFrameDone)
|
||||||
|
Plugin.StatusBar.Draw(Plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Dictionary<string, InputChannel> GetValidChannels()
|
internal Dictionary<string, InputChannel> GetValidChannels()
|
||||||
@@ -955,6 +1091,16 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
private void DrawChannelName(Tab activeTab)
|
private void DrawChannelName(Tab activeTab)
|
||||||
{
|
{
|
||||||
|
// v1.4.9 R2: plain-text fallback on the first frame. ReadChannelName
|
||||||
|
// builds SeString chunks and DrawChunks runs SeString-Renderer layout
|
||||||
|
// — together ~18ms first-frame. Frame 1 renders the real chunks; the
|
||||||
|
// user sees the tab name for ~17ms during the post-reload window.
|
||||||
|
if (!_firstFrameDone)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(activeTab.Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var currentChannel = ReadChannelName(activeTab);
|
var currentChannel = ReadChannelName(activeTab);
|
||||||
if (!currentChannel.SequenceEqual(PreviousChannel))
|
if (!currentChannel.SequenceEqual(PreviousChannel))
|
||||||
PreviousChannel = currentChannel;
|
PreviousChannel = currentChannel;
|
||||||
@@ -1588,7 +1734,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(ex, "Error drawing chat log");
|
_logger.LogWarning(ex, "Error drawing chat log");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1620,17 +1766,21 @@ public sealed class ChatLogWindow : Window
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Active-tab underline pill (2px accent). No native ImGui underline API,
|
// Active-tab underline pill (2px accent). No native ImGui underline API,
|
||||||
// so we use a direct DrawList pass.
|
// so we use a direct DrawList pass. Pill height scales with GlobalScale
|
||||||
|
// and all coordinates round to physical pixels so the line stays crisp
|
||||||
|
// on 125/150% DPI setups instead of bleeding into a sub-pixel blur.
|
||||||
{
|
{
|
||||||
var theme = Plugin.ThemeRegistry.Active;
|
var theme = Plugin.ThemeRegistry.Active;
|
||||||
var min = ImGui.GetItemRectMin();
|
var min = ImGui.GetItemRectMin();
|
||||||
var max = ImGui.GetItemRectMax();
|
var max = ImGui.GetItemRectMax();
|
||||||
const float pillHeight = 2f;
|
var pillHeight = MathF.Max(1f, MathF.Round(2f * ImGuiHelpers.GlobalScale));
|
||||||
|
var yBottom = MathF.Round(max.Y);
|
||||||
|
var yTop = yBottom - pillHeight;
|
||||||
ImGui
|
ImGui
|
||||||
.GetWindowDrawList()
|
.GetWindowDrawList()
|
||||||
.AddRectFilled(
|
.AddRectFilled(
|
||||||
new Vector2(min.X, max.Y - pillHeight),
|
new Vector2(MathF.Round(min.X), yTop),
|
||||||
new Vector2(max.X, max.Y),
|
new Vector2(MathF.Round(max.X), yBottom),
|
||||||
ColourUtil.RgbaToAbgr(theme.Colors.Accent)
|
ColourUtil.RgbaToAbgr(theme.Colors.Accent)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1649,6 +1799,30 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.WantedTab = null;
|
Plugin.WantedTab = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sidebar render order: persistent tabs in their original Plugin.Config.Tabs
|
||||||
|
// position, then pinned TempTabs, then unpinned TempTabs. Returns indices
|
||||||
|
// into Plugin.Config.Tabs so tabI in the loop body still mirrors the real
|
||||||
|
// list position (LastTab / WantedTab stay consistent).
|
||||||
|
private static List<int> BuildSidebarRenderOrder()
|
||||||
|
{
|
||||||
|
var tabs = Plugin.Config.Tabs;
|
||||||
|
var persistent = new List<int>(tabs.Count);
|
||||||
|
var pinned = new List<int>();
|
||||||
|
var unpinned = new List<int>();
|
||||||
|
for (var i = 0; i < tabs.Count; i++)
|
||||||
|
{
|
||||||
|
if (TabLifecycleHelpers.IsInPinnedPool(tabs[i]))
|
||||||
|
pinned.Add(i);
|
||||||
|
else if (TabLifecycleHelpers.IsInUnpinnedPool(tabs[i]))
|
||||||
|
unpinned.Add(i);
|
||||||
|
else
|
||||||
|
persistent.Add(i);
|
||||||
|
}
|
||||||
|
persistent.AddRange(pinned);
|
||||||
|
persistent.AddRange(unpinned);
|
||||||
|
return persistent;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawTabSidebar()
|
private void DrawTabSidebar()
|
||||||
{
|
{
|
||||||
var currentTab = -1;
|
var currentTab = -1;
|
||||||
@@ -1661,7 +1835,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (!tabTable.Success)
|
if (!tabTable.Success)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, 44f);
|
var sidebarWidth = Math.Clamp(Plugin.Config.SidebarWidth, 44, 160);
|
||||||
|
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, sidebarWidth);
|
||||||
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
|
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
@@ -1680,23 +1855,42 @@ public sealed class ChatLogWindow : Window
|
|||||||
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
|
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
|
||||||
|
|
||||||
var previousTab = Plugin.CurrentTab;
|
var previousTab = Plugin.CurrentTab;
|
||||||
// Divider rendered once before the first temp tab with a live unit counter.
|
// Render order: persistent → pinned TempTabs → unpinned TempTabs.
|
||||||
|
// Underlying Plugin.Config.Tabs order is untouched (tabI mirrors
|
||||||
|
// the real list index), only the display sequence groups by
|
||||||
|
// section so each section can carry its own divider header.
|
||||||
|
var renderOrder = BuildSidebarRenderOrder();
|
||||||
|
var pinnedHeaderRendered = false;
|
||||||
var tempTabHeaderRendered = false;
|
var tempTabHeaderRendered = false;
|
||||||
var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
var pinnedCount = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||||
|
var unpinnedTempCount = Plugin.Config.Tabs.Count(
|
||||||
|
TabLifecycleHelpers.IsInUnpinnedPool
|
||||||
|
);
|
||||||
|
|
||||||
for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++)
|
foreach (var tabI in renderOrder)
|
||||||
{
|
{
|
||||||
var tab = Plugin.Config.Tabs[tabI];
|
var tab = Plugin.Config.Tabs[tabI];
|
||||||
if (tab.PopOut)
|
if (tab.PopOut)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (tab.IsTempTab && !tempTabHeaderRendered)
|
if (TabLifecycleHelpers.IsInPinnedPool(tab) && !pinnedHeaderRendered)
|
||||||
{
|
{
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||||
{
|
{
|
||||||
ImGui.TextDisabled(
|
ImGui.TextDisabled(
|
||||||
$"{HellionStrings.AutoTellTabs_SectionHeader} ({tempTabCount})"
|
$"{HellionStrings.PinTab_SectionHeader} ({pinnedCount})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pinnedHeaderRendered = true;
|
||||||
|
}
|
||||||
|
else if (TabLifecycleHelpers.IsInUnpinnedPool(tab) && !tempTabHeaderRendered)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled(
|
||||||
|
$"{HellionStrings.AutoTellTabs_SectionHeader} ({unpinnedTempCount})"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
tempTabHeaderRendered = true;
|
tempTabHeaderRendered = true;
|
||||||
@@ -1785,9 +1979,12 @@ public sealed class ChatLogWindow : Window
|
|||||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
|
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
|
||||||
using (Plugin.FontManager.FontAwesome.Push())
|
using (Plugin.FontManager.FontAwesome.Push())
|
||||||
{
|
{
|
||||||
|
// Button stretches with the configured sidebar width so a
|
||||||
|
// user-widened sidebar feels intentional, not a 36px icon
|
||||||
|
// floating in empty space.
|
||||||
clicked = ImGui.Button(
|
clicked = ImGui.Button(
|
||||||
$"{icon.ToIconString()}##sidebar-tab-{tabI}",
|
$"{icon.ToIconString()}##sidebar-tab-{tabI}",
|
||||||
new Vector2(36f, ImGui.GetFrameHeight())
|
new Vector2(sidebarWidth - 8f, ImGui.GetFrameHeight())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1847,11 +2044,35 @@ public sealed class ChatLogWindow : Window
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pin indicator: subtle thumbtack glyph top-left of the icon.
|
||||||
|
// Muted colour because the "Pinned" section header already
|
||||||
|
// groups these tabs visually — this is just a per-tab
|
||||||
|
// confirmation glyph, not the primary discoverability cue.
|
||||||
|
if (tab.IsPinned)
|
||||||
|
{
|
||||||
|
var min = ImGui.GetItemRectMin();
|
||||||
|
const float pinPadding = 1f;
|
||||||
|
var pinPos = new Vector2(min.X + pinPadding, min.Y + pinPadding);
|
||||||
|
var pinColor = theme.Colors.TextMuted;
|
||||||
|
// Dim further so the glyph reads as a hint, not a badge.
|
||||||
|
var pinAbgr = ColourUtil.RgbaToAbgr(pinColor) & 0x77FFFFFFu;
|
||||||
|
using (Plugin.FontManager.FontAwesome.Push())
|
||||||
|
{
|
||||||
|
ImGui
|
||||||
|
.GetWindowDrawList()
|
||||||
|
.AddText(pinPos, pinAbgr, FontAwesomeIcon.Thumbtack.ToIconString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tooltip mit Tab-Name + Unread-Counter beim Hover.
|
// Tooltip mit Tab-Name + Unread-Counter beim Hover.
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
{
|
{
|
||||||
using var tt = ImRaii.Tooltip();
|
using var tt = ImRaii.Tooltip();
|
||||||
ImGui.TextUnformatted($"{tab.Name}{unread}");
|
ImGui.TextUnformatted($"{tab.Name}{unread}");
|
||||||
|
if (tab.IsPinned)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(HellionStrings.PinTab_PinnedTooltip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawTabContextMenu(tab, tabI);
|
DrawTabContextMenu(tab, tabI);
|
||||||
@@ -1975,10 +2196,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
||||||
}
|
}
|
||||||
ImGui.SameLine(0f, gapAfterCrown);
|
ImGui.SameLine(0f, gapAfterCrown);
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
DrawHonorificTitleText(rendered, titleColor, title.Glow);
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(rendered);
|
|
||||||
}
|
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
@@ -1989,6 +2207,35 @@ public sealed class ChatLogWindow : Window
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renders the title text, optionally with a glow outline pre-pass. Glow is
|
||||||
|
// drawn at 8 cardinal offsets (±1 px) in the glow colour at reduced alpha,
|
||||||
|
// then the primary text on top. The pre-pass uses the window draw list so
|
||||||
|
// it composites correctly with the regular ImGui text that follows.
|
||||||
|
private void DrawHonorificTitleText(string rendered, Vector4 titleColor, Vector3? glow)
|
||||||
|
{
|
||||||
|
if (Plugin.Config.ShowHonorificGlow && glow is { } g)
|
||||||
|
{
|
||||||
|
var pos = ImGui.GetCursorScreenPos();
|
||||||
|
var glowColor = new Vector4(g.X, g.Y, g.Z, 0.4f);
|
||||||
|
var glowAbgr = ImGui.ColorConvertFloat4ToU32(glowColor);
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
for (var dy = -1; dy <= 1; dy++)
|
||||||
|
{
|
||||||
|
for (var dx = -1; dx <= 1; dx++)
|
||||||
|
{
|
||||||
|
if (dx == 0 && dy == 0)
|
||||||
|
continue;
|
||||||
|
drawList.AddText(new Vector2(pos.X + dx, pos.Y + dy), glowAbgr, rendered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(rendered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// One-time hint banner for the pop-out header button and right-click pathway.
|
// One-time hint banner for the pop-out header button and right-click pathway.
|
||||||
private float DrawV061HintBannerIfNeeded()
|
private float DrawV061HintBannerIfNeeded()
|
||||||
{
|
{
|
||||||
@@ -2035,7 +2282,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
Plugin.Config.SeenPopOutHeaderHint = true;
|
Plugin.Config.SeenPopOutHeaderHint = true;
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
Plugin.Log.Debug("v0.6.1 pop-out header hint dismissed");
|
_logger.LogDebug("v0.6.1 pop-out header hint dismissed");
|
||||||
if (openSettings)
|
if (openSettings)
|
||||||
Plugin.SettingsWindow.Toggle();
|
Plugin.SettingsWindow.Toggle();
|
||||||
}
|
}
|
||||||
@@ -2100,10 +2347,52 @@ public sealed class ChatLogWindow : Window
|
|||||||
anyChanged = true;
|
anyChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab.IsTempTab)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawPinControls(tab);
|
||||||
|
}
|
||||||
|
|
||||||
if (anyChanged)
|
if (anyChanged)
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawPinControls(Tab tab)
|
||||||
|
{
|
||||||
|
var svc = Plugin.AutoTellTabsService;
|
||||||
|
if (svc == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (tab.IsPinned)
|
||||||
|
{
|
||||||
|
if (ImGui.MenuItem(HellionStrings.PinTab_MenuUnpin))
|
||||||
|
{
|
||||||
|
svc.Unpin(tab);
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var atCap = svc.PinnedTempTabCount >= AutoTellTabsService.MaxPinnedTempTabs;
|
||||||
|
if (ImGui.MenuItem(HellionStrings.PinTab_MenuPin, enabled: !atCap))
|
||||||
|
{
|
||||||
|
if (svc.TryPin(tab))
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip(
|
||||||
|
atCap
|
||||||
|
? string.Format(
|
||||||
|
HellionStrings.PinTab_LimitReached,
|
||||||
|
AutoTellTabsService.MaxPinnedTempTabs
|
||||||
|
)
|
||||||
|
: HellionStrings.PinTab_PinTooltip
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly List<bool> PopOutDocked = [];
|
internal readonly List<bool> PopOutDocked = [];
|
||||||
internal readonly HashSet<Guid> PopOutWindows = [];
|
internal readonly HashSet<Guid> PopOutWindows = [];
|
||||||
|
|
||||||
@@ -2131,7 +2420,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (PopOutWindows.Contains(tab.Identifier))
|
if (PopOutWindows.Contains(tab.Identifier))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var window = new Popout(this, tab, i);
|
var window = new Popout(this, tab, i, _loggerFactory.CreateLogger<Popout>());
|
||||||
|
|
||||||
Plugin.WindowSystem.AddWindow(window);
|
Plugin.WindowSystem.AddWindow(window);
|
||||||
PopOutWindows.Add(tab.Identifier);
|
PopOutWindows.Add(tab.Identifier);
|
||||||
@@ -2648,7 +2937,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
var viewport = ImGui.GetMainViewport();
|
var viewport = ImGui.GetMainViewport();
|
||||||
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
||||||
Position = safePos;
|
Position = safePos;
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
|
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+80
-21
@@ -2,6 +2,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
@@ -16,6 +17,7 @@ using HellionChat.Resources;
|
|||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Lumina.Data.Files;
|
using Lumina.Data.Files;
|
||||||
using Lumina.Text.ReadOnly;
|
using Lumina.Text.ReadOnly;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using MoreLinq;
|
using MoreLinq;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
@@ -33,11 +35,21 @@ public class DbViewer : Window
|
|||||||
|
|
||||||
private int CurrentPage = 1;
|
private int CurrentPage = 1;
|
||||||
private string SimpleSearchTerm = "";
|
private string SimpleSearchTerm = "";
|
||||||
|
|
||||||
|
// v1.4.8 H2: opt-in full-text search across the whole DB via FTS5.
|
||||||
|
// Transient UI state (per-session), not persisted -- users opt in fresh
|
||||||
|
// every time so they always see the page-filter as the default mode.
|
||||||
|
private bool UseFullTextSearch;
|
||||||
|
|
||||||
private bool OnlyCurrentCharacter = true;
|
private bool OnlyCurrentCharacter = true;
|
||||||
private readonly Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels;
|
private readonly Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels;
|
||||||
|
|
||||||
private bool IsProcessing;
|
private bool IsProcessing;
|
||||||
private long ProcessingStart = Environment.TickCount64;
|
private long ProcessingStart = Environment.TickCount64;
|
||||||
|
|
||||||
|
// Bumped per trigger so a late worker drops itself instead of overwriting
|
||||||
|
// a newer result.
|
||||||
|
private long _ftsFilterSeq;
|
||||||
private (DateTime Min, DateTime Max, int Page, bool Local, int ChannelCount) LastProcessed;
|
private (DateTime Min, DateTime Max, int Page, bool Local, int ChannelCount) LastProcessed;
|
||||||
|
|
||||||
private string MinDateString = "";
|
private string MinDateString = "";
|
||||||
@@ -56,10 +68,13 @@ public class DbViewer : Window
|
|||||||
|
|
||||||
private bool NeedsScrollReset;
|
private bool NeedsScrollReset;
|
||||||
|
|
||||||
public DbViewer(Plugin plugin)
|
private readonly ILogger<DbViewer> _logger;
|
||||||
|
|
||||||
|
public DbViewer(Plugin plugin, ILogger<DbViewer> logger)
|
||||||
: base("DBViewer###chat2-dbviewer")
|
: base("DBViewer###chat2-dbviewer")
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
|
_logger = logger;
|
||||||
SelectedChannels = TabsUtil.MostlyPlayer;
|
SelectedChannels = TabsUtil.MostlyPlayer;
|
||||||
|
|
||||||
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
||||||
@@ -82,29 +97,13 @@ public class DbViewer : Window
|
|||||||
|
|
||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Plugin
|
|
||||||
.Commands.Register(
|
|
||||||
"/hellionView",
|
|
||||||
"Get access to your message history, with simple filter options.",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.Execute += Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin
|
// Slash-command tear-down moved to Plugin.TearDownCommands.
|
||||||
.Commands.Register(
|
|
||||||
"/hellionView",
|
|
||||||
"Get access to your message history, with simple filter options.",
|
|
||||||
true
|
|
||||||
)
|
|
||||||
.Execute -= Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Toggle(string _, string __) => Toggle();
|
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
|
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
|
||||||
@@ -233,6 +232,24 @@ public class DbViewer : Window
|
|||||||
tooltipRight: Language.Page_ArrowRight_Tooltip
|
tooltipRight: Language.Page_ArrowRight_Tooltip
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Full-text search toggle (v1.4.8 H2). IsFtsIndexBuilt is a cached
|
||||||
|
// volatile bool in MessageStore -- single field read per frame, no
|
||||||
|
// SELECT count(*). ImRaii.Disabled blocks any click while the index
|
||||||
|
// is still being built, so no defensive force-off branch needed
|
||||||
|
// inside the if-body. UseFullTextSearch is transient UI state, so we
|
||||||
|
// do not call SaveConfig here.
|
||||||
|
var ftsReady = Plugin.MessageManager.Store.IsFtsIndexBuilt;
|
||||||
|
using (ImRaii.Disabled(!ftsReady))
|
||||||
|
{
|
||||||
|
if (ImGui.Checkbox(HellionStrings.DbViewer_FullTextToggle, ref UseFullTextSearch))
|
||||||
|
TriggerFilterRefresh();
|
||||||
|
}
|
||||||
|
ImGuiUtil.HelpMarker(
|
||||||
|
ftsReady
|
||||||
|
? HellionStrings.DbViewer_FullTextToggle_Hint_PhraseMode
|
||||||
|
: HellionStrings.DbViewer_FullTextToggle_Hint_Indexing
|
||||||
|
);
|
||||||
|
|
||||||
ImGui.SameLine(ImGui.GetContentRegionMax().X - width);
|
ImGui.SameLine(ImGui.GetContentRegionMax().X - width);
|
||||||
ImGui.SetNextItemWidth(width);
|
ImGui.SetNextItemWidth(width);
|
||||||
if (
|
if (
|
||||||
@@ -243,7 +260,7 @@ public class DbViewer : Window
|
|||||||
30
|
30
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Filtered = Filter(Messages);
|
TriggerFilterRefresh();
|
||||||
|
|
||||||
// Third row
|
// Third row
|
||||||
|
|
||||||
@@ -307,7 +324,7 @@ public class DbViewer : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Failed reading messages from database");
|
_logger.LogError(ex, "Failed reading messages from database");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -447,11 +464,53 @@ public class DbViewer : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FTS path hits SQLite per keystroke -- dispatch to a worker, drop stale
|
||||||
|
// results via _ftsFilterSeq. Page-filter path is in-memory LINQ, stays
|
||||||
|
// inline.
|
||||||
|
private void TriggerFilterRefresh()
|
||||||
|
{
|
||||||
|
if (!UseFullTextSearch || !Plugin.MessageManager.Store.IsFtsIndexBuilt)
|
||||||
|
{
|
||||||
|
Filtered = Filter(Messages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshot = Messages;
|
||||||
|
var mySeq = Interlocked.Increment(ref _ftsFilterSeq);
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = Filter(snapshot);
|
||||||
|
if (Interlocked.Read(ref _ftsFilterSeq) == mySeq)
|
||||||
|
Filtered = result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "FTS filter worker failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private ConcurrentStack<Message> Filter(Message[] messages)
|
private ConcurrentStack<Message> Filter(Message[] messages)
|
||||||
{
|
{
|
||||||
if (SimpleSearchTerm == "")
|
if (SimpleSearchTerm == "")
|
||||||
return new ConcurrentStack<Message>(messages.Reverse().OrderByDescending(m => m.Date));
|
return new ConcurrentStack<Message>(messages.Reverse().OrderByDescending(m => m.Date));
|
||||||
|
|
||||||
|
// Full-text mode bypasses the page-bounded messages array and queries
|
||||||
|
// the FTS5 index across the whole DB. IsFtsIndexBuilt re-check guards
|
||||||
|
// against the (rare) case of the toggle being on while the index is
|
||||||
|
// mid-rebuild -- ImRaii.Disabled prevents the user from flipping it,
|
||||||
|
// but a Dispose-and-reopen during indexing could leave UseFullTextSearch
|
||||||
|
// true while ftsReady flipped back to false; the local fallback below
|
||||||
|
// still serves the page.
|
||||||
|
if (UseFullTextSearch && Plugin.MessageManager.Store.IsFtsIndexBuilt)
|
||||||
|
{
|
||||||
|
var guidHits = Plugin.MessageManager.Store.FullTextSearch(SimpleSearchTerm);
|
||||||
|
var resolved = Plugin.MessageManager.Store.LoadByGuids(guidHits);
|
||||||
|
return new ConcurrentStack<Message>(resolved.OrderByDescending(m => m.Date));
|
||||||
|
}
|
||||||
|
|
||||||
return new ConcurrentStack<Message>(
|
return new ConcurrentStack<Message>(
|
||||||
messages
|
messages
|
||||||
.Reverse()
|
.Reverse()
|
||||||
@@ -570,7 +629,7 @@ public class DbViewer : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Failed creating txt backup");
|
_logger.LogError(ex, "Failed creating txt backup");
|
||||||
|
|
||||||
Notification.Content = "Error ...";
|
Notification.Content = "Error ...";
|
||||||
Notification.Type = NotificationType.Error;
|
Notification.Type = NotificationType.Error;
|
||||||
|
|||||||
@@ -28,17 +28,13 @@ public class DebuggerWindow : Window, IDisposable
|
|||||||
|
|
||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute += Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin.Commands.Register("/hellionDebugger", showInHelp: false).Execute -= Toggle;
|
// Slash-command tear-down moved to Plugin.TearDownCommands.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Toggle(string _, string __) => Toggle();
|
|
||||||
|
|
||||||
public override unsafe void Draw()
|
public override unsafe void Draw()
|
||||||
{
|
{
|
||||||
var agent = (nint)AgentItemDetail.Instance();
|
var agent = (nint)AgentItemDetail.Instance();
|
||||||
|
|||||||
@@ -1,17 +1,40 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
|
using HellionChat.Branding;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Privacy;
|
using HellionChat.Privacy;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Themes;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
|
// Multi-step first-run wizard. public sealed because Plugin.cs has a
|
||||||
|
// public-typed property on this class — narrowing to internal would
|
||||||
|
// be a build break across the assembly boundary. State lives in a
|
||||||
|
// nested WizardState record; every step writes nullable Pending*
|
||||||
|
// fields, and CommitPending() applies only the non-null ones so
|
||||||
|
// users who skip a step never get their existing config overwritten.
|
||||||
public sealed class FirstRunWizard : Window
|
public sealed class FirstRunWizard : Window
|
||||||
{
|
{
|
||||||
|
// Forge-Bronze (#C2410C). The same constant lives in ThemeRegistry
|
||||||
|
// and the forge-announce workflow; pinning it locally keeps the
|
||||||
|
// wizard render path free of registry lookups during draw.
|
||||||
|
private static readonly Vector4 ForgeBronze = new(0xC2 / 255f, 0x41 / 255f, 0x0C / 255f, 1f);
|
||||||
|
private static readonly Vector4 ForgeBronzeDim = new(
|
||||||
|
0xC2 / 255f,
|
||||||
|
0x41 / 255f,
|
||||||
|
0x0C / 255f,
|
||||||
|
0.3f
|
||||||
|
);
|
||||||
|
|
||||||
|
private const int TotalSteps = 4;
|
||||||
|
|
||||||
private readonly Plugin Plugin;
|
private readonly Plugin Plugin;
|
||||||
|
private readonly WizardState _state = new();
|
||||||
|
|
||||||
internal FirstRunWizard(Plugin plugin)
|
internal FirstRunWizard(Plugin plugin)
|
||||||
: base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
|
: base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
|
||||||
@@ -20,115 +43,546 @@ public sealed class FirstRunWizard : Window
|
|||||||
|
|
||||||
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking;
|
Flags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking;
|
||||||
SizeCondition = ImGuiCond.Appearing;
|
SizeCondition = ImGuiCond.Appearing;
|
||||||
Size = new Vector2(900, 560);
|
Size = new Vector2(720, 480);
|
||||||
SizeConstraints = new WindowSizeConstraints
|
SizeConstraints = new WindowSizeConstraints
|
||||||
{
|
{
|
||||||
MinimumSize = new Vector2(720, 480),
|
MinimumSize = new Vector2(600, 400),
|
||||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
// Closing the wizard without picking anything = the user accepts
|
// OnClose fires on explicit X-click and on plugin dispose. We never
|
||||||
// whatever defaults are already in place. Mark as complete so we
|
// implicitly accept the defaults here — both the explicit "Decide
|
||||||
// don't pester them again on the next launch.
|
// later" footer link and a successful "Finish ✓" set FirstRunCompleted
|
||||||
if (!Plugin.Config.FirstRunCompleted)
|
// = true, so the wizard does not reopen on the next plugin load
|
||||||
{
|
// regardless of which path the user took.
|
||||||
Plugin.Config.FirstRunCompleted = true;
|
|
||||||
Plugin.SaveConfig();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
ImGui.TextWrapped(HellionStrings.Wizard_Intro);
|
DrawPagination();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
switch (_state.CurrentStep)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
DrawStepWelcome();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
DrawStepPrivacy();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
DrawStepPowerSettings();
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
DrawStepDone();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_state.CurrentStep = 1;
|
||||||
|
DrawStepWelcome();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawPagination()
|
||||||
|
{
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
var avail = ImGui.GetContentRegionAvail();
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f;
|
var cursor = ImGui.GetCursorScreenPos();
|
||||||
var cardHeight = avail.Y - ImGui.GetTextLineHeightWithSpacing();
|
const float radius = 5f;
|
||||||
|
const float spacing = 16f;
|
||||||
|
var totalWidth = (TotalSteps - 1) * spacing;
|
||||||
|
var startX = cursor.X + avail.X - totalWidth - radius;
|
||||||
|
|
||||||
DrawCard(
|
for (var i = 0; i < TotalSteps; i++)
|
||||||
"privacy-first",
|
{
|
||||||
cardWidth,
|
var color = (i + 1) == _state.CurrentStep ? ForgeBronze : ForgeBronzeDim;
|
||||||
cardHeight,
|
var packed = ImGui.GetColorU32(color);
|
||||||
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
|
draw.AddCircleFilled(
|
||||||
HellionStrings.Wizard_Profile_PrivacyFirst_Description,
|
new Vector2(startX + i * spacing, cursor.Y + radius),
|
||||||
null,
|
radius,
|
||||||
HellionStrings.Wizard_Profile_PrivacyFirst_Apply,
|
packed
|
||||||
ApplyPrivacyFirst
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
// Reserve vertical space the circles consumed so the next widget starts below them.
|
||||||
|
ImGui.Dummy(new Vector2(0, radius * 2));
|
||||||
|
}
|
||||||
|
|
||||||
DrawCard(
|
private void DrawFooter(bool showBack, bool showSkip, string primaryLabel, Action onPrimary)
|
||||||
"casual",
|
{
|
||||||
cardWidth,
|
var spacing = ImGui.GetStyle().ItemSpacing.Y;
|
||||||
cardHeight,
|
var primaryWidth =
|
||||||
HellionStrings.Wizard_Profile_Casual_Heading,
|
ImGui.CalcTextSize(primaryLabel).X + ImGui.GetStyle().FramePadding.X * 2 + 16f;
|
||||||
HellionStrings.Wizard_Profile_Casual_Description,
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
null,
|
|
||||||
HellionStrings.Wizard_Profile_Casual_Apply,
|
|
||||||
ApplyCasual
|
|
||||||
);
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
// Push the footer to the bottom of the window so step contents
|
||||||
|
// above can size themselves with GetContentRegionAvail().
|
||||||
|
var lineHeight = ImGui.GetFrameHeightWithSpacing();
|
||||||
|
var pushDown = avail.Y - lineHeight - spacing;
|
||||||
|
if (pushDown > 0)
|
||||||
|
ImGui.Dummy(new Vector2(0, pushDown));
|
||||||
|
|
||||||
DrawCard(
|
ImGui.Separator();
|
||||||
"full-history",
|
ImGui.Spacing();
|
||||||
cardWidth,
|
|
||||||
cardHeight,
|
if (showBack)
|
||||||
HellionStrings.Wizard_Profile_FullHistory_Heading,
|
{
|
||||||
HellionStrings.Wizard_Profile_FullHistory_Description,
|
if (ImGui.Button(HellionStrings.Wizard_Nav_Back))
|
||||||
HellionStrings.Wizard_Profile_FullHistory_GdprWarning,
|
_state.CurrentStep = Math.Max(1, _state.CurrentStep - 1);
|
||||||
HellionStrings.Wizard_Profile_FullHistory_Apply,
|
ImGui.SameLine();
|
||||||
ApplyFullHistory
|
}
|
||||||
|
|
||||||
|
if (showSkip)
|
||||||
|
{
|
||||||
|
if (ImGui.Button(HellionStrings.Wizard_Step1_Skip_Label))
|
||||||
|
{
|
||||||
|
// Skip path = matches today's Cancel path: mark first-run
|
||||||
|
// complete, save, close. No CommitPending — the user said
|
||||||
|
// 'decide later', so existing config stays as-is.
|
||||||
|
Plugin.Config.FirstRunCompleted = true;
|
||||||
|
Plugin.SaveConfig();
|
||||||
|
IsOpen = false;
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGuiUtil.Tooltip(HellionStrings.Wizard_Step1_Skip_Tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-align the primary action button.
|
||||||
|
var rightX = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X - primaryWidth;
|
||||||
|
if (rightX > ImGui.GetCursorPosX())
|
||||||
|
ImGui.SameLine(rightX);
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, ForgeBronze))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ForgeBronze))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ForgeBronze))
|
||||||
|
{
|
||||||
|
if (ImGui.Button($"{primaryLabel}##wizard-primary"))
|
||||||
|
onPrimary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStepWelcome()
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step1_Title);
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Banner is opt-in: the full silhouette dominates the wizard window
|
||||||
|
// at the default size, so the TreeNode is folded by default and the
|
||||||
|
// onboarding copy stays the primary focus. Mirrors the pre-rewrite
|
||||||
|
// collapsible anchor from v1.5.1.
|
||||||
|
using (var tree = ImRaii.TreeNode("Hellion Forge"))
|
||||||
|
{
|
||||||
|
if (tree.Success)
|
||||||
|
{
|
||||||
|
using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
|
||||||
|
{
|
||||||
|
// CalcTextSize must run inside the MonoFont push so the
|
||||||
|
// measurement matches the glyph width actually used for
|
||||||
|
// rendering.
|
||||||
|
var bannerSize = ImGui.CalcTextSize(HellionForgeAscii.FoxBanner);
|
||||||
|
ImGui.SetCursorPosX((ImGui.GetContentRegionAvail().X - bannerSize.X) * 0.5f);
|
||||||
|
ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped(HellionStrings.Wizard_Step1_Subtitle);
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped(HellionStrings.Wizard_Step1_Footer_Hint);
|
||||||
|
|
||||||
|
DrawFooter(
|
||||||
|
showBack: false,
|
||||||
|
showSkip: true,
|
||||||
|
HellionStrings.Wizard_Nav_Next,
|
||||||
|
() => _state.CurrentStep = 2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawCard(
|
private void DrawStepPrivacy()
|
||||||
string id,
|
{
|
||||||
float width,
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step2_Title);
|
||||||
float height,
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Reserve footer height (separator + spacing + button row) so the
|
||||||
|
// 2x2 grid uses the rest of the window.
|
||||||
|
var footerReserve =
|
||||||
|
ImGui.GetFrameHeightWithSpacing()
|
||||||
|
+ ImGui.GetStyle().ItemSpacing.Y * 3
|
||||||
|
+ ImGui.GetTextLineHeight();
|
||||||
|
var grid = ImGui.GetContentRegionAvail();
|
||||||
|
var cardWidth = (grid.X - ImGui.GetStyle().ItemSpacing.X) / 2f;
|
||||||
|
var cardHeight = (grid.Y - footerReserve - ImGui.GetStyle().ItemSpacing.Y) / 2f;
|
||||||
|
|
||||||
|
// Top row.
|
||||||
|
DrawProfileCard(
|
||||||
|
PrivacyProfile.PrivacyFirst,
|
||||||
|
"🔒",
|
||||||
|
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
|
||||||
|
HellionStrings.Wizard_Profile_PrivacyFirst_Description,
|
||||||
|
recommended: false,
|
||||||
|
cardWidth,
|
||||||
|
cardHeight
|
||||||
|
);
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawProfileCard(
|
||||||
|
PrivacyProfile.Casual,
|
||||||
|
"💬",
|
||||||
|
HellionStrings.Wizard_Profile_Casual_Heading,
|
||||||
|
HellionStrings.Wizard_Profile_Casual_Description,
|
||||||
|
recommended: true,
|
||||||
|
cardWidth,
|
||||||
|
cardHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bottom row.
|
||||||
|
DrawProfileCard(
|
||||||
|
PrivacyProfile.Roleplay,
|
||||||
|
"🎭",
|
||||||
|
HellionStrings.Wizard_Profile_Roleplay_Heading,
|
||||||
|
HellionStrings.Wizard_Profile_Roleplay_Description,
|
||||||
|
recommended: false,
|
||||||
|
cardWidth,
|
||||||
|
cardHeight
|
||||||
|
);
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawProfileCard(
|
||||||
|
PrivacyProfile.FullHistory,
|
||||||
|
"📚",
|
||||||
|
HellionStrings.Wizard_Profile_FullHistory_Heading,
|
||||||
|
HellionStrings.Wizard_Profile_FullHistory_Description,
|
||||||
|
recommended: false,
|
||||||
|
cardWidth,
|
||||||
|
cardHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextDisabled(HellionStrings.Wizard_Step2_RecommendedFooter);
|
||||||
|
|
||||||
|
DrawFooter(
|
||||||
|
showBack: true,
|
||||||
|
showSkip: true,
|
||||||
|
HellionStrings.Wizard_Nav_Next,
|
||||||
|
() => _state.CurrentStep = 3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawProfileCard(
|
||||||
|
PrivacyProfile profile,
|
||||||
|
string emoji,
|
||||||
string heading,
|
string heading,
|
||||||
string description,
|
string description,
|
||||||
string? warning,
|
bool recommended,
|
||||||
string buttonLabel,
|
float width,
|
||||||
Action onApply
|
float height
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
using var child = ImRaii.Child($"##wizard-card-{id}", new Vector2(width, height), true);
|
var isSelected = _state.PendingProfile == profile;
|
||||||
|
// GetStyleColorVec4 returns a pointer to the live style entry in
|
||||||
|
// Dalamud.Bindings.ImGui, which would require unsafe. Use the U32
|
||||||
|
// packed-colour overload of PushColor for the default branch so we
|
||||||
|
// can stay in safe code while still matching the current border.
|
||||||
|
var borderColor = isSelected
|
||||||
|
? ImGui.GetColorU32(ForgeBronze)
|
||||||
|
: ImGui.GetColorU32(ImGuiCol.Border);
|
||||||
|
|
||||||
|
using var _border = ImRaii.PushColor(ImGuiCol.Border, borderColor);
|
||||||
|
using var child = ImRaii.Child(
|
||||||
|
$"##profile-card-{profile}",
|
||||||
|
new Vector2(width, height),
|
||||||
|
true
|
||||||
|
);
|
||||||
if (!child.Success)
|
if (!child.Success)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGui.TextUnformatted(heading);
|
// InvisibleButton over the full card area, then SetCursorScreenPos
|
||||||
|
// back to draw the heading/description content on top. Selectable
|
||||||
|
// would be semantically wrong here — the card is a standalone
|
||||||
|
// choice tile, not a list-item inside a list/menu. The button
|
||||||
|
// takes the click for the entire card area, and IsItemHovered()
|
||||||
|
// on it (if we wire one up later) would naturally cover the full
|
||||||
|
// tile. Visual feedback comes from the border colour above.
|
||||||
|
var startPos = ImGui.GetCursorScreenPos();
|
||||||
|
var cardArea = ImGui.GetContentRegionAvail();
|
||||||
|
if (ImGui.InvisibleButton($"##profile-hit-{profile}", cardArea))
|
||||||
|
_state.PendingProfile = profile;
|
||||||
|
|
||||||
|
ImGui.SetCursorScreenPos(startPos);
|
||||||
|
|
||||||
|
ImGui.TextUnformatted($"{emoji} {heading}{(recommended ? " ★" : string.Empty)}");
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStepPowerSettings()
|
||||||
|
{
|
||||||
|
// Seed only the two recommendation fields here. Other fields remain
|
||||||
|
// null until the user touches the corresponding control.
|
||||||
|
// Spec FR-4: the wizard explicitly recommends LoadPreviousSession =
|
||||||
|
// true and FilterIncludePreviousSessions = true (Config defaults are
|
||||||
|
// false). The other four fields (AutoTellTabsHistoryPreload,
|
||||||
|
// UseCompactDensity, PrettierTimestamps, Theme) follow the generic
|
||||||
|
// null-semantics from Spec Z.176: a null pending means the user did
|
||||||
|
// not touch that control, so CommitPending must not write back. They
|
||||||
|
// are read live from Plugin.Config below for the ImGui ref-binding
|
||||||
|
// but never seeded into Pending* without a user gesture.
|
||||||
|
_state.PendingLoadPreviousSession ??= true;
|
||||||
|
_state.PendingFilterIncludePreviousSessions ??= true;
|
||||||
|
|
||||||
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Title);
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// History section.
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
||||||
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_History);
|
||||||
|
|
||||||
|
var loadPrev = _state.PendingLoadPreviousSession ?? true;
|
||||||
|
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_LoadPreviousSession_Label, ref loadPrev))
|
||||||
|
{
|
||||||
|
_state.PendingLoadPreviousSession = loadPrev;
|
||||||
|
// Mirror the DataManagement coupling: turning load-previous on
|
||||||
|
// also turns filter-include on (otherwise old messages bypass
|
||||||
|
// the filter chain), and turning filter-include off forces
|
||||||
|
// load-previous off. Same idiom as Ui/SettingsTabs/DataManagement.cs:182-200.
|
||||||
|
if (loadPrev)
|
||||||
|
_state.PendingFilterIncludePreviousSessions = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterPrev = _state.PendingFilterIncludePreviousSessions ?? true;
|
||||||
|
if (
|
||||||
|
ImGui.Checkbox(
|
||||||
|
HellionStrings.Wizard_Step3_FilterIncludePreviousSessions_Label,
|
||||||
|
ref filterPrev
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_state.PendingFilterIncludePreviousSessions = filterPrev;
|
||||||
|
if (!filterPrev)
|
||||||
|
_state.PendingLoadPreviousSession = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Tell-Tabs section.
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
||||||
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_TellTabs);
|
||||||
|
|
||||||
|
var preload =
|
||||||
|
_state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload;
|
||||||
|
if (
|
||||||
|
ImGui.SliderInt(
|
||||||
|
HellionStrings.Wizard_Step3_AutoTellTabsHistoryPreload_Label,
|
||||||
|
ref preload,
|
||||||
|
0,
|
||||||
|
100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_state.PendingAutoTellTabsHistoryPreload = preload;
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Visual section.
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
||||||
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step3_Section_Visual);
|
||||||
|
|
||||||
|
var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity;
|
||||||
|
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_UseCompactDensity_Label, ref compact))
|
||||||
|
_state.PendingUseCompactDensity = compact;
|
||||||
|
|
||||||
|
var pretty = _state.PendingPrettierTimestamps ?? Plugin.Config.PrettierTimestamps;
|
||||||
|
if (ImGui.Checkbox(HellionStrings.Wizard_Step3_PrettierTimestamps_Label, ref pretty))
|
||||||
|
_state.PendingPrettierTimestamps = pretty;
|
||||||
|
|
||||||
|
// Theme dropdown — built-ins only. Custom themes are power-user
|
||||||
|
// territory and would clutter the first-run flow.
|
||||||
|
var currentSlug = _state.PendingTheme ?? Plugin.Config.Theme;
|
||||||
|
var builtIns = Plugin.ThemeRegistry.AllBuiltIns().ToList();
|
||||||
|
var currentIndex = builtIns.FindIndex(t =>
|
||||||
|
string.Equals(t.Slug, currentSlug, StringComparison.OrdinalIgnoreCase)
|
||||||
|
);
|
||||||
|
if (currentIndex < 0)
|
||||||
|
currentIndex = 0;
|
||||||
|
|
||||||
|
using (
|
||||||
|
var combo = ImRaii.Combo(
|
||||||
|
HellionStrings.Wizard_Step3_Theme_Label,
|
||||||
|
builtIns[currentIndex].Name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (combo.Success)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < builtIns.Count; i++)
|
||||||
|
{
|
||||||
|
var isSelected = i == currentIndex;
|
||||||
|
if (ImGui.Selectable(builtIns[i].Name, isSelected))
|
||||||
|
_state.PendingTheme = builtIns[i].Slug;
|
||||||
|
if (isSelected)
|
||||||
|
ImGui.SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawFooter(
|
||||||
|
showBack: true,
|
||||||
|
showSkip: true,
|
||||||
|
HellionStrings.Wizard_Nav_Next,
|
||||||
|
() => _state.CurrentStep = 4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStepDone()
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step4_Title);
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// ✓ symbol, centred-ish via dummy padding.
|
||||||
|
var checkmark = "✓";
|
||||||
|
var checkSize = ImGui.CalcTextSize(checkmark);
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
ImGui.Dummy(new Vector2((avail.X - checkSize.X) * 0.5f, 0));
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
||||||
|
ImGui.TextUnformatted(checkmark);
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
ImGui.TextWrapped(description);
|
// Summary card.
|
||||||
|
using (var summary = ImRaii.Child("##wizard-summary", new Vector2(-1, 130), true))
|
||||||
if (warning is not null)
|
|
||||||
{
|
{
|
||||||
ImGui.Spacing();
|
if (summary.Success)
|
||||||
ImGuiUtil.WarningText(warning);
|
{
|
||||||
|
ImGui.TextUnformatted(HellionStrings.Wizard_Step4_SummaryHeading);
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var profileLabel = _state.PendingProfile switch
|
||||||
|
{
|
||||||
|
PrivacyProfile.PrivacyFirst =>
|
||||||
|
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
|
||||||
|
PrivacyProfile.Casual => HellionStrings.Wizard_Profile_Casual_Heading,
|
||||||
|
PrivacyProfile.Roleplay => HellionStrings.Wizard_Profile_Roleplay_Heading,
|
||||||
|
PrivacyProfile.FullHistory => HellionStrings.Wizard_Profile_FullHistory_Heading,
|
||||||
|
_ => HellionStrings.Wizard_Step4_Summary_Unchanged,
|
||||||
|
};
|
||||||
|
ImGui.TextWrapped(
|
||||||
|
string.Format(HellionStrings.Wizard_Step4_Summary_Profile, profileLabel)
|
||||||
|
);
|
||||||
|
|
||||||
|
var historyLabel =
|
||||||
|
(_state.PendingLoadPreviousSession ?? false)
|
||||||
|
? HellionStrings.Wizard_Step3_LoadPreviousSession_Label
|
||||||
|
: HellionStrings.Wizard_Step4_Summary_Unchanged;
|
||||||
|
ImGui.TextWrapped(
|
||||||
|
string.Format(HellionStrings.Wizard_Step4_Summary_History, historyLabel)
|
||||||
|
);
|
||||||
|
|
||||||
|
var preloadValue =
|
||||||
|
_state.PendingAutoTellTabsHistoryPreload
|
||||||
|
?? Plugin.Config.AutoTellTabsHistoryPreload;
|
||||||
|
ImGui.TextWrapped(
|
||||||
|
string.Format(HellionStrings.Wizard_Step4_Summary_TellTabs, preloadValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
var compact = _state.PendingUseCompactDensity ?? Plugin.Config.UseCompactDensity;
|
||||||
|
var pretty = _state.PendingPrettierTimestamps ?? Plugin.Config.PrettierTimestamps;
|
||||||
|
var themeSlug = _state.PendingTheme ?? Plugin.Config.Theme;
|
||||||
|
var themeName = Plugin.ThemeRegistry.Get(themeSlug).Name;
|
||||||
|
var visualParts = new List<string>();
|
||||||
|
if (compact)
|
||||||
|
visualParts.Add(HellionStrings.Wizard_Step3_UseCompactDensity_Label);
|
||||||
|
if (pretty)
|
||||||
|
visualParts.Add(HellionStrings.Wizard_Step3_PrettierTimestamps_Label);
|
||||||
|
visualParts.Add(themeName);
|
||||||
|
ImGui.TextWrapped(
|
||||||
|
string.Format(
|
||||||
|
HellionStrings.Wizard_Step4_Summary_Visual,
|
||||||
|
string.Join(", ", visualParts)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the button to the bottom of the card.
|
ImGui.Spacing();
|
||||||
var lineHeight = ImGui.GetFrameHeightWithSpacing();
|
|
||||||
var remaining = ImGui.GetContentRegionAvail().Y - lineHeight;
|
|
||||||
if (remaining > 0)
|
|
||||||
ImGui.Dummy(new Vector2(0, remaining));
|
|
||||||
|
|
||||||
if (ImGui.Button($"{buttonLabel}##{id}", new Vector2(-1, 0)))
|
// Inline FR-3 hint with placeholder for preload count.
|
||||||
|
var preloadForHint =
|
||||||
|
_state.PendingAutoTellTabsHistoryPreload ?? Plugin.Config.AutoTellTabsHistoryPreload;
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ForgeBronze))
|
||||||
|
ImGui.TextWrapped(string.Format(HellionStrings.Wizard_Step4_TestHint, preloadForHint));
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextDisabled(HellionStrings.Wizard_Step4_SettingsHint);
|
||||||
|
|
||||||
|
DrawFooter(
|
||||||
|
showBack: true,
|
||||||
|
showSkip: false,
|
||||||
|
HellionStrings.Wizard_Nav_Finish,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
CommitPending();
|
||||||
|
Plugin.Config.FirstRunCompleted = true;
|
||||||
|
Plugin.SaveConfig();
|
||||||
|
IsOpen = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes only non-null pending values back to Config. A null pending
|
||||||
|
// means the user did not touch that step's control, so the existing
|
||||||
|
// Config value is preserved. Theme switch goes through ThemeRegistry
|
||||||
|
// so the active palette updates live for the rest of the session.
|
||||||
|
internal void CommitPending()
|
||||||
|
{
|
||||||
|
switch (_state.PendingProfile)
|
||||||
{
|
{
|
||||||
onApply();
|
case PrivacyProfile.PrivacyFirst:
|
||||||
Plugin.Config.FirstRunCompleted = true;
|
ApplyPrivacyFirst();
|
||||||
Plugin.SaveConfig();
|
break;
|
||||||
IsOpen = false;
|
case PrivacyProfile.Casual:
|
||||||
|
ApplyCasual();
|
||||||
|
break;
|
||||||
|
case PrivacyProfile.Roleplay:
|
||||||
|
ApplyRoleplay();
|
||||||
|
break;
|
||||||
|
case PrivacyProfile.FullHistory:
|
||||||
|
ApplyFullHistory();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_state.PendingLoadPreviousSession.HasValue)
|
||||||
|
Plugin.Config.LoadPreviousSession = _state.PendingLoadPreviousSession.Value;
|
||||||
|
|
||||||
|
if (_state.PendingFilterIncludePreviousSessions.HasValue)
|
||||||
|
Plugin.Config.FilterIncludePreviousSessions = _state
|
||||||
|
.PendingFilterIncludePreviousSessions
|
||||||
|
.Value;
|
||||||
|
|
||||||
|
if (_state.PendingAutoTellTabsHistoryPreload.HasValue)
|
||||||
|
Plugin.Config.AutoTellTabsHistoryPreload = _state
|
||||||
|
.PendingAutoTellTabsHistoryPreload
|
||||||
|
.Value;
|
||||||
|
|
||||||
|
if (_state.PendingUseCompactDensity.HasValue)
|
||||||
|
Plugin.Config.UseCompactDensity = _state.PendingUseCompactDensity.Value;
|
||||||
|
|
||||||
|
if (_state.PendingPrettierTimestamps.HasValue)
|
||||||
|
Plugin.Config.PrettierTimestamps = _state.PendingPrettierTimestamps.Value;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_state.PendingTheme))
|
||||||
|
{
|
||||||
|
Plugin.Config.Theme = _state.PendingTheme;
|
||||||
|
Plugin.ThemeRegistry.Switch(_state.PendingTheme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +614,20 @@ public sealed class FirstRunWizard : Window
|
|||||||
Plugin.Config.RetentionPerChannelDays = policy;
|
Plugin.Config.RetentionPerChannelDays = policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyRoleplay()
|
||||||
|
{
|
||||||
|
Plugin.Config.PrivacyFilterEnabled = true;
|
||||||
|
Plugin.Config.PrivacyPersistChannels = [.. PrivacyDefaults.RoleplayWhitelist];
|
||||||
|
Plugin.Config.PrivacyPersistUnknownChannels = false;
|
||||||
|
|
||||||
|
Plugin.Config.RetentionEnabled = true;
|
||||||
|
Plugin.Config.RetentionDefaultDays = 30;
|
||||||
|
var policy = PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value);
|
||||||
|
foreach (var (type, days) in PrivacyDefaults.RoleplayRetentionOverrides)
|
||||||
|
policy[type] = days;
|
||||||
|
Plugin.Config.RetentionPerChannelDays = policy;
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyFullHistory()
|
private void ApplyFullHistory()
|
||||||
{
|
{
|
||||||
// Full history = upstream Chat 2 behavior. Filter off, retention off,
|
// Full history = upstream Chat 2 behavior. Filter off, retention off,
|
||||||
@@ -171,4 +639,34 @@ public sealed class FirstRunWizard : Window
|
|||||||
Plugin.Config.RetentionEnabled = false;
|
Plugin.Config.RetentionEnabled = false;
|
||||||
Plugin.Config.RetentionPerChannelDays.Clear();
|
Plugin.Config.RetentionPerChannelDays.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test-only entry point so SelfTests/WizardStateSmokeStep can advance
|
||||||
|
// the state machine without spawning ImGui input events.
|
||||||
|
internal void TestOnly_AdvanceTo(int step) =>
|
||||||
|
_state.CurrentStep = Math.Clamp(step, 1, TotalSteps);
|
||||||
|
|
||||||
|
// Test-only setter so the smoke-test can pin a profile selection
|
||||||
|
// without driving the ImGui card-click path.
|
||||||
|
internal void TestOnly_SetPendingProfile(PrivacyProfile profile) =>
|
||||||
|
_state.PendingProfile = profile;
|
||||||
|
|
||||||
|
internal enum PrivacyProfile
|
||||||
|
{
|
||||||
|
PrivacyFirst,
|
||||||
|
Casual,
|
||||||
|
Roleplay,
|
||||||
|
FullHistory,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WizardState
|
||||||
|
{
|
||||||
|
public int CurrentStep { get; set; } = 1;
|
||||||
|
public PrivacyProfile? PendingProfile { get; set; }
|
||||||
|
public bool? PendingLoadPreviousSession { get; set; }
|
||||||
|
public bool? PendingFilterIncludePreviousSessions { get; set; }
|
||||||
|
public int? PendingAutoTellTabsHistoryPreload { get; set; }
|
||||||
|
public bool? PendingUseCompactDensity { get; set; }
|
||||||
|
public bool? PendingPrettierTimestamps { get; set; }
|
||||||
|
public string? PendingTheme { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,10 @@ internal static class HellionStyle
|
|||||||
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
||||||
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
||||||
|
|
||||||
// ChildBg alpha: child areas rendered inside ChatLogWindow would
|
// ChildBg alpha resolution lives in HellionStyleHelpers so the
|
||||||
// multiply their alpha with WindowBg, making 50% opacity appear
|
// threshold logic can be covered by a pure-helper test in the
|
||||||
// ~75% solid. At full opacity the theme's alpha is preserved; below
|
// build suite.
|
||||||
// it ChildBg goes fully transparent so only WindowBg sets the final
|
var childBgWithAlpha = HellionStyleHelpers.ResolveChildBgAlpha(c.ChildBg, windowOpacity);
|
||||||
// coverage.
|
|
||||||
var childBgAlpha = windowOpacity >= 0.999f ? (c.ChildBg & 0xFFu) : 0u;
|
|
||||||
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | childBgAlpha;
|
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
|
internal static class HellionStyleHelpers
|
||||||
|
{
|
||||||
|
// Child surfaces are drawn over WindowBg, so at partial window opacity
|
||||||
|
// the theme's own ChildBg alpha would double-multiply and read too solid.
|
||||||
|
// Above ~full opacity we preserve the theme alpha; below it we wipe to 0
|
||||||
|
// so WindowBg alone carries the coverage. The 0.999f threshold is a
|
||||||
|
// float-imprecision guard around the user-facing 100% slider value.
|
||||||
|
// TEST-MIRROR: ../../Hellion Build test/_Helpers/HellionStyleHelpersTests.cs
|
||||||
|
public static uint ResolveChildBgAlpha(uint themeChildBgRgba, float windowOpacity)
|
||||||
|
{
|
||||||
|
var alphaPreserved = windowOpacity >= 0.999f;
|
||||||
|
var childBgAlpha = alphaPreserved ? (themeChildBgRgba & 0xFFu) : 0u;
|
||||||
|
return (themeChildBgRgba & 0xFFFFFF00u) | childBgAlpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using Dalamud.Bindings.ImGui;
|
|||||||
using Dalamud.Interface.Style;
|
using Dalamud.Interface.Style;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ internal class Popout : Window
|
|||||||
private readonly ChatLogWindow ChatLogWindow;
|
private readonly ChatLogWindow ChatLogWindow;
|
||||||
private readonly Tab Tab;
|
private readonly Tab Tab;
|
||||||
private readonly int Idx;
|
private readonly int Idx;
|
||||||
|
private readonly ILogger<Popout> _logger;
|
||||||
|
|
||||||
private long FrameTime;
|
private long FrameTime;
|
||||||
private long LastActivityTime = Environment.TickCount64;
|
private long LastActivityTime = Environment.TickCount64;
|
||||||
@@ -23,12 +25,13 @@ internal class Popout : Window
|
|||||||
// Exposed so AutoTellTabsService can locate this window during LRU eviction.
|
// Exposed so AutoTellTabsService can locate this window during LRU eviction.
|
||||||
internal Guid TabIdentifier => Tab.Identifier;
|
internal Guid TabIdentifier => Tab.Identifier;
|
||||||
|
|
||||||
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx)
|
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx, ILogger<Popout> logger)
|
||||||
: base($"{tab.Name}##popout")
|
: base($"{tab.Name}##popout")
|
||||||
{
|
{
|
||||||
ChatLogWindow = chatLogWindow;
|
ChatLogWindow = chatLogWindow;
|
||||||
Tab = tab;
|
Tab = tab;
|
||||||
Idx = idx;
|
Idx = idx;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
Size = new Vector2(350, 350);
|
Size = new Vector2(350, 350);
|
||||||
SizeCondition = ImGuiCond.FirstUseEver;
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
@@ -175,7 +178,7 @@ internal class Popout : Window
|
|||||||
{
|
{
|
||||||
Plugin.Config.SeenPopOutInputHint = true;
|
Plugin.Config.SeenPopOutInputHint = true;
|
||||||
ChatLogWindow.Plugin.SaveConfig();
|
ChatLogWindow.Plugin.SaveConfig();
|
||||||
Plugin.Log.Debug("Pop-Out input hint dismissed");
|
_logger.LogDebug("Pop-Out input hint dismissed");
|
||||||
if (openSettings)
|
if (openSettings)
|
||||||
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
||||||
}
|
}
|
||||||
@@ -214,13 +217,13 @@ internal class Popout : Window
|
|||||||
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: None -> Battle");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -232,7 +235,7 @@ internal class Popout : Window
|
|||||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +245,7 @@ internal class Popout : Window
|
|||||||
&& !Plugin.GposeActive
|
&& !Plugin.GposeActive
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Verbose(
|
_logger.LogTrace(
|
||||||
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
|
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
|
||||||
);
|
);
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
@@ -251,7 +254,7 @@ internal class Popout : Window
|
|||||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
Plugin.Log.Verbose(
|
_logger.LogTrace(
|
||||||
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
|
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -259,7 +262,7 @@ internal class Popout : Window
|
|||||||
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
||||||
{
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
_logger.LogTrace($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
||||||
}
|
}
|
||||||
|
|
||||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
|
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
|
||||||
|
|||||||
@@ -29,21 +29,13 @@ public class SeStringDebugger : Window
|
|||||||
|
|
||||||
RespectCloseHotkey = false;
|
RespectCloseHotkey = false;
|
||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
#if DEBUG
|
// Slash-command tear-down moved to Plugin.TearDownCommands.
|
||||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Toggle(string _, string __) => Toggle();
|
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
if (Plugin.MessageManager.LastMessage.Sender == null)
|
if (Plugin.MessageManager.LastMessage.Sender == null)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Dalamud.Utility;
|
|||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Ui.SettingsTabs;
|
using HellionChat.Ui.SettingsTabs;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
private SettingsView View = SettingsView.Overview;
|
private SettingsView View = SettingsView.Overview;
|
||||||
private readonly SettingsOverview Overview;
|
private readonly SettingsOverview Overview;
|
||||||
|
|
||||||
internal SettingsWindow(Plugin plugin)
|
internal SettingsWindow(Plugin plugin, ILoggerFactory loggerFactory)
|
||||||
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
||||||
{
|
{
|
||||||
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
|
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
|
||||||
@@ -45,13 +46,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
Tabs =
|
Tabs =
|
||||||
[
|
[
|
||||||
new General(Plugin, Mutable),
|
new General(Plugin, Mutable),
|
||||||
new ThemeAndLayout(Plugin, Mutable),
|
new ThemeAndLayout(Plugin, Mutable, loggerFactory.CreateLogger<ThemeAndLayout>()),
|
||||||
new FontsAndColours(Plugin, Mutable),
|
new FontsAndColours(Plugin, Mutable, loggerFactory.CreateLogger<FontsAndColours>()),
|
||||||
new SettingsTabs.Window(Plugin, Mutable),
|
new SettingsTabs.Window(Plugin, Mutable),
|
||||||
new Chat(Plugin, Mutable),
|
new Chat(Plugin, Mutable),
|
||||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||||
new SettingsTabs.Privacy(Plugin, Mutable),
|
new SettingsTabs.Privacy(Plugin, Mutable),
|
||||||
new DataManagement(Plugin, Mutable),
|
new DataManagement(Plugin, Mutable, loggerFactory.CreateLogger<DataManagement>()),
|
||||||
new SettingsTabs.Integrations(Plugin, Mutable),
|
new SettingsTabs.Integrations(Plugin, Mutable),
|
||||||
new Information(Mutable),
|
new Information(Mutable),
|
||||||
];
|
];
|
||||||
@@ -60,23 +61,11 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
DisableWindowSounds = true;
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
Initialise();
|
Initialise();
|
||||||
|
|
||||||
Plugin
|
|
||||||
.Commands.Register("/hellion", "Perform various actions with Hellion Chat.")
|
|
||||||
.Execute += Command;
|
|
||||||
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
|
// Slash-command + OpenConfigUi tear-down moved to Plugin.TearDownCommands.
|
||||||
Plugin.Commands.Register("/hellion").Execute -= Command;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Command(string command, string args)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(args))
|
|
||||||
Toggle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Initialise()
|
private void Initialise()
|
||||||
@@ -199,12 +188,12 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (ImGui.Button(buttonLabel2))
|
if (ImGui.Button(buttonLabel2))
|
||||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
|
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/infiii");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
||||||
if (ImGui.Button(buttonLabel))
|
if (ImGui.Button(buttonLabel))
|
||||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/lojewalo");
|
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/lojewalo");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!save)
|
if (!save)
|
||||||
@@ -240,7 +229,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fontChanged || fontSizeChanged || italicStateChanged)
|
if (fontChanged || fontSizeChanged || italicStateChanged)
|
||||||
Plugin.FontManager.BuildFonts();
|
Plugin.FontManager.RebuildDelegateFonts();
|
||||||
|
|
||||||
if (languageChanged)
|
if (languageChanged)
|
||||||
Plugin.LanguageChanged(Plugin.Interface.UiLanguage);
|
Plugin.LanguageChanged(Plugin.Interface.UiLanguage);
|
||||||
|
|||||||
@@ -79,11 +79,13 @@ internal sealed class SettingsOverview
|
|||||||
// 110f accommodates two-line subtexts; wrap width is matched in DrawCard.
|
// 110f accommodates two-line subtexts; wrap width is matched in DrawCard.
|
||||||
var cardHeight = 110f;
|
var cardHeight = 110f;
|
||||||
|
|
||||||
|
// One draw-list lookup per frame instead of one per card.
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
var cardDefs = BuildCardDefs();
|
var cardDefs = BuildCardDefs();
|
||||||
for (var i = 0; i < cardDefs.Length; i++)
|
for (var i = 0; i < cardDefs.Length; i++)
|
||||||
{
|
{
|
||||||
var (icon, title, subtext) = cardDefs[i];
|
var (icon, title, subtext) = cardDefs[i];
|
||||||
DrawCard(i, icon, title, subtext, cardWidth, cardHeight);
|
DrawCard(i, icon, title, subtext, cardWidth, cardHeight, drawList);
|
||||||
|
|
||||||
if ((i + 1) % columns != 0 && i != cardDefs.Length - 1)
|
if ((i + 1) % columns != 0 && i != cardDefs.Length - 1)
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -96,7 +98,8 @@ internal sealed class SettingsOverview
|
|||||||
string title,
|
string title,
|
||||||
string subtext,
|
string subtext,
|
||||||
float w,
|
float w,
|
||||||
float h
|
float h,
|
||||||
|
ImDrawListPtr drawList
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
// BeginGroup makes the card a single layout item so SameLine works
|
// BeginGroup makes the card a single layout item so SameLine works
|
||||||
@@ -108,8 +111,7 @@ internal sealed class SettingsOverview
|
|||||||
var hovered = ImGui.IsItemHovered();
|
var hovered = ImGui.IsItemHovered();
|
||||||
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
||||||
|
|
||||||
var draw = ImGui.GetWindowDrawList();
|
drawList.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||||
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
|
||||||
|
|
||||||
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
||||||
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
||||||
@@ -120,15 +122,15 @@ internal sealed class SettingsOverview
|
|||||||
|
|
||||||
using (_window.Plugin.FontManager.FontAwesome.Push())
|
using (_window.Plugin.FontManager.FontAwesome.Push())
|
||||||
{
|
{
|
||||||
draw.AddText(iconPos, titleColor, icon.ToIconString());
|
drawList.AddText(iconPos, titleColor, icon.ToIconString());
|
||||||
}
|
}
|
||||||
|
|
||||||
draw.AddText(titlePos, titleColor, title);
|
drawList.AddText(titlePos, titleColor, title);
|
||||||
|
|
||||||
// Subtext wraps at card inner width (16px padding each side) via DrawList
|
// Subtext wraps at card inner width (16px padding each side) via DrawList
|
||||||
// to avoid expanding the group bounds and breaking SameLine in the card row.
|
// to avoid expanding the group bounds and breaking SameLine in the card row.
|
||||||
var subtextWrapWidth = w - 32f;
|
var subtextWrapWidth = w - 32f;
|
||||||
draw.AddText(
|
drawList.AddText(
|
||||||
ImGui.GetFont(),
|
ImGui.GetFont(),
|
||||||
ImGui.GetFontSize(),
|
ImGui.GetFontSize(),
|
||||||
subtextPos,
|
subtextPos,
|
||||||
|
|||||||
@@ -139,6 +139,12 @@ internal sealed class Chat : ISettingsTab
|
|||||||
);
|
);
|
||||||
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMsgUniqueLink_Description);
|
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMsgUniqueLink_Description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.Checkbox(
|
||||||
|
HellionStrings.Settings_Chat_SymbolPicker_Enable_Name,
|
||||||
|
ref Mutable.SymbolPickerEnabled
|
||||||
|
);
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Chat_SymbolPicker_Enable_Description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using HellionChat.Export;
|
|||||||
using HellionChat.Privacy;
|
using HellionChat.Privacy;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui.SettingsTabs;
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
{
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
|
private readonly ILogger<DataManagement> _logger;
|
||||||
|
|
||||||
public string Name =>
|
public string Name =>
|
||||||
HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement";
|
HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement";
|
||||||
@@ -136,10 +138,11 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
internal DataManagement(Plugin plugin, Configuration mutable)
|
internal DataManagement(Plugin plugin, Configuration mutable, ILogger<DataManagement> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
@@ -229,7 +232,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Unable to delete old database");
|
_logger.LogError(e, "Unable to delete old database");
|
||||||
WrapperUtil.AddNotification(
|
WrapperUtil.AddNotification(
|
||||||
Language.Options_Database_Old_Delete_Error,
|
Language.Options_Database_Old_Delete_Error,
|
||||||
NotificationType.Error
|
NotificationType.Error
|
||||||
@@ -391,7 +394,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
|
|
||||||
Plugin.Log.Information($"Manual retention run deleted {deleted} expired messages.");
|
_logger.LogInformation($"Manual retention run deleted {deleted} expired messages.");
|
||||||
|
|
||||||
if (deleted > 0)
|
if (deleted > 0)
|
||||||
{
|
{
|
||||||
@@ -405,7 +408,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
.Wait(TimeSpan.FromSeconds(5))
|
.Wait(TimeSpan.FromSeconds(5))
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(
|
_logger.LogWarning(
|
||||||
"Retention sweep: framework refresh timed out after 5s."
|
"Retention sweep: framework refresh timed out after 5s."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -418,7 +421,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Manual retention run failed");
|
_logger.LogError(e, "Manual retention run failed");
|
||||||
WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -566,7 +569,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Failed to compute cleanup preview");
|
_logger.LogError(e, "Failed to compute cleanup preview");
|
||||||
WrapperUtil.AddNotification(
|
WrapperUtil.AddNotification(
|
||||||
HellionStrings.Cleanup_PreviewError,
|
HellionStrings.Cleanup_PreviewError,
|
||||||
NotificationType.Error
|
NotificationType.Error
|
||||||
@@ -587,7 +590,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
||||||
Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages");
|
_logger.LogInformation($"Privacy cleanup: deleted {deleted} messages");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!Plugin
|
!Plugin
|
||||||
@@ -599,7 +602,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
.Wait(TimeSpan.FromSeconds(5))
|
.Wait(TimeSpan.FromSeconds(5))
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
_logger.LogWarning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapperUtil.AddNotification(
|
WrapperUtil.AddNotification(
|
||||||
@@ -609,7 +612,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Privacy cleanup failed");
|
_logger.LogError(e, "Privacy cleanup failed");
|
||||||
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -769,7 +772,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(e, "Export failed");
|
_logger.LogError(e, "Export failed");
|
||||||
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -849,7 +852,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Clearing messages from database");
|
_logger.LogWarning("Clearing messages from database");
|
||||||
Plugin.MessageManager.Store.ClearMessages();
|
Plugin.MessageManager.Store.ClearMessages();
|
||||||
Plugin.MessageManager.ClearAllTabs();
|
Plugin.MessageManager.ClearAllTabs();
|
||||||
|
|
||||||
@@ -907,7 +910,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
|
|
||||||
private void InsertMessages(int count)
|
private void InsertMessages(int count)
|
||||||
{
|
{
|
||||||
Plugin.Log.Info($"Inserting {count} messages due to user request");
|
_logger.LogInformation($"Inserting {count} messages due to user request");
|
||||||
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
var playerName = Plugin.PlayerState.CharacterName;
|
var playerName = Plugin.PlayerState.CharacterName;
|
||||||
@@ -952,7 +955,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
|
|
||||||
var elapsedTicks = stopwatch.ElapsedTicks;
|
var elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -962,7 +965,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
|
|
||||||
elapsedTicks = stopwatch.ElapsedTicks;
|
elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -973,7 +976,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
Plugin.MessageManager.ClearAllTabs();
|
Plugin.MessageManager.ClearAllTabs();
|
||||||
elapsedTicks = stopwatch.ElapsedTicks;
|
elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -986,7 +989,7 @@ internal sealed class DataManagement : ISettingsTab
|
|||||||
Plugin.MessageManager.FilterAllTabs();
|
Plugin.MessageManager.FilterAllTabs();
|
||||||
elapsedTicks = stopwatch.ElapsedTicks;
|
elapsedTicks = stopwatch.ElapsedTicks;
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
Plugin.Log.Info(
|
_logger.LogInformation(
|
||||||
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui.SettingsTabs;
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
@@ -14,14 +15,16 @@ internal sealed class FontsAndColours : ISettingsTab
|
|||||||
{
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
|
private readonly ILogger<FontsAndColours> _logger;
|
||||||
|
|
||||||
public string Name =>
|
public string Name =>
|
||||||
HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours";
|
HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours";
|
||||||
|
|
||||||
internal FontsAndColours(Plugin plugin, Configuration mutable)
|
internal FontsAndColours(Plugin plugin, Configuration mutable, ILogger<FontsAndColours> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
@@ -312,6 +315,6 @@ internal sealed class FontsAndColours : ISettingsTab
|
|||||||
}
|
}
|
||||||
Plugin.SaveConfig();
|
Plugin.SaveConfig();
|
||||||
GlobalParametersCache.Refresh();
|
GlobalParametersCache.Refresh();
|
||||||
Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
_logger.LogDebug($"Applied chat colour preset: {preset.DisplayName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Dalamud.Interface;
|
|||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using HellionChat.Branding;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
|
||||||
@@ -58,6 +59,8 @@ internal sealed class Information : ISettingsTab
|
|||||||
{
|
{
|
||||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||||
|
|
||||||
|
DrawHellionForgeSection();
|
||||||
|
ImGui.Spacing();
|
||||||
DrawVersionInfoSection();
|
DrawVersionInfoSection();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
DrawAboutSection();
|
DrawAboutSection();
|
||||||
@@ -65,6 +68,20 @@ internal sealed class Information : ISettingsTab
|
|||||||
DrawChangelogSection();
|
DrawChangelogSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provenance anchor — folded by default so the tab opens to the
|
||||||
|
// version-info section as before. Expands to show the full Hellion
|
||||||
|
// Forge silhouette in monospace.
|
||||||
|
private void DrawHellionForgeSection()
|
||||||
|
{
|
||||||
|
using var tree = ImRaii.TreeNode("Hellion Forge");
|
||||||
|
if (!tree.Success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
|
using (Plugin.Interface.UiBuilder.MonoFontHandle.Push())
|
||||||
|
ImGui.TextUnformatted(HellionForgeAscii.FoxBanner);
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawVersionInfoSection()
|
private void DrawVersionInfoSection()
|
||||||
{
|
{
|
||||||
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_VersionInfo_Heading);
|
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Information_VersionInfo_Heading);
|
||||||
@@ -97,7 +114,7 @@ internal sealed class Information : ISettingsTab
|
|||||||
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
||||||
Dalamud.Utility.Util.OpenLink(
|
Plugin.PlatformUtil.OpenLink(
|
||||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues"
|
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -116,7 +133,7 @@ internal sealed class Information : ISettingsTab
|
|||||||
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
|
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
||||||
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
|
Plugin.PlatformUtil.OpenLink("https://hellion-media.de");
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10.0f);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
@@ -137,7 +154,7 @@ internal sealed class Information : ISettingsTab
|
|||||||
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
||||||
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
Plugin.PlatformUtil.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10.0f);
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ internal sealed class Integrations : ISettingsTab
|
|||||||
{
|
{
|
||||||
ImGui.TextWrapped(HellionStrings.Settings_Integrations_Honorific_ToggleHint);
|
ImGui.TextWrapped(HellionStrings.Settings_Integrations_Honorific_ToggleHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
ImGui.Checkbox(
|
||||||
|
HellionStrings.Settings_Integrations_Honorific_Glow_Toggle,
|
||||||
|
ref Mutable.ShowHonorificGlow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Integrations_Honorific_Glow_Hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Honorific has no LICENSE in its repo so we link upstream and author
|
// Honorific has no LICENSE in its repo so we link upstream and author
|
||||||
@@ -79,12 +90,12 @@ internal sealed class Integrations : ISettingsTab
|
|||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo))
|
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo))
|
||||||
{
|
{
|
||||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificRepo);
|
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificRepo);
|
||||||
}
|
}
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkAuthor))
|
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkAuthor))
|
||||||
{
|
{
|
||||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificAuthor);
|
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificAuthor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +204,7 @@ internal sealed class Integrations : ISettingsTab
|
|||||||
|
|
||||||
if (ImGui.Button(HellionStrings.Settings_Integrations_GotAnIdea_LinkLabel))
|
if (ImGui.Button(HellionStrings.Settings_Integrations_GotAnIdea_LinkLabel))
|
||||||
{
|
{
|
||||||
Dalamud.Utility.Util.OpenLink(BrandingLinks.HellionForgeDiscordInvite);
|
Plugin.PlatformUtil.OpenLink(BrandingLinks.HellionForgeDiscordInvite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Themes;
|
using HellionChat.Themes;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ui.SettingsTabs;
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
@@ -11,16 +12,18 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
|||||||
{
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
|
private readonly ILogger<ThemeAndLayout> _logger;
|
||||||
|
|
||||||
private string? _applyDismissedFor;
|
private string? _applyDismissedFor;
|
||||||
|
|
||||||
public string Name =>
|
public string Name =>
|
||||||
HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout";
|
HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout";
|
||||||
|
|
||||||
internal ThemeAndLayout(Plugin plugin, Configuration mutable)
|
internal ThemeAndLayout(Plugin plugin, Configuration mutable, ILogger<ThemeAndLayout> logger)
|
||||||
{
|
{
|
||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw(bool changed)
|
public void Draw(bool changed)
|
||||||
@@ -78,7 +81,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
|||||||
{
|
{
|
||||||
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
Dalamud.Utility.Util.OpenLink(dir);
|
Plugin.PlatformUtil.OpenLink(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -90,7 +93,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
|||||||
var path = Path.Combine(dir, fileName);
|
var path = Path.Combine(dir, fileName);
|
||||||
var json = ThemeJsonWriter.Serialize(active);
|
var json = ThemeJsonWriter.Serialize(active);
|
||||||
File.WriteAllText(path, json);
|
File.WriteAllText(path, json);
|
||||||
Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}");
|
_logger.LogInformation($"Exported active theme '{active.Slug}' to {path}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,6 +253,26 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
|||||||
string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)
|
string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Mutable.SidebarTabView)
|
||||||
|
{
|
||||||
|
var sidebarWidth = Mutable.SidebarWidth;
|
||||||
|
if (
|
||||||
|
ImGui.SliderInt(
|
||||||
|
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Name,
|
||||||
|
ref sidebarWidth,
|
||||||
|
44,
|
||||||
|
160,
|
||||||
|
$"{sidebarWidth} px"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Mutable.SidebarWidth = sidebarWidth;
|
||||||
|
}
|
||||||
|
ImGuiUtil.HelpMarker(
|
||||||
|
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Globalization;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
@@ -9,12 +10,20 @@ using HellionChat.Util;
|
|||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
// Bottom status bar, 22px tall. Slots left to right: channel indicator,
|
// Bottom status bar. Slots left to right: channel indicator, privacy badge,
|
||||||
// privacy badge, counts, tells (hidden at 0), version (right-aligned).
|
// counts, tells (hidden at 0), version (right-aligned). Updates at 1Hz;
|
||||||
// Updates at 1Hz; format strings are cached between updates.
|
// format strings are cached between updates.
|
||||||
internal sealed class StatusBar
|
internal sealed class StatusBar
|
||||||
{
|
{
|
||||||
public const float Height = 22f;
|
// DPI-aware bar height. The previous fixed 22px constant clipped on
|
||||||
|
// Windows display-scaling >100% because ImGui renders the font bigger
|
||||||
|
// than the reservation. GetTextLineHeightWithSpacing scales with the
|
||||||
|
// current ImGui font; the 2px spacer is GlobalScale-rounded to stay
|
||||||
|
// on integer pixel boundaries (same idiom as v1.4.6 F7.2 underline-pill
|
||||||
|
// in ChatLogWindow.cs:1639-1653).
|
||||||
|
public static float Height =>
|
||||||
|
ImGui.GetTextLineHeightWithSpacing() + MathF.Round(2f * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
private const long UpdateIntervalMs = 1000;
|
private const long UpdateIntervalMs = 1000;
|
||||||
|
|
||||||
// Initially outdated so the first frame always computes fresh.
|
// Initially outdated so the first frame always computes fresh.
|
||||||
@@ -144,14 +153,20 @@ internal sealed class StatusBar
|
|||||||
ImGui.TextUnformatted(_cachedTellsText);
|
ImGui.TextUnformatted(_cachedTellsText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slot 5: version, right-aligned, muted
|
// Slot 5: version, right-aligned, muted. Hidden when the window is
|
||||||
|
// too narrow to fit all five slots — the other four need ~200 px
|
||||||
|
// before the version text starts clipping into them.
|
||||||
var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
|
var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
|
||||||
var versionWidth = ImGui.CalcTextSize(versionText).X;
|
var versionWidth = ImGui.CalcTextSize(versionText).X;
|
||||||
var contentRegionMax = ImGui.GetContentRegionMax().X;
|
var contentRegionMax = ImGui.GetContentRegionMax().X;
|
||||||
ImGui.SameLine(contentRegionMax - versionWidth);
|
const float MinOtherSlotsWidth = 200f;
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
if (contentRegionMax - versionWidth > MinOtherSlotsWidth)
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(versionText);
|
ImGui.SameLine(contentRegionMax - versionWidth);
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(versionText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user