Compare commits
246 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 | |||
| 61d5a33683 | |||
| 7ed689587b | |||
| 612bf8814f | |||
| be17472cd5 | |||
| 8bf50151d5 | |||
| 57da455700 | |||
| 0982b68a4a | |||
| 0fc88e480a | |||
| 7eb50e2c8d | |||
| 58e754c169 | |||
| 83064cd40b | |||
| 5ca3b73b7f | |||
| 570a6f071c | |||
| 11ad5db127 | |||
| 5c550e8587 | |||
| eb2a04c56b | |||
| 3f714d6f38 | |||
| 747e0e1574 | |||
| debfdcd278 | |||
| f85daf3dbe | |||
| 3b24b2adc4 | |||
| c493340104 | |||
| 3a7f9b3adb | |||
| b1b6402827 | |||
| 7d73def53d | |||
| c4c85cf4b8 | |||
| a37882893e | |||
| 702e4ca160 | |||
| 1ebc7b820f | |||
| 3152312890 | |||
| 4000bbd199 | |||
| 3cabdf3e15 | |||
| 05c28f7e92 | |||
| 699d4ede1d | |||
| 31673fdff6 | |||
| 07337108bc | |||
| fd82033666 | |||
| cd01fa63a1 | |||
| b81c50b433 | |||
| 355a57089b | |||
| cf7ab6226c | |||
| 03da6d58a4 | |||
| 90a4544ab2 | |||
| 9b4557f197 | |||
| e594258cf3 | |||
| bb863c5b32 | |||
| 0797d1a517 | |||
| 8dc8b87580 | |||
| baeec369e6 | |||
| a1f2b22b19 | |||
| 5931f2f301 | |||
| 0b25df0ea7 | |||
| b75c7b177a | |||
| ccc5a4e17a | |||
| daa800c8b1 | |||
| a531973c0d | |||
| 4c8b0da3da | |||
| 9a8a014795 | |||
| 9640d336a6 | |||
| 12ce015d83 | |||
| f455bf4736 | |||
| 9bc66c7cf3 | |||
| e9022de150 | |||
| cb327b8073 | |||
| 1c354d18bb | |||
| 0ed88691c2 | |||
| c64fcfd4d1 | |||
| 6689cdb968 | |||
| 345aa3ea2a | |||
| 1ffc41f97d | |||
| 36b92f0520 | |||
| cb612044ea | |||
| 71081d8344 | |||
| 54bfeb0f6f | |||
| 5f83c70292 | |||
| 3d7883ee01 | |||
| e4ee7aaafa | |||
| aff2528a6f | |||
| 0d2ee63420 | |||
| de9d1ac60b | |||
| 19f7099af0 | |||
| f8a734d93f | |||
| 3f7e86b32e | |||
| e5bf375b42 | |||
| 93329087a9 | |||
| 72d568e5b3 | |||
| c9dfd024b2 | |||
| 8c624a0032 | |||
| 079e280226 | |||
| 3bdf45c29c | |||
| d257a41660 | |||
| 36f2bbd8d1 | |||
| da291b7fca | |||
| c8485233d5 | |||
| 2d768e4edb | |||
| e58376bf50 | |||
| dceb028184 | |||
| 33a4d94c44 | |||
| b2f158f893 | |||
| da6da32651 | |||
| 477591e2fa | |||
| ddb293399e | |||
| 7494b001a2 | |||
| 9f0a40bedc | |||
| 8da05c3080 | |||
| 5b5f52f86e | |||
| af3caa9b96 | |||
| 206b25b8d6 | |||
| 00deef01a4 | |||
| 74e2c655f0 | |||
| fa91c4e847 | |||
| 1125caabca | |||
| eead8d813c | |||
| 28b20ad6d3 | |||
| a88ec1714d | |||
| 0110295e7d | |||
| 9752206996 | |||
| 2f4e4c33ca | |||
| b30b6b135c | |||
| df0844b737 | |||
| 21d703bf0b | |||
| 4048f0b8d0 | |||
| 2d0e9ae70c | |||
| eaf11dcebe | |||
| 9bd8262191 | |||
| ddb00a0836 | |||
| aec8ba15f2 | |||
| c84eae199b | |||
| 9ead8098f5 | |||
| b190456005 | |||
| ebc0999a8e | |||
| c0b3edb20c |
+243
-147
@@ -1,156 +1,252 @@
|
|||||||
|
# ##############################################################
|
||||||
|
# #
|
||||||
|
# # .editorconfig – Hellion Forge / Hellion Media
|
||||||
|
# #
|
||||||
|
# # Überarbeitet: Mai 2026
|
||||||
|
# #
|
||||||
|
# # Strategie:
|
||||||
|
# # - Standard-.NET-Conventions (private Fields = _camelCase)
|
||||||
|
# # - CSharpier übernimmt die meiste Formatierung
|
||||||
|
# # - Hier: Naming, IDE-Hints, Backup-Format-Regeln
|
||||||
|
# #
|
||||||
|
# # ##############################################################
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Defaults (alle Files)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
indent_style=space
|
indent_style = space
|
||||||
tab_width=4
|
tab_width = 4
|
||||||
indent_size=4
|
indent_size = 4
|
||||||
trim_trailing_whitespace=true
|
charset = utf-8
|
||||||
insert_final_newline=false
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
# JetBrains Rider custom properties for code formatting styles
|
|
||||||
resharper_csharp_brace_style=next_line
|
|
||||||
|
|
||||||
resharper_csharp_braces_for_foreach=not_required
|
# =====================================================
|
||||||
resharper_csharp_braces_for_for=not_required
|
# Markdown: Trailing Spaces erlaubt (2 Spaces = <br>)
|
||||||
resharper_csharp_braces_for_while=not_required
|
# =====================================================
|
||||||
charset=utf-8
|
|
||||||
end_of_line=crlf
|
|
||||||
|
|
||||||
# Microsoft .NET properties
|
[*.md]
|
||||||
csharp_new_line_before_members_in_object_initializers=false
|
trim_trailing_whitespace = false
|
||||||
csharp_preferred_modifier_order=public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
|
|
||||||
csharp_style_prefer_utf8_string_literals=true:suggestion
|
|
||||||
csharp_style_var_elsewhere=true:suggestion
|
|
||||||
csharp_style_var_for_built_in_types=true:suggestion
|
|
||||||
csharp_style_var_when_type_is_apparent=true:suggestion
|
|
||||||
dotnet_naming_rule.private_constants_rule.import_to_resharper=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
|
|
||||||
resharper_autodetect_indent_settings=true
|
|
||||||
resharper_cpp_insert_final_newline=true
|
|
||||||
resharper_csharp_insert_final_newline=false
|
|
||||||
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
|
# JSON / YAML / Web-Configs: 2-Space-Indent
|
||||||
resharper_arrange_this_qualifier_highlighting=hint
|
# Konsistent mit yamllint und Prettier-Override
|
||||||
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,.stylelintrc,bowerrc,jest.config}]
|
[*.{yaml,yml}]
|
||||||
indent_style=space
|
indent_size = 2
|
||||||
indent_size=2
|
|
||||||
|
|
||||||
[{*.yaml,*.yml}]
|
[*.{json,jsonc,har,jsb2,jsb3,postman_collection,postman_environment}]
|
||||||
indent_style=space
|
indent_size = 2
|
||||||
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}]
|
[{.babelrc,.eslintrc,.prettierrc,.markdownlintrc,.stylelintrc,bowerrc}]
|
||||||
indent_style=space
|
indent_size = 2
|
||||||
indent_size=4
|
|
||||||
tab_width=4
|
|
||||||
|
# =====================================================
|
||||||
|
# .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_else = true
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# C# Format – Switch-Einrückung
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
csharp_indent_case_contents = true
|
||||||
|
csharp_indent_switch_labels = true
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# .NET Style – Qualification (kein "this." nötig)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
dotnet_style_qualification_for_field = false:suggestion
|
||||||
|
dotnet_style_qualification_for_property = false:suggestion
|
||||||
|
dotnet_style_qualification_for_method = false:suggestion
|
||||||
|
dotnet_style_qualification_for_event = false:suggestion
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# .NET Style – Predefined Types (int statt Int32 etc.)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# .NET Style – Parentheses
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
+178
-2
@@ -1,2 +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
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Source Code (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
|
||||||
@@ -3,6 +3,12 @@ name: Build
|
|||||||
# Verifies that every push to main and every PR still builds against the
|
# Verifies that every push to main and every PR still builds against the
|
||||||
# current Dalamud staging branch. Does not produce release artefacts; the
|
# current Dalamud staging branch. Does not produce release artefacts; the
|
||||||
# release workflow handles that on tag.
|
# release workflow handles that on tag.
|
||||||
|
#
|
||||||
|
# Linux runner: gitea.com Cloud Actions provides ubuntu-latest. The plugin
|
||||||
|
# csproj targets net10.0-windows, but `dotnet build` cross-compiles on
|
||||||
|
# Linux as long as the Dalamud staging assemblies are present at the
|
||||||
|
# expected lookup path ($(HOME)/.xlcore/dalamud/Hooks/dev/, which the
|
||||||
|
# Dalamud SDK 15 uses on Linux).
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -21,36 +27,27 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build (Release)
|
name: Build (Release)
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@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
|
||||||
shell: pwsh
|
|
||||||
run: |
|
run: |
|
||||||
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||||
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
mkdir -p "$hooks"
|
||||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $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
|
||||||
|
|
||||||
- name: Upload build output
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: HellionChat-build-${{ github.run_number }}
|
|
||||||
path: HellionChat/bin/Release/**/HellionChat/**
|
|
||||||
if-no-files-found: warn
|
|
||||||
retention-days: 14
|
|
||||||
@@ -34,10 +34,9 @@ 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 lives under Settings → Environments
|
# The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret
|
||||||
# → Webhook (case-sensitive). Without this declaration the secret is
|
# on Gitea (Settings → Actions → Secrets). Repo-level secrets are in
|
||||||
# not in scope for the job.
|
# scope for every job by default, no environment: declaration needed.
|
||||||
environment: Webhook
|
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -45,7 +44,7 @@ jobs:
|
|||||||
# 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 }}
|
||||||
|
|
||||||
@@ -102,16 +101,16 @@ jobs:
|
|||||||
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
||||||
$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(2) } else { $_ }
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
}) -join "`n"
|
}) -join "`n"
|
||||||
|
|
||||||
$header = "**Hellion Chat $version"
|
$header = "**v$version "
|
||||||
$start = $changelogBody.IndexOf($header)
|
$start = $changelogBody.IndexOf($header)
|
||||||
if ($start -lt 0) {
|
if ($start -lt 0) {
|
||||||
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
||||||
}
|
}
|
||||||
$rest = $changelogBody.Substring($start)
|
$rest = $changelogBody.Substring($start)
|
||||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 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)) {
|
||||||
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||||
@@ -121,20 +120,40 @@ jobs:
|
|||||||
$enBlock = $rest.TrimEnd()
|
$enBlock = $rest.TrimEnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
# ---------- Embed-Felder + Per-Field-Caps (Discord-Hard-Limits) ----------
|
||||||
|
# Discord enforces per-embed-field limits separately from the
|
||||||
|
# combined-total limit. We split the DE and EN blocks into two
|
||||||
|
# embeds that share the same release URL so Discord stitches
|
||||||
|
# them into one visual card. Hard caps per Discord docs:
|
||||||
|
# description: 4096 per embed
|
||||||
|
# title: 256 per embed
|
||||||
|
# footer.text: 2048 per embed
|
||||||
|
# combined sum across all embeds: 6000
|
||||||
$title = "Hellion Chat $version — $subtitle"
|
$title = "Hellion Chat $version — $subtitle"
|
||||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
$deDesc = "**Deutsch**`n`n$deBody"
|
||||||
|
$enDesc = "**English**`n`n$enBlock"
|
||||||
$footerText = "Hellion Forge · $versionsnatur"
|
$footerText = "Hellion Forge · $versionsnatur"
|
||||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
$releaseUrl = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||||
if ($totalChars -gt 5500) {
|
|
||||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
|
||||||
}
|
|
||||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
|
||||||
|
|
||||||
# ---------- Embed-Payload bauen ----------
|
if ($deDesc.Length -gt 4096) {
|
||||||
|
throw "V6a: DE-Body too long for one embed ($($deDesc.Length) chars, max 4096). Trim .github/forge-posts/$tag.md or post the announcement manually (see forge style §8)."
|
||||||
|
}
|
||||||
|
if ($enDesc.Length -gt 4096) {
|
||||||
|
throw "V6b: EN-Block too long for one embed ($($enDesc.Length) chars, max 4096). Trim the changelog entry in HellionChat/HellionChat.yaml or post manually."
|
||||||
|
}
|
||||||
|
$totalChars = $title.Length + $deDesc.Length + $enDesc.Length + $footerText.Length
|
||||||
|
if ($totalChars -gt 6000) {
|
||||||
|
throw "V6c: Combined embed chars $totalChars exceed Discord's 6000-total limit. Major-Release detected — post manually via Bot/Multi-Embed (see forge style §8)."
|
||||||
|
}
|
||||||
|
Write-Host "Embed-Caps OK: de=$($deDesc.Length)/4096, en=$($enDesc.Length)/4096, total=$totalChars/6000"
|
||||||
|
|
||||||
|
# ---------- Embed-Payload bauen (zwei Embeds, gleiche url) ----------
|
||||||
|
# Sharing the same `url` tells Discord to render both embeds as a
|
||||||
|
# single contiguous card block. The title sits on the first embed,
|
||||||
|
# the footer + timestamp on the last so it reads as one post.
|
||||||
$payload = [ordered]@{
|
$payload = [ordered]@{
|
||||||
username = "Forge Herald"
|
username = "Forge Herald"
|
||||||
avatar_url = "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png"
|
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||||
content = "<@&1500489631555260446>"
|
content = "<@&1500489631555260446>"
|
||||||
allowed_mentions = [ordered]@{
|
allowed_mentions = [ordered]@{
|
||||||
parse = @()
|
parse = @()
|
||||||
@@ -143,9 +162,14 @@ jobs:
|
|||||||
embeds = @(
|
embeds = @(
|
||||||
[ordered]@{
|
[ordered]@{
|
||||||
title = $title
|
title = $title
|
||||||
url = "https://github.com/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
url = $releaseUrl
|
||||||
color = 12730636
|
color = 12730636
|
||||||
description = $description
|
description = $deDesc
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
url = $releaseUrl
|
||||||
|
color = 12730636
|
||||||
|
description = $enDesc
|
||||||
footer = [ordered]@{ text = $footerText }
|
footer = [ordered]@{ text = $footerText }
|
||||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||||
}
|
}
|
||||||
@@ -2,30 +2,30 @@ name: Release
|
|||||||
|
|
||||||
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
|
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
|
||||||
# current Dalamud staging branch, locates the latest.zip produced by
|
# current Dalamud staging branch, locates the latest.zip produced by
|
||||||
# DalamudPackager and attaches it to the matching GitHub Release.
|
# DalamudPackager and attaches it to the matching Gitea Release.
|
||||||
#
|
#
|
||||||
# User-controlled inputs touched by this workflow:
|
# User-controlled inputs touched by this workflow:
|
||||||
# - the tag name (filtered by on.tags = v*, validated again at runtime
|
# - the tag name (filtered by on.tags = v*, validated again at runtime
|
||||||
# against ^v\d+\.\d+\.\d+$ before being used in any string)
|
# against ^v\d+\.\d+\.\d+$ before being used in any string)
|
||||||
# All other values are either repo-controlled (paths under
|
# All other values are either repo-controlled (paths under
|
||||||
# HellionChat/bin/Release derived from Get-ChildItem) or pinned URLs to
|
# HellionChat/bin/Release derived from find / Get-ChildItem) or pinned
|
||||||
# goatcorp / GitHub. Nothing from a webhook event payload (issue/PR
|
# URLs to goatcorp / gitea. Nothing from a webhook event payload (issue/PR
|
||||||
# titles, commit messages, etc.) flows into a run-step.
|
# titles, commit messages, etc.) flows into a run-step.
|
||||||
|
#
|
||||||
|
# Linux runner: gitea.com Cloud Actions only ships ubuntu-latest. The
|
||||||
|
# plugin csproj targets net10.0-windows, `dotnet build` cross-compiles on
|
||||||
|
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
|
||||||
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
|
||||||
# The tag input is validated against the same semver regex as the
|
# ref step below hard-fails if a non-tag ref is selected, because the
|
||||||
# auto-trigger before any string interpolation happens.
|
# release-action reads GITHUB_REF directly and rejects anything that
|
||||||
|
# does not start with refs/tags/.
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Existing tag to (re)release, e.g. v0.6.1'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -33,46 +33,51 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Build and attach release ZIP
|
name: Build and attach release ZIP
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# On push:tags, github.ref_name is the tag — checkout default works.
|
# release-action@main reads GITHUB_REF directly (its action.yml
|
||||||
# On workflow_dispatch, ref defaults to the branch the action was
|
# does not declare a tag_name input). Validate up-front so manual
|
||||||
# invoked from; we need to explicitly check out the tag the user
|
# dispatches from a branch ref fail loud here instead of burning
|
||||||
# supplied so the build comes from the tagged commit, not main.
|
# a full build before the final step errors out with "ref X is
|
||||||
|
# not a tag".
|
||||||
|
- name: Validate tag ref
|
||||||
|
run: |
|
||||||
|
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::Push a tag, or pick the tag (not main) in the workflow_dispatch Ref dropdown."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
|
||||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@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
|
||||||
shell: pwsh
|
|
||||||
run: |
|
run: |
|
||||||
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||||
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
mkdir -p "$hooks"
|
||||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $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
|
||||||
shell: pwsh
|
|
||||||
run: |
|
run: |
|
||||||
$zip = Get-ChildItem -Path HellionChat\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1
|
zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
|
||||||
if (-not $zip)
|
if [ -z "$zip" ]; then
|
||||||
{
|
echo "latest.zip not found under HellionChat/bin/Release" >&2
|
||||||
throw "latest.zip not found under HellionChat\bin\Release"
|
exit 1
|
||||||
}
|
fi
|
||||||
Write-Host "Found: $($zip.FullName)"
|
echo "Found: $zip"
|
||||||
"path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
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
|
||||||
@@ -87,12 +92,11 @@ jobs:
|
|||||||
- name: Generate release body
|
- name: Generate release body
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
# github.ref_name is the tag because Validate tag ref above
|
||||||
# push:tags carries it in github.ref_name. Either way the value
|
# already enforced refs/tags/v*. Read via env: so the value
|
||||||
# is treated as a PowerShell variable (env-var pass), not as
|
# is a PowerShell variable, not inline shell text, and gets
|
||||||
# inline shell text, and validated against the semver regex
|
# re-validated against the semver regex below.
|
||||||
# below before any string interpolation.
|
TAG_NAME: ${{ 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+$') {
|
||||||
@@ -109,20 +113,22 @@ jobs:
|
|||||||
|
|
||||||
# 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
|
||||||
# 2-space yaml indent 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(2) } else { $_ }
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
}) -join "`n"
|
}) -join "`n"
|
||||||
|
|
||||||
$header = "**Hellion Chat $version"
|
# Subblock convention: "**vX.Y.Z — <subtitle> (<date>)**"
|
||||||
|
# matches verify-changelog-sync.sh and slim-rule grep.
|
||||||
|
$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**Hellion Chat ", 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)) {
|
||||||
@@ -150,15 +156,28 @@ jobs:
|
|||||||
Write-Host $body
|
Write-Host $body
|
||||||
Write-Host "----------------------------------------"
|
Write-Host "----------------------------------------"
|
||||||
|
|
||||||
- name: Attach to GitHub release
|
# release-action@main only declares files/title/body/pre_release/
|
||||||
uses: softprops/action-gh-release@v3
|
# draft/api_key/insecure as inputs (see its action.yml). It silently
|
||||||
|
# ignores anything else, including body_path and tag_name. The tag
|
||||||
|
# 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.
|
||||||
|
- name: Expose release body for release-action
|
||||||
|
id: body
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo 'content<<RELEASE_BODY_EOF'
|
||||||
|
cat release-body.md
|
||||||
|
echo 'RELEASE_BODY_EOF'
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Gitea-native release action. Creates the release if the tag has no
|
||||||
|
# release yet, or updates the existing one with latest.zip attached
|
||||||
|
# and the generated body. The auto-injected GITHUB_TOKEN on Gitea
|
||||||
|
# Actions has Gitea-API scope and is sufficient for release write.
|
||||||
|
- name: Attach to Gitea release
|
||||||
|
uses: https://gitea.com/actions/release-action@main
|
||||||
with:
|
with:
|
||||||
# Explicit tag_name so the action targets the correct release in
|
|
||||||
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
|
||||||
# modes. Without this, dispatch runs would default to the branch
|
|
||||||
# ref (main) and fail to find the release.
|
|
||||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
|
||||||
files: ${{ steps.locate.outputs.path }}
|
files: ${{ steps.locate.outputs.path }}
|
||||||
body_path: release-body.md
|
body: ${{ steps.body.outputs.content }}
|
||||||
fail_on_unmatched_files: true
|
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||||
generate_release_notes: false
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Security
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 6 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
scan:
|
||||||
|
uses: JonKazama-Hellion/security-workflows/.gitea/workflows/security-scan.yml@main
|
||||||
|
with:
|
||||||
|
# MessageStore.cs uses string-interpolation in CommandText for table
|
||||||
|
# names and clause-joins that come from internal code constants, not
|
||||||
|
# user input. Values are bound via SqlParameter, the SQL surface is
|
||||||
|
# local-only inside a Dalamud plugin. Semgrep matches the pattern
|
||||||
|
# without dataflow, so it flags those eight call sites; CodeQL
|
||||||
|
# would not. Suppressed for this repo only.
|
||||||
|
semgrep-exclude-rules: 'csharp.lang.security.sqli.csharp-sqli.csharp-sqli'
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# .githooks/pre-push — invokes preflight.sh (A/B/C/D=build).
|
||||||
|
exec scripts/preflight.sh
|
||||||
@@ -8,7 +8,7 @@ body:
|
|||||||
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
|
||||||
use the [private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
|
report it privately to [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D)
|
||||||
instead.
|
instead.
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
@@ -16,7 +16,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: HellionChat version
|
label: HellionChat version
|
||||||
description: From Settings → Information → Version
|
description: From Settings → Information → Version
|
||||||
placeholder: "0.5.4"
|
placeholder: '0.5.4'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ blank_issues_enabled: false
|
|||||||
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Security vulnerability
|
- name: Security vulnerability
|
||||||
url: https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
url: mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||||
about: Do not open a public issue for security problems. Use the private advisory 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: If the issue exists in upstream Chat 2 too, please report it there so the original maintainers see it as well.
|
about:
|
||||||
|
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
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Scope estimate from your side
|
label: Scope estimate from your side
|
||||||
options:
|
options:
|
||||||
- "Small (one tab, one toggle, one filter)"
|
- 'Small (one tab, one toggle, one filter)'
|
||||||
- "Medium (a settings section, persistent state, one new file)"
|
- 'Medium (a settings section, persistent state, one new file)'
|
||||||
- "Large (architectural, touches the message pipeline or the database)"
|
- 'Large (architectural, touches the message pipeline or the database)'
|
||||||
- "I don't know"
|
- "I don't know"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ Thanks for contributing to HellionChat. Please fill in the sections
|
|||||||
below so the review goes quickly. Delete sections that genuinely do
|
below so the review goes quickly. Delete sections that genuinely do
|
||||||
not apply, but do not delete the whole template.
|
not apply, but do not delete the whole template.
|
||||||
|
|
||||||
If this is a security fix, stop here and use a private security
|
If this is a security fix, stop here and report it privately by
|
||||||
advisory instead:
|
e-mail instead of opening a public PR:
|
||||||
https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
@@ -18,12 +18,11 @@ https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
|||||||
|
|
||||||
- [ ] 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
|
- [ ] Breaking change (config migration, removed feature, or behaviour change that user-visible
|
||||||
change that user-visible defaults rely on)
|
defaults rely on)
|
||||||
- [ ] Documentation only
|
- [ ] Documentation only
|
||||||
- [ ] Translation update
|
- [ ] Translation update
|
||||||
- [ ] Build, CI or tooling change
|
- [ ] Build, CI or tooling change
|
||||||
- [ ] Upstream cherry-pick from Chat 2
|
|
||||||
|
|
||||||
## Linked issue
|
## Linked issue
|
||||||
|
|
||||||
@@ -53,7 +52,6 @@ new commands, new translations, removed behaviour. If none, write
|
|||||||
bump and is it covered by the existing migration tests?
|
bump and is it covered by the existing migration tests?
|
||||||
- Does this change the schema in MessageStore?
|
- Does this change the schema in MessageStore?
|
||||||
- Does this change the repo.json or HellionChat.yaml manifest fields?
|
- Does this change the repo.json or HellionChat.yaml manifest fields?
|
||||||
- Does this affect the upstream cherry-pick path? See docs/UPSTREAM_SYNC.md.
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
@@ -61,12 +59,9 @@ new commands, new translations, removed behaviour. If none, write
|
|||||||
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and
|
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and
|
||||||
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
|
[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
|
- [ ] I added or updated tests where the existing test infrastructure made that practical, or I have
|
||||||
made that practical, or I have explained why tests are not
|
explained why tests are not applicable.
|
||||||
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
|
- [ ] I did not include any AI-generated code without disclosing it in the PR description (see
|
||||||
change is user-visible.
|
[AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)).
|
||||||
- [ ] I did not include any AI-generated code without disclosing it
|
- [ ] I confirm my contribution is released under the [EUPL-1.2](../LICENSE).
|
||||||
in the PR description (see [AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)).
|
|
||||||
- [ ] I confirm my contribution is released under the
|
|
||||||
[EUPL-1.2](../LICENSE).
|
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: monday
|
day: monday
|
||||||
time: "07:00"
|
time: '07:00'
|
||||||
timezone: Europe/Berlin
|
timezone: Europe/Berlin
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
labels:
|
labels:
|
||||||
- dependencies
|
- dependencies
|
||||||
- nuget
|
- nuget
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(deps)"
|
prefix: 'chore(deps)'
|
||||||
groups:
|
groups:
|
||||||
patches:
|
patches:
|
||||||
update-types:
|
update-types:
|
||||||
@@ -32,11 +32,11 @@ updates:
|
|||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
time: "07:00"
|
time: '07:00'
|
||||||
timezone: Europe/Berlin
|
timezone: Europe/Berlin
|
||||||
open-pull-requests-limit: 3
|
open-pull-requests-limit: 3
|
||||||
labels:
|
labels:
|
||||||
- dependencies
|
- dependencies
|
||||||
- github-actions
|
- github-actions
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(actions)"
|
prefix: 'chore(actions)'
|
||||||
|
|||||||
@@ -2,11 +2,17 @@
|
|||||||
subtitle: "Theme Foundation"
|
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 Grove
|
|
||||||
- Settings öffnet jetzt eine Card-Grid-Übersicht — Klick auf eine Card führt in den Detail-View, Breadcrumb und ESC zurück zur Übersicht
|
- Theme-Engine mit fünf Built-In-Themes: Hellion Arctic (Default), Chat 2 Klassik, Event Horizon,
|
||||||
|
Moonlit Bloom, Mint Grove
|
||||||
|
- Settings öffnet jetzt eine Card-Grid-Übersicht — Klick auf eine Card führt in den Detail-View,
|
||||||
|
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 abgelegt
|
- Eigene Themes als JSON in `pluginConfigs/HellionChat/themes/` — Beispiel-Vorlage wird beim ersten
|
||||||
- Optional pro Theme eigene Chat-Channel-Farben mit Übernehmen/Behalten-Banner — niemals automatisch überschrieben
|
Start automatisch abgelegt
|
||||||
|
- 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 → Themes
|
- Migration v13 → v14: alle User landen auf Hellion Arctic. Wer den Upstream-Look will, wählt Chat 2
|
||||||
|
Klassik in Settings → Themes
|
||||||
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
|
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
|
||||||
|
|||||||
@@ -2,14 +2,23 @@
|
|||||||
subtitle: "Layout Refresh"
|
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 aktiven Tab
|
|
||||||
|
- Sidebar im neuen Look: fix 44 px breit, nur Icons, Tab-Name als Tooltip beim Hover, vertikale
|
||||||
|
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 (envelope/star/heart/bell/bookmark/flag/fire) plus eigene Farbe aus 12-Farb-Palette — 84 Kombinationen, gleicher Partner ergibt konsistent dieselbe
|
- Auto-Tell-Tabs sind jetzt visuell unterscheidbar: jeder Tell-Partner bekommt ein eigenes Icon
|
||||||
- Pulsierender roter Dot oben rechts am Sidebar-Icon zeigt ungelesene Nachrichten an. Sanft, 2-Sekunden-Cycle, deaktivierbar über `Configuration.ReduceMotion` (UI-Toggle in v1.3.0)
|
(envelope/star/heart/bell/bookmark/flag/fire) plus eigene Farbe aus 12-Farb-Palette — 84
|
||||||
- Bottom-Status-Bar (22 px) mit fünf Live-Slots: aktiver Channel + Color-Dot, Privacy-Badge, Tab/Message-Counter, Auto-Tell-Counter, Plugin-Version. Update 1×/Sek
|
Kombinationen, gleicher Partner ergibt konsistent dieselbe
|
||||||
- Card-Rows als Default-Message-Render: Sender-Header in Channel-Farbe, Body neue Zeile, dezenter Trenner. `Compact Density`-Toggle in Aussehen schaltet zurück auf den Einzeiler
|
- Pulsierender roter Dot oben rechts am Sidebar-Icon zeigt ungelesene Nachrichten an. Sanft,
|
||||||
- Bug-Fix: Settings speichern löscht den Chat-Verlauf nicht mehr. Refilter läuft jetzt nur wenn Filter-relevante Settings geändert wurden — Cosmetic-Änderungen lassen den Chat unverändert. Persistente und Auto-Tell-Tabs überleben beide
|
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, Auto-Tell-Counter, Plugin-Version. Update 1×/Sek
|
||||||
|
- Card-Rows als Default-Message-Render: Sender-Header in Channel-Farbe, Body neue Zeile, dezenter
|
||||||
|
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 Settings geändert wurden — Cosmetic-Änderungen lassen den Chat unverändert.
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Settings Cleanup"
|
||||||
|
versionsnatur: "UX-Polish-Cycle"
|
||||||
|
---
|
||||||
|
|
||||||
|
- Settings-Übersicht thematisch re-sortiert: zusammenhängende Optionen wohnen jetzt zusammen, jede
|
||||||
|
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** (Schriftart, Schriftgröße, Chat-Farben pro Channel), **Daten-Verwaltung**
|
||||||
|
(Aufbewahrung, Cleanup, Export, DB-Viewer, Advanced-Tools — vorher zwischen Datenschutz und
|
||||||
|
Datenbank verteilt)
|
||||||
|
- 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
|
||||||
|
- 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, `Stilname`-Auswahl, alter `WindowAlpha`-Slider, ungenutztes
|
||||||
|
`ShowThemeQuickPicker`
|
||||||
|
- 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 gewinnt der User-Wert). Backup der Pre-v16-Config liegt unter
|
||||||
|
`pluginConfigs/HellionChat.json.pre-v16-backup`. User die `Stilüberschreiben` aktiv hatten sehen
|
||||||
|
einen einmaligen Hinweis-Toast
|
||||||
|
- UX-Default-Bumps für Bestand-User mit Default-Werten: Card-Rows-Layout zurück auf Single-Line, NG+
|
||||||
|
standardmäßig hidden, gleiche Zeitstempel werden zusammengefasst, MaxLinesToRender auf
|
||||||
|
konservativere 2500
|
||||||
|
- Frische Installs starten mit dem Hellion-Brand-Chat-Color-Preset out-of-the-box (der
|
||||||
|
First-Run-Wizard hat keine Preset-Wahl)
|
||||||
|
- 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) wie ursprünglich geplant.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Theme Expansion"
|
||||||
|
versionsnatur: "Theme-Pack-Patch"
|
||||||
|
---
|
||||||
|
|
||||||
|
- Vier neue Built-in-Themes verlängern die Auswahl im Picker — keine Engine-Änderung, keine Settings
|
||||||
|
angefasst, einfach mehr Farboptionen
|
||||||
|
- **Night Blue** — Royal Blue auf tiefem Marineblau. Kühles Tech-Dashboard-Mood, bewusst neutral
|
||||||
|
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 Event Horizon, aber dunkler und dichter; der Türkis-Akzent
|
||||||
|
hält die beiden klar auseinander
|
||||||
|
- **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
|
||||||
|
Wong/Okabe-Ito-Palette. Channel-Identität bleibt erhalten (Tell pink, Yell gelb, Shout orange,
|
||||||
|
Party blau, FC grün); die Töne sind so gewählt dass jeder Channel auch unter Rot-Grün-Schwäche
|
||||||
|
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 unverändert weiter
|
||||||
|
- 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) kann später nachgeliefert werden, falls Bedarf kommt.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Plugin Integrations: Honorific"
|
||||||
|
versionsnatur: "Plugin-Integration-Cycle 1"
|
||||||
|
---
|
||||||
|
|
||||||
|
- 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 Message-Log mit der von dir gewählten Farbe, Auto-Hide wenn Honorific nicht
|
||||||
|
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
|
||||||
|
- **Neuer Integrations-Settings-Tab** mit Status-Indikator (erkannt, nicht installiert,
|
||||||
|
inkompatibel) und Toggle. Plus Vorschau-Block der die fünf weiteren geplanten Cycles ankündigt:
|
||||||
|
Kontextmenü-Aktionen, Smart Notifications (NotificationMaster), RP-Status-Block (Moodles und
|
||||||
|
LightlessClient), ExtraChat-Channels, Quick-DM-Button (XIVInstantMessenger)
|
||||||
|
- **Maintainer-Attribution** im Tab als Höflichkeits-Geste, zwei Buttons zum Honorific-Repo und zum
|
||||||
|
Caraxi-Profil. Plus Hellion-Forge-Discord-Button für Community-Vorschläge zu künftigen
|
||||||
|
Integrationen
|
||||||
|
- Keine Migration, keine Schema-Änderung. Wer Honorific eh schon nutzt sieht den Custom-Titel
|
||||||
|
automatisch sobald HellionChat aktualisiert
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
subtitle: Critical Lifecycle Fixes
|
||||||
|
versionsnatur: Stability-Hotfix
|
||||||
|
---
|
||||||
|
|
||||||
|
**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 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 manuellen GC.Collect überflüssig
|
||||||
|
- **Worker-Threads** (PendingMessage, RetentionSweep) sind jetzt explizit IsBackground=true, das
|
||||||
|
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 mehr auf disposed EmoteImages-Einträge nach Plugin-Reload
|
||||||
|
- **DisposeAsync-Timeout** (10s) warnt jetzt laut statt silent zu failen
|
||||||
|
- **Plugin-Dispose** flushed pending DeferredSave bevor Services abgebaut werden,
|
||||||
|
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 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.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
subtitle: Theme Engine Performance
|
||||||
|
versionsnatur: Performance-Patch
|
||||||
|
---
|
||||||
|
|
||||||
|
**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, Custom-Theme- Hot-Reload überlebt transiente File-Locks beim Editor-Save. Plus zehnter
|
||||||
|
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-Pack-Form vor-konvertiert. HellionStyle.PushGlobal liest aus dem
|
||||||
|
Cache statt pro Slot pro Frame durch ColourUtil.RgbaToAbgr zu jagen. Real gemessene
|
||||||
|
Frame-Time-Recovery: **~13 %** in typischer Render-Szene (Plan-Erwartung war 2-6 % konservativ,
|
||||||
|
real ~10-15 %)
|
||||||
|
- **Custom-Theme File-Lock-Härtung.** Wenn der User ein Theme-JSON gerade speichert während
|
||||||
|
HellionChat reloaden will, fängt der Loader jetzt explizit Sharing-Violation und Lock-Violation
|
||||||
|
ab. Last-Known-Good-Snapshot bleibt im Picker, beim nächsten Tick wird automatisch retry'd —
|
||||||
|
vorher fiel das Theme aus der Liste bis zum Plugin-Reload
|
||||||
|
- **Defensive Cache-Refresh beim Theme-Switch.** Falls ein Theme auf einem alten Pfad ohne
|
||||||
|
Cache-Fill in den Speicher gekommen ist, holt Switch() das beim Anwenden nach
|
||||||
|
- **Synthwave Sunset als zehnter Built-In.** Hot Magenta + Cyan auf Mitternachts-Violett,
|
||||||
|
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 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.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
subtitle: ChatLog Frame-Hot-Path
|
||||||
|
versionsnatur: Performance-Patch
|
||||||
|
---
|
||||||
|
|
||||||
|
**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 und der Settings-StatusBar eliminiert.
|
||||||
|
|
||||||
|
- **Card-Mode-Border-Loop entlastet.** DrawMessages hebt Theme, DrawList, Window-Left, Window-Right
|
||||||
|
und die ABGR- Border-Color einmalig vor den Per-Message-Loop. Bei 100 sichtbaren Messages sind das
|
||||||
|
gut 500 redundante P/Invokes und Property-Reads, die der Hoist eliminiert. Pop-Out- Heavy-Setups
|
||||||
|
(mehrere parallele Chat-Windows) profitieren proportional, weil der Hoist pro DrawMessages-Call
|
||||||
|
greift, also pro Window
|
||||||
|
- **Auto-Tell Tab-Tint und Icon gecached.** Die Hash-Color- Berechnung für Auto-Tell-Tabs lief pro
|
||||||
|
Tab pro Frame, mit zwei String-Allokationen pro Tab (eine für Tint-Hash, eine für Icon-Hash). Der
|
||||||
|
neue TabTintCache liest pre-computed Werte aus dem Tab und rechnet nur neu wenn das Tell-Target
|
||||||
|
drifted. Beide Caches haben separate Validation-Keys, also keine Cross-Invalidation zwischen Tint-
|
||||||
|
und Icon-Pfad. AutoTellTabTint selbst bleibt pure Hash-Helper, weiterhin ohne Tab-Awareness
|
||||||
|
- **StatusBar-Aggregation hinter Cache-Gate.** Die Status- Leiste am unteren Window-Rand summiert
|
||||||
|
die Tab-Message- Counts und zählt die Auto-Tell-Tabs pro Frame. Der Cache- Gate (1 Sekunde) lag
|
||||||
|
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- Multiplikation pro Window.
|
||||||
|
|
||||||
|
Keine Schema-Bumps, keine User-sichtbaren Funktions- Änderungen außer dass die Frames im Chat-Log
|
||||||
|
und in der Settings-Statusleiste merklich glatter laufen.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
subtitle: Async-Lifecycle + Gitea-Cutover
|
||||||
|
versionsnatur: Architecture-Refactor
|
||||||
|
---
|
||||||
|
|
||||||
|
**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 das Custom-Repo zieht von GitHub auf Gitea um.
|
||||||
|
|
||||||
|
- **Async-Plugin-Architektur.** Konstruktor übernimmt nur noch die Bootstrap-Essentials
|
||||||
|
(Config-Load, Language-Init, Conflict-Detection). Migrationen, Service-Allokationen,
|
||||||
|
Window-Konstruktion und Hook-Subscription wandern in LoadAsync, sodass Dalamud die UI während der
|
||||||
|
schweren Arbeit responsive halten kann. Per-Line-CaptureFailure in DisposeAsync mirrort
|
||||||
|
LightlessSync's Pattern, plus Idempotency-Guard gegen Reload-Races
|
||||||
|
- **Custom-Repo-URL umgezogen auf Gitea.** Bestehende Tester müssen einmalig in XIVLauncher die
|
||||||
|
Custom-Repo-URL auf
|
||||||
|
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||||
|
umstellen, dann XIVLauncher neu starten. Das alte GitHub-Repo bleibt als eingefrorener
|
||||||
|
v1.4.2-Snapshot stehen und wird nicht mehr aktualisiert
|
||||||
|
- **Schema-Gate statt Migrations-Kette.** Die v9 → v16 Migrationen sind raus, ersetzt durch einen
|
||||||
|
harten Schema-Check in Phase 1. Configs auf Schema v16+ laden direkt; ältere Configs (vor v1.2.1)
|
||||||
|
bekommen jetzt eine klare „install v1.4.2 first"-Fehlermeldung statt eines impliziten
|
||||||
|
Migrations-Pfads
|
||||||
|
- **AutoTranslate-Cache läuft im Hintergrund.** Der Cache füllt sich jetzt fire-and-forget statt
|
||||||
|
blockierend im Plugin-Load. Trade-off: die erste Auto-Translate-Nutzung einer Session kann einen
|
||||||
|
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.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
subtitle: Threading- und IPC-Sicherheits-Politur
|
||||||
|
versionsnatur: Wartung und Robustheit
|
||||||
|
---
|
||||||
|
|
||||||
|
**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 Hot-Path-Lock im Auto-Tell-Tab-Counter fällt weg, IPC-Cleanup wird sichtbar wenn
|
||||||
|
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 einem Lock gemacht. Jetzt läuft das über einen Interlocked-Counter der
|
||||||
|
parallel zur Tabs-Liste mitgeführt wird, inklusive Resync-Hook für den Snapshot-Restore-Pfad in
|
||||||
|
`SaveConfig`. Plus Pure-Helper-Test-Mirror in der Build-Suite damit die Atomicity-Semantik nicht
|
||||||
|
versehentlich wegrefactored wird
|
||||||
|
- **HonorificService selbst-dokumentierende Threading-Banner.** Statt eines Block-Comments am
|
||||||
|
Klassen-Ende hat jede IPC-Callback-Methode jetzt einen 1-Zeilen-Banner darüber, der den
|
||||||
|
Thread-Kontext direkt am Call-Site benennt (framework only, framework scheduled, any). Mehr Hilfe
|
||||||
|
für künftige Reviews als ein abstraktes Threading-Kapitel
|
||||||
|
- **Unsubscribe-Failure ist jetzt sichtbar.** `TryUnsubscribe` hat ein Honorific-Unsubscribe-Failure
|
||||||
|
bisher als Debug geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription
|
||||||
|
kann den Service über Plugin-Reloads hinweg leben lassen, also läuft der Log jetzt auf Warning
|
||||||
|
- **AutoTranslate-Warmup blockiert den Plugin-Unload nicht mehr.** Der Cache-Warmup-Thread war ohne
|
||||||
|
`IsBackground=true` unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu
|
||||||
|
MessageManager und RetentionSweep (beide seit v1.4.0)
|
||||||
|
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType
|
||||||
|
einführt der weder in der Whitelist noch in den Defaults steht, wird er bisher silent durch den
|
||||||
|
Failsafe geleitet. Jetzt loggt der Filter einmalig pro Runtime eine Warning mit dem Type und dem
|
||||||
|
Failsafe-Wert. Dedup über ein NonSerialized-HashSet, also kein Log-Spam
|
||||||
|
- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen
|
||||||
|
Configs jetzt auf `true`, damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt
|
||||||
|
wird bevor der User entscheiden kann. Bestehende Configs behalten ihre Wahl, weil der Deserializer
|
||||||
|
den Initializer überschreibt. Keine Migration, kein Schema-Bump
|
||||||
|
|
||||||
|
Keine User-sichtbaren Funktions-Änderungen außer dem Default-Flip für neue Installationen. Settings,
|
||||||
|
Themes, Tabs und 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.
|
||||||
+15
-12
@@ -1,26 +1,29 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
This release is distributed via the HellionChat custom repository, not the
|
This release is distributed via the HellionChat custom repository, not the Dalamud main plugin repo.
|
||||||
Dalamud main plugin repo. To install:
|
To install:
|
||||||
|
|
||||||
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
||||||
2. Add the URL:
|
2. Add the URL:
|
||||||
`https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json`
|
`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://github.com/JonKazama-Hellion/HellionChat/blob/main/README.md) — features, architecture, build
|
- [README](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/README.md)
|
||||||
- [Privacy notice](https://github.com/JonKazama-Hellion/HellionChat/blob/main/PRIVACY.md) — what the plugin stores and sends
|
— features, architecture, build
|
||||||
- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/docs/THIRD_PARTY_NOTICES.md) — dependencies and licences
|
- [Privacy notice](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/PRIVACY.md)
|
||||||
- [Security policy](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SECURITY.md) — vulnerability reporting
|
— what the plugin stores and sends
|
||||||
- [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths
|
- [Third-party notices](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/docs/THIRD_PARTY_NOTICES.md)
|
||||||
|
— dependencies and licences
|
||||||
|
- [Security policy](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SECURITY.md)
|
||||||
|
— vulnerability reporting
|
||||||
|
- [Support](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SUPPORT.md)
|
||||||
|
— bug reports, questions, contact paths
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
[EUPL-1.2](https://github.com/JonKazama-Hellion/HellionChat/blob/main/LICENSE).
|
[EUPL-1.2](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/LICENSE).
|
||||||
Based on [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna,
|
Based on [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna, also EUPL-1.2.
|
||||||
also EUPL-1.2.
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
name: CodeQL
|
|
||||||
|
|
||||||
# Replaces the GitHub default-setup CodeQL scan. The default setup runs
|
|
||||||
# without resolving the Dalamud assemblies (they live in a user-AppData
|
|
||||||
# path) and reports "Low C# analysis quality" because call-target
|
|
||||||
# resolution sits at ~64%. This workflow downloads the Dalamud staging
|
|
||||||
# distribution before the build, runs a manual dotnet build, and then
|
|
||||||
# lets CodeQL analyse the fully-resolved compilation. Quality climbs
|
|
||||||
# back above the 85% thresholds.
|
|
||||||
#
|
|
||||||
# This workflow only consumes trusted inputs: the tag/branch ref via
|
|
||||||
# the standard checkout action, and the Dalamud distribution URL which
|
|
||||||
# is pinned to a goatcorp-controlled GitHub Pages target. No user-
|
|
||||||
# controlled event payload (issue title, PR body, commit message) flows
|
|
||||||
# into a run-step.
|
|
||||||
#
|
|
||||||
# Disable the default setup in the repo before this workflow lands:
|
|
||||||
# Settings -> Code security -> Code scanning -> "CodeQL analysis" tile
|
|
||||||
# -> Switch to advanced.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
schedule:
|
|
||||||
- cron: '17 6 * * 1'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze-csharp:
|
|
||||||
name: Analyze (csharp)
|
|
||||||
runs-on: windows-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
|
|
||||||
- name: Setup .NET 10
|
|
||||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
|
||||||
with:
|
|
||||||
dotnet-version: 10.0.x
|
|
||||||
|
|
||||||
- name: Download Dalamud staging
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
|
||||||
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
|
||||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
|
||||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
|
||||||
with:
|
|
||||||
languages: csharp
|
|
||||||
build-mode: manual
|
|
||||||
queries: security-extended
|
|
||||||
|
|
||||||
- name: Restore
|
|
||||||
run: dotnet restore HellionChat/HellionChat.csproj
|
|
||||||
|
|
||||||
- name: Build (Release)
|
|
||||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
|
||||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
|
||||||
with:
|
|
||||||
category: /language:csharp
|
|
||||||
|
|
||||||
analyze-actions:
|
|
||||||
name: Analyze (actions)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
|
||||||
with:
|
|
||||||
languages: actions
|
|
||||||
build-mode: none
|
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
|
||||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
|
||||||
with:
|
|
||||||
category: /language:actions
|
|
||||||
+458
-227
@@ -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
|
||||||
.vscode/
|
!.env.sample
|
||||||
scripts/
|
|
||||||
|
|
||||||
# Local test project (stays out of the published plugin repo;
|
# Private Keys & Zertifikate
|
||||||
# pure-function safety net for refactor cycles)
|
*.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/
|
||||||
|
scripts/setup-dev-env.sh
|
||||||
|
|
||||||
|
# Lokales Test-Projekt (bleibt aus dem Plugin-Repo raus;
|
||||||
|
# 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,71 +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
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"MD003": { "style": "atx" },
|
||||||
|
"MD004": { "style": "dash" },
|
||||||
|
"MD007": { "indent": 2 },
|
||||||
|
"MD009": { "br_spaces": 2, "strict": false, "list_item_empty_lines": false },
|
||||||
|
"MD013": false,
|
||||||
|
"MD024": { "siblings_only": true },
|
||||||
|
"MD029": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD036": false,
|
||||||
|
"MD040": true,
|
||||||
|
"MD041": false,
|
||||||
|
"MD046": { "style": "fenced" },
|
||||||
|
"MD048": { "style": "backtick" },
|
||||||
|
"MD049": { "style": "underscore" },
|
||||||
|
"MD050": { "style": "asterisk" }
|
||||||
|
}
|
||||||
@@ -0,0 +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/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
# === JS / Web Build Output ===
|
||||||
|
node_modules/
|
||||||
|
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
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# Zeilenlängen-Check aus (konsistent mit markdownlint MD013)
|
||||||
|
line-length: disable
|
||||||
|
|
||||||
|
# YAML ohne führendes "---" erlaubt
|
||||||
|
document-start: disable
|
||||||
|
|
||||||
|
# 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
|
||||||
+58
-77
@@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
## A Note on This Project
|
## A Note on This Project
|
||||||
|
|
||||||
HellionChat is a one-person side project developed under Hellion Forge.
|
HellionChat is a one-person side project developed under Hellion Forge. I maintain this in my spare
|
||||||
I maintain this in my spare time, which means replies can take a few
|
time, which means replies can take a few days. Please do not escalate just because a thread is
|
||||||
days. Please do not escalate just because a thread is quiet.
|
quiet.
|
||||||
|
|
||||||
When in doubt, assume good intent. Contributors come from different
|
When in doubt, assume good intent. Contributors come from different backgrounds, time zones and
|
||||||
backgrounds, time zones and skill levels. A clarifying question is
|
skill levels. A clarifying question is almost always a better first move than an accusation.
|
||||||
almost always a better first move than an accusation.
|
|
||||||
|
|
||||||
Please also keep discussions on topic. This project is about a Dalamud
|
Please also keep discussions on topic. This project is about a Dalamud chat plugin. Off-topic
|
||||||
chat plugin. Off-topic arguments belong elsewhere.
|
arguments belong elsewhere.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,88 +18,75 @@ chat plugin. Off-topic arguments belong 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
|
We are committed to fostering an environment that respects and promotes the dignity, rights, and
|
||||||
the dignity, rights, and contributions of all individuals, regardless
|
contributions of all individuals, regardless of characteristics including race, ethnicity, caste,
|
||||||
of characteristics including race, ethnicity, caste, color, age,
|
color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or
|
||||||
physical characteristics, neurodiversity, disability, sex or gender,
|
expression, sexual orientation, language, philosophy or religion, national or social origin,
|
||||||
gender identity or expression, sexual orientation, language, philosophy
|
socio-economic position, level of education, or other status. The same privileges of participation
|
||||||
or religion, national or social origin, socio-economic position, level
|
are extended to everyone who participates in good faith and in accordance with this Covenant.
|
||||||
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
|
While acknowledging differences in social norms, we all strive to meet our community's expectations
|
||||||
our community's expectations for positive behavior. We also understand
|
for positive behavior. We also understand that our words and actions may be interpreted differently
|
||||||
that our words and actions may be interpreted differently than we intend
|
than we intend based on culture, background, or native language.
|
||||||
based on culture, background, or native language.
|
|
||||||
|
|
||||||
With these considerations in mind, we agree to behave mindfully toward
|
With these considerations in mind, we agree to behave mindfully toward each other and act in ways
|
||||||
each other and act in ways that center our shared values, including:
|
that center our shared values, including:
|
||||||
|
|
||||||
1. Respecting the **purpose of our community**, our activities, and our
|
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
|
||||||
ways of gathering.
|
|
||||||
2. Engaging **kindly and honestly** with others.
|
2. Engaging **kindly and honestly** with others.
|
||||||
3. Respecting **different viewpoints** and experiences.
|
3. Respecting **different viewpoints** and experiences.
|
||||||
4. **Taking responsibility** for our actions and contributions.
|
4. **Taking responsibility** for our actions and contributions.
|
||||||
5. Gracefully giving and accepting **constructive feedback**.
|
5. Gracefully giving and accepting **constructive feedback**.
|
||||||
6. Committing to **repairing harm** when it occurs.
|
6. Committing to **repairing harm** when it occurs.
|
||||||
7. Behaving in other ways that promote and sustain the **well-being of
|
7. Behaving in other ways that promote and sustain the **well-being of our community**.
|
||||||
our community**.
|
|
||||||
|
|
||||||
## Restricted Behaviors
|
## Restricted Behaviors
|
||||||
|
|
||||||
We agree to restrict the following behaviors in our community.
|
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of
|
||||||
Instances, threats, and promotion of these behaviors are violations of
|
these behaviors are violations of this Code of Conduct.
|
||||||
this Code of Conduct.
|
|
||||||
|
|
||||||
1. **Harassment.** Violating explicitly expressed boundaries or engaging
|
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal
|
||||||
in unnecessary personal attention after any clear request to stop.
|
attention after any clear request to stop.
|
||||||
2. **Character attacks.** Making insulting, demeaning, or pejorative
|
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a
|
||||||
comments directed at a community member or group of people.
|
community member or group of people.
|
||||||
3. **Stereotyping or discrimination.** Characterizing anyone's
|
3. **Stereotyping or discrimination.** Characterizing anyone's personality or behavior on the basis
|
||||||
personality or behavior on the basis of immutable identities or
|
of immutable identities or traits.
|
||||||
traits.
|
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate
|
||||||
4. **Sexualization.** Behaving in a way that would generally be
|
in the context or purpose of the community.
|
||||||
considered inappropriately intimate in the context or purpose of the
|
5. **Violating confidentiality.** Sharing or acting on someone's personal or private information
|
||||||
community.
|
without their permission.
|
||||||
5. **Violating confidentiality.** Sharing or acting on someone's
|
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person
|
||||||
personal or private information without their permission.
|
or group.
|
||||||
6. **Endangerment.** Causing, encouraging, or threatening violence or
|
7. Behaving in other ways that **threaten the well-being** of our community.
|
||||||
other harm toward any person or group.
|
|
||||||
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,
|
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone
|
||||||
or pretending to be someone else to evade enforcement actions.
|
else to evade enforcement actions.
|
||||||
2. **Failing to credit sources.** Not properly crediting the sources of
|
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
|
||||||
content you contribute.
|
3. **Promotional materials.** Sharing marketing or other commercial content in a way that is outside
|
||||||
3. **Promotional materials.** Sharing marketing or other commercial
|
the norms of the community.
|
||||||
content in a way that is outside the norms of the community.
|
4. **Irresponsible communication.** Failing to responsibly present content which includes, links to,
|
||||||
4. **Irresponsible communication.** Failing to responsibly present
|
or describes any other restricted behaviors.
|
||||||
content which includes, links to, or describes any other restricted
|
|
||||||
behaviors.
|
|
||||||
|
|
||||||
## Reporting
|
## Reporting
|
||||||
|
|
||||||
If something here is being broken, contact me directly. Do not open a
|
If something here is being broken, contact me directly. Do not open a public issue.
|
||||||
public issue.
|
|
||||||
|
|
||||||
| Channel | Address |
|
| Channel | Address |
|
||||||
| ---------- | ------------------------ |
|
| ---------- | -------------------------- |
|
||||||
| 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
|
Reports stay private. I will acknowledge within a few weekdays (European business hours) and tell
|
||||||
(European business hours) and tell you what I plan to do.
|
you what I plan to do.
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
I am the sole maintainer, so enforcement is a single-person process.
|
I am the sole maintainer, so enforcement is a single-person process. I will pick the lightest
|
||||||
I will pick the lightest measure that actually 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.
|
||||||
@@ -109,25 +95,20 @@ I will pick the lightest measure that actually resolves the situation:
|
|||||||
5. Temporary block from the repository or related spaces.
|
5. Temporary block from the repository or related spaces.
|
||||||
6. Permanent block.
|
6. Permanent block.
|
||||||
|
|
||||||
Severe cases skip the lower steps. I will not negotiate over harassment
|
Severe cases skip the lower steps. I will not negotiate over harassment or threats.
|
||||||
or threats.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies to all spaces the project owns or that I
|
This Code of Conduct applies to all spaces the project owns or that I run on its behalf: the GitHub
|
||||||
run on its behalf: the GitHub repository, GitHub Discussions,
|
repository, GitHub Discussions, project-related Discord conversations, and the maintainer contact
|
||||||
project-related Discord conversations, and the maintainer contact
|
listed in [`SECURITY.md`](SECURITY.md). It also applies when someone is identifiably representing
|
||||||
listed in [`SECURITY.md`](SECURITY.md). It also applies when someone
|
HellionChat elsewhere, for example when posting as a HellionChat maintainer in the Dalamud Discord.
|
||||||
is identifiably representing 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
|
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, available at
|
||||||
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
|
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA
|
||||||
Source and licensed under CC BY-SA 4.0. To view a copy of this
|
4.0. To view a copy of this license, visit
|
||||||
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/).
|
||||||
|
|||||||
+96
-93
@@ -1,87 +1,75 @@
|
|||||||
# Contributing to HellionChat
|
# Contributing to HellionChat
|
||||||
|
|
||||||
Thanks for taking a look. HellionChat is a one-person side project
|
Thanks for taking a look. HellionChat is a one-person side project developed under Hellion Forge. It
|
||||||
developed under Hellion Forge. It started as a fork of
|
started as a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo) and has since become a
|
||||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) and has since become
|
standalone plugin under its own namespace, IPC channels and source tree (standalone-cut completed in
|
||||||
a standalone plugin under its own namespace, IPC channels and
|
v1.0.0). Forking HellionChat itself is explicitly permitted under the EUPL-1.2.
|
||||||
source tree (standalone-cut completed in 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
|
This document explains what I am looking for, what I am not, and how to make a contribution land
|
||||||
to make a contribution land smoothly.
|
smoothly.
|
||||||
|
|
||||||
## Before You Open Anything
|
## Before You Open Anything
|
||||||
|
|
||||||
- Read the [README](README.md) so you understand the scope: a
|
- Read the [README](README.md) so you understand the scope: a privacy-focused, EUPL-1.2-licensed
|
||||||
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
|
Dalamud plugin that intentionally removes the upstream webinterface and ships privacy-first
|
||||||
removes the upstream webinterface and ships privacy-first defaults.
|
defaults.
|
||||||
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Cherry-picks
|
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Active cherry-picking from upstream Chat 2
|
||||||
from upstream Chat 2 are selective and deliberate; not everything
|
has ended in the v1.4.x cycle; HellionChat continues as an independent codebase. Existing
|
||||||
that lands there belongs here.
|
upstream-derived code keeps its attribution. New contributions stand on their own and do not need
|
||||||
- Read [`SECURITY.md`](SECURITY.md). Anything security-sensitive goes
|
to be cherry-pick-compatible.
|
||||||
through a private advisory, never a public issue or PR.
|
- 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
|
- Bug fixes for behaviour documented in the README, the in-plugin settings or the changelog.
|
||||||
settings or the changelog.
|
- Translation contributions for Hellion-specific strings via direct pull requests against
|
||||||
- Translation contributions for Hellion-specific strings via direct
|
`HellionChat/Resources/HellionStrings.*.resx`. Translations for upstream Chat 2 strings
|
||||||
pull requests against
|
(`Language.*.resx`) are not handled here; those go to the upstream Chat 2 project.
|
||||||
`HellionChat/Resources/HellionStrings.*.resx`. Translations for
|
|
||||||
upstream Chat 2 strings (`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
|
- New features that fit the privacy-first scope and do not duplicate what an existing Dalamud plugin
|
||||||
what an existing Dalamud plugin already does well.
|
already does well.
|
||||||
|
|
||||||
## What I Will Probably Decline
|
## What I Will Probably Decline
|
||||||
|
|
||||||
- Re-introducing the webinterface or any remote-access feature. It was
|
- Re-introducing the webinterface or any remote-access feature. It was removed in v0.2.0 on purpose.
|
||||||
removed in v0.2.0 on purpose. See the README section
|
See the README section "Was gegenüber Chat 2 fehlt".
|
||||||
"Was gegenüber Chat 2 fehlt".
|
- Features that bypass the privacy filter or weaken the default retention behaviour without an
|
||||||
- Features that bypass the privacy filter or weaken the default
|
explicit, documented opt-in.
|
||||||
retention behaviour without an explicit, documented opt-in.
|
- Sweeping refactors that touch large parts of the codebase. The maintenance cost outweighs the
|
||||||
- Sweeping refactors that touch large parts of the codebase. They make
|
benefit for a one-person project. (This used to be doubly important because of the upstream
|
||||||
selective upstream cherry-picks much harder and the maintenance cost
|
cherry-pick path; that path is closed now, but the rule still holds on its own merits.)
|
||||||
outweighs the benefit for a one-person project.
|
|
||||||
- AI-generated code dropped in without disclosure or human review. See
|
- AI-generated code dropped in without disclosure or human review. See
|
||||||
[`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) for how I handle
|
[`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) for how I handle AI assistance on my side; I
|
||||||
AI assistance on my side; I expect comparable transparency from
|
expect comparable transparency from contributors.
|
||||||
contributors.
|
|
||||||
|
|
||||||
If you are unsure whether an idea fits, open a feature-request issue
|
If you are unsure whether an idea fits, open a feature-request issue first and ask before writing
|
||||||
first and ask before writing code. I would rather say "no" to a
|
code. I would rather say "no" to a proposal than to a finished pull request.
|
||||||
proposal than to a finished pull request.
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
1. Open an issue (bug or feature request) using the templates under
|
1. Open an issue (bug or feature request) using the templates under `.github/ISSUE_TEMPLATE/`. Skip
|
||||||
`.github/ISSUE_TEMPLATE/`. Skip this for trivial typos.
|
this for trivial typos.
|
||||||
2. Fork the repository and branch off `main`. Branch naming is
|
2. Fork the repository and branch off `main`. Branch naming is informal; something like
|
||||||
informal; something like `fix/auto-tell-history-empty` or
|
`fix/auto-tell-history-empty` or `feat/theme-export` is fine.
|
||||||
`feat/theme-export` is fine.
|
3. Match the existing code style. The repository ships an `.editorconfig` that VS Code and Rider
|
||||||
3. Match the existing code style. The repository ships an
|
pick up automatically.
|
||||||
`.editorconfig` that VS Code and Rider pick up automatically.
|
4. Keep commits focused. Several small commits with clear messages are easier to review than one
|
||||||
4. Keep commits focused. Several small commits with clear messages are
|
large one. Squash-on-merge happens at the PR level if needed.
|
||||||
easier to review than one large one. Squash-on-merge happens at
|
5. If your change touches user-visible behaviour, update the README and/or the changelog block in
|
||||||
the PR level if needed.
|
`HellionChat/HellionChat.yaml` and `repo.json`. I bump the version number myself at release time.
|
||||||
5. If your change touches user-visible behaviour, update the README
|
6. Open the pull request against `main`. The PR template will ask you to summarise the change, the
|
||||||
and/or the changelog block in `HellionChat/HellionChat.yaml` and
|
testing you did and any compatibility notes.
|
||||||
`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 any
|
|
||||||
compatibility notes.
|
|
||||||
|
|
||||||
## Build and Test
|
## Build and Test
|
||||||
|
|
||||||
The project targets `net10.0-windows` against Dalamud SDK 15. To build
|
The project targets `net10.0-windows` against Dalamud SDK 15. To build locally you need:
|
||||||
locally you need:
|
|
||||||
|
|
||||||
- .NET 10 SDK
|
- .NET 10 SDK
|
||||||
- A working Dalamud dev environment with `DALAMUD_HOME` set
|
- A working Dalamud dev environment with `DALAMUD_HOME` set (XIVLauncher installed and launched once
|
||||||
(XIVLauncher installed and launched once is the simplest 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
|
||||||
@@ -89,59 +77,74 @@ 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
|
There are currently no tests in `HellionChat.sln`. If you add a test project, point it at the
|
||||||
project, point it at the relevant subsystems (privacy filter,
|
relevant subsystems (privacy filter, configuration migration, message store) and mention it in the
|
||||||
configuration migration, message store) and mention it in the PR.
|
PR.
|
||||||
|
|
||||||
For a smoke test in-game: build, copy the output into your Dalamud
|
For a smoke test in-game: build, copy the output into your Dalamud `devPlugins/HellionChat/`
|
||||||
`devPlugins/HellionChat/` directory and load it via `/xlplugins`.
|
directory and load it via `/xlplugins`.
|
||||||
|
|
||||||
## Continuous Integration
|
## Continuous Integration
|
||||||
|
|
||||||
Every push and every pull request runs:
|
Every push and every pull request runs:
|
||||||
|
|
||||||
| Workflow | What it checks |
|
| Workflow | What it checks |
|
||||||
| ------------- | ------------------------------------- |
|
| ------------ | -------------------------------- |
|
||||||
| `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.
|
A pull request will not be merged while either of these is failing. CodeQL findings on changed code
|
||||||
CodeQL findings on changed code need to be addressed; pre-existing
|
need to be addressed; pre-existing findings on untouched code are tracked separately.
|
||||||
findings on untouched code are tracked separately.
|
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Hellion-specific strings live in
|
Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx` (English source) and
|
||||||
`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`
|
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are **not** translated here.
|
||||||
are **not** translated here. They are owned by the upstream project
|
They are kept as-is from the last upstream sync and remain the work of the Chat 2 Crowdin community.
|
||||||
and synced in via cherry-pick. Please contribute those to
|
Active cherry-picking from upstream ended in the v1.4.x cycle (see
|
||||||
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) instead.
|
[`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md)), so future translation improvements to those
|
||||||
|
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
|
- Your contribution is your own work, or you have the right to contribute it under the project
|
||||||
contribute it under the project licence.
|
licence.
|
||||||
- You agree that your contribution will be released under the
|
- You agree that your contribution will be released under the [EUPL-1.2](LICENSE), the same licence
|
||||||
[EUPL-1.2](LICENSE), the same licence as the rest of the project.
|
as the rest of the project.
|
||||||
|
|
||||||
There is no separate CLA. Forking HellionChat is explicitly permitted
|
There is no separate CLA. Forking HellionChat is explicitly permitted under the EUPL-1.2, as with
|
||||||
under the EUPL-1.2, as with any EUPL-licensed project.
|
any EUPL-licensed project.
|
||||||
|
|
||||||
## Response Times
|
## Response Times
|
||||||
|
|
||||||
| Channel | Address |
|
| Channel | Address |
|
||||||
| ------------- | -------------------------- |
|
| ------------- | --------------------------------------- |
|
||||||
| GitHub Issues | Preferred for bugs and feature requests |
|
| GitHub Issues | Preferred for bugs and feature requests |
|
||||||
| 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
|
I respond on weekdays during European business hours and take weekends and FFXIV patch days off. A
|
||||||
and FFXIV patch days off. A pull request that sits for a few days has
|
pull request that sits for a few days has not been ignored. Pinging once after a week is fine;
|
||||||
not been ignored. Pinging once after a week is fine; please do not
|
please do not ping daily.
|
||||||
ping daily.
|
|
||||||
|
## First-time setup
|
||||||
|
|
||||||
|
After cloning, run once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/setup-hooks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This wires `core.hooksPath` to `.githooks/`. The pre-push hook runs preflight
|
||||||
|
(versions/manifest/changelog/build).
|
||||||
|
|
||||||
|
### 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 development, contact the maintainer.
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
|||||||
Source code
|
Source code
|
||||||
═══════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Copyright (c) 2022-2026 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
Copyright (c) 2022-2026 **[Infiziert90 (Infi)](https://github.com/Infiziert90)** and **[Anna](https://github.com/anna-is-cute)**
|
||||||
Original ChatTwo authors and copyright holders of the upstream
|
Original ChatTwo authors and copyright holders of the upstream
|
||||||
plugin this fork is built on. Their work covers the message store,
|
plugin this fork is built on. Their work covers the message store,
|
||||||
the channel filtering, the sidebar tab system, the FFXIV chat
|
the channel filtering, the sidebar tab system, the FFXIV chat
|
||||||
hooks, the localisation infrastructure and most of the
|
hooks, the localisation infrastructure and most of the
|
||||||
architecture HellionChat still relies on.
|
architecture HellionChat still relies on.
|
||||||
|
|
||||||
Copyright (c) 2025-2026 Florian Wathling / Hellion Online Media
|
Copyright (c) 2025-2026 Florian Wathling / Hellion Online Media
|
||||||
HellionChat-specific modifications, including the privacy filter,
|
HellionChat-specific modifications, including the privacy filter,
|
||||||
per-channel retention sweep, export pipeline, Auto-Tell-Tabs,
|
per-channel retention sweep, export pipeline, Auto-Tell-Tabs,
|
||||||
German localisation and the EUPL-1.2 fork maintenance.
|
German localisation and the EUPL-1.2 fork maintenance.
|
||||||
|
|
||||||
Source code is licensed under the European Union Public Licence
|
Source code is licensed under the European Union Public Licence
|
||||||
(EUPL), Version 1.2 only. The full Licence text lives in the LICENSE
|
(EUPL), Version 1.2 only. The full Licence text lives in the LICENSE
|
||||||
file at the root of this repository. The official Licence website is
|
file at the root of this repository. The official Licence website is
|
||||||
at: https://eupl.eu/1.2/en/
|
at: <https://eupl.eu/1.2/en/>
|
||||||
|
|
||||||
This Work is provided "AS IS" without warranties of any kind. See
|
This Work is provided "AS IS" without warranties of any kind. See
|
||||||
Article 7 (Disclaimer of Warranty) and Article 8 (Disclaimer of
|
Article 7 (Disclaimer of Warranty) and Article 8 (Disclaimer of
|
||||||
@@ -42,8 +42,8 @@ Bundled assets
|
|||||||
═══════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Exo 2 font (HellionChat/Resources/HellionFont.ttf)
|
Exo 2 font (HellionChat/Resources/HellionFont.ttf)
|
||||||
SIL Open Font License 1.1, full text in HellionFont-OFL.txt.
|
SIL Open Font License 1.1, full text in HellionFont-OFL.txt.
|
||||||
Bundled with permission per the OFL terms.
|
Bundled with permission per the OFL terms.
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|||||||
+232
-127
@@ -1,49 +1,57 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
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 Dalamud.Game.Text;
|
using Microsoft.Extensions.Logging;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs.
|
// Auto-Tell-Tabs: spawns session-only tabs per /tell partner.
|
||||||
//
|
// Subscribes to MessageManager.MessageProcessed and ClientState.Logout.
|
||||||
// Spawns a session-only tab per /tell partner so a club greeter can track
|
|
||||||
// multiple parallel conversations without losing context. Subscribes to
|
|
||||||
// MessageManager.MessageProcessed for live tells and to ClientState.Logout
|
|
||||||
// for the cleanup pass; everything else hangs off these two entry points.
|
|
||||||
//
|
|
||||||
// See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian vault).
|
|
||||||
internal sealed class AutoTellTabsService : IDisposable
|
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();
|
||||||
|
|
||||||
|
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
|
||||||
|
// of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
|
||||||
|
// in their own bucket. A configurable cap is a vault-backlog anchor for
|
||||||
|
// a later cycle if tester feedback demands it.
|
||||||
|
internal const int MaxPinnedTempTabs = 5;
|
||||||
|
|
||||||
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
|
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||||
{
|
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||||
get
|
// Replaces the F2.1 Interlocked counter because the new pin-state
|
||||||
{
|
// transitions are cold-path and don't need lock-free reads.
|
||||||
lock (_tempTabsLock)
|
internal int ActiveTempTabCount =>
|
||||||
{
|
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||||
return Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
|
||||||
}
|
internal int PinnedTempTabCount => Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Initialize()
|
internal void Initialize()
|
||||||
{
|
{
|
||||||
@@ -52,11 +60,53 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pinned tabs come out of the JSON with TellTarget set but
|
||||||
|
// CurrentChannel reset (NonSerialized). Without re-seeding, the chat
|
||||||
|
// input has no tell-target on the active pinned tab, and the
|
||||||
|
// game-side channel hook only repaints CurrentChannel once the user
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RehydratePinnedTabs()
|
||||||
|
{
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
if (!_initialized)
|
if (!_initialized)
|
||||||
@@ -76,7 +126,10 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.Code.Type != ChatType.TellIncoming && message.Code.Type != ChatType.TellOutgoing)
|
if (
|
||||||
|
message.Code.Type != ChatType.TellIncoming
|
||||||
|
&& message.Code.Type != ChatType.TellOutgoing
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -84,15 +137,13 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
var partner = ExtractTellPartner(message);
|
var partner = ExtractTellPartner(message);
|
||||||
if (partner == null)
|
if (partner == null)
|
||||||
{
|
{
|
||||||
// Real message without a player payload — e.g. GM tells, which
|
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
||||||
// we deliberately skip. The diagnostics make future regressions
|
_logger.LogWarning(
|
||||||
// (FFXIV changing tell payload shape, new edge cases) findable
|
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
||||||
// without having to crank up debug logging at the source.
|
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
||||||
Plugin.Log.Warning(
|
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
||||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " +
|
+ $"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}"
|
||||||
$"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " +
|
);
|
||||||
$"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " +
|
|
||||||
$"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,9 +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)
|
||||||
{
|
{
|
||||||
// Tab already exists; Tab.Matches has already routed this
|
// Already routed via MessageManager pipeline. Repair the
|
||||||
// message via the MessageManager pipeline (see Task 2 sender
|
// tell-target if the fallback hit a pinned tab whose
|
||||||
// filter).
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,12 +185,10 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
{
|
{
|
||||||
if (message.Code.Type == ChatType.TellIncoming)
|
if (message.Code.Type == ChatType.TellIncoming)
|
||||||
{
|
{
|
||||||
// Incoming tell: the sender is the conversation partner. The
|
// Sender is the partner; check chunks first, then raw SeString as fallback
|
||||||
// PlayerPayload normally rides on a chunk's Link slot, but for
|
var fromSender =
|
||||||
// some tell types FFXIV only puts it in the raw SeString —
|
ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||||
// fall back to that before giving up.
|
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||||
var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender)
|
|
||||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
|
||||||
if (fromSender != null)
|
if (fromSender != null)
|
||||||
{
|
{
|
||||||
return (fromSender.PlayerName, fromSender.World.RowId);
|
return (fromSender.PlayerName, fromSender.World.RowId);
|
||||||
@@ -133,21 +196,20 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outgoing tell: the local player is the sender, the partner shows
|
// Outgoing tell: check content first, then channels's TellTarget as fallback
|
||||||
// up either as a payload in the content (for tells typed via the
|
var fromContent =
|
||||||
// Chat 2 input bar) or as the channel's tracked tell target (set by
|
ChunkUtil.TryGetPlayerPayload(message.Content)
|
||||||
// the SetContextTellTarget game hook). Same SeString fallback.
|
?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
|
||||||
var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content)
|
?? ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||||
?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
|
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||||
?? ChunkUtil.TryGetPlayerPayload(message.Sender)
|
|
||||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
|
||||||
if (fromContent != null)
|
if (fromContent != null)
|
||||||
{
|
{
|
||||||
return (fromContent.PlayerName, fromContent.World.RowId);
|
return (fromContent.PlayerName, fromContent.World.RowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var current = _plugin.CurrentTab.CurrentChannel.TellTarget
|
var current =
|
||||||
?? _plugin.CurrentTab.CurrentChannel.TempTellTarget;
|
_plugin.CurrentTab.CurrentChannel.TellTarget
|
||||||
|
?? _plugin.CurrentTab.CurrentChannel.TempTellTarget;
|
||||||
if (current != null && current.IsSet())
|
if (current != null && current.IsSet())
|
||||||
{
|
{
|
||||||
return (current.Name, current.World);
|
return (current.Name, current.World);
|
||||||
@@ -156,24 +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()
|
||||||
{
|
{
|
||||||
// Greeted tabs are dropped before un-greeted ones (the user said
|
// Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
|
||||||
// "I'm done with that conversation"), and within each bucket we
|
// never drop candidates. They leave the bucket only via Unpin or
|
||||||
// pick the oldest LastActivity. This protects active conversations
|
// PromoteToPermanent.
|
||||||
// and unfinished greetings while still freeing up a slot.
|
var victim = Plugin
|
||||||
var victim = Plugin.Config.Tabs
|
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||||
.Select((tab, idx) => (Tab: tab, Index: idx))
|
.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
|
||||||
.Where(t => t.Tab.IsTempTab)
|
|
||||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||||
.ThenBy(t => t.Tab.LastActivity)
|
.ThenBy(t => t.Tab.LastActivity)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
@@ -183,16 +256,12 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// v0.6.1 — if the victim is currently popped out, tear down the
|
// Clean up pop-out window if tab is popped out
|
||||||
// matching Popout window first. Otherwise the window stays in
|
|
||||||
// PopOutWindows + WindowSystem and renders empty / re-spawns on the
|
|
||||||
// next AddPopOutsToDraw tick. Latent since pop-outs were introduced;
|
|
||||||
// becomes visible with AutoTellTabsOpenAsPopout where dropping a
|
|
||||||
// popped tab is now a routine code path.
|
|
||||||
if (victim.Tab.PopOut)
|
if (victim.Tab.PopOut)
|
||||||
{
|
{
|
||||||
var popout = _plugin.ChatLogWindow.ActivePopouts
|
var popout = _plugin.ChatLogWindow.ActivePopouts.FirstOrDefault(p =>
|
||||||
.FirstOrDefault(p => p.TabIdentifier == victim.Tab.Identifier);
|
p.TabIdentifier == victim.Tab.Identifier
|
||||||
|
);
|
||||||
if (popout != null)
|
if (popout != null)
|
||||||
{
|
{
|
||||||
popout.IsOpen = false;
|
popout.IsOpen = false;
|
||||||
@@ -201,8 +270,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
|
|
||||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||||
|
|
||||||
// Re-anchor the active tab so the user does not silently end up on
|
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||||
// a different conversation when their tab gets dropped or shifted.
|
|
||||||
if (victim.Index <= _plugin.LastTab)
|
if (victim.Index <= _plugin.LastTab)
|
||||||
{
|
{
|
||||||
_plugin.WantedTab = 0;
|
_plugin.WantedTab = 0;
|
||||||
@@ -213,22 +281,12 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
{
|
{
|
||||||
var tab = BuildTempTab(partner.Name, partner.World);
|
var tab = BuildTempTab(partner.Name, partner.World);
|
||||||
|
|
||||||
// Preload first so the tab opens with chronological history above
|
// Preload history: chronological order with current message already persisted
|
||||||
// the current message — and so a slow DB query never causes a
|
|
||||||
// visible "empty tab, then history pops in" effect on screen.
|
|
||||||
// The current message is already persisted in the store by the
|
|
||||||
// time MessageProcessed fires (see MessageManager.cs: UpsertMessage
|
|
||||||
// runs before the event), so we have to exclude it explicitly to
|
|
||||||
// avoid the separator landing below the live tell.
|
|
||||||
PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);
|
PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);
|
||||||
|
|
||||||
tab.AddMessage(currentMessage, unread: true);
|
tab.AddMessage(currentMessage, unread: true);
|
||||||
|
|
||||||
// Hellion Chat v0.6.1 — opt-in: open new /tell tabs directly as a
|
// Open as pop-out if configured (set before Tabs.Add for next render-tick)
|
||||||
// pop-out window. Set BEFORE Tabs.Add so the next render-tick's
|
|
||||||
// AddPopOutsToDraw() sees PopOut=true and spawns the Popout window
|
|
||||||
// alongside the tab going into the list. No SaveConfig() because
|
|
||||||
// auto-tell tabs are IsTempTab (session-only, never persisted).
|
|
||||||
if (Plugin.Config.AutoTellTabsOpenAsPopout)
|
if (Plugin.Config.AutoTellTabsOpenAsPopout)
|
||||||
{
|
{
|
||||||
tab.PopOut = true;
|
tab.PopOut = true;
|
||||||
@@ -263,9 +321,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
{
|
{
|
||||||
return $"{playerName}@{worldRow.Name}";
|
return $"{playerName}@{worldRow.Name}";
|
||||||
}
|
}
|
||||||
// World sheet lookup miss is rare (only for FFXIV worlds Dalamud has
|
// Fallback if world lookup misses (rare; only for unseen worlds)
|
||||||
// not yet seen). Fall back to the raw RowId so the user still has a
|
|
||||||
// unique, readable label.
|
|
||||||
return $"{playerName}@World{worldRowId}";
|
return $"{playerName}@World{worldRowId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,14 +335,13 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Pull one extra row because the live tell that triggered this
|
// Pull one extra row: current message is already in store and would eat a preload slot
|
||||||
// spawn is already in the store and would otherwise eat one of
|
|
||||||
// the user's preload-budget slots.
|
|
||||||
var history = _store.GetTellHistoryWithSender(
|
var history = _store.GetTellHistoryWithSender(
|
||||||
_messageManager.CurrentContentId,
|
_messageManager.CurrentContentId,
|
||||||
senderName,
|
senderName,
|
||||||
senderWorld,
|
senderWorld,
|
||||||
preloadCount + 1);
|
preloadCount + 1
|
||||||
|
);
|
||||||
|
|
||||||
var historicMessages = history
|
var historicMessages = history
|
||||||
.Where(m => m.Id != currentMessageId)
|
.Where(m => m.Id != currentMessageId)
|
||||||
@@ -295,36 +350,30 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
|
|
||||||
if (historicMessages.Count == 0)
|
if (historicMessages.Count == 0)
|
||||||
{
|
{
|
||||||
// No prior tells with this player — leave the tab to start
|
// No prior tells; leave tab empty to avoid orphaned "history loaded" marker
|
||||||
// empty so the user does not see a "history loaded" marker
|
|
||||||
// sitting alone above the very first message.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The history list is already oldest-first, so a plain AddPrune
|
// History is oldest-first; add in order for chronological display
|
||||||
// loop produces the chronological order the user expects to see
|
|
||||||
// when the tab opens.
|
|
||||||
foreach (var message in historicMessages)
|
foreach (var message in historicMessages)
|
||||||
{
|
{
|
||||||
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visible separator between the loaded history and the live
|
// Separator between history and live tell (sorts after history but before current)
|
||||||
// tell that triggered this spawn. Goes in last so it sorts
|
|
||||||
// after the historical messages but before the current one.
|
|
||||||
tab.Messages.AddPrune(
|
tab.Messages.AddPrune(
|
||||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator),
|
MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator),
|
||||||
MessageManager.MessageDisplayLimit);
|
MessageManager.MessageDisplayLimit
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Non-fatal: the tab still spawns, but the user gets a visible
|
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
||||||
// notice instead of silently missing history. The error logs
|
_logger.LogError(ex, "[AutoTellTabs] History preload failed");
|
||||||
// once with full stack trace for diagnosis.
|
|
||||||
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
|
|
||||||
tab.Messages.AddPrune(
|
tab.Messages.AddPrune(
|
||||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||||
MessageManager.MessageDisplayLimit);
|
MessageManager.MessageDisplayLimit
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,9 +409,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
|
|
||||||
lock (_tempTabsLock)
|
lock (_tempTabsLock)
|
||||||
{
|
{
|
||||||
// Frame-race guard (E5): the sidebar might still render a tab
|
// Guard against frame-race: sidebar might render a tab already removed by LRU or logout
|
||||||
// that has already been removed by LRU drop or logout cleanup.
|
|
||||||
// Silently skip the toggle so we don't mutate stale state.
|
|
||||||
if (!Plugin.Config.Tabs.Contains(tab))
|
if (!Plugin.Config.Tabs.Contains(tab))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -376,43 +423,101 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
{
|
{
|
||||||
lock (_tempTabsLock)
|
lock (_tempTabsLock)
|
||||||
{
|
{
|
||||||
// Snapshot whether the active tab is about to be removed, BEFORE
|
// Pinned TempTabs must survive char-switch — that's the whole point
|
||||||
// we mutate the list — index lookups would lie to us afterwards.
|
// 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]);
|
||||||
|
|
||||||
// v0.6.1 — symmetric to DropOldestTempTab cleanup: tear down any
|
var poppedTempTabIds = Plugin
|
||||||
// popped-out temp tab windows before removing the tabs themselves,
|
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
|
||||||
// otherwise PopOutWindows + WindowSystem keep ghost entries until
|
|
||||||
// the next plugin reload. Especially relevant once Auto-Pop-Out is
|
|
||||||
// enabled — every logout would otherwise leak as many ghosts as
|
|
||||||
// there were active /tell pop-outs.
|
|
||||||
var poppedTempTabIds = Plugin.Config.Tabs
|
|
||||||
.Where(t => t.IsTempTab && t.PopOut)
|
|
||||||
.Select(t => t.Identifier)
|
.Select(t => t.Identifier)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (poppedTempTabIds.Count > 0)
|
if (poppedTempTabIds.Count > 0)
|
||||||
{
|
{
|
||||||
var poppedSet = poppedTempTabIds.ToHashSet();
|
var poppedSet = poppedTempTabIds.ToHashSet();
|
||||||
foreach (var popout in _plugin.ChatLogWindow.ActivePopouts
|
foreach (
|
||||||
.Where(p => poppedSet.Contains(p.TabIdentifier))
|
var popout in _plugin
|
||||||
.ToList())
|
.ChatLogWindow.ActivePopouts.Where(p => poppedSet.Contains(p.TabIdentifier))
|
||||||
|
.ToList()
|
||||||
|
)
|
||||||
{
|
{
|
||||||
popout.IsOpen = false;
|
popout.IsOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||||
|
|
||||||
// Force a switch to tab 0 if the active tab was a temp tab OR
|
// Force switch to tab 0 if active tab was an unpinned temp tab or
|
||||||
// if drops before the active index pushed LastTab out of range.
|
// index is now out of range. Pinned tabs survive — no switch needed.
|
||||||
// Otherwise the user keeps their current persistent tab.
|
|
||||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||||
if (currentWasTempTab || !stillValid)
|
if (currentWasUnpinnedTempTab || !stillValid)
|
||||||
{
|
{
|
||||||
_plugin.WantedTab = 0;
|
_plugin.WantedTab = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal bool TryPin(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsTempTab || tab.IsPinned)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PinnedTempTabCount >= MaxPinnedTempTabs)
|
||||||
|
{
|
||||||
|
WrapperUtil.AddNotification(
|
||||||
|
string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
|
||||||
|
NotificationType.Warning
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsPinned = true;
|
||||||
|
_logger.LogDebug(
|
||||||
|
$"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
|
||||||
|
);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Unpin(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsPinned)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the unpinned pool is already full, dropping the oldest before
|
||||||
|
// flipping the flag avoids counting the just-unpinned tab as a drop
|
||||||
|
// candidate.
|
||||||
|
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||||
|
{
|
||||||
|
DropOldestTempTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsPinned = false;
|
||||||
|
_logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name);
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void PromoteToPermanent(Tab tab)
|
||||||
|
{
|
||||||
|
if (!tab.IsTempTab)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.IsTempTab = false;
|
||||||
|
tab.IsPinned = false;
|
||||||
|
tab.TellTarget = TellTarget.Empty();
|
||||||
|
_logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)");
|
||||||
|
_plugin.SaveConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Branding;
|
||||||
|
|
||||||
|
// Centralised — a future invite/URL rotation only touches this file.
|
||||||
|
internal static class BrandingLinks
|
||||||
|
{
|
||||||
|
public const string HellionForgeDiscordInvite = "https://discord.gg/X9V7Kcv5gR";
|
||||||
|
public const string HellionForgeGitea = "https://gitea.hellion-forge.cloud/Hellion-Forge";
|
||||||
|
public const string HellionChatRepo =
|
||||||
|
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
||||||
|
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
||||||
|
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,6 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using HellionChat.Resources;
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -10,17 +10,19 @@ internal static class ChatTwoConflictDetector
|
|||||||
|
|
||||||
public static void ThrowIfChatTwoIsLoaded(IDalamudPluginInterface pluginInterface)
|
public static void ThrowIfChatTwoIsLoaded(IDalamudPluginInterface pluginInterface)
|
||||||
{
|
{
|
||||||
var conflict = pluginInterface.InstalledPlugins
|
var conflict = pluginInterface.InstalledPlugins.FirstOrDefault(p =>
|
||||||
.FirstOrDefault(p =>
|
p.InternalName == UpstreamInternalName && p.IsLoaded
|
||||||
p.InternalName == UpstreamInternalName &&
|
);
|
||||||
p.IsLoaded);
|
|
||||||
|
|
||||||
if (conflict is null)
|
if (conflict is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var message = HellionStrings.ChatTwoConflictTitle + "\n\n" +
|
var message =
|
||||||
HellionStrings.ChatTwoConflictBody + "\n\n" +
|
HellionStrings.ChatTwoConflictTitle
|
||||||
HellionStrings.ChatTwoConflictAction;
|
+ "\n\n"
|
||||||
|
+ HellionStrings.ChatTwoConflictBody
|
||||||
|
+ "\n\n"
|
||||||
|
+ HellionStrings.ChatTwoConflictAction;
|
||||||
|
|
||||||
throw new System.InvalidOperationException(message);
|
throw new System.InvalidOperationException(message);
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-27
@@ -1,5 +1,5 @@
|
|||||||
using HellionChat.Code;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using HellionChat.Code;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
@@ -25,24 +25,23 @@ public abstract class Chunk
|
|||||||
Link = link;
|
Link = link;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal SeString? GetSeString() => Source switch
|
internal SeString? GetSeString() =>
|
||||||
{
|
Source switch
|
||||||
ChunkSource.None => null,
|
{
|
||||||
ChunkSource.Sender => Message?.SenderSource,
|
ChunkSource.None => null,
|
||||||
ChunkSource.Content => Message?.ContentSource,
|
ChunkSource.Sender => Message?.SenderSource,
|
||||||
_ => null,
|
ChunkSource.Content => Message?.ContentSource,
|
||||||
};
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
// Returns basic text for hashing (content for TextChunk, icon name for IconChunk)
|
||||||
/// Get some basic text for use in generating hashes.
|
|
||||||
/// </summary>
|
|
||||||
internal string StringValue()
|
internal string StringValue()
|
||||||
{
|
{
|
||||||
return this switch
|
return this switch
|
||||||
{
|
{
|
||||||
TextChunk text => text.Content,
|
TextChunk text => text.Content,
|
||||||
IconChunk icon => icon.Icon.ToString(),
|
IconChunk icon => icon.Icon.ToString(),
|
||||||
_ => ""
|
_ => "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,18 +56,29 @@ public enum ChunkSource
|
|||||||
[MessagePackObject(AllowPrivate = true)]
|
[MessagePackObject(AllowPrivate = true)]
|
||||||
public class TextChunk : Chunk
|
public class TextChunk : Chunk
|
||||||
{
|
{
|
||||||
[Key(2)] public ChatType? FallbackColour;
|
[Key(2)]
|
||||||
[Key(3)] public uint? Foreground;
|
public ChatType? FallbackColour;
|
||||||
[Key(4)] public uint? Glow;
|
|
||||||
[Key(5)] public bool Italic;
|
|
||||||
[Key(6)] public string Content;
|
|
||||||
|
|
||||||
private TextChunk(Chunk chunk, string content) : base(chunk.Source, chunk.Link)
|
[Key(3)]
|
||||||
|
public uint? Foreground;
|
||||||
|
|
||||||
|
[Key(4)]
|
||||||
|
public uint? Glow;
|
||||||
|
|
||||||
|
[Key(5)]
|
||||||
|
public bool Italic;
|
||||||
|
|
||||||
|
[Key(6)]
|
||||||
|
public string Content;
|
||||||
|
|
||||||
|
private TextChunk(Chunk chunk, string content)
|
||||||
|
: base(chunk.Source, chunk.Link)
|
||||||
{
|
{
|
||||||
Content = content;
|
Content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal TextChunk(ChunkSource source, Payload? link, string content) : base(source, link)
|
internal TextChunk(ChunkSource source, Payload? link, string content)
|
||||||
|
: base(source, link)
|
||||||
{
|
{
|
||||||
// This has been null in the past, and it broke rendering code.
|
// This has been null in the past, and it broke rendering code.
|
||||||
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
||||||
@@ -76,7 +86,16 @@ public class TextChunk : Chunk
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once UnusedMember.Global // Used by MessagePack
|
// ReSharper disable once UnusedMember.Global // Used by MessagePack
|
||||||
public TextChunk(ChunkSource source, Payload? link, ChatType? fallbackColour, uint? foreground, uint? glow, bool italic, string content) : base(source, link)
|
public TextChunk(
|
||||||
|
ChunkSource source,
|
||||||
|
Payload? link,
|
||||||
|
ChatType? fallbackColour,
|
||||||
|
uint? foreground,
|
||||||
|
uint? glow,
|
||||||
|
bool italic,
|
||||||
|
string content
|
||||||
|
)
|
||||||
|
: base(source, link)
|
||||||
{
|
{
|
||||||
FallbackColour = fallbackColour;
|
FallbackColour = fallbackColour;
|
||||||
Foreground = foreground;
|
Foreground = foreground;
|
||||||
@@ -87,9 +106,6 @@ public class TextChunk : Chunk
|
|||||||
Content = content ?? "";
|
Content = content ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new TextChunk with identical styling to this one.
|
|
||||||
/// </summary>
|
|
||||||
public TextChunk NewWithStyle(ChunkSource source, Payload? link, string content)
|
public TextChunk NewWithStyle(ChunkSource source, Payload? link, string content)
|
||||||
{
|
{
|
||||||
return new TextChunk(source, link, content)
|
return new TextChunk(source, link, content)
|
||||||
@@ -101,9 +117,6 @@ public class TextChunk : Chunk
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new TextChunk with identical styling to this one.
|
|
||||||
/// </summary>
|
|
||||||
public TextChunk NewWithStyle(Chunk chunk, string content)
|
public TextChunk NewWithStyle(Chunk chunk, string content)
|
||||||
{
|
{
|
||||||
return new TextChunk(chunk, content)
|
return new TextChunk(chunk, content)
|
||||||
@@ -122,7 +135,8 @@ public class IconChunk : Chunk
|
|||||||
[Key(2)]
|
[Key(2)]
|
||||||
public BitmapFontIcon Icon { get; set; }
|
public BitmapFontIcon Icon { get; set; }
|
||||||
|
|
||||||
public IconChunk(ChunkSource source, Payload? link, BitmapFontIcon icon) : base(source, link)
|
public IconChunk(ChunkSource source, Payload? link, BitmapFontIcon icon)
|
||||||
|
: base(source, link)
|
||||||
{
|
{
|
||||||
Icon = icon;
|
Icon = icon;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class ChatCode
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ChatCode(byte type, byte source, byte target)
|
public ChatCode(byte type, byte source, byte target)
|
||||||
: this((XivChatType)type, (XivChatRelationKind)source, (XivChatRelationKind)target) {}
|
: this((XivChatType)type, (XivChatRelationKind)source, (XivChatRelationKind)target) { }
|
||||||
|
|
||||||
public bool IsBattle()
|
public bool IsBattle()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,36 +7,36 @@ public enum ChatSource : ushort
|
|||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
|
|
||||||
/// <summary>The player currently controlled by the local client.</summary>
|
// The player controlled by this client
|
||||||
LocalPlayer = 1 << XivChatRelationKind.LocalPlayer,
|
LocalPlayer = 1 << XivChatRelationKind.LocalPlayer,
|
||||||
|
|
||||||
/// <summary>A player in the same 4-man or 8-man party as the local player.</summary>
|
// Member of the local party
|
||||||
PartyMember = 1 << XivChatRelationKind.PartyMember,
|
PartyMember = 1 << XivChatRelationKind.PartyMember,
|
||||||
|
|
||||||
/// <summary>A player in the same alliance raid.</summary>
|
// Member of the alliance
|
||||||
AllianceMember = 1 << XivChatRelationKind.AllianceMember,
|
AllianceMember = 1 << XivChatRelationKind.AllianceMember,
|
||||||
|
|
||||||
/// <summary>A player not in the local player's party or alliance.</summary>
|
// Other player
|
||||||
OtherPlayer = 1 << XivChatRelationKind.OtherPlayer,
|
OtherPlayer = 1 << XivChatRelationKind.OtherPlayer,
|
||||||
|
|
||||||
/// <summary>An enemy entity that is currently in combat with the player or party.</summary>
|
// Enemy in combat
|
||||||
EngagedEnemy = 1 << XivChatRelationKind.EngagedEnemy,
|
EngagedEnemy = 1 << XivChatRelationKind.EngagedEnemy,
|
||||||
|
|
||||||
/// <summary>An enemy entity that is not yet in combat or claimed.</summary>
|
// Enemy out of combat
|
||||||
UnengagedEnemy = 1 << XivChatRelationKind.UnengagedEnemy,
|
UnengagedEnemy = 1 << XivChatRelationKind.UnengagedEnemy,
|
||||||
|
|
||||||
/// <summary>An NPC that is friendly or neutral to the player (e.g., EventNPCs).</summary>
|
// Friendly NPC
|
||||||
FriendlyNpc = 1 << XivChatRelationKind.FriendlyNpc,
|
FriendlyNpc = 1 << XivChatRelationKind.FriendlyNpc,
|
||||||
|
|
||||||
/// <summary>A pet (Summoner/Scholar) or companion (Chocobo) belonging to the local player.</summary>
|
// Own pet or companion
|
||||||
PetOrCompanion = 1 << XivChatRelationKind.PetOrCompanion,
|
PetOrCompanion = 1 << XivChatRelationKind.PetOrCompanion,
|
||||||
|
|
||||||
/// <summary>A pet or companion belonging to a member of the local player's party.</summary>
|
// Pet or companion of party members
|
||||||
PetOrCompanionParty = 1 << XivChatRelationKind.PetOrCompanionParty,
|
PetOrCompanionParty = 1 << XivChatRelationKind.PetOrCompanionParty,
|
||||||
|
|
||||||
/// <summary>A pet or companion belonging to a member of the alliance.</summary>
|
// Pet or companion of alliance members
|
||||||
PetOrCompanionAlliance = 1 << XivChatRelationKind.PetOrCompanionAlliance,
|
PetOrCompanionAlliance = 1 << XivChatRelationKind.PetOrCompanionAlliance,
|
||||||
|
|
||||||
/// <summary>A pet or companion belonging to a player not in the party or alliance.</summary>
|
// Pet or companion of other players
|
||||||
PetOrCompanionOther = 1 << XivChatRelationKind.PetOrCompanionOther,
|
PetOrCompanionOther = 1 << XivChatRelationKind.PetOrCompanionOther,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,32 @@ namespace HellionChat.Code;
|
|||||||
internal static class ChatSourceExt
|
internal static class ChatSourceExt
|
||||||
{
|
{
|
||||||
internal const ChatSource All =
|
internal const ChatSource All =
|
||||||
ChatSource.LocalPlayer | ChatSource.PartyMember | ChatSource.AllianceMember |
|
ChatSource.LocalPlayer
|
||||||
ChatSource.OtherPlayer | ChatSource.EngagedEnemy | ChatSource.UnengagedEnemy |
|
| ChatSource.PartyMember
|
||||||
ChatSource.FriendlyNpc | ChatSource.PetOrCompanion | ChatSource.PetOrCompanionParty |
|
| ChatSource.AllianceMember
|
||||||
ChatSource.PetOrCompanionAlliance | ChatSource.PetOrCompanionOther;
|
| ChatSource.OtherPlayer
|
||||||
|
| ChatSource.EngagedEnemy
|
||||||
|
| ChatSource.UnengagedEnemy
|
||||||
|
| ChatSource.FriendlyNpc
|
||||||
|
| ChatSource.PetOrCompanion
|
||||||
|
| ChatSource.PetOrCompanionParty
|
||||||
|
| ChatSource.PetOrCompanionAlliance
|
||||||
|
| ChatSource.PetOrCompanionOther;
|
||||||
|
|
||||||
internal static string Name(this ChatSource source) => source switch
|
internal static string Name(this ChatSource source) =>
|
||||||
{
|
source switch
|
||||||
ChatSource.LocalPlayer => Language.ChatSource_Self,
|
{
|
||||||
ChatSource.PartyMember => Language.ChatSource_PartyMember,
|
ChatSource.LocalPlayer => Language.ChatSource_Self,
|
||||||
ChatSource.AllianceMember => Language.ChatSource_AllianceMember,
|
ChatSource.PartyMember => Language.ChatSource_PartyMember,
|
||||||
ChatSource.OtherPlayer => Language.ChatSource_Other,
|
ChatSource.AllianceMember => Language.ChatSource_AllianceMember,
|
||||||
ChatSource.EngagedEnemy => Language.ChatSource_EngagedEnemy,
|
ChatSource.OtherPlayer => Language.ChatSource_Other,
|
||||||
ChatSource.UnengagedEnemy => Language.ChatSource_UnengagedEnemy,
|
ChatSource.EngagedEnemy => Language.ChatSource_EngagedEnemy,
|
||||||
ChatSource.FriendlyNpc => Language.ChatSource_FriendlyNpc,
|
ChatSource.UnengagedEnemy => Language.ChatSource_UnengagedEnemy,
|
||||||
ChatSource.PetOrCompanion => Language.ChatSource_SelfPet,
|
ChatSource.FriendlyNpc => Language.ChatSource_FriendlyNpc,
|
||||||
ChatSource.PetOrCompanionParty => Language.ChatSource_PartyPet,
|
ChatSource.PetOrCompanion => Language.ChatSource_SelfPet,
|
||||||
ChatSource.PetOrCompanionAlliance => Language.ChatSource_AlliancePet,
|
ChatSource.PetOrCompanionParty => Language.ChatSource_PartyPet,
|
||||||
ChatSource.PetOrCompanionOther => Language.ChatSource_OtherPet,
|
ChatSource.PetOrCompanionAlliance => Language.ChatSource_AlliancePet,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(source), source, null),
|
ChatSource.PetOrCompanionOther => Language.ChatSource_OtherPet,
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(source), source, null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+265
-252
@@ -1,92 +1,98 @@
|
|||||||
|
using Dalamud.Game.Config;
|
||||||
using HellionChat.Resources;
|
using HellionChat.Resources;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Dalamud.Game.Config;
|
|
||||||
|
|
||||||
namespace HellionChat.Code;
|
namespace HellionChat.Code;
|
||||||
|
|
||||||
internal static class ChatTypeExt
|
internal static class ChatTypeExt
|
||||||
{
|
{
|
||||||
internal static IEnumerable<(string, ChatType[])> SortOrder =>
|
internal static IEnumerable<(string, ChatType[])> SortOrder =>
|
||||||
[
|
|
||||||
(Language.Options_Tabs_ChannelTypes_Special, [ChatType.Debug, ChatType.Urgent, ChatType.Notice]),
|
|
||||||
|
|
||||||
(Language.Options_Tabs_ChannelTypes_Chat,
|
|
||||||
[
|
[
|
||||||
ChatType.Say,
|
(
|
||||||
ChatType.Yell,
|
Language.Options_Tabs_ChannelTypes_Special,
|
||||||
ChatType.Shout,
|
[ChatType.Debug, ChatType.Urgent, ChatType.Notice]
|
||||||
ChatType.TellIncoming,
|
),
|
||||||
ChatType.TellOutgoing,
|
(
|
||||||
ChatType.Party,
|
Language.Options_Tabs_ChannelTypes_Chat,
|
||||||
ChatType.CrossParty,
|
[
|
||||||
ChatType.Alliance,
|
ChatType.Say,
|
||||||
ChatType.FreeCompany,
|
ChatType.Yell,
|
||||||
ChatType.PvpTeam,
|
ChatType.Shout,
|
||||||
ChatType.CrossLinkshell1,
|
ChatType.TellIncoming,
|
||||||
ChatType.CrossLinkshell2,
|
ChatType.TellOutgoing,
|
||||||
ChatType.CrossLinkshell3,
|
ChatType.Party,
|
||||||
ChatType.CrossLinkshell4,
|
ChatType.CrossParty,
|
||||||
ChatType.CrossLinkshell5,
|
ChatType.Alliance,
|
||||||
ChatType.CrossLinkshell6,
|
ChatType.FreeCompany,
|
||||||
ChatType.CrossLinkshell7,
|
ChatType.PvpTeam,
|
||||||
ChatType.CrossLinkshell8,
|
ChatType.CrossLinkshell1,
|
||||||
ChatType.Linkshell1,
|
ChatType.CrossLinkshell2,
|
||||||
ChatType.Linkshell2,
|
ChatType.CrossLinkshell3,
|
||||||
ChatType.Linkshell3,
|
ChatType.CrossLinkshell4,
|
||||||
ChatType.Linkshell4,
|
ChatType.CrossLinkshell5,
|
||||||
ChatType.Linkshell5,
|
ChatType.CrossLinkshell6,
|
||||||
ChatType.Linkshell6,
|
ChatType.CrossLinkshell7,
|
||||||
ChatType.Linkshell7,
|
ChatType.CrossLinkshell8,
|
||||||
ChatType.Linkshell8,
|
ChatType.Linkshell1,
|
||||||
ChatType.NoviceNetwork,
|
ChatType.Linkshell2,
|
||||||
ChatType.StandardEmote,
|
ChatType.Linkshell3,
|
||||||
ChatType.CustomEmote
|
ChatType.Linkshell4,
|
||||||
]),
|
ChatType.Linkshell5,
|
||||||
|
ChatType.Linkshell6,
|
||||||
(Language.Options_Tabs_ChannelTypes_Battle,
|
ChatType.Linkshell7,
|
||||||
[
|
ChatType.Linkshell8,
|
||||||
ChatType.Damage,
|
ChatType.NoviceNetwork,
|
||||||
ChatType.Miss,
|
ChatType.StandardEmote,
|
||||||
ChatType.Action,
|
ChatType.CustomEmote,
|
||||||
ChatType.Item,
|
]
|
||||||
ChatType.Healing,
|
),
|
||||||
ChatType.GainBuff,
|
(
|
||||||
ChatType.LoseBuff,
|
Language.Options_Tabs_ChannelTypes_Battle,
|
||||||
ChatType.GainDebuff,
|
[
|
||||||
ChatType.LoseDebuff
|
ChatType.Damage,
|
||||||
]),
|
ChatType.Miss,
|
||||||
|
ChatType.Action,
|
||||||
(Language.Options_Tabs_ChannelTypes_Announcements,
|
ChatType.Item,
|
||||||
[
|
ChatType.Healing,
|
||||||
ChatType.System,
|
ChatType.GainBuff,
|
||||||
ChatType.BattleSystem,
|
ChatType.LoseBuff,
|
||||||
ChatType.GatheringSystem,
|
ChatType.GainDebuff,
|
||||||
ChatType.Error,
|
ChatType.LoseDebuff,
|
||||||
ChatType.Echo,
|
]
|
||||||
ChatType.NoviceNetworkSystem,
|
),
|
||||||
ChatType.FreeCompanyAnnouncement,
|
(
|
||||||
ChatType.PvpTeamAnnouncement,
|
Language.Options_Tabs_ChannelTypes_Announcements,
|
||||||
ChatType.FreeCompanyLoginLogout,
|
[
|
||||||
ChatType.PvpTeamLoginLogout,
|
ChatType.System,
|
||||||
ChatType.RetainerSale,
|
ChatType.BattleSystem,
|
||||||
ChatType.NpcDialogue,
|
ChatType.GatheringSystem,
|
||||||
ChatType.NpcAnnouncement,
|
ChatType.Error,
|
||||||
ChatType.LootNotice,
|
ChatType.Echo,
|
||||||
ChatType.Progress,
|
ChatType.NoviceNetworkSystem,
|
||||||
ChatType.LootRoll,
|
ChatType.FreeCompanyAnnouncement,
|
||||||
ChatType.Crafting,
|
ChatType.PvpTeamAnnouncement,
|
||||||
ChatType.Gathering,
|
ChatType.FreeCompanyLoginLogout,
|
||||||
ChatType.PeriodicRecruitmentNotification,
|
ChatType.PvpTeamLoginLogout,
|
||||||
ChatType.Sign,
|
ChatType.RetainerSale,
|
||||||
ChatType.RandomNumber,
|
ChatType.NpcDialogue,
|
||||||
ChatType.Orchestrion,
|
ChatType.NpcAnnouncement,
|
||||||
ChatType.MessageBook,
|
ChatType.LootNotice,
|
||||||
ChatType.Alarm,
|
ChatType.Progress,
|
||||||
ChatType.GlamourNotifications
|
ChatType.LootRoll,
|
||||||
])
|
ChatType.Crafting,
|
||||||
// Note: ExtraChat linkshells are handled separately in the tab settings
|
ChatType.Gathering,
|
||||||
// UI.
|
ChatType.PeriodicRecruitmentNotification,
|
||||||
];
|
ChatType.Sign,
|
||||||
|
ChatType.RandomNumber,
|
||||||
|
ChatType.Orchestrion,
|
||||||
|
ChatType.MessageBook,
|
||||||
|
ChatType.Alarm,
|
||||||
|
ChatType.GlamourNotifications,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
// Note: ExtraChat linkshells are handled separately in the tab settings
|
||||||
|
// UI.
|
||||||
|
];
|
||||||
|
|
||||||
internal static string Name(this ChatType type)
|
internal static string Name(this ChatType type)
|
||||||
{
|
{
|
||||||
@@ -143,7 +149,8 @@ internal static class ChatTypeExt
|
|||||||
ChatType.FreeCompanyAnnouncement => Language.ChatType_FreeCompanyAnnouncement,
|
ChatType.FreeCompanyAnnouncement => Language.ChatType_FreeCompanyAnnouncement,
|
||||||
ChatType.FreeCompanyLoginLogout => Language.ChatType_FreeCompanyLoginLogout,
|
ChatType.FreeCompanyLoginLogout => Language.ChatType_FreeCompanyLoginLogout,
|
||||||
ChatType.RetainerSale => Language.ChatType_RetainerSale,
|
ChatType.RetainerSale => Language.ChatType_RetainerSale,
|
||||||
ChatType.PeriodicRecruitmentNotification => Language.ChatType_PeriodicRecruitmentNotification,
|
ChatType.PeriodicRecruitmentNotification =>
|
||||||
|
Language.ChatType_PeriodicRecruitmentNotification,
|
||||||
ChatType.Sign => Language.ChatType_Sign,
|
ChatType.Sign => Language.ChatType_Sign,
|
||||||
ChatType.RandomNumber => Language.ChatType_RandomNumber,
|
ChatType.RandomNumber => Language.ChatType_RandomNumber,
|
||||||
ChatType.NoviceNetworkSystem => Language.ChatType_NoviceNetworkSystem,
|
ChatType.NoviceNetworkSystem => Language.ChatType_NoviceNetworkSystem,
|
||||||
@@ -306,181 +313,187 @@ internal static class ChatTypeExt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static InputChannel? ToInputChannel(this ChatType type) => type switch
|
internal static InputChannel? ToInputChannel(this ChatType type) =>
|
||||||
{
|
type switch
|
||||||
ChatType.TellOutgoing => InputChannel.Tell,
|
{
|
||||||
ChatType.Say => InputChannel.Say,
|
ChatType.TellOutgoing => InputChannel.Tell,
|
||||||
ChatType.Party => InputChannel.Party,
|
ChatType.Say => InputChannel.Say,
|
||||||
ChatType.Alliance => InputChannel.Alliance,
|
ChatType.Party => InputChannel.Party,
|
||||||
ChatType.Yell => InputChannel.Yell,
|
ChatType.Alliance => InputChannel.Alliance,
|
||||||
ChatType.Shout => InputChannel.Shout,
|
ChatType.Yell => InputChannel.Yell,
|
||||||
ChatType.FreeCompany => InputChannel.FreeCompany,
|
ChatType.Shout => InputChannel.Shout,
|
||||||
ChatType.PvpTeam => InputChannel.PvpTeam,
|
ChatType.FreeCompany => InputChannel.FreeCompany,
|
||||||
ChatType.NoviceNetwork => InputChannel.NoviceNetwork,
|
ChatType.PvpTeam => InputChannel.PvpTeam,
|
||||||
ChatType.CrossLinkshell1 => InputChannel.CrossLinkshell1,
|
ChatType.NoviceNetwork => InputChannel.NoviceNetwork,
|
||||||
ChatType.CrossLinkshell2 => InputChannel.CrossLinkshell2,
|
ChatType.CrossLinkshell1 => InputChannel.CrossLinkshell1,
|
||||||
ChatType.CrossLinkshell3 => InputChannel.CrossLinkshell3,
|
ChatType.CrossLinkshell2 => InputChannel.CrossLinkshell2,
|
||||||
ChatType.CrossLinkshell4 => InputChannel.CrossLinkshell4,
|
ChatType.CrossLinkshell3 => InputChannel.CrossLinkshell3,
|
||||||
ChatType.CrossLinkshell5 => InputChannel.CrossLinkshell5,
|
ChatType.CrossLinkshell4 => InputChannel.CrossLinkshell4,
|
||||||
ChatType.CrossLinkshell6 => InputChannel.CrossLinkshell6,
|
ChatType.CrossLinkshell5 => InputChannel.CrossLinkshell5,
|
||||||
ChatType.CrossLinkshell7 => InputChannel.CrossLinkshell7,
|
ChatType.CrossLinkshell6 => InputChannel.CrossLinkshell6,
|
||||||
ChatType.CrossLinkshell8 => InputChannel.CrossLinkshell8,
|
ChatType.CrossLinkshell7 => InputChannel.CrossLinkshell7,
|
||||||
ChatType.Linkshell1 => InputChannel.Linkshell1,
|
ChatType.CrossLinkshell8 => InputChannel.CrossLinkshell8,
|
||||||
ChatType.Linkshell2 => InputChannel.Linkshell2,
|
ChatType.Linkshell1 => InputChannel.Linkshell1,
|
||||||
ChatType.Linkshell3 => InputChannel.Linkshell3,
|
ChatType.Linkshell2 => InputChannel.Linkshell2,
|
||||||
ChatType.Linkshell4 => InputChannel.Linkshell4,
|
ChatType.Linkshell3 => InputChannel.Linkshell3,
|
||||||
ChatType.Linkshell5 => InputChannel.Linkshell5,
|
ChatType.Linkshell4 => InputChannel.Linkshell4,
|
||||||
ChatType.Linkshell6 => InputChannel.Linkshell6,
|
ChatType.Linkshell5 => InputChannel.Linkshell5,
|
||||||
ChatType.Linkshell7 => InputChannel.Linkshell7,
|
ChatType.Linkshell6 => InputChannel.Linkshell6,
|
||||||
ChatType.Linkshell8 => InputChannel.Linkshell8,
|
ChatType.Linkshell7 => InputChannel.Linkshell7,
|
||||||
_ => null,
|
ChatType.Linkshell8 => InputChannel.Linkshell8,
|
||||||
};
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
internal static bool IsGm(this ChatType type) => type switch
|
internal static bool IsGm(this ChatType type) =>
|
||||||
{
|
type switch
|
||||||
ChatType.GmTell => true,
|
{
|
||||||
ChatType.GmSay => true,
|
ChatType.GmTell => true,
|
||||||
ChatType.GmShout => true,
|
ChatType.GmSay => true,
|
||||||
ChatType.GmYell => true,
|
ChatType.GmShout => true,
|
||||||
ChatType.GmParty => true,
|
ChatType.GmYell => true,
|
||||||
ChatType.GmFreeCompany => true,
|
ChatType.GmParty => true,
|
||||||
ChatType.GmLinkshell1 => true,
|
ChatType.GmFreeCompany => true,
|
||||||
ChatType.GmLinkshell2 => true,
|
ChatType.GmLinkshell1 => true,
|
||||||
ChatType.GmLinkshell3 => true,
|
ChatType.GmLinkshell2 => true,
|
||||||
ChatType.GmLinkshell4 => true,
|
ChatType.GmLinkshell3 => true,
|
||||||
ChatType.GmLinkshell5 => true,
|
ChatType.GmLinkshell4 => true,
|
||||||
ChatType.GmLinkshell6 => true,
|
ChatType.GmLinkshell5 => true,
|
||||||
ChatType.GmLinkshell7 => true,
|
ChatType.GmLinkshell6 => true,
|
||||||
ChatType.GmLinkshell8 => true,
|
ChatType.GmLinkshell7 => true,
|
||||||
ChatType.GmNoviceNetwork => true,
|
ChatType.GmLinkshell8 => true,
|
||||||
_ => false,
|
ChatType.GmNoviceNetwork => true,
|
||||||
};
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
internal static bool IsExtraChatLinkshell(this ChatType type) => type switch
|
internal static bool IsExtraChatLinkshell(this ChatType type) =>
|
||||||
{
|
type switch
|
||||||
ChatType.ExtraChatLinkshell1 => true,
|
{
|
||||||
ChatType.ExtraChatLinkshell2 => true,
|
ChatType.ExtraChatLinkshell1 => true,
|
||||||
ChatType.ExtraChatLinkshell3 => true,
|
ChatType.ExtraChatLinkshell2 => true,
|
||||||
ChatType.ExtraChatLinkshell4 => true,
|
ChatType.ExtraChatLinkshell3 => true,
|
||||||
ChatType.ExtraChatLinkshell5 => true,
|
ChatType.ExtraChatLinkshell4 => true,
|
||||||
ChatType.ExtraChatLinkshell6 => true,
|
ChatType.ExtraChatLinkshell5 => true,
|
||||||
ChatType.ExtraChatLinkshell7 => true,
|
ChatType.ExtraChatLinkshell6 => true,
|
||||||
ChatType.ExtraChatLinkshell8 => true,
|
ChatType.ExtraChatLinkshell7 => true,
|
||||||
_ => false,
|
ChatType.ExtraChatLinkshell8 => true,
|
||||||
};
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
public static UiConfigOption ToConfigEntry(this ChatType type) => type switch
|
public static UiConfigOption ToConfigEntry(this ChatType type) =>
|
||||||
{
|
type switch
|
||||||
ChatType.Say => UiConfigOption.ColorSay,
|
{
|
||||||
ChatType.Shout => UiConfigOption.ColorShout,
|
ChatType.Say => UiConfigOption.ColorSay,
|
||||||
ChatType.TellOutgoing => UiConfigOption.ColorTell,
|
ChatType.Shout => UiConfigOption.ColorShout,
|
||||||
ChatType.Party => UiConfigOption.ColorParty,
|
ChatType.TellOutgoing => UiConfigOption.ColorTell,
|
||||||
ChatType.Linkshell1 => UiConfigOption.ColorLS1,
|
ChatType.Party => UiConfigOption.ColorParty,
|
||||||
ChatType.Linkshell2 => UiConfigOption.ColorLS2,
|
ChatType.Linkshell1 => UiConfigOption.ColorLS1,
|
||||||
ChatType.Linkshell3 => UiConfigOption.ColorLS3,
|
ChatType.Linkshell2 => UiConfigOption.ColorLS2,
|
||||||
ChatType.Linkshell4 => UiConfigOption.ColorLS4,
|
ChatType.Linkshell3 => UiConfigOption.ColorLS3,
|
||||||
ChatType.Linkshell5 => UiConfigOption.ColorLS5,
|
ChatType.Linkshell4 => UiConfigOption.ColorLS4,
|
||||||
ChatType.Linkshell6 => UiConfigOption.ColorLS6,
|
ChatType.Linkshell5 => UiConfigOption.ColorLS5,
|
||||||
ChatType.Linkshell7 => UiConfigOption.ColorLS7,
|
ChatType.Linkshell6 => UiConfigOption.ColorLS6,
|
||||||
ChatType.Linkshell8 => UiConfigOption.ColorLS8,
|
ChatType.Linkshell7 => UiConfigOption.ColorLS7,
|
||||||
ChatType.FreeCompany => UiConfigOption.ColorFCompany,
|
ChatType.Linkshell8 => UiConfigOption.ColorLS8,
|
||||||
ChatType.NoviceNetwork => UiConfigOption.ColorBeginner,
|
ChatType.FreeCompany => UiConfigOption.ColorFCompany,
|
||||||
ChatType.CustomEmote => UiConfigOption.ColorEmoteUser,
|
ChatType.NoviceNetwork => UiConfigOption.ColorBeginner,
|
||||||
ChatType.StandardEmote => UiConfigOption.ColorEmote,
|
ChatType.CustomEmote => UiConfigOption.ColorEmoteUser,
|
||||||
ChatType.Yell => UiConfigOption.ColorYell,
|
ChatType.StandardEmote => UiConfigOption.ColorEmote,
|
||||||
ChatType.GainBuff => UiConfigOption.ColorBuffGive,
|
ChatType.Yell => UiConfigOption.ColorYell,
|
||||||
ChatType.GainDebuff => UiConfigOption.ColorDebuffGive,
|
ChatType.GainBuff => UiConfigOption.ColorBuffGive,
|
||||||
ChatType.System => UiConfigOption.ColorSysMsg,
|
ChatType.GainDebuff => UiConfigOption.ColorDebuffGive,
|
||||||
ChatType.NpcDialogue => UiConfigOption.ColorNpcSay,
|
ChatType.System => UiConfigOption.ColorSysMsg,
|
||||||
ChatType.LootRoll => UiConfigOption.ColorLoot,
|
ChatType.NpcDialogue => UiConfigOption.ColorNpcSay,
|
||||||
ChatType.FreeCompanyAnnouncement => UiConfigOption.ColorFCAnnounce,
|
ChatType.LootRoll => UiConfigOption.ColorLoot,
|
||||||
ChatType.PvpTeamAnnouncement => UiConfigOption.ColorPvPGroupAnnounce,
|
ChatType.FreeCompanyAnnouncement => UiConfigOption.ColorFCAnnounce,
|
||||||
_ => UiConfigOption.ColorSay,
|
ChatType.PvpTeamAnnouncement => UiConfigOption.ColorPvPGroupAnnounce,
|
||||||
};
|
_ => UiConfigOption.ColorSay,
|
||||||
|
};
|
||||||
|
|
||||||
internal static bool HasSource(this ChatType type) => type switch
|
internal static bool HasSource(this ChatType type) =>
|
||||||
{
|
type switch
|
||||||
// Battle
|
{
|
||||||
ChatType.Damage => true,
|
// Battle
|
||||||
ChatType.Miss => true,
|
ChatType.Damage => true,
|
||||||
ChatType.Action => true,
|
ChatType.Miss => true,
|
||||||
ChatType.Item => true,
|
ChatType.Action => true,
|
||||||
ChatType.Healing => true,
|
ChatType.Item => true,
|
||||||
ChatType.GainBuff => true,
|
ChatType.Healing => true,
|
||||||
ChatType.LoseBuff => true,
|
ChatType.GainBuff => true,
|
||||||
ChatType.GainDebuff => true,
|
ChatType.LoseBuff => true,
|
||||||
ChatType.LoseDebuff => true,
|
ChatType.GainDebuff => true,
|
||||||
|
ChatType.LoseDebuff => true,
|
||||||
|
|
||||||
// Announcements
|
// Announcements
|
||||||
ChatType.System => true,
|
ChatType.System => true,
|
||||||
ChatType.BattleSystem => true,
|
ChatType.BattleSystem => true,
|
||||||
ChatType.Error => true,
|
ChatType.Error => true,
|
||||||
ChatType.LootNotice => true,
|
ChatType.LootNotice => true,
|
||||||
ChatType.Progress => true,
|
ChatType.Progress => true,
|
||||||
ChatType.LootRoll => true,
|
ChatType.LootRoll => true,
|
||||||
ChatType.Crafting => true,
|
ChatType.Crafting => true,
|
||||||
ChatType.Gathering => true,
|
ChatType.Gathering => true,
|
||||||
ChatType.FreeCompanyLoginLogout => true,
|
ChatType.FreeCompanyLoginLogout => true,
|
||||||
ChatType.PvpTeamLoginLogout => true,
|
ChatType.PvpTeamLoginLogout => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
internal static ChatType Parent(this ChatType type) => type switch
|
internal static ChatType Parent(this ChatType type) =>
|
||||||
{
|
type switch
|
||||||
ChatType.Say => ChatType.Say,
|
{
|
||||||
ChatType.GmSay => ChatType.Say,
|
ChatType.Say => ChatType.Say,
|
||||||
ChatType.Shout => ChatType.Shout,
|
ChatType.GmSay => ChatType.Say,
|
||||||
ChatType.GmShout => ChatType.Shout,
|
ChatType.Shout => ChatType.Shout,
|
||||||
ChatType.TellOutgoing => ChatType.TellOutgoing,
|
ChatType.GmShout => ChatType.Shout,
|
||||||
ChatType.TellIncoming => ChatType.TellOutgoing,
|
ChatType.TellOutgoing => ChatType.TellOutgoing,
|
||||||
ChatType.GmTell => ChatType.TellOutgoing,
|
ChatType.TellIncoming => ChatType.TellOutgoing,
|
||||||
ChatType.Party => ChatType.Party,
|
ChatType.GmTell => ChatType.TellOutgoing,
|
||||||
ChatType.CrossParty => ChatType.Party,
|
ChatType.Party => ChatType.Party,
|
||||||
ChatType.GmParty => ChatType.Party,
|
ChatType.CrossParty => ChatType.Party,
|
||||||
ChatType.Linkshell1 => ChatType.Linkshell1,
|
ChatType.GmParty => ChatType.Party,
|
||||||
ChatType.GmLinkshell1 => ChatType.Linkshell1,
|
ChatType.Linkshell1 => ChatType.Linkshell1,
|
||||||
ChatType.Linkshell2 => ChatType.Linkshell2,
|
ChatType.GmLinkshell1 => ChatType.Linkshell1,
|
||||||
ChatType.GmLinkshell2 => ChatType.Linkshell2,
|
ChatType.Linkshell2 => ChatType.Linkshell2,
|
||||||
ChatType.Linkshell3 => ChatType.Linkshell3,
|
ChatType.GmLinkshell2 => ChatType.Linkshell2,
|
||||||
ChatType.GmLinkshell3 => ChatType.Linkshell3,
|
ChatType.Linkshell3 => ChatType.Linkshell3,
|
||||||
ChatType.Linkshell4 => ChatType.Linkshell4,
|
ChatType.GmLinkshell3 => ChatType.Linkshell3,
|
||||||
ChatType.GmLinkshell4 => ChatType.Linkshell4,
|
ChatType.Linkshell4 => ChatType.Linkshell4,
|
||||||
ChatType.Linkshell5 => ChatType.Linkshell5,
|
ChatType.GmLinkshell4 => ChatType.Linkshell4,
|
||||||
ChatType.GmLinkshell5 => ChatType.Linkshell5,
|
ChatType.Linkshell5 => ChatType.Linkshell5,
|
||||||
ChatType.Linkshell6 => ChatType.Linkshell6,
|
ChatType.GmLinkshell5 => ChatType.Linkshell5,
|
||||||
ChatType.GmLinkshell6 => ChatType.Linkshell6,
|
ChatType.Linkshell6 => ChatType.Linkshell6,
|
||||||
ChatType.Linkshell7 => ChatType.Linkshell7,
|
ChatType.GmLinkshell6 => ChatType.Linkshell6,
|
||||||
ChatType.GmLinkshell7 => ChatType.Linkshell7,
|
ChatType.Linkshell7 => ChatType.Linkshell7,
|
||||||
ChatType.Linkshell8 => ChatType.Linkshell8,
|
ChatType.GmLinkshell7 => ChatType.Linkshell7,
|
||||||
ChatType.GmLinkshell8 => ChatType.Linkshell8,
|
ChatType.Linkshell8 => ChatType.Linkshell8,
|
||||||
ChatType.FreeCompany => ChatType.FreeCompany,
|
ChatType.GmLinkshell8 => ChatType.Linkshell8,
|
||||||
ChatType.GmFreeCompany => ChatType.FreeCompany,
|
ChatType.FreeCompany => ChatType.FreeCompany,
|
||||||
ChatType.NoviceNetwork => ChatType.NoviceNetwork,
|
ChatType.GmFreeCompany => ChatType.FreeCompany,
|
||||||
ChatType.GmNoviceNetwork => ChatType.NoviceNetwork,
|
ChatType.NoviceNetwork => ChatType.NoviceNetwork,
|
||||||
ChatType.CustomEmote => ChatType.CustomEmote,
|
ChatType.GmNoviceNetwork => ChatType.NoviceNetwork,
|
||||||
ChatType.StandardEmote => ChatType.StandardEmote,
|
ChatType.CustomEmote => ChatType.CustomEmote,
|
||||||
ChatType.Yell => ChatType.Yell,
|
ChatType.StandardEmote => ChatType.StandardEmote,
|
||||||
ChatType.GmYell => ChatType.Yell,
|
ChatType.Yell => ChatType.Yell,
|
||||||
ChatType.GainBuff => ChatType.GainBuff,
|
ChatType.GmYell => ChatType.Yell,
|
||||||
ChatType.LoseBuff => ChatType.GainBuff,
|
ChatType.GainBuff => ChatType.GainBuff,
|
||||||
ChatType.GainDebuff => ChatType.GainDebuff,
|
ChatType.LoseBuff => ChatType.GainBuff,
|
||||||
ChatType.LoseDebuff => ChatType.GainDebuff,
|
ChatType.GainDebuff => ChatType.GainDebuff,
|
||||||
ChatType.System => ChatType.System,
|
ChatType.LoseDebuff => ChatType.GainDebuff,
|
||||||
ChatType.Alarm => ChatType.System,
|
ChatType.System => ChatType.System,
|
||||||
ChatType.GlamourNotifications => ChatType.System,
|
ChatType.Alarm => ChatType.System,
|
||||||
ChatType.RetainerSale => ChatType.System,
|
ChatType.GlamourNotifications => ChatType.System,
|
||||||
ChatType.PeriodicRecruitmentNotification => ChatType.System,
|
ChatType.RetainerSale => ChatType.System,
|
||||||
ChatType.Sign => ChatType.System,
|
ChatType.PeriodicRecruitmentNotification => ChatType.System,
|
||||||
ChatType.Orchestrion => ChatType.System,
|
ChatType.Sign => ChatType.System,
|
||||||
ChatType.MessageBook => ChatType.System,
|
ChatType.Orchestrion => ChatType.System,
|
||||||
ChatType.NpcDialogue => ChatType.NpcDialogue,
|
ChatType.MessageBook => ChatType.System,
|
||||||
ChatType.NpcAnnouncement => ChatType.NpcDialogue,
|
ChatType.NpcDialogue => ChatType.NpcDialogue,
|
||||||
ChatType.LootRoll => ChatType.LootRoll,
|
ChatType.NpcAnnouncement => ChatType.NpcDialogue,
|
||||||
ChatType.RandomNumber => ChatType.LootRoll,
|
ChatType.LootRoll => ChatType.LootRoll,
|
||||||
ChatType.FreeCompanyAnnouncement => ChatType.FreeCompanyAnnouncement,
|
ChatType.RandomNumber => ChatType.LootRoll,
|
||||||
ChatType.FreeCompanyLoginLogout => ChatType.FreeCompanyAnnouncement,
|
ChatType.FreeCompanyAnnouncement => ChatType.FreeCompanyAnnouncement,
|
||||||
ChatType.PvpTeamAnnouncement => ChatType.PvpTeamAnnouncement,
|
ChatType.FreeCompanyLoginLogout => ChatType.FreeCompanyAnnouncement,
|
||||||
ChatType.PvpTeamLoginLogout => ChatType.PvpTeamAnnouncement,
|
ChatType.PvpTeamAnnouncement => ChatType.PvpTeamAnnouncement,
|
||||||
_ => type,
|
ChatType.PvpTeamLoginLogout => ChatType.PvpTeamAnnouncement,
|
||||||
};
|
_ => type,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-145
@@ -4,111 +4,114 @@ namespace HellionChat.Code;
|
|||||||
|
|
||||||
internal static class InputChannelExt
|
internal static class InputChannelExt
|
||||||
{
|
{
|
||||||
internal static ChatType ToChatType(this InputChannel input) => input switch
|
internal static ChatType ToChatType(this InputChannel input) =>
|
||||||
{
|
input switch
|
||||||
InputChannel.Tell => ChatType.TellOutgoing,
|
{
|
||||||
InputChannel.Say => ChatType.Say,
|
InputChannel.Tell => ChatType.TellOutgoing,
|
||||||
InputChannel.Party => ChatType.Party,
|
InputChannel.Say => ChatType.Say,
|
||||||
InputChannel.Alliance => ChatType.Alliance,
|
InputChannel.Party => ChatType.Party,
|
||||||
InputChannel.Yell => ChatType.Yell,
|
InputChannel.Alliance => ChatType.Alliance,
|
||||||
InputChannel.Shout => ChatType.Shout,
|
InputChannel.Yell => ChatType.Yell,
|
||||||
InputChannel.FreeCompany => ChatType.FreeCompany,
|
InputChannel.Shout => ChatType.Shout,
|
||||||
InputChannel.PvpTeam => ChatType.PvpTeam,
|
InputChannel.FreeCompany => ChatType.FreeCompany,
|
||||||
InputChannel.NoviceNetwork => ChatType.NoviceNetwork,
|
InputChannel.PvpTeam => ChatType.PvpTeam,
|
||||||
InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1,
|
InputChannel.NoviceNetwork => ChatType.NoviceNetwork,
|
||||||
InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2,
|
InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1,
|
||||||
InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3,
|
InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2,
|
||||||
InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4,
|
InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3,
|
||||||
InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5,
|
InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4,
|
||||||
InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6,
|
InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5,
|
||||||
InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7,
|
InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6,
|
||||||
InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8,
|
InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7,
|
||||||
InputChannel.Linkshell1 => ChatType.Linkshell1,
|
InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8,
|
||||||
InputChannel.Linkshell2 => ChatType.Linkshell2,
|
InputChannel.Linkshell1 => ChatType.Linkshell1,
|
||||||
InputChannel.Linkshell3 => ChatType.Linkshell3,
|
InputChannel.Linkshell2 => ChatType.Linkshell2,
|
||||||
InputChannel.Linkshell4 => ChatType.Linkshell4,
|
InputChannel.Linkshell3 => ChatType.Linkshell3,
|
||||||
InputChannel.Linkshell5 => ChatType.Linkshell5,
|
InputChannel.Linkshell4 => ChatType.Linkshell4,
|
||||||
InputChannel.Linkshell6 => ChatType.Linkshell6,
|
InputChannel.Linkshell5 => ChatType.Linkshell5,
|
||||||
InputChannel.Linkshell7 => ChatType.Linkshell7,
|
InputChannel.Linkshell6 => ChatType.Linkshell6,
|
||||||
InputChannel.Linkshell8 => ChatType.Linkshell8,
|
InputChannel.Linkshell7 => ChatType.Linkshell7,
|
||||||
InputChannel.ExtraChatLinkshell1 => ChatType.ExtraChatLinkshell1,
|
InputChannel.Linkshell8 => ChatType.Linkshell8,
|
||||||
InputChannel.ExtraChatLinkshell2 => ChatType.ExtraChatLinkshell2,
|
InputChannel.ExtraChatLinkshell1 => ChatType.ExtraChatLinkshell1,
|
||||||
InputChannel.ExtraChatLinkshell3 => ChatType.ExtraChatLinkshell3,
|
InputChannel.ExtraChatLinkshell2 => ChatType.ExtraChatLinkshell2,
|
||||||
InputChannel.ExtraChatLinkshell4 => ChatType.ExtraChatLinkshell4,
|
InputChannel.ExtraChatLinkshell3 => ChatType.ExtraChatLinkshell3,
|
||||||
InputChannel.ExtraChatLinkshell5 => ChatType.ExtraChatLinkshell5,
|
InputChannel.ExtraChatLinkshell4 => ChatType.ExtraChatLinkshell4,
|
||||||
InputChannel.ExtraChatLinkshell6 => ChatType.ExtraChatLinkshell6,
|
InputChannel.ExtraChatLinkshell5 => ChatType.ExtraChatLinkshell5,
|
||||||
InputChannel.ExtraChatLinkshell7 => ChatType.ExtraChatLinkshell7,
|
InputChannel.ExtraChatLinkshell6 => ChatType.ExtraChatLinkshell6,
|
||||||
InputChannel.ExtraChatLinkshell8 => ChatType.ExtraChatLinkshell8,
|
InputChannel.ExtraChatLinkshell7 => ChatType.ExtraChatLinkshell7,
|
||||||
InputChannel.Invalid => ChatType.Echo,
|
InputChannel.ExtraChatLinkshell8 => ChatType.ExtraChatLinkshell8,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null),
|
InputChannel.Invalid => ChatType.Echo,
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null),
|
||||||
|
};
|
||||||
|
|
||||||
public static uint LinkshellIndex(this InputChannel channel) => channel switch
|
public static uint LinkshellIndex(this InputChannel channel) =>
|
||||||
{
|
channel switch
|
||||||
InputChannel.Linkshell1 => 0,
|
{
|
||||||
InputChannel.Linkshell2 => 1,
|
InputChannel.Linkshell1 => 0,
|
||||||
InputChannel.Linkshell3 => 2,
|
InputChannel.Linkshell2 => 1,
|
||||||
InputChannel.Linkshell4 => 3,
|
InputChannel.Linkshell3 => 2,
|
||||||
InputChannel.Linkshell5 => 4,
|
InputChannel.Linkshell4 => 3,
|
||||||
InputChannel.Linkshell6 => 5,
|
InputChannel.Linkshell5 => 4,
|
||||||
InputChannel.Linkshell7 => 6,
|
InputChannel.Linkshell6 => 5,
|
||||||
InputChannel.Linkshell8 => 7,
|
InputChannel.Linkshell7 => 6,
|
||||||
InputChannel.CrossLinkshell1 => 0,
|
InputChannel.Linkshell8 => 7,
|
||||||
InputChannel.CrossLinkshell2 => 1,
|
InputChannel.CrossLinkshell1 => 0,
|
||||||
InputChannel.CrossLinkshell3 => 2,
|
InputChannel.CrossLinkshell2 => 1,
|
||||||
InputChannel.CrossLinkshell4 => 3,
|
InputChannel.CrossLinkshell3 => 2,
|
||||||
InputChannel.CrossLinkshell5 => 4,
|
InputChannel.CrossLinkshell4 => 3,
|
||||||
InputChannel.CrossLinkshell6 => 5,
|
InputChannel.CrossLinkshell5 => 4,
|
||||||
InputChannel.CrossLinkshell7 => 6,
|
InputChannel.CrossLinkshell6 => 5,
|
||||||
InputChannel.CrossLinkshell8 => 7,
|
InputChannel.CrossLinkshell7 => 6,
|
||||||
InputChannel.ExtraChatLinkshell1 => 0,
|
InputChannel.CrossLinkshell8 => 7,
|
||||||
InputChannel.ExtraChatLinkshell2 => 1,
|
InputChannel.ExtraChatLinkshell1 => 0,
|
||||||
InputChannel.ExtraChatLinkshell3 => 2,
|
InputChannel.ExtraChatLinkshell2 => 1,
|
||||||
InputChannel.ExtraChatLinkshell4 => 3,
|
InputChannel.ExtraChatLinkshell3 => 2,
|
||||||
InputChannel.ExtraChatLinkshell5 => 4,
|
InputChannel.ExtraChatLinkshell4 => 3,
|
||||||
InputChannel.ExtraChatLinkshell6 => 5,
|
InputChannel.ExtraChatLinkshell5 => 4,
|
||||||
InputChannel.ExtraChatLinkshell7 => 6,
|
InputChannel.ExtraChatLinkshell6 => 5,
|
||||||
InputChannel.ExtraChatLinkshell8 => 7,
|
InputChannel.ExtraChatLinkshell7 => 6,
|
||||||
_ => uint.MaxValue,
|
InputChannel.ExtraChatLinkshell8 => 7,
|
||||||
};
|
_ => uint.MaxValue,
|
||||||
|
};
|
||||||
|
|
||||||
public static string Prefix(this InputChannel channel) => channel switch
|
public static string Prefix(this InputChannel channel) =>
|
||||||
{
|
channel switch
|
||||||
InputChannel.Tell => "/t",
|
{
|
||||||
InputChannel.Say => "/s",
|
InputChannel.Tell => "/t",
|
||||||
InputChannel.Party => "/p",
|
InputChannel.Say => "/s",
|
||||||
InputChannel.Alliance => "/a",
|
InputChannel.Party => "/p",
|
||||||
InputChannel.Yell => "/y",
|
InputChannel.Alliance => "/a",
|
||||||
InputChannel.Shout => "/sh",
|
InputChannel.Yell => "/y",
|
||||||
InputChannel.FreeCompany => "/fc",
|
InputChannel.Shout => "/sh",
|
||||||
InputChannel.PvpTeam => "/pt",
|
InputChannel.FreeCompany => "/fc",
|
||||||
InputChannel.NoviceNetwork => "/b",
|
InputChannel.PvpTeam => "/pt",
|
||||||
InputChannel.CrossLinkshell1 => "/cwl1",
|
InputChannel.NoviceNetwork => "/b",
|
||||||
InputChannel.CrossLinkshell2 => "/cwl2",
|
InputChannel.CrossLinkshell1 => "/cwl1",
|
||||||
InputChannel.CrossLinkshell3 => "/cwl3",
|
InputChannel.CrossLinkshell2 => "/cwl2",
|
||||||
InputChannel.CrossLinkshell4 => "/cwl4",
|
InputChannel.CrossLinkshell3 => "/cwl3",
|
||||||
InputChannel.CrossLinkshell5 => "/cwl5",
|
InputChannel.CrossLinkshell4 => "/cwl4",
|
||||||
InputChannel.CrossLinkshell6 => "/cwl6",
|
InputChannel.CrossLinkshell5 => "/cwl5",
|
||||||
InputChannel.CrossLinkshell7 => "/cwl7",
|
InputChannel.CrossLinkshell6 => "/cwl6",
|
||||||
InputChannel.CrossLinkshell8 => "/cwl8",
|
InputChannel.CrossLinkshell7 => "/cwl7",
|
||||||
InputChannel.Linkshell1 => "/l1",
|
InputChannel.CrossLinkshell8 => "/cwl8",
|
||||||
InputChannel.Linkshell2 => "/l2",
|
InputChannel.Linkshell1 => "/l1",
|
||||||
InputChannel.Linkshell3 => "/l3",
|
InputChannel.Linkshell2 => "/l2",
|
||||||
InputChannel.Linkshell4 => "/l4",
|
InputChannel.Linkshell3 => "/l3",
|
||||||
InputChannel.Linkshell5 => "/l5",
|
InputChannel.Linkshell4 => "/l4",
|
||||||
InputChannel.Linkshell6 => "/l6",
|
InputChannel.Linkshell5 => "/l5",
|
||||||
InputChannel.Linkshell7 => "/l7",
|
InputChannel.Linkshell6 => "/l6",
|
||||||
InputChannel.Linkshell8 => "/l8",
|
InputChannel.Linkshell7 => "/l7",
|
||||||
InputChannel.ExtraChatLinkshell1 => "/ecl1",
|
InputChannel.Linkshell8 => "/l8",
|
||||||
InputChannel.ExtraChatLinkshell2 => "/ecl2",
|
InputChannel.ExtraChatLinkshell1 => "/ecl1",
|
||||||
InputChannel.ExtraChatLinkshell3 => "/ecl3",
|
InputChannel.ExtraChatLinkshell2 => "/ecl2",
|
||||||
InputChannel.ExtraChatLinkshell4 => "/ecl4",
|
InputChannel.ExtraChatLinkshell3 => "/ecl3",
|
||||||
InputChannel.ExtraChatLinkshell5 => "/ecl5",
|
InputChannel.ExtraChatLinkshell4 => "/ecl4",
|
||||||
InputChannel.ExtraChatLinkshell6 => "/ecl6",
|
InputChannel.ExtraChatLinkshell5 => "/ecl5",
|
||||||
InputChannel.ExtraChatLinkshell7 => "/ecl7",
|
InputChannel.ExtraChatLinkshell6 => "/ecl6",
|
||||||
InputChannel.ExtraChatLinkshell8 => "/ecl8",
|
InputChannel.ExtraChatLinkshell7 => "/ecl7",
|
||||||
_ => "/e",
|
InputChannel.ExtraChatLinkshell8 => "/ecl8",
|
||||||
};
|
_ => "/e",
|
||||||
|
};
|
||||||
|
|
||||||
public static IEnumerable<TextCommand>? TextCommands(this InputChannel channel)
|
public static IEnumerable<TextCommand>? TextCommands(this InputChannel channel)
|
||||||
{
|
{
|
||||||
@@ -145,51 +148,56 @@ internal static class InputChannelExt
|
|||||||
if (ids.Length == 0)
|
if (ids.Length == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return ids.Where(id => Sheets.TextCommandSheet.HasRow(id)).Select(id => Sheets.TextCommandSheet.GetRow(id));
|
return ids.Where(id => Sheets.TextCommandSheet.HasRow(id))
|
||||||
|
.Select(id => Sheets.TextCommandSheet.GetRow(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsLinkshell(this InputChannel channel) => channel switch
|
internal static bool IsLinkshell(this InputChannel channel) =>
|
||||||
{
|
channel switch
|
||||||
InputChannel.Linkshell1 => true,
|
{
|
||||||
InputChannel.Linkshell2 => true,
|
InputChannel.Linkshell1 => true,
|
||||||
InputChannel.Linkshell3 => true,
|
InputChannel.Linkshell2 => true,
|
||||||
InputChannel.Linkshell4 => true,
|
InputChannel.Linkshell3 => true,
|
||||||
InputChannel.Linkshell5 => true,
|
InputChannel.Linkshell4 => true,
|
||||||
InputChannel.Linkshell6 => true,
|
InputChannel.Linkshell5 => true,
|
||||||
InputChannel.Linkshell7 => true,
|
InputChannel.Linkshell6 => true,
|
||||||
InputChannel.Linkshell8 => true,
|
InputChannel.Linkshell7 => true,
|
||||||
_ => false,
|
InputChannel.Linkshell8 => true,
|
||||||
};
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
internal static bool IsCrossLinkshell(this InputChannel channel) => channel switch
|
internal static bool IsCrossLinkshell(this InputChannel channel) =>
|
||||||
{
|
channel switch
|
||||||
InputChannel.CrossLinkshell1 => true,
|
{
|
||||||
InputChannel.CrossLinkshell2 => true,
|
InputChannel.CrossLinkshell1 => true,
|
||||||
InputChannel.CrossLinkshell3 => true,
|
InputChannel.CrossLinkshell2 => true,
|
||||||
InputChannel.CrossLinkshell4 => true,
|
InputChannel.CrossLinkshell3 => true,
|
||||||
InputChannel.CrossLinkshell5 => true,
|
InputChannel.CrossLinkshell4 => true,
|
||||||
InputChannel.CrossLinkshell6 => true,
|
InputChannel.CrossLinkshell5 => true,
|
||||||
InputChannel.CrossLinkshell7 => true,
|
InputChannel.CrossLinkshell6 => true,
|
||||||
InputChannel.CrossLinkshell8 => true,
|
InputChannel.CrossLinkshell7 => true,
|
||||||
_ => false,
|
InputChannel.CrossLinkshell8 => true,
|
||||||
};
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
internal static bool IsExtraChatLinkshell(this InputChannel channel) => channel switch
|
internal static bool IsExtraChatLinkshell(this InputChannel channel) =>
|
||||||
{
|
channel switch
|
||||||
InputChannel.ExtraChatLinkshell1 => true,
|
{
|
||||||
InputChannel.ExtraChatLinkshell2 => true,
|
InputChannel.ExtraChatLinkshell1 => true,
|
||||||
InputChannel.ExtraChatLinkshell3 => true,
|
InputChannel.ExtraChatLinkshell2 => true,
|
||||||
InputChannel.ExtraChatLinkshell4 => true,
|
InputChannel.ExtraChatLinkshell3 => true,
|
||||||
InputChannel.ExtraChatLinkshell5 => true,
|
InputChannel.ExtraChatLinkshell4 => true,
|
||||||
InputChannel.ExtraChatLinkshell6 => true,
|
InputChannel.ExtraChatLinkshell5 => true,
|
||||||
InputChannel.ExtraChatLinkshell7 => true,
|
InputChannel.ExtraChatLinkshell6 => true,
|
||||||
InputChannel.ExtraChatLinkshell8 => true,
|
InputChannel.ExtraChatLinkshell7 => true,
|
||||||
_ => false,
|
InputChannel.ExtraChatLinkshell8 => true,
|
||||||
};
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
internal static bool IsValid(this InputChannel channel) => channel switch
|
internal static bool IsValid(this InputChannel channel) =>
|
||||||
{
|
channel switch
|
||||||
InputChannel.Invalid => false,
|
{
|
||||||
_ => true,
|
InputChannel.Invalid => false,
|
||||||
};
|
_ => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-8
@@ -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()
|
||||||
{
|
{
|
||||||
@@ -16,15 +23,22 @@ internal sealed class Commands : IDisposable
|
|||||||
{
|
{
|
||||||
foreach (var wrapper in Registered.Values)
|
foreach (var wrapper in Registered.Values)
|
||||||
{
|
{
|
||||||
Plugin.CommandManager.AddHandler(wrapper.Name, new CommandInfo(Invoke)
|
Plugin.CommandManager.AddHandler(
|
||||||
{
|
wrapper.Name,
|
||||||
HelpMessage = wrapper.Description ?? string.Empty,
|
new CommandInfo(Invoke)
|
||||||
ShowInHelp = wrapper.ShowInHelp,
|
{
|
||||||
});
|
HelpMessage = wrapper.Description ?? string.Empty,
|
||||||
|
ShowInHelp = wrapper.ShowInHelp,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal CommandWrapper Register(string name, string? description = null, bool? showInHelp = null)
|
internal CommandWrapper Register(
|
||||||
|
string name,
|
||||||
|
string? description = null,
|
||||||
|
bool? showInHelp = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (Registered.TryGetValue(name, out var wrapper))
|
if (Registered.TryGetValue(name, out var wrapper))
|
||||||
{
|
{
|
||||||
@@ -45,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+338
-272
@@ -1,14 +1,14 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using HellionChat.Code;
|
|
||||||
using HellionChat.GameFunctions.Types;
|
|
||||||
using HellionChat.Resources;
|
|
||||||
using HellionChat.Util;
|
|
||||||
using Dalamud;
|
using Dalamud;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Configuration;
|
using Dalamud.Configuration;
|
||||||
using Dalamud.Game.ClientState.Keys;
|
using Dalamud.Game.ClientState.Keys;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Interface.FontIdentifier;
|
using Dalamud.Interface.FontIdentifier;
|
||||||
using Dalamud.Bindings.ImGui;
|
using HellionChat.Code;
|
||||||
|
using HellionChat.GameFunctions.Types;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -27,37 +27,48 @@ public class ConfigKeyBind
|
|||||||
modString += Language.Keybind_Modifier_Shift + " + ";
|
modString += Language.Keybind_Modifier_Shift + " + ";
|
||||||
if (Modifier.HasFlag(ModifierFlag.Alt))
|
if (Modifier.HasFlag(ModifierFlag.Alt))
|
||||||
modString += Language.Keybind_Modifier_Alt + " + ";
|
modString += Language.Keybind_Modifier_Alt + " + ";
|
||||||
return modString+Key.GetFancyName();
|
return modString + Key.GetFancyName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 15;
|
private const int LatestVersion = 17;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
// v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
|
// Slug-based; ThemeRegistry resolves the object at runtime.
|
||||||
public string Theme = "hellion-arctic";
|
public string Theme = "hellion-arctic";
|
||||||
|
|
||||||
// v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
|
// Global window opacity, applied across all themes.
|
||||||
// HellionThemeWindowOpacity beim Bump v13 → v14.
|
|
||||||
public float WindowOpacity = 0.85f;
|
public float WindowOpacity = 0.85f;
|
||||||
|
|
||||||
// v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden
|
// Reserved for future UI toggles; pre-declared to avoid a migration later.
|
||||||
// vorab angelegt, damit später keine Migration nötig ist.
|
|
||||||
public bool ReduceMotion;
|
public bool ReduceMotion;
|
||||||
public bool UseCompactDensity;
|
|
||||||
public bool ShowThemeQuickPicker;
|
|
||||||
|
|
||||||
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
// v1.2.1: default flipped false → true. Compact single-line layout is
|
||||||
// Master-switch defaults to true; set false to restore upstream behavior.
|
// more readable than the card-rows layout introduced in v1.2.0.
|
||||||
|
public bool UseCompactDensity = true;
|
||||||
|
|
||||||
|
// Privacy by Default master switch. Set false to restore upstream behaviour.
|
||||||
public bool PrivacyFilterEnabled = true;
|
public bool PrivacyFilterEnabled = true;
|
||||||
|
|
||||||
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
||||||
public HashSet<ChatType> PrivacyPersistChannels = [];
|
public HashSet<ChatType> PrivacyPersistChannels = [];
|
||||||
// Failsafe for ChatTypes added by future FFXIV patches we don't know about.
|
|
||||||
public bool PrivacyPersistUnknownChannels;
|
// Failsafe for ChatTypes added by future FFXIV patches. New configs default
|
||||||
|
// to the failsafe via PrivacyDefaults; existing configs keep their saved
|
||||||
|
// choice because the deserializer overrides this initializer.
|
||||||
|
public bool PrivacyPersistUnknownChannels = Privacy
|
||||||
|
.PrivacyDefaults
|
||||||
|
.DefaultPersistUnknownChannels;
|
||||||
|
|
||||||
|
// F3.2: dedup unknown-ChatType warnings so a chatty filter doesn't spam
|
||||||
|
// the log every frame. NonSerialized so the warning fires once per
|
||||||
|
// runtime, not once-ever-per-install.
|
||||||
|
[NonSerialized]
|
||||||
|
private readonly HashSet<ChatType> _warnedUnknownChannels = new();
|
||||||
|
|
||||||
public bool IsAllowedForStorage(ChatType type)
|
public bool IsAllowedForStorage(ChatType type)
|
||||||
{
|
{
|
||||||
@@ -65,71 +76,61 @@ public class Configuration : IPluginConfiguration
|
|||||||
return true;
|
return true;
|
||||||
if (PrivacyPersistChannels.Contains(type))
|
if (PrivacyPersistChannels.Contains(type))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
// F3.2: log first occurrence of a ChatType the running build doesn't
|
||||||
|
// recognise — i.e. one a future FFXIV patch may have added. Known
|
||||||
|
// types the user opted out of are routed through the failsafe
|
||||||
|
// silently, like before.
|
||||||
|
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
||||||
|
{
|
||||||
|
Plugin.LogProxy.Warning(
|
||||||
|
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
||||||
|
type,
|
||||||
|
PrivacyPersistUnknownChannels
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return PrivacyPersistUnknownChannels;
|
return PrivacyPersistUnknownChannels;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hellion Chat — Message retention (GDPR data minimization, time axis).
|
// Retention master switch defaults to false — plugin will not delete
|
||||||
// Master switch defaults to false; the plugin will not delete history
|
// history until the user explicitly opts in.
|
||||||
// until the user explicitly opts in.
|
|
||||||
public bool RetentionEnabled;
|
public bool RetentionEnabled;
|
||||||
public int RetentionDefaultDays = 30;
|
public int RetentionDefaultDays = 30;
|
||||||
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
||||||
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
|
public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue;
|
||||||
|
|
||||||
// Hellion Chat first-run wizard — opens once on a fresh install. Existing
|
|
||||||
// ChatTwo users skip it because the v6→v7 migration sets the flag.
|
|
||||||
public bool FirstRunCompleted;
|
public bool FirstRunCompleted;
|
||||||
|
|
||||||
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
// Tracks which plugin version last surfaced the first-run wizard.
|
||||||
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
|
// When the running version is newer than this, Plugin.LoadAsync
|
||||||
// fresh install gets the Hellion typography out-of-the-box; flip OFF
|
// re-opens the wizard once so existing users see major UX reworks
|
||||||
// to fall back to the user's chosen system or Dalamud font.
|
// (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;
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing
|
// v1.4.7 opt-in: renders the Honorific glow outline when the title carries
|
||||||
// /tell spawns a session-only tab dedicated to that conversation
|
// a Glow colour. Default OFF — keeps v1.4.6 visuals untouched for users
|
||||||
// partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian).
|
// 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;
|
||||||
// Hard cap on simultaneously open auto tell tabs. Range enforced by the
|
|
||||||
// settings slider (1–50). LRU drop favors greeted tabs first.
|
|
||||||
public int AutoTellTabsLimit = 15;
|
public int AutoTellTabsLimit = 15;
|
||||||
// When true the sidebar shows only a thin separator before the temp
|
|
||||||
// tabs; when false a section header "Active Tells (n)" is rendered.
|
|
||||||
public bool AutoTellTabsCompactDisplay;
|
public bool AutoTellTabsCompactDisplay;
|
||||||
// Number of prior tells to preload from the message store when an
|
|
||||||
// auto tell tab is spawned. Range 0–100; 0 disables preload.
|
|
||||||
public int AutoTellTabsHistoryPreload = 20;
|
public int AutoTellTabsHistoryPreload = 20;
|
||||||
// Show the greeter "marked-as-greeted" toggle button next to each
|
|
||||||
// temp tab and dim the tab name when set. Off by default because the
|
// Sidebar width in pixels. Default 44 mirrors the icon-only layout from
|
||||||
// workflow is specific to club-greeter use cases — most users just
|
// v1.2.0; users can widen up to 160 to fit a section-header line like
|
||||||
// want the auto tabs themselves without the extra UI affordance.
|
// "Active Tells (3)" without truncation.
|
||||||
|
public int SidebarWidth = 44;
|
||||||
public bool AutoTellTabsShowGreetedToggle;
|
public bool AutoTellTabsShowGreetedToggle;
|
||||||
|
|
||||||
// Hellion Chat — One-Time-Hint-Banner that introduces the v0.6.0 pop-out
|
|
||||||
// input feature. Set to true once the user dismisses the banner from a
|
|
||||||
// pop-out window; never reset after that.
|
|
||||||
public bool SeenPopOutInputHint;
|
public bool SeenPopOutInputHint;
|
||||||
|
|
||||||
// Hellion Chat — v0.6.0 master switch for the pop-out input bar.
|
|
||||||
// Global on purpose: per-tab makes no sense for Auto-Tell-Tabs which
|
|
||||||
// are session-only and would force the user to re-enable it for every
|
|
||||||
// new conversation. Default flipped to ON in v0.6.1 (was OFF in v0.6.0)
|
|
||||||
// because tester feedback called the manual toggle "umständlich, wirkt
|
|
||||||
// unfertig". v11 → v12 migration applies the same flip to existing users.
|
|
||||||
public bool PopOutInputEnabled = true;
|
public bool PopOutInputEnabled = true;
|
||||||
|
|
||||||
// Hellion Chat — v0.6.1 One-Time-Hint-Banner that introduces the
|
|
||||||
// chat-header pop-out toolbar button and reminds about the pop-out
|
|
||||||
// input default flip. Set to true once the user dismisses the banner
|
|
||||||
// from the main chat window; never reset after that.
|
|
||||||
public bool SeenPopOutHeaderHint;
|
public bool SeenPopOutHeaderHint;
|
||||||
|
|
||||||
// Hellion Chat — v0.6.1 opt-in: when true, AutoTellTabsService.SpawnTempTab
|
|
||||||
// sets tab.PopOut = true on every new auto-tell tab so the conversation
|
|
||||||
// pops out as its own window directly. Closing the pop-out returns the
|
|
||||||
// tab to the sidebar via the standard Popout.OnClose() flow. Default OFF
|
|
||||||
// because the existing sidebar workflow is what most users (especially
|
|
||||||
// club greeters tracking many parallel tells) expect by default.
|
|
||||||
public bool AutoTellTabsOpenAsPopout;
|
public bool AutoTellTabsOpenAsPopout;
|
||||||
|
|
||||||
public int GetRetentionDays(ChatType type)
|
public int GetRetentionDays(ChatType type)
|
||||||
@@ -147,7 +148,9 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool HideWhenUiHidden = true;
|
public bool HideWhenUiHidden = true;
|
||||||
public bool HideInLoadingScreens;
|
public bool HideInLoadingScreens;
|
||||||
public bool HideInBattle;
|
public bool HideInBattle;
|
||||||
public bool HideInNewGamePlusMenu;
|
|
||||||
|
// v1.2.1: default flipped false → true for consistency with other hide defaults.
|
||||||
|
public bool HideInNewGamePlusMenu = true;
|
||||||
public bool HideWhenInactive;
|
public bool HideWhenInactive;
|
||||||
public int InactivityHideTimeout = 10;
|
public int InactivityHideTimeout = 10;
|
||||||
public bool InactivityHideActiveDuringBattle = true;
|
public bool InactivityHideActiveDuringBattle = true;
|
||||||
@@ -162,13 +165,8 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool NativeItemTooltips = true;
|
public bool NativeItemTooltips = true;
|
||||||
public bool PrettierTimestamps = true;
|
public bool PrettierTimestamps = true;
|
||||||
public bool MoreCompactPretty;
|
public bool MoreCompactPretty;
|
||||||
public bool HideSameTimestamps;
|
public bool HideSameTimestamps = true;
|
||||||
public bool ShowNoviceNetwork;
|
public bool ShowNoviceNetwork;
|
||||||
// Hellion Chat — vertical sidebar tab layout reads better than the
|
|
||||||
// horizontal tab strip in the company of Auto-Tell-Tabs (a club
|
|
||||||
// greeter typically tracks 5–15 simultaneous conversations). Bestand
|
|
||||||
// users keep their saved value untouched — only fresh installs pick
|
|
||||||
// up the new default.
|
|
||||||
public bool SidebarTabView = true;
|
public bool SidebarTabView = true;
|
||||||
public bool PrintChangelog = true;
|
public bool PrintChangelog = true;
|
||||||
public bool OnlyPreviewIf;
|
public bool OnlyPreviewIf;
|
||||||
@@ -187,18 +185,13 @@ 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 = 5_000; // 1-10000
|
public int MaxLinesToRender = 2_500; // 1-10000
|
||||||
// Default ON to match a German / European 24h locale. The
|
|
||||||
// ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via
|
|
||||||
// CultureInfo.InvariantCulture so the result is consistent across
|
|
||||||
// host locales.
|
|
||||||
public bool Use24HourClock = true;
|
public bool Use24HourClock = true;
|
||||||
|
|
||||||
public bool ShowEmotes = true;
|
public bool ShowEmotes = true;
|
||||||
public HashSet<string> BlockedEmotes = [];
|
public HashSet<string> BlockedEmotes = [];
|
||||||
|
|
||||||
public bool FontsEnabled = true;
|
public bool FontsEnabled = true;
|
||||||
public ExtraGlyphRanges ExtraGlyphRanges = 0;
|
public ExtraGlyphRanges ExtraGlyphRanges = 0;
|
||||||
public float FontSizeV2 = 12.75f;
|
public float FontSizeV2 = 12.75f;
|
||||||
@@ -222,14 +215,22 @@ public class Configuration : IPluginConfiguration
|
|||||||
};
|
};
|
||||||
|
|
||||||
public float TooltipOffset;
|
public float TooltipOffset;
|
||||||
public float WindowAlpha = 100f;
|
|
||||||
public Dictionary<ChatType, uint> ChatColours = new();
|
public Dictionary<ChatType, uint> ChatColours = BuildDefaultChatColours();
|
||||||
|
|
||||||
|
private static Dictionary<ChatType, uint> BuildDefaultChatColours()
|
||||||
|
{
|
||||||
|
var defaults = new Dictionary<ChatType, uint>();
|
||||||
|
foreach (
|
||||||
|
var (channel, colour) in HellionChat.Resources.ChatColourPresets.All["Hellion"].Colours
|
||||||
|
)
|
||||||
|
defaults[channel] = colour;
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
public bool ColorSelectedInputChannelButton = true;
|
public bool ColorSelectedInputChannelButton = true;
|
||||||
public List<Tab> Tabs = [];
|
public List<Tab> Tabs = [];
|
||||||
|
|
||||||
public bool OverrideStyle;
|
|
||||||
public string? ChosenStyle;
|
|
||||||
|
|
||||||
public ConfigKeyBind? ChatTabForward;
|
public ConfigKeyBind? ChatTabForward;
|
||||||
public ConfigKeyBind? ChatTabBackward;
|
public ConfigKeyBind? ChatTabBackward;
|
||||||
|
|
||||||
@@ -249,7 +250,10 @@ public class Configuration : IPluginConfiguration
|
|||||||
HideWhenInactive = other.HideWhenInactive;
|
HideWhenInactive = other.HideWhenInactive;
|
||||||
InactivityHideTimeout = other.InactivityHideTimeout;
|
InactivityHideTimeout = other.InactivityHideTimeout;
|
||||||
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
||||||
InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(pair => pair.Key, pair => pair.Value);
|
InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(
|
||||||
|
pair => pair.Key,
|
||||||
|
pair => pair.Value
|
||||||
|
);
|
||||||
InactivityHideExtraChatAll = other.InactivityHideExtraChatAll;
|
InactivityHideExtraChatAll = other.InactivityHideExtraChatAll;
|
||||||
InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet();
|
InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet();
|
||||||
ShowHideButton = other.ShowHideButton;
|
ShowHideButton = other.ShowHideButton;
|
||||||
@@ -276,14 +280,13 @@ 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;
|
||||||
Use24HourClock = other.Use24HourClock;
|
Use24HourClock = other.Use24HourClock;
|
||||||
ShowEmotes = other.ShowEmotes;
|
ShowEmotes = other.ShowEmotes;
|
||||||
// Deep-copy the set so the live and mutable Configuration instances don't share state
|
// Deep-copy so settings window edits don't leak into live config before Save.
|
||||||
// — a HashSet reference assignment would cause edits in the settings window to leak
|
|
||||||
// into the live config before the user clicks Save.
|
|
||||||
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
|
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
|
||||||
FontsEnabled = other.FontsEnabled;
|
FontsEnabled = other.FontsEnabled;
|
||||||
ItalicEnabled = other.ItalicEnabled;
|
ItalicEnabled = other.ItalicEnabled;
|
||||||
@@ -294,71 +297,70 @@ public class Configuration : IPluginConfiguration
|
|||||||
ItalicFontV2 = other.ItalicFontV2;
|
ItalicFontV2 = other.ItalicFontV2;
|
||||||
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
|
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
|
||||||
TooltipOffset = other.TooltipOffset;
|
TooltipOffset = other.TooltipOffset;
|
||||||
WindowAlpha = other.WindowAlpha;
|
|
||||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||||
// never present in a disk-loaded copy. Keep the live temp tabs of
|
// not destroy open tell conversations. Pinned TempTabs are persistent
|
||||||
// *this* configuration alive across an UpdateFrom so a settings
|
// and come through `other` like regular tabs; unpinned TempTabs are
|
||||||
// save (or sidebar-mode toggle) does not silently destroy the
|
// session-only and held from the local state. For persistent tabs
|
||||||
// user's open tell conversations.
|
// (incl. pinned), capture live runtime state by Identifier and restore
|
||||||
//
|
// it onto the freshly cloned tabs — CurrentChannel is critical because
|
||||||
// For persistent tabs we go through Tab.Clone() which intentionally
|
// the user may have switched channel in-game between settings-open
|
||||||
// does NOT copy the NonSerialized Messages list (avoids shared
|
// and settings-save, and we'd otherwise overwrite that with the
|
||||||
// mutable state on disk-load). On a settings save that means the
|
// settings-time snapshot.
|
||||||
// chat history for every persistent tab would be wiped — bug
|
var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||||
// reported by Flo 2026-05-05. We work around it by capturing the
|
var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t))
|
||||||
// live MessageList (and LastSendUnread counter) by Identifier
|
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread, t.CurrentChannel));
|
||||||
// before the replace, then restoring it onto the freshly cloned
|
|
||||||
// tabs whose Identifier survives Tab.Clone(). New tabs added in
|
|
||||||
// settings get a fresh empty MessageList; deleted tabs lose their
|
|
||||||
// history (intended).
|
|
||||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
|
||||||
var livePersistentSession = Tabs
|
|
||||||
.Where(t => !t.IsTempTab)
|
|
||||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
|
|
||||||
|
|
||||||
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t =>
|
Tabs = other
|
||||||
{
|
.Tabs.Where(t => !t.IsTempTab || t.IsPinned)
|
||||||
var clone = t.Clone();
|
.Select(t =>
|
||||||
if (livePersistentSession.TryGetValue(clone.Identifier, out var live))
|
|
||||||
{
|
{
|
||||||
clone.Messages = live.Messages;
|
var clone = t.Clone();
|
||||||
clone.LastSendUnread = live.LastSendUnread;
|
if (livePersistentSession.TryGetValue(clone.Identifier, out var live))
|
||||||
}
|
{
|
||||||
return clone;
|
clone.Messages = live.Messages;
|
||||||
}).ToList();
|
clone.LastSendUnread = live.LastSendUnread;
|
||||||
Tabs.AddRange(liveTempTabs);
|
clone.CurrentChannel = live.CurrentChannel;
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
Tabs.AddRange(liveUnpinnedTempTabs);
|
||||||
|
|
||||||
OverrideStyle = other.OverrideStyle;
|
|
||||||
ChosenStyle = other.ChosenStyle;
|
|
||||||
ChatTabForward = other.ChatTabForward;
|
ChatTabForward = other.ChatTabForward;
|
||||||
ChatTabBackward = other.ChatTabBackward;
|
ChatTabBackward = other.ChatTabBackward;
|
||||||
|
|
||||||
PrivacyFilterEnabled = other.PrivacyFilterEnabled;
|
PrivacyFilterEnabled = other.PrivacyFilterEnabled;
|
||||||
PrivacyPersistChannels = [..other.PrivacyPersistChannels];
|
PrivacyPersistChannels = [.. other.PrivacyPersistChannels];
|
||||||
PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels;
|
PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels;
|
||||||
|
|
||||||
RetentionEnabled = other.RetentionEnabled;
|
RetentionEnabled = other.RetentionEnabled;
|
||||||
RetentionDefaultDays = other.RetentionDefaultDays;
|
RetentionDefaultDays = other.RetentionDefaultDays;
|
||||||
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value);
|
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(
|
||||||
|
p => p.Key,
|
||||||
|
p => p.Value
|
||||||
|
);
|
||||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||||
|
|
||||||
FirstRunCompleted = other.FirstRunCompleted;
|
FirstRunCompleted = other.FirstRunCompleted;
|
||||||
|
WizardLastShownVersion = other.WizardLastShownVersion;
|
||||||
UseHellionFont = other.UseHellionFont;
|
UseHellionFont = other.UseHellionFont;
|
||||||
|
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
||||||
|
ShowHonorificGlow = other.ShowHonorificGlow;
|
||||||
|
|
||||||
// v1.1.0 theme engine fields
|
// v1.1.0 theme engine fields
|
||||||
Theme = other.Theme;
|
Theme = other.Theme;
|
||||||
WindowOpacity = other.WindowOpacity;
|
WindowOpacity = other.WindowOpacity;
|
||||||
ReduceMotion = other.ReduceMotion;
|
ReduceMotion = other.ReduceMotion;
|
||||||
UseCompactDensity = other.UseCompactDensity;
|
UseCompactDensity = other.UseCompactDensity;
|
||||||
ShowThemeQuickPicker = other.ShowThemeQuickPicker;
|
|
||||||
|
|
||||||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||||||
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;
|
||||||
@@ -378,21 +380,23 @@ public enum UnreadMode
|
|||||||
|
|
||||||
public static class UnreadModeExt
|
public static class UnreadModeExt
|
||||||
{
|
{
|
||||||
internal static string Name(this UnreadMode mode) => mode switch
|
internal static string Name(this UnreadMode mode) =>
|
||||||
{
|
mode switch
|
||||||
UnreadMode.All => Language.UnreadMode_All,
|
{
|
||||||
UnreadMode.Unseen => Language.UnreadMode_Unseen,
|
UnreadMode.All => Language.UnreadMode_All,
|
||||||
UnreadMode.None => Language.UnreadMode_None,
|
UnreadMode.Unseen => Language.UnreadMode_Unseen,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
UnreadMode.None => Language.UnreadMode_None,
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||||
|
};
|
||||||
|
|
||||||
internal static string? Tooltip(this UnreadMode mode) => mode switch
|
internal static string? Tooltip(this UnreadMode mode) =>
|
||||||
{
|
mode switch
|
||||||
UnreadMode.All => Language.UnreadMode_All_Tooltip,
|
{
|
||||||
UnreadMode.Unseen => Language.UnreadMode_Unseen_Tooltip,
|
UnreadMode.All => Language.UnreadMode_All_Tooltip,
|
||||||
UnreadMode.None => Language.UnreadMode_None_Tooltip,
|
UnreadMode.Unseen => Language.UnreadMode_Unseen_Tooltip,
|
||||||
_ => null,
|
UnreadMode.None => Language.UnreadMode_None_Tooltip,
|
||||||
};
|
_ => null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -400,9 +404,7 @@ public class Tab
|
|||||||
{
|
{
|
||||||
public string Name = Language.Tab_DefaultName;
|
public string Name = Language.Tab_DefaultName;
|
||||||
|
|
||||||
// v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet:
|
// Optional FontAwesome glyph name; null falls back to TabIconMapping default.
|
||||||
// Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name).
|
|
||||||
// User können hier per Settings → Tabs einen eigenen Glyph setzen.
|
|
||||||
public string? Icon = null;
|
public string? Icon = null;
|
||||||
|
|
||||||
[Obsolete("Removed in favor of SelectedChannels")]
|
[Obsolete("Removed in favor of SelectedChannels")]
|
||||||
@@ -433,37 +435,65 @@ 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();
|
||||||
|
|
||||||
[NonSerialized] public uint Unread;
|
[NonSerialized]
|
||||||
[NonSerialized] public uint LastSendUnread;
|
public uint Unread;
|
||||||
[NonSerialized] public long LastActivity;
|
|
||||||
[NonSerialized] public MessageList Messages = new();
|
|
||||||
|
|
||||||
[NonSerialized] public UsedChannel CurrentChannel = new();
|
[NonSerialized]
|
||||||
|
public uint LastSendUnread;
|
||||||
|
|
||||||
[NonSerialized] public Guid Identifier = Guid.NewGuid();
|
[NonSerialized]
|
||||||
|
public long LastActivity;
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the
|
[NonSerialized]
|
||||||
// sidebar to mark a tell partner as already greeted in the current
|
public MessageList Messages = new();
|
||||||
// session. NonSerialized because the temp tab itself is session-only.
|
|
||||||
[NonSerialized] public bool IsGreeted;
|
[NonSerialized]
|
||||||
|
public UsedChannel CurrentChannel = new();
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
public Guid Identifier = Guid.NewGuid();
|
||||||
|
|
||||||
|
// Session-only greeted flag for club-greeter workflows.
|
||||||
|
[NonSerialized]
|
||||||
|
public bool IsGreeted;
|
||||||
|
|
||||||
|
// Separate validation keys per cache so TellTarget changes don't
|
||||||
|
// cause GetTint and GetIcon to strand each other with stale entries.
|
||||||
|
[NonSerialized]
|
||||||
|
internal string? _cachedTintTellName;
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
internal uint _cachedTintTellWorld;
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
internal uint _cachedTellTint;
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
internal string? _cachedIconTellName;
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
internal uint _cachedIconTellWorld;
|
||||||
|
|
||||||
|
[NonSerialized]
|
||||||
|
internal string? _cachedTellIcon;
|
||||||
|
|
||||||
public bool Matches(Message message)
|
public bool Matches(Message message)
|
||||||
{
|
{
|
||||||
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-tell temp tabs are bound to a single conversation partner;
|
// Temp tabs are bound to a single conversation partner — other tells
|
||||||
// every other tell that matches the channel filter must NOT land
|
// matching the channel filter must not land here.
|
||||||
// here, otherwise all temp tabs would mirror "Tell Exclusive".
|
|
||||||
if (IsTempTab && TellTarget?.IsSet() == true)
|
if (IsTempTab && TellTarget?.IsSet() == true)
|
||||||
{
|
|
||||||
return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
|
return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -475,12 +505,17 @@ public class Tab
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Unread += 1;
|
Unread += 1;
|
||||||
if (message.Matches(Plugin.Config.InactivityHideChannelsV2, Plugin.Config.InactivityHideExtraChatAll, Plugin.Config.InactivityHideExtraChatChannels))
|
if (
|
||||||
|
message.Matches(
|
||||||
|
Plugin.Config.InactivityHideChannelsV2,
|
||||||
|
Plugin.Config.InactivityHideExtraChatAll,
|
||||||
|
Plugin.Config.InactivityHideExtraChatChannels
|
||||||
|
)
|
||||||
|
)
|
||||||
LastActivity = Environment.TickCount64;
|
LastActivity = Environment.TickCount64;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear() => Messages.Clear();
|
||||||
=> Messages.Clear();
|
|
||||||
|
|
||||||
public Tab Clone()
|
public Tab Clone()
|
||||||
{
|
{
|
||||||
@@ -501,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,
|
||||||
@@ -512,16 +547,14 @@ 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// Ordered message list with duplicate ID tracking, sorting and mutex protection.
|
||||||
/// MessageList provides an ordered list of messages with duplicate ID
|
|
||||||
/// tracking, sorting and mutex protection.
|
|
||||||
/// </summary>
|
|
||||||
public class MessageList
|
public class MessageList
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim LockSlim = new(1, 1);
|
private readonly SemaphoreSlim LockSlim = new(1, 1);
|
||||||
@@ -609,23 +642,24 @@ public class Tab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// Current message count. Lock-per-read is acceptable for 1×/sec status bar polling.
|
||||||
/// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read
|
|
||||||
/// ist OK für 1×/sec Status-Bar-Polling (v1.2.0).
|
|
||||||
/// </summary>
|
|
||||||
public int Count
|
public int Count
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
LockSlim.Wait(-1);
|
LockSlim.Wait(-1);
|
||||||
try { return Messages.Count; }
|
try
|
||||||
finally { LockSlim.Release(); }
|
{
|
||||||
|
return Messages.Count;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LockSlim.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// Returns an array copy of the message list for usage outside of main thread.
|
||||||
/// Returns an array copy of the message list for usage outside of main thread
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Message[]> GetCopy(int millisecondsTimeout = -1)
|
public async Task<Message[]> GetCopy(int millisecondsTimeout = -1)
|
||||||
{
|
{
|
||||||
await LockSlim.WaitAsync(millisecondsTimeout);
|
await LockSlim.WaitAsync(millisecondsTimeout);
|
||||||
@@ -639,17 +673,16 @@ public class Tab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// Returns a read-only list while holding a reader lock. Use with a using statement.
|
||||||
/// GetReadOnly returns a read-only list of messages while holding a
|
|
||||||
/// reader lock. The list should be used with a using statement.
|
|
||||||
/// </summary>
|
|
||||||
public RLockedMessageList GetReadOnly(int millisecondsTimeout = -1)
|
public RLockedMessageList GetReadOnly(int millisecondsTimeout = -1)
|
||||||
{
|
{
|
||||||
LockSlim.Wait(millisecondsTimeout);
|
LockSlim.Wait(millisecondsTimeout);
|
||||||
return new RLockedMessageList(LockSlim, Messages);
|
return new RLockedMessageList(LockSlim, Messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RLockedMessageList(SemaphoreSlim lockSlim, List<Message> messages) : IReadOnlyList<Message>, IDisposable
|
public class RLockedMessageList(SemaphoreSlim lockSlim, List<Message> messages)
|
||||||
|
: IReadOnlyList<Message>,
|
||||||
|
IDisposable
|
||||||
{
|
{
|
||||||
public IEnumerator<Message> GetEnumerator()
|
public IEnumerator<Message> GetEnumerator()
|
||||||
{
|
{
|
||||||
@@ -694,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]
|
||||||
@@ -708,15 +764,16 @@ public enum PreviewPosition
|
|||||||
|
|
||||||
public static class PreviewPositionExt
|
public static class PreviewPositionExt
|
||||||
{
|
{
|
||||||
public static string Name(this PreviewPosition position) => position switch
|
public static string Name(this PreviewPosition position) =>
|
||||||
{
|
position switch
|
||||||
PreviewPosition.None => Language.Options_Preview_None,
|
{
|
||||||
PreviewPosition.Inside => Language.Options_Preview_Inside,
|
PreviewPosition.None => Language.Options_Preview_None,
|
||||||
PreviewPosition.Top => Language.Options_Preview_Top,
|
PreviewPosition.Inside => Language.Options_Preview_Inside,
|
||||||
PreviewPosition.Bottom => Language.Options_Preview_Bottom,
|
PreviewPosition.Top => Language.Options_Preview_Top,
|
||||||
PreviewPosition.Tooltip => Language.Options_Preview_Tooltip,
|
PreviewPosition.Bottom => Language.Options_Preview_Bottom,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(position), position, null),
|
PreviewPosition.Tooltip => Language.Options_Preview_Tooltip,
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(position), position, null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -729,13 +786,14 @@ public enum CommandHelpSide
|
|||||||
|
|
||||||
public static class CommandHelpSideExt
|
public static class CommandHelpSideExt
|
||||||
{
|
{
|
||||||
public static string Name(this CommandHelpSide side) => side switch
|
public static string Name(this CommandHelpSide side) =>
|
||||||
{
|
side switch
|
||||||
CommandHelpSide.None => Language.CommandHelpSide_None,
|
{
|
||||||
CommandHelpSide.Left => Language.CommandHelpSide_Left,
|
CommandHelpSide.None => Language.CommandHelpSide_None,
|
||||||
CommandHelpSide.Right => Language.CommandHelpSide_Right,
|
CommandHelpSide.Left => Language.CommandHelpSide_Left,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(side), side, null),
|
CommandHelpSide.Right => Language.CommandHelpSide_Right,
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(side), side, null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -747,19 +805,21 @@ public enum KeybindMode
|
|||||||
|
|
||||||
public static class KeybindModeExt
|
public static class KeybindModeExt
|
||||||
{
|
{
|
||||||
public static string Name(this KeybindMode mode) => mode switch
|
public static string Name(this KeybindMode mode) =>
|
||||||
{
|
mode switch
|
||||||
KeybindMode.Flexible => Language.KeybindMode_Flexible_Name,
|
{
|
||||||
KeybindMode.Strict => Language.KeybindMode_Strict_Name,
|
KeybindMode.Flexible => Language.KeybindMode_Flexible_Name,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
KeybindMode.Strict => Language.KeybindMode_Strict_Name,
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||||
|
};
|
||||||
|
|
||||||
public static string? Tooltip(this KeybindMode mode) => mode switch
|
public static string? Tooltip(this KeybindMode mode) =>
|
||||||
{
|
mode switch
|
||||||
KeybindMode.Flexible => Language.KeybindMode_Flexible_Tooltip,
|
{
|
||||||
KeybindMode.Strict => Language.KeybindMode_Strict_Tooltip,
|
KeybindMode.Flexible => Language.KeybindMode_Flexible_Tooltip,
|
||||||
_ => null,
|
KeybindMode.Strict => Language.KeybindMode_Strict_Tooltip,
|
||||||
};
|
_ => null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -788,49 +848,51 @@ public enum LanguageOverride
|
|||||||
|
|
||||||
public static class LanguageOverrideExt
|
public static class LanguageOverrideExt
|
||||||
{
|
{
|
||||||
public static string Name(this LanguageOverride mode) => mode switch
|
public static string Name(this LanguageOverride mode) =>
|
||||||
{
|
mode switch
|
||||||
LanguageOverride.None => Language.LanguageOverride_None,
|
{
|
||||||
LanguageOverride.ChineseSimplified => "简体中文",
|
LanguageOverride.None => Language.LanguageOverride_None,
|
||||||
LanguageOverride.ChineseTraditional => "繁體中文",
|
LanguageOverride.ChineseSimplified => "简体中文",
|
||||||
LanguageOverride.Dutch => "Nederlands",
|
LanguageOverride.ChineseTraditional => "繁體中文",
|
||||||
LanguageOverride.English => "English",
|
LanguageOverride.Dutch => "Nederlands",
|
||||||
LanguageOverride.French => "Français",
|
LanguageOverride.English => "English",
|
||||||
LanguageOverride.German => "Deutsch",
|
LanguageOverride.French => "Français",
|
||||||
LanguageOverride.Greek => "Ελληνικά",
|
LanguageOverride.German => "Deutsch",
|
||||||
// LanguageOverride.Italian => "Italiano",
|
LanguageOverride.Greek => "Ελληνικά",
|
||||||
LanguageOverride.Japanese => "日本語",
|
// LanguageOverride.Italian => "Italiano",
|
||||||
// LanguageOverride.Korean => "한국어 (Korean)",
|
LanguageOverride.Japanese => "日本語",
|
||||||
// LanguageOverride.Norwegian => "Norsk",
|
// LanguageOverride.Korean => "한국어 (Korean)",
|
||||||
LanguageOverride.PortugueseBrazil => "Português do Brasil",
|
// LanguageOverride.Norwegian => "Norsk",
|
||||||
LanguageOverride.Romanian => "Română",
|
LanguageOverride.PortugueseBrazil => "Português do Brasil",
|
||||||
LanguageOverride.Russian => "Русский",
|
LanguageOverride.Romanian => "Română",
|
||||||
LanguageOverride.Spanish => "Español",
|
LanguageOverride.Russian => "Русский",
|
||||||
LanguageOverride.Swedish => "Svenska",
|
LanguageOverride.Spanish => "Español",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
LanguageOverride.Swedish => "Svenska",
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||||
|
};
|
||||||
|
|
||||||
public static string Code(this LanguageOverride mode) => mode switch
|
public static string Code(this LanguageOverride mode) =>
|
||||||
{
|
mode switch
|
||||||
LanguageOverride.None => "",
|
{
|
||||||
LanguageOverride.ChineseSimplified => "zh-hans",
|
LanguageOverride.None => "",
|
||||||
LanguageOverride.ChineseTraditional => "zh-hant",
|
LanguageOverride.ChineseSimplified => "zh-hans",
|
||||||
LanguageOverride.Dutch => "nl",
|
LanguageOverride.ChineseTraditional => "zh-hant",
|
||||||
LanguageOverride.English => "en",
|
LanguageOverride.Dutch => "nl",
|
||||||
LanguageOverride.French => "fr",
|
LanguageOverride.English => "en",
|
||||||
LanguageOverride.German => "de",
|
LanguageOverride.French => "fr",
|
||||||
LanguageOverride.Greek => "el",
|
LanguageOverride.German => "de",
|
||||||
// LanguageOverride.Italian => "it",
|
LanguageOverride.Greek => "el",
|
||||||
LanguageOverride.Japanese => "ja",
|
// LanguageOverride.Italian => "it",
|
||||||
// LanguageOverride.Korean => "ko",
|
LanguageOverride.Japanese => "ja",
|
||||||
// LanguageOverride.Norwegian => "no",
|
// LanguageOverride.Korean => "ko",
|
||||||
LanguageOverride.PortugueseBrazil => "pt-br",
|
// LanguageOverride.Norwegian => "no",
|
||||||
LanguageOverride.Romanian => "ro",
|
LanguageOverride.PortugueseBrazil => "pt-br",
|
||||||
LanguageOverride.Russian => "ru",
|
LanguageOverride.Romanian => "ro",
|
||||||
LanguageOverride.Spanish => "es",
|
LanguageOverride.Russian => "ru",
|
||||||
LanguageOverride.Swedish => "sv",
|
LanguageOverride.Spanish => "es",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
LanguageOverride.Swedish => "sv",
|
||||||
};
|
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -848,27 +910,31 @@ public enum ExtraGlyphRanges
|
|||||||
|
|
||||||
public static class ExtraGlyphRangesExt
|
public static class ExtraGlyphRangesExt
|
||||||
{
|
{
|
||||||
public static string Name(this ExtraGlyphRanges ranges) => ranges switch
|
public static string Name(this ExtraGlyphRanges ranges) =>
|
||||||
{
|
ranges switch
|
||||||
ExtraGlyphRanges.ChineseFull => Language.ExtraGlyphRanges_ChineseFull_Name,
|
{
|
||||||
ExtraGlyphRanges.ChineseSimplifiedCommon => Language.ExtraGlyphRanges_ChineseSimplifiedCommon_Name,
|
ExtraGlyphRanges.ChineseFull => Language.ExtraGlyphRanges_ChineseFull_Name,
|
||||||
ExtraGlyphRanges.Cyrillic => Language.ExtraGlyphRanges_Cyrillic_Name,
|
ExtraGlyphRanges.ChineseSimplifiedCommon =>
|
||||||
ExtraGlyphRanges.Japanese => Language.ExtraGlyphRanges_Japanese_Name,
|
Language.ExtraGlyphRanges_ChineseSimplifiedCommon_Name,
|
||||||
ExtraGlyphRanges.Korean => Language.ExtraGlyphRanges_Korean_Name,
|
ExtraGlyphRanges.Cyrillic => Language.ExtraGlyphRanges_Cyrillic_Name,
|
||||||
ExtraGlyphRanges.Thai => Language.ExtraGlyphRanges_Thai_Name,
|
ExtraGlyphRanges.Japanese => Language.ExtraGlyphRanges_Japanese_Name,
|
||||||
ExtraGlyphRanges.Vietnamese => Language.ExtraGlyphRanges_Vietnamese_Name,
|
ExtraGlyphRanges.Korean => Language.ExtraGlyphRanges_Korean_Name,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
ExtraGlyphRanges.Thai => Language.ExtraGlyphRanges_Thai_Name,
|
||||||
};
|
ExtraGlyphRanges.Vietnamese => Language.ExtraGlyphRanges_Vietnamese_Name,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
||||||
|
};
|
||||||
|
|
||||||
public static unsafe nint Range(this ExtraGlyphRanges ranges) => ranges switch
|
public static unsafe nint Range(this ExtraGlyphRanges ranges) =>
|
||||||
{
|
ranges switch
|
||||||
ExtraGlyphRanges.ChineseFull => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseFull(),
|
{
|
||||||
ExtraGlyphRanges.ChineseSimplifiedCommon => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseSimplifiedCommon(),
|
ExtraGlyphRanges.ChineseFull => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseFull(),
|
||||||
ExtraGlyphRanges.Cyrillic => (nint)ImGui.GetIO().Fonts.GetGlyphRangesCyrillic(),
|
ExtraGlyphRanges.ChineseSimplifiedCommon => (nint)
|
||||||
ExtraGlyphRanges.Japanese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesJapanese(),
|
ImGui.GetIO().Fonts.GetGlyphRangesChineseSimplifiedCommon(),
|
||||||
ExtraGlyphRanges.Korean => (nint)ImGui.GetIO().Fonts.GetGlyphRangesKorean(),
|
ExtraGlyphRanges.Cyrillic => (nint)ImGui.GetIO().Fonts.GetGlyphRangesCyrillic(),
|
||||||
ExtraGlyphRanges.Thai => (nint)ImGui.GetIO().Fonts.GetGlyphRangesThai(),
|
ExtraGlyphRanges.Japanese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesJapanese(),
|
||||||
ExtraGlyphRanges.Vietnamese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesVietnamese(),
|
ExtraGlyphRanges.Korean => (nint)ImGui.GetIO().Fonts.GetGlyphRangesKorean(),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
ExtraGlyphRanges.Thai => (nint)ImGui.GetIO().Fonts.GetGlyphRangesThai(),
|
||||||
};
|
ExtraGlyphRanges.Vietnamese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesVietnamese(),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-58
@@ -1,10 +1,11 @@
|
|||||||
using System.Numerics;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Numerics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Textures;
|
using Dalamud.Interface.Textures;
|
||||||
using Dalamud.Interface.Textures.TextureWraps;
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
@@ -14,11 +15,31 @@ public static class EmoteCache
|
|||||||
{
|
{
|
||||||
private static readonly string[] NotWorking =
|
private static readonly string[] NotWorking =
|
||||||
[
|
[
|
||||||
":tf:", "(ditto)", "c!", "h!", "l!", "M&Mjc", "LUL3D", "p!",
|
":tf:",
|
||||||
"POLICE2", "r!", "Pussy", "s!", "v!", "w!", "x0r6ztGiggle",
|
"(ditto)",
|
||||||
"z!", "xar2EDM", "iron95Pls", "Clap2", "AlienPls3", "Life",
|
"c!",
|
||||||
"peepoPogClimbingTreeHard4House", "monkaGIGAftRobertDowneyJr",
|
"h!",
|
||||||
"DogLookingSussyAndCold", "DICKS"
|
"l!",
|
||||||
|
"M&Mjc",
|
||||||
|
"LUL3D",
|
||||||
|
"p!",
|
||||||
|
"POLICE2",
|
||||||
|
"r!",
|
||||||
|
"Pussy",
|
||||||
|
"s!",
|
||||||
|
"v!",
|
||||||
|
"w!",
|
||||||
|
"x0r6ztGiggle",
|
||||||
|
"z!",
|
||||||
|
"xar2EDM",
|
||||||
|
"iron95Pls",
|
||||||
|
"Clap2",
|
||||||
|
"AlienPls3",
|
||||||
|
"Life",
|
||||||
|
"peepoPogClimbingTreeHard4House",
|
||||||
|
"monkaGIGAftRobertDowneyJr",
|
||||||
|
"DogLookingSussyAndCold",
|
||||||
|
"DICKS",
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly HttpClient Client = new();
|
private static readonly HttpClient Client = new();
|
||||||
@@ -55,10 +76,10 @@ public static class EmoteCache
|
|||||||
{
|
{
|
||||||
Unloaded,
|
Unloaded,
|
||||||
Loading,
|
Loading,
|
||||||
Done
|
Done,
|
||||||
}
|
}
|
||||||
|
|
||||||
// All of this data is uninitalized while State is not `LoadingState.Done`
|
// All fields below are uninitialised while State != Done.
|
||||||
public static LoadingState State = LoadingState.Unloaded;
|
public static LoadingState State = LoadingState.Unloaded;
|
||||||
|
|
||||||
private static readonly Dictionary<string, Emote> Cache = new();
|
private static readonly Dictionary<string, Emote> Cache = new();
|
||||||
@@ -66,20 +87,36 @@ public static class EmoteCache
|
|||||||
|
|
||||||
public static string[] SortedCodeArray = [];
|
public static string[] SortedCodeArray = [];
|
||||||
|
|
||||||
// Plugin-scoped cancellation source for in-flight emote loads. Dispose
|
// Cancelled on Dispose to stop in-flight downloads; replaced on re-enable.
|
||||||
// cancels every running download/texture-create so the workers don't
|
|
||||||
// touch a torn-down TextureProvider on plugin reload. Replaced with a
|
|
||||||
// fresh source on the next LoadData() call so a re-enable still works.
|
|
||||||
private static CancellationTokenSource Cts = new();
|
private static CancellationTokenSource Cts = new();
|
||||||
internal static CancellationToken Token => Cts.Token;
|
internal static CancellationToken Token => Cts.Token;
|
||||||
|
|
||||||
|
// Tracks in-flight loads so Dispose can drain them before teardown.
|
||||||
|
private static readonly ConcurrentBag<Task> PendingLoads = new();
|
||||||
|
|
||||||
|
internal static void TrackLoad(Task loadTask, string emoteCode)
|
||||||
|
{
|
||||||
|
PendingLoads.Add(
|
||||||
|
loadTask.ContinueWith(
|
||||||
|
t =>
|
||||||
|
{
|
||||||
|
if (t.IsFaulted)
|
||||||
|
Plugin.LogProxy.Error(
|
||||||
|
t.Exception!,
|
||||||
|
$"EmoteCache load failed for {emoteCode}"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
TaskScheduler.Default
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task LoadData()
|
public static async Task LoadData()
|
||||||
{
|
{
|
||||||
if (State is not LoadingState.Unloaded)
|
if (State is not LoadingState.Unloaded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Refresh the CTS in case Dispose was called and we're being re-enabled
|
// Reset CTS if Dispose was called and the plugin is being re-enabled.
|
||||||
// in the same process (Dalamud /xlplugins toggle).
|
|
||||||
if (Cts.IsCancellationRequested)
|
if (Cts.IsCancellationRequested)
|
||||||
Cts = new CancellationTokenSource();
|
Cts = new CancellationTokenSource();
|
||||||
|
|
||||||
@@ -101,13 +138,13 @@ public static class EmoteCache
|
|||||||
var topList = await top.Content.ReadAsStringAsync(ct);
|
var topList = await top.Content.ReadAsStringAsync(ct);
|
||||||
|
|
||||||
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
||||||
// BetterTTV occasionally returns entries with a null Code; the
|
// BetterTTV occasionally returns entries with a null Code;
|
||||||
// upstream code passed those straight into Dictionary.TryAdd
|
// skip them so a single bad row doesn't break the whole cache.
|
||||||
// and tripped ArgumentNullException, killing the whole emote
|
|
||||||
// load. Skip them defensively so a single bad row no longer
|
|
||||||
// breaks the cache for everyone else.
|
|
||||||
foreach (var emote in jsonList)
|
foreach (var emote in jsonList)
|
||||||
if (!string.IsNullOrEmpty(emote.Emote.Code) && !NotWorking.Contains(emote.Emote.Code))
|
if (
|
||||||
|
!string.IsNullOrEmpty(emote.Emote.Code)
|
||||||
|
&& !NotWorking.Contains(emote.Emote.Code)
|
||||||
|
)
|
||||||
Cache.TryAdd(emote.Emote.Code, emote.Emote);
|
Cache.TryAdd(emote.Emote.Code, emote.Emote);
|
||||||
|
|
||||||
lastId = jsonList.Last().Id;
|
lastId = jsonList.Last().Id;
|
||||||
@@ -118,27 +155,32 @@ public static class EmoteCache
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Plugin disposed while the cache was loading; leave State on
|
// Plugin disposed mid-load; State stays on Loading so re-enable can retry.
|
||||||
// Loading so a subsequent re-enable can re-issue LoadData with
|
|
||||||
// a fresh CTS (handled above).
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Reset to Unloaded so a later trigger (e.g. the user reopening
|
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
||||||
// the Emotes tab after the network recovers) can retry. Without
|
|
||||||
// this the State stays on Loading and the early-out at the top
|
|
||||||
// of LoadData blocks every further attempt until 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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Dispose()
|
public static void Dispose()
|
||||||
{
|
{
|
||||||
// Cancel in-flight downloads / texture creates so the async-void
|
|
||||||
// Load methods bail out before they touch a disposed TextureProvider.
|
|
||||||
Cts.Cancel();
|
Cts.Cancel();
|
||||||
|
|
||||||
|
// 5s upper bound; anything still running gets abandoned.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Task.WaitAll(PendingLoads.ToArray(), TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
catch (AggregateException)
|
||||||
|
{
|
||||||
|
// Faults already logged in TrackLoad.
|
||||||
|
}
|
||||||
|
|
||||||
|
while (PendingLoads.TryTake(out _)) { }
|
||||||
|
|
||||||
foreach (var emote in EmoteImages.Values)
|
foreach (var emote in EmoteImages.Values)
|
||||||
emote.InnerDispose();
|
emote.InnerDispose();
|
||||||
}
|
}
|
||||||
@@ -175,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,18 +238,21 @@ public static class EmoteCache
|
|||||||
|
|
||||||
internal async Task<byte[]> LoadAsync(Emote emote, CancellationToken ct)
|
internal async Task<byte[]> LoadAsync(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// BetterTTV-supplied Id and ImageType are interpolated straight
|
// Path-traversal guard: resolve and verify the candidate path stays
|
||||||
// into the filename. HTTPS protects the wire, but a compromised
|
// inside the cache directory before reading or writing.
|
||||||
// upstream could still hand us "../foo" and write into the
|
var dir = Path.GetFullPath(
|
||||||
// pluginConfigs root (or worse). Resolve the candidate path and
|
Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1")
|
||||||
// refuse anything that escapes the cache directory.
|
);
|
||||||
var dir = Path.GetFullPath(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1"));
|
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
var dirPrefix = dir.EndsWith(Path.DirectorySeparatorChar) ? dir : dir + Path.DirectorySeparatorChar;
|
var dirPrefix = dir.EndsWith(Path.DirectorySeparatorChar)
|
||||||
|
? dir
|
||||||
|
: dir + Path.DirectorySeparatorChar;
|
||||||
var filePath = Path.GetFullPath(Path.Join(dir, $"{emote.Id}.{emote.ImageType}"));
|
var filePath = Path.GetFullPath(Path.Join(dir, $"{emote.Id}.{emote.ImageType}"));
|
||||||
if (!filePath.StartsWith(dirPrefix, StringComparison.Ordinal))
|
if (!filePath.StartsWith(dirPrefix, StringComparison.Ordinal))
|
||||||
throw new InvalidOperationException($"Emote path escapes cache directory: id={emote.Id}, type={emote.ImageType}");
|
throw new InvalidOperationException(
|
||||||
|
$"Emote path escapes cache directory: id={emote.Id}, type={emote.ImageType}"
|
||||||
|
);
|
||||||
|
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
@@ -218,7 +263,12 @@ public static class EmoteCache
|
|||||||
var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct);
|
var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct);
|
||||||
RawData = await content.Content.ReadAsByteArrayAsync(ct);
|
RawData = await content.Content.ReadAsByteArrayAsync(ct);
|
||||||
|
|
||||||
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
await using var stream = new FileStream(
|
||||||
|
filePath,
|
||||||
|
FileMode.Create,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.Read
|
||||||
|
);
|
||||||
await stream.WriteAsync(RawData, ct);
|
await stream.WriteAsync(RawData, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,11 +283,12 @@ public static class EmoteCache
|
|||||||
public ImGuiEmote Prepare(Emote emote)
|
public ImGuiEmote Prepare(Emote emote)
|
||||||
{
|
{
|
||||||
var ct = EmoteCache.Token;
|
var ct = EmoteCache.Token;
|
||||||
Task.Run(() => Load(emote, ct), ct);
|
// Task.Run keeps the sync prefix off the ImGui render thread.
|
||||||
|
EmoteCache.TrackLoad(Task.Run(() => LoadAsyncTracked(emote, ct), ct), emote.Code);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Load(Emote emote, CancellationToken ct)
|
private async Task LoadAsyncTracked(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -246,18 +297,17 @@ public static class EmoteCache
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image, cancellationToken: ct);
|
Texture = await Plugin.TextureProvider.CreateFromImageAsync(
|
||||||
|
image,
|
||||||
|
cancellationToken: ct
|
||||||
|
);
|
||||||
IsLoaded = true;
|
IsLoaded = true;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException) { }
|
||||||
{
|
|
||||||
// Plugin disposed mid-load; the EmoteImages entry is also
|
|
||||||
// being torn down, no extra cleanup needed.
|
|
||||||
}
|
|
||||||
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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,11 +360,11 @@ public static class EmoteCache
|
|||||||
public ImGuiGif Prepare(Emote emote)
|
public ImGuiGif Prepare(Emote emote)
|
||||||
{
|
{
|
||||||
var ct = EmoteCache.Token;
|
var ct = EmoteCache.Token;
|
||||||
Task.Run(() => Load(emote, ct), ct);
|
EmoteCache.TrackLoad(Task.Run(() => LoadAsyncTracked(emote, ct), ct), emote.Code);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Load(Emote emote, CancellationToken ct)
|
private async Task LoadAsyncTracked(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -334,13 +384,17 @@ public static class EmoteCache
|
|||||||
|
|
||||||
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
||||||
|
|
||||||
// Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
|
// Match browser behaviour: anything under 20ms rounds up to 100ms.
|
||||||
if (delay < 0.02f)
|
if (delay < 0.02f)
|
||||||
delay = 0.1f;
|
delay = 0.1f;
|
||||||
|
|
||||||
var buffer = new byte[4 * frame.Width * frame.Height];
|
var buffer = new byte[4 * frame.Width * frame.Height];
|
||||||
frame.CopyPixelDataTo(buffer);
|
frame.CopyPixelDataTo(buffer);
|
||||||
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer, cancellationToken: ct);
|
var tex = await Plugin.TextureProvider.CreateFromRawAsync(
|
||||||
|
RawImageSpecification.Rgba32(frame.Width, frame.Height),
|
||||||
|
buffer,
|
||||||
|
cancellationToken: ct
|
||||||
|
);
|
||||||
frames.Add((tex, delay));
|
frames.Add((tex, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +403,7 @@ public static class EmoteCache
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Plugin disposed mid-load; partial frames are released by
|
// Plugin disposed mid-load; release any partial frames.
|
||||||
// InnerDispose on the next dispose pass.
|
|
||||||
foreach (var f in Frames)
|
foreach (var f in Frames)
|
||||||
f.Texture.Dispose();
|
f.Texture.Dispose();
|
||||||
Frames = [];
|
Frames = [];
|
||||||
@@ -358,8 +411,8 @@ 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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,42 +13,42 @@ internal enum ExportFormat
|
|||||||
|
|
||||||
internal static class ExportFormatExt
|
internal static class ExportFormatExt
|
||||||
{
|
{
|
||||||
internal static string Extension(this ExportFormat fmt) => fmt switch
|
internal static string Extension(this ExportFormat fmt) =>
|
||||||
{
|
fmt switch
|
||||||
ExportFormat.Markdown => "md",
|
{
|
||||||
ExportFormat.Json => "json",
|
ExportFormat.Markdown => "md",
|
||||||
ExportFormat.Csv => "csv",
|
ExportFormat.Json => "json",
|
||||||
_ => "txt",
|
ExportFormat.Csv => "csv",
|
||||||
};
|
_ => "txt",
|
||||||
|
};
|
||||||
|
|
||||||
internal static string Filter(this ExportFormat fmt) => fmt switch
|
internal static string Filter(this ExportFormat fmt) =>
|
||||||
{
|
fmt switch
|
||||||
ExportFormat.Markdown => ".md",
|
{
|
||||||
ExportFormat.Json => ".json",
|
ExportFormat.Markdown => ".md",
|
||||||
ExportFormat.Csv => ".csv",
|
ExportFormat.Json => ".json",
|
||||||
_ => ".txt",
|
ExportFormat.Csv => ".csv",
|
||||||
};
|
_ => ".txt",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Serializes message snapshots to Markdown, JSON, or CSV.
|
||||||
/// Serializes message snapshots into Markdown, JSON, or CSV. The caller is
|
// Caller handles pre-filtering except sender substring, which requires deserialized SeString.TextValue.
|
||||||
/// expected to filter the input enumerable; this class only handles
|
|
||||||
/// formatting and writes to the supplied path. Sender substring filtering
|
|
||||||
/// happens here because it requires deserialized SeString.TextValue.
|
|
||||||
/// </summary>
|
|
||||||
internal static class MessageExporter
|
internal static class MessageExporter
|
||||||
{
|
{
|
||||||
internal record FilterDescription(
|
internal record FilterDescription(
|
||||||
IReadOnlyCollection<int>? ChatTypes,
|
IReadOnlyCollection<int>? ChatTypes,
|
||||||
DateTimeOffset? From,
|
DateTimeOffset? From,
|
||||||
DateTimeOffset? To,
|
DateTimeOffset? To,
|
||||||
string? SenderSubstring);
|
string? SenderSubstring
|
||||||
|
);
|
||||||
|
|
||||||
internal static int ExportToFile(
|
internal static int ExportToFile(
|
||||||
string path,
|
string path,
|
||||||
ExportFormat format,
|
ExportFormat format,
|
||||||
IEnumerable<Message> messages,
|
IEnumerable<Message> messages,
|
||||||
FilterDescription filter)
|
FilterDescription filter
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var matching = filter.SenderSubstring is { Length: > 0 } needle
|
var matching = filter.SenderSubstring is { Length: > 0 } needle
|
||||||
? messages.Where(m => MatchesSender(m, needle))
|
? messages.Where(m => MatchesSender(m, needle))
|
||||||
@@ -64,10 +64,14 @@ internal static class MessageExporter
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool MatchesSender(Message m, string needle)
|
private static bool MatchesSender(Message m, string needle) =>
|
||||||
=> m.SenderSource.TextValue.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
m.SenderSource.TextValue.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private static int WriteMarkdown(StreamWriter w, IEnumerable<Message> messages, FilterDescription filter)
|
private static int WriteMarkdown(
|
||||||
|
StreamWriter w,
|
||||||
|
IEnumerable<Message> messages,
|
||||||
|
FilterDescription filter
|
||||||
|
)
|
||||||
{
|
{
|
||||||
w.WriteLine("# Hellion Chat Export");
|
w.WriteLine("# Hellion Chat Export");
|
||||||
w.WriteLine();
|
w.WriteLine();
|
||||||
@@ -92,6 +96,7 @@ internal static class MessageExporter
|
|||||||
var chatType = (ChatType)(ushort)m.Code.Type;
|
var chatType = (ChatType)(ushort)m.Code.Type;
|
||||||
var sender = m.SenderSource.TextValue.Trim().Trim('<', '>', '[', ']', ':').Trim();
|
var sender = m.SenderSource.TextValue.Trim().Trim('<', '>', '[', ']', ':').Trim();
|
||||||
var content = m.ContentSource.TextValue;
|
var content = m.ContentSource.TextValue;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(sender))
|
if (string.IsNullOrEmpty(sender))
|
||||||
w.WriteLine($"**[{localDate:HH:mm}] {chatType}:** {content}");
|
w.WriteLine($"**[{localDate:HH:mm}] {chatType}:** {content}");
|
||||||
else
|
else
|
||||||
@@ -107,7 +112,9 @@ internal static class MessageExporter
|
|||||||
private static void WriteFilterSummaryMarkdown(StreamWriter w, FilterDescription filter)
|
private static void WriteFilterSummaryMarkdown(StreamWriter w, FilterDescription filter)
|
||||||
{
|
{
|
||||||
if (filter.ChatTypes is { Count: > 0 })
|
if (filter.ChatTypes is { Count: > 0 })
|
||||||
w.WriteLine($"ChatTypes: {string.Join(", ", filter.ChatTypes.Select(t => $"{(ChatType)(ushort)t}({t})"))}");
|
w.WriteLine(
|
||||||
|
$"ChatTypes: {string.Join(", ", filter.ChatTypes.Select(t => $"{(ChatType)(ushort)t}({t})"))}"
|
||||||
|
);
|
||||||
if (filter.From is not null)
|
if (filter.From is not null)
|
||||||
w.WriteLine($"From: {filter.From.Value.ToLocalTime():yyyy-MM-dd HH:mm}");
|
w.WriteLine($"From: {filter.From.Value.ToLocalTime():yyyy-MM-dd HH:mm}");
|
||||||
if (filter.To is not null)
|
if (filter.To is not null)
|
||||||
@@ -116,10 +123,13 @@ internal static class MessageExporter
|
|||||||
w.WriteLine($"Sender contains: \"{filter.SenderSubstring}\"");
|
w.WriteLine($"Sender contains: \"{filter.SenderSubstring}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int WriteJson(StreamWriter w, IEnumerable<Message> messages, FilterDescription filter)
|
private static int WriteJson(
|
||||||
|
StreamWriter w,
|
||||||
|
IEnumerable<Message> messages,
|
||||||
|
FilterDescription filter
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Manual JSON to avoid pulling in System.Text.Json policy choices.
|
// Manual JSON to avoid System.Text.Json policy coupling.
|
||||||
// Output is a single object with metadata and an array of messages.
|
|
||||||
w.Write("{\n \"exported_at\": \"");
|
w.Write("{\n \"exported_at\": \"");
|
||||||
w.Write(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
|
w.Write(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
|
||||||
w.Write("\",\n \"plugin\": \"Hellion Chat\",\n");
|
w.Write("\",\n \"plugin\": \"Hellion Chat\",\n");
|
||||||
@@ -130,9 +140,17 @@ internal static class MessageExporter
|
|||||||
else
|
else
|
||||||
w.Write("null");
|
w.Write("null");
|
||||||
w.Write(",\n \"from\": ");
|
w.Write(",\n \"from\": ");
|
||||||
w.Write(filter.From is null ? "null" : "\"" + filter.From.Value.ToString("O", CultureInfo.InvariantCulture) + "\"");
|
w.Write(
|
||||||
|
filter.From is null
|
||||||
|
? "null"
|
||||||
|
: "\"" + filter.From.Value.ToString("O", CultureInfo.InvariantCulture) + "\""
|
||||||
|
);
|
||||||
w.Write(",\n \"to\": ");
|
w.Write(",\n \"to\": ");
|
||||||
w.Write(filter.To is null ? "null" : "\"" + filter.To.Value.ToString("O", CultureInfo.InvariantCulture) + "\"");
|
w.Write(
|
||||||
|
filter.To is null
|
||||||
|
? "null"
|
||||||
|
: "\"" + filter.To.Value.ToString("O", CultureInfo.InvariantCulture) + "\""
|
||||||
|
);
|
||||||
w.Write(",\n \"sender_substring\": ");
|
w.Write(",\n \"sender_substring\": ");
|
||||||
w.Write(filter.SenderSubstring is null ? "null" : JsonString(filter.SenderSubstring));
|
w.Write(filter.SenderSubstring is null ? "null" : JsonString(filter.SenderSubstring));
|
||||||
w.Write("\n },\n \"messages\": [\n");
|
w.Write("\n },\n \"messages\": [\n");
|
||||||
@@ -166,9 +184,13 @@ internal static class MessageExporter
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int WriteCsv(StreamWriter w, IEnumerable<Message> messages, FilterDescription filter)
|
private static int WriteCsv(
|
||||||
|
StreamWriter w,
|
||||||
|
IEnumerable<Message> messages,
|
||||||
|
FilterDescription filter
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Header line always written so empty exports are still importable.
|
// Header always written so empty exports remain importable.
|
||||||
w.WriteLine("Date,ChatType,ChatTypeName,Sender,Content,Receiver,ContentId");
|
w.WriteLine("Date,ChatType,ChatTypeName,Sender,Content,Receiver,ContentId");
|
||||||
var count = 0;
|
var count = 0;
|
||||||
foreach (var m in messages)
|
foreach (var m in messages)
|
||||||
@@ -201,13 +223,27 @@ internal static class MessageExporter
|
|||||||
{
|
{
|
||||||
switch (c)
|
switch (c)
|
||||||
{
|
{
|
||||||
case '"': sb.Append("\\\""); break;
|
case '"':
|
||||||
case '\\': sb.Append("\\\\"); break;
|
sb.Append("\\\"");
|
||||||
case '\b': sb.Append("\\b"); break;
|
break;
|
||||||
case '\f': sb.Append("\\f"); break;
|
case '\\':
|
||||||
case '\n': sb.Append("\\n"); break;
|
sb.Append("\\\\");
|
||||||
case '\r': sb.Append("\\r"); break;
|
break;
|
||||||
case '\t': sb.Append("\\t"); break;
|
case '\b':
|
||||||
|
sb.Append("\\b");
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
sb.Append("\\f");
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
sb.Append("\\n");
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
sb.Append("\\r");
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
sb.Append("\\t");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (c < 0x20)
|
if (c < 0x20)
|
||||||
sb.Append($"\\u{(int)c:x4}");
|
sb.Append($"\\u{(int)c:x4}");
|
||||||
|
|||||||
+214
-111
@@ -1,48 +1,208 @@
|
|||||||
using Dalamud;
|
using Dalamud;
|
||||||
|
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.Bindings.ImGui;
|
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 = [];
|
||||||
|
|
||||||
public static readonly HashSet<float> AxisFontSizeList =
|
public static readonly HashSet<float> AxisFontSizeList =
|
||||||
[
|
[
|
||||||
9.6f, 10f, 12f, 14f, 16f,
|
9.6f,
|
||||||
18f, 18.4f, 20f, 23f, 34f,
|
10f,
|
||||||
36f, 40f, 45f, 46f, 68f, 90f,
|
12f,
|
||||||
|
14f,
|
||||||
|
16f,
|
||||||
|
18f,
|
||||||
|
18.4f,
|
||||||
|
20f,
|
||||||
|
23f,
|
||||||
|
34f,
|
||||||
|
36f,
|
||||||
|
40f,
|
||||||
|
45f,
|
||||||
|
46f,
|
||||||
|
68f,
|
||||||
|
90f,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// <summary>
|
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
||||||
/// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
|
|
||||||
/// extracted from the assembly's manifest resources on first use; the
|
|
||||||
/// load happens inside the font atlas build callback so we keep the
|
|
||||||
/// allocation off the plugin constructor's hot path.
|
|
||||||
/// </summary>
|
|
||||||
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 = typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
|
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
|
||||||
?? throw new FileNotFoundException("Hellion font resource not embedded in the assembly");
|
"HellionFont.ttf"
|
||||||
|
);
|
||||||
|
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();
|
||||||
@@ -54,11 +214,9 @@ public class FontManager
|
|||||||
ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
|
ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
|
||||||
{
|
{
|
||||||
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
|
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
|
||||||
// text
|
|
||||||
foreach (var range in ranges)
|
foreach (var range in ranges)
|
||||||
builder.AddRanges((ushort*)range);
|
builder.AddRanges((ushort*)range);
|
||||||
|
|
||||||
// chars
|
|
||||||
if (chars != null)
|
if (chars != null)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < chars.Count; i += 2)
|
for (var i = 0; i < chars.Count; i += 2)
|
||||||
@@ -66,8 +224,8 @@ public class FontManager
|
|||||||
if (chars[i] == 0)
|
if (chars[i] == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
for (var j = (uint) chars[i]; j <= chars[i + 1]; j++)
|
for (var j = (uint)chars[i]; j <= chars[i + 1]; j++)
|
||||||
builder.AddChar((ushort) j);
|
builder.AddChar((ushort)j);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,16 +234,14 @@ 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("ĂăÂâÎîȘșȚț");
|
||||||
|
|
||||||
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
|
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
|
||||||
for (var i = 0x2460; i <= 0x24B5; i++)
|
for (var i = 0x2460; i <= 0x24B5; i++)
|
||||||
builder.AddChar((char) i);
|
builder.AddChar((char)i);
|
||||||
|
|
||||||
builder.AddChar('⓪');
|
builder.AddChar('⓪');
|
||||||
return builder.BuildRangesToArray();
|
return builder.BuildRangesToArray();
|
||||||
@@ -100,99 +256,46 @@ public class FontManager
|
|||||||
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BuildFonts()
|
// Add font with fallback to NotoSansCjkRegular if unavailable
|
||||||
{
|
private static ImFontPtr AddFontWithFallback(
|
||||||
SetUpRanges();
|
IFontAtlasBuildToolkitPreBuild tk,
|
||||||
|
IFontId fontId,
|
||||||
Axis = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2)));
|
SafeFontConfig config,
|
||||||
AxisItalic = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
|
string slot
|
||||||
{
|
)
|
||||||
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 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font)
|
|
||||||
// wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet.
|
|
||||||
// Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
|
|
||||||
// Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei
|
|
||||||
// UseHellionFont=true wirkungslos, was 4K-User mit größerer
|
|
||||||
// Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten).
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to add a user-configured font to the build toolkit, falling back to
|
|
||||||
/// the bundled NotoSansCjkRegular asset if the configured font isn't
|
|
||||||
/// available on the system. Without this guard a stale SystemFontId
|
|
||||||
/// pointing at a font the user uninstalled or that never existed on
|
|
||||||
/// Linux (e.g. "Crimson Text") tears down the entire font atlas build.
|
|
||||||
/// </summary>
|
|
||||||
private static ImFontPtr AddFontWithFallback(IFontAtlasBuildToolkitPreBuild tk, IFontId fontId, SafeFontConfig config, string slot)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return fontId.AddToBuildToolkit(tk, config);
|
return fontId.AddToBuildToolkit(tk, config);
|
||||||
}
|
}
|
||||||
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
|
catch (Exception e)
|
||||||
|
when (e
|
||||||
|
is FileNotFoundException
|
||||||
|
or DirectoryNotFoundException
|
||||||
|
or IOException
|
||||||
|
or InvalidOperationException
|
||||||
|
or ArgumentException
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning(e, $"Configured {slot} font unavailable, falling back to NotoSansCjkRegular");
|
// 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,
|
||||||
|
$"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float SizeInPt(float px) => (float) (px * 3.0 / 4.0);
|
public static float SizeInPt(float px) => (float)(px * 3.0 / 4.0);
|
||||||
public static float SizeInPx(float pt) => (float) (pt * 4.0 / 3.0);
|
|
||||||
public static float GetFontSize() => Plugin.Config.FontsEnabled ? Plugin.Config.GlobalFontV2.SizePx : SizeInPx(Plugin.Config.FontSizeV2);
|
public static float SizeInPx(float pt) => (float)(pt * 4.0 / 3.0);
|
||||||
|
|
||||||
|
public static float GetFontSize() =>
|
||||||
|
Plugin.Config.FontsEnabled
|
||||||
|
? Plugin.Config.GlobalFontV2.SizePx
|
||||||
|
: SizeInPx(Plugin.Config.FontSizeV2);
|
||||||
}
|
}
|
||||||
|
|||||||
+290
-117
@@ -1,8 +1,4 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using HellionChat.Code;
|
|
||||||
using HellionChat.GameFunctions.Types;
|
|
||||||
using HellionChat.Resources;
|
|
||||||
using HellionChat.Util;
|
|
||||||
using Dalamud.Game.Config;
|
using Dalamud.Game.Config;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
@@ -17,9 +13,13 @@ using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
|||||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using HellionChat.Code;
|
||||||
|
using HellionChat.GameFunctions.Types;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
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;
|
||||||
@@ -28,20 +28,55 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
// Functions
|
// Functions
|
||||||
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")]
|
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")]
|
||||||
private readonly delegate* unmanaged<RaptureLogModule*, ushort, Utf8String*, Utf8String*, ulong, ulong, ushort, byte, int, byte, void> PrintTellNative = null!;
|
private readonly delegate* unmanaged<
|
||||||
|
RaptureLogModule*,
|
||||||
|
ushort,
|
||||||
|
Utf8String*,
|
||||||
|
Utf8String*,
|
||||||
|
ulong,
|
||||||
|
ulong,
|
||||||
|
ushort,
|
||||||
|
byte,
|
||||||
|
int,
|
||||||
|
byte,
|
||||||
|
void> PrintTellNative = null!;
|
||||||
|
|
||||||
[Signature("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8C 24 ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 ?? 48 8B 8C 24")]
|
[Signature(
|
||||||
private readonly delegate* unmanaged<NetworkModule*, ulong, ushort, Utf8String*, Utf8String*, ushort, ushort, byte> SendTellNative = null!;
|
"E8 ?? ?? ?? ?? 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8C 24 ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 ?? 48 8B 8C 24"
|
||||||
|
)]
|
||||||
|
private readonly delegate* unmanaged<
|
||||||
|
NetworkModule*,
|
||||||
|
ulong,
|
||||||
|
ushort,
|
||||||
|
Utf8String*,
|
||||||
|
Utf8String*,
|
||||||
|
ushort,
|
||||||
|
ushort,
|
||||||
|
byte> SendTellNative = null!;
|
||||||
|
|
||||||
// Client::UI::AddonChatLog.OnRefresh
|
// Client::UI::AddonChatLog.OnRefresh
|
||||||
[Signature("40 53 57 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4D 8B F8", DetourName = nameof(ChatLogRefreshDetour))]
|
[Signature(
|
||||||
|
"40 53 57 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4D 8B F8",
|
||||||
|
DetourName = nameof(ChatLogRefreshDetour)
|
||||||
|
)]
|
||||||
private Hook<ChatLogRefreshDelegate>? ChatLogRefreshHook = null!;
|
private Hook<ChatLogRefreshDelegate>? ChatLogRefreshHook = null!;
|
||||||
private delegate byte ChatLogRefreshDelegate(nint log, ushort eventId, AtkValue* value);
|
private delegate byte ChatLogRefreshDelegate(nint log, ushort eventId, AtkValue* value);
|
||||||
|
|
||||||
// Replace with CS version later
|
// Replace with CS version later
|
||||||
[Signature("48 89 5C 24 ?? 55 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 83 B9", DetourName = nameof(ContextMenuTellInForayDetour))]
|
[Signature(
|
||||||
|
"48 89 5C 24 ?? 55 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 83 B9",
|
||||||
|
DetourName = nameof(ContextMenuTellInForayDetour)
|
||||||
|
)]
|
||||||
private Hook<ContextMenuTellInForayDelegate>? ContextMenuTellInForayHook = null!;
|
private Hook<ContextMenuTellInForayDelegate>? ContextMenuTellInForayHook = null!;
|
||||||
private delegate void ContextMenuTellInForayDelegate(RaptureShellModule* module, Utf8String* playerName, Utf8String* worldName, ushort worldId, ulong accountId, ulong contentId, ushort reason);
|
private delegate void ContextMenuTellInForayDelegate(
|
||||||
|
RaptureShellModule* module,
|
||||||
|
Utf8String* playerName,
|
||||||
|
Utf8String* worldName,
|
||||||
|
ushort worldId,
|
||||||
|
ulong accountId,
|
||||||
|
ulong contentId,
|
||||||
|
ushort reason
|
||||||
|
);
|
||||||
|
|
||||||
private readonly Hook<AgentChatLog.Delegates.ChangeChannelName>? ChangeChannelNameHook;
|
private readonly Hook<AgentChatLog.Delegates.ChangeChannelName>? ChangeChannelNameHook;
|
||||||
private readonly Hook<RaptureShellModule.Delegates.ReplyInSelectedChatMode>? ReplyInSelectedChatModeHook;
|
private readonly Hook<RaptureShellModule.Delegates.ReplyInSelectedChatMode>? ReplyInSelectedChatModeHook;
|
||||||
@@ -58,27 +93,42 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
FullName = 0,
|
FullName = 0,
|
||||||
SurnameAbbreviated = 1,
|
SurnameAbbreviated = 1,
|
||||||
ForenameAbbreviated = 2,
|
ForenameAbbreviated = 2,
|
||||||
Initials = 3
|
Initials = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
ContextMenuTellInForayHook?.Enable();
|
ContextMenuTellInForayHook?.Enable();
|
||||||
|
|
||||||
ChangeChannelNameHook = Plugin.GameInteropProvider.HookFromAddress<AgentChatLog.Delegates.ChangeChannelName>(AgentChatLog.MemberFunctionPointers.ChangeChannelName, ChangeChannelNameDetour);
|
ChangeChannelNameHook =
|
||||||
|
Plugin.GameInteropProvider.HookFromAddress<AgentChatLog.Delegates.ChangeChannelName>(
|
||||||
|
AgentChatLog.MemberFunctionPointers.ChangeChannelName,
|
||||||
|
ChangeChannelNameDetour
|
||||||
|
);
|
||||||
ChangeChannelNameHook.Enable();
|
ChangeChannelNameHook.Enable();
|
||||||
|
|
||||||
ReplyInSelectedChatModeHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.ReplyInSelectedChatMode>(RaptureShellModule.MemberFunctionPointers.ReplyInSelectedChatMode, ReplyInSelectedChatModeDetour);
|
ReplyInSelectedChatModeHook =
|
||||||
|
Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.ReplyInSelectedChatMode>(
|
||||||
|
RaptureShellModule.MemberFunctionPointers.ReplyInSelectedChatMode,
|
||||||
|
ReplyInSelectedChatModeDetour
|
||||||
|
);
|
||||||
ReplyInSelectedChatModeHook.Enable();
|
ReplyInSelectedChatModeHook.Enable();
|
||||||
|
|
||||||
SetChatLogTellTargetHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTarget>(RaptureShellModule.MemberFunctionPointers.SetContextTellTarget, SetContextTellTarget);
|
SetChatLogTellTargetHook =
|
||||||
|
Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTarget>(
|
||||||
|
RaptureShellModule.MemberFunctionPointers.SetContextTellTarget,
|
||||||
|
SetContextTellTarget
|
||||||
|
);
|
||||||
SetChatLogTellTargetHook.Enable();
|
SetChatLogTellTargetHook.Enable();
|
||||||
|
|
||||||
Plugin.ClientState.Login += Login;
|
Plugin.ClientState.Login += Login;
|
||||||
@@ -108,12 +158,13 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
return utf == null ? null : utf->ToString();
|
return utf == null ? null : utf->ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetRotateIdx(RotateMode mode) => mode switch
|
private static int GetRotateIdx(RotateMode mode) =>
|
||||||
{
|
mode switch
|
||||||
RotateMode.Forward => 1,
|
{
|
||||||
RotateMode.Reverse => -1,
|
RotateMode.Forward => 1,
|
||||||
_ => 0,
|
RotateMode.Reverse => -1,
|
||||||
};
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
internal static void RotateLinkshellHistory(RotateMode mode)
|
internal static void RotateLinkshellHistory(RotateMode mode)
|
||||||
{
|
{
|
||||||
@@ -127,8 +178,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
internal static void RotateCrossLinkshellHistory(RotateMode mode) =>
|
internal static void RotateCrossLinkshellHistory(RotateMode mode) =>
|
||||||
UIModule.Instance()->RotateCrossLinkshellHistory(GetRotateIdx(mode));
|
UIModule.Instance()->RotateCrossLinkshellHistory(GetRotateIdx(mode));
|
||||||
|
|
||||||
// This function looks up a channel's user-defined color.
|
// Look up a channel's user-defined color, returns null if 0
|
||||||
// If this function ever returns 0, it returns null instead.
|
|
||||||
internal uint? GetChannelColor(ChatType type)
|
internal uint? GetChannelColor(ChatType type)
|
||||||
{
|
{
|
||||||
var parent = type.Parent();
|
var parent = type.Parent();
|
||||||
@@ -168,13 +218,12 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
|
|
||||||
if (Plugin.Functions.KeybindManager.DirectChat && LastTypedCharacter != null)
|
if (Plugin.Functions.KeybindManager.DirectChat && LastTypedCharacter != null)
|
||||||
{
|
{
|
||||||
// FIXME: this whole system sucks
|
// Capture the just-typed character input
|
||||||
// FIXME v2: I hate everything about this, but it works
|
|
||||||
Plugin.Framework.RunOnTick(() =>
|
Plugin.Framework.RunOnTick(() =>
|
||||||
{
|
{
|
||||||
string? input = null;
|
string? input = null;
|
||||||
|
|
||||||
var utf8Bytes = MemoryHelper.ReadRaw((nint)LastTypedCharacter+0x4, 2);
|
var utf8Bytes = MemoryHelper.ReadRaw((nint)LastTypedCharacter + 0x4, 2);
|
||||||
var chars = Encoding.UTF8.GetString(utf8Bytes).ToCharArray();
|
var chars = Encoding.UTF8.GetString(utf8Bytes).ToCharArray();
|
||||||
if (chars.Length == 0)
|
if (chars.Length == 0)
|
||||||
return;
|
return;
|
||||||
@@ -185,11 +234,13 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, });
|
Plugin.ChatLogWindow.Activated(
|
||||||
|
new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -197,7 +248,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
string? addIfNotPresent = null;
|
string? addIfNotPresent = null;
|
||||||
|
|
||||||
var str = value + 2;
|
var str = value + 2;
|
||||||
if (str != null && ((int) str->Type & 0xF) == (int) ValueType.String && str->String.HasValue)
|
if (str != null && ((int)str->Type & 0xF) == (int)ValueType.String && str->String.HasValue)
|
||||||
{
|
{
|
||||||
var add = str->String.ToString();
|
var add = str->String.ToString();
|
||||||
if (add.Length > 0)
|
if (add.Length > 0)
|
||||||
@@ -206,23 +257,23 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// We already called this function once, so we skip the duplicated call
|
// Prevent duplicate calls
|
||||||
// Also return the original value here so that vanilla chat receives all information
|
|
||||||
if (Plugin.ChatLogWindow.TellSpecial)
|
if (Plugin.ChatLogWindow.TellSpecial)
|
||||||
{
|
|
||||||
Plugin.Log.Information("Return early to prevent duplicated call...");
|
|
||||||
return ChatLogRefreshHook!.Original(log, eventId, value);
|
return ChatLogRefreshHook!.Original(log, eventId, value);
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, });
|
Plugin.ChatLogWindow.Activated(
|
||||||
|
new ChatActivatedArgs(new ChannelSwitchInfo(null))
|
||||||
|
{
|
||||||
|
AddIfNotPresent = addIfNotPresent,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevent the game from focusing the chat log
|
return 1; // Prevent vanilla chat log from gaining focus
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CStringPointer ChangeChannelNameDetour(AgentChatLog* agent)
|
private CStringPointer ChangeChannelNameDetour(AgentChatLog* agent)
|
||||||
@@ -231,9 +282,9 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
if (agent == null)
|
if (agent == null)
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
var channel = (uint) RaptureShellModule.Instance()->ChatType;
|
var channel = (uint)RaptureShellModule.Instance()->ChatType;
|
||||||
if (channel is 17 or 18)
|
if (channel is 17 or 18)
|
||||||
channel = (uint) InputChannel.Tell;
|
channel = (uint)InputChannel.Tell;
|
||||||
|
|
||||||
var name = SeString.Parse(agent->ChannelLabel);
|
var name = SeString.Parse(agent->ChannelLabel);
|
||||||
if (name.Payloads.Count == 0)
|
if (name.Payloads.Count == 0)
|
||||||
@@ -248,18 +299,18 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
|
|
||||||
string? playerName = null;
|
string? playerName = null;
|
||||||
ushort worldId = 0;
|
ushort worldId = 0;
|
||||||
if (channel == (uint) InputChannel.Tell)
|
if (channel == (uint)InputChannel.Tell)
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
Channel = (InputChannel) channel,
|
Channel = (InputChannel)channel,
|
||||||
Name = nameChunks,
|
Name = nameChunks,
|
||||||
TellTarget = playerName != null ? new TellTarget(playerName, worldId, 0, 0) : null
|
TellTarget = playerName != null ? new TellTarget(playerName, worldId, 0, 0) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@@ -274,33 +325,68 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetChannel((InputChannel) replyMode);
|
SetChannel((InputChannel)replyMode);
|
||||||
ReplyInSelectedChatModeHook!.Original(agent);
|
ReplyInSelectedChatModeHook!.Original(agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool SetContextTellTarget(RaptureShellModule* a1, Utf8String* playerName, Utf8String* worldName, ushort worldId, ulong accountId, ulong contentId, ushort reason, bool setChatType)
|
private bool SetContextTellTarget(
|
||||||
|
RaptureShellModule* a1,
|
||||||
|
Utf8String* playerName,
|
||||||
|
Utf8String* worldName,
|
||||||
|
ushort worldId,
|
||||||
|
ulong accountId,
|
||||||
|
ulong contentId,
|
||||||
|
ushort reason,
|
||||||
|
bool setChatType
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (playerName != null)
|
if (playerName != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason);
|
var target = new TellTarget(
|
||||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell, permanent: setChatType))
|
playerName->ToString(),
|
||||||
{
|
worldId,
|
||||||
TellReason = (TellReason) reason,
|
contentId,
|
||||||
TellTarget = target,
|
(TellReason)reason
|
||||||
});
|
);
|
||||||
|
Plugin.ChatLogWindow.Activated(
|
||||||
|
new ChatActivatedArgs(
|
||||||
|
new ChannelSwitchInfo(InputChannel.Tell, permanent: setChatType)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
TellReason = (TellReason)reason,
|
||||||
|
TellTarget = target,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetChatLogTellTargetHook!.Original(a1, playerName, worldName, worldId, accountId, contentId, reason, setChatType);
|
return SetChatLogTellTargetHook!.Original(
|
||||||
|
a1,
|
||||||
|
playerName,
|
||||||
|
worldName,
|
||||||
|
worldId,
|
||||||
|
accountId,
|
||||||
|
contentId,
|
||||||
|
reason,
|
||||||
|
setChatType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ContextMenuTellInForayDetour(RaptureShellModule* a1, Utf8String* playerName, Utf8String* worldName, ushort worldId, ulong accountId, ulong contentId, ushort reason)
|
private void ContextMenuTellInForayDetour(
|
||||||
|
RaptureShellModule* a1,
|
||||||
|
Utf8String* playerName,
|
||||||
|
Utf8String* worldName,
|
||||||
|
ushort worldId,
|
||||||
|
ulong accountId,
|
||||||
|
ulong contentId,
|
||||||
|
ushort reason
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (!Plugin.CurrentTab.CurrentChannel.UseTempChannel)
|
if (!Plugin.CurrentTab.CurrentChannel.UseTempChannel)
|
||||||
Plugin.CurrentTab.CurrentChannel.UseTempChannel = true;
|
Plugin.CurrentTab.CurrentChannel.UseTempChannel = true;
|
||||||
@@ -309,36 +395,56 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason);
|
var target = new TellTarget(
|
||||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell))
|
playerName->ToString(),
|
||||||
{
|
worldId,
|
||||||
TellReason = (TellReason) reason,
|
contentId,
|
||||||
TellTarget = target,
|
(TellReason)reason
|
||||||
TellSpecial = Sheets.IsInForay(), // Handle Eureka/Bozja special
|
);
|
||||||
});
|
Plugin.ChatLogWindow.Activated(
|
||||||
|
new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell))
|
||||||
|
{
|
||||||
|
TellReason = (TellReason)reason,
|
||||||
|
TellTarget = target,
|
||||||
|
TellSpecial = Sheets.IsInForay(), // Handle Eureka/Bozja special
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextMenuTellInForayHook!.Original(a1, playerName, worldName, worldId, accountId, contentId, reason);
|
ContextMenuTellInForayHook!.Original(
|
||||||
|
a1,
|
||||||
|
playerName,
|
||||||
|
worldName,
|
||||||
|
worldId,
|
||||||
|
accountId,
|
||||||
|
contentId,
|
||||||
|
reason
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// ---------------------------------------------------------------
|
||||||
/// Returns true if the channel is any non-linkshell channel, or if the
|
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||||
/// linkshell actually exists.
|
// - Renamed ValidAnyLinkshell -> IsChannelOrExistingLinkshell. The
|
||||||
/// </summary>
|
// name now states intent: returns true for any non-linkshell
|
||||||
internal static bool ValidAnyLinkshell(InputChannel channel)
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,17 +452,22 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
if (idx > 7)
|
if (idx > 7)
|
||||||
return false;
|
return false;
|
||||||
return InfoProxyLinkshell.Instance()->LinkShells[(int) idx].Id != 0;
|
return InfoProxyLinkshell.Instance()->LinkShells[(int)idx].Id != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool ValidCrossLinkshell(uint idx)
|
internal static bool ValidCrossLinkshell(uint idx)
|
||||||
{
|
{
|
||||||
if (idx > 7)
|
if (idx > 7)
|
||||||
return false;
|
return false;
|
||||||
return InfoProxyCrossWorldLinkshell.Instance()->CrossWorldLinkshells[(int) idx].Name.Length > 0;
|
return InfoProxyCrossWorldLinkshell.Instance()->CrossWorldLinkshells[(int)idx].Name.Length
|
||||||
|
> 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint? RotateLinkshell(uint currentIndex, RotateMode rotate, Func<uint, bool> validFn)
|
private static uint? RotateLinkshell(
|
||||||
|
uint currentIndex,
|
||||||
|
RotateMode rotate,
|
||||||
|
Func<uint, bool> validFn
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (rotate == RotateMode.None)
|
if (rotate == RotateMode.None)
|
||||||
return null;
|
return null;
|
||||||
@@ -365,13 +476,12 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
RotateMode.Forward => 1,
|
RotateMode.Forward => 1,
|
||||||
RotateMode.Reverse => -1,
|
RotateMode.Reverse => -1,
|
||||||
_ => 1
|
_ => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Iterate up to 8 times to find a valid linkshell.
|
for (var i = 0; i < 8; i++) // Find valid linkshell within 8 iterations
|
||||||
for (var i = 0; i < 8; i++)
|
|
||||||
{
|
{
|
||||||
currentIndex = (uint) ((8 + currentIndex + delta) % 8);
|
currentIndex = (uint)((8 + currentIndex + delta) % 8);
|
||||||
if (validFn(currentIndex))
|
if (validFn(currentIndex))
|
||||||
return currentIndex;
|
return currentIndex;
|
||||||
}
|
}
|
||||||
@@ -379,30 +489,43 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static InputChannel? ResolveTempInputChannel(InputChannel? currentTempChannel, InputChannel channel, RotateMode rotate)
|
internal static InputChannel? ResolveTempInputChannel(
|
||||||
|
InputChannel? currentTempChannel,
|
||||||
|
InputChannel channel,
|
||||||
|
RotateMode rotate
|
||||||
|
)
|
||||||
{
|
{
|
||||||
switch (channel)
|
switch (channel)
|
||||||
{
|
{
|
||||||
case InputChannel.Linkshell1 or InputChannel.CrossLinkshell1 when rotate != RotateMode.None:
|
case InputChannel.Linkshell1
|
||||||
|
or InputChannel.CrossLinkshell1 when rotate != RotateMode.None:
|
||||||
{
|
{
|
||||||
var module = UIModule.Instance();
|
var module = UIModule.Instance();
|
||||||
|
|
||||||
var currentIndex = channel is InputChannel.Linkshell1 ? (uint) module->LinkshellCycle : (uint) module->CrossWorldLinkshellCycle;
|
var currentIndex =
|
||||||
|
channel is InputChannel.Linkshell1
|
||||||
|
? (uint)module->LinkshellCycle
|
||||||
|
: (uint)module->CrossWorldLinkshellCycle;
|
||||||
if (currentTempChannel != null)
|
if (currentTempChannel != null)
|
||||||
{
|
{
|
||||||
switch (channel)
|
switch (channel)
|
||||||
{
|
{
|
||||||
case InputChannel.Linkshell1 when currentTempChannel.Value.IsLinkshell():
|
case InputChannel.Linkshell1 when currentTempChannel.Value.IsLinkshell():
|
||||||
case InputChannel.CrossLinkshell1 when currentTempChannel.Value.IsCrossLinkshell():
|
case InputChannel.CrossLinkshell1
|
||||||
|
when currentTempChannel.Value.IsCrossLinkshell():
|
||||||
currentIndex = currentTempChannel.Value.LinkshellIndex();
|
currentIndex = currentTempChannel.Value.LinkshellIndex();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell);
|
var idx = RotateLinkshell(
|
||||||
|
currentIndex,
|
||||||
|
rotate,
|
||||||
|
channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell
|
||||||
|
);
|
||||||
// RotateLinkshell returns null when no valid linkshell is found within 8 iterations.
|
// RotateLinkshell returns null when no valid linkshell is found within 8 iterations.
|
||||||
// Forward the null so the caller can keep the existing channel instead of crashing on nullable arithmetic.
|
// Forward the null so the caller can keep the existing channel instead of crashing on nullable arithmetic.
|
||||||
return idx is null ? null : channel + idx.Value;
|
return idx is null ? null : channel + idx.Value; // null if not found, otherwise new channel
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return channel;
|
return channel;
|
||||||
@@ -411,11 +534,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
|
|
||||||
internal void SetChannel(InputChannel channel, TellTarget? tellTarget = null)
|
internal void SetChannel(InputChannel channel, TellTarget? tellTarget = null)
|
||||||
{
|
{
|
||||||
// ExtraChat linkshells aren't supported in game so we never want to
|
// Ignore ExtraChat linkshells (use ChatLogWindow.SetChannel() instead)
|
||||||
// call the ChangeChatChannel function with them.
|
|
||||||
//
|
|
||||||
// Callers should call ChatLogWindow.SetChannel() which handles
|
|
||||||
// ExtraChat channels
|
|
||||||
if (channel.IsExtraChatLinkshell())
|
if (channel.IsExtraChatLinkshell())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -424,18 +543,30 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SetEurekaTellChannel(string name, string worldName, ushort worldId, ulong accountId, ulong objectId, ushort reason, bool setChatType)
|
internal void SetEurekaTellChannel(
|
||||||
|
string name,
|
||||||
|
string worldName,
|
||||||
|
ushort worldId,
|
||||||
|
ulong accountId,
|
||||||
|
ulong objectId,
|
||||||
|
ushort reason,
|
||||||
|
bool setChatType
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// param6 is 0 for contentId and 1 for objectId
|
|
||||||
// param7 is always 0 ?
|
|
||||||
|
|
||||||
if (!Plugin.CurrentTab.CurrentChannel.UseTempChannel)
|
if (!Plugin.CurrentTab.CurrentChannel.UseTempChannel)
|
||||||
Plugin.CurrentTab.CurrentChannel.UseTempChannel = true;
|
Plugin.CurrentTab.CurrentChannel.UseTempChannel = true;
|
||||||
|
|
||||||
@@ -446,7 +577,17 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
var utfName = Utf8String.FromString(name);
|
var utfName = Utf8String.FromString(name);
|
||||||
var utfWorld = Utf8String.FromString(worldName);
|
var utfWorld = Utf8String.FromString(worldName);
|
||||||
|
|
||||||
RaptureShellModule.Instance()->SetTellTargetInForay(utfName, utfWorld, worldId, accountId, objectId, reason, setChatType);
|
RaptureShellModule
|
||||||
|
.Instance()
|
||||||
|
->SetTellTargetInForay(
|
||||||
|
utfName,
|
||||||
|
utfWorld,
|
||||||
|
worldId,
|
||||||
|
accountId,
|
||||||
|
objectId,
|
||||||
|
reason,
|
||||||
|
setChatType
|
||||||
|
);
|
||||||
|
|
||||||
utfName->Dtor(true);
|
utfName->Dtor(true);
|
||||||
utfWorld->Dtor(true);
|
utfWorld->Dtor(true);
|
||||||
@@ -475,19 +616,30 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
mes->Dtor(true);
|
mes->Dtor(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SendTell(TellReason reason, ulong contentId, string name, ushort homeWorld, byte[] message, string rawText)
|
internal void SendTell(
|
||||||
|
TellReason reason,
|
||||||
|
ulong contentId,
|
||||||
|
string name,
|
||||||
|
ushort homeWorld,
|
||||||
|
byte[] message,
|
||||||
|
string rawText
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (contentId == 0)
|
if (contentId == 0)
|
||||||
{
|
{
|
||||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||||
Plugin.Log.Warning("Tried to send a tell with ContentId being 0, sorry this is an internal error.");
|
_logger.LogWarning(
|
||||||
|
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var uName = Utf8String.FromString(name);
|
var uName = Utf8String.FromString(name);
|
||||||
var uMessage = Utf8String.FromSequence(message.NullTerminate());
|
var uMessage = Utf8String.FromSequence(message.NullTerminate());
|
||||||
|
|
||||||
var encoded = Utf8String.FromUtf8String(PronounModule.Instance()->ProcessString(uMessage, true));
|
var encoded = Utf8String.FromUtf8String(
|
||||||
|
PronounModule.Instance()->ProcessString(uMessage, true)
|
||||||
|
);
|
||||||
var decoded = EncodeMessage(rawText);
|
var decoded = EncodeMessage(rawText);
|
||||||
AutoTranslate.ReplaceWithPayload(ref decoded);
|
AutoTranslate.ReplaceWithPayload(ref decoded);
|
||||||
|
|
||||||
@@ -500,9 +652,28 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
if (reason == TellReason.Direct)
|
if (reason == TellReason.Direct)
|
||||||
reason = TellReason.Friend;
|
reason = TellReason.Friend;
|
||||||
|
|
||||||
var ok = SendTellNative(networkModule, contentId, homeWorld, uName, encoded, (ushort) reason, homeWorld);
|
var ok = SendTellNative(
|
||||||
|
networkModule,
|
||||||
|
contentId,
|
||||||
|
homeWorld,
|
||||||
|
uName,
|
||||||
|
encoded,
|
||||||
|
(ushort)reason,
|
||||||
|
homeWorld
|
||||||
|
);
|
||||||
if (ok == 1)
|
if (ok == 1)
|
||||||
PrintTellNative(logModule, 33, uName, &decodedUtf8String, 0, contentId, homeWorld, 255, 0, 0);
|
PrintTellNative(
|
||||||
|
logModule,
|
||||||
|
33,
|
||||||
|
uName,
|
||||||
|
&decodedUtf8String,
|
||||||
|
0,
|
||||||
|
contentId,
|
||||||
|
homeWorld,
|
||||||
|
255,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
else
|
else
|
||||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||||
|
|
||||||
@@ -511,7 +682,8 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
uMessage->Dtor(true);
|
uMessage->Dtor(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] EncodeMessage(string str) {
|
private static byte[] EncodeMessage(string str)
|
||||||
|
{
|
||||||
using var input = new Utf8String(str);
|
using var input = new Utf8String(str);
|
||||||
using var output = new Utf8String();
|
using var output = new Utf8String();
|
||||||
|
|
||||||
@@ -524,7 +696,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
var uC = Utf8String.FromString(c.ToString());
|
var uC = Utf8String.FromString(c.ToString());
|
||||||
|
|
||||||
uC->SanitizeString((AllowedEntities) 0x27F);
|
uC->SanitizeString((AllowedEntities)0x27F);
|
||||||
var wasValid = uC->ToString().Length > 0;
|
var wasValid = uC->ToString().Length > 0;
|
||||||
|
|
||||||
uC->Dtor(true);
|
uC->Dtor(true);
|
||||||
@@ -537,7 +709,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
var ok = Plugin.GameConfig.TryGet(UiConfigOption.LogNameType, out uint type);
|
var ok = Plugin.GameConfig.TryGet(UiConfigOption.LogNameType, out uint type);
|
||||||
if (!ok || !Enum.IsDefined(typeof(PlayerNameDisplayType), type))
|
if (!ok || !Enum.IsDefined(typeof(PlayerNameDisplayType), type))
|
||||||
return PlayerNameDisplayType.FullName;
|
return PlayerNameDisplayType.FullName;
|
||||||
return (PlayerNameDisplayType) type;
|
return (PlayerNameDisplayType)type;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal string AbbreviatePlayerName(string playerName)
|
internal string AbbreviatePlayerName(string playerName)
|
||||||
@@ -557,20 +729,21 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
|
|
||||||
return CurrentPlayerNameDisplayType switch
|
return CurrentPlayerNameDisplayType switch
|
||||||
{
|
{
|
||||||
PlayerNameDisplayType.SurnameAbbreviated => $"{split.First()} {split.Last().FirstOrDefault('A')}.",
|
PlayerNameDisplayType.SurnameAbbreviated =>
|
||||||
PlayerNameDisplayType.ForenameAbbreviated => $"{split.First().FirstOrDefault('A')}. {split.Last()}",
|
$"{split.First()} {split.Last().FirstOrDefault('A')}.",
|
||||||
PlayerNameDisplayType.Initials => $"{split.First().FirstOrDefault('A')}. {split.Last().FirstOrDefault('A')}.",
|
PlayerNameDisplayType.ForenameAbbreviated =>
|
||||||
_ => playerName
|
$"{split.First().FirstOrDefault('A')}. {split.Last()}",
|
||||||
|
PlayerNameDisplayType.Initials =>
|
||||||
|
$"{split.First().FirstOrDefault('A')}. {split.Last().FirstOrDefault('A')}.",
|
||||||
|
_ => playerName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool CheckHideFlags()
|
internal bool CheckHideFlags()
|
||||||
{
|
{
|
||||||
// Only hide the chat in a cutscene when the vanilla chat would've
|
// Only hide chat in cutscene when vanilla chat would also be hidden
|
||||||
// also been hidden. This prevents Chat 2 from hiding for a split
|
|
||||||
// second before the cutscene actually starts, because the game sets
|
|
||||||
// the cutscene conditions before processing the skip.
|
|
||||||
var raptureAtkUnitManager = RaptureAtkUnitManager.Instance();
|
var raptureAtkUnitManager = RaptureAtkUnitManager.Instance();
|
||||||
return raptureAtkUnitManager == null || raptureAtkUnitManager->UiFlags.HasFlag(UiFlags.Chat);
|
return raptureAtkUnitManager == null
|
||||||
|
|| raptureAtkUnitManager->UiFlags.HasFlag(UiFlags.Chat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using HellionChat.Resources;
|
|
||||||
using Dalamud.Memory;
|
using Dalamud.Memory;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
|
|
||||||
@@ -15,7 +15,15 @@ public unsafe class ChatBox
|
|||||||
mes->Dtor(true);
|
mes->Dtor(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SendMessage(string message)
|
public static void SendMessage(string message) => SendMessageUnsafe(ValidateMessage(message));
|
||||||
|
|
||||||
|
// sanitiserOverride allows xUnit to bypass Utf8String->SanitizeString (game memory only).
|
||||||
|
// Returns encoded bytes so SendMessage avoids a second GetBytes call.
|
||||||
|
// TEST-MIRROR: ../../../Hellion Build test/GameFunctions/ChatBoxTests.cs
|
||||||
|
internal static byte[] ValidateMessage(
|
||||||
|
string message,
|
||||||
|
Func<string, string>? sanitiserOverride = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var bytes = Encoding.UTF8.GetBytes(message);
|
var bytes = Encoding.UTF8.GetBytes(message);
|
||||||
if (bytes.Length == 0)
|
if (bytes.Length == 0)
|
||||||
@@ -24,20 +32,19 @@ public unsafe class ChatBox
|
|||||||
if (bytes.Length > 500)
|
if (bytes.Length > 500)
|
||||||
throw new ArgumentException(Language.ChatBox_Error_Too_Long, nameof(message));
|
throw new ArgumentException(Language.ChatBox_Error_Too_Long, nameof(message));
|
||||||
|
|
||||||
if (message.Length != SanitiseText(message).Length)
|
var sanitiser = sanitiserOverride ?? SanitiseText;
|
||||||
|
if (message.Length != sanitiser(message).Length)
|
||||||
throw new ArgumentException(Language.ChatBox_Error_Invalid, nameof(message));
|
throw new ArgumentException(Language.ChatBox_Error_Invalid, nameof(message));
|
||||||
|
|
||||||
SendMessageUnsafe(bytes);
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SanitiseText(string text)
|
private static string SanitiseText(string text)
|
||||||
{
|
{
|
||||||
var uText = Utf8String.FromString(text);
|
var uText = Utf8String.FromString(text);
|
||||||
|
uText->SanitizeString((AllowedEntities)0x27F);
|
||||||
uText->SanitizeString((AllowedEntities) 0x27F);
|
|
||||||
var sanitised = uText->ToString();
|
var sanitised = uText->ToString();
|
||||||
uText->Dtor(true);
|
uText->Dtor(true);
|
||||||
|
|
||||||
return sanitised;
|
return sanitised;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using HellionChat.Util;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
|
|
||||||
@@ -10,7 +10,9 @@ internal sealed unsafe class Context
|
|||||||
internal static void InviteToNoviceNetwork(string name, ushort world)
|
internal static void InviteToNoviceNetwork(string name, ushort world)
|
||||||
{
|
{
|
||||||
// can specify content id if we have it, but there's no need
|
// can specify content id if we have it, but there's no need
|
||||||
InfoProxyNoviceNetwork.Instance()->InviteToNoviceNetwork(0, 0, world, name.ToTerminatedBytes());
|
InfoProxyNoviceNetwork
|
||||||
|
.Instance()
|
||||||
|
->InviteToNoviceNetwork(0, 0, world, name.ToTerminatedBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void TryOn(uint itemId, byte stainId)
|
internal static void TryOn(uint itemId, byte stainId)
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -23,23 +24,36 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
internal const string NewGamePlusAddonName = "QuestRedo";
|
internal const string NewGamePlusAddonName = "QuestRedo";
|
||||||
|
|
||||||
#region Hooks
|
#region Hooks
|
||||||
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
|
[Signature(
|
||||||
|
"E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F",
|
||||||
|
DetourName = nameof(ResolveTextCommandPlaceholderDetour)
|
||||||
|
)]
|
||||||
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
|
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
|
||||||
private delegate nint ResolveTextCommandPlaceholderDelegate(nint a1, byte* placeholderText, byte a3, byte a4);
|
private delegate nint ResolveTextCommandPlaceholderDelegate(
|
||||||
|
nint a1,
|
||||||
|
byte* placeholderText,
|
||||||
|
byte a3,
|
||||||
|
byte a4
|
||||||
|
);
|
||||||
#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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,41 +61,30 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
{
|
{
|
||||||
Chat.Dispose();
|
Chat.Dispose();
|
||||||
KeybindManager.Dispose();
|
KeybindManager.Dispose();
|
||||||
|
|
||||||
ResolveTextCommandPlaceholderHook?.Dispose();
|
ResolveTextCommandPlaceholderHook?.Dispose();
|
||||||
|
|
||||||
Marshal.FreeHGlobal(PlaceholderNamePtr);
|
Marshal.FreeHGlobal(PlaceholderNamePtr);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SendFriendRequest(string name, ushort world)
|
internal void SendFriendRequest(string name, ushort world) =>
|
||||||
{
|
|
||||||
ListCommand(name, world, "friendlist");
|
ListCommand(name, world, "friendlist");
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddToBlacklist(string name, ushort world)
|
internal void AddToBlacklist(string name, ushort world) => ListCommand(name, world, "blist");
|
||||||
{
|
|
||||||
ListCommand(name, world, "blist");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddToMuteList(ulong accountId, ulong contentId, string name, short worldId)
|
internal void AddToMuteList(ulong accountId, ulong contentId, string name, short worldId) =>
|
||||||
{
|
|
||||||
AgentMutelist.Instance()->Add(accountId, contentId, name, worldId);
|
AgentMutelist.Instance()->Add(accountId, contentId, name, worldId);
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddToTermsList(SeString content)
|
internal void AddToTermsList(SeString content) =>
|
||||||
{
|
|
||||||
AgentTermFilter.Instance()->OpenNewFilterWindow(content.EncodeWithNullTerminator());
|
AgentTermFilter.Instance()->OpenNewFilterWindow(content.EncodeWithNullTerminator());
|
||||||
}
|
|
||||||
|
|
||||||
private void ListCommand(string name, ushort world, string commandName)
|
private void ListCommand(string name, ushort world, string commandName)
|
||||||
{
|
{
|
||||||
var worldRow = Sheets.WorldSheet.GetRow(world);
|
var worldRow = Sheets.WorldSheet.GetRow(world);
|
||||||
|
|
||||||
ReplacementName = $"{name}@{worldRow.Name.ToString()}";
|
ReplacementName = $"{name}@{worldRow.Name.ToString()}";
|
||||||
ChatBox.SendMessage($"/{commandName} add {Placeholder}");
|
ChatBox.SendMessage($"/{commandName} add {Placeholder}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T* GetAddon<T>(string name) where T : unmanaged
|
private static T* GetAddon<T>(string name)
|
||||||
|
where T : unmanaged
|
||||||
{
|
{
|
||||||
var addon = RaptureAtkModule.Instance()->RaptureAtkUnitManager.GetAddonByName(name);
|
var addon = RaptureAtkModule.Instance()->RaptureAtkUnitManager.GetAddonByName(name);
|
||||||
return addon != null && addon->IsReady ? (T*)addon : null;
|
return addon != null && addon->IsReady ? (T*)addon : null;
|
||||||
@@ -99,7 +102,6 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
{
|
{
|
||||||
for (var i = 0; i < 4; i++)
|
for (var i = 0; i < 4; i++)
|
||||||
SetAddonInteractable($"ChatLogPanel_{i}", interactable);
|
SetAddonInteractable($"ChatLogPanel_{i}", interactable);
|
||||||
|
|
||||||
SetAddonInteractable("ChatLog", interactable);
|
SetAddonInteractable("ChatLog", interactable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +117,6 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
var agent = AgentItemDetail.Instance();
|
var agent = AgentItemDetail.Instance();
|
||||||
var addon = GetAddon<AtkUnitBase>("ItemDetail");
|
var addon = GetAddon<AtkUnitBase>("ItemDetail");
|
||||||
|
|
||||||
// atkStage ain't gonna be null or we have bigger problems
|
|
||||||
if (agent == null || addon == null)
|
if (agent == null || addon == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -124,23 +125,19 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
agent->Index = 0;
|
agent->Index = 0;
|
||||||
agent->Flag1 &= 0xEF;
|
agent->Flag1 &= 0xEF;
|
||||||
agent->ItemId = id;
|
agent->ItemId = id;
|
||||||
// agent->Flag2 = 1;
|
|
||||||
// agent->Flag3 = 0;
|
// TODO: Revert when CS offset lands in a release build.
|
||||||
// TODO: Revert whenever CS is merged
|
|
||||||
*(byte*)((nint)agent + 0x21A) = 1;
|
*(byte*)((nint)agent + 0x21A) = 1;
|
||||||
*(byte*)((nint)agent + 0x21E) = 0;
|
*(byte*)((nint)agent + 0x21E) = 0;
|
||||||
|
|
||||||
// This just probably needs to be set
|
|
||||||
agent->AddonId = addon->Id;
|
agent->AddonId = addon->Id;
|
||||||
|
|
||||||
// Skips early return
|
|
||||||
atkStage->TooltipManager.TooltipType |= 2;
|
atkStage->TooltipManager.TooltipType |= 2;
|
||||||
addon->Show(false, 15);
|
addon->Show(false, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void CloseItemTooltip()
|
internal static void CloseItemTooltip()
|
||||||
{
|
{
|
||||||
// hide addon first to prevent the "addon close" sound
|
// Hide addon first to suppress the "addon close" sound.
|
||||||
var addon = GetAddon<AtkUnitBase>("ItemDetail");
|
var addon = GetAddon<AtkUnitBase>("ItemDetail");
|
||||||
if (addon != null)
|
if (addon != null)
|
||||||
addon->Hide(true, false, 0);
|
addon->Hide(true, false, 0);
|
||||||
@@ -158,35 +155,31 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
|
|
||||||
internal static void OpenPartyFinder()
|
internal static void OpenPartyFinder()
|
||||||
{
|
{
|
||||||
// this whole method: 6.05: 84433A (FF 97 ?? ?? ?? ?? 41 B4 01)
|
// 6.05: 84433A (FF 97 ?? ?? ?? ?? 41 B4 01)
|
||||||
var lfg = AgentLookingForGroup.Instance();
|
var lfg = AgentLookingForGroup.Instance();
|
||||||
if (lfg->IsAgentActive())
|
if (lfg->IsAgentActive())
|
||||||
{
|
{
|
||||||
var addonId = lfg->GetAddonId();
|
var addonId = lfg->GetAddonId();
|
||||||
var atkModule = RaptureAtkModule.Instance();
|
var atkModule = RaptureAtkModule.Instance();
|
||||||
var atkModuleVtbl = (void**) atkModule->AtkModule.VirtualTable;
|
var atkModuleVtbl = (void**)atkModule->AtkModule.VirtualTable;
|
||||||
var vf27 = (delegate* unmanaged<RaptureAtkModule*, ulong, ulong, byte>) atkModuleVtbl[27];
|
var vf27 = (delegate* unmanaged<RaptureAtkModule*, ulong, ulong, byte>)
|
||||||
|
atkModuleVtbl[27];
|
||||||
vf27(atkModule, addonId, 1);
|
vf27(atkModule, addonId, 1);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 6.05: 8443DD
|
// 6.05: 8443DD
|
||||||
if (*(uint*) ((nint) lfg + 0x2C20) > 0)
|
if (*(uint*)((nint)lfg + 0x2C20) > 0)
|
||||||
lfg->Hide();
|
lfg->Hide();
|
||||||
else
|
else
|
||||||
lfg->Show();
|
lfg->Show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsMentor()
|
internal static bool IsMentor() => PlayerState.Instance()->IsMentor();
|
||||||
{
|
|
||||||
return PlayerState.Instance()->IsMentor();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static InfoProxyCommonList.CharacterData[] GetFriends()
|
internal static InfoProxyCommonList.CharacterData[] GetFriends() =>
|
||||||
{
|
InfoProxyFriendList.Instance()->CharDataSpan.ToArray();
|
||||||
return InfoProxyFriendList.Instance()->CharDataSpan.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void OpenQuestLog(RowRef<Quest> quest)
|
internal static void OpenQuestLog(RowRef<Quest> quest)
|
||||||
{
|
{
|
||||||
@@ -197,7 +190,14 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uint.TryParse(splits[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var questId))
|
if (
|
||||||
|
!uint.TryParse(
|
||||||
|
splits[1],
|
||||||
|
NumberStyles.Any,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
out var questId
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin.ChatGui.Print("Unable to parse quest id");
|
Plugin.ChatGui.Print("Unable to parse quest id");
|
||||||
return;
|
return;
|
||||||
@@ -206,20 +206,12 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
AgentQuestJournal.Instance()->OpenForQuest(questId, 1);
|
AgentQuestJournal.Instance()->OpenForQuest(questId, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void OpenPartyFinder(uint id)
|
internal static void OpenPartyFinder(uint id) =>
|
||||||
{
|
|
||||||
AgentLookingForGroup.Instance()->OpenListing(id);
|
AgentLookingForGroup.Instance()->OpenListing(id);
|
||||||
}
|
|
||||||
|
|
||||||
internal static void OpenAchievement(uint id)
|
internal static void OpenAchievement(uint id) => AgentAchievement.Instance()->OpenById(id);
|
||||||
{
|
|
||||||
AgentAchievement.Instance()->OpenById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsInInstance()
|
internal static bool IsInInstance() => Plugin.Condition[ConditionFlag.BoundByDuty56];
|
||||||
{
|
|
||||||
return Plugin.Condition[ConditionFlag.BoundByDuty56];
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryOpenAdventurerPlate(ulong playerId)
|
internal static bool TryOpenAdventurerPlate(ulong playerId)
|
||||||
{
|
{
|
||||||
@@ -230,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,10 +231,10 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
internal static void ClickNoviceNetworkButton()
|
internal static void ClickNoviceNetworkButton()
|
||||||
{
|
{
|
||||||
var agent = AgentChatLog.Instance();
|
var agent = AgentChatLog.Instance();
|
||||||
// case 3
|
var value = new AtkValue { Type = ValueType.Int, Int = 3 }; // case 3
|
||||||
var value = new AtkValue { Type = ValueType.Int, Int = 3, };
|
|
||||||
var result = 0;
|
var result = 0;
|
||||||
var vf0 = *(delegate* unmanaged<AgentChatLog*, int*, AtkValue*, ulong, ulong, int*>*) agent->VirtualTable;
|
var vf0 = *(delegate* unmanaged<AgentChatLog*, int*, AtkValue*, ulong, ulong, int*>*)
|
||||||
|
agent->VirtualTable;
|
||||||
vf0(agent, &result, &value, 0, 0);
|
vf0(agent, &result, &value, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,32 +243,35 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
|
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
|
||||||
private string? ReplacementName;
|
private string? ReplacementName;
|
||||||
|
|
||||||
private nint ResolveTextCommandPlaceholderDetour(nint a1, byte* placeholderText, byte a3, byte a4)
|
private nint ResolveTextCommandPlaceholderDetour(
|
||||||
|
nint a1,
|
||||||
|
byte* placeholderText,
|
||||||
|
byte a3,
|
||||||
|
byte a4
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// The detour is only invoked through the hook, so the hook should
|
// Hook field is nullable due to the Signature attribute, but will never
|
||||||
// never be null here, but the nullable field declaration forces us
|
// be null during normal execution; guard covers the teardown race only.
|
||||||
// to handle the theoretical race during teardown.
|
|
||||||
if (ResolveTextCommandPlaceholderHook is null)
|
if (ResolveTextCommandPlaceholderHook is null)
|
||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
|
|
||||||
var placeholder = MemoryHelper.ReadStringNullTerminated((nint) placeholderText);
|
var placeholder = MemoryHelper.ReadStringNullTerminated((nint)placeholderText);
|
||||||
if (ReplacementName == null || placeholder != Placeholder)
|
if (ReplacementName == null || placeholder != Placeholder)
|
||||||
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
||||||
|
|
||||||
// The fixed buffer is 128 bytes; UTF-8 + null-terminator must fit.
|
// Guard against a malformed ReplacementName overflowing the 128-byte buffer.
|
||||||
// FFXIV player names plus an @World suffix should never approach this
|
|
||||||
// limit, but a malformed ReplacementName must not overflow the buffer.
|
|
||||||
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($"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original.");
|
_logger.LogWarning(
|
||||||
|
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
||||||
|
);
|
||||||
ReplacementName = null;
|
ReplacementName = null;
|
||||||
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
||||||
}
|
}
|
||||||
|
|
||||||
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
|
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
|
||||||
ReplacementName = null;
|
ReplacementName = null;
|
||||||
|
|
||||||
return PlaceholderNamePtr;
|
return PlaceholderNamePtr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using HellionChat.Code;
|
using Dalamud.Bindings.ImGui;
|
||||||
using HellionChat.GameFunctions.Types;
|
|
||||||
using HellionChat.Util;
|
|
||||||
using Dalamud.Game.ClientState.Keys;
|
using Dalamud.Game.ClientState.Keys;
|
||||||
using Dalamud.Game.Config;
|
using Dalamud.Game.Config;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using Dalamud.Bindings.ImGui;
|
using HellionChat.Code;
|
||||||
|
using HellionChat.GameFunctions.Types;
|
||||||
|
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;
|
||||||
|
|
||||||
internal enum KeyboardSource {
|
internal enum KeyboardSource
|
||||||
|
{
|
||||||
Game,
|
Game,
|
||||||
ImGui
|
ImGui,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal unsafe class KeybindManager : IDisposable {
|
internal unsafe class KeybindManager : IDisposable
|
||||||
|
{
|
||||||
private Plugin Plugin { get; }
|
private Plugin Plugin { get; }
|
||||||
|
|
||||||
internal bool DirectChat;
|
internal bool DirectChat;
|
||||||
@@ -26,70 +29,79 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
private bool VanillaTextInputHasFocus;
|
private bool VanillaTextInputHasFocus;
|
||||||
|
|
||||||
private readonly Dictionary<string, Keybind> Keybinds = new();
|
private readonly Dictionary<string, Keybind> Keybinds = new();
|
||||||
private static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept = new Dictionary<string, ChannelSwitchInfo>
|
private static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept =
|
||||||
{
|
new Dictionary<string, ChannelSwitchInfo>
|
||||||
["CMD_CHAT"] = new(null),
|
{
|
||||||
["CMD_COMMAND"] = new(null, text: "/"),
|
["CMD_CHAT"] = new(null),
|
||||||
["CMD_REPLY"] = new(InputChannel.Tell, rotate: RotateMode.Forward),
|
["CMD_COMMAND"] = new(null, text: "/"),
|
||||||
["CMD_REPLY_REV"] = new(InputChannel.Tell, rotate: RotateMode.Reverse),
|
["CMD_REPLY"] = new(InputChannel.Tell, rotate: RotateMode.Forward),
|
||||||
["CMD_SAY"] = new(InputChannel.Say),
|
["CMD_REPLY_REV"] = new(InputChannel.Tell, rotate: RotateMode.Reverse),
|
||||||
["CMD_YELL"] = new(InputChannel.Yell),
|
["CMD_SAY"] = new(InputChannel.Say),
|
||||||
["CMD_SHOUT"] = new(InputChannel.Shout),
|
["CMD_YELL"] = new(InputChannel.Yell),
|
||||||
["CMD_PARTY"] = new(InputChannel.Party),
|
["CMD_SHOUT"] = new(InputChannel.Shout),
|
||||||
["CMD_ALLIANCE"] = new(InputChannel.Alliance),
|
["CMD_PARTY"] = new(InputChannel.Party),
|
||||||
["CMD_FREECOM"] = new(InputChannel.FreeCompany),
|
["CMD_ALLIANCE"] = new(InputChannel.Alliance),
|
||||||
["PVPTEAM_CHAT"] = new(InputChannel.PvpTeam),
|
["CMD_FREECOM"] = new(InputChannel.FreeCompany),
|
||||||
["CMD_CWLINKSHELL"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Forward),
|
["PVPTEAM_CHAT"] = new(InputChannel.PvpTeam),
|
||||||
["CMD_CWLINKSHELL_REV"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Reverse),
|
["CMD_CWLINKSHELL"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Forward),
|
||||||
["CMD_CWLINKSHELL_1"] = new(InputChannel.CrossLinkshell1),
|
["CMD_CWLINKSHELL_REV"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Reverse),
|
||||||
["CMD_CWLINKSHELL_2"] = new(InputChannel.CrossLinkshell2),
|
["CMD_CWLINKSHELL_1"] = new(InputChannel.CrossLinkshell1),
|
||||||
["CMD_CWLINKSHELL_3"] = new(InputChannel.CrossLinkshell3),
|
["CMD_CWLINKSHELL_2"] = new(InputChannel.CrossLinkshell2),
|
||||||
["CMD_CWLINKSHELL_4"] = new(InputChannel.CrossLinkshell4),
|
["CMD_CWLINKSHELL_3"] = new(InputChannel.CrossLinkshell3),
|
||||||
["CMD_CWLINKSHELL_5"] = new(InputChannel.CrossLinkshell5),
|
["CMD_CWLINKSHELL_4"] = new(InputChannel.CrossLinkshell4),
|
||||||
["CMD_CWLINKSHELL_6"] = new(InputChannel.CrossLinkshell6),
|
["CMD_CWLINKSHELL_5"] = new(InputChannel.CrossLinkshell5),
|
||||||
["CMD_CWLINKSHELL_7"] = new(InputChannel.CrossLinkshell7),
|
["CMD_CWLINKSHELL_6"] = new(InputChannel.CrossLinkshell6),
|
||||||
["CMD_CWLINKSHELL_8"] = new(InputChannel.CrossLinkshell8),
|
["CMD_CWLINKSHELL_7"] = new(InputChannel.CrossLinkshell7),
|
||||||
["CMD_LINKSHELL"] = new(InputChannel.Linkshell1, rotate: RotateMode.Forward),
|
["CMD_CWLINKSHELL_8"] = new(InputChannel.CrossLinkshell8),
|
||||||
["CMD_LINKSHELL_REV"] = new(InputChannel.Linkshell1, rotate: RotateMode.Reverse),
|
["CMD_LINKSHELL"] = new(InputChannel.Linkshell1, rotate: RotateMode.Forward),
|
||||||
["CMD_LINKSHELL_1"] = new(InputChannel.Linkshell1),
|
["CMD_LINKSHELL_REV"] = new(InputChannel.Linkshell1, rotate: RotateMode.Reverse),
|
||||||
["CMD_LINKSHELL_2"] = new(InputChannel.Linkshell2),
|
["CMD_LINKSHELL_1"] = new(InputChannel.Linkshell1),
|
||||||
["CMD_LINKSHELL_3"] = new(InputChannel.Linkshell3),
|
["CMD_LINKSHELL_2"] = new(InputChannel.Linkshell2),
|
||||||
["CMD_LINKSHELL_4"] = new(InputChannel.Linkshell4),
|
["CMD_LINKSHELL_3"] = new(InputChannel.Linkshell3),
|
||||||
["CMD_LINKSHELL_5"] = new(InputChannel.Linkshell5),
|
["CMD_LINKSHELL_4"] = new(InputChannel.Linkshell4),
|
||||||
["CMD_LINKSHELL_6"] = new(InputChannel.Linkshell6),
|
["CMD_LINKSHELL_5"] = new(InputChannel.Linkshell5),
|
||||||
["CMD_LINKSHELL_7"] = new(InputChannel.Linkshell7),
|
["CMD_LINKSHELL_6"] = new(InputChannel.Linkshell6),
|
||||||
["CMD_LINKSHELL_8"] = new(InputChannel.Linkshell8),
|
["CMD_LINKSHELL_7"] = new(InputChannel.Linkshell7),
|
||||||
["CMD_BEGINNER"] = new(InputChannel.NoviceNetwork),
|
["CMD_LINKSHELL_8"] = new(InputChannel.Linkshell8),
|
||||||
["CMD_REPLY_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Forward),
|
["CMD_BEGINNER"] = new(InputChannel.NoviceNetwork),
|
||||||
["CMD_REPLY_REV_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Reverse),
|
["CMD_REPLY_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Forward),
|
||||||
["CMD_SAY_ALWAYS"] = new(InputChannel.Say, true),
|
["CMD_REPLY_REV_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Reverse),
|
||||||
["CMD_YELL_ALWAYS"] = new(InputChannel.Yell, true),
|
["CMD_SAY_ALWAYS"] = new(InputChannel.Say, true),
|
||||||
["CMD_PARTY_ALWAYS"] = new(InputChannel.Party, true),
|
["CMD_YELL_ALWAYS"] = new(InputChannel.Yell, true),
|
||||||
["CMD_ALLIANCE_ALWAYS"] = new(InputChannel.Alliance, true),
|
["CMD_PARTY_ALWAYS"] = new(InputChannel.Party, true),
|
||||||
["CMD_FREECOM_ALWAYS"] = new(InputChannel.FreeCompany, true),
|
["CMD_ALLIANCE_ALWAYS"] = new(InputChannel.Alliance, true),
|
||||||
["PVPTEAM_CHAT_ALWAYS"] = new(InputChannel.PvpTeam, true),
|
["CMD_FREECOM_ALWAYS"] = new(InputChannel.FreeCompany, true),
|
||||||
["CMD_CWLINKSHELL_ALWAYS"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Forward),
|
["PVPTEAM_CHAT_ALWAYS"] = new(InputChannel.PvpTeam, true),
|
||||||
["CMD_CWLINKSHELL_ALWAYS_REV"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Reverse),
|
["CMD_CWLINKSHELL_ALWAYS"] = new(
|
||||||
["CMD_CWLINKSHELL_1_ALWAYS"] = new(InputChannel.CrossLinkshell1, true),
|
InputChannel.CrossLinkshell1,
|
||||||
["CMD_CWLINKSHELL_2_ALWAYS"] = new(InputChannel.CrossLinkshell2, true),
|
true,
|
||||||
["CMD_CWLINKSHELL_3_ALWAYS"] = new(InputChannel.CrossLinkshell3, true),
|
RotateMode.Forward
|
||||||
["CMD_CWLINKSHELL_4_ALWAYS"] = new(InputChannel.CrossLinkshell4, true),
|
),
|
||||||
["CMD_CWLINKSHELL_5_ALWAYS"] = new(InputChannel.CrossLinkshell5, true),
|
["CMD_CWLINKSHELL_ALWAYS_REV"] = new(
|
||||||
["CMD_CWLINKSHELL_6_ALWAYS"] = new(InputChannel.CrossLinkshell6, true),
|
InputChannel.CrossLinkshell1,
|
||||||
["CMD_CWLINKSHELL_7_ALWAYS"] = new(InputChannel.CrossLinkshell7, true),
|
true,
|
||||||
["CMD_CWLINKSHELL_8_ALWAYS"] = new(InputChannel.CrossLinkshell8, true),
|
RotateMode.Reverse
|
||||||
["CMD_LINKSHELL_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Forward),
|
),
|
||||||
["CMD_LINKSHELL_REV_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Reverse),
|
["CMD_CWLINKSHELL_1_ALWAYS"] = new(InputChannel.CrossLinkshell1, true),
|
||||||
["CMD_LINKSHELL_1_ALWAYS"] = new(InputChannel.Linkshell1, true),
|
["CMD_CWLINKSHELL_2_ALWAYS"] = new(InputChannel.CrossLinkshell2, true),
|
||||||
["CMD_LINKSHELL_2_ALWAYS"] = new(InputChannel.Linkshell2, true),
|
["CMD_CWLINKSHELL_3_ALWAYS"] = new(InputChannel.CrossLinkshell3, true),
|
||||||
["CMD_LINKSHELL_3_ALWAYS"] = new(InputChannel.Linkshell3, true),
|
["CMD_CWLINKSHELL_4_ALWAYS"] = new(InputChannel.CrossLinkshell4, true),
|
||||||
["CMD_LINKSHELL_4_ALWAYS"] = new(InputChannel.Linkshell4, true),
|
["CMD_CWLINKSHELL_5_ALWAYS"] = new(InputChannel.CrossLinkshell5, true),
|
||||||
["CMD_LINKSHELL_5_ALWAYS"] = new(InputChannel.Linkshell5, true),
|
["CMD_CWLINKSHELL_6_ALWAYS"] = new(InputChannel.CrossLinkshell6, true),
|
||||||
["CMD_LINKSHELL_6_ALWAYS"] = new(InputChannel.Linkshell6, true),
|
["CMD_CWLINKSHELL_7_ALWAYS"] = new(InputChannel.CrossLinkshell7, true),
|
||||||
["CMD_LINKSHELL_7_ALWAYS"] = new(InputChannel.Linkshell7, true),
|
["CMD_CWLINKSHELL_8_ALWAYS"] = new(InputChannel.CrossLinkshell8, true),
|
||||||
["CMD_LINKSHELL_8_ALWAYS"] = new(InputChannel.Linkshell8, true),
|
["CMD_LINKSHELL_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Forward),
|
||||||
["CMD_BEGINNER_ALWAYS"] = new(InputChannel.NoviceNetwork, true)
|
["CMD_LINKSHELL_REV_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Reverse),
|
||||||
};
|
["CMD_LINKSHELL_1_ALWAYS"] = new(InputChannel.Linkshell1, true),
|
||||||
|
["CMD_LINKSHELL_2_ALWAYS"] = new(InputChannel.Linkshell2, true),
|
||||||
|
["CMD_LINKSHELL_3_ALWAYS"] = new(InputChannel.Linkshell3, true),
|
||||||
|
["CMD_LINKSHELL_4_ALWAYS"] = new(InputChannel.Linkshell4, true),
|
||||||
|
["CMD_LINKSHELL_5_ALWAYS"] = new(InputChannel.Linkshell5, true),
|
||||||
|
["CMD_LINKSHELL_6_ALWAYS"] = new(InputChannel.Linkshell6, true),
|
||||||
|
["CMD_LINKSHELL_7_ALWAYS"] = new(InputChannel.Linkshell7, true),
|
||||||
|
["CMD_LINKSHELL_8_ALWAYS"] = new(InputChannel.Linkshell8, true),
|
||||||
|
["CMD_BEGINNER_ALWAYS"] = new(InputChannel.NoviceNetwork, true),
|
||||||
|
};
|
||||||
|
|
||||||
// List of keys that can be used as a part of keybinds while the chat is
|
// List of keys that can be used as a part of keybinds while the chat is
|
||||||
// focused WITHOUT modifiers. All other keys can only be used if their
|
// focused WITHOUT modifiers. All other keys can only be used if their
|
||||||
@@ -295,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.
|
||||||
@@ -353,12 +368,22 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
return key.TryToImGui(out var imguiKey) && ImGui.IsKeyPressed(imguiKey);
|
return key.TryToImGui(out var imguiKey) && ImGui.IsKeyPressed(imguiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ComboPressed(KeyboardSource source, VirtualKey key, ModifierFlag modifier, ModifierFlag? modifierState = null, bool modifiersOnly = false)
|
private static bool ComboPressed(
|
||||||
|
KeyboardSource source,
|
||||||
|
VirtualKey key,
|
||||||
|
ModifierFlag modifier,
|
||||||
|
ModifierFlag? modifierState = null,
|
||||||
|
bool modifiersOnly = false
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// When we're in an input, we don't want to process any keybinds that
|
// When we're in an input, we don't want to process any keybinds that
|
||||||
// don't have a modifier (or only use shift) and are not explicitly
|
// don't have a modifier (or only use shift) and are not explicitly
|
||||||
// whitelisted.
|
// whitelisted.
|
||||||
if (modifiersOnly && !ModifierlessChatKeys.Contains(key) && modifier is ModifierFlag.None or ModifierFlag.Shift)
|
if (
|
||||||
|
modifiersOnly
|
||||||
|
&& !ModifierlessChatKeys.Contains(key)
|
||||||
|
&& modifier is ModifierFlag.None or ModifierFlag.Shift
|
||||||
|
)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
modifierState ??= GetModifiers(source);
|
modifierState ??= GetModifiers(source);
|
||||||
@@ -366,26 +391,43 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
{
|
{
|
||||||
KeybindMode.Strict => modifier == modifierState.Value,
|
KeybindMode.Strict => modifier == modifierState.Value,
|
||||||
KeybindMode.Flexible => modifierState.Value.HasFlag(modifier),
|
KeybindMode.Flexible => modifierState.Value.HasFlag(modifier),
|
||||||
_ => false
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
return KeyPressed(source, key) && modifierPressed;
|
return KeyPressed(source, key) && modifierPressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ConfigKeybindPressed(KeyboardSource source, ConfigKeyBind? bind, ModifierFlag? modifierState = null, bool modifiersOnly = false)
|
private static bool ConfigKeybindPressed(
|
||||||
|
KeyboardSource source,
|
||||||
|
ConfigKeyBind? bind,
|
||||||
|
ModifierFlag? modifierState = null,
|
||||||
|
bool modifiersOnly = false
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return bind != null && ComboPressed(source, bind.Key, bind.Modifier, modifierState: modifierState, modifiersOnly: modifiersOnly);
|
return bind != null
|
||||||
|
&& ComboPressed(
|
||||||
|
source,
|
||||||
|
bind.Key,
|
||||||
|
bind.Modifier,
|
||||||
|
modifierState: modifierState,
|
||||||
|
modifiersOnly: modifiersOnly
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleKeybinds(IFramework _ ) => HandleKeybinds(KeyboardSource.Game);
|
private void HandleKeybinds(IFramework _) => HandleKeybinds(KeyboardSource.Game);
|
||||||
|
|
||||||
internal void HandleKeybinds(KeyboardSource source, bool ignoreChatOpen = false, bool modifiersOnly = false)
|
internal void HandleKeybinds(
|
||||||
|
KeyboardSource source,
|
||||||
|
bool ignoreChatOpen = false,
|
||||||
|
bool modifiersOnly = false
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Refresh current keybinds every 5s
|
// Refresh current keybinds every 5s
|
||||||
if (LastRefresh + 5 * 1000 < Environment.TickCount64)
|
if (LastRefresh + 5 * 1000 < Environment.TickCount64)
|
||||||
{
|
{
|
||||||
UpdateKeybinds();
|
UpdateKeybinds();
|
||||||
DirectChat = Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option;
|
DirectChat =
|
||||||
|
Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option;
|
||||||
LastRefresh = Environment.TickCount64;
|
LastRefresh = Environment.TickCount64;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,10 +475,18 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
|
|
||||||
void Intercept(VirtualKey vk, ModifierFlag modifier)
|
void Intercept(VirtualKey vk, ModifierFlag modifier)
|
||||||
{
|
{
|
||||||
if (!ComboPressed(source, vk, modifier, modifierState: modifierState, modifiersOnly: modifiersOnly))
|
if (
|
||||||
|
!ComboPressed(
|
||||||
|
source,
|
||||||
|
vk,
|
||||||
|
modifier,
|
||||||
|
modifierState: modifierState,
|
||||||
|
modifiersOnly: modifiersOnly
|
||||||
|
)
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var bits = BitOperations.PopCount((uint) modifier);
|
var bits = BitOperations.PopCount((uint)modifier);
|
||||||
if (bits < currentBest.Item3)
|
if (bits < currentBest.Item3)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -457,11 +507,11 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null;
|
TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null;
|
||||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(info) { TellReason = reason, });
|
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(info) { TellReason = reason });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
_logger.LogError(ex, "Error in chat Activated event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,11 +544,11 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
var key2 = outData.KeySettings[1];
|
var key2 = outData.KeySettings[1];
|
||||||
return new Keybind
|
return new Keybind
|
||||||
{
|
{
|
||||||
Key1 = RemapInvalidVirtualKey((VirtualKey) key1.Key),
|
Key1 = RemapInvalidVirtualKey((VirtualKey)key1.Key),
|
||||||
Modifier1 = (ModifierFlag) key1.KeyModifier,
|
Modifier1 = (ModifierFlag)key1.KeyModifier,
|
||||||
|
|
||||||
Key2 = RemapInvalidVirtualKey((VirtualKey) key2.Key),
|
Key2 = RemapInvalidVirtualKey((VirtualKey)key2.Key),
|
||||||
Modifier2 = (ModifierFlag) key2.KeyModifier,
|
Modifier2 = (ModifierFlag)key2.KeyModifier,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,9 +556,9 @@ internal unsafe class KeybindManager : IDisposable {
|
|||||||
{
|
{
|
||||||
return key switch
|
return key switch
|
||||||
{
|
{
|
||||||
VirtualKey.F23 => VirtualKey.OEM_2, // /?
|
VirtualKey.F23 => VirtualKey.OEM_2, // /?
|
||||||
(VirtualKey) 140 => VirtualKey.OEM_7, // '"
|
(VirtualKey)140 => VirtualKey.OEM_7, // '"
|
||||||
_ => key
|
_ => key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
using HellionChat.Resources;
|
|
||||||
using HellionChat.Util;
|
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat.GameFunctions;
|
namespace HellionChat.GameFunctions;
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ internal static unsafe class Party
|
|||||||
internal static void InviteSameWorld(string name, ushort world, ulong contentId)
|
internal static void InviteSameWorld(string name, ushort world, ulong contentId)
|
||||||
{
|
{
|
||||||
// this only works if target is on the same world
|
// this only works if target is on the same world
|
||||||
fixed (byte* namePtr = name.ToTerminatedBytes()) {
|
fixed (byte* namePtr = name.ToTerminatedBytes())
|
||||||
|
{
|
||||||
InfoProxyPartyInvite.Instance()->InviteToParty(contentId, namePtr, world);
|
InfoProxyPartyInvite.Instance()->InviteToParty(contentId, namePtr, world);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,14 +45,16 @@ internal static unsafe class Party
|
|||||||
|
|
||||||
internal static void Kick(string name, ulong contentId)
|
internal static void Kick(string name, ulong contentId)
|
||||||
{
|
{
|
||||||
fixed (byte* namePtr = name.ToTerminatedBytes()) {
|
fixed (byte* namePtr = name.ToTerminatedBytes())
|
||||||
|
{
|
||||||
AgentPartyMember.Instance()->Kick(namePtr, 0, contentId);
|
AgentPartyMember.Instance()->Kick(namePtr, 0, contentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Promote(string name, ulong contentId)
|
internal static void Promote(string name, ulong contentId)
|
||||||
{
|
{
|
||||||
fixed (byte* namePtr = name.ToTerminatedBytes()) {
|
fixed (byte* namePtr = name.ToTerminatedBytes())
|
||||||
|
{
|
||||||
AgentPartyMember.Instance()->Promote(namePtr, 0, contentId);
|
AgentPartyMember.Instance()->Promote(namePtr, 0, contentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ using HellionChat.Code;
|
|||||||
|
|
||||||
namespace HellionChat.GameFunctions.Types;
|
namespace HellionChat.GameFunctions.Types;
|
||||||
|
|
||||||
internal class ChannelSwitchInfo {
|
internal class ChannelSwitchInfo
|
||||||
|
{
|
||||||
internal InputChannel? Channel { get; }
|
internal InputChannel? Channel { get; }
|
||||||
internal bool Permanent { get; }
|
internal bool Permanent { get; }
|
||||||
internal RotateMode Rotate { get; }
|
internal RotateMode Rotate { get; }
|
||||||
internal string? Text { get; }
|
internal string? Text { get; }
|
||||||
|
|
||||||
internal ChannelSwitchInfo(InputChannel? channel, bool permanent = false, RotateMode rotate = RotateMode.None, string? text = null)
|
internal ChannelSwitchInfo(
|
||||||
|
InputChannel? channel,
|
||||||
|
bool permanent = false,
|
||||||
|
RotateMode rotate = RotateMode.None,
|
||||||
|
string? text = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Channel = channel;
|
Channel = channel;
|
||||||
Permanent = permanent;
|
Permanent = permanent;
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ public class TellTarget
|
|||||||
Reason = reason;
|
Reason = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSet()
|
public bool IsSet() => !string.IsNullOrEmpty(Name) && World > 0;
|
||||||
=> !string.IsNullOrEmpty(Name) && World > 0;
|
|
||||||
|
|
||||||
public string ToWorldString()
|
public string ToWorldString() =>
|
||||||
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
|
Sheets.WorldSheet.TryGetRow(World, out var worldRow)
|
||||||
|
? worldRow.Name.ToString()
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
public string ToTargetString()
|
public string ToTargetString() => $"{Name}@{ToWorldString()}";
|
||||||
=> $"{Name}@{ToWorldString()}";
|
|
||||||
|
|
||||||
public unsafe void FromTarget(IPlayerCharacter target)
|
public unsafe void FromTarget(IPlayerCharacter target)
|
||||||
{
|
{
|
||||||
@@ -39,5 +39,12 @@ 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,36 +1,29 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Hellion Chat versioning runs separately from upstream Chat 2.
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
<Version>1.5.2</Version>
|
||||||
called out in the yaml changelog so users can see what it
|
|
||||||
derives from. -->
|
|
||||||
<Version>1.2.0</Version>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Honor packages.lock.json on restore so floating version ranges
|
<!-- Use lock file to pin exact versions -->
|
||||||
don't silently drift between machines or CI runs. -->
|
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
<!-- v1.0.0 standalone cut — both AssemblyName and RootNamespace
|
<!-- v1.0.0+: standalone fork, no upstream cherry-pick compatibility -->
|
||||||
are HellionChat. The plugin no longer maintains source-level
|
|
||||||
cherry-pick compatibility with upstream Infiziert90/ChatTwo;
|
|
||||||
upstream changes are integrated manually if at all. -->
|
|
||||||
<AssemblyName>HellionChat</AssemblyName>
|
<AssemblyName>HellionChat</AssemblyName>
|
||||||
<RootNamespace>HellionChat</RootNamespace>
|
<RootNamespace>HellionChat</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Closed ranges on packages with breaking-change history block a
|
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
||||||
surprise major bump when the lock file is regenerated. The
|
|
||||||
lock file pins the exact version per build; the upper bound
|
|
||||||
keeps the unlock path from drifting across major lines. -->
|
|
||||||
<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" />
|
||||||
<!-- Override the transitively-referenced native SQLite build to one
|
<!-- v1.5.0 DI-container foundation; matches Lightless pin (Hosting 10.0.7) -->
|
||||||
that ships SQLite >= 3.50.3 (CVE-2025-6965 memory corruption,
|
<PackageReference
|
||||||
CVE-2025-7709 fixed in 3.50.x). Microsoft.Data.Sqlite 10.0.7
|
Include="Microsoft.Extensions.DependencyInjection"
|
||||||
pulls SQLitePCLRaw 2.1.11 which carries the older lib; pinning
|
Version="[10.0.7, 11.0.0)"
|
||||||
the lib package directly forces the newer native binary
|
/>
|
||||||
without a major bump on the managed wrapper. -->
|
<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) -->
|
||||||
<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" />
|
||||||
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
|
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
|
||||||
@@ -38,9 +31,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Pure-function test suites in HellionChat.Tests need access to
|
<!-- Test assembly needs access to internal helpers (not redistributed) -->
|
||||||
the internal helper classes (StringUtil, UriPayload, Tokenizer
|
|
||||||
etc.). Test assembly does not get redistributed. -->
|
|
||||||
<InternalsVisibleTo Include="HellionChat.Tests" />
|
<InternalsVisibleTo Include="HellionChat.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -59,15 +50,7 @@
|
|||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- HellionChat — Hellion-specific resource bundle (HellionStrings.resx
|
<!-- Embedded resources: Hellion font (Exo 2, OFL-1.1) + manifest resource -->
|
||||||
+ HellionStrings.<lang>.resx) is picked up automatically by the SDK
|
|
||||||
default include. Designer.cs is hand-maintained, no auto-gen needed. -->
|
|
||||||
|
|
||||||
<!-- Bundled Hellion font (Exo 2, OFL-1.1). Embedded as a manifest
|
|
||||||
resource with a fixed LogicalName so FontManager can pull the
|
|
||||||
bytes back at runtime via AddFontFromMemory. The OFL license
|
|
||||||
text travels with it inside the assembly to satisfy the
|
|
||||||
"license must be distributed with the font" clause. -->
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Resources\HellionFont.ttf">
|
<EmbeddedResource Include="Resources\HellionFont.ttf">
|
||||||
<LogicalName>HellionFont.ttf</LogicalName>
|
<LogicalName>HellionFont.ttf</LogicalName>
|
||||||
@@ -75,24 +58,22 @@
|
|||||||
<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. ASCII
|
||||||
<!-- Plugin icon. Copy images/* into the build output so Dalamud
|
study folder is source-only material, no need to ship it. -->
|
||||||
finds the icon next to the DLL, and let the SDK default
|
|
||||||
DalamudPackager pipeline include the same path in the
|
|
||||||
release ZIP. Earlier we shipped a custom DalamudPackager
|
|
||||||
targets override that explicitly set HandleImages and
|
|
||||||
ImagesPath; that override conflicted with the SDK 15
|
|
||||||
default and the resulting manifest carried no IconUrl.
|
|
||||||
Removed in v0.5.2. -->
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="images\**">
|
<None Include="images\**" Exclude="images\ascii\**">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+188
-317
@@ -1,359 +1,230 @@
|
|||||||
name: Hellion Chat
|
name: Hellion Chat
|
||||||
author: JonKazama-Hellion
|
author: Jon Kazama (Hellion Forge)
|
||||||
punchline: Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)
|
punchline: A Hellion Forge plugin — privacy-focused chat replacement for FFXIV, built for EU, US and JP data rules.
|
||||||
description: |-
|
description: |-
|
||||||
Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV
|
Chat replacement for FINAL FANTASY XIV with privacy controls built around
|
||||||
based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally
|
EU, US and JP data-protection rules.
|
||||||
removed (the optional webinterface) and a stack of privacy controls is
|
|
||||||
added on top. Tabs, channel filters, RGB colours, emotes, screenshot
|
|
||||||
mode, IPC integration and the chat replacement window itself work the
|
|
||||||
same. The webinterface is intentionally not part of Hellion Chat because
|
|
||||||
it serves a different use case from the smaller default footprint this
|
|
||||||
plugin is built around.
|
|
||||||
|
|
||||||
On top of that, Hellion Chat adds privacy and data-handling controls
|
By default only your own conversations are stored. Public chat, NPC
|
||||||
designed to align with the modern data protection rules that apply
|
dialogue and system messages stay out of the database unless you opt in.
|
||||||
across the EU, the United States and Japan. By default only your own
|
Retention windows are configurable per channel, history can be wiped
|
||||||
conversations are stored; messages from strangers, NPCs and system
|
retroactively, and everything can be exported on demand.
|
||||||
spam stay out of the database. Retention windows are configurable per
|
|
||||||
channel, history can be wiped retroactively, and stored data can be
|
|
||||||
exported on demand.
|
|
||||||
|
|
||||||
Key privacy and data-handling features:
|
Features:
|
||||||
|
- Channel whitelist with a Privacy-First default
|
||||||
|
- Per-channel retention with a daily background sweep
|
||||||
|
- Retroactive cleanup (Ctrl+Shift confirm)
|
||||||
|
- Export to Markdown, JSON or CSV
|
||||||
|
- First-run wizard with three preset profiles
|
||||||
|
- Bilingual UI (EN/DE) with live language switching
|
||||||
|
- Own config and database — no shared state with other plugins
|
||||||
|
|
||||||
- Channel whitelist with a Privacy-First default
|
Based on Chat 2 by Infi and Anna (EUPL-1.2).
|
||||||
- Per-channel retention with a daily background sweep
|
Support: https://discord.gg/X9V7Kcv5gR
|
||||||
- Retroactive cleanup with a Ctrl+Shift confirm
|
repo_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat
|
||||||
- Export to Markdown, JSON or CSV
|
|
||||||
- First-run wizard with three preset profiles (Privacy-First, Casual,
|
|
||||||
Full History)
|
|
||||||
- Bilingual UI (English and German) with live language switching
|
|
||||||
- Independent plugin state — own config file and database directory,
|
|
||||||
so Hellion Chat does not share state with upstream Chat 2
|
|
||||||
|
|
||||||
v1.1.0 — Theme engine with five built-in themes (Hellion Arctic,
|
|
||||||
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove) plus
|
|
||||||
JSON-based custom-theme authoring. Settings rebuilt around a card
|
|
||||||
grid with section detail views. See docs/THEME-AUTHORING.md.
|
|
||||||
|
|
||||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
|
||||||
|
|
||||||
Modding & support: join the Hellion Forge Discord at
|
|
||||||
https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
|
|
||||||
other Hellion Online Media plugins/tools.
|
|
||||||
repo_url: https://github.com/JonKazama-Hellion/HellionChat
|
|
||||||
accepts_feedback: true
|
accepts_feedback: true
|
||||||
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png
|
icon_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png
|
||||||
image_urls:
|
image_urls:
|
||||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png
|
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png
|
||||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/settingsOverview.png
|
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png
|
||||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/themesPicker.png
|
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png
|
||||||
tags:
|
tags:
|
||||||
- Social
|
- Social
|
||||||
- UI
|
- UI
|
||||||
- Chat
|
- Chat
|
||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
**Hellion Chat 1.2.0 — Layout Refresh**
|
**v1.5.2 — First-Run Wizard Rework (2026-05-18)**
|
||||||
|
|
||||||
Second UI cycle: tab layouts modernised in both modes, a new
|
UX patch. The first-run wizard becomes a four-step flow with a
|
||||||
bottom status bar, card-rows as default message render, and
|
new Roleplay privacy profile and a power-settings step that
|
||||||
Auto-Tell tabs that you can finally tell apart at a glance.
|
surfaces previously-hidden defaults. Existing v1.5.1 users see
|
||||||
|
the new wizard once on first v1.5.2 boot.
|
||||||
|
|
||||||
Sidebar (icon-only, fixed 44 px):
|
What changes user-visible:
|
||||||
|
|
||||||
- Tab name on hover-tooltip, vertical accent pill on the
|
- Wizard navigation: Welcome → Privacy profile → Power settings
|
||||||
active tab, child background no longer paints the top
|
→ Done. Forge-Bronze pagination dots, dedicated stage for the
|
||||||
padding area.
|
power settings so they are no longer buried in Settings.
|
||||||
- Per-tab custom icons via Settings → Tabs.
|
- Fourth privacy profile "Roleplay": Privacy-First plus Say and
|
||||||
- Auto-Tell tabs: each partner gets a hashed icon (envelope/
|
both emote types, with a 30-day window for Say and a 90-day
|
||||||
star/heart/bell/bookmark/flag/fire) plus hashed color
|
window for emotes. Shout, Yell and Novice Network stay out.
|
||||||
(12-color palette) — 84 distinct combinations.
|
- Privacy picker becomes a 2x2 grid. Casual stays the
|
||||||
- Pulsing red dot in the top-right of any tab with unread
|
recommended option with a ★ marker.
|
||||||
messages, subtle 2-second sine pulse, respects
|
- Power-settings step covers Load Previous Session, Filter
|
||||||
Configuration.ReduceMotion.
|
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.
|
||||||
|
|
||||||
Top tabs:
|
Under the hood:
|
||||||
|
|
||||||
- Accent underline pill on the active tab instead of the old
|
- WizardStateSmokeStep added to /xlperf alongside the FontManager
|
||||||
background fill. Icon prefixes were attempted but reverted
|
and ThemeSwitch self-tests.
|
||||||
— Dalamud's default font atlas has no FontAwesome glyphs.
|
- Twelve new pure-helper xUnit Facts in the Build Suite cover
|
||||||
|
all four privacy profile sets and their retention overrides.
|
||||||
|
|
||||||
Bottom status bar (22 px, 1×/sec cached):
|
Migration v17 stays (no schema bump). The Configuration grows
|
||||||
|
one optional string field (WizardLastShownVersion) which
|
||||||
|
defaults to empty for legacy users.
|
||||||
|
|
||||||
- Active channel with color dot, Privacy-First badge, tab +
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
message counters, auto-tell counter (hidden at zero),
|
|
||||||
plugin version (right-aligned, muted).
|
|
||||||
|
|
||||||
Message rendering:
|
---
|
||||||
|
|
||||||
- Card rows by default — sender header in channel color, body
|
**v1.5.1 — FontAtlas Refactor and Hellion Forge Signature (2026-05-17)**
|
||||||
on its own line, subtle border between cards.
|
|
||||||
- Compact-Density toggle in Appearance returns the classic
|
|
||||||
single-line `[HH:mm] Sender: Text` layout.
|
|
||||||
|
|
||||||
Bug fixes from in-game testing:
|
Hybrid FontManager refactor plus an embedded provenance mark.
|
||||||
|
|
||||||
- Settings save no longer wipes chat history. Refilter cycle
|
What changes under the hood:
|
||||||
only runs when filter-relevant settings actually changed
|
|
||||||
(privacy, channel selection); cosmetic changes leave the
|
|
||||||
chat intact. Persistent and Auto-Tell tabs both survive.
|
|
||||||
- Hellion Schrift (Exo 2) no longer blocks font-size
|
|
||||||
adjustment — 4K users can scale up properly.
|
|
||||||
- Sidebar buttons align with the first message row, status
|
|
||||||
bar version slot is no longer clipped.
|
|
||||||
|
|
||||||
Migration v14 → v15: legacy theme fields removed
|
- FontManager handle creation moves into the ctor inside a single
|
||||||
(HellionThemeEnabled, HellionThemeWindowOpacity). All other
|
atlas.SuppressAutoRebuild() block. The font atlas now builds once
|
||||||
settings preserved.
|
per plugin load instead of four to five times — less CPU and GPU
|
||||||
|
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).
|
||||||
|
|
||||||
Polish (lerps, theme crossfade, header quick-picker) follows
|
Honorific full-gradient port (originally the v1.5.1 main item) was
|
||||||
in v1.3.0.
|
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.
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
User-visible:
|
||||||
|
|
||||||
**Hellion Chat 1.1.0 — Theme Foundation**
|
- 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.
|
||||||
|
|
||||||
First major UI cycle after the standalone v1.0.0 cut. Theme engine,
|
Note on performance: the cross-plugin baseline target from v1.5.0
|
||||||
five built-in themes, customisable JSON themes, modernised settings
|
(matching Lightless and XIVInstantMessenger at ~7 ms HITCH) did
|
||||||
layout.
|
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.
|
||||||
|
|
||||||
New themes (Settings → Themes):
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
- **Hellion Arctic** — the brand default, Arctic Cyan + Ember Glow
|
---
|
||||||
on industrial slate.
|
|
||||||
- **Chat 2 Klassik** — Steel Blue on neutral grey, eckige Kanten.
|
|
||||||
The upstream Chat 2 look on the new engine.
|
|
||||||
- **Event Horizon** — Cosmic Purple on near-black. Deep-space mood.
|
|
||||||
- **Moonlit Bloom** — Bloom Magenta + Soft Sage on deep-violet
|
|
||||||
night.
|
|
||||||
- **Mint Grove** — Mint Green + Honey Amber on deep forest. First
|
|
||||||
member of the Grove family.
|
|
||||||
|
|
||||||
Theme engine highlights:
|
**v1.5.0 — DI Foundation and Service Refactor (2026-05-17)**
|
||||||
|
|
||||||
- Slug-based selection in Settings → Themes with mini-mockup
|
Major architecture cycle. The plugin bootstrap moves to a
|
||||||
previews per theme.
|
generic-host DI container (Microsoft.Extensions.Hosting +
|
||||||
- Click a theme card and the whole plugin (chat, settings,
|
IServiceCollection) modelled on Lightless Sync. Service logging
|
||||||
pop-outs, viewer) repaints instantly.
|
moves from a static Plugin.LogProxy locator to typed
|
||||||
- Custom themes via JSON in pluginConfigs/HellionChat/themes/.
|
Microsoft.Extensions.Logging.ILogger<T> via constructor injection,
|
||||||
Example template seeded on first launch.
|
bridged over Dalamud's IPluginLog by a custom DalamudLogger trio.
|
||||||
- Optional per-theme chat-channel colours. When a theme proposes
|
|
||||||
its own chat colours and yours differ, a dezent banner offers
|
|
||||||
Apply / Keep — never auto-overwriting.
|
|
||||||
- Migration v13 → v14: existing users land on Hellion Arctic. Pick
|
|
||||||
Chat 2 Klassik to keep the upstream look.
|
|
||||||
|
|
||||||
Settings layout:
|
What changes under the hood:
|
||||||
|
|
||||||
- New card-grid overview on Settings open. Click a card to drill
|
- 18 instance-class services migrate to ILogger<T> via constructor
|
||||||
into the section.
|
injection across four slices: data layer (MessageStore,
|
||||||
- Breadcrumb back to overview, ESC also returns.
|
MessageManager, AutoTellTabsService), IPC and integrations
|
||||||
- Detail view drops the redundant tab list — section content uses
|
(HonorificService, IpcManager, TypingIpc, ExtraChat, the three
|
||||||
the full width.
|
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.
|
||||||
|
|
||||||
Branding:
|
User-visible:
|
||||||
|
|
||||||
- Plugin icon swapped from the ChatTwo derivative to the Hellion
|
- Slash-command insert fix: pasting a slash command into the
|
||||||
Forge hammer.
|
chat input (Friend List "/tell" action, plugin-driven inserts
|
||||||
- New docs/THEME-AUTHORING.md walks you through writing your own
|
from Artisan, AllaganTools etc.) now replaces the existing
|
||||||
themes with the Forge logo on top.
|
input instead of concatenating. Cherry-picked from ChatTwo
|
||||||
|
upstream ee7768ac with namespace adaptation.
|
||||||
|
|
||||||
Technical:
|
Migration v17 stays (no schema bump).
|
||||||
|
|
||||||
- HellionStyle.PushGlobal is now theme-driven. Configuration.
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
HellionThemeEnabled is deprecated and will be removed in v1.2.0.
|
|
||||||
- New ThemeRegistry singleton with LastWriteTime-cached custom-
|
|
||||||
theme loader.
|
|
||||||
- 51 local unit tests cover the data model, registry, JSON round-
|
|
||||||
trip and built-in sanity checks.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
---
|
||||||
|
|
||||||
**Hellion Chat 1.0.3 — Polish patch**
|
**v1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)**
|
||||||
|
|
||||||
- New: optionally hide chat (and every other plugin window) while the
|
Eleventh and final sub-patch of the v1.4.x polish-sweep series.
|
||||||
New Game+ menu is open. Toggle in Settings → Window → Frame, default
|
Symbol picker for the chat input, a tell-history reload fix for
|
||||||
off. Closing the menu restores all windows.
|
users with many active partners, and a closing cleanup sweep
|
||||||
- New: optionally tint the channel selector button next to the input
|
before v1.5.0 picks up the DI-container adoption.
|
||||||
field with the currently active channel's colour. Toggle in
|
|
||||||
Settings → Appearance → Colours, default on. Matches the existing
|
|
||||||
input-text tint and respects ExtraChat overrides.
|
|
||||||
- Fix: status, item and other inline hover icons keep their original
|
|
||||||
aspect ratio. Debuff icons with non-square dimensions are no longer
|
|
||||||
visually squished into a 32×32 box.
|
|
||||||
- Diagnostic: hide-state transitions (battle, cutscene, user-hide,
|
|
||||||
cutscene override) are now logged on Verbose level for easier bug
|
|
||||||
reports — off by default, enable with `/xllog set HellionChat verbose`.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
- 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).
|
||||||
|
|
||||||
**Hellion Chat 1.0.1 — Window Position Recovery**
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
- Automatic bounds check on the first draw after plugin load.
|
---
|
||||||
When the persisted window position has no overlap with the
|
|
||||||
primary viewport, the window snaps to a safe top-left default.
|
|
||||||
Helpful after a monitor disconnect, resolution change or
|
|
||||||
multi-monitor layout switch between sessions.
|
|
||||||
- New "Reset Window Position" button in Settings → Window → Frame
|
|
||||||
as a manual escape hatch for edge cases the automatic check
|
|
||||||
doesn't catch.
|
|
||||||
|
|
||||||
Tested on Linux/Wayland with a hard-cut three-monitor reduction;
|
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||||
window recovers cleanly without manual JSON editing.
|
|
||||||
|
|
||||||
Housekeeping carried over since v1.0.0:
|
|
||||||
|
|
||||||
- Documentation restructured into docs/ folder. New CHANGELOG,
|
|
||||||
CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added
|
|
||||||
- Stale ChatTwo/* paths in repo configs updated to HellionChat/*
|
|
||||||
- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString
|
|
||||||
Unicode fix relevant for non-ASCII channel/tab names)
|
|
||||||
- GitHub Actions: actions/setup-dotnet bumped 4 → 5,
|
|
||||||
github/codeql-action bumped 3 → 4
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 1.0.0 — Standalone Major Release**
|
|
||||||
|
|
||||||
First fully standalone release. Internal cleanup plus a sweep of
|
|
||||||
pre-existing correctness, security, threading and resource-leak
|
|
||||||
fixes carried over from the upstream codebase. No user action
|
|
||||||
required — auto-update applies cleanly, configuration and database
|
|
||||||
paths unchanged.
|
|
||||||
|
|
||||||
Standalone identity:
|
|
||||||
|
|
||||||
- Code namespace consolidated from ChatTwo.* to HellionChat.* across
|
|
||||||
all source files
|
|
||||||
- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:
|
|
||||||
Register, Available, Unregister, Invoke, GetChatInputState,
|
|
||||||
ChatInputStateChanged) — third-party plugins that bound to the old
|
|
||||||
channels need to be updated; none known at release time
|
|
||||||
- ImGui popup ID renamed to hellionchat-context-popup
|
|
||||||
- Repository folder restructured (ChatTwo/ → HellionChat/), all CI
|
|
||||||
and build paths updated accordingly
|
|
||||||
- Public-facing descriptions reworded from upstream-fork framing to
|
|
||||||
standalone framing (Chat 2 attribution preserved per EUPL-1.2)
|
|
||||||
- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'
|
|
||||||
|
|
||||||
Safety:
|
|
||||||
|
|
||||||
- Plugin now refuses to load when upstream Chat 2 is also active —
|
|
||||||
bilingual conflict message in EN/DE, throw before any subsystem
|
|
||||||
initialization, prevents the runtime crash that previously occurred
|
|
||||||
when both plugins replaced the same chat window in parallel
|
|
||||||
- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory
|
|
||||||
corruption from aggregate-term overflow, CVE-2025-7709)
|
|
||||||
- NuGet restore now honors packages.lock.json so transitive
|
|
||||||
dependencies don't drift between machines or CI runs
|
|
||||||
|
|
||||||
Default tab layout sharpened (one-time tab reset on first start):
|
|
||||||
|
|
||||||
The first-run tab layout is reorganized into five thematic tabs
|
|
||||||
based on external tester feedback. General contains only Say,
|
|
||||||
Yell and Shout (immediate-surroundings public chat). System
|
|
||||||
absorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,
|
|
||||||
Gathering, PF recruitment pings) and announcement noise
|
|
||||||
(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)
|
|
||||||
that previously lived in General. FreeCompany, Group and
|
|
||||||
Linkshell each own their channel set. The static Tell tab is
|
|
||||||
gone — Auto-Tell-Tabs spawns per-conversation tabs on demand.
|
|
||||||
The Beginner / Novice-Network preset is no longer added by
|
|
||||||
default but is still available via Settings, Tabs.
|
|
||||||
|
|
||||||
This is a one-time tab-layout reset for users on config version
|
|
||||||
12 or older. Privacy, Retention, Theme and every other setting
|
|
||||||
is preserved. Your previous tab configuration is written to
|
|
||||||
pluginConfigs/HellionChat.json.pre-v13-backup so you can restore
|
|
||||||
it manually if you prefer the old layout.
|
|
||||||
|
|
||||||
Crash-class fixes (formerly latent in upstream):
|
|
||||||
|
|
||||||
- MathUtil.HasOverlap now uses a correct AABB test; identical or
|
|
||||||
edge-touching rectangles are no longer reported as non-overlapping
|
|
||||||
- ChatCode.Equals compares fields directly instead of GetHashCode;
|
|
||||||
removes the hash-collision anti-pattern
|
|
||||||
- IpcManager.Dispose uses UnregisterAction to match the matching
|
|
||||||
RegisterAction call; previous mismatch leaked the action
|
|
||||||
subscription on every plugin reload
|
|
||||||
- ExtraChat.Dispose now unsubscribes all three IPC subscriptions
|
|
||||||
(was only the first); leaks closed
|
|
||||||
- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address
|
|
||||||
before dereferencing the unsafe Character* cast
|
|
||||||
- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the
|
|
||||||
Hook reference instead of using the null-forgiving operator
|
|
||||||
- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so
|
|
||||||
a tab drop or empty-worlds list no longer crashes the UI
|
|
||||||
- Debugger.cs now declares IDisposable so the existing Dispose runs
|
|
||||||
|
|
||||||
Correctness fixes:
|
|
||||||
|
|
||||||
- GlobalParametersCache.GetValue captures Cache into a local before
|
|
||||||
the bounds check, so a concurrent Refresh can't slip a different
|
|
||||||
array between check and read
|
|
||||||
- IconUtil binary search bounds initialized to entries.Length-1 and
|
|
||||||
reset on redirect-restart; entries.Length==0 short-circuits
|
|
||||||
- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was
|
|
||||||
Region.RowId) so it actually returns same-DC worlds
|
|
||||||
- Message.cs back-reference loop iterates the processed Sender/Content
|
|
||||||
properties so chunks added by CheckMessageContent get Message set
|
|
||||||
- Language.zh-Hans Webinterface_Start_Success corrected to
|
|
||||||
"网页界面已启动" (was "网页界面已停止")
|
|
||||||
|
|
||||||
Threading and async:
|
|
||||||
|
|
||||||
- AutoTranslate Entries/ValidEntries are now serialized behind a
|
|
||||||
single lock; the preload worker thread and main thread no longer
|
|
||||||
race on the underlying dictionary/hash set
|
|
||||||
- Privacy retention and cleanup workers bound their framework-refresh
|
|
||||||
waits to 5 seconds with a logged timeout; a hung framework tick can
|
|
||||||
no longer deadlock the background worker
|
|
||||||
|
|
||||||
Resource handling:
|
|
||||||
|
|
||||||
- EmoteCache reuses the static HttpClient instead of allocating a new
|
|
||||||
one per call (closed socket leak)
|
|
||||||
- FontManager wraps HttpClient/HttpResponseMessage in using-blocks
|
|
||||||
and adds EnsureSuccessStatusCode; failed downloads no longer
|
|
||||||
silently produce a zero-byte font file
|
|
||||||
- SearchSelector mixes the row index into the ImGui ID stack so
|
|
||||||
selectables don't collapse to a single ambiguous ID
|
|
||||||
- SettingsTabs/Chat blocked-emote add-button now opens its selector
|
|
||||||
popup on left-click
|
|
||||||
|
|
||||||
Performance:
|
|
||||||
|
|
||||||
- DbViewer text export caches filteredHistory.Count once instead of
|
|
||||||
re-enumerating the IEnumerable on every batch (O(N) instead of
|
|
||||||
O(N²) on large histories)
|
|
||||||
|
|
||||||
License attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md
|
|
||||||
and the Credits section in README) is unchanged.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**
|
|
||||||
|
|
||||||
- Pop-out button now visible in the chat header (no more hunting
|
|
||||||
through the right-click menu)
|
|
||||||
- One-time hint banner explains pop-out tabs and the right-click
|
|
||||||
shortcut
|
|
||||||
- New setting: open new /tell tabs directly as pop-out windows
|
|
||||||
(Settings → Chat → Auto-Tell-Tabs)
|
|
||||||
- Pop-out input is now enabled by default — closing a pop-out still
|
|
||||||
returns the tab to the sidebar
|
|
||||||
- Bugfix: dropping or logging out with an LRU/popped auto-tell tab
|
|
||||||
now also closes its pop-out window (no more ghost windows)
|
|
||||||
- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out
|
|
||||||
hint banner was visible (also fixed retroactively for the v0.6.0
|
|
||||||
banner inside pop-outs)
|
|
||||||
|
|
||||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using Dalamud.Plugin;
|
||||||
|
using HellionChat.Ipc;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Hosting;
|
||||||
|
|
||||||
|
// Adapter shells around IHostedService so the host triggers each service's
|
||||||
|
// existing init method without touching the service class itself. Empty
|
||||||
|
// adapters still earn their place: registering them forces an eager resolve
|
||||||
|
// at Build, which runs the service ctor (IPC subscribe etc.) right then
|
||||||
|
// instead of lazily on first GetRequiredService.
|
||||||
|
|
||||||
|
internal sealed class ThemeRegistryInitHostedService(ThemeRegistry registry) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Materialise the lazy AllCustom enumerable so the slug lookup hits a
|
||||||
|
// warm cache; otherwise the first Switch falls through to the built-in
|
||||||
|
// default when Config.Theme points at a custom slug.
|
||||||
|
foreach (var _ in registry.AllCustom()) { }
|
||||||
|
registry.Switch(Plugin.Config.Theme);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC subscribers do their wiring in the ctor, so StartAsync stays empty —
|
||||||
|
// the registration alone forces an eager resolve which runs that wiring.
|
||||||
|
|
||||||
|
internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly IpcManager _ipc = ipc;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TypingIpcInitHostedService(TypingIpc typingIpc) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly TypingIpc _typingIpc = typingIpc;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ExtraChatInitHostedService(ExtraChat extraChat) : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ExtraChat _extraChat = extraChat;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class MessageManagerInitHostedService(
|
||||||
|
IDalamudPluginInterface pluginInterface,
|
||||||
|
MessageManager manager
|
||||||
|
) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// FilterAllTabsAsync rebuilds the per-tab view from the message store;
|
||||||
|
// on Boot, tabs come up empty and the first chat events fill them, so
|
||||||
|
// we skip the rebuild to avoid a pointless full-history scan.
|
||||||
|
if (pluginInterface.Reason is not PluginLoadReason.Boot)
|
||||||
|
manager.FilterAllTabsAsync();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService service)
|
||||||
|
: IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
service.Initialize();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
internal sealed class DalamudLogger : ILogger
|
||||||
|
{
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
|
||||||
|
public DalamudLogger(string name, IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDisposable? ILogger.BeginScope<TState>(TState state) => default!;
|
||||||
|
|
||||||
|
// Filtering happens in Dalamud's /xllog. Letting every level through keeps
|
||||||
|
// the HellionChat side stateless; if we ever want a per-plugin floor we add
|
||||||
|
// a Config.LogLevel and tighten this method.
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => true;
|
||||||
|
|
||||||
|
public void Log<TState>(
|
||||||
|
LogLevel logLevel,
|
||||||
|
EventId eventId,
|
||||||
|
TState state,
|
||||||
|
Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// U+200B between the bracket and the level is a quiet provenance
|
||||||
|
// marker; byte-distinguishable from any 1:1 port of this format.
|
||||||
|
if ((int)logLevel <= (int)LogLevel.Information)
|
||||||
|
{
|
||||||
|
_pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.Append($"[{_name}]{{{(int)logLevel}}} {state} {exception?.Message}");
|
||||||
|
if (!string.IsNullOrWhiteSpace(exception?.StackTrace))
|
||||||
|
sb.AppendLine(exception.StackTrace);
|
||||||
|
|
||||||
|
var inner = exception?.InnerException;
|
||||||
|
while (inner != null)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"InnerException {inner}: {inner.Message}");
|
||||||
|
sb.AppendLine(inner.StackTrace);
|
||||||
|
inner = inner.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logLevel == LogLevel.Warning)
|
||||||
|
_pluginLog.Warning(sb.ToString());
|
||||||
|
else if (logLevel == LogLevel.Error)
|
||||||
|
_pluginLog.Error(sb.ToString());
|
||||||
|
else
|
||||||
|
_pluginLog.Fatal(sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using HellionChat.Branding;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
[ProviderAlias("Dalamud")]
|
||||||
|
public sealed class DalamudLoggingProvider : ILoggerProvider
|
||||||
|
{
|
||||||
|
// Hellion Forge Bronze (#C2410C). Mixed into the bootstrap fingerprint.
|
||||||
|
private const string HellionMarker = "HellionForgeBronzeC2410C";
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers = new(
|
||||||
|
StringComparer.OrdinalIgnoreCase
|
||||||
|
);
|
||||||
|
|
||||||
|
private readonly IPluginLog _pluginLog;
|
||||||
|
|
||||||
|
public DalamudLoggingProvider(IPluginLog pluginLog)
|
||||||
|
{
|
||||||
|
_pluginLog = pluginLog;
|
||||||
|
EmitBootstrapBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One-shot per plugin load. Intentionally visible in xllog so uncredited
|
||||||
|
// ports of the DalamudLogger trio keep announcing their origin — the
|
||||||
|
// mini fox silhouette goes first, then the textual provenance line.
|
||||||
|
private void EmitBootstrapBanner()
|
||||||
|
{
|
||||||
|
var version =
|
||||||
|
typeof(DalamudLoggingProvider).Assembly.GetName().Version?.ToString() ?? "0.0.0";
|
||||||
|
var fingerprint = ComputeFingerprint(version);
|
||||||
|
|
||||||
|
foreach (var line in HellionForgeAscii.FoxMini.Split('\n'))
|
||||||
|
{
|
||||||
|
var trimmed = line.TrimEnd('\r');
|
||||||
|
if (trimmed.Length > 0)
|
||||||
|
_pluginLog.Information(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pluginLog.Information("by Julia Moon - Hellion Forge");
|
||||||
|
_pluginLog.Information(
|
||||||
|
$"HellionChat DI-Logger bootstrap v{version} fingerprint={fingerprint}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeFingerprint(string version)
|
||||||
|
{
|
||||||
|
var seed = Encoding.UTF8.GetBytes($"{HellionMarker}-{version}");
|
||||||
|
var hash = SHA256.HashData(seed);
|
||||||
|
var sb = new StringBuilder(8);
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
sb.Append(hash[i].ToString("x2"));
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
{
|
||||||
|
// Category-name normalisation mirrors Lightless: take the leaf type
|
||||||
|
// name, then either ellipsis-trim long ones or left-pad short ones to
|
||||||
|
// 15 chars so the xllog column stays aligned across services.
|
||||||
|
var catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last();
|
||||||
|
if (catName.Length > 15)
|
||||||
|
catName = string.Concat(
|
||||||
|
catName.AsSpan(0, 6),
|
||||||
|
"...",
|
||||||
|
catName.AsSpan(catName.Length - 6, 6)
|
||||||
|
);
|
||||||
|
else
|
||||||
|
catName = catName.PadLeft(15);
|
||||||
|
|
||||||
|
return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _pluginLog));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_loggers.Clear();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace HellionChat.Infrastructure.Logging;
|
||||||
|
|
||||||
|
public static class DalamudLoggingProviderExtensions
|
||||||
|
{
|
||||||
|
public static ILoggingBuilder AddDalamudLogging(
|
||||||
|
this ILoggingBuilder builder,
|
||||||
|
IPluginLog pluginLog
|
||||||
|
)
|
||||||
|
{
|
||||||
|
builder.ClearProviders();
|
||||||
|
builder.Services.TryAddEnumerable(
|
||||||
|
ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>(
|
||||||
|
_ => new DalamudLoggingProvider(pluginLog)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,9 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
// Hellion Chat — v0.6.0 shared input history. Replaces the embedded
|
// Shared input history for all ChatInputBars (main and pop-out windows).
|
||||||
// ChatLogWindow.InputBacklog so that pop-out windows with their own
|
// Push deduplicates: existing entries are moved to the end when re-added.
|
||||||
// ChatInputBar can navigate the same Up/Down history as the main window.
|
// TEST-MIRROR: ../../Hellion Build test/Util/InputHistoryServiceTests.cs
|
||||||
// Index semantics are kept identical to the v0.5.x InputBacklog:
|
|
||||||
// index 0 = oldest entry
|
|
||||||
// index Count - 1 = newest entry
|
|
||||||
// Push performs move-to-newest deduplication: existing entries are
|
|
||||||
// removed before the new one is appended at the end.
|
|
||||||
public static class InputHistoryService
|
public static class InputHistoryService
|
||||||
{
|
{
|
||||||
private const int MaxSize = 30;
|
private const int MaxSize = 30;
|
||||||
@@ -26,8 +21,7 @@ public static class InputHistoryService
|
|||||||
|
|
||||||
var trimmed = entry.Trim();
|
var trimmed = entry.Trim();
|
||||||
|
|
||||||
// Move-to-newest: existing entries are removed before the append
|
// Move-to-newest: remove existing entry before adding at the end
|
||||||
// so the same line typed twice does not occupy two history slots.
|
|
||||||
for (var i = 0; i < _entries.Count; i++)
|
for (var i = 0; i < _entries.Count; i++)
|
||||||
{
|
{
|
||||||
if (_entries[i] == trimmed)
|
if (_entries[i] == trimmed)
|
||||||
@@ -48,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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace HellionChat.Integrations;
|
||||||
|
|
||||||
|
// Newtonsoft.Json is used here for IPC compatibility with Honorific, which
|
||||||
|
// serialises TitleData with it. It's a transitive Dalamud dependency — no
|
||||||
|
// new NuGet entry needed. The rest of HellionChat uses System.Text.Json.
|
||||||
|
internal sealed class HonorificService : IDisposable
|
||||||
|
{
|
||||||
|
private const string IpcNamespace = "Honorific";
|
||||||
|
|
||||||
|
// Major version of the Honorific IPC contract we're built against.
|
||||||
|
internal const uint ExpectedApiMajor = 3;
|
||||||
|
|
||||||
|
// IPC gates — kept as fields so Dispose can unsubscribe the same instances.
|
||||||
|
private readonly ICallGateSubscriber<(uint, uint)> _apiVersion;
|
||||||
|
private readonly ICallGateSubscriber<string> _getLocalCharacterTitle;
|
||||||
|
private readonly ICallGateSubscriber<string, object> _localCharacterTitleChanged;
|
||||||
|
private readonly ICallGateSubscriber<object> _ready;
|
||||||
|
private readonly ICallGateSubscriber<object> _disposing;
|
||||||
|
|
||||||
|
private readonly ILogger<HonorificService> _logger;
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
private bool _versionWarningLogged;
|
||||||
|
|
||||||
|
// Thread: framework only — IPC delivery + ImGui render both run there.
|
||||||
|
public HonorificTitleData? CurrentTitle { get; private set; }
|
||||||
|
public bool IsAvailable { get; private set; }
|
||||||
|
public (uint Major, uint Minor)? DetectedApiVersion { get; private set; }
|
||||||
|
|
||||||
|
public HonorificService(
|
||||||
|
IDalamudPluginInterface pluginInterface,
|
||||||
|
ILogger<HonorificService> logger,
|
||||||
|
IFramework framework
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_framework = framework;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Gate objects are cached per-name by Dalamud and safe to register
|
||||||
|
// before Honorific loads — they just won't fire until it does.
|
||||||
|
// Initial pull is scheduled on the framework thread because plugin
|
||||||
|
// constructors run on the loader thread, and Honorific's IPC handlers
|
||||||
|
// read ObjectTable.LocalPlayer which throws off the framework thread.
|
||||||
|
_apiVersion = pluginInterface.GetIpcSubscriber<(uint, uint)>($"{IpcNamespace}.ApiVersion");
|
||||||
|
_getLocalCharacterTitle = pluginInterface.GetIpcSubscriber<string>(
|
||||||
|
$"{IpcNamespace}.GetLocalCharacterTitle"
|
||||||
|
);
|
||||||
|
_localCharacterTitleChanged = pluginInterface.GetIpcSubscriber<string, object>(
|
||||||
|
$"{IpcNamespace}.LocalCharacterTitleChanged"
|
||||||
|
);
|
||||||
|
_ready = pluginInterface.GetIpcSubscriber<object>($"{IpcNamespace}.Ready");
|
||||||
|
_disposing = pluginInterface.GetIpcSubscriber<object>($"{IpcNamespace}.Disposing");
|
||||||
|
|
||||||
|
_localCharacterTitleChanged.Subscribe(OnTitleChanged);
|
||||||
|
_ready.Subscribe(OnReady);
|
||||||
|
_disposing.Subscribe(OnDisposing);
|
||||||
|
|
||||||
|
_framework.RunOnFrameworkThread(TryInitialPull);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Wrap each unsubscribe — a missing gate must not block the others.
|
||||||
|
// Leaking a subscription keeps this service alive across plugin reloads.
|
||||||
|
TryUnsubscribe(() => _localCharacterTitleChanged.Unsubscribe(OnTitleChanged));
|
||||||
|
TryUnsubscribe(() => _ready.Unsubscribe(OnReady));
|
||||||
|
TryUnsubscribe(() => _disposing.Unsubscribe(OnDisposing));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread: framework (scheduled from ctor and OnReady).
|
||||||
|
private void TryInitialPull()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var version = _apiVersion.InvokeFunc();
|
||||||
|
DetectedApiVersion = version;
|
||||||
|
|
||||||
|
if (!IsApiVersionCompatible(version))
|
||||||
|
{
|
||||||
|
if (!_versionWarningLogged)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Honorific API version mismatch — expected major 3, "
|
||||||
|
+ "found {Major}.{Minor}. Disabling Honorific integration.",
|
||||||
|
version.Item1,
|
||||||
|
version.Item2
|
||||||
|
);
|
||||||
|
_versionWarningLogged = true;
|
||||||
|
}
|
||||||
|
IsAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsAvailable = true;
|
||||||
|
_versionWarningLogged = false;
|
||||||
|
var json = _getLocalCharacterTitle.InvokeFunc();
|
||||||
|
CurrentTitle = ParseTitleJson(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Honorific not installed or not yet initialised — Ready will retry.
|
||||||
|
_logger.LogDebug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
|
||||||
|
IsAvailable = false;
|
||||||
|
CurrentTitle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread: framework (Dalamud IPC delivery contract).
|
||||||
|
private void OnTitleChanged(string json)
|
||||||
|
{
|
||||||
|
// Skip updates on version mismatch; subscription stays live for reload.
|
||||||
|
if (!IsAvailable)
|
||||||
|
return;
|
||||||
|
CurrentTitle = ParseTitleJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread: any (Honorific dispatches NotifyReady from its own thread).
|
||||||
|
private void OnReady()
|
||||||
|
{
|
||||||
|
_framework.RunOnFrameworkThread(TryInitialPull);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread: framework (IPC delivery contract); idempotent — Disposing fires once.
|
||||||
|
private void OnDisposing()
|
||||||
|
{
|
||||||
|
// Honorific unloading — clear cached state so the header hides next frame.
|
||||||
|
// Subscriptions stay registered in case Honorific reloads.
|
||||||
|
// CurrentTitle is already nulled by OnTitleChanged before this fires,
|
||||||
|
// re-clearing here is belt-and-braces.
|
||||||
|
CurrentTitle = null;
|
||||||
|
IsAvailable = false;
|
||||||
|
DetectedApiVersion = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread: framework (called from Dispose, which runs on the framework
|
||||||
|
// cleanup block in Plugin.DisposeAsync).
|
||||||
|
private void TryUnsubscribe(Action unsubscribe)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Warning not Debug — a silent unsubscribe failure leaks a live
|
||||||
|
// subscription across plugin reloads.
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Honorific unsubscribe failed (likely API break or gate already gone)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static HonorificTitleData? ParseTitleJson(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<HonorificTitleData>(json);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsApiVersionCompatible((uint Major, uint Minor) apiVersion)
|
||||||
|
{
|
||||||
|
return apiVersion.Major == ExpectedApiMajor;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool ShouldRenderSlot(
|
||||||
|
bool toggleEnabled,
|
||||||
|
bool isAvailable,
|
||||||
|
HonorificTitleData? title
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!toggleEnabled)
|
||||||
|
return false;
|
||||||
|
if (!isAvailable)
|
||||||
|
return false;
|
||||||
|
if (title is null)
|
||||||
|
return false;
|
||||||
|
if (title.IsOriginal)
|
||||||
|
return false;
|
||||||
|
if (string.IsNullOrEmpty(title.Title))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace HellionChat.Integrations;
|
||||||
|
|
||||||
|
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||||
|
// so HellionChat loads cleanly when Honorific is absent.
|
||||||
|
//
|
||||||
|
// 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(
|
||||||
|
string? Title,
|
||||||
|
bool IsPrefix,
|
||||||
|
bool IsOriginal,
|
||||||
|
Vector3? Color,
|
||||||
|
Vector3? Glow,
|
||||||
|
Vector3? Color3,
|
||||||
|
int? GradientColourSet,
|
||||||
|
string? GradientAnimationStyle
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Integrations;
|
||||||
|
|
||||||
|
// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
|
||||||
|
internal static class IntegrationLinks
|
||||||
|
{
|
||||||
|
public const string HonorificRepo = "https://github.com/Caraxi/Honorific";
|
||||||
|
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
|
||||||
@@ -15,30 +18,46 @@ public sealed class ExtraChat : IDisposable
|
|||||||
#pragma warning restore CS0649
|
#pragma warning restore CS0649
|
||||||
|
|
||||||
private ICallGateSubscriber<OverrideInfo, object> OverrideChannelGate { get; }
|
private ICallGateSubscriber<OverrideInfo, object> OverrideChannelGate { get; }
|
||||||
private ICallGateSubscriber<Dictionary<string, uint>, Dictionary<string, uint>> ChannelCommandColoursGate { get; }
|
private ICallGateSubscriber<
|
||||||
private ICallGateSubscriber<Dictionary<Guid, string>, Dictionary<Guid, string>> ChannelNamesGate { get; }
|
Dictionary<string, uint>,
|
||||||
|
Dictionary<string, uint>
|
||||||
|
> ChannelCommandColoursGate { get; }
|
||||||
|
private ICallGateSubscriber<
|
||||||
|
Dictionary<Guid, string>,
|
||||||
|
Dictionary<Guid, string>
|
||||||
|
> ChannelNamesGate { get; }
|
||||||
|
|
||||||
internal (string, uint)? ChannelOverride { get; set; }
|
internal (string, uint)? ChannelOverride { get; set; }
|
||||||
|
|
||||||
// Volatile reference: IPC callbacks (OnChannelCommandColours/OnChannelNames) fire on a
|
// volatile: IPC callbacks fire on a Dalamud thread while ImGui reads these.
|
||||||
// Dalamud-dispatcher thread while the ImGui thread reads the IReadOnlyDictionary projections.
|
// Reference assignment is atomic on x64, but the barrier ensures visibility
|
||||||
// Reference assignment is atomic on x64, but the JIT (especially Mono on Wine/Linux) needs
|
// across threads (especially Mono/Wine). See AUDIT-2026-05-05 [SEC-01].
|
||||||
// the volatile barrier to guarantee visibility across threads. See AUDIT-2026-05-05 [SEC-01].
|
|
||||||
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
|
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
|
||||||
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
|
internal IReadOnlyDictionary<string, uint> ChannelCommandColours =>
|
||||||
|
ChannelCommandColoursInternal;
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>("ExtraChat.OverrideChannelColour");
|
_logger = logger;
|
||||||
ChannelCommandColoursGate = Plugin.Interface.GetIpcSubscriber<Dictionary<string, uint>, Dictionary<string, uint>>("ExtraChat.ChannelCommandColours");
|
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>(
|
||||||
ChannelNamesGate = Plugin.Interface.GetIpcSubscriber<Dictionary<Guid, string>, Dictionary<Guid, string>>("ExtraChat.ChannelNames");
|
"ExtraChat.OverrideChannelColour"
|
||||||
|
);
|
||||||
|
ChannelCommandColoursGate = Plugin.Interface.GetIpcSubscriber<
|
||||||
|
Dictionary<string, uint>,
|
||||||
|
Dictionary<string, uint>
|
||||||
|
>("ExtraChat.ChannelCommandColours");
|
||||||
|
ChannelNamesGate = Plugin.Interface.GetIpcSubscriber<
|
||||||
|
Dictionary<Guid, string>,
|
||||||
|
Dictionary<Guid, string>
|
||||||
|
>("ExtraChat.ChannelNames");
|
||||||
|
|
||||||
OverrideChannelGate.Subscribe(OnOverrideChannel);
|
OverrideChannelGate.Subscribe(OnOverrideChannel);
|
||||||
ChannelCommandColoursGate.Subscribe(OnChannelCommandColours);
|
ChannelCommandColoursGate.Subscribe(OnChannelCommandColours);
|
||||||
ChannelNamesGate.Subscribe(OnChannelNames);
|
ChannelNamesGate.Subscribe(OnChannelNames);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
|
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
|
||||||
@@ -46,8 +65,8 @@ public sealed class ExtraChat : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// ExtraChat is optional; missing IPC peer 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?)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,22 +79,11 @@ public sealed class ExtraChat : IDisposable
|
|||||||
|
|
||||||
private void OnOverrideChannel(OverrideInfo info)
|
private void OnOverrideChannel(OverrideInfo info)
|
||||||
{
|
{
|
||||||
if (info.Channel == null)
|
ChannelOverride = info.Channel == null ? null : (info.Channel, info.Rgba);
|
||||||
{
|
|
||||||
ChannelOverride = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChannelOverride = (info.Channel, info.Rgba);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnChannelCommandColours(Dictionary<string, uint> obj)
|
private void OnChannelCommandColours(Dictionary<string, uint> obj) =>
|
||||||
{
|
|
||||||
ChannelCommandColoursInternal = obj;
|
ChannelCommandColoursInternal = obj;
|
||||||
}
|
|
||||||
|
|
||||||
private void OnChannelNames(Dictionary<Guid, string> obj)
|
private void OnChannelNames(Dictionary<Guid, string> obj) => ChannelNamesInternal = obj;
|
||||||
{
|
|
||||||
ChannelNamesInternal = obj;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
using HellionChat.Code;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using HellionChat.Code;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace HellionChat.Ipc;
|
namespace HellionChat.Ipc;
|
||||||
|
|
||||||
using ChatInputState = (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType);
|
using ChatInputState = (
|
||||||
|
bool InputVisible,
|
||||||
|
bool InputFocused,
|
||||||
|
bool HasText,
|
||||||
|
bool IsTyping,
|
||||||
|
int TextLength,
|
||||||
|
ChatType ChannelType
|
||||||
|
);
|
||||||
|
|
||||||
internal sealed class TypingIpc : IDisposable
|
internal sealed class TypingIpc : IDisposable
|
||||||
{
|
{
|
||||||
@@ -12,17 +20,44 @@ 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>("HellionChat.GetChatInputState");
|
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||||
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("HellionChat.ChatInputStateChanged");
|
"HellionChat.GetChatInputState"
|
||||||
|
);
|
||||||
|
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>(
|
||||||
|
"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()
|
||||||
@@ -30,19 +65,22 @@ internal sealed class TypingIpc : IDisposable
|
|||||||
var log = Plugin.ChatLogWindow;
|
var log = Plugin.ChatLogWindow;
|
||||||
|
|
||||||
var usedChannel = Plugin.CurrentTab.CurrentChannel;
|
var usedChannel = Plugin.CurrentTab.CurrentChannel;
|
||||||
var inputChannel = usedChannel.UseTempChannel ? usedChannel.TempChannel : usedChannel.Channel;
|
var inputChannel = usedChannel.UseTempChannel
|
||||||
|
? usedChannel.TempChannel
|
||||||
|
: usedChannel.Channel;
|
||||||
var channelType = inputChannel.ToChatType();
|
var channelType = inputChannel.ToChatType();
|
||||||
|
|
||||||
return (InputVisible: !log.IsHidden,
|
return (
|
||||||
|
InputVisible: !log.IsHidden,
|
||||||
log.InputFocused,
|
log.InputFocused,
|
||||||
HasText: log.Chat.Length > 0,
|
HasText: log.Chat.Length > 0,
|
||||||
IsTyping: log is { InputFocused: true, Chat.Length: > 0 },
|
IsTyping: log is { InputFocused: true, Chat.Length: > 0 },
|
||||||
TextLength: log.Chat.Length,
|
TextLength: log.Chat.Length,
|
||||||
ChannelType: channelType);
|
ChannelType: channelType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatInputState GetState()
|
private ChatInputState GetState() => BuildState();
|
||||||
=> BuildState();
|
|
||||||
|
|
||||||
internal void Update()
|
internal void Update()
|
||||||
{
|
{
|
||||||
@@ -53,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,20 +1,52 @@
|
|||||||
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; }
|
||||||
private ICallGateProvider<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?> InvokeGate { get; }
|
private ICallGateProvider<
|
||||||
|
string,
|
||||||
|
PlayerPayload?,
|
||||||
|
ulong,
|
||||||
|
Payload?,
|
||||||
|
SeString?,
|
||||||
|
SeString?,
|
||||||
|
object?
|
||||||
|
> 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);
|
||||||
|
|
||||||
@@ -23,14 +55,56 @@ internal sealed class IpcManager : IDisposable
|
|||||||
UnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>("HellionChat.Unregister");
|
UnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>("HellionChat.Unregister");
|
||||||
UnregisterGate.RegisterAction(Unregister);
|
UnregisterGate.RegisterAction(Unregister);
|
||||||
|
|
||||||
InvokeGate = Plugin.Interface.GetIpcProvider<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?>("HellionChat.Invoke");
|
InvokeGate = Plugin.Interface.GetIpcProvider<
|
||||||
|
string,
|
||||||
|
PlayerPayload?,
|
||||||
|
ulong,
|
||||||
|
Payload?,
|
||||||
|
SeString?,
|
||||||
|
SeString?,
|
||||||
|
object?
|
||||||
|
>("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(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content)
|
internal void Invoke(
|
||||||
|
string id,
|
||||||
|
PlayerPayload? sender,
|
||||||
|
ulong contentId,
|
||||||
|
Payload? payload,
|
||||||
|
SeString? senderString,
|
||||||
|
SeString? content
|
||||||
|
)
|
||||||
{
|
{
|
||||||
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()
|
||||||
@@ -49,6 +123,8 @@ internal sealed class IpcManager : IDisposable
|
|||||||
{
|
{
|
||||||
UnregisterGate.UnregisterAction();
|
UnregisterGate.UnregisterAction();
|
||||||
RegisterGate.UnregisterFunc();
|
RegisterGate.UnregisterFunc();
|
||||||
|
ChatTwoUnregisterGate.UnregisterAction();
|
||||||
|
ChatTwoRegisterGate.UnregisterFunc();
|
||||||
Registered.Clear();
|
Registered.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-39
@@ -1,12 +1,12 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using HellionChat.Code;
|
|
||||||
using HellionChat.Util;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
using HellionChat.Code;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -33,7 +33,16 @@ public partial class Message
|
|||||||
public Dictionary<Guid, float?> Height { get; } = new();
|
public Dictionary<Guid, float?> Height { get; } = new();
|
||||||
public Dictionary<Guid, bool> IsVisible { get; } = new();
|
public Dictionary<Guid, bool> IsVisible { get; } = new();
|
||||||
|
|
||||||
public Message(ulong receiver, ulong contentId, ulong accountId, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource)
|
public Message(
|
||||||
|
ulong receiver,
|
||||||
|
ulong contentId,
|
||||||
|
ulong accountId,
|
||||||
|
ChatCode code,
|
||||||
|
List<Chunk> sender,
|
||||||
|
List<Chunk> content,
|
||||||
|
SeString senderSource,
|
||||||
|
SeString contentSource
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var extraChatChannel = ExtractExtraChatChannel(contentSource);
|
var extraChatChannel = ExtractExtraChatChannel(contentSource);
|
||||||
Receiver = receiver;
|
Receiver = receiver;
|
||||||
@@ -56,7 +65,18 @@ public partial class Message
|
|||||||
chunk.Message = this;
|
chunk.Message = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Message(Guid id, ulong receiver, ulong contentId, DateTimeOffset date, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource, Guid extraChatChannel)
|
public Message(
|
||||||
|
Guid id,
|
||||||
|
ulong receiver,
|
||||||
|
ulong contentId,
|
||||||
|
DateTimeOffset date,
|
||||||
|
ChatCode code,
|
||||||
|
List<Chunk> sender,
|
||||||
|
List<Chunk> content,
|
||||||
|
SeString senderSource,
|
||||||
|
SeString contentSource,
|
||||||
|
Guid extraChatChannel
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Receiver = receiver;
|
Receiver = receiver;
|
||||||
@@ -82,7 +102,11 @@ public partial class Message
|
|||||||
return new Message(0, 0, 0, code, [], content, new SeString(), new SeString());
|
return new Message(0, 0, 0, code, [], content, new SeString(), new SeString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Matches(Dictionary<ChatType, (ChatSource Source, ChatSource Target)> channels, bool allExtraChatChannels, HashSet<Guid> extraChatChannels)
|
public bool Matches(
|
||||||
|
Dictionary<ChatType, (ChatSource Source, ChatSource Target)> channels,
|
||||||
|
bool allExtraChatChannels,
|
||||||
|
HashSet<Guid> extraChatChannels
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if (ExtraChatChannel != Guid.Empty)
|
if (ExtraChatChannel != Guid.Empty)
|
||||||
return allExtraChatChannels || extraChatChannels.Contains(ExtraChatChannel);
|
return allExtraChatChannels || extraChatChannels.Contains(ExtraChatChannel);
|
||||||
@@ -90,16 +114,21 @@ public partial class Message
|
|||||||
var source = (ChatSource)(1 << (int)Code.Source);
|
var source = (ChatSource)(1 << (int)Code.Source);
|
||||||
var target = (ChatSource)(1 << (int)Code.Target);
|
var target = (ChatSource)(1 << (int)Code.Target);
|
||||||
return Code.Type.IsGm()
|
return Code.Type.IsGm()
|
||||||
|| channels.TryGetValue(Code.Type, out var sources)
|
|| channels.TryGetValue(Code.Type, out var sources)
|
||||||
&& (Code.Source is 0 || sources.Source.HasFlag(source) || sources.Target.HasFlag(target));
|
&& (
|
||||||
|
Code.Source is 0
|
||||||
|
|| sources.Source.HasFlag(source)
|
||||||
|
|| sources.Target.HasFlag(target)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GenerateHash()
|
private int GenerateHash()
|
||||||
{
|
{
|
||||||
var hash = SortCodeV2.GetHashCode()
|
var hash =
|
||||||
^ ExtraChatChannel.GetHashCode()
|
SortCodeV2.GetHashCode()
|
||||||
^ string.Join("", Sender.Select(c => c.StringValue())).GetHashCode()
|
^ ExtraChatChannel.GetHashCode()
|
||||||
^ string.Join("", Content.Select(c => c.StringValue())).GetHashCode();
|
^ string.Join("", Sender.Select(c => c.StringValue())).GetHashCode()
|
||||||
|
^ string.Join("", Content.Select(c => c.StringValue())).GetHashCode();
|
||||||
|
|
||||||
if (Plugin.Config.CollapseKeepUniqueLinks)
|
if (Plugin.Config.CollapseKeepUniqueLinks)
|
||||||
{
|
{
|
||||||
@@ -124,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,13 +175,15 @@ public partial class Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
var nextIsAutoTranslate = false;
|
var nextIsAutoTranslate = false;
|
||||||
var checkForEmotes = (Code.IsPlayerMessage() || extraChatChannel != Guid.Empty) && Plugin.Config.ShowEmotes;
|
var checkForEmotes =
|
||||||
|
(Code.IsPlayerMessage() || extraChatChannel != Guid.Empty) && Plugin.Config.ShowEmotes;
|
||||||
foreach (var chunk in oldChunks)
|
foreach (var chunk in oldChunks)
|
||||||
{
|
{
|
||||||
// Use as is if it's not a text chunk, it already has a payload, or is auto translate
|
// Use as is if it's not a text chunk, it already has a payload, or is auto translate
|
||||||
if (chunk is not TextChunk text || chunk.Link != null || nextIsAutoTranslate)
|
if (chunk is not TextChunk text || chunk.Link != null || nextIsAutoTranslate)
|
||||||
{
|
{
|
||||||
nextIsAutoTranslate = chunk is IconChunk { Icon: BitmapFontIcon.AutoTranslateBegin };
|
nextIsAutoTranslate =
|
||||||
|
chunk is IconChunk { Icon: BitmapFontIcon.AutoTranslateBegin };
|
||||||
|
|
||||||
// No need to call AddChunkWithMessage here since the chunk
|
// No need to call AddChunkWithMessage here since the chunk
|
||||||
// already has the Message field set.
|
// already has the Message field set.
|
||||||
@@ -173,15 +204,23 @@ public partial class Message
|
|||||||
var word = wordBuilder.ToString();
|
var word = wordBuilder.ToString();
|
||||||
wordBuilder.Clear();
|
wordBuilder.Clear();
|
||||||
|
|
||||||
|
|
||||||
var wordUsed = false;
|
var wordUsed = false;
|
||||||
var tokenUsed = false;
|
var tokenUsed = false;
|
||||||
|
|
||||||
if (checkForEmotes && EmoteCache.Exists(word) && !Plugin.Config.BlockedEmotes.Contains(word))
|
if (
|
||||||
|
checkForEmotes
|
||||||
|
&& EmoteCache.Exists(word)
|
||||||
|
&& !Plugin.Config.BlockedEmotes.Contains(word)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Add the previous sentence before adding the emote
|
// Add the previous sentence before adding the emote
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk, sentenceBuilder.ToString()));
|
AddChunkWithMessage(text.NewWithStyle(chunk, sentenceBuilder.ToString()));
|
||||||
AddChunkWithMessage(new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), word) { FallbackColour = text.FallbackColour });
|
AddChunkWithMessage(
|
||||||
|
new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), word)
|
||||||
|
{
|
||||||
|
FallbackColour = text.FallbackColour,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
wordUsed = true;
|
wordUsed = true;
|
||||||
sentenceBuilder.Clear();
|
sentenceBuilder.Clear();
|
||||||
@@ -190,15 +229,31 @@ public partial class Message
|
|||||||
if (token.TokenType == Tokenizer.TokenType.UrlString)
|
if (token.TokenType == Tokenizer.TokenType.UrlString)
|
||||||
{
|
{
|
||||||
// Add the previous sentence before adding the url
|
// Add the previous sentence before adding the url
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, sentenceBuilder.Append(!wordUsed ? word : "").ToString()));
|
AddChunkWithMessage(
|
||||||
|
text.NewWithStyle(
|
||||||
|
chunk.Source,
|
||||||
|
chunk.Link,
|
||||||
|
sentenceBuilder.Append(!wordUsed ? word : "").ToString()
|
||||||
|
)
|
||||||
|
);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, UriPayload.ResolveUri(token.Value), token.Value));
|
AddChunkWithMessage(
|
||||||
|
text.NewWithStyle(
|
||||||
|
chunk.Source,
|
||||||
|
UriPayload.ResolveUri(token.Value),
|
||||||
|
token.Value
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (UriFormatException)
|
catch (UriFormatException)
|
||||||
{
|
{
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, token.Value));
|
AddChunkWithMessage(
|
||||||
Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'");
|
text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
|
||||||
|
);
|
||||||
|
Plugin.LogProxy.Debug(
|
||||||
|
$"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
wordUsed = true;
|
wordUsed = true;
|
||||||
@@ -215,7 +270,12 @@ public partial class Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
// End of string reached, we add our leftover
|
// End of string reached, we add our leftover
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk, sentenceBuilder.Append(!wordUsed ? word : "").ToString()));
|
AddChunkWithMessage(
|
||||||
|
text.NewWithStyle(
|
||||||
|
chunk,
|
||||||
|
sentenceBuilder.Append(!wordUsed ? word : "").ToString()
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,20 +341,30 @@ public partial class Message
|
|||||||
< 500_000 => ItemKind.Normal,
|
< 500_000 => ItemKind.Normal,
|
||||||
< 1_000_000 => ItemKind.Collectible,
|
< 1_000_000 => ItemKind.Collectible,
|
||||||
< 2_000_000 => ItemKind.Hq,
|
< 2_000_000 => ItemKind.Hq,
|
||||||
_ => ItemKind.EventItem
|
_ => ItemKind.EventItem,
|
||||||
};
|
};
|
||||||
|
|
||||||
var name = kind != ItemKind.EventItem
|
var name =
|
||||||
? Sheets.ItemSheet.GetRow(item.ItemId).Name.ToString()
|
kind != ItemKind.EventItem
|
||||||
: Sheets.EventItemSheet.GetRow(item.ItemId).Name.ToString();
|
? Sheets.ItemSheet.GetRow(item.ItemId).Name.ToString()
|
||||||
|
: Sheets.EventItemSheet.GetRow(item.ItemId).Name.ToString();
|
||||||
|
|
||||||
var link = new ItemPayload(item.ItemId, kind, $"{SeIconChar.LinkMarker.ToIconChar()}{name}");
|
var link = new ItemPayload(
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, link.DisplayName ?? "Unknown"));
|
item.ItemId,
|
||||||
|
kind,
|
||||||
|
$"{SeIconChar.LinkMarker.ToIconChar()}{name}"
|
||||||
|
);
|
||||||
|
AddChunkWithMessage(
|
||||||
|
text.NewWithStyle(chunk.Source, link, link.DisplayName ?? "Unknown")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else if (split == "<status>")
|
else if (split == "<status>")
|
||||||
{
|
{
|
||||||
var statusId = AgentChatLog.Instance()->ContextStatusId;
|
var statusId = AgentChatLog.Instance()->ContextStatusId;
|
||||||
if (statusId == 0 || !Sheets.StatusSheet.TryGetRow(statusId, out var statusRow))
|
if (
|
||||||
|
statusId == 0
|
||||||
|
|| !Sheets.StatusSheet.TryGetRow(statusId, out var statusRow)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
||||||
continue;
|
continue;
|
||||||
@@ -305,7 +375,7 @@ public partial class Message
|
|||||||
{
|
{
|
||||||
1 => $"{SeIconChar.Buff.ToIconString()}{nameValue}",
|
1 => $"{SeIconChar.Buff.ToIconString()}{nameValue}",
|
||||||
2 => $"{SeIconChar.Debuff.ToIconString()}{nameValue}",
|
2 => $"{SeIconChar.Debuff.ToIconString()}{nameValue}",
|
||||||
_ => nameValue
|
_ => nameValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
var link = new StatusPayload(statusId);
|
var link = new StatusPayload(statusId);
|
||||||
@@ -321,18 +391,32 @@ public partial class Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
var mapCoords = agentMap->FlagMapMarkers[0];
|
var mapCoords = agentMap->FlagMapMarkers[0];
|
||||||
var rawX = (int)(MathF.Round(mapCoords.XFloat, 3, MidpointRounding.AwayFromZero) * 1000);
|
var rawX = (int)(
|
||||||
var rawY = (int)(MathF.Round(mapCoords.YFloat, 3, MidpointRounding.AwayFromZero) * 1000);
|
MathF.Round(mapCoords.XFloat, 3, MidpointRounding.AwayFromZero) * 1000
|
||||||
|
);
|
||||||
|
var rawY = (int)(
|
||||||
|
MathF.Round(mapCoords.YFloat, 3, MidpointRounding.AwayFromZero) * 1000
|
||||||
|
);
|
||||||
|
|
||||||
var link = new MapLinkPayload(mapCoords.TerritoryId, mapCoords.MapId, rawX, rawY);
|
var link = new MapLinkPayload(
|
||||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, $"{SeIconChar.LinkMarker.ToIconChar()}{link.PlaceName} {link.CoordinateString}"));
|
mapCoords.TerritoryId,
|
||||||
|
mapCoords.MapId,
|
||||||
|
rawX,
|
||||||
|
rawY
|
||||||
|
);
|
||||||
|
AddChunkWithMessage(
|
||||||
|
text.NewWithStyle(
|
||||||
|
chunk.Source,
|
||||||
|
link,
|
||||||
|
$"{SeIconChar.LinkMarker.ToIconChar()}{link.PlaceName} {link.CoordinateString}"
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
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}'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+122
-80
@@ -1,9 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HellionChat.Code;
|
|
||||||
using HellionChat.Resources;
|
|
||||||
using HellionChat.Util;
|
|
||||||
using Dalamud.Game.Chat;
|
using Dalamud.Game.Chat;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
@@ -11,9 +8,13 @@ using Dalamud.Hooking;
|
|||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using HellionChat.Code;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
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,21 +23,13 @@ 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; } = [];
|
||||||
private ulong LastContentId { get; set; }
|
private ulong LastContentId { get; set; }
|
||||||
|
|
||||||
// Messages go into the PendingSync queue first, which will be consumed one
|
// PendingSync (main thread) → PendingAsync (worker thread); LinkedList for O(1) Last access
|
||||||
// at a time in the main thread. This is to delay the async processing until
|
|
||||||
// after we've received the content ID from the ContentIdResolver hook.
|
|
||||||
//
|
|
||||||
// After that, the message is enqueued in the PendingAsync queue, which will
|
|
||||||
// be consumed in a separate thread and perform more processing (emotes,
|
|
||||||
// URLs) as well as inserting the message into the database.
|
|
||||||
// LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last
|
|
||||||
// every hook call. Queue<T>.Last() is the LINQ extension and walks the
|
|
||||||
// whole queue (O(n)); LinkedList<T>.Last is an O(1) node reference.
|
|
||||||
private LinkedList<PendingMessage> PendingSync { get; } = [];
|
private LinkedList<PendingMessage> PendingSync { get; } = [];
|
||||||
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
|
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
|
||||||
private readonly Thread PendingMessageThread;
|
private readonly Thread PendingMessageThread;
|
||||||
@@ -53,23 +46,39 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs hook. Fires after a fully processed
|
// Auto-Tell-Tabs hook: fires after a message is processed and stored, allowing
|
||||||
// message has been routed to all matching persistent tabs and stored
|
// AutoTellTabsService to spawn or refresh temp tabs without coupling.
|
||||||
// in the database. The AutoTellTabsService subscribes to spawn or
|
|
||||||
// refresh temp tabs without having to wedge itself into ProcessMessage
|
|
||||||
// directly.
|
|
||||||
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(() => ProcessPendingMessages(PendingThreadCancellationToken.Token));
|
PendingMessageThread = new Thread(() =>
|
||||||
|
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
};
|
||||||
PendingMessageThread.Start();
|
PendingMessageThread.Start();
|
||||||
|
|
||||||
ContentIdResolverHook = Plugin.GameInteropProvider.HookFromAddress<RaptureLogModule.Delegates.AddMsgSourceEntry>(RaptureLogModule.MemberFunctionPointers.AddMsgSourceEntry, ContentIdResolver);
|
ContentIdResolverHook =
|
||||||
|
Plugin.GameInteropProvider.HookFromAddress<RaptureLogModule.Delegates.AddMsgSourceEntry>(
|
||||||
|
RaptureLogModule.MemberFunctionPointers.AddMsgSourceEntry,
|
||||||
|
ContentIdResolver
|
||||||
|
);
|
||||||
ContentIdResolverHook.Enable();
|
ContentIdResolverHook.Enable();
|
||||||
|
|
||||||
Plugin.ChatGui.ChatMessageUnhandled += ChatMessage;
|
Plugin.ChatGui.ChatMessageUnhandled += ChatMessage;
|
||||||
@@ -85,19 +94,20 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
Plugin.ChatGui.ChatMessageUnhandled -= ChatMessage;
|
Plugin.ChatGui.ChatMessageUnhandled -= ChatMessage;
|
||||||
|
|
||||||
await PendingThreadCancellationToken.CancelAsync();
|
await PendingThreadCancellationToken.CancelAsync();
|
||||||
var timeout = 10_000; // 10s
|
|
||||||
while (timeout > 0)
|
|
||||||
{
|
|
||||||
if (!PendingMessageThread.IsAlive)
|
|
||||||
break;
|
|
||||||
|
|
||||||
timeout -= 100;
|
// 10s cooperative window; Thread.Abort is gone since .NET 5, so a
|
||||||
|
// stuck worker has to ride out the next AppDomain unload.
|
||||||
|
var deadline = TimeSpan.FromSeconds(10);
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
while (stopwatch.Elapsed < deadline && PendingMessageThread.IsAlive)
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
Plugin.Log.Debug("Sleeping because PendingMessageThread thread still alive");
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancellationTokenSource owns an unmanaged WaitHandle; dispose after the
|
if (PendingMessageThread.IsAlive)
|
||||||
// worker thread has drained, otherwise it leaks across plugin reloads.
|
_logger.LogWarning(
|
||||||
|
"PendingMessageThread did not observe cancellation within 10s. "
|
||||||
|
+ "Worker remains on background thread; next plugin reload releases it."
|
||||||
|
);
|
||||||
|
|
||||||
PendingThreadCancellationToken.Dispose();
|
PendingThreadCancellationToken.Dispose();
|
||||||
|
|
||||||
Store.Dispose();
|
Store.Dispose();
|
||||||
@@ -139,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
|
||||||
@@ -151,12 +161,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
|
|
||||||
internal void ClearAllTabs()
|
internal void ClearAllTabs()
|
||||||
{
|
{
|
||||||
// Hellion Chat — TempTabs haben keine DB-Persistenz (session-only,
|
// TempTabs are session-only (not persisted); exclude them to preserve Tell history
|
||||||
// direkt vom AutoTellTabsService befüllt). Ein Clear+Refilter würde
|
|
||||||
// sie leer hinterlassen weil FilterAllTabs nichts aus der DB
|
|
||||||
// findet — Tells sind oft durch Privacy-Filter blockiert oder
|
|
||||||
// schlicht session-flüchtig. TempTabs vom Clear-Pfad ausschließen
|
|
||||||
// damit Settings-Save den Tell-Verlauf nicht zerstört.
|
|
||||||
foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
|
foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
|
||||||
tab.Clear();
|
tab.Clear();
|
||||||
}
|
}
|
||||||
@@ -169,32 +174,32 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
|
|
||||||
using var messages = Store.GetMostRecentMessages(CurrentContentId, since);
|
using var messages = Store.GetMostRecentMessages(CurrentContentId, since);
|
||||||
|
|
||||||
// We store the pending messages to be added to the chat log in a
|
// TempTabs are excluded; they maintain live state from AutoTellTabsService
|
||||||
// temporary list, and apply them all at once after filtering.
|
var pendingTabs = Plugin
|
||||||
// TempTabs werden ausgeschlossen — sie bleiben live-state aus dem
|
.Config.Tabs.Where(t => !t.IsTempTab)
|
||||||
// AutoTellTabsService, ein DB-Refilter würde sie nur partial
|
.Select(tab => (tab, new List<Message>()))
|
||||||
// wiederherstellen falls Tells in DB liegen, oder leer lassen wenn
|
.ToList();
|
||||||
// Privacy-Filter sie blockiert hat.
|
|
||||||
var pendingTabs = Plugin.Config.Tabs.Where(t => !t.IsTempTab).Select(tab => (tab, new List<Message>())).ToList();
|
|
||||||
foreach (var message in messages)
|
foreach (var message in messages)
|
||||||
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
|
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
|
||||||
pendingMessages.Add(message);
|
pendingMessages.Add(message);
|
||||||
|
|
||||||
// Apply the messages to the chat log in one go.
|
// Apply messages to chat log all at once.
|
||||||
foreach (var (tab, pendingMessages) in pendingTabs)
|
foreach (var (tab, pendingMessages) in pendingTabs)
|
||||||
tab.Messages.AddSortPrune(pendingMessages, MessageDisplayLimit);
|
tab.Messages.AddSortPrune(pendingMessages, MessageDisplayLimit);
|
||||||
|
|
||||||
if (!messages.DidError) return;
|
if (!messages.DidError)
|
||||||
|
return;
|
||||||
|
|
||||||
WrapperUtil.AddNotification(Language.LoadMessages_Error, NotificationType.Error);
|
WrapperUtil.AddNotification(Language.LoadMessages_Error, NotificationType.Error);
|
||||||
|
|
||||||
// Mark the failed messages as deleted so we don't try to load them
|
// Mark failed messages as deleted to prevent retry attempts
|
||||||
// again.
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,14 +215,18 @@ 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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public (SeString? Sender, SeString? Message) LastMessage = (null, null);
|
public (SeString? Sender, SeString? Message) LastMessage = (null, null);
|
||||||
|
|
||||||
private void ChatMessage(IChatMessage message)
|
private void ChatMessage(IChatMessage message)
|
||||||
{
|
{
|
||||||
LastMessage = (message.Sender, message.Message);
|
LastMessage = (message.Sender, message.Message);
|
||||||
@@ -236,21 +245,29 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// Update colour codes.
|
// Update colour codes.
|
||||||
GlobalParametersCache.Refresh();
|
GlobalParametersCache.Refresh();
|
||||||
|
|
||||||
// We delay messages to be handed off to the async processing thread
|
// Delay to next tick to get content ID from ContentIdResolver hook
|
||||||
// in the next tick, otherwise we can't get the content ID from the hook
|
|
||||||
// below.
|
|
||||||
PendingSync.AddLast(pendingMessage);
|
PendingSync.AddLast(pendingMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This hook is called immediately after receiving a message with the
|
private unsafe void ContentIdResolver(
|
||||||
// message's content ID. If multiple messages are received in the same tick,
|
RaptureLogModule* agent,
|
||||||
// this will be called for each message immediately after ChatMessage is
|
ulong contentId,
|
||||||
// called for each message.
|
ulong accountId,
|
||||||
private unsafe void ContentIdResolver(RaptureLogModule* agent, ulong contentId, ulong accountId, int messageIndex, ushort worldId, ushort chatType)
|
int messageIndex,
|
||||||
|
ushort worldId,
|
||||||
|
ushort chatType
|
||||||
|
)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
|
ContentIdResolverHook?.Original(
|
||||||
|
agent,
|
||||||
|
contentId,
|
||||||
|
accountId,
|
||||||
|
messageIndex,
|
||||||
|
worldId,
|
||||||
|
chatType
|
||||||
|
);
|
||||||
if (PendingSync.Last is not { } last)
|
if (PendingSync.Last is not { } last)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -259,13 +276,17 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error in ContentIdResolver");
|
_logger.LogError(ex, "Error in ContentIdResolver");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessMessage(PendingMessage pendingMessage)
|
private void ProcessMessage(PendingMessage pendingMessage)
|
||||||
{
|
{
|
||||||
var chatCode = new ChatCode(pendingMessage.LogKind, pendingMessage.SourceKind, pendingMessage.TargetKind);
|
var chatCode = new ChatCode(
|
||||||
|
pendingMessage.LogKind,
|
||||||
|
pendingMessage.SourceKind,
|
||||||
|
pendingMessage.TargetKind
|
||||||
|
);
|
||||||
|
|
||||||
NameFormatting? formatting = null;
|
NameFormatting? formatting = null;
|
||||||
if (pendingMessage.Sender.Payloads.Count > 0)
|
if (pendingMessage.Sender.Payloads.Count > 0)
|
||||||
@@ -274,13 +295,36 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
var senderChunks = new List<Chunk>();
|
var senderChunks = new List<Chunk>();
|
||||||
if (formatting is { IsPresent: true })
|
if (formatting is { IsPresent: true })
|
||||||
{
|
{
|
||||||
senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.Before) { FallbackColour = chatCode.Type });
|
senderChunks.Add(
|
||||||
senderChunks.AddRange(ChunkUtil.ToChunks(pendingMessage.Sender, ChunkSource.Sender, chatCode.Type));
|
new TextChunk(ChunkSource.None, null, formatting.Before)
|
||||||
senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.After) { FallbackColour = chatCode.Type });
|
{
|
||||||
|
FallbackColour = chatCode.Type,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
senderChunks.AddRange(
|
||||||
|
ChunkUtil.ToChunks(pendingMessage.Sender, ChunkSource.Sender, chatCode.Type)
|
||||||
|
);
|
||||||
|
senderChunks.Add(
|
||||||
|
new TextChunk(ChunkSource.None, null, formatting.After)
|
||||||
|
{
|
||||||
|
FallbackColour = chatCode.Type,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentChunks = ChunkUtil.ToChunks(pendingMessage.Content, ChunkSource.Content, chatCode.Type).ToList();
|
var contentChunks = ChunkUtil
|
||||||
var message = new Message(CurrentContentId, pendingMessage.ContentId, pendingMessage.AccountId, chatCode, senderChunks, contentChunks, pendingMessage.Sender, pendingMessage.Content);
|
.ToChunks(pendingMessage.Content, ChunkSource.Content, chatCode.Type)
|
||||||
|
.ToList();
|
||||||
|
var message = new Message(
|
||||||
|
CurrentContentId,
|
||||||
|
pendingMessage.ContentId,
|
||||||
|
pendingMessage.AccountId,
|
||||||
|
chatCode,
|
||||||
|
senderChunks,
|
||||||
|
contentChunks,
|
||||||
|
pendingMessage.Sender,
|
||||||
|
pendingMessage.Content
|
||||||
|
);
|
||||||
|
|
||||||
if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle())
|
if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle())
|
||||||
Store.UpsertMessage(message);
|
Store.UpsertMessage(message);
|
||||||
@@ -288,7 +332,9 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
var currentMatches = Plugin.CurrentTab.Matches(message);
|
var currentMatches = Plugin.CurrentTab.Matches(message);
|
||||||
foreach (var tab in Plugin.Config.Tabs)
|
foreach (var tab in Plugin.Config.Tabs)
|
||||||
{
|
{
|
||||||
var unread = !(tab.UnreadMode == UnreadMode.Unseen && Plugin.CurrentTab != tab && currentMatches);
|
var unread = !(
|
||||||
|
tab.UnreadMode == UnreadMode.Unseen && Plugin.CurrentTab != tab && currentMatches
|
||||||
|
);
|
||||||
|
|
||||||
if (tab.Matches(message))
|
if (tab.Matches(message))
|
||||||
tab.AddMessage(message, unread);
|
tab.AddMessage(message, unread);
|
||||||
@@ -305,16 +351,12 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
|
|
||||||
internal static NameFormatting Empty()
|
internal static NameFormatting Empty()
|
||||||
{
|
{
|
||||||
return new NameFormatting { IsPresent = false, };
|
return new NameFormatting { IsPresent = false };
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static NameFormatting Of(string before, string after)
|
internal static NameFormatting Of(string before, string after)
|
||||||
{
|
{
|
||||||
return new NameFormatting
|
return new NameFormatting { Before = before, After = after };
|
||||||
{
|
|
||||||
Before = before,
|
|
||||||
After = after,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +391,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
var after = formats
|
var after = formats
|
||||||
.GetRange(firstStringParam + 1, secondStringParam - firstStringParam)
|
.GetRange(firstStringParam + 1, secondStringParam - firstStringParam)
|
||||||
.Where(payload => payload.Type == ReadOnlySePayloadType.Text)
|
.Where(payload => payload.Type == ReadOnlySePayloadType.Text)
|
||||||
.Select(text => Encoding.UTF8.GetString(text.Body.Span)); // Can't use `ToString()` as it defaults to macro
|
.Select(text => Encoding.UTF8.GetString(text.Body.Span));
|
||||||
|
|
||||||
var nameFormatting = NameFormatting.Of(string.Join("", before), string.Join("", after));
|
var nameFormatting = NameFormatting.Of(string.Join("", before), string.Join("", after));
|
||||||
Formats[type] = nameFormatting;
|
Formats[type] = nameFormatting;
|
||||||
|
|||||||
+811
-424
File diff suppressed because it is too large
Load Diff
+190
-54
@@ -1,8 +1,5 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using HellionChat.Code;
|
using Dalamud.Bindings.ImGui;
|
||||||
using HellionChat.Resources;
|
|
||||||
using HellionChat.Ui;
|
|
||||||
using HellionChat.Util;
|
|
||||||
using Dalamud.Game.Addon.Lifecycle;
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
@@ -18,11 +15,15 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using Dalamud.Bindings.ImGui;
|
using HellionChat.Code;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Ui;
|
||||||
|
using HellionChat.Util;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Action = System.Action;
|
using Action = System.Action;
|
||||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
|
||||||
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
||||||
|
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||||
|
|
||||||
namespace HellionChat;
|
namespace HellionChat;
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -107,7 +111,9 @@ public sealed class PayloadHandler
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
var contentId = chunk.Message?.ContentId ?? 0;
|
var contentId = chunk.Message?.ContentId ?? 0;
|
||||||
var sender = chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload) as PlayerPayload;
|
var sender =
|
||||||
|
chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload)
|
||||||
|
as PlayerPayload;
|
||||||
|
|
||||||
using var menu = ImRaii.Menu(Language.Context_Integrations);
|
using var menu = ImRaii.Menu(Language.Context_Integrations);
|
||||||
if (!menu.Success)
|
if (!menu.Success)
|
||||||
@@ -118,17 +124,27 @@ public sealed class PayloadHandler
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LogWindow.Plugin.Ipc.Invoke(id, sender, contentId, payload, chunk.Message?.SenderSource, chunk.Message?.ContentSource);
|
LogWindow.Plugin.Ipc.Invoke(
|
||||||
|
id,
|
||||||
|
sender,
|
||||||
|
contentId,
|
||||||
|
payload,
|
||||||
|
chunk.Message?.SenderSource,
|
||||||
|
chunk.Message?.ContentSource
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Plugin.Log.Error(ex, "Error executing integration");
|
_logger.LogError(ex, "Error executing integration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cursor == ImGui.GetCursorPos())
|
if (cursor == ImGui.GetCursorPos())
|
||||||
{
|
{
|
||||||
using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]);
|
using var pushedColor = ImRaii.PushColor(
|
||||||
|
ImGuiCol.Text,
|
||||||
|
ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]
|
||||||
|
);
|
||||||
ImGui.Text("No integrations available");
|
ImGui.Text("No integrations available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,10 +184,16 @@ public sealed class PayloadHandler
|
|||||||
if (message.Sender.Count > 0 && ImGui.Selectable(Language.Context_CopyContent))
|
if (message.Sender.Count > 0 && ImGui.Selectable(Language.Context_CopyContent))
|
||||||
{
|
{
|
||||||
ImGui.SetClipboardText(StringifyMessage(message));
|
ImGui.SetClipboardText(StringifyMessage(message));
|
||||||
WrapperUtil.AddNotification(Language.Context_CopyContentSuccess, NotificationType.Info);
|
WrapperUtil.AddNotification(
|
||||||
|
Language.Context_CopyContentSuccess,
|
||||||
|
NotificationType.Info
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
|
using var pushedColor = ImRaii.PushColor(
|
||||||
|
ImGuiCol.Text,
|
||||||
|
ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]
|
||||||
|
);
|
||||||
ImGui.TextUnformatted(message.Code.Type.Name());
|
ImGui.TextUnformatted(message.Code.Type.Name());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +206,8 @@ public sealed class PayloadHandler
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|
||||||
var chunks = withSender ? message.Sender.Concat(message.Content) : message.Content;
|
var chunks = withSender ? message.Sender.Concat(message.Content) : message.Content;
|
||||||
return chunks.Where(chunk => chunk is TextChunk)
|
return chunks
|
||||||
|
.Where(chunk => chunk is TextChunk)
|
||||||
.Cast<TextChunk>()
|
.Cast<TextChunk>()
|
||||||
.Select(text => text.Content)
|
.Select(text => text.Content)
|
||||||
.Aggregate(string.Concat);
|
.Aggregate(string.Concat);
|
||||||
@@ -255,7 +278,10 @@ public sealed class PayloadHandler
|
|||||||
public unsafe void MoveTooltip(AddonEvent type, AddonArgs args)
|
public unsafe void MoveTooltip(AddonEvent type, AddonArgs args)
|
||||||
{
|
{
|
||||||
// Only move if the user has the "Next to Cursor" option selected
|
// Only move if the user has the "Next to Cursor" option selected
|
||||||
if (!Plugin.GameConfig.TryGet(UiControlOption.DetailTrackingType, out uint selected) || selected != 0)
|
if (
|
||||||
|
!Plugin.GameConfig.TryGet(UiControlOption.DetailTrackingType, out uint selected)
|
||||||
|
|| selected != 0
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (LogWindow.LastViewport != ImGuiHelpers.MainViewport.Handle)
|
if (LogWindow.LastViewport != ImGuiHelpers.MainViewport.Handle)
|
||||||
@@ -274,7 +300,10 @@ public sealed class PayloadHandler
|
|||||||
|
|
||||||
var component = atkBase->WindowNode->AtkResNode;
|
var component = atkBase->WindowNode->AtkResNode;
|
||||||
var atkPos = new Vector2(component.ScreenX, component.ScreenY);
|
var atkPos = new Vector2(component.ScreenX, component.ScreenY);
|
||||||
var atkSize = new Vector2(component.GetWidth() * component.ScaleX, component.GetHeight() * component.GetScaleY());
|
var atkSize = new Vector2(
|
||||||
|
component.GetWidth() * component.ScaleX,
|
||||||
|
component.GetHeight() * component.GetScaleY()
|
||||||
|
);
|
||||||
|
|
||||||
var chatRect = new MathUtil.Rectangle(LogWindow.LastWindowPos, LogWindow.LastWindowSize);
|
var chatRect = new MathUtil.Rectangle(LogWindow.LastWindowPos, LogWindow.LastWindowSize);
|
||||||
var addonRect = new MathUtil.Rectangle(atkPos, atkSize);
|
var addonRect = new MathUtil.Rectangle(atkPos, atkSize);
|
||||||
@@ -302,7 +331,7 @@ public sealed class PayloadHandler
|
|||||||
|
|
||||||
if (!chatRect.HasOverlap(addonRect))
|
if (!chatRect.HasOverlap(addonRect))
|
||||||
{
|
{
|
||||||
atkBase->SetPosition((short) addonRect.X, (short) addonRect.Y);
|
atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +349,7 @@ public sealed class PayloadHandler
|
|||||||
|
|
||||||
if (!chatRect.HasOverlap(addonRect))
|
if (!chatRect.HasOverlap(addonRect))
|
||||||
{
|
{
|
||||||
atkBase->SetPosition((short) addonRect.X, (short) addonRect.Y);
|
atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +358,7 @@ public sealed class PayloadHandler
|
|||||||
var y = Math.Clamp(chatRect.SizeY - atkSize.Y, 0, float.MaxValue);
|
var y = Math.Clamp(chatRect.SizeY - atkSize.Y, 0, float.MaxValue);
|
||||||
y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom
|
y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom
|
||||||
|
|
||||||
atkBase->SetPosition((short) x, (short) y);
|
atkBase->SetPosition((short)x, (short)y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private const float MaxInlineIconSize = 32f;
|
private const float MaxInlineIconSize = 32f;
|
||||||
@@ -339,20 +368,25 @@ public sealed class PayloadHandler
|
|||||||
if (icon.Size.X <= 0 || icon.Size.Y <= 0)
|
if (icon.Size.X <= 0 || icon.Size.Y <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var width = (float) icon.Size.X;
|
var width = (float)icon.Size.X;
|
||||||
var height = (float) icon.Size.Y;
|
var height = (float)icon.Size.Y;
|
||||||
var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height));
|
var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height));
|
||||||
var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale);
|
var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale);
|
||||||
|
|
||||||
var cursor = ImGui.GetCursorPos();
|
var cursor = ImGui.GetCursorPos();
|
||||||
ImGui.Image(icon.Handle, size);
|
ImGui.Image(icon.Handle, size);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
|
ImGui.SetCursorPos(
|
||||||
|
cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HoverStatus(StatusPayload status)
|
private void HoverStatus(StatusPayload status)
|
||||||
{
|
{
|
||||||
if (Plugin.TextureProvider.GetFromGameIcon(status.Status.Value.Icon).GetWrapOrDefault() is { } icon)
|
if (
|
||||||
|
Plugin.TextureProvider.GetFromGameIcon(status.Status.Value.Icon).GetWrapOrDefault() is
|
||||||
|
{ } icon
|
||||||
|
)
|
||||||
InlineIcon(icon);
|
InlineIcon(icon);
|
||||||
|
|
||||||
var builder = new SeStringBuilder();
|
var builder = new SeStringBuilder();
|
||||||
@@ -374,7 +408,11 @@ public sealed class PayloadHandler
|
|||||||
LogWindow.DrawChunks(name.ToList());
|
LogWindow.DrawChunks(name.ToList());
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
var desc = ChunkUtil.ToChunks(status.Status.Value.Description.ToDalamudString(), ChunkSource.None, null);
|
var desc = ChunkUtil.ToChunks(
|
||||||
|
status.Status.Value.Description.ToDalamudString(),
|
||||||
|
ChunkSource.None,
|
||||||
|
null
|
||||||
|
);
|
||||||
LogWindow.DrawChunks(desc.ToList());
|
LogWindow.DrawChunks(desc.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,14 +427,23 @@ public sealed class PayloadHandler
|
|||||||
if (!item.Item.TryGetValue(out Item resolvedItem))
|
if (!item.Item.TryGetValue(out Item resolvedItem))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(resolvedItem.Icon, item.IsHQ)).GetWrapOrDefault() is { } icon)
|
if (
|
||||||
|
Plugin
|
||||||
|
.TextureProvider.GetFromGameIcon(new GameIconLookup(resolvedItem.Icon, item.IsHQ))
|
||||||
|
.GetWrapOrDefault() is
|
||||||
|
{ } icon
|
||||||
|
)
|
||||||
InlineIcon(icon);
|
InlineIcon(icon);
|
||||||
|
|
||||||
var name = ChunkUtil.ToChunks(resolvedItem.Name.ToDalamudString(), ChunkSource.None, null);
|
var name = ChunkUtil.ToChunks(resolvedItem.Name.ToDalamudString(), ChunkSource.None, null);
|
||||||
LogWindow.DrawChunks(name.ToList());
|
LogWindow.DrawChunks(name.ToList());
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
var desc = ChunkUtil.ToChunks(resolvedItem.Description.ToDalamudString(), ChunkSource.None, null);
|
var desc = ChunkUtil.ToChunks(
|
||||||
|
resolvedItem.Description.ToDalamudString(),
|
||||||
|
ChunkSource.None,
|
||||||
|
null
|
||||||
|
);
|
||||||
LogWindow.DrawChunks(desc.ToList());
|
LogWindow.DrawChunks(desc.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +452,12 @@ public sealed class PayloadHandler
|
|||||||
if (!Sheets.EventItemSheet.TryGetRow(payload.RawItemId, out var itemRow))
|
if (!Sheets.EventItemSheet.TryGetRow(payload.RawItemId, out var itemRow))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon)).GetWrapOrDefault() is { } icon)
|
if (
|
||||||
|
Plugin
|
||||||
|
.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon))
|
||||||
|
.GetWrapOrDefault() is
|
||||||
|
{ } icon
|
||||||
|
)
|
||||||
InlineIcon(icon);
|
InlineIcon(icon);
|
||||||
|
|
||||||
var name = ChunkUtil.ToChunks(itemRow.Name.ToDalamudString(), ChunkSource.None, null);
|
var name = ChunkUtil.ToChunks(itemRow.Name.ToDalamudString(), ChunkSource.None, null);
|
||||||
@@ -415,7 +467,11 @@ public sealed class PayloadHandler
|
|||||||
if (!Sheets.EventItemHelpSheet.TryGetRow(payload.RawItemId, out var itemHelpRow))
|
if (!Sheets.EventItemHelpSheet.TryGetRow(payload.RawItemId, out var itemHelpRow))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LogWindow.DrawChunks(ChunkUtil.ToChunks(itemHelpRow.Description.ToDalamudString(), ChunkSource.None, null).ToList());
|
LogWindow.DrawChunks(
|
||||||
|
ChunkUtil
|
||||||
|
.ToChunks(itemHelpRow.Description.ToDalamudString(), ChunkSource.None, null)
|
||||||
|
.ToList()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HoverUri(UriPayload uri)
|
private void HoverUri(UriPayload uri)
|
||||||
@@ -438,7 +494,10 @@ public sealed class PayloadHandler
|
|||||||
ClickLinkPayload(chunk, payload, link);
|
ClickLinkPayload(chunk, payload, link);
|
||||||
break;
|
break;
|
||||||
case DalamudPartyFinderPayload pf:
|
case DalamudPartyFinderPayload pf:
|
||||||
if (pf.LinkType == DalamudPartyFinderPayload.PartyFinderLinkType.PartyFinderNotification)
|
if (
|
||||||
|
pf.LinkType
|
||||||
|
== DalamudPartyFinderPayload.PartyFinderLinkType.PartyFinderNotification
|
||||||
|
)
|
||||||
GameFunctions.GameFunctions.OpenPartyFinder();
|
GameFunctions.GameFunctions.OpenPartyFinder();
|
||||||
else
|
else
|
||||||
GameFunctions.GameFunctions.OpenPartyFinder(pf.ListingId);
|
GameFunctions.GameFunctions.OpenPartyFinder(pf.ListingId);
|
||||||
@@ -473,9 +532,14 @@ public sealed class PayloadHandler
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var payloads = source.Payloads.Skip(start).Take(end - start + 1).ToList();
|
var payloads = source.Payloads.Skip(start).Take(end - start + 1).ToList();
|
||||||
if (!Plugin.ChatGui.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
|
if (
|
||||||
|
!Plugin.ChatGui.RegisteredLinkHandlers.TryGetValue(
|
||||||
|
(link.Plugin, link.CommandId),
|
||||||
|
out var value
|
||||||
|
)
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Could not find DalamudLinkHandlers");
|
_logger.LogWarning("Could not find DalamudLinkHandlers");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +572,12 @@ public sealed class PayloadHandler
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var hq = payload.Kind == ItemKind.Hq;
|
var hq = payload.Kind == ItemKind.Hq;
|
||||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon, hq)).GetWrapOrDefault() is { } icon)
|
if (
|
||||||
|
Plugin
|
||||||
|
.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon, hq))
|
||||||
|
.GetWrapOrDefault() is
|
||||||
|
{ } icon
|
||||||
|
)
|
||||||
InlineIcon(icon);
|
InlineIcon(icon);
|
||||||
|
|
||||||
var name = itemRow.Name.ToDalamudString();
|
var name = itemRow.Name.ToDalamudString();
|
||||||
@@ -554,10 +623,18 @@ public sealed class PayloadHandler
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var item = Sheets.EventItemSheet.GetRow(payload.ItemId);
|
var item = Sheets.EventItemSheet.GetRow(payload.ItemId);
|
||||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(item.Icon)).GetWrapOrDefault() is { } icon)
|
if (
|
||||||
|
Plugin
|
||||||
|
.TextureProvider.GetFromGameIcon(new GameIconLookup(item.Icon))
|
||||||
|
.GetWrapOrDefault() is
|
||||||
|
{ } icon
|
||||||
|
)
|
||||||
InlineIcon(icon);
|
InlineIcon(icon);
|
||||||
|
|
||||||
LogWindow.DrawChunks(ChunkUtil.ToChunks(item.Name.ToDalamudString(), ChunkSource.None, null).ToList(), false);
|
LogWindow.DrawChunks(
|
||||||
|
ChunkUtil.ToChunks(item.Name.ToDalamudString(), ChunkSource.None, null).ToList(),
|
||||||
|
false
|
||||||
|
);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
var realItemId = payload.RawItemId;
|
var realItemId = payload.RawItemId;
|
||||||
@@ -585,7 +662,7 @@ public sealed class PayloadHandler
|
|||||||
{
|
{
|
||||||
name.AddRange([
|
name.AddRange([
|
||||||
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
|
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
|
||||||
new TextChunk(ChunkSource.None, null, world.Value.Name.ExtractText())
|
new TextChunk(ChunkSource.None, null, world.Value.Name.ExtractText()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,7 +683,15 @@ public sealed class PayloadHandler
|
|||||||
}
|
}
|
||||||
else if (validContentId)
|
else if (validContentId)
|
||||||
{
|
{
|
||||||
LogWindow.Plugin.Functions.Chat.SetEurekaTellChannel(player.PlayerName, world.Value.Name.ToString(), (ushort) world.RowId, 0, chunk.Message!.ContentId, 0, false);
|
LogWindow.Plugin.Functions.Chat.SetEurekaTellChannel(
|
||||||
|
player.PlayerName,
|
||||||
|
world.Value.Name.ToString(),
|
||||||
|
(ushort)world.RowId,
|
||||||
|
0,
|
||||||
|
chunk.Message!.ContentId,
|
||||||
|
0,
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LogWindow.Activate = true;
|
LogWindow.Activate = true;
|
||||||
@@ -615,12 +700,18 @@ public sealed class PayloadHandler
|
|||||||
if (world.Value.IsPublic)
|
if (world.Value.IsPublic)
|
||||||
{
|
{
|
||||||
var party = Plugin.PartyList;
|
var party = Plugin.PartyList;
|
||||||
var leader = party[(int) party.PartyLeaderIndex]?.ContentId;
|
var leader = party[(int)party.PartyLeaderIndex]?.ContentId;
|
||||||
var isLeader = party.Length == 0 || Plugin.PlayerState.ContentId == leader;
|
var isLeader = party.Length == 0 || Plugin.PlayerState.ContentId == leader;
|
||||||
var member = party.FirstOrDefault(member => member.Name.TextValue == player.PlayerName && member.World.RowId == world.RowId);
|
var member = party.FirstOrDefault(member =>
|
||||||
|
member.Name.TextValue == player.PlayerName && member.World.RowId == world.RowId
|
||||||
|
);
|
||||||
var isInParty = member != null;
|
var isInParty = member != null;
|
||||||
var inInstance = GameFunctions.GameFunctions.IsInInstance();
|
var inInstance = GameFunctions.GameFunctions.IsInInstance();
|
||||||
var inPartyInstance = Sheets.TerritorySheet.GetRow(Plugin.ClientState.TerritoryType).TerritoryIntendedUse.RowId is (41 or 47 or 48 or 52 or 53 or 61);
|
var inPartyInstance =
|
||||||
|
Sheets
|
||||||
|
.TerritorySheet.GetRow(Plugin.ClientState.TerritoryType)
|
||||||
|
.TerritoryIntendedUse.RowId
|
||||||
|
is (41 or 47 or 48 or 52 or 53 or 61);
|
||||||
if (isLeader)
|
if (isLeader)
|
||||||
{
|
{
|
||||||
if (!isInParty)
|
if (!isInParty)
|
||||||
@@ -636,10 +727,20 @@ public sealed class PayloadHandler
|
|||||||
if (menu.Success)
|
if (menu.Success)
|
||||||
{
|
{
|
||||||
if (ImGui.Selectable(Language.Context_InviteToParty_SameWorld))
|
if (ImGui.Selectable(Language.Context_InviteToParty_SameWorld))
|
||||||
GameFunctions.Party.InviteSameWorld(player.PlayerName, (ushort)world.RowId, chunk.Message?.ContentId ?? 0);
|
GameFunctions.Party.InviteSameWorld(
|
||||||
|
player.PlayerName,
|
||||||
|
(ushort)world.RowId,
|
||||||
|
chunk.Message?.ContentId ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
if (validContentId && ImGui.Selectable(Language.Context_InviteToParty_DifferentWorld))
|
if (
|
||||||
GameFunctions.Party.InviteOtherWorld(chunk.Message!.ContentId, (ushort)world.RowId);
|
validContentId
|
||||||
|
&& ImGui.Selectable(Language.Context_InviteToParty_DifferentWorld)
|
||||||
|
)
|
||||||
|
GameFunctions.Party.InviteOtherWorld(
|
||||||
|
chunk.Message!.ContentId,
|
||||||
|
(ushort)world.RowId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,23 +755,41 @@ public sealed class PayloadHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isFriend = GameFunctions.GameFunctions.GetFriends().Any(friend => friend.NameString == player.PlayerName && friend.HomeWorld == world.RowId);
|
var isFriend = GameFunctions
|
||||||
|
.GameFunctions.GetFriends()
|
||||||
|
.Any(friend =>
|
||||||
|
friend.NameString == player.PlayerName && friend.HomeWorld == world.RowId
|
||||||
|
);
|
||||||
if (!isFriend && ImGui.Selectable(Language.Context_SendFriendRequest))
|
if (!isFriend && ImGui.Selectable(Language.Context_SendFriendRequest))
|
||||||
LogWindow.Plugin.Functions.SendFriendRequest(player.PlayerName, (ushort) world.RowId);
|
LogWindow.Plugin.Functions.SendFriendRequest(
|
||||||
|
player.PlayerName,
|
||||||
|
(ushort)world.RowId
|
||||||
|
);
|
||||||
|
|
||||||
using (var menuBlockFunctions = ImRaii.Menu(Language.Context_BlockFunctions))
|
using (var menuBlockFunctions = ImRaii.Menu(Language.Context_BlockFunctions))
|
||||||
{
|
{
|
||||||
if (menuBlockFunctions.Success)
|
if (menuBlockFunctions.Success)
|
||||||
{
|
{
|
||||||
if (ImGui.Selectable(Language.Context_AddToBlacklist))
|
if (ImGui.Selectable(Language.Context_AddToBlacklist))
|
||||||
LogWindow.Plugin.Functions.AddToBlacklist(player.PlayerName, (ushort)world.RowId);
|
LogWindow.Plugin.Functions.AddToBlacklist(
|
||||||
|
player.PlayerName,
|
||||||
|
(ushort)world.RowId
|
||||||
|
);
|
||||||
|
|
||||||
if (chunk.Message != null)
|
if (chunk.Message != null)
|
||||||
{
|
{
|
||||||
var message = chunk.Message;
|
var message = chunk.Message;
|
||||||
|
|
||||||
if (message.AccountId != 0 && ImGui.Selectable(Language.Context_AddToMuteList))
|
if (
|
||||||
LogWindow.Plugin.Functions.AddToMuteList(message.AccountId, message.ContentId, player.PlayerName, (short) world.RowId);
|
message.AccountId != 0
|
||||||
|
&& ImGui.Selectable(Language.Context_AddToMuteList)
|
||||||
|
)
|
||||||
|
LogWindow.Plugin.Functions.AddToMuteList(
|
||||||
|
message.AccountId,
|
||||||
|
message.ContentId,
|
||||||
|
player.PlayerName,
|
||||||
|
(short)world.RowId
|
||||||
|
);
|
||||||
|
|
||||||
if (ImGui.Selectable(Language.Context_AddToTermsFilter))
|
if (ImGui.Selectable(Language.Context_AddToTermsFilter))
|
||||||
LogWindow.Plugin.Functions.AddToTermsList(message.ContentSource);
|
LogWindow.Plugin.Functions.AddToTermsList(message.ContentSource);
|
||||||
@@ -678,8 +797,11 @@ public sealed class PayloadHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GameFunctions.GameFunctions.IsMentor() && ImGui.Selectable(Language.Context_InviteToNoviceNetwork))
|
if (
|
||||||
GameFunctions.Context.InviteToNoviceNetwork(player.PlayerName, (ushort) world.RowId);
|
GameFunctions.GameFunctions.IsMentor()
|
||||||
|
&& ImGui.Selectable(Language.Context_InviteToNoviceNetwork)
|
||||||
|
)
|
||||||
|
GameFunctions.Context.InviteToNoviceNetwork(player.PlayerName, (ushort)world.RowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
|
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
|
||||||
@@ -694,7 +816,10 @@ public sealed class PayloadHandler
|
|||||||
|
|
||||||
if (validContentId && ImGui.Selectable(Language.Context_AdventurerPlate))
|
if (validContentId && ImGui.Selectable(Language.Context_AdventurerPlate))
|
||||||
if (!GameFunctions.GameFunctions.TryOpenAdventurerPlate(chunk.Message!.ContentId))
|
if (!GameFunctions.GameFunctions.TryOpenAdventurerPlate(chunk.Message!.ContentId))
|
||||||
WrapperUtil.AddNotification(Language.Context_AdventurerPlateError, NotificationType.Warning);
|
WrapperUtil.AddNotification(
|
||||||
|
Language.Context_AdventurerPlateError,
|
||||||
|
NotificationType.Warning
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IPlayerCharacter? FindCharacterForPayload(PlayerPayload payload)
|
private IPlayerCharacter? FindCharacterForPayload(PlayerPayload payload)
|
||||||
@@ -728,13 +853,21 @@ public sealed class PayloadHandler
|
|||||||
if (ImGui.Selectable(Language.Context_CopyLink))
|
if (ImGui.Selectable(Language.Context_CopyLink))
|
||||||
{
|
{
|
||||||
ImGui.SetClipboardText(uri.Uri.ToString());
|
ImGui.SetClipboardText(uri.Uri.ToString());
|
||||||
WrapperUtil.AddNotification(Language.Context_CopyLinkNotification, NotificationType.Info);
|
WrapperUtil.AddNotification(
|
||||||
|
Language.Context_CopyLinkNotification,
|
||||||
|
NotificationType.Info
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawStatusPopup(StatusPayload status)
|
private void DrawStatusPopup(StatusPayload status)
|
||||||
{
|
{
|
||||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(status.Status.Value.Icon)).GetWrapOrDefault() is { } icon)
|
if (
|
||||||
|
Plugin
|
||||||
|
.TextureProvider.GetFromGameIcon(new GameIconLookup(status.Status.Value.Icon))
|
||||||
|
.GetWrapOrDefault() is
|
||||||
|
{ } icon
|
||||||
|
)
|
||||||
InlineIcon(icon);
|
InlineIcon(icon);
|
||||||
|
|
||||||
var builder = new SeStringBuilder();
|
var builder = new SeStringBuilder();
|
||||||
@@ -752,7 +885,10 @@ public sealed class PayloadHandler
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogWindow.DrawChunks(ChunkUtil.ToChunks(builder.BuiltString, ChunkSource.None, null).ToList(), false);
|
LogWindow.DrawChunks(
|
||||||
|
ChunkUtil.ToChunks(builder.BuiltString, ChunkSource.None, null).ToList(),
|
||||||
|
false
|
||||||
|
);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
if (ImGui.Selectable(Language.Context_Link))
|
if (ImGui.Selectable(Language.Context_Link))
|
||||||
|
|||||||
+720
-413
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,15 @@ namespace HellionChat.Privacy;
|
|||||||
|
|
||||||
internal static class PrivacyDefaults
|
internal static class PrivacyDefaults
|
||||||
{
|
{
|
||||||
// Privacy-First default whitelist (DSGVO Art. 25 - Privacy by Default).
|
// F3.1: failsafe for ChatTypes added by future FFXIV patches. New installs
|
||||||
// Only the player's own conversations are persisted out-of-the-box.
|
// persist unknown channels so a major patch's added ChatType isn't silently
|
||||||
// Public chat (Say/Shout/Yell), Novice Network, NPC dialogue, system
|
// dropped before the user can opt in or out. Existing configs keep their
|
||||||
// logs and battle messages are NOT persisted unless the user opts in.
|
// explicit choice — see Configuration.cs PrivacyPersistUnknownChannels.
|
||||||
|
internal const bool DefaultPersistUnknownChannels = true;
|
||||||
|
|
||||||
|
// DSGVO Art. 25 (Privacy by Default): only the player's own conversations
|
||||||
|
// are persisted out-of-the-box. Public chat, NPC dialogue, system logs and
|
||||||
|
// battle messages require explicit opt-in.
|
||||||
internal static readonly IReadOnlySet<ChatType> PrivacyFirstWhitelist = new HashSet<ChatType>
|
internal static readonly IReadOnlySet<ChatType> PrivacyFirstWhitelist = new HashSet<ChatType>
|
||||||
{
|
{
|
||||||
ChatType.TellIncoming,
|
ChatType.TellIncoming,
|
||||||
@@ -42,54 +47,54 @@ internal static class PrivacyDefaults
|
|||||||
ChatType.ExtraChatLinkshell8,
|
ChatType.ExtraChatLinkshell8,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default retention windows per channel (in days). Channels not listed
|
// Per-channel retention in days. Unlisted channels fall back to
|
||||||
// here fall back to Configuration.RetentionDefaultDays. Reflects the
|
// Configuration.RetentionDefaultDays. Tells: 365, everything else: 90.
|
||||||
// design spec: Tells 365, own-conversation channels 90, everything else
|
internal static readonly IReadOnlyDictionary<ChatType, int> DefaultRetentionDays =
|
||||||
// shorter via the global default.
|
new Dictionary<ChatType, int>
|
||||||
internal static readonly IReadOnlyDictionary<ChatType, int> DefaultRetentionDays = new Dictionary<ChatType, int>
|
{
|
||||||
{
|
[ChatType.TellIncoming] = 365,
|
||||||
[ChatType.TellIncoming] = 365,
|
[ChatType.TellOutgoing] = 365,
|
||||||
[ChatType.TellOutgoing] = 365,
|
|
||||||
|
|
||||||
[ChatType.Party] = 90,
|
[ChatType.Party] = 90,
|
||||||
[ChatType.CrossParty] = 90,
|
[ChatType.CrossParty] = 90,
|
||||||
[ChatType.Alliance] = 90,
|
[ChatType.Alliance] = 90,
|
||||||
[ChatType.PvpTeam] = 90,
|
[ChatType.PvpTeam] = 90,
|
||||||
[ChatType.FreeCompany] = 90,
|
[ChatType.FreeCompany] = 90,
|
||||||
|
|
||||||
[ChatType.Linkshell1] = 90,
|
[ChatType.Linkshell1] = 90,
|
||||||
[ChatType.Linkshell2] = 90,
|
[ChatType.Linkshell2] = 90,
|
||||||
[ChatType.Linkshell3] = 90,
|
[ChatType.Linkshell3] = 90,
|
||||||
[ChatType.Linkshell4] = 90,
|
[ChatType.Linkshell4] = 90,
|
||||||
[ChatType.Linkshell5] = 90,
|
[ChatType.Linkshell5] = 90,
|
||||||
[ChatType.Linkshell6] = 90,
|
[ChatType.Linkshell6] = 90,
|
||||||
[ChatType.Linkshell7] = 90,
|
[ChatType.Linkshell7] = 90,
|
||||||
[ChatType.Linkshell8] = 90,
|
[ChatType.Linkshell8] = 90,
|
||||||
|
|
||||||
[ChatType.CrossLinkshell1] = 90,
|
[ChatType.CrossLinkshell1] = 90,
|
||||||
[ChatType.CrossLinkshell2] = 90,
|
[ChatType.CrossLinkshell2] = 90,
|
||||||
[ChatType.CrossLinkshell3] = 90,
|
[ChatType.CrossLinkshell3] = 90,
|
||||||
[ChatType.CrossLinkshell4] = 90,
|
[ChatType.CrossLinkshell4] = 90,
|
||||||
[ChatType.CrossLinkshell5] = 90,
|
[ChatType.CrossLinkshell5] = 90,
|
||||||
[ChatType.CrossLinkshell6] = 90,
|
[ChatType.CrossLinkshell6] = 90,
|
||||||
[ChatType.CrossLinkshell7] = 90,
|
[ChatType.CrossLinkshell7] = 90,
|
||||||
[ChatType.CrossLinkshell8] = 90,
|
[ChatType.CrossLinkshell8] = 90,
|
||||||
|
|
||||||
[ChatType.ExtraChatLinkshell1] = 90,
|
[ChatType.ExtraChatLinkshell1] = 90,
|
||||||
[ChatType.ExtraChatLinkshell2] = 90,
|
[ChatType.ExtraChatLinkshell2] = 90,
|
||||||
[ChatType.ExtraChatLinkshell3] = 90,
|
[ChatType.ExtraChatLinkshell3] = 90,
|
||||||
[ChatType.ExtraChatLinkshell4] = 90,
|
[ChatType.ExtraChatLinkshell4] = 90,
|
||||||
[ChatType.ExtraChatLinkshell5] = 90,
|
[ChatType.ExtraChatLinkshell5] = 90,
|
||||||
[ChatType.ExtraChatLinkshell6] = 90,
|
[ChatType.ExtraChatLinkshell6] = 90,
|
||||||
[ChatType.ExtraChatLinkshell7] = 90,
|
[ChatType.ExtraChatLinkshell7] = 90,
|
||||||
[ChatType.ExtraChatLinkshell8] = 90,
|
[ChatType.ExtraChatLinkshell8] = 90,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Casual profile = Privacy-First plus public chat (Say/Shout/Yell, both
|
// Casual: Privacy-First + public chat (Say/Shout/Yell, emotes, Novice
|
||||||
// emote types, Novice Network), kept for a short 24-hour window so the
|
// Network) with a 1-day window so recent RP/trade is searchable but
|
||||||
// last RP scene or shout trade is still searchable but third-party data
|
// third-party data doesn't accumulate.
|
||||||
// doesn't accumulate forever.
|
internal static readonly IReadOnlySet<ChatType> CasualWhitelist = new HashSet<ChatType>(
|
||||||
internal static readonly IReadOnlySet<ChatType> CasualWhitelist = new HashSet<ChatType>(PrivacyFirstWhitelist)
|
PrivacyFirstWhitelist
|
||||||
|
)
|
||||||
{
|
{
|
||||||
ChatType.Say,
|
ChatType.Say,
|
||||||
ChatType.Shout,
|
ChatType.Shout,
|
||||||
@@ -99,13 +104,39 @@ internal static class PrivacyDefaults
|
|||||||
ChatType.NoviceNetwork,
|
ChatType.NoviceNetwork,
|
||||||
};
|
};
|
||||||
|
|
||||||
internal static readonly IReadOnlyDictionary<ChatType, int> CasualRetentionOverrides = new Dictionary<ChatType, int>
|
internal static readonly IReadOnlyDictionary<ChatType, int> CasualRetentionOverrides =
|
||||||
|
new Dictionary<ChatType, int>
|
||||||
|
{
|
||||||
|
[ChatType.Say] = 1,
|
||||||
|
[ChatType.Shout] = 1,
|
||||||
|
[ChatType.Yell] = 1,
|
||||||
|
[ChatType.CustomEmote] = 1,
|
||||||
|
[ChatType.StandardEmote] = 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] = 1,
|
ChatType.Say,
|
||||||
[ChatType.Shout] = 1,
|
ChatType.CustomEmote,
|
||||||
[ChatType.Yell] = 1,
|
ChatType.StandardEmote,
|
||||||
[ChatType.CustomEmote] = 1,
|
|
||||||
[ChatType.StandardEmote] = 1,
|
|
||||||
[ChatType.NoviceNetwork] = 1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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/\ /---~\\ ~}}
|
||||||
|
_// _// ~}
|
||||||
@@ -4,16 +4,14 @@ using HellionChat.Util;
|
|||||||
|
|
||||||
namespace HellionChat.Resources;
|
namespace HellionChat.Resources;
|
||||||
|
|
||||||
// Hellion Chat — v0.6.0 built-in colour presets for the ChatColours
|
// Built-in colour presets applied via Settings UI → ChatColours.
|
||||||
// settings section. Read-only static data; users apply a preset via the
|
// Battle-channel types are intentionally excluded to preserve combat-log tuning.
|
||||||
// settings UI which overwrites Configuration.ChatColours immediately.
|
|
||||||
// Battle-channel types are intentionally NOT covered by the stylistic
|
|
||||||
// presets so that combat-log tuning the user has done stays intact.
|
|
||||||
public sealed record ChatColourPreset(
|
public sealed record ChatColourPreset(
|
||||||
string DisplayName,
|
string DisplayName,
|
||||||
string LocalizationKey,
|
string LocalizationKey,
|
||||||
bool IsBrandPreset,
|
bool IsBrandPreset,
|
||||||
IReadOnlyDictionary<ChatType, uint> Colours);
|
IReadOnlyDictionary<ChatType, uint> Colours
|
||||||
|
);
|
||||||
|
|
||||||
public static class ChatColourPresets
|
public static class ChatColourPresets
|
||||||
{
|
{
|
||||||
@@ -27,43 +25,48 @@ public static class ChatColourPresets
|
|||||||
DisplayName: "ChatTwo Default",
|
DisplayName: "ChatTwo Default",
|
||||||
LocalizationKey: "ChatColourPresets_Default",
|
LocalizationKey: "ChatColourPresets_Default",
|
||||||
IsBrandPreset: false,
|
IsBrandPreset: false,
|
||||||
Colours: BuildDefault()),
|
Colours: BuildDefault()
|
||||||
|
),
|
||||||
["HighContrast"] = new(
|
["HighContrast"] = new(
|
||||||
DisplayName: "High-Contrast",
|
DisplayName: "High-Contrast",
|
||||||
LocalizationKey: "ChatColourPresets_HighContrast",
|
LocalizationKey: "ChatColourPresets_HighContrast",
|
||||||
IsBrandPreset: false,
|
IsBrandPreset: false,
|
||||||
Colours: BuildHighContrast()),
|
Colours: BuildHighContrast()
|
||||||
|
),
|
||||||
["Pastell"] = new(
|
["Pastell"] = new(
|
||||||
DisplayName: "Pastell",
|
DisplayName: "Pastell",
|
||||||
LocalizationKey: "ChatColourPresets_Pastell",
|
LocalizationKey: "ChatColourPresets_Pastell",
|
||||||
IsBrandPreset: false,
|
IsBrandPreset: false,
|
||||||
Colours: BuildPastell()),
|
Colours: BuildPastell()
|
||||||
|
),
|
||||||
["DarkModeTuned"] = new(
|
["DarkModeTuned"] = new(
|
||||||
DisplayName: "Dark-Mode-Tuned",
|
DisplayName: "Dark-Mode-Tuned",
|
||||||
LocalizationKey: "ChatColourPresets_DarkModeTuned",
|
LocalizationKey: "ChatColourPresets_DarkModeTuned",
|
||||||
IsBrandPreset: false,
|
IsBrandPreset: false,
|
||||||
Colours: BuildDarkModeTuned()),
|
Colours: BuildDarkModeTuned()
|
||||||
|
),
|
||||||
["Hellion"] = new(
|
["Hellion"] = new(
|
||||||
DisplayName: "Hellion",
|
DisplayName: "Hellion",
|
||||||
LocalizationKey: "ChatColourPresets_Hellion",
|
LocalizationKey: "ChatColourPresets_Hellion",
|
||||||
IsBrandPreset: true,
|
IsBrandPreset: true,
|
||||||
Colours: BuildHellion()),
|
Colours: BuildHellion()
|
||||||
|
),
|
||||||
["NightBlue"] = new(
|
["NightBlue"] = new(
|
||||||
DisplayName: "Night Blue",
|
DisplayName: "Night Blue",
|
||||||
LocalizationKey: "ChatColourPresets_NightBlue",
|
LocalizationKey: "ChatColourPresets_NightBlue",
|
||||||
IsBrandPreset: false,
|
IsBrandPreset: false,
|
||||||
Colours: BuildNightBlue()),
|
Colours: BuildNightBlue()
|
||||||
|
),
|
||||||
["IndigoViolet"] = new(
|
["IndigoViolet"] = new(
|
||||||
DisplayName: "Indigo Violet",
|
DisplayName: "Indigo Violet",
|
||||||
LocalizationKey: "ChatColourPresets_IndigoViolet",
|
LocalizationKey: "ChatColourPresets_IndigoViolet",
|
||||||
IsBrandPreset: false,
|
IsBrandPreset: false,
|
||||||
Colours: BuildIndigoViolet()),
|
Colours: BuildIndigoViolet()
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Default preset spiegelt 1:1 die Werte aus ChatTypeExt.DefaultColor.
|
// Mirrors ChatTypeExt.DefaultColor; channels without a default are skipped.
|
||||||
// Channels ohne Default-Wert (return null) werden ausgelassen — wer sie
|
|
||||||
// anwenden will, behält seine aktuelle Farbe.
|
|
||||||
private static IReadOnlyDictionary<ChatType, uint> BuildDefault()
|
private static IReadOnlyDictionary<ChatType, uint> BuildDefault()
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<ChatType, uint>();
|
var dict = new Dictionary<ChatType, uint>();
|
||||||
@@ -175,79 +178,55 @@ public static class ChatColourPresets
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hellion brand preset — Arctic Cyan + Ember Orange palette aus
|
// Hellion brand preset — Arctic Cyan + Ember Orange palette.
|
||||||
// /mnt/ssd-fast/Projekte/hellion-media/hellion-media-website/BRANDING.md
|
// Cyan family for Standard/Tell, Ember/Warning for loud channels,
|
||||||
// (Schema-Stand 2026-04-16). Channels sind über das ganze Brand-Spektrum
|
// Status colours for Linkshells, darker variants for CrossLinkshells.
|
||||||
// verteilt damit jede Zeile auf einen Glance unterscheidbar ist:
|
|
||||||
// Cyan-Familie für Standard/Tell, Ember + Warning für laute Channels,
|
|
||||||
// Status-Farben (Success, Danger) für Linkshells. CrossLinkshells
|
|
||||||
// nutzen die dunkleren/sattersten Varianten derselben Hue-Familien.
|
|
||||||
private static IReadOnlyDictionary<ChatType, uint> BuildHellion()
|
private static IReadOnlyDictionary<ChatType, uint> BuildHellion()
|
||||||
{
|
{
|
||||||
return new Dictionary<ChatType, uint>
|
return new Dictionary<ChatType, uint>
|
||||||
{
|
{
|
||||||
// Standard / Tell — Cyan-Familie (Brand-Primary)
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light #4DD9E8
|
||||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light #4DD9E8
|
|
||||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan #00BED2
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan #00BED2
|
||||||
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark #0097A7
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark #0097A7
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning #F0AD4E
|
||||||
// Laute Channels — Ember/Warning
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember #F97316
|
||||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning #F0AD4E
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success #5CB85C
|
||||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember #F97316
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark #E85D04
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||||
// Gruppen-Channels — Success/Ember-dark/Cyan
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
|
||||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success #5CB85C
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(251, 146, 60), // Ember-light #FB923C
|
||||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark #E85D04
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning
|
||||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success
|
||||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(77, 217, 232),// Cyan-light
|
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
|
||||||
|
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||||
// Linkshells 1-8 — über das ganze Brand-Spektrum verteilt
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||||
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(251, 146, 60), // Ember-light #FB923C
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember
|
||||||
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(217, 83, 79), // Danger #D9534F
|
||||||
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark
|
||||||
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(200, 140, 50), // Warning-dark
|
||||||
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(60, 140, 60), // Success-dark
|
||||||
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||||
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember
|
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||||
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(217, 83, 79), // Danger #D9534F
|
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(0, 110, 130), // Cyan-darker
|
||||||
|
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(220, 90, 30), // Ember-medium
|
||||||
// CrossWorld-Linkshells 1-8 — dunklere/sattersere Varianten
|
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(170, 60, 60), // Danger-dark
|
||||||
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark
|
|
||||||
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(200, 140, 50), // Warning-dark
|
|
||||||
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(60, 140, 60), // Success-dark
|
|
||||||
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
|
||||||
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
|
||||||
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(0, 110, 130), // Cyan-darker
|
|
||||||
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(220, 90, 30), // Ember-medium
|
|
||||||
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(170, 60, 60), // Danger-dark
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bonus preset — Night Blue, KAZAMA-Stimmungs-Theme aus
|
// Night Blue — cool nautical theme, deep navy without purple.
|
||||||
// /mnt/HDD-Data1/Obsidian/Vault/Systeme/KAZAMA/Theming/Night Blue + Indigo Violet Themes.md
|
|
||||||
// Klassisch, kühl, technisch — Marineblau-Tiefe ohne Lila-Anteil.
|
|
||||||
// Bewusst NICHT als Brand-Preset markiert (Vault-Boundary): die KAZAMA-Themes
|
|
||||||
// sind persönliche Stimmungs-Themes, nicht Teil des Hellion-Brand-Systems.
|
|
||||||
private static IReadOnlyDictionary<ChatType, uint> BuildNightBlue()
|
private static IReadOnlyDictionary<ChatType, uint> BuildNightBlue()
|
||||||
{
|
{
|
||||||
return new Dictionary<ChatType, uint>
|
return new Dictionary<ChatType, uint>
|
||||||
{
|
{
|
||||||
// Standard / Tell — Royal Blue Akzent-Familie
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(230, 237, 247), // text-primary
|
||||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(230, 237, 247), // text-primary
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(106, 176, 255), // akzent-hot
|
||||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(106, 176, 255),// akzent-hot
|
|
||||||
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
||||||
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74), // warning
|
||||||
// Laute Channels — Warning/Danger Status-Töne
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122), // danger
|
||||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74), // warning
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151), // success
|
||||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122), // danger
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100), // warm-orange-light
|
||||||
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
||||||
// Gruppen — Success/Akzent-Variations
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(140, 160, 191), // text-dim
|
||||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151), // success
|
|
||||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100), // warm-orange-light
|
|
||||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
|
||||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(140, 160, 191),// text-dim
|
|
||||||
|
|
||||||
// Linkshells 1-8 — über Spektrum verteilt
|
|
||||||
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||||
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||||
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
||||||
@@ -256,8 +235,6 @@ public static class ChatColourPresets
|
|||||||
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(100, 200, 220),
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(100, 200, 220),
|
||||||
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(106, 176, 255),
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(106, 176, 255),
|
||||||
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(140, 160, 191),
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(140, 160, 191),
|
||||||
|
|
||||||
// CrossWorld-Linkshells — gedämpfte Variants
|
|
||||||
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
||||||
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
||||||
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
||||||
@@ -269,30 +246,20 @@ public static class ChatColourPresets
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bonus preset — Indigo Violet, KAZAMA-Stimmungs-Theme aus demselben
|
// Indigo Violet — warm-mystic theme, deep indigo with violet accent.
|
||||||
// Vault-Doc. Warm-mystisch, "Galaxy/Glitter/Nordlicht" — tiefes Indigo
|
|
||||||
// mit kräftigem Violet-Akzent. Persönlicher Favorit (siehe Vault).
|
|
||||||
// Auch nicht als Brand-Preset (siehe NightBlue-Note oben).
|
|
||||||
private static IReadOnlyDictionary<ChatType, uint> BuildIndigoViolet()
|
private static IReadOnlyDictionary<ChatType, uint> BuildIndigoViolet()
|
||||||
{
|
{
|
||||||
return new Dictionary<ChatType, uint>
|
return new Dictionary<ChatType, uint>
|
||||||
{
|
{
|
||||||
// Standard / Tell — Royal Violet Akzent-Familie
|
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary
|
||||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary (light lavender)
|
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(176, 124, 255), // akzent-hot
|
||||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(176, 124, 255),// akzent-hot
|
|
||||||
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
||||||
|
|
||||||
// Laute Channels — geteilt mit Night Blue (Status-Farben)
|
|
||||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122),
|
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122),
|
||||||
|
|
||||||
// Gruppen
|
|
||||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151),
|
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151),
|
||||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
||||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(168, 144, 208),// text-dim
|
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(168, 144, 208), // text-dim
|
||||||
|
|
||||||
// Linkshells 1-8
|
|
||||||
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||||
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||||
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
||||||
@@ -301,8 +268,6 @@ public static class ChatColourPresets
|
|||||||
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(139, 77, 222),
|
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(139, 77, 222),
|
||||||
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(130, 90, 200),
|
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(130, 90, 200),
|
||||||
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(168, 144, 208),
|
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(168, 144, 208),
|
||||||
|
|
||||||
// CrossWorld-Linkshells
|
|
||||||
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
||||||
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
||||||
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
||||||
|
|||||||
+120
@@ -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));
|
||||||
@@ -323,4 +371,76 @@ internal class HellionStrings
|
|||||||
// Hellion Chat — v1.2.0 Appearance / Compact-Density toggle
|
// Hellion Chat — v1.2.0 Appearance / Compact-Density toggle
|
||||||
internal static string Appearance_UseCompactDensity_Name => Get(nameof(Appearance_UseCompactDensity_Name));
|
internal static string Appearance_UseCompactDensity_Name => Get(nameof(Appearance_UseCompactDensity_Name));
|
||||||
internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description));
|
internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.2.1 Settings Cleanup: new card titles + subtexts
|
||||||
|
internal static string Settings_Card_ThemeAndLayout_Title => Get(nameof(Settings_Card_ThemeAndLayout_Title));
|
||||||
|
internal static string Settings_Card_ThemeAndLayout_Subtext => Get(nameof(Settings_Card_ThemeAndLayout_Subtext));
|
||||||
|
internal static string Settings_Card_FontsAndColours_Title => Get(nameof(Settings_Card_FontsAndColours_Title));
|
||||||
|
internal static string Settings_Card_FontsAndColours_Subtext => Get(nameof(Settings_Card_FontsAndColours_Subtext));
|
||||||
|
internal static string Settings_Card_DataManagement_Title => Get(nameof(Settings_Card_DataManagement_Title));
|
||||||
|
internal static string Settings_Card_DataManagement_Subtext => Get(nameof(Settings_Card_DataManagement_Subtext));
|
||||||
|
internal static string Settings_Card_Integrations_Title => Get(nameof(Settings_Card_Integrations_Title));
|
||||||
|
internal static string Settings_Card_Integrations_Subtext => Get(nameof(Settings_Card_Integrations_Subtext));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.2.1 Theme & Layout tab section headings + WindowOpacity slider
|
||||||
|
internal static string Settings_ThemeAndLayout_Theme_Heading => Get(nameof(Settings_ThemeAndLayout_Theme_Heading));
|
||||||
|
internal static string Settings_ThemeAndLayout_WindowStyle_Heading => Get(nameof(Settings_ThemeAndLayout_WindowStyle_Heading));
|
||||||
|
internal static string Settings_ThemeAndLayout_TimestampStyle_Heading => Get(nameof(Settings_ThemeAndLayout_TimestampStyle_Heading));
|
||||||
|
internal static string Settings_ThemeAndLayout_WindowOpacity_Name => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Name));
|
||||||
|
internal static string Settings_ThemeAndLayout_WindowOpacity_Description => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Description));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.2.1 Fonts & Colours tab section headings
|
||||||
|
internal static string Settings_FontsAndColours_Fonts_Heading => Get(nameof(Settings_FontsAndColours_Fonts_Heading));
|
||||||
|
internal static string Settings_FontsAndColours_Colours_Heading => Get(nameof(Settings_FontsAndColours_Colours_Heading));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.2.1 Data Management tab section headings
|
||||||
|
internal static string Settings_DataManagement_Storage_Heading => Get(nameof(Settings_DataManagement_Storage_Heading));
|
||||||
|
internal static string Settings_DataManagement_Retention_Heading => Get(nameof(Settings_DataManagement_Retention_Heading));
|
||||||
|
internal static string Settings_DataManagement_Cleanup_Heading => Get(nameof(Settings_DataManagement_Cleanup_Heading));
|
||||||
|
internal static string Settings_DataManagement_Export_Heading => Get(nameof(Settings_DataManagement_Export_Heading));
|
||||||
|
internal static string Settings_DataManagement_DbViewer_Heading => Get(nameof(Settings_DataManagement_DbViewer_Heading));
|
||||||
|
internal static string Settings_DataManagement_Advanced_Heading => Get(nameof(Settings_DataManagement_Advanced_Heading));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.2.1 Window-tab Behaviour heading (replaces Frame heading)
|
||||||
|
internal static string Settings_Window_Frame_Behaviour_Heading => Get(nameof(Settings_Window_Frame_Behaviour_Heading));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.2.1 Migration v15 → v16 toast
|
||||||
|
internal static string Migration_v16_OverrideStyle_Toast => Get(nameof(Migration_v16_OverrideStyle_Toast));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.3.0 Integrations tab (Honorific + Coming-Soon roadmap)
|
||||||
|
internal static string Settings_Tab_Integrations => Get(nameof(Settings_Tab_Integrations));
|
||||||
|
internal static string Settings_Integrations_Intro => Get(nameof(Settings_Integrations_Intro));
|
||||||
|
internal static string Settings_Integrations_Honorific_SectionHeader => Get(nameof(Settings_Integrations_Honorific_SectionHeader));
|
||||||
|
internal static string Settings_Integrations_Honorific_Status_Detected => Get(nameof(Settings_Integrations_Honorific_Status_Detected));
|
||||||
|
internal static string Settings_Integrations_Honorific_Status_NotInstalled => Get(nameof(Settings_Integrations_Honorific_Status_NotInstalled));
|
||||||
|
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_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_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_Intro => Get(nameof(Settings_Integrations_ComingSoon_Intro));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_ContextMenu_Title => Get(nameof(Settings_Integrations_ComingSoon_ContextMenu_Title));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_ContextMenu_Description => Get(nameof(Settings_Integrations_ComingSoon_ContextMenu_Description));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_Notifications_Title => Get(nameof(Settings_Integrations_ComingSoon_Notifications_Title));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_Notifications_Description => Get(nameof(Settings_Integrations_ComingSoon_Notifications_Description));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_RPStatus_Title => Get(nameof(Settings_Integrations_ComingSoon_RPStatus_Title));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_RPStatus_Description => Get(nameof(Settings_Integrations_ComingSoon_RPStatus_Description));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_ExtraChat_Title => Get(nameof(Settings_Integrations_ComingSoon_ExtraChat_Title));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_ExtraChat_Description => Get(nameof(Settings_Integrations_ComingSoon_ExtraChat_Description));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_QuickDM_Title => Get(nameof(Settings_Integrations_ComingSoon_QuickDM_Title));
|
||||||
|
internal static string Settings_Integrations_ComingSoon_QuickDM_Description => Get(nameof(Settings_Integrations_ComingSoon_QuickDM_Description));
|
||||||
|
internal static string Settings_Integrations_GotAnIdea_SectionHeader => Get(nameof(Settings_Integrations_GotAnIdea_SectionHeader));
|
||||||
|
internal static string Settings_Integrations_GotAnIdea_Body => Get(nameof(Settings_Integrations_GotAnIdea_Body));
|
||||||
|
internal static string Settings_Integrations_GotAnIdea_LinkLabel => Get(nameof(Settings_Integrations_GotAnIdea_LinkLabel));
|
||||||
|
|
||||||
|
// Hellion Chat — v1.3.0 Honorific title slot 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>
|
||||||
@@ -639,7 +779,7 @@
|
|||||||
<value>Allgemein</value>
|
<value>Allgemein</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_General_Subtext" xml:space="preserve">
|
<data name="Settings_Card_General_Subtext" xml:space="preserve">
|
||||||
<value>Sprache und grundlegendes Verhalten</value>
|
<value>Sprache, Eingabe, Audio und Performance.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
|
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
|
||||||
<value>Erscheinungsbild</value>
|
<value>Erscheinungsbild</value>
|
||||||
@@ -657,25 +797,25 @@
|
|||||||
<value>Fenster</value>
|
<value>Fenster</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
|
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
|
||||||
<value>Fensterposition, Rahmen, Hide-Zustände</value>
|
<value>Wann das Fenster sichtbar ist und ob es sich bewegen lässt.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Chat_Title" xml:space="preserve">
|
<data name="Settings_Card_Chat_Title" xml:space="preserve">
|
||||||
<value>Chat</value>
|
<value>Chat</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
|
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
|
||||||
<value>Chat-Verhalten, Emotes, Auto-Tells</value>
|
<value>Tells, Vorschau, Nachrichten-Verhalten und Emotes.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
|
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
|
||||||
<value>Tabs</value>
|
<value>Tabs</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
|
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
|
||||||
<value>Tab-Layout, Kanäle, eigene Tabs</value>
|
<value>Eigene Chat-Tabs anlegen und konfigurieren.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
|
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
|
||||||
<value>Datenschutz</value>
|
<value>Datenschutz</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
|
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
|
||||||
<value>Filter, Aufbewahrung, Bereinigung, Export</value>
|
<value>Privacy-Filter pro Channel und was gespeichert werden darf.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Database_Title" xml:space="preserve">
|
<data name="Settings_Card_Database_Title" xml:space="preserve">
|
||||||
<value>Datenbank</value>
|
<value>Datenbank</value>
|
||||||
@@ -687,7 +827,7 @@
|
|||||||
<value>Information</value>
|
<value>Information</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
|
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
|
||||||
<value>Über, Mitwirkende, Support</value>
|
<value>Version, Mission, Lizenz und Changelog.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Settings_Tab_Themes" xml:space="preserve">
|
<data name="Settings_Tab_Themes" xml:space="preserve">
|
||||||
<value>Themes</value>
|
<value>Themes</value>
|
||||||
@@ -728,4 +868,166 @@
|
|||||||
<data name="Appearance_UseCompactDensity_Description" xml:space="preserve">
|
<data name="Appearance_UseCompactDensity_Description" xml:space="preserve">
|
||||||
<value>Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen.</value>
|
<value>Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Card_ThemeAndLayout_Title" xml:space="preserve">
|
||||||
|
<value>Theme & Layout</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_ThemeAndLayout_Subtext" xml:space="preserve">
|
||||||
|
<value>Theme, Fenster-Rahmen und Zeitstempel-Style.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_FontsAndColours_Title" xml:space="preserve">
|
||||||
|
<value>Schriften & Farben</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_FontsAndColours_Subtext" xml:space="preserve">
|
||||||
|
<value>Schriftart, Schriftgröße und Chat-Farben pro Channel.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_DataManagement_Title" xml:space="preserve">
|
||||||
|
<value>Daten-Verwaltung</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_DataManagement_Subtext" xml:space="preserve">
|
||||||
|
<value>Aufbewahrung, Aufräumen, Export und Datenbank-Statistiken.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Integrations_Title" xml:space="preserve">
|
||||||
|
<value>Integrationen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Integrations_Subtext" xml:space="preserve">
|
||||||
|
<value>Andere Dalamud-Plugins, mit denen HellionChat zusammenarbeitet. Kommende Integrationen in der Vorschau.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_Theme_Heading" xml:space="preserve">
|
||||||
|
<value>Theme</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_WindowStyle_Heading" xml:space="preserve">
|
||||||
|
<value>Fenster-Style</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_TimestampStyle_Heading" xml:space="preserve">
|
||||||
|
<value>Zeitstempel-Style</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_WindowOpacity_Name" xml:space="preserve">
|
||||||
|
<value>Fenster-Transparenz</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_ThemeAndLayout_WindowOpacity_Description" xml:space="preserve">
|
||||||
|
<value>Wie durchsichtig der Fensterhintergrund ist. Niedrigere Werte lassen mehr vom Spiel durchscheinen. Tipp: Dalamud's Per-Window-Menü (Hamburger in der Titelleiste) bietet pro Fenster eigene Overrides für Deckkraft, Hintergrund-Blur, Durchklick und Anpinnen — die haben Vorrang über diesen Slider für das jeweilige Fenster.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_FontsAndColours_Fonts_Heading" xml:space="preserve">
|
||||||
|
<value>Schriftarten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_FontsAndColours_Colours_Heading" xml:space="preserve">
|
||||||
|
<value>Chat-Farben</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_DataManagement_Storage_Heading" xml:space="preserve">
|
||||||
|
<value>Speicherung</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_DataManagement_Retention_Heading" xml:space="preserve">
|
||||||
|
<value>Aufbewahrung</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_DataManagement_Cleanup_Heading" xml:space="preserve">
|
||||||
|
<value>Cleanup</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_DataManagement_Export_Heading" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_DataManagement_DbViewer_Heading" xml:space="preserve">
|
||||||
|
<value>Datenbank-Viewer</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_DataManagement_Advanced_Heading" xml:space="preserve">
|
||||||
|
<value>Erweitert (Shift+Klick zum Öffnen)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Window_Frame_Behaviour_Heading" xml:space="preserve">
|
||||||
|
<value>Verhalten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Migration_v16_OverrideStyle_Toast" xml:space="preserve">
|
||||||
|
<value>Hellion Chat 1.2.1 hat das Settings-Menü neu sortiert und die alte „Stilüberschreiben"-Option entfernt (überholt durch das Theme-System aus 1.1.0). Deine restlichen Einstellungen bleiben unverändert. Die Fenster-Transparenz ist nach „Theme & Layout" migriert. Ein Backup der vorherigen Config liegt unter pluginConfigs/HellionChat.json.pre-v16-backup neben der aktiven HellionChat.json.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Tab_Integrations" xml:space="preserve">
|
||||||
|
<value>Integrationen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Intro" xml:space="preserve">
|
||||||
|
<value>Plugin-Integrationen lassen HellionChat mit anderen installierten Dalamud-Plugins zusammenarbeiten. Jede Integration erkennt ihr Ziel automatisch und deaktiviert sich still, wenn das Ziel-Plugin fehlt.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_SectionHeader" xml:space="preserve">
|
||||||
|
<value>Honorific</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Status_Detected" xml:space="preserve">
|
||||||
|
<value>Erkannt (v{0}.{1})</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Status_NotInstalled" xml:space="preserve">
|
||||||
|
<value>Nicht installiert</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Status_Incompatible" xml:space="preserve">
|
||||||
|
<value>Inkompatible API-Version ({0} erwartet, {1}.{2} gefunden)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_Toggle" xml:space="preserve">
|
||||||
|
<value>Honorific-Titel im Chat-Header anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<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>
|
||||||
|
</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">
|
||||||
|
<value>Honorific auf GitHub</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_Honorific_LinkAuthor" xml:space="preserve">
|
||||||
|
<value>von Caraxi</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_SectionHeader" xml:space="preserve">
|
||||||
|
<value>Demnächst</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_Intro" xml:space="preserve">
|
||||||
|
<value>Diese Integrationen sind auf der Roadmap. Die Einstellungen erscheinen automatisch, sobald das jeweilige Plugin angebunden ist.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_ContextMenu_Title" xml:space="preserve">
|
||||||
|
<value>Kontextmenü-Aktionen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_ContextMenu_Description" xml:space="preserve">
|
||||||
|
<value>Rechtsklick auf einen Namen im Chat: zu PlayerTrack springen, Lodestone-Profil öffnen oder mit einem Klick eine DM verfassen.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_Notifications_Title" xml:space="preserve">
|
||||||
|
<value>Smart Notifications (NotificationMaster)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_Notifications_Description" xml:space="preserve">
|
||||||
|
<value>Mentions und DMs über NotificationMaster: System-Toasts, Taskbar-Flash und Sounds pro Channel.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_RPStatus_Title" xml:space="preserve">
|
||||||
|
<value>RP-Status-Block (Moodles · LightlessClient)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_RPStatus_Description" xml:space="preserve">
|
||||||
|
<value>Moodles-Status-Icons und Pair-Badges direkt neben den Chat-Namen anzeigen, für mehr Roleplay-Kontext.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_ExtraChat_Title" xml:space="preserve">
|
||||||
|
<value>ExtraChat-Channels</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_ExtraChat_Description" xml:space="preserve">
|
||||||
|
<value>End-to-End-verschlüsselte Cross-Datacenter-Linkshells nativ in HellionChat hosten.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_QuickDM_Title" xml:space="preserve">
|
||||||
|
<value>Quick-DM-Button (XIVInstantMessenger)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_ComingSoon_QuickDM_Description" xml:space="preserve">
|
||||||
|
<value>DM-Schnellzugriff direkt aus dem Chat-Fenster, ein Klick.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_GotAnIdea_SectionHeader" xml:space="preserve">
|
||||||
|
<value>Idee?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_GotAnIdea_Body" xml:space="preserve">
|
||||||
|
<value>Idee für eine Plugin-Integration, die nicht auf der Liste steht? Komm auf den Hellion-Forge-Discord und schreib mir. Community-Input bestimmt die Roadmap.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Integrations_GotAnIdea_LinkLabel" xml:space="preserve">
|
||||||
|
<value>Hellion Forge öffnen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ChatHeader_HonorificTitle_Tooltip" xml:space="preserve">
|
||||||
|
<value>Custom-Titel von Honorific</value>
|
||||||
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle" xml:space="preserve">
|
||||||
|
<value>Volltext-Suche</value>
|
||||||
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle_Hint_Indexing" xml:space="preserve">
|
||||||
|
<value>Der Volltext-Index wird noch gebaut. Die lokale Suche bleibt verfügbar.</value>
|
||||||
|
</data>
|
||||||
|
<data name="DbViewer_FullTextToggle_Hint_PhraseMode" xml:space="preserve">
|
||||||
|
<value>Sucht nach der exakten Wortfolge. Mehrere Wörter werden nur gefunden, wenn sie zusammen und in dieser Reihenfolge stehen. Wer rohe FTS5-MATCH-Syntax nutzen will, setzt eigene Anführungszeichen um den Suchbegriff.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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,79 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
|
||||||
|
namespace HellionChat.SelfTests;
|
||||||
|
|
||||||
|
// Validates the runtime theme-switch contract: polls ThemeRegistry.Active
|
||||||
|
// per frame until the slug moves away and back, then sanity-checks that
|
||||||
|
// the ABGR cache was recomputed on switch.
|
||||||
|
internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
private string? initialSlug;
|
||||||
|
private bool switchedAway;
|
||||||
|
|
||||||
|
public ThemeSwitchSelfTestStep(Plugin plugin)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "Hellion Chat - Theme switch";
|
||||||
|
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var registry = this.plugin.ThemeRegistry;
|
||||||
|
if (registry is null)
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
|
||||||
|
var active = registry.Active;
|
||||||
|
if (active is null)
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
|
||||||
|
if (!HasPopulatedCache(active))
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
|
||||||
|
if (this.initialSlug is null)
|
||||||
|
{
|
||||||
|
this.initialSlug = active.Slug;
|
||||||
|
ImGui.Text(
|
||||||
|
$"Initial theme: \"{this.initialSlug}\". Open Settings -> Theme & Layout and pick a different theme."
|
||||||
|
);
|
||||||
|
return SelfTestStepResult.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.switchedAway)
|
||||||
|
{
|
||||||
|
if (!string.Equals(active.Slug, this.initialSlug, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
this.switchedAway = true;
|
||||||
|
return SelfTestStepResult.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Text($"Switch the active theme away from \"{this.initialSlug}\".");
|
||||||
|
return SelfTestStepResult.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(active.Slug, this.initialSlug, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ImGui.Text($"Switch back to \"{this.initialSlug}\" to finish the test.");
|
||||||
|
return SelfTestStepResult.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CleanUp()
|
||||||
|
{
|
||||||
|
this.initialSlug = null;
|
||||||
|
this.switchedAway = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any non-zero slot confirms the cache was recomputed — no reference
|
||||||
|
// comparison since custom themes can share slot values with built-ins.
|
||||||
|
private static bool HasPopulatedCache(Theme theme)
|
||||||
|
{
|
||||||
|
var cache = theme.AbgrCache;
|
||||||
|
return (cache.Primary | cache.WindowBg | cache.TextPrimary | cache.Border) != 0u;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,12 +32,12 @@ public static class Sheets
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsInForay() =>
|
public static bool IsInForay() =>
|
||||||
TerritorySheet.TryGetRow(Plugin.ClientState.TerritoryType, out var row) &&
|
TerritorySheet.TryGetRow(Plugin.ClientState.TerritoryType, out var row)
|
||||||
row.TerritoryIntendedUse.RowId is 41 or 61;
|
&& row.TerritoryIntendedUse.RowId is 41 or 61;
|
||||||
|
|
||||||
public static IEnumerable<World> WorldsOnDatacenter(IPlayerCharacter character)
|
public static IEnumerable<World> WorldsOnDatacenter(IPlayerCharacter character)
|
||||||
{
|
{
|
||||||
var dcRow = character.HomeWorld.Value.DataCenter.RowId;
|
var dcRow = character.HomeWorld.Value.DataCenter.RowId;
|
||||||
return WorldSheet.Where(world => world.IsPublic && world.DataCenter.RowId == dcRow);
|
return WorldSheet.Where(world => world.IsPublic && world.DataCenter.RowId == dcRow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,45 +6,47 @@ internal static class Chat2Classic
|
|||||||
{
|
{
|
||||||
public const string Slug = "chat2-classic";
|
public const string Slug = "chat2-classic";
|
||||||
|
|
||||||
public static Theme Build() => new(
|
public static Theme Build() =>
|
||||||
Slug: Slug,
|
new(
|
||||||
Name: "Chat 2 Klassik",
|
Slug: Slug,
|
||||||
Author: "Upstream (Infi & Anna)",
|
Name: "Chat 2 Klassik",
|
||||||
Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.",
|
Author: "Upstream (Infi & Anna)",
|
||||||
Colors: new ThemeColors(
|
Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.",
|
||||||
PrimaryDark: ColourUtil.HexToRgba("#3D6E92"),
|
Colors: new ThemeColors(
|
||||||
Primary: ColourUtil.HexToRgba("#4682B4"),
|
PrimaryDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||||
PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"),
|
Primary: ColourUtil.HexToRgba("#4682B4"),
|
||||||
PrimaryGlow: ColourUtil.HexToRgba("#4682B466"),
|
PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#4682B466"),
|
||||||
AccentDark: ColourUtil.HexToRgba("#3D6E92"),
|
AccentDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||||
Accent: ColourUtil.HexToRgba("#4682B4"),
|
Accent: ColourUtil.HexToRgba("#4682B4"),
|
||||||
AccentLight: ColourUtil.HexToRgba("#5C9DC8"),
|
AccentLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||||
|
Identity: ColourUtil.HexToRgba("#4682B4"),
|
||||||
Identity: ColourUtil.HexToRgba("#4682B4"),
|
WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#141414"),
|
||||||
WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"),
|
FrameBg: ColourUtil.HexToRgba("#1A1A1A"),
|
||||||
ChildBg: ColourUtil.HexToRgba("#141414"),
|
Surface: ColourUtil.HexToRgba("#202020"),
|
||||||
FrameBg: ColourUtil.HexToRgba("#1A1A1A"),
|
SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"),
|
||||||
Surface: ColourUtil.HexToRgba("#202020"),
|
Border: ColourUtil.HexToRgba("#404040"),
|
||||||
SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"),
|
TextPrimary: ColourUtil.HexToRgba("#E6E6E6"),
|
||||||
Border: ColourUtil.HexToRgba("#404040"),
|
TextMuted: ColourUtil.HexToRgba("#999999"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#666666"),
|
||||||
TextPrimary: ColourUtil.HexToRgba("#E6E6E6"),
|
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||||
TextMuted: ColourUtil.HexToRgba("#999999"),
|
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||||
TextDim: ColourUtil.HexToRgba("#666666"),
|
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#4682B4")
|
||||||
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
),
|
||||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
Layout: new ThemeLayout(
|
||||||
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
WindowRounding: 0f,
|
||||||
StatusInfo: ColourUtil.HexToRgba("#4682B4")
|
ChildRounding: 0f,
|
||||||
),
|
PopupRounding: 0f,
|
||||||
Layout: new ThemeLayout(
|
FrameRounding: 0f,
|
||||||
WindowRounding: 0f, ChildRounding: 0f, PopupRounding: 0f,
|
GrabRounding: 0f,
|
||||||
FrameRounding: 0f, GrabRounding: 0f, TabRounding: 0f,
|
TabRounding: 0f,
|
||||||
ScrollbarRounding: 0f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
ScrollbarRounding: 0f,
|
||||||
),
|
WindowBorderSize: 1f,
|
||||||
Typography: new ThemeTypography(),
|
FrameBorderSize: 1f
|
||||||
IsBuiltIn: true
|
),
|
||||||
);
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,72 +6,76 @@ internal static class EventHorizon
|
|||||||
{
|
{
|
||||||
public const string Slug = "event-horizon";
|
public const string Slug = "event-horizon";
|
||||||
|
|
||||||
public static Theme Build() => new(
|
public static Theme Build() =>
|
||||||
Slug: Slug,
|
new(
|
||||||
Name: "Event Horizon",
|
Slug: Slug,
|
||||||
Author: "Hellion Online Media",
|
Name: "Event Horizon",
|
||||||
Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.",
|
Author: "Hellion Forge",
|
||||||
Colors: new ThemeColors(
|
Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.",
|
||||||
PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"),
|
Colors: new ThemeColors(
|
||||||
Primary: ColourUtil.HexToRgba("#9D5CFF"),
|
PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"),
|
||||||
PrimaryLight: ColourUtil.HexToRgba("#B585FF"),
|
Primary: ColourUtil.HexToRgba("#9D5CFF"),
|
||||||
PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"),
|
PrimaryLight: ColourUtil.HexToRgba("#B585FF"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"),
|
||||||
AccentDark: ColourUtil.HexToRgba("#C9982E"),
|
AccentDark: ColourUtil.HexToRgba("#C9982E"),
|
||||||
Accent: ColourUtil.HexToRgba("#E0AB36"),
|
Accent: ColourUtil.HexToRgba("#E0AB36"),
|
||||||
AccentLight: ColourUtil.HexToRgba("#F2C25C"),
|
AccentLight: ColourUtil.HexToRgba("#F2C25C"),
|
||||||
|
Identity: ColourUtil.HexToRgba("#9D5CFF"),
|
||||||
Identity: ColourUtil.HexToRgba("#9D5CFF"),
|
WindowBg: ColourUtil.HexToRgba("#040308"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#0A081A"),
|
||||||
WindowBg: ColourUtil.HexToRgba("#040308"),
|
FrameBg: ColourUtil.HexToRgba("#140F23"),
|
||||||
ChildBg: ColourUtil.HexToRgba("#0A081A"),
|
Surface: ColourUtil.HexToRgba("#1B1530"),
|
||||||
FrameBg: ColourUtil.HexToRgba("#140F23"),
|
SurfaceHover: ColourUtil.HexToRgba("#251D40"),
|
||||||
Surface: ColourUtil.HexToRgba("#1B1530"),
|
Border: ColourUtil.HexToRgba("#9D5CFF44"),
|
||||||
SurfaceHover: ColourUtil.HexToRgba("#251D40"),
|
TextPrimary: ColourUtil.HexToRgba("#E6E0F5"),
|
||||||
Border: ColourUtil.HexToRgba("#9D5CFF44"),
|
TextMuted: ColourUtil.HexToRgba("#9890B5"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#5A5570"),
|
||||||
TextPrimary: ColourUtil.HexToRgba("#E6E0F5"),
|
StatusSuccess: ColourUtil.HexToRgba("#26A269"),
|
||||||
TextMuted: ColourUtil.HexToRgba("#9890B5"),
|
StatusDanger: ColourUtil.HexToRgba("#ED333B"),
|
||||||
TextDim: ColourUtil.HexToRgba("#5A5570"),
|
StatusWarning: ColourUtil.HexToRgba("#E0AB36"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#9D5CFF")
|
||||||
StatusSuccess: ColourUtil.HexToRgba("#26A269"),
|
),
|
||||||
StatusDanger: ColourUtil.HexToRgba("#ED333B"),
|
Layout: new ThemeLayout(
|
||||||
StatusWarning: ColourUtil.HexToRgba("#E0AB36"),
|
WindowRounding: 6f,
|
||||||
StatusInfo: ColourUtil.HexToRgba("#9D5CFF")
|
ChildRounding: 5f,
|
||||||
),
|
PopupRounding: 5f,
|
||||||
Layout: new ThemeLayout(
|
FrameRounding: 4f,
|
||||||
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
GrabRounding: 4f,
|
||||||
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
TabRounding: 4f,
|
||||||
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
ScrollbarRounding: 4f,
|
||||||
),
|
WindowBorderSize: 1f,
|
||||||
Typography: new ThemeTypography(),
|
FrameBorderSize: 1f
|
||||||
IsBuiltIn: true,
|
),
|
||||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
Typography: new ThemeTypography(),
|
||||||
{
|
IsBuiltIn: true,
|
||||||
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
|
ChatColors: new ThemeChatColors(
|
||||||
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
|
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
// Lila. Channel-Identität bleibt klar erkennbar.
|
{
|
||||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
|
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
|
||||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
|
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
|
||||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
|
// Lila. Channel-Identität bleibt klar erkennbar.
|
||||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
|
||||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
|
||||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"),
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
|
||||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"),
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"),
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"),
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"),
|
||||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"),
|
||||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"),
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
||||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"),
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"),
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
||||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"),
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"),
|
||||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"),
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"),
|
||||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"),
|
||||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"),
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"),
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"),
|
||||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"),
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"),
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"),
|
||||||
})
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||||
);
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user