Compare commits
543 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a46d89c197 | |||
| 57b6ead003 | |||
| a42cc2a97e | |||
| 96ff4ddfd8 | |||
| 0bfe3a62cb | |||
| 01a7f9b4ec | |||
| 0237602ab7 | |||
| a600f014eb | |||
| a35067f80a | |||
| 74b07519f5 | |||
| 8dade8c4b2 | |||
| 35e8d3a7fe | |||
| 38586db9d8 | |||
| c357873604 | |||
| 67bec11f10 | |||
| 35efdd4628 | |||
| 271a6ae650 | |||
| 003bd5c695 | |||
| e1f84a9b10 | |||
| 9745abea0c | |||
| 1e418ab86f | |||
| 1c820b7f53 | |||
| 2cc260170e | |||
| de86084dbc | |||
| f56b968768 | |||
| edab5c7a6d | |||
| 82cbf4c281 | |||
| 00ae81751b | |||
| 89384702b4 | |||
| 54316313dc | |||
| 4059b363a3 | |||
| 0220e5d756 | |||
| 2315f10d91 | |||
| 3283e51381 | |||
| 7e960371a3 | |||
| f2a2daf39d | |||
| 7d87f1c4fe | |||
| fe84fd558e | |||
| 624ad20404 | |||
| 54ff88d6d4 | |||
| c955f30422 | |||
| 7a1bd1babc | |||
| d0be75e79d | |||
| e0ead86616 | |||
| b66005daea | |||
| 0fe66d2c3c | |||
| 169168cea9 | |||
| f6d3794d87 | |||
| 763f5a3f5d | |||
| 8a18f7caaa | |||
| 5f7bfb5890 | |||
| 3be4e73c27 | |||
| 667950c98e | |||
| 3e91177833 | |||
| 51f18e46a0 | |||
| f66316161b | |||
| 679b8f0f5e | |||
| 0e470fcdce | |||
| abbbf95002 | |||
| fbbbeebade | |||
| 7c9b90c767 | |||
| b81894b859 | |||
| 655c903cb5 | |||
| 8c4afaac17 | |||
| c6a3780753 | |||
| d9f6704316 | |||
| 011490368b | |||
| 8ed10a536b | |||
| 6051e49307 | |||
| 55120e6572 | |||
| 7542d48983 | |||
| 7b36763359 | |||
| eecedd9f97 | |||
| 1003a88cad | |||
| 299fd59cbb | |||
| 74bcb91b65 | |||
| 2c64aaa251 | |||
| 607d2c7241 | |||
| b2a0f3a77c | |||
| d26c4701fa | |||
| 7f317a2b18 | |||
| 38149059c3 | |||
| 67175419a9 | |||
| d3fdcdf43d | |||
| f4ea460644 | |||
| d5735d8dcc | |||
| 80b48ac3ad | |||
| cddd29a986 | |||
| 799fdb67cc | |||
| 69fa0fecbd | |||
| fd5f970a8b | |||
| fee2459e73 | |||
| 63cad62c89 | |||
| dca5de4085 | |||
| 8edc3c70d3 | |||
| 3c33acf6d7 | |||
| c8ba8c1cd0 | |||
| 94e4828aeb | |||
| 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 | |||
| 64cadcf87b | |||
| 0165cba966 | |||
| 3da550c2fc | |||
| 4b43fdb0ad | |||
| 56621669b2 | |||
| ed2a0f7374 | |||
| 59e86cd8dd | |||
| a74e3da030 | |||
| b8ed2a1ce5 | |||
| e6c6c02780 | |||
| ab9ebedeee | |||
| 11af4ce4c4 | |||
| 8a78390a15 | |||
| 23e47e06c0 | |||
| ff60576f3c | |||
| 5b5bacfc41 | |||
| eb8b7be2f5 | |||
| eb05e04f79 | |||
| 2f0affcdbb | |||
| dfa7c47887 | |||
| acf799440e | |||
| 3e98b9103f | |||
| 4a613f7acb | |||
| af5f4d380a | |||
| ecf1e93a1b | |||
| e404a2e0d9 | |||
| d485f5ea1f | |||
| b48684ce5a | |||
| a11c8bc6e9 | |||
| 985a284e7d | |||
| e629518550 | |||
| c28c972ae3 | |||
| bc0f44712f | |||
| f663cb3c14 | |||
| 5a9c2018b0 | |||
| a1cdae05d0 | |||
| c17f5ae516 | |||
| a2db8cb639 | |||
| 507efc8cda | |||
| 6f3cf2f3ce | |||
| c979a05d6c | |||
| c53e453341 | |||
| 2519b413f8 | |||
| e5ac4faf7b | |||
| 0c26d1aa67 | |||
| 8b13ba1fdc | |||
| 52da5d5e23 | |||
| 916640fb60 | |||
| feeb1df4eb | |||
| f2086865ce | |||
| 15a89dd6e7 | |||
| 53952717c0 | |||
| fcbbd174b6 | |||
| d41cea0031 | |||
| c943a2cff3 | |||
| abcd0847ef | |||
| 2f52cbb7d4 | |||
| 9103bbb892 | |||
| 8f9c01d322 | |||
| af4651b37e | |||
| 485dc4e1b4 | |||
| c878d24d11 | |||
| cb5c940a84 | |||
| dd3a0ea069 | |||
| 4bf6c3ef1f | |||
| 2378ce6bf2 | |||
| b85db24601 | |||
| cae7d76206 | |||
| 4c6d52e652 | |||
| cbfdfe35be | |||
| 537b96c79f | |||
| d3d28924e6 | |||
| 48f1fb5ba1 | |||
| 0b13efd0b5 | |||
| 289fe2eb78 | |||
| fe9e66b0ff | |||
| 990edd8300 | |||
| db95ec7dff | |||
| 7e036c1d00 | |||
| 1c511a147d | |||
| f093d93761 | |||
| e7c8667497 | |||
| 497197eb2c | |||
| 08b2ffc600 | |||
| 8db3eca46c | |||
| 4d54eabdac | |||
| 698eb01bbe | |||
| a3fbaab173 | |||
| 57291e925d | |||
| 8e9332ac8c | |||
| fcb72e2b78 | |||
| 7012e8c0d8 | |||
| 176474ec2a | |||
| 9fc8749d15 | |||
| 09634b416d | |||
| 393ef175bf | |||
| d63c710836 | |||
| fa9baa3929 | |||
| 4c18b9a62b | |||
| 26c12c3410 | |||
| 76a4de1192 | |||
| 55aeaea5b9 | |||
| e6d25f3e38 | |||
| 740c7cf1bb | |||
| 71f0b63079 | |||
| 8ee54bb8df | |||
| e3ce41306e | |||
| af7c757e63 | |||
| a10c115b9b | |||
| 6d49dbad3e | |||
| a651b3b9ad | |||
| 3f2e56be67 | |||
| feb6e262e4 | |||
| 1d557f1b0e | |||
| fea4965889 | |||
| 91663832f0 | |||
| 9cf1b19801 | |||
| 1f7f0945c5 | |||
| cd6afb32cb | |||
| 7d5496e959 | |||
| ed426556e1 | |||
| 96c445356b | |||
| 1c2d361b77 | |||
| 581aae1735 | |||
| 70109e1896 | |||
| 0df5819a88 | |||
| 3fbbe8543f | |||
| 03dfb8e3da | |||
| a987e97610 | |||
| ecd46ed630 | |||
| f2f7599f81 | |||
| ac158907ea | |||
| 9506af49db | |||
| c882eac1ca | |||
| 7a6b44048a | |||
| 0d39d59a04 | |||
| f0e0db55e3 | |||
| f207239d56 | |||
| ccf2ec9f12 | |||
| aff7a5e7ce | |||
| cd84ca2b3f | |||
| 7c645afa1d | |||
| 24c1e0e754 | |||
| 9f6a0807d1 | |||
| 15f83c8b0e | |||
| c7253bdf02 | |||
| cf10c566dd | |||
| acfe838bc6 | |||
| 9e1f559644 | |||
| 2c79a67dae | |||
| 1687271bfd | |||
| cb5457ba2e | |||
| a701f6c103 | |||
| 8cad8651d2 | |||
| 61b547606c | |||
| 059cfa6e28 | |||
| 71d84e4486 | |||
| 92301869ed | |||
| c3d06a9c94 | |||
| 911c870e24 | |||
| 8cda19d993 | |||
| 62621ba855 | |||
| 497c259031 | |||
| 9ad9d2acd2 | |||
| 1b63765caa | |||
| 61764459ed | |||
| 1b7f2c40e6 | |||
| 93d52ae819 | |||
| 48b3d5c6b1 | |||
| e9a9d8a01c | |||
| a155a57f33 | |||
| 90b83a0690 | |||
| f10301c3e4 | |||
| 8571a936a4 | |||
| 3f6144836c | |||
| 53c432a635 | |||
| 340cadf3b9 | |||
| 8d6868aef6 | |||
| 6e8fcc8cc3 | |||
| 57670ffc76 | |||
| 2144eedd76 | |||
| 43daef83de | |||
| 4a9ad426e7 | |||
| 13beda3a8d | |||
| 18c05af4db | |||
| df6e1e1cbd | |||
| 01b1a14511 | |||
| b6af8d559c | |||
| 22dbfc2e24 | |||
| 2f3b01732c | |||
| 88803382dd | |||
| 74c51163c7 | |||
| 877ff4ba18 | |||
| ad2feb5a27 | |||
| 46b63ffdd1 | |||
| 4ba5004322 | |||
| 3584c94523 | |||
| 303729f3d3 | |||
| 12085ff1e2 | |||
| e4593a0fda | |||
| 3fc42963ae | |||
| 7c52e890e6 | |||
| 4d977d5118 | |||
| ddd72a878e | |||
| 66450dd518 | |||
| 7de28ef9b2 | |||
| da3c1f6832 | |||
| e66ae1f5b4 | |||
| 281a1e172f | |||
| 45a5035426 | |||
| e1931fc7d2 | |||
| 2201478a54 | |||
| 50963ccf1b | |||
| fde85e6d69 | |||
| c22b169b73 | |||
| 6839ccaf34 | |||
| fa108c2271 | |||
| 395a0d7c98 | |||
| b76bfb3cfc | |||
| 0512e4729c | |||
| 654f24c609 | |||
| 0e2a14197c | |||
| 52e163a472 | |||
| e086afe2a8 | |||
| c97ce7543b | |||
| cca4571470 | |||
| 444d7f8e2e | |||
| 71ae95d79c | |||
| 9a38f7f094 | |||
| c33e519bb9 | |||
| 14e585ef63 | |||
| d4aa3971c5 | |||
| e9ec587e3b | |||
| 39cd7ab801 | |||
| bb6259e14d | |||
| 757370dd53 | |||
| 3f35b76c54 | |||
| 74bdc4f927 | |||
| eb379d84ef | |||
| 7add74dbbe | |||
| e91c7a3888 | |||
| f8b0804321 | |||
| a9d4e9bd69 | |||
| 7e3e4c8b72 | |||
| 397c84be2c | |||
| 269708150d | |||
| a2977ef75b | |||
| baa4d011e8 | |||
| 4810e8b518 | |||
| 133f5c536f | |||
| 92bb368d2b | |||
| 07f47f32e3 | |||
| 141fcbf074 | |||
| 32c410e8e2 | |||
| 824037e55f | |||
| 173cb76bea | |||
| 2736551505 | |||
| 0679a0e57a | |||
| 02cbfff748 | |||
| 9c86619c9f | |||
| 6b44310e04 | |||
| 59332ce9ea | |||
| 462530dec5 | |||
| 8e964ca498 | |||
| 1f2cb000a2 | |||
| 4f25c2756b | |||
| de0d2c80cd | |||
| 2ce30383d9 | |||
| a857714064 | |||
| 705c7d3116 | |||
| bf5d03c7ea | |||
| 960ce980d3 | |||
| c09aa26ffc | |||
| c2801c4113 | |||
| 7bacd1aaba | |||
| 23e0f37dfb | |||
| 96fa05dc9b | |||
| d891ec5e50 | |||
| e219b3e1fe | |||
| 135f7a9bf7 | |||
| 81d3c9ca6b | |||
| cb90c6ab93 | |||
| 2ad81cc3ef |
+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
|
##############################################################
|
||||||
ChatTwo/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
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
# Verifies that every push to main and every PR still builds against the
|
||||||
|
# current Dalamud staging branch. Does not produce release artefacts; the
|
||||||
|
# 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:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Minimum permissions for a build-only workflow: read the repo, nothing
|
||||||
|
# else. Closes the CodeQL "Workflow does not contain permissions" alert
|
||||||
|
# and matches the principle-of-least-privilege the security guide
|
||||||
|
# recommends for workflows that don't push or create releases.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (Release)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Download Dalamud staging
|
||||||
|
run: |
|
||||||
|
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||||
|
mkdir -p "$hooks"
|
||||||
|
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||||
|
unzip -oq dalamud.zip -d "$hooks"
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: dotnet restore HellionChat/HellionChat.csproj
|
||||||
|
|
||||||
|
- name: Build (Release)
|
||||||
|
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
name: Forge Announce
|
||||||
|
|
||||||
|
# Triggered when a vX.Y.Z tag is pushed. Reads .github/forge-posts/<tag>.md
|
||||||
|
# (Frontmatter + DE bullet body) and the matching English block from
|
||||||
|
# HellionChat/HellionChat.yaml, builds a Discord-Webhook embed and posts
|
||||||
|
# it to the Hellion Forge #changelog channel.
|
||||||
|
#
|
||||||
|
# Decoupled from release.yml: a fail here does not block the GitHub
|
||||||
|
# release, and a fail there does not block the announce. Spec lives in
|
||||||
|
# the Vault under "Hellion Chat Forge-Auto-Announce Spec".
|
||||||
|
#
|
||||||
|
# Security: the only user-controlled inputs that enter run-steps are the
|
||||||
|
# tag name and the frontmatter values from a repo-internal markdown file.
|
||||||
|
# Tag name is read via env: (TAG_NAME, $env:TAG_NAME) and validated against
|
||||||
|
# ^v\d+\.\d+\.\d+$ before any string interpolation. Frontmatter values are
|
||||||
|
# parsed by regex with explicit length caps. No webhook event payload data
|
||||||
|
# (issue titles, PR bodies, commit messages, etc.) flows into run-steps.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Existing tag to (re)post, e.g. v1.1.0'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
announce:
|
||||||
|
name: Post changelog to Hellion Forge
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret
|
||||||
|
# on Gitea (Settings → Actions → Secrets). Repo-level secrets are in
|
||||||
|
# scope for every job by default, no environment: declaration needed.
|
||||||
|
timeout-minutes: 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# On push:tags github.ref points at the tag commit; on workflow_dispatch
|
||||||
|
# the user supplies the tag explicitly. Always check out that tag so
|
||||||
|
# the yaml + forge-posts file are read from the tagged tree, not main.
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||||
|
|
||||||
|
# Build embed-payload as a JSON file on disk. PowerShell-Core (pwsh)
|
||||||
|
# ships pre-installed on ubuntu-latest so we get the same scripting
|
||||||
|
# patterns release.yml uses on windows-latest. Tag is read via env: to
|
||||||
|
# treat it as a string variable rather than inline shell text, and
|
||||||
|
# validated against the semver regex before any interpolation.
|
||||||
|
- name: Build embed payload
|
||||||
|
id: build
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
|
run: |
|
||||||
|
$tag = $env:TAG_NAME
|
||||||
|
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
|
throw "V1: Refusing to announce non-semver tag: $tag"
|
||||||
|
}
|
||||||
|
$version = $tag.Substring(1)
|
||||||
|
|
||||||
|
# ---------- Forge-Post-Datei lesen ----------
|
||||||
|
$forgePath = ".github/forge-posts/$tag.md"
|
||||||
|
if (-not (Test-Path $forgePath)) {
|
||||||
|
throw "V2: Forge-Post-Datei für $tag fehlt unter .github/forge-posts/. Datei vor dem Tag anlegen, dann Tag re-pushen oder workflow_dispatch."
|
||||||
|
}
|
||||||
|
$forgeRaw = Get-Content -Path $forgePath -Raw
|
||||||
|
|
||||||
|
# Frontmatter (--- … ---) am Datei-Anfang
|
||||||
|
if ($forgeRaw -notmatch '(?s)\A---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)\z') {
|
||||||
|
throw "V3: Frontmatter (---) fehlt oder ist defekt in $forgePath"
|
||||||
|
}
|
||||||
|
$fmText = $matches[1]
|
||||||
|
$deBody = $matches[2].Trim()
|
||||||
|
|
||||||
|
$subtitle = $null
|
||||||
|
$versionsnatur = $null
|
||||||
|
foreach ($line in ($fmText -split "`r?`n")) {
|
||||||
|
if ($line -match '^subtitle:\s*"?([^"]*)"?\s*$') { $subtitle = $matches[1] }
|
||||||
|
if ($line -match '^versionsnatur:\s*"?([^"]*)"?\s*$') { $versionsnatur = $matches[1] }
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($subtitle)) { throw "V3: Frontmatter-Feld 'subtitle' fehlt in $forgePath" }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($versionsnatur)) { throw "V3: Frontmatter-Feld 'versionsnatur' fehlt in $forgePath" }
|
||||||
|
if ($subtitle.Length -gt 60) { throw "V4: Frontmatter-Feld 'subtitle' überschreitet Limit ($($subtitle.Length) Char, max 60)" }
|
||||||
|
if ($versionsnatur.Length -gt 40) { throw "V4: Frontmatter-Feld 'versionsnatur' überschreitet Limit ($($versionsnatur.Length) Char, max 40)" }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($deBody)) { throw "V3: DE-Body fehlt in $forgePath" }
|
||||||
|
|
||||||
|
# ---------- EN-Block aus HellionChat.yaml ziehen ----------
|
||||||
|
# 1:1 Pattern aus release.yml — gleicher Header-Marker, gleiches
|
||||||
|
# Trailer-Verhalten. Bei Drift die zwei Workflows synchron halten.
|
||||||
|
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||||
|
$raw = Get-Content -Path $yamlPath -Raw
|
||||||
|
$marker = "changelog: |-"
|
||||||
|
$idx = $raw.IndexOf($marker)
|
||||||
|
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
||||||
|
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||||
|
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||||
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
|
}) -join "`n"
|
||||||
|
|
||||||
|
$header = "**v$version "
|
||||||
|
$start = $changelogBody.IndexOf($header)
|
||||||
|
if ($start -lt 0) {
|
||||||
|
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
||||||
|
}
|
||||||
|
$rest = $changelogBody.Substring($start)
|
||||||
|
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||||
|
$trailer = $rest.IndexOf("`n`n---")
|
||||||
|
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||||
|
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||||
|
} elseif ($trailer -ge 0) {
|
||||||
|
$enBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||||
|
} else {
|
||||||
|
$enBlock = $rest.TrimEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- Embed-Felder + Per-Field-Caps (Discord-Hard-Limits) ----------
|
||||||
|
# Discord enforces per-embed-field limits separately from the
|
||||||
|
# combined-total limit. We split the DE and EN blocks into two
|
||||||
|
# embeds that share the same release URL so Discord stitches
|
||||||
|
# them into one visual card. Hard caps per Discord docs:
|
||||||
|
# description: 4096 per embed
|
||||||
|
# title: 256 per embed
|
||||||
|
# footer.text: 2048 per embed
|
||||||
|
# combined sum across all embeds: 6000
|
||||||
|
$title = "Hellion Chat $version — $subtitle"
|
||||||
|
$deDesc = "**Deutsch**`n`n$deBody"
|
||||||
|
$enDesc = "**English**`n`n$enBlock"
|
||||||
|
$footerText = "Hellion Forge · $versionsnatur"
|
||||||
|
$releaseUrl = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||||
|
|
||||||
|
if ($deDesc.Length -gt 4096) {
|
||||||
|
throw "V6a: DE-Body too long for one embed ($($deDesc.Length) chars, max 4096). Trim .github/forge-posts/$tag.md or post the announcement manually (see forge style §8)."
|
||||||
|
}
|
||||||
|
if ($enDesc.Length -gt 4096) {
|
||||||
|
throw "V6b: EN-Block too long for one embed ($($enDesc.Length) chars, max 4096). Trim the changelog entry in HellionChat/HellionChat.yaml or post manually."
|
||||||
|
}
|
||||||
|
$totalChars = $title.Length + $deDesc.Length + $enDesc.Length + $footerText.Length
|
||||||
|
if ($totalChars -gt 6000) {
|
||||||
|
throw "V6c: Combined embed chars $totalChars exceed Discord's 6000-total limit. Major-Release detected — post manually via Bot/Multi-Embed (see forge style §8)."
|
||||||
|
}
|
||||||
|
Write-Host "Embed-Caps OK: de=$($deDesc.Length)/4096, en=$($enDesc.Length)/4096, total=$totalChars/6000"
|
||||||
|
|
||||||
|
# ---------- Embed-Payload bauen (zwei Embeds, gleiche url) ----------
|
||||||
|
# Sharing the same `url` tells Discord to render both embeds as a
|
||||||
|
# single contiguous card block. The title sits on the first embed,
|
||||||
|
# the footer + timestamp on the last so it reads as one post.
|
||||||
|
$payload = [ordered]@{
|
||||||
|
username = "Forge Herald"
|
||||||
|
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||||
|
content = "<@&1500489631555260446>"
|
||||||
|
allowed_mentions = [ordered]@{
|
||||||
|
parse = @()
|
||||||
|
roles = @("1500489631555260446")
|
||||||
|
}
|
||||||
|
embeds = @(
|
||||||
|
[ordered]@{
|
||||||
|
title = $title
|
||||||
|
url = $releaseUrl
|
||||||
|
color = 12730636
|
||||||
|
description = $deDesc
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
url = $releaseUrl
|
||||||
|
color = 12730636
|
||||||
|
description = $enDesc
|
||||||
|
footer = [ordered]@{ text = $footerText }
|
||||||
|
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$payloadJson = $payload | ConvertTo-Json -Depth 8 -Compress
|
||||||
|
# Ausgabe-Datei ohne trailing newline für sauberes curl --data-binary @-
|
||||||
|
[System.IO.File]::WriteAllText("$PWD/embed-payload.json", $payloadJson, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
Write-Host "Payload size: $($payloadJson.Length) chars"
|
||||||
|
Write-Host "Embed title: $title"
|
||||||
|
Write-Host "Embed footer: $footerText"
|
||||||
|
|
||||||
|
# POST to the Hellion Forge changelog webhook. curl from PowerShell-Core
|
||||||
|
# so we can pipe the payload via stdin (--data-binary @-) and keep
|
||||||
|
# secrets out of process arg lists. One retry on 5xx, hard fail on 4xx.
|
||||||
|
- name: POST to Hellion Forge webhook
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
DISCORD_FORGE_WEBHOOK: ${{ secrets.DISCORD_FORGE_WEBHOOK }}
|
||||||
|
run: |
|
||||||
|
if ([string]::IsNullOrEmpty($env:DISCORD_FORGE_WEBHOOK)) {
|
||||||
|
throw "V7: DISCORD_FORGE_WEBHOOK secret is empty. Check Settings → Environments → Webhook."
|
||||||
|
}
|
||||||
|
|
||||||
|
$payloadFile = "$PWD/embed-payload.json"
|
||||||
|
if (-not (Test-Path $payloadFile)) {
|
||||||
|
throw "Embed payload file missing — previous step did not produce embed-payload.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxAttempts = 2
|
||||||
|
$attempt = 0
|
||||||
|
while ($attempt -lt $maxAttempts) {
|
||||||
|
$attempt++
|
||||||
|
Write-Host "POST attempt $attempt of $maxAttempts"
|
||||||
|
$tmpResp = "$PWD/.webhook-response"
|
||||||
|
$tmpHeaders = "$PWD/.webhook-headers"
|
||||||
|
# --silent suppresses progress; --show-error prints errors so
|
||||||
|
# the workflow log shows what happened. -w prints HTTP status
|
||||||
|
# to stdout for inspection. -o captures body for diagnosis,
|
||||||
|
# -D captures headers.
|
||||||
|
$rawStatus = Get-Content $payloadFile -Raw |
|
||||||
|
curl --silent --show-error `
|
||||||
|
--header 'Content-Type: application/json' `
|
||||||
|
--data-binary '@-' `
|
||||||
|
-D $tmpHeaders `
|
||||||
|
-o $tmpResp `
|
||||||
|
-w '%{http_code}' `
|
||||||
|
"$env:DISCORD_FORGE_WEBHOOK"
|
||||||
|
$status = [int]$rawStatus
|
||||||
|
Write-Host "HTTP status: $status"
|
||||||
|
|
||||||
|
if ($status -ge 200 -and $status -lt 300) {
|
||||||
|
Write-Host "Forge announce POST succeeded."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$bodySnippet = ""
|
||||||
|
if (Test-Path $tmpResp) {
|
||||||
|
$bodySnippet = (Get-Content $tmpResp -Raw -ErrorAction SilentlyContinue)
|
||||||
|
if ($bodySnippet.Length -gt 500) { $bodySnippet = $bodySnippet.Substring(0, 500) + " …" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status -ge 400 -and $status -lt 500) {
|
||||||
|
# E2: 4xx is permanent — webhook revoked, channel deleted,
|
||||||
|
# payload malformed. No retry.
|
||||||
|
throw "E2: Discord-Webhook returned permanent $status. Body: $bodySnippet"
|
||||||
|
}
|
||||||
|
|
||||||
|
# E1: 5xx (or transport-level fail with status 0) — wait + retry once
|
||||||
|
if ($attempt -lt $maxAttempts) {
|
||||||
|
Write-Host "Transient $status — sleeping 30s before retry."
|
||||||
|
Start-Sleep -Seconds 30
|
||||||
|
} else {
|
||||||
|
throw "E1: Discord-Webhook returned transient $status after $maxAttempts attempts. Body: $bodySnippet"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
|
||||||
|
# current Dalamud staging branch, locates the latest.zip produced by
|
||||||
|
# DalamudPackager and attaches it to the matching Gitea Release.
|
||||||
|
#
|
||||||
|
# User-controlled inputs touched by this workflow:
|
||||||
|
# - the tag name (filtered by on.tags = v*, validated again at runtime
|
||||||
|
# against ^v\d+\.\d+\.\d+$ before being used in any string)
|
||||||
|
# All other values are either repo-controlled (paths under
|
||||||
|
# HellionChat/bin/Release derived from find / Get-ChildItem) or pinned
|
||||||
|
# URLs to goatcorp / gitea. Nothing from a webhook event payload (issue/PR
|
||||||
|
# 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:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
|
||||||
|
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
|
||||||
|
# ref step below hard-fails if a non-tag ref is selected, because the
|
||||||
|
# release-action reads GITHUB_REF directly and rejects anything that
|
||||||
|
# does not start with refs/tags/.
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build and attach release ZIP
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# release-action@main reads GITHUB_REF directly (its action.yml
|
||||||
|
# does not declare a tag_name input). Validate up-front so manual
|
||||||
|
# dispatches from a branch ref fail loud here instead of burning
|
||||||
|
# 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
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup .NET 10
|
||||||
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Download Dalamud staging
|
||||||
|
run: |
|
||||||
|
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||||
|
mkdir -p "$hooks"
|
||||||
|
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||||
|
unzip -oq dalamud.zip -d "$hooks"
|
||||||
|
|
||||||
|
- name: Build (Release)
|
||||||
|
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
|
||||||
|
|
||||||
|
- name: Locate latest.zip
|
||||||
|
id: locate
|
||||||
|
run: |
|
||||||
|
zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
|
||||||
|
if [ -z "$zip" ]; then
|
||||||
|
echo "latest.zip not found under HellionChat/bin/Release" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Found: $zip"
|
||||||
|
echo "path=$zip" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Build a release body from the matching changelog block in
|
||||||
|
# HellionChat.yaml plus a static install / docs footer. Fails the
|
||||||
|
# workflow if no block exists for the tagged version, which is the
|
||||||
|
# automated counterpart to the "yaml + repo.json + release body
|
||||||
|
# kept in sync" rule.
|
||||||
|
#
|
||||||
|
# GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
|
||||||
|
# tag value is treated as a PowerShell variable, not as inline shell
|
||||||
|
# text. The strict regex below rejects anything that is not a clean
|
||||||
|
# semver tag before it is used to build a string.
|
||||||
|
- name: Generate release body
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
# github.ref_name is the tag because Validate tag ref above
|
||||||
|
# already enforced refs/tags/v*. Read via env: so the value
|
||||||
|
# is a PowerShell variable, not inline shell text, and gets
|
||||||
|
# re-validated against the semver regex below.
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
$tag = $env:TAG_NAME
|
||||||
|
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
|
throw "Refusing to generate release body for non-semver tag: $tag"
|
||||||
|
}
|
||||||
|
$version = $tag.Substring(1)
|
||||||
|
|
||||||
|
$yamlPath = "HellionChat/HellionChat.yaml"
|
||||||
|
$raw = Get-Content -Path $yamlPath -Raw
|
||||||
|
|
||||||
|
$marker = "changelog: |-"
|
||||||
|
$idx = $raw.IndexOf($marker)
|
||||||
|
if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }
|
||||||
|
|
||||||
|
# changelog: is the last top-level key in the manifest, so
|
||||||
|
# everything after the marker is the literal block. Strip the
|
||||||
|
# 4-space yaml indent (prettier convention) from each line.
|
||||||
|
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||||
|
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||||
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
|
}) -join "`n"
|
||||||
|
|
||||||
|
# Subblock convention: "**vX.Y.Z — <subtitle> (<date>)**"
|
||||||
|
# matches verify-changelog-sync.sh and slim-rule grep.
|
||||||
|
$header = "**v$version "
|
||||||
|
$start = $changelogBody.IndexOf($header)
|
||||||
|
if ($start -lt 0) {
|
||||||
|
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
||||||
|
}
|
||||||
|
|
||||||
|
$rest = $changelogBody.Substring($start)
|
||||||
|
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||||
|
$trailer = $rest.IndexOf("`n`n---")
|
||||||
|
|
||||||
|
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||||
|
$currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||||
|
} elseif ($trailer -ge 0) {
|
||||||
|
$currentBlock = $rest.Substring(0, $trailer).TrimEnd()
|
||||||
|
} else {
|
||||||
|
$currentBlock = $rest.TrimEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static install / docs / licence footer is maintained as a
|
||||||
|
# separate file so the workflow YAML stays clean (no embedded
|
||||||
|
# heredoc that would have to be indented under the run-block).
|
||||||
|
$footerPath = ".github/release-footer.md"
|
||||||
|
if (-not (Test-Path $footerPath)) {
|
||||||
|
throw "Release footer template not found: $footerPath"
|
||||||
|
}
|
||||||
|
$footer = Get-Content -Path $footerPath -Raw
|
||||||
|
|
||||||
|
$body = $currentBlock + "`n" + $footer
|
||||||
|
$body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline
|
||||||
|
|
||||||
|
Write-Host "Generated release body for $tag :"
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
Write-Host $body
|
||||||
|
Write-Host "----------------------------------------"
|
||||||
|
|
||||||
|
# release-action@main only declares files/title/body/pre_release/
|
||||||
|
# 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:
|
||||||
|
files: ${{ steps.locate.outputs.path }}
|
||||||
|
body: ${{ steps.body.outputs.content }}
|
||||||
|
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# HellionChat is a hobby project and does not solicit funding.
|
||||||
|
#
|
||||||
|
# If you want to support the work that made HellionChat possible,
|
||||||
|
# please consider supporting the upstream Chat 2 maintainers:
|
||||||
|
#
|
||||||
|
# Infiziert90 (Infi): https://ko-fi.com/infiii
|
||||||
|
# Anna Clemens: https://ko-fi.com/lojewalo
|
||||||
|
#
|
||||||
|
# Both Ko-fi pages are also linked in the plugin's settings panel.
|
||||||
|
|
||||||
|
# No platforms enabled — keep this file present so GitHub recognises
|
||||||
|
# the project as having considered funding without showing a Sponsor
|
||||||
|
# button on the repository page.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Something in HellionChat is broken or behaves wrong
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for reporting. Please fill in the fields below so I can
|
||||||
|
reproduce the issue. If this is a security issue, stop here and
|
||||||
|
report it privately to [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D)
|
||||||
|
instead.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: HellionChat version
|
||||||
|
description: From Settings → Information → Version
|
||||||
|
placeholder: '0.5.4'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
options:
|
||||||
|
- Windows (XIVLauncher)
|
||||||
|
- Linux (XIVLauncher Core)
|
||||||
|
- macOS (XIVLauncher Core / wine)
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened
|
||||||
|
description: Plain description, no log dumps yet
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: What you expected
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: How to reproduce
|
||||||
|
description: Step-by-step from "open settings" or "log in" through to the broken behaviour
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Relevant /xllog excerpt
|
||||||
|
description: Filter for "HellionChat" if the log is huge
|
||||||
|
render: text
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirm
|
||||||
|
attributes:
|
||||||
|
label: Pre-flight
|
||||||
|
options:
|
||||||
|
- label: I am running the latest version of HellionChat
|
||||||
|
required: true
|
||||||
|
- label: I have searched existing issues for duplicates
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: Security vulnerability
|
||||||
|
url: mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||||
|
about: Do not open a public issue for security problems. Report by e-mail instead.
|
||||||
|
|
||||||
|
- name: Upstream Chat 2 issue
|
||||||
|
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.
|
||||||
|
|
||||||
|
- name: Discord
|
||||||
|
url: https://discord.com/users/j.j_kazama
|
||||||
|
about: Quick questions, casual feedback. Bug reports still go through the issue tracker for tracking.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest a feature or enhancement for HellionChat
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for the suggestion. HellionChat focuses on privacy by
|
||||||
|
default and a small, well-scoped feature set. Suggestions that
|
||||||
|
align with that scope are easier to accept than ones that pull
|
||||||
|
the plugin toward "do everything".
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: What problem are you trying to solve
|
||||||
|
description: The user-side problem, not the proposed solution yet
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: What you would like HellionChat to do
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives you have considered
|
||||||
|
description: Other plugins, manual workarounds, settings combinations
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: scope
|
||||||
|
attributes:
|
||||||
|
label: Scope estimate from your side
|
||||||
|
options:
|
||||||
|
- 'Small (one tab, one toggle, one filter)'
|
||||||
|
- 'Medium (a settings section, persistent state, one new file)'
|
||||||
|
- 'Large (architectural, touches the message pipeline or the database)'
|
||||||
|
- "I don't know"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirm
|
||||||
|
attributes:
|
||||||
|
label: Pre-flight
|
||||||
|
options:
|
||||||
|
- label: I have searched existing issues for similar requests
|
||||||
|
required: true
|
||||||
|
- label: I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat 2
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<!--
|
||||||
|
Thanks for contributing to HellionChat. Please fill in the sections
|
||||||
|
below so the review goes quickly. Delete sections that genuinely do
|
||||||
|
not apply, but do not delete the whole template.
|
||||||
|
|
||||||
|
If this is a security fix, stop here and report it privately by
|
||||||
|
e-mail instead of opening a public PR:
|
||||||
|
mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- One or two sentences. What does this PR change and why. -->
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
<!-- Tick all that apply. -->
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change that adds behaviour)
|
||||||
|
- [ ] Breaking change (config migration, removed feature, or behaviour change that user-visible
|
||||||
|
defaults rely on)
|
||||||
|
- [ ] Documentation only
|
||||||
|
- [ ] Translation update
|
||||||
|
- [ ] Build, CI or tooling change
|
||||||
|
|
||||||
|
## Linked issue
|
||||||
|
|
||||||
|
<!-- e.g. "Closes #42" or "Refs #42". For trivial typo fixes, "n/a". -->
|
||||||
|
|
||||||
|
## How I tested this
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Built locally with `dotnet build -c Release`
|
||||||
|
- Ran `dotnet test`
|
||||||
|
- Loaded the plugin in-game on Windows / Linux / macOS via XIVLauncher
|
||||||
|
- Specific scenarios I exercised in-game
|
||||||
|
-->
|
||||||
|
|
||||||
|
## User-visible changes
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Anything the end user will notice. New settings, changed defaults,
|
||||||
|
new commands, new translations, removed behaviour. If none, write
|
||||||
|
"none".
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Compatibility notes
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- Does this require a configuration migration? If yes, which version
|
||||||
|
bump and is it covered by the existing migration tests?
|
||||||
|
- Does this change the schema in MessageStore?
|
||||||
|
- Does this change the repo.json or HellionChat.yaml manifest fields?
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and
|
||||||
|
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
|
||||||
|
- [ ] My change matches the existing code style (`.editorconfig`).
|
||||||
|
- [ ] I added or updated tests where the existing test infrastructure made that practical, or I have
|
||||||
|
explained why tests are not applicable.
|
||||||
|
- [ ] I updated the README, in-plugin strings or documentation if my change is user-visible.
|
||||||
|
- [ ] I did not include any AI-generated code without disclosing it in the PR description (see
|
||||||
|
[AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)).
|
||||||
|
- [ ] I confirm my contribution is released under the [EUPL-1.2](../LICENSE).
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
# NuGet package updates for the plugin project. Weekly cadence keeps the
|
||||||
|
# noise down while still catching transitive security advisories within
|
||||||
|
# a few days of disclosure.
|
||||||
|
- package-ecosystem: nuget
|
||||||
|
directory: /HellionChat
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: monday
|
||||||
|
time: '07:00'
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
- nuget
|
||||||
|
commit-message:
|
||||||
|
prefix: 'chore(deps)'
|
||||||
|
groups:
|
||||||
|
patches:
|
||||||
|
update-types:
|
||||||
|
- patch
|
||||||
|
minor:
|
||||||
|
update-types:
|
||||||
|
- minor
|
||||||
|
|
||||||
|
# GitHub Actions versions in .github/workflows. Lower cadence because
|
||||||
|
# Action releases ship less frequently and are usually safe to defer
|
||||||
|
# for a month.
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
time: '07:00'
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
- github-actions
|
||||||
|
commit-message:
|
||||||
|
prefix: 'chore(actions)'
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Theme Foundation"
|
||||||
|
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
|
||||||
|
- 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
|
||||||
|
- Optional pro Theme eigene Chat-Channel-Farben mit Übernehmen/Behalten-Banner — niemals automatisch
|
||||||
|
überschrieben
|
||||||
|
- 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
|
||||||
|
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Layout Refresh"
|
||||||
|
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
|
||||||
|
- Top-Tabs bekommen eine Akzent-Underline statt Background-Fill am aktiven Tab
|
||||||
|
- 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
|
||||||
|
- 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)
|
||||||
|
- 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
|
||||||
|
- Migration v14 → v15: alte Theme-Felder entfernt, alle anderen Settings bleiben
|
||||||
|
|
||||||
|
Animation-Polish (Lerps, Theme-Crossfade, Quick-Picker) folgt in v1.3.0.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
subtitle: "24 Sprachen, Inter Light statt Exo 2, HITCH 74 → 20 ms"
|
||||||
|
versionsnatur: "Localisation + Font-Stack"
|
||||||
|
---
|
||||||
|
- **24 wählbare UI-Sprachen.** Aus dem ursprünglich nur als FR-Lokalisierung geplanten Cycle ist eine breite Welle geworden: Catalan, Czech, Danish, Dutch, English, Finnish, French, German, Greek, Hungarian, Italian, Japanese, Korean, Norsk bokmål, Polish, Portuguese (BR), Portuguese (PT), Romanian, Russian, Spanish, Swedish, Turkish, Ukrainian, Simplified Chinese, Traditional Chinese. Dropdown sortiert alphabetisch nach Endonym, „None" oben angepinnt. Nicht-native Übersetzungen sind AI-assisted und für Community-Review im Forge-Discord markiert.
|
||||||
|
- **Inter Light statt Exo 2 als bundled Schrift.** Plus NotoSansCjkRegular als dritte Merge-Schicht. Damit deckt der Stack Latin Extended-A/B, Greek polytonic, Cyrillic Supplement und CJK (inkl. Hangul, Simplified-Han nach Reform) ab — die nicht-vanilla-FFXIV-Sprachen waren mit Exo 2 nicht lesbar.
|
||||||
|
- **HITCH 74 → ~20 ms als Side-Effect.** Der UiBuilder-First-Frame-Lag lag seit v1.4.x stabil bei 74 ms; v1.5.1 wollte ihn in Richtung 7 ms ziehen, fiel als „Hypothese zu optimistisch" durch. Echter Grund: `Plugin.cs:937` push'te `RegularFont` nur wenn `FontsEnabled` true war — die „Mitgelieferte Schrift verwenden"-Logik setzte `FontsEnabled = false` mit, der bundled-Pfad war die ganze v1.5.x-Reihe tot, FFXIVs Axis-Font übernahm und kostete ~50 ms extra. Fix routet `RegularFont` jetzt auch über `UseHellionFont`. Median ~20 ms im 5-Reload-Stresstest (17.9-23.6 ms, Linux/Wine; Windows-Baseline steht aus).
|
||||||
|
- **Glyph-Ranges aktivieren sich automatisch beim Sprachwechsel** plus eine One-Shot-Migration für User die schon eine non-Latin-Sprache eingestellt hatten. Neue WarningText unter dem Sprach-Dropdown weist darauf hin, dass FFXIVs Chat-Engine offiziell nur EN/DE/FR/JA-Glyphen rendert — andere Schriften können in der Game-Eingabe Garbled-Output zeigen.
|
||||||
|
- **Unter der Haube.** Drei-Layer-Font-Stack, zwei neue ExtraGlyphRanges-Flags (`LatinExtended`, `Greek`), `LanguageOverride`-Enum wächst um zehn Locales plus drei reaktivierte (Italian, Korean, Norwegian mit `nb`). Append-only damit User-Configs stabil bleiben. Migration v17 bleibt.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Theme-Crossfade, Quick-Picker, Hover-Animationen"
|
||||||
|
versionsnatur: "Polish & Motion"
|
||||||
|
---
|
||||||
|
- **Theme-Crossfade.** Theme-Wechsel blenden jetzt sanft über rund 300 ms ineinander, statt hart umzuschalten. Alle Hellion-Flächen gleiten mit: Sidebar, Titel, Buttons, Tabs, Scrollbar, Trennlinien. Der Fenster-Hintergrund snappt bewusst weiter, damit das Per-Window-Deckkraft-Setting aus Dalamuds Pinning-Menü unangetastet bleibt.
|
||||||
|
- **Header-Quick-Picker.** Neuer Paletten-Button links vom Zahnrad im Chat-Header. Ein Klick öffnet ein kompaktes Popup mit zwei Sektionen: alle Built-in- und Custom-Themes sowie alle Tabs. Der aktive Eintrag trägt ein Häkchen, ein Klick wechselt ohne das Popup zu schließen. So lassen sich mehrere Wechsel hintereinander erledigen, ohne den Umweg über die Einstellungen.
|
||||||
|
- **Sanfte Hover-Animationen.** Sidebar-Icons faden bei Hover sanft von gedimmt auf volle Deckkraft. Card-Mode-Trennlinien heben sich beim Überfahren einer Zeile für den ganzen Tab dezent ab. Beides framerate-unabhängig gerechnet, also auch bei Wine-Stall-Frames stabil.
|
||||||
|
- **Bewegung reduzieren.** Neuer Toggle im Tab für Theme und Layout. Er deaktiviert Crossfade, Hover-Animationen und das Pulsieren ungelesener Tabs für alle, die eine statische Oberfläche bevorzugen.
|
||||||
|
- Drei P3-Items plus der Accessibility-Toggle, kein Schema-Bump, keine Migration. Eine kleine Polish-Welle vor den größeren Cycles.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
## How to install
|
||||||
|
|
||||||
|
This release is distributed via the HellionChat custom repository, not the Dalamud main plugin repo.
|
||||||
|
To install:
|
||||||
|
|
||||||
|
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
||||||
|
2. Add the URL:
|
||||||
|
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||||
|
3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install
|
||||||
|
|
||||||
|
## Project documents
|
||||||
|
|
||||||
|
- [README](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/README.md)
|
||||||
|
— features, architecture, build
|
||||||
|
- [Privacy notice](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/PRIVACY.md)
|
||||||
|
— what the plugin stores and sends
|
||||||
|
- [Third-party notices](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/docs/THIRD_PARTY_NOTICES.md)
|
||||||
|
— 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
|
||||||
|
|
||||||
|
[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, also EUPL-1.2.
|
||||||
+459
-219
@@ -1,33 +1,193 @@
|
|||||||
## Ignore Visual Studio temporary files, build results, and
|
##############################################################
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
##
|
##
|
||||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
## .gitignore – Hellion Forge / Hellion Media
|
||||||
|
##
|
||||||
|
## Basis: github/gitignore VisualStudio.gitignore
|
||||||
|
## Überarbeitet: Mai 2026
|
||||||
|
## Status: Original-Patterns vollständig erhalten,
|
||||||
|
## neu sortiert in logische Sektionen,
|
||||||
|
## Sicherheits- & Tooling-Sektionen ergänzt.
|
||||||
|
##
|
||||||
|
## Markierungen:
|
||||||
|
## [!! OBSOLET 2026 !!] → Tool offiziell eingestellt,
|
||||||
|
## Pattern bleibt aus Vorsicht drin.
|
||||||
|
##
|
||||||
|
##############################################################
|
||||||
|
|
||||||
# Local development environment (HellionChat fork)
|
|
||||||
|
# =====================================================
|
||||||
|
# [!! KRITISCH !!] Secrets, Keys & Credentials
|
||||||
|
# Diese Sachen dürfen NIEMALS im Repo landen!
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Environment Files
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
.env.bak*
|
.env.bak*
|
||||||
.envrc
|
.envrc
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.sample
|
||||||
|
|
||||||
|
# Private Keys & Zertifikate
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.cer
|
||||||
|
*.crt
|
||||||
|
*.csr
|
||||||
|
*.gpg
|
||||||
|
*.asc
|
||||||
|
|
||||||
|
# SSH Keys (falls jemand die ins Repo legt)
|
||||||
|
id_rsa
|
||||||
|
id_ed25519
|
||||||
|
id_ecdsa
|
||||||
|
known_hosts
|
||||||
|
|
||||||
|
# Auth-/Token-Files
|
||||||
|
auth.json
|
||||||
|
.npmrc
|
||||||
|
.pypirc
|
||||||
|
secrets.json
|
||||||
|
|
||||||
|
# ASP.NET / .NET App-Configs mit lokalen Secrets
|
||||||
|
appsettings.*.local.json
|
||||||
|
appsettings.Local.json
|
||||||
|
local.settings.json
|
||||||
|
|
||||||
|
# Memory Dumps (können Credentials im Heap enthalten!)
|
||||||
|
*.dmp
|
||||||
|
*.mdmp
|
||||||
|
crash.log
|
||||||
|
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Projekt-spezifisch (HellionChat Fork)
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
# Lokale Entwicklungsumgebung
|
||||||
.vscode/
|
.vscode/
|
||||||
scripts/
|
scripts/setup-dev-env.sh
|
||||||
|
|
||||||
|
# Lokales Test-Projekt (bleibt aus dem Plugin-Repo raus;
|
||||||
|
# pure-function safety net für Refactor-Cycles)
|
||||||
|
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/
|
||||||
@@ -43,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
|
||||||
@@ -101,6 +242,7 @@ StyleCopReport.xml
|
|||||||
*.tmp_proj
|
*.tmp_proj
|
||||||
*_wpftmp.csproj
|
*_wpftmp.csproj
|
||||||
*.log
|
*.log
|
||||||
|
*.binlog
|
||||||
*.vspscc
|
*.vspscc
|
||||||
*.vssscc
|
*.vssscc
|
||||||
.builds
|
.builds
|
||||||
@@ -108,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
|
||||||
@@ -121,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
|
||||||
@@ -224,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
|
||||||
@@ -233,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
|
||||||
@@ -284,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
|
||||||
@@ -312,66 +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
|
|
||||||
|
|
||||||
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
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# AI assistance disclosure
|
|
||||||
|
|
||||||
Per the [Dalamud Plugin AI Usage Policy](https://github.com/goatcorp/DalamudPluginsD17/),
|
|
||||||
this fork uses AI assistance at the **Pair** level. Pair means the maintainer
|
|
||||||
plans the architecture, decides what gets built, reviews each change and
|
|
||||||
tests against the running game; Claude (Anthropic) helps explain Dalamud
|
|
||||||
APIs, suggests patterns, drafts code on request, and reviews approaches.
|
|
||||||
Neither side acts autonomously: nothing ships without the maintainer's
|
|
||||||
review, and Claude can't run the game.
|
|
||||||
|
|
||||||
The level varies by area and over time. Some commits are mostly hand-written
|
|
||||||
with the AI used as a sounding board, others lean more on Claude for an API
|
|
||||||
walkthrough or a code draft that the maintainer then reads, edits and
|
|
||||||
integrates. The maintainer's commitment is to be able to explain why every
|
|
||||||
piece of Hellion code is the way it is — not "I typed every character."
|
|
||||||
|
|
||||||
## What's where
|
|
||||||
|
|
||||||
Upstream Chat 2 (by Infi & Anna, EUPL-1.2) is the foundation and was not
|
|
||||||
produced with AI assistance. Hellion-specific code lives in
|
|
||||||
`ChatTwo/Privacy/`, `ChatTwo/Export/`, `Resources/HellionStrings*`,
|
|
||||||
`Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`,
|
|
||||||
plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs`
|
|
||||||
and `Plugin.cs`. These were developed with Pair-level assistance as
|
|
||||||
described above; the share of human vs. AI authorship varies file by file
|
|
||||||
and is expected to keep shifting toward more hand-written work as the
|
|
||||||
maintainer's plugin-dev experience grows.
|
|
||||||
|
|
||||||
## What AI is not used for
|
|
||||||
|
|
||||||
- **Visual assets.** Logos, icons, banners, screenshots are human-drawn or
|
|
||||||
taken from the running game.
|
|
||||||
- **German translations.** Written by the maintainer (native speaker).
|
|
||||||
|
|
||||||
## Tooling
|
|
||||||
|
|
||||||
- Claude (Anthropic) via Claude Code CLI as the main pair partner.
|
|
||||||
- Context7 / Microsoft Learn for current Dalamud and .NET documentation.
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
|
|
||||||
Questions about this disclosure: <https://github.com/JonKazama-Hellion/HellionChat/issues>.
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## A Note on This Project
|
||||||
|
|
||||||
|
HellionChat is a one-person side project developed under Hellion Forge. I maintain this in my spare
|
||||||
|
time, which means replies can take a few days. Please do not escalate just because a thread is
|
||||||
|
quiet.
|
||||||
|
|
||||||
|
When in doubt, assume good intent. Contributors come from different backgrounds, time zones and
|
||||||
|
skill levels. A clarifying question is almost always a better first move than an accusation.
|
||||||
|
|
||||||
|
Please also keep discussions on topic. This project is about a Dalamud chat plugin. Off-topic
|
||||||
|
arguments belong elsewhere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We pledge to make our community welcoming, safe, and equitable for all.
|
||||||
|
|
||||||
|
We are committed to fostering an environment that respects and promotes the dignity, rights, and
|
||||||
|
contributions of all individuals, regardless of characteristics including race, ethnicity, caste,
|
||||||
|
color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or
|
||||||
|
expression, sexual orientation, language, philosophy or religion, national or social origin,
|
||||||
|
socio-economic position, level of education, or other status. The same privileges of participation
|
||||||
|
are extended to everyone who participates in good faith and in accordance with this Covenant.
|
||||||
|
|
||||||
|
## Encouraged Behaviors
|
||||||
|
|
||||||
|
While acknowledging differences in social norms, we all strive to meet our community's expectations
|
||||||
|
for positive behavior. We also understand that our words and actions may be interpreted differently
|
||||||
|
than we intend based on culture, background, or native language.
|
||||||
|
|
||||||
|
With these considerations in mind, we agree to behave mindfully toward each other and act in ways
|
||||||
|
that center our shared values, including:
|
||||||
|
|
||||||
|
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
|
||||||
|
2. Engaging **kindly and honestly** with others.
|
||||||
|
3. Respecting **different viewpoints** and experiences.
|
||||||
|
4. **Taking responsibility** for our actions and contributions.
|
||||||
|
5. Gracefully giving and accepting **constructive feedback**.
|
||||||
|
6. Committing to **repairing harm** when it occurs.
|
||||||
|
7. Behaving in other ways that promote and sustain the **well-being of our community**.
|
||||||
|
|
||||||
|
## Restricted Behaviors
|
||||||
|
|
||||||
|
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of
|
||||||
|
these behaviors are violations of this Code of Conduct.
|
||||||
|
|
||||||
|
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal
|
||||||
|
attention after any clear request to stop.
|
||||||
|
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a
|
||||||
|
community member or group of people.
|
||||||
|
3. **Stereotyping or discrimination.** Characterizing anyone's personality or behavior on the basis
|
||||||
|
of immutable identities or traits.
|
||||||
|
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate
|
||||||
|
in the context or purpose of the community.
|
||||||
|
5. **Violating confidentiality.** Sharing or acting on someone's personal or private information
|
||||||
|
without their permission.
|
||||||
|
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person
|
||||||
|
or group.
|
||||||
|
7. Behaving in other ways that **threaten the well-being** of our community.
|
||||||
|
|
||||||
|
### Other Restrictions
|
||||||
|
|
||||||
|
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone
|
||||||
|
else to evade enforcement actions.
|
||||||
|
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
|
||||||
|
3. **Promotional materials.** Sharing marketing or other commercial content in a way that is outside
|
||||||
|
the norms of the community.
|
||||||
|
4. **Irresponsible communication.** Failing to responsibly present content which includes, links to,
|
||||||
|
or describes any other restricted behaviors.
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
If something here is being broken, contact me directly. Do not open a public issue.
|
||||||
|
|
||||||
|
| Channel | Address |
|
||||||
|
| ---------- | -------------------------- |
|
||||||
|
| Email | `kontakt@hellion-media.de` |
|
||||||
|
| Discord DM | `@j.j_kazama` |
|
||||||
|
|
||||||
|
Reports stay private. I will acknowledge within a few weekdays (European business hours) and tell
|
||||||
|
you what I plan to do.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
I am the sole maintainer, so enforcement is a single-person process. I will pick the lightest
|
||||||
|
measure that actually resolves the situation:
|
||||||
|
|
||||||
|
1. Private note asking the behaviour to stop.
|
||||||
|
2. Public correction in the affected thread.
|
||||||
|
3. Edit or removal of the offending content.
|
||||||
|
4. Private written warning with a cooldown period.
|
||||||
|
5. Temporary block from the repository or related spaces.
|
||||||
|
6. Permanent block.
|
||||||
|
|
||||||
|
Severe cases skip the lower steps. I will not negotiate over harassment or threats.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies to all spaces the project owns or that I run on its behalf: the GitHub
|
||||||
|
repository, GitHub Discussions, project-related Discord conversations, and the maintainer contact
|
||||||
|
listed in [`SECURITY.md`](SECURITY.md). It also applies when someone is identifiably representing
|
||||||
|
HellionChat elsewhere, for example when posting as a HellionChat maintainer in the Dalamud Discord.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, available at
|
||||||
|
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
|
||||||
|
|
||||||
|
Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA
|
||||||
|
4.0. To view a copy of this license, visit
|
||||||
|
[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
# Contributing to HellionChat
|
||||||
|
|
||||||
|
Thanks for taking a look. HellionChat is a one-person side project developed under Hellion Forge. It
|
||||||
|
started as a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo) and has since become a
|
||||||
|
standalone plugin under its own namespace, IPC channels and 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 to make a contribution land
|
||||||
|
smoothly.
|
||||||
|
|
||||||
|
## Before You Open Anything
|
||||||
|
|
||||||
|
- Read the [README](README.md) so you understand the scope: a privacy-focused, EUPL-1.2-licensed
|
||||||
|
Dalamud plugin that intentionally removes the upstream webinterface and ships privacy-first
|
||||||
|
defaults.
|
||||||
|
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Active cherry-picking from upstream Chat 2
|
||||||
|
has ended in the v1.4.x cycle; HellionChat continues as an independent codebase. Existing
|
||||||
|
upstream-derived code keeps its attribution. New contributions stand on their own and do not need
|
||||||
|
to be cherry-pick-compatible.
|
||||||
|
- Read [`SECURITY.md`](SECURITY.md). Anything security-sensitive goes through a private advisory,
|
||||||
|
never a public issue or PR.
|
||||||
|
- Read the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
## What I Will Accept
|
||||||
|
|
||||||
|
- Bug fixes for behaviour documented in the README, the in-plugin settings or the changelog.
|
||||||
|
- Translation contributions for Hellion-specific strings via direct pull requests against
|
||||||
|
`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).
|
||||||
|
- Performance fixes with a measurable before/after.
|
||||||
|
- New features that fit the privacy-first scope and do not duplicate what an existing Dalamud plugin
|
||||||
|
already does well.
|
||||||
|
|
||||||
|
## What I Will Probably Decline
|
||||||
|
|
||||||
|
- Re-introducing the webinterface or any remote-access feature. It was removed in v0.2.0 on purpose.
|
||||||
|
See the README section "Was gegenüber Chat 2 fehlt".
|
||||||
|
- Features that bypass the privacy filter or weaken the default retention behaviour without an
|
||||||
|
explicit, documented opt-in.
|
||||||
|
- Sweeping refactors that touch large parts of the codebase. The maintenance cost outweighs the
|
||||||
|
benefit for a one-person project. (This used to be doubly important because of the upstream
|
||||||
|
cherry-pick path; that path is closed now, but the rule still holds on its own merits.)
|
||||||
|
- AI-generated code dropped in without disclosure or human review. See
|
||||||
|
[`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) for how I handle AI assistance on my side; I
|
||||||
|
expect comparable transparency from contributors.
|
||||||
|
|
||||||
|
If you are unsure whether an idea fits, open a feature-request issue first and ask before writing
|
||||||
|
code. I would rather say "no" to a proposal than to a finished pull request.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Open an issue (bug or feature request) using the templates under `.github/ISSUE_TEMPLATE/`. Skip
|
||||||
|
this for trivial typos.
|
||||||
|
2. Fork the repository and branch off `main`. Branch naming is informal; something like
|
||||||
|
`fix/auto-tell-history-empty` or `feat/theme-export` is fine.
|
||||||
|
3. Match the existing code style. The repository ships an `.editorconfig` that VS Code and Rider
|
||||||
|
pick up automatically.
|
||||||
|
4. Keep commits focused. Several small commits with clear messages are easier to review than one
|
||||||
|
large one. Squash-on-merge happens at the PR level if needed.
|
||||||
|
5. If your change touches user-visible behaviour, update the README and/or the changelog block in
|
||||||
|
`HellionChat/HellionChat.yaml` and `repo.json`. I bump the version number myself at release time.
|
||||||
|
6. Open the pull request against `main`. The PR template will ask you to summarise the change, the
|
||||||
|
testing you did and any compatibility notes.
|
||||||
|
|
||||||
|
## Build and Test
|
||||||
|
|
||||||
|
The project targets `net10.0-windows` against Dalamud SDK 15. To build locally you need:
|
||||||
|
|
||||||
|
- .NET 10 SDK
|
||||||
|
- A working Dalamud dev environment with `DALAMUD_HOME` set (XIVLauncher installed and launched once
|
||||||
|
is the simplest path)
|
||||||
|
- VS Code with the C# Dev Kit, Rider, or Visual Studio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
dotnet build HellionChat.sln -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
There are currently no tests in `HellionChat.sln`. If you add a test project, point it at the
|
||||||
|
relevant subsystems (privacy filter, configuration migration, message store) and mention it in the
|
||||||
|
PR.
|
||||||
|
|
||||||
|
For a smoke test in-game: build, copy the output into your Dalamud `devPlugins/HellionChat/`
|
||||||
|
directory and load it via `/xlplugins`.
|
||||||
|
|
||||||
|
## Continuous Integration
|
||||||
|
|
||||||
|
Every push and every pull request runs:
|
||||||
|
|
||||||
|
| Workflow | What it checks |
|
||||||
|
| ------------ | -------------------------------- |
|
||||||
|
| `build.yml` | `dotnet build` and `dotnet test` |
|
||||||
|
| `codeql.yml` | CodeQL security analysis |
|
||||||
|
|
||||||
|
A pull request will not be merged while either of these is failing. CodeQL findings on changed code
|
||||||
|
need to be addressed; pre-existing findings on untouched code are tracked separately.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx` (English source) and
|
||||||
|
`HellionStrings.<lang>.resx` (per-language). These are accepted as direct pull requests.
|
||||||
|
|
||||||
|
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are **not** translated here.
|
||||||
|
They are kept as-is from the last upstream sync and remain the work of the Chat 2 Crowdin community.
|
||||||
|
Active cherry-picking from upstream ended in the v1.4.x cycle (see
|
||||||
|
[`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
|
||||||
|
|
||||||
|
By submitting a pull request you confirm that:
|
||||||
|
|
||||||
|
- Your contribution is your own work, or you have the right to contribute it under the project
|
||||||
|
licence.
|
||||||
|
- You agree that your contribution will be released under the [EUPL-1.2](LICENSE), the same licence
|
||||||
|
as the rest of the project.
|
||||||
|
|
||||||
|
There is no separate CLA. Forking HellionChat is explicitly permitted under the EUPL-1.2, as with
|
||||||
|
any EUPL-licensed project.
|
||||||
|
|
||||||
|
## Response Times
|
||||||
|
|
||||||
|
| Channel | Address |
|
||||||
|
| ------------- | --------------------------------------- |
|
||||||
|
| GitHub Issues | Preferred for bugs and feature requests |
|
||||||
|
| Discord DM | `@j.j_kazama` |
|
||||||
|
| Email | `kontakt@hellion-media.de` |
|
||||||
|
|
||||||
|
I respond on weekdays during European business hours and take weekends and FFXIV patch days off. A
|
||||||
|
pull request that sits for a few days has not been ignored. Pinging once after a week is fine;
|
||||||
|
please do not 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.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
Source code
|
||||||
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
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
|
||||||
|
plugin this fork is built on. Their work covers the message store,
|
||||||
|
the channel filtering, the sidebar tab system, the FFXIV chat
|
||||||
|
hooks, the localisation infrastructure and most of the
|
||||||
|
architecture HellionChat still relies on.
|
||||||
|
|
||||||
|
Copyright (c) 2025-2026 Florian Wathling / Hellion Online Media
|
||||||
|
HellionChat-specific modifications, including the privacy filter,
|
||||||
|
per-channel retention sweep, export pipeline, Auto-Tell-Tabs,
|
||||||
|
German localisation and the EUPL-1.2 fork maintenance.
|
||||||
|
|
||||||
|
Source code is licensed under the European Union Public Licence
|
||||||
|
(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
|
||||||
|
at: <https://eupl.eu/1.2/en/>
|
||||||
|
|
||||||
|
This Work is provided "AS IS" without warranties of any kind. See
|
||||||
|
Article 7 (Disclaimer of Warranty) and Article 8 (Disclaimer of
|
||||||
|
Liability) of the Licence for the legally binding wording.
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
Visual assets
|
||||||
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
Copyright (c) 2026 Florian Eck
|
||||||
|
|
||||||
|
Designer of the Hellion Forge logo and Hellion Online Media logo
|
||||||
|
(variants located in docs/images and HellionChat/images).
|
||||||
|
Exclusive usage and marketing rights licensed to Hellion Online
|
||||||
|
Media. These assets are NOT covered by the EUPL-1.2 source code
|
||||||
|
licence above and may not be reused, modified, or redistributed
|
||||||
|
without separate permission from the copyright holder.
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
Bundled assets
|
||||||
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Exo 2 font (HellionChat/Resources/HellionFont.ttf)
|
||||||
|
SIL Open Font License 1.1, full text in HellionFont-OFL.txt.
|
||||||
|
Bundled with permission per the OFL terms.
|
||||||
|
|
||||||
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
Acknowledgements directed at the upstream ChatTwo authors live in
|
||||||
|
NOTICE.md. The manual upstream-sync workflow lives in UPSTREAM_SYNC.md.
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>net10.0-windows</TargetFrameworks>
|
|
||||||
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.6.3" />
|
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.6.3" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\ChatTwo\ChatTwo.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev</DalamudLibPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
|
|
||||||
<DalamudLibPath>$(DALAMUD_HOME)</DalamudLibPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsCI)' == 'true'">
|
|
||||||
<DalamudLibPath>$(HOME)/dalamud</DalamudLibPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Dalamud">
|
|
||||||
<HintPath>$(DalamudLibPath)\Dalamud.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="FFXIVClientStructs">
|
|
||||||
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Lumina">
|
|
||||||
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Lumina.Excel">
|
|
||||||
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
|
|
||||||
<Private>false</Private>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using ChatTwo.Code;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Dalamud.Game.Text;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
||||||
using Chat2PartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
|
|
||||||
|
|
||||||
namespace ChatTwo.Tests;
|
|
||||||
|
|
||||||
[TestClass]
|
|
||||||
[TestSubject(typeof(MessageStore))]
|
|
||||||
public class MessageStoreTest {
|
|
||||||
// From Message.cs
|
|
||||||
private static readonly byte[] ExtraChatChannelPayloadBytes = [0, 0x27, 18, 0x20];
|
|
||||||
|
|
||||||
public TestContext TestContext { get; set; }
|
|
||||||
|
|
||||||
public static string GetImportPath() {
|
|
||||||
string[] importPaths = [
|
|
||||||
@".\TestData",
|
|
||||||
@"..\TestData",
|
|
||||||
@"..\..\TestData",
|
|
||||||
@"..\..\..\TestData",
|
|
||||||
];
|
|
||||||
var importPath = importPaths.FirstOrDefault(Directory.Exists);
|
|
||||||
if (string.IsNullOrEmpty(importPath)) {
|
|
||||||
throw new DirectoryNotFoundException("Could not find the import path");
|
|
||||||
}
|
|
||||||
return importPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void StoreAndRetrieve() {
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
|
||||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
|
||||||
TestContext.WriteLine("Using database path: " + dbPath);
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
|
|
||||||
// Write the message.
|
|
||||||
var input = BigMessage();
|
|
||||||
store.UpsertMessage(input);
|
|
||||||
|
|
||||||
// Read the message back.
|
|
||||||
using var messageEnumerator = store.GetMostRecentMessages();
|
|
||||||
var messages = messageEnumerator.ToList();
|
|
||||||
Assert.AreEqual(1, messages.Count);
|
|
||||||
AssertMessagesEqual(input, messages.First());
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
public void RetrieveMultiple() {
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
|
||||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
|
||||||
TestContext.WriteLine("Using database path: " + dbPath);
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
|
|
||||||
// Insert 10 messages in the wrong order of date.
|
|
||||||
var messages = new List<Message>();
|
|
||||||
const uint receiver = 12345;
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
for (var i = 0; i < 10; i++) {
|
|
||||||
var message = BigMessage(true, receiver, now.AddSeconds(-i));
|
|
||||||
TestContext.WriteLine($"Inserting message {i}: {message.Id}");
|
|
||||||
store.UpsertMessage(message);
|
|
||||||
messages.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert a message for a different receiver. This shouldn't be returned
|
|
||||||
// because of the receiver filtering.
|
|
||||||
var otherReceiverMsg = BigMessage(receiver: receiver + 1, dateTime: now.AddSeconds(1));
|
|
||||||
TestContext.WriteLine($"Inserting other receiver message: {otherReceiverMsg.Id}");
|
|
||||||
store.UpsertMessage(otherReceiverMsg);
|
|
||||||
|
|
||||||
// Query the most recent 5 messages. Should return the 4 newest messages
|
|
||||||
// from the list, as well as the different receiver message because we
|
|
||||||
// aren't filtering.
|
|
||||||
using var unfilteredMessageEnumerator = store.GetMostRecentMessages(count: 5);
|
|
||||||
var outputMessages = unfilteredMessageEnumerator.ToList();
|
|
||||||
var gotIds = outputMessages.Select(m => m.Id).ToList();
|
|
||||||
TestContext.WriteLine($"Query 1 got IDs: {string.Join(", ", gotIds)}");
|
|
||||||
AssertGuidsEqual(new List<Guid> {
|
|
||||||
messages[3].Id,
|
|
||||||
messages[2].Id,
|
|
||||||
messages[1].Id,
|
|
||||||
messages[0].Id,
|
|
||||||
otherReceiverMsg.Id
|
|
||||||
}, gotIds);
|
|
||||||
|
|
||||||
// Query the most recent 5 messages but filter by receiver ID.
|
|
||||||
using var filteredByReceiverMessageEnumerator = store.GetMostRecentMessages(receiver: receiver, count: 5);
|
|
||||||
outputMessages = filteredByReceiverMessageEnumerator.ToList();
|
|
||||||
gotIds = outputMessages.Select(m => m.Id).ToList();
|
|
||||||
TestContext.WriteLine($"Query 2 got IDs: {string.Join(", ", gotIds)}");
|
|
||||||
AssertGuidsEqual(new List<Guid> {
|
|
||||||
messages[4].Id,
|
|
||||||
messages[3].Id,
|
|
||||||
messages[2].Id,
|
|
||||||
messages[1].Id,
|
|
||||||
messages[0].Id,
|
|
||||||
}, gotIds);
|
|
||||||
|
|
||||||
// Query the most recent 5 messages but only since a specific date.
|
|
||||||
using var filteredByReceiverAndDateMessageEnumerator = store.GetMostRecentMessages(receiver, since: messages[1].Date, count: 5);
|
|
||||||
outputMessages = filteredByReceiverAndDateMessageEnumerator.ToList();
|
|
||||||
gotIds = outputMessages.Select(m => m.Id).ToList();
|
|
||||||
TestContext.WriteLine($"Query 3 got IDs: {string.Join(", ", gotIds)}");
|
|
||||||
AssertGuidsEqual(new List<Guid> {
|
|
||||||
messages[1].Id,
|
|
||||||
messages[0].Id,
|
|
||||||
}, gotIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(5000)]
|
|
||||||
// This test guards against the data format changing in an incompatible way.
|
|
||||||
public void RetrieveExisting() {
|
|
||||||
var input = BigMessage(uniqId: false);
|
|
||||||
|
|
||||||
var dbPath = Path.Join(GetImportPath(), "existing.db");
|
|
||||||
TestContext.WriteLine($"Using existing database: {dbPath}");
|
|
||||||
Assert.IsTrue(File.Exists(dbPath));
|
|
||||||
|
|
||||||
// Uncomment this section to regenerate the existing database.
|
|
||||||
/*
|
|
||||||
File.Delete(dbPath);
|
|
||||||
using (var newStore = new MessageStore(dbPath)) {
|
|
||||||
newStore.UpsertMessage(input);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
using var existingMessageEnumerator = store.GetMostRecentMessages();
|
|
||||||
var output = existingMessageEnumerator.ToList();
|
|
||||||
Assert.AreEqual(1, output.Count);
|
|
||||||
AssertMessagesEqual(input, output[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
[Timeout(30_000)]
|
|
||||||
public void ProfileMany() {
|
|
||||||
const int count = 20_000;
|
|
||||||
|
|
||||||
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
|
|
||||||
var dbPath = Path.Join(tempDir.FullName, "test.db");
|
|
||||||
TestContext.WriteLine("Using database path: " + dbPath);
|
|
||||||
using var store = new MessageStore(dbPath);
|
|
||||||
|
|
||||||
for (var i = 0; i < count; i++) {
|
|
||||||
var message = BigMessage(uniqId: true);
|
|
||||||
store.UpsertMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var messageEnumerator = store.GetMostRecentMessages(count: count);
|
|
||||||
var messages = messageEnumerator.ToList();
|
|
||||||
Assert.AreEqual(count, messages.Count);
|
|
||||||
foreach (var message in messages) {
|
|
||||||
// Load the message because they are lazily parsed.
|
|
||||||
Assert.IsTrue(message.Id != Guid.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Message BigMessage(bool uniqId = true, uint receiver = 12345, DateTimeOffset? dateTime = null) {
|
|
||||||
// NOTE: These values aren't valid in the game.
|
|
||||||
// NOTE: we can't test UiForeground, UiGlow, or AutoTranslatePayload
|
|
||||||
// because they load data from the game.
|
|
||||||
var senderSeString = new SeStringBuilder()
|
|
||||||
.AddText("<")
|
|
||||||
.Add(new PlayerPayload("Player Name", 12345))
|
|
||||||
.AddItalics("Player Name")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddText(">: ")
|
|
||||||
.Build();
|
|
||||||
var extraChatId = Guid.Parse("03d9e6d4-dc1a-4005-bbe7-66b8c3529277");
|
|
||||||
var contentSeString = new SeStringBuilder()
|
|
||||||
.Add(new RawPayload(ExtraChatChannelPayloadBytes.Concat(extraChatId.ToByteArray()).ToArray()))
|
|
||||||
.AddIcon(BitmapFontIcon.IslandSanctuary)
|
|
||||||
.AddMapLink(1, 2, 3, 4)
|
|
||||||
.AddText("map")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddQuestLink(12345)
|
|
||||||
.AddText("quest")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.Add(new DalamudLinkPayload())
|
|
||||||
.AddText("dalamud")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddStatusLink(12345)
|
|
||||||
.AddText("status")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.AddPartyFinderLink(12345)
|
|
||||||
.AddText("party finder")
|
|
||||||
.Add(RawPayload.LinkTerminator)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Add Chat 2 specific payloads (that can't be serialized into the
|
|
||||||
// SeString).
|
|
||||||
var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, ChatType.Say).ToList();
|
|
||||||
contentChunks = contentChunks.Concat([
|
|
||||||
new TextChunk(ChunkSource.Content, new Chat2PartyFinderPayload(12345), "chat 2 party finder"),
|
|
||||||
new TextChunk(ChunkSource.Content, new AchievementPayload(12345), "chat 2 achievement"),
|
|
||||||
new TextChunk(ChunkSource.Content, new UriPayload(new Uri("https://dalamud.dev")), "chat 2 uri"),
|
|
||||||
]).ToList();
|
|
||||||
|
|
||||||
var chatCode = new ChatCode((XivChatType)46, XivChatRelationKind.LocalPlayer, XivChatRelationKind.EngagedEnemy);
|
|
||||||
return new Message(
|
|
||||||
uniqId ? Guid.NewGuid() : Guid.Parse("f011343e-6a21-49e5-a6f9-238f0f1f8c2c"),
|
|
||||||
receiver,
|
|
||||||
54321,
|
|
||||||
dateTime ?? DateTimeOffset.FromUnixTimeMilliseconds(1713520182440),
|
|
||||||
chatCode,
|
|
||||||
ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, ChatType.Debug).ToList(),
|
|
||||||
contentChunks,
|
|
||||||
senderSeString,
|
|
||||||
contentSeString,
|
|
||||||
extraChatId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void AssertMessagesEqual(Message input, Message output) {
|
|
||||||
// Check basic fields.
|
|
||||||
Assert.AreEqual(input.Id, output.Id);
|
|
||||||
Assert.AreEqual(input.Receiver, output.Receiver);
|
|
||||||
Assert.AreEqual(input.ContentId, output.ContentId);
|
|
||||||
// Assert time is within 1 second
|
|
||||||
var timeDifference = Math.Abs(input.Date.ToUniversalTime().Subtract(output.Date.ToUniversalTime()).TotalSeconds);
|
|
||||||
Assert.IsTrue(timeDifference < 1);
|
|
||||||
Assert.AreEqual(input.Code, output.Code);
|
|
||||||
Assert.AreEqual($"{input.SenderSource.Encode():X}", $"{output.SenderSource.Encode():X}");
|
|
||||||
Assert.AreEqual($"{input.ContentSource.Encode():X}", $"{output.ContentSource.Encode():X}");
|
|
||||||
Assert.AreEqual(input.SortCodeV2, output.SortCodeV2);
|
|
||||||
Assert.AreEqual(input.ExtraChatChannel, output.ExtraChatChannel);
|
|
||||||
|
|
||||||
// Check chunks.
|
|
||||||
AssertChunksEqual(input.Sender, output.Sender);
|
|
||||||
AssertChunksEqual(input.Content, output.Content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssertChunksEqual(IReadOnlyList<Chunk> inputChunks, IReadOnlyList<Chunk> outputChunks) {
|
|
||||||
Assert.AreEqual(inputChunks.Count, outputChunks.Count);
|
|
||||||
for (var i = 0; i < inputChunks.Count; i++) {
|
|
||||||
var inputChunk = inputChunks[i];
|
|
||||||
var outputChunk = outputChunks[i];
|
|
||||||
Assert.AreEqual(inputChunk.Source, outputChunk.Source);
|
|
||||||
switch (inputChunk.Link) {
|
|
||||||
case AchievementPayload inputAchievementPayload:
|
|
||||||
Assert.AreEqual(inputAchievementPayload.Id, ((AchievementPayload) outputChunk.Link)!.Id);
|
|
||||||
break;
|
|
||||||
case Chat2PartyFinderPayload inputPartyFinderPayload:
|
|
||||||
Assert.AreEqual(inputPartyFinderPayload.Id, ((Chat2PartyFinderPayload) outputChunk.Link)!.Id);
|
|
||||||
break;
|
|
||||||
case UriPayload inputUriPayload:
|
|
||||||
Assert.AreEqual(inputUriPayload.Uri, ((UriPayload) outputChunk.Link)!.Uri);
|
|
||||||
break;
|
|
||||||
case null:
|
|
||||||
Assert.IsTrue(outputChunk.Link == null);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Assert.AreEqual($"{inputChunk.Link.Encode():X}", $"{outputChunk.Link!.Encode():X}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (inputChunk) {
|
|
||||||
case TextChunk inputTextChunk:
|
|
||||||
var outputTextChunk = (TextChunk)outputChunk;
|
|
||||||
Assert.AreEqual(inputTextChunk.FallbackColour, outputTextChunk.FallbackColour);
|
|
||||||
Assert.AreEqual(inputTextChunk.Foreground, outputTextChunk.Foreground);
|
|
||||||
Assert.AreEqual(inputTextChunk.Glow, outputTextChunk.Glow);
|
|
||||||
Assert.AreEqual(inputTextChunk.Italic, outputTextChunk.Italic);
|
|
||||||
Assert.AreEqual(inputTextChunk.Content, outputTextChunk.Content);
|
|
||||||
break;
|
|
||||||
case IconChunk inputIconChunk:
|
|
||||||
Assert.AreEqual(inputIconChunk.Icon, ((IconChunk) outputChunk).Icon);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Exception("Unknown chunk type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AssertGuidsEqual(IReadOnlyList<Guid> expected, IReadOnlyList<Guid> got) {
|
|
||||||
Assert.AreEqual(expected.Count, got.Count);
|
|
||||||
for (var i = 0; i < expected.Count; i++) {
|
|
||||||
Assert.AreEqual(expected[i].ToString(), got[i].ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
BIN
Binary file not shown.
-22
@@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatTwo", "ChatTwo\ChatTwo.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatTwo.Tests", "ChatTwo.Tests\ChatTwo.Tests.csproj", "{A9FE423A-240C-4EDA-ACC6-21474B562128}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
|
||||||
<PropertyGroup>
|
|
||||||
<!-- Hellion Chat versioning runs separately from upstream Chat 2.
|
|
||||||
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
|
||||||
called out in the yaml changelog so users can see what it
|
|
||||||
derives from. -->
|
|
||||||
<Version>0.1.0</Version>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<!-- HellionChat fork: assembly is renamed so Dalamud uses
|
|
||||||
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
|
|
||||||
keeping our state independent from the upstream plugin.
|
|
||||||
Code namespace stays ChatTwo.* so upstream cherry-picks
|
|
||||||
apply cleanly. -->
|
|
||||||
<AssemblyName>HellionChat</AssemblyName>
|
|
||||||
<RootNamespace>ChatTwo</RootNamespace>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
|
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
|
||||||
<PackageReference Include="Pidgin" Version="3.3.0" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
<PackageReference Include="Watson.Lite" Version="6.3.9" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Resources\Language.Designer.cs">
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Language.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Update="Resources\Language.resx">
|
|
||||||
<Generator>ResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Language.Designer.cs</LastGenOutput>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- HellionChat — Hellion-specific resource bundle (HellionStrings.resx
|
|
||||||
+ 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>
|
|
||||||
<EmbeddedResource Include="Resources\HellionFont.ttf">
|
|
||||||
<LogicalName>HellionFont.ttf</LogicalName>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
|
||||||
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="images\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!--This doesn't work until Plogon is updated to include NodeJS-->
|
|
||||||
<!-- <Target Name="NodeJS Compile" BeforeTargets="BeforeCompile">-->
|
|
||||||
<!-- <Exec Command="npm install" WorkingDirectory="Http\Frontend"/>-->
|
|
||||||
<!-- <Exec Command="npm run build" WorkingDirectory="Http\Frontend"/>-->
|
|
||||||
<!-- </Target>-->
|
|
||||||
<!-- -->
|
|
||||||
<!-- <Target Name="CopyFiles" AfterTargets="Build">-->
|
|
||||||
<!-- <ItemGroup>-->
|
|
||||||
<!-- <Files Include="$(MSBuildThisFileDirectory)\Http\Frontend\build\**" />-->
|
|
||||||
<!-- </ItemGroup>-->
|
|
||||||
<!-- -->
|
|
||||||
<!-- <Copy SourceFiles="@(Files)" DestinationFolder="$(TargetDir)\Frontend\%(RecursiveDir)" />-->
|
|
||||||
<!-- </Target>-->
|
|
||||||
|
|
||||||
<!-- <Target Name="NodeJS Compile" BeforeTargets="BeforeCompile" Condition="'$(Configuration)' == 'Debug'">-->
|
|
||||||
<!-- <Exec Command="npm install" WorkingDirectory="Http\Frontend"/>-->
|
|
||||||
<!-- <Exec Command="npm run build" WorkingDirectory="Http\Frontend"/>-->
|
|
||||||
<!-- </Target>-->
|
|
||||||
|
|
||||||
<Target Name="UnzipBuild" AfterTargets="Build">
|
|
||||||
<Unzip SourceFiles="websiteBuild.zip" DestinationFolder="$(TargetDir)\Frontend"/>
|
|
||||||
</Target>
|
|
||||||
</Project>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
using Dalamud.Game.Text;
|
|
||||||
|
|
||||||
namespace ChatTwo.Code;
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum ChatSource : ushort
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
|
|
||||||
/// <summary>The player currently controlled by the local client.</summary>
|
|
||||||
LocalPlayer = 1 << XivChatRelationKind.LocalPlayer,
|
|
||||||
|
|
||||||
/// <summary>A player in the same 4-man or 8-man party as the local player.</summary>
|
|
||||||
PartyMember = 1 << XivChatRelationKind.PartyMember,
|
|
||||||
|
|
||||||
/// <summary>A player in the same alliance raid.</summary>
|
|
||||||
AllianceMember = 1 << XivChatRelationKind.AllianceMember,
|
|
||||||
|
|
||||||
/// <summary>A player not in the local player's party or alliance.</summary>
|
|
||||||
OtherPlayer = 1 << XivChatRelationKind.OtherPlayer,
|
|
||||||
|
|
||||||
/// <summary>An enemy entity that is currently in combat with the player or party.</summary>
|
|
||||||
EngagedEnemy = 1 << XivChatRelationKind.EngagedEnemy,
|
|
||||||
|
|
||||||
/// <summary>An enemy entity that is not yet in combat or claimed.</summary>
|
|
||||||
UnengagedEnemy = 1 << XivChatRelationKind.UnengagedEnemy,
|
|
||||||
|
|
||||||
/// <summary>An NPC that is friendly or neutral to the player (e.g., EventNPCs).</summary>
|
|
||||||
FriendlyNpc = 1 << XivChatRelationKind.FriendlyNpc,
|
|
||||||
|
|
||||||
/// <summary>A pet (Summoner/Scholar) or companion (Chocobo) belonging to the local player.</summary>
|
|
||||||
PetOrCompanion = 1 << XivChatRelationKind.PetOrCompanion,
|
|
||||||
|
|
||||||
/// <summary>A pet or companion belonging to a member of the local player's party.</summary>
|
|
||||||
PetOrCompanionParty = 1 << XivChatRelationKind.PetOrCompanionParty,
|
|
||||||
|
|
||||||
/// <summary>A pet or companion belonging to a member of the alliance.</summary>
|
|
||||||
PetOrCompanionAlliance = 1 << XivChatRelationKind.PetOrCompanionAlliance,
|
|
||||||
|
|
||||||
/// <summary>A pet or companion belonging to a player not in the party or alliance.</summary>
|
|
||||||
PetOrCompanionOther = 1 << XivChatRelationKind.PetOrCompanionOther,
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using ChatTwo.Resources;
|
|
||||||
|
|
||||||
namespace ChatTwo.Code;
|
|
||||||
|
|
||||||
internal static class ChatSourceExt
|
|
||||||
{
|
|
||||||
internal const ChatSource All =
|
|
||||||
ChatSource.LocalPlayer | ChatSource.PartyMember | ChatSource.AllianceMember |
|
|
||||||
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
|
|
||||||
{
|
|
||||||
ChatSource.LocalPlayer => Language.ChatSource_Self,
|
|
||||||
ChatSource.PartyMember => Language.ChatSource_PartyMember,
|
|
||||||
ChatSource.AllianceMember => Language.ChatSource_AllianceMember,
|
|
||||||
ChatSource.OtherPlayer => Language.ChatSource_Other,
|
|
||||||
ChatSource.EngagedEnemy => Language.ChatSource_EngagedEnemy,
|
|
||||||
ChatSource.UnengagedEnemy => Language.ChatSource_UnengagedEnemy,
|
|
||||||
ChatSource.FriendlyNpc => Language.ChatSource_FriendlyNpc,
|
|
||||||
ChatSource.PetOrCompanion => Language.ChatSource_SelfPet,
|
|
||||||
ChatSource.PetOrCompanionParty => Language.ChatSource_PartyPet,
|
|
||||||
ChatSource.PetOrCompanionAlliance => Language.ChatSource_AlliancePet,
|
|
||||||
ChatSource.PetOrCompanionOther => Language.ChatSource_OtherPet,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(source), source, null),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,486 +0,0 @@
|
|||||||
using ChatTwo.Resources;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Dalamud.Game.Config;
|
|
||||||
|
|
||||||
namespace ChatTwo.Code;
|
|
||||||
|
|
||||||
internal static class ChatTypeExt
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
ChatType.Shout,
|
|
||||||
ChatType.TellIncoming,
|
|
||||||
ChatType.TellOutgoing,
|
|
||||||
ChatType.Party,
|
|
||||||
ChatType.CrossParty,
|
|
||||||
ChatType.Alliance,
|
|
||||||
ChatType.FreeCompany,
|
|
||||||
ChatType.PvpTeam,
|
|
||||||
ChatType.CrossLinkshell1,
|
|
||||||
ChatType.CrossLinkshell2,
|
|
||||||
ChatType.CrossLinkshell3,
|
|
||||||
ChatType.CrossLinkshell4,
|
|
||||||
ChatType.CrossLinkshell5,
|
|
||||||
ChatType.CrossLinkshell6,
|
|
||||||
ChatType.CrossLinkshell7,
|
|
||||||
ChatType.CrossLinkshell8,
|
|
||||||
ChatType.Linkshell1,
|
|
||||||
ChatType.Linkshell2,
|
|
||||||
ChatType.Linkshell3,
|
|
||||||
ChatType.Linkshell4,
|
|
||||||
ChatType.Linkshell5,
|
|
||||||
ChatType.Linkshell6,
|
|
||||||
ChatType.Linkshell7,
|
|
||||||
ChatType.Linkshell8,
|
|
||||||
ChatType.NoviceNetwork,
|
|
||||||
ChatType.StandardEmote,
|
|
||||||
ChatType.CustomEmote
|
|
||||||
]),
|
|
||||||
|
|
||||||
(Language.Options_Tabs_ChannelTypes_Battle,
|
|
||||||
[
|
|
||||||
ChatType.Damage,
|
|
||||||
ChatType.Miss,
|
|
||||||
ChatType.Action,
|
|
||||||
ChatType.Item,
|
|
||||||
ChatType.Healing,
|
|
||||||
ChatType.GainBuff,
|
|
||||||
ChatType.LoseBuff,
|
|
||||||
ChatType.GainDebuff,
|
|
||||||
ChatType.LoseDebuff
|
|
||||||
]),
|
|
||||||
|
|
||||||
(Language.Options_Tabs_ChannelTypes_Announcements,
|
|
||||||
[
|
|
||||||
ChatType.System,
|
|
||||||
ChatType.BattleSystem,
|
|
||||||
ChatType.GatheringSystem,
|
|
||||||
ChatType.Error,
|
|
||||||
ChatType.Echo,
|
|
||||||
ChatType.NoviceNetworkSystem,
|
|
||||||
ChatType.FreeCompanyAnnouncement,
|
|
||||||
ChatType.PvpTeamAnnouncement,
|
|
||||||
ChatType.FreeCompanyLoginLogout,
|
|
||||||
ChatType.PvpTeamLoginLogout,
|
|
||||||
ChatType.RetainerSale,
|
|
||||||
ChatType.NpcDialogue,
|
|
||||||
ChatType.NpcAnnouncement,
|
|
||||||
ChatType.LootNotice,
|
|
||||||
ChatType.Progress,
|
|
||||||
ChatType.LootRoll,
|
|
||||||
ChatType.Crafting,
|
|
||||||
ChatType.Gathering,
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
ChatType.Debug => Language.ChatType_Debug,
|
|
||||||
ChatType.Urgent => Language.ChatType_Urgent,
|
|
||||||
ChatType.Notice => Language.ChatType_Notice,
|
|
||||||
ChatType.Say => Language.ChatType_Say,
|
|
||||||
ChatType.Shout => Language.ChatType_Shout,
|
|
||||||
ChatType.TellOutgoing => Language.ChatType_TellOutgoing,
|
|
||||||
ChatType.TellIncoming => Language.ChatType_TellIncoming,
|
|
||||||
ChatType.Party => Language.ChatType_Party,
|
|
||||||
ChatType.Alliance => Language.ChatType_Alliance,
|
|
||||||
ChatType.Linkshell1 => Language.ChatType_Linkshell1,
|
|
||||||
ChatType.Linkshell2 => Language.ChatType_Linkshell2,
|
|
||||||
ChatType.Linkshell3 => Language.ChatType_Linkshell3,
|
|
||||||
ChatType.Linkshell4 => Language.ChatType_Linkshell4,
|
|
||||||
ChatType.Linkshell5 => Language.ChatType_Linkshell5,
|
|
||||||
ChatType.Linkshell6 => Language.ChatType_Linkshell6,
|
|
||||||
ChatType.Linkshell7 => Language.ChatType_Linkshell7,
|
|
||||||
ChatType.Linkshell8 => Language.ChatType_Linkshell8,
|
|
||||||
ChatType.FreeCompany => Language.ChatType_FreeCompany,
|
|
||||||
ChatType.NoviceNetwork => Language.ChatType_NoviceNetwork,
|
|
||||||
ChatType.CustomEmote => Language.ChatType_CustomEmotes,
|
|
||||||
ChatType.StandardEmote => Language.ChatType_StandardEmotes,
|
|
||||||
ChatType.Yell => Language.ChatType_Yell,
|
|
||||||
ChatType.CrossParty => Language.ChatType_CrossWorldParty,
|
|
||||||
ChatType.PvpTeam => Language.ChatType_PvpTeam,
|
|
||||||
ChatType.CrossLinkshell1 => Language.ChatType_CrossLinkshell1,
|
|
||||||
ChatType.Damage => Language.ChatType_Damage,
|
|
||||||
ChatType.Miss => Language.ChatType_Miss,
|
|
||||||
ChatType.Action => Language.ChatType_Action,
|
|
||||||
ChatType.Item => Language.ChatType_Item,
|
|
||||||
ChatType.Healing => Language.ChatType_Healing,
|
|
||||||
ChatType.GainBuff => Language.ChatType_GainBuff,
|
|
||||||
ChatType.GainDebuff => Language.ChatType_GainDebuff,
|
|
||||||
ChatType.LoseBuff => Language.ChatType_LoseBuff,
|
|
||||||
ChatType.LoseDebuff => Language.ChatType_LoseDebuff,
|
|
||||||
ChatType.Alarm => Language.ChatType_Alarm,
|
|
||||||
ChatType.GlamourNotifications => Language.ChatType_Glamour,
|
|
||||||
ChatType.Echo => Language.ChatType_Echo,
|
|
||||||
ChatType.System => Language.ChatType_System,
|
|
||||||
ChatType.BattleSystem => Language.ChatType_BattleSystem,
|
|
||||||
ChatType.GatheringSystem => Language.ChatType_GatheringSystem,
|
|
||||||
ChatType.Error => Language.ChatType_Error,
|
|
||||||
ChatType.NpcDialogue => Language.ChatType_NpcDialogue,
|
|
||||||
ChatType.LootNotice => Language.ChatType_LootNotice,
|
|
||||||
ChatType.Progress => Language.ChatType_Progress,
|
|
||||||
ChatType.LootRoll => Language.ChatType_LootRoll,
|
|
||||||
ChatType.Crafting => Language.ChatType_Crafting,
|
|
||||||
ChatType.Gathering => Language.ChatType_Gathering,
|
|
||||||
ChatType.NpcAnnouncement => Language.ChatType_NpcAnnouncement,
|
|
||||||
ChatType.FreeCompanyAnnouncement => Language.ChatType_FreeCompanyAnnouncement,
|
|
||||||
ChatType.FreeCompanyLoginLogout => Language.ChatType_FreeCompanyLoginLogout,
|
|
||||||
ChatType.RetainerSale => Language.ChatType_RetainerSale,
|
|
||||||
ChatType.PeriodicRecruitmentNotification => Language.ChatType_PeriodicRecruitmentNotification,
|
|
||||||
ChatType.Sign => Language.ChatType_Sign,
|
|
||||||
ChatType.RandomNumber => Language.ChatType_RandomNumber,
|
|
||||||
ChatType.NoviceNetworkSystem => Language.ChatType_NoviceNetworkSystem,
|
|
||||||
ChatType.Orchestrion => Language.ChatType_Orchestrion,
|
|
||||||
ChatType.PvpTeamAnnouncement => Language.ChatType_PvpTeamAnnouncement,
|
|
||||||
ChatType.PvpTeamLoginLogout => Language.ChatType_PvpTeamLoginLogout,
|
|
||||||
ChatType.MessageBook => Language.ChatType_MessageBook,
|
|
||||||
ChatType.GmTell => Language.ChatType_GmTell,
|
|
||||||
ChatType.GmSay => Language.ChatType_GmSay,
|
|
||||||
ChatType.GmShout => Language.ChatType_GmShout,
|
|
||||||
ChatType.GmYell => Language.ChatType_GmYell,
|
|
||||||
ChatType.GmParty => Language.ChatType_GmParty,
|
|
||||||
ChatType.GmFreeCompany => Language.ChatType_GmFreeCompany,
|
|
||||||
ChatType.GmLinkshell1 => Language.ChatType_GmLinkshell1,
|
|
||||||
ChatType.GmLinkshell2 => Language.ChatType_GmLinkshell2,
|
|
||||||
ChatType.GmLinkshell3 => Language.ChatType_GmLinkshell3,
|
|
||||||
ChatType.GmLinkshell4 => Language.ChatType_GmLinkshell4,
|
|
||||||
ChatType.GmLinkshell5 => Language.ChatType_GmLinkshell5,
|
|
||||||
ChatType.GmLinkshell6 => Language.ChatType_GmLinkshell6,
|
|
||||||
ChatType.GmLinkshell7 => Language.ChatType_GmLinkshell7,
|
|
||||||
ChatType.GmLinkshell8 => Language.ChatType_GmLinkshell8,
|
|
||||||
ChatType.GmNoviceNetwork => Language.ChatType_GmNoviceNetwork,
|
|
||||||
ChatType.CrossLinkshell2 => Language.ChatType_CrossLinkshell2,
|
|
||||||
ChatType.CrossLinkshell3 => Language.ChatType_CrossLinkshell3,
|
|
||||||
ChatType.CrossLinkshell4 => Language.ChatType_CrossLinkshell4,
|
|
||||||
ChatType.CrossLinkshell5 => Language.ChatType_CrossLinkshell5,
|
|
||||||
ChatType.CrossLinkshell6 => Language.ChatType_CrossLinkshell6,
|
|
||||||
ChatType.CrossLinkshell7 => Language.ChatType_CrossLinkshell7,
|
|
||||||
ChatType.CrossLinkshell8 => Language.ChatType_CrossLinkshell8,
|
|
||||||
ChatType.ExtraChatLinkshell1 => Language.ChatType_ExtraChatLinkshell1,
|
|
||||||
ChatType.ExtraChatLinkshell2 => Language.ChatType_ExtraChatLinkshell2,
|
|
||||||
ChatType.ExtraChatLinkshell3 => Language.ChatType_ExtraChatLinkshell3,
|
|
||||||
ChatType.ExtraChatLinkshell4 => Language.ChatType_ExtraChatLinkshell4,
|
|
||||||
ChatType.ExtraChatLinkshell5 => Language.ChatType_ExtraChatLinkshell5,
|
|
||||||
ChatType.ExtraChatLinkshell6 => Language.ChatType_ExtraChatLinkshell6,
|
|
||||||
ChatType.ExtraChatLinkshell7 => Language.ChatType_ExtraChatLinkshell7,
|
|
||||||
ChatType.ExtraChatLinkshell8 => Language.ChatType_ExtraChatLinkshell8,
|
|
||||||
_ => type.ToString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static uint? DefaultColor(this ChatType type)
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case ChatType.Debug:
|
|
||||||
return ColourUtil.ComponentsToRgba(204, 204, 204);
|
|
||||||
case ChatType.Urgent:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 127, 127);
|
|
||||||
case ChatType.Notice:
|
|
||||||
return ColourUtil.ComponentsToRgba(179, 140, 255);
|
|
||||||
|
|
||||||
case ChatType.Say:
|
|
||||||
case ChatType.GmSay:
|
|
||||||
return ColourUtil.ComponentsToRgba(247, 247, 247);
|
|
||||||
case ChatType.Shout:
|
|
||||||
case ChatType.GmShout:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 166, 102);
|
|
||||||
case ChatType.TellIncoming:
|
|
||||||
case ChatType.TellOutgoing:
|
|
||||||
case ChatType.GmTell:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 184, 222);
|
|
||||||
case ChatType.Party:
|
|
||||||
case ChatType.CrossParty:
|
|
||||||
case ChatType.GmParty:
|
|
||||||
return ColourUtil.ComponentsToRgba(102, 229, 255);
|
|
||||||
case ChatType.Alliance:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 127, 0);
|
|
||||||
case ChatType.NoviceNetwork:
|
|
||||||
case ChatType.NoviceNetworkSystem:
|
|
||||||
case ChatType.GmNoviceNetwork:
|
|
||||||
return ColourUtil.ComponentsToRgba(212, 255, 125);
|
|
||||||
case ChatType.Linkshell1:
|
|
||||||
case ChatType.Linkshell2:
|
|
||||||
case ChatType.Linkshell3:
|
|
||||||
case ChatType.Linkshell4:
|
|
||||||
case ChatType.Linkshell5:
|
|
||||||
case ChatType.Linkshell6:
|
|
||||||
case ChatType.Linkshell7:
|
|
||||||
case ChatType.Linkshell8:
|
|
||||||
case ChatType.CrossLinkshell1:
|
|
||||||
case ChatType.CrossLinkshell2:
|
|
||||||
case ChatType.CrossLinkshell3:
|
|
||||||
case ChatType.CrossLinkshell4:
|
|
||||||
case ChatType.CrossLinkshell5:
|
|
||||||
case ChatType.CrossLinkshell6:
|
|
||||||
case ChatType.CrossLinkshell7:
|
|
||||||
case ChatType.CrossLinkshell8:
|
|
||||||
case ChatType.GmLinkshell1:
|
|
||||||
case ChatType.GmLinkshell2:
|
|
||||||
case ChatType.GmLinkshell3:
|
|
||||||
case ChatType.GmLinkshell4:
|
|
||||||
case ChatType.GmLinkshell5:
|
|
||||||
case ChatType.GmLinkshell6:
|
|
||||||
case ChatType.GmLinkshell7:
|
|
||||||
case ChatType.GmLinkshell8:
|
|
||||||
return ColourUtil.ComponentsToRgba(212, 255, 125);
|
|
||||||
case ChatType.StandardEmote:
|
|
||||||
return ColourUtil.ComponentsToRgba(186, 255, 240);
|
|
||||||
case ChatType.CustomEmote:
|
|
||||||
return ColourUtil.ComponentsToRgba(186, 255, 240);
|
|
||||||
case ChatType.Yell:
|
|
||||||
case ChatType.GmYell:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 255, 0);
|
|
||||||
case ChatType.Echo:
|
|
||||||
return ColourUtil.ComponentsToRgba(204, 204, 204);
|
|
||||||
case ChatType.System:
|
|
||||||
case ChatType.GatheringSystem:
|
|
||||||
case ChatType.PeriodicRecruitmentNotification:
|
|
||||||
case ChatType.Orchestrion:
|
|
||||||
case ChatType.Alarm:
|
|
||||||
case ChatType.GlamourNotifications:
|
|
||||||
case ChatType.RetainerSale:
|
|
||||||
case ChatType.Sign:
|
|
||||||
case ChatType.MessageBook:
|
|
||||||
return ColourUtil.ComponentsToRgba(204, 204, 204);
|
|
||||||
case ChatType.NpcAnnouncement:
|
|
||||||
case ChatType.NpcDialogue:
|
|
||||||
return ColourUtil.ComponentsToRgba(171, 214, 71);
|
|
||||||
case ChatType.Error:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 74, 74);
|
|
||||||
case ChatType.FreeCompany:
|
|
||||||
case ChatType.FreeCompanyAnnouncement:
|
|
||||||
case ChatType.FreeCompanyLoginLogout:
|
|
||||||
case ChatType.GmFreeCompany:
|
|
||||||
return ColourUtil.ComponentsToRgba(171, 219, 229);
|
|
||||||
case ChatType.PvpTeam:
|
|
||||||
return ColourUtil.ComponentsToRgba(171, 219, 229);
|
|
||||||
case ChatType.PvpTeamAnnouncement:
|
|
||||||
case ChatType.PvpTeamLoginLogout:
|
|
||||||
return ColourUtil.ComponentsToRgba(171, 219, 229);
|
|
||||||
case ChatType.Action:
|
|
||||||
case ChatType.Item:
|
|
||||||
case ChatType.LootNotice:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 255, 176);
|
|
||||||
case ChatType.Progress:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 222, 115);
|
|
||||||
case ChatType.LootRoll:
|
|
||||||
case ChatType.RandomNumber:
|
|
||||||
return ColourUtil.ComponentsToRgba(199, 191, 158);
|
|
||||||
case ChatType.Crafting:
|
|
||||||
case ChatType.Gathering:
|
|
||||||
return ColourUtil.ComponentsToRgba(222, 191, 247);
|
|
||||||
case ChatType.Damage:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 125, 125);
|
|
||||||
case ChatType.Miss:
|
|
||||||
return ColourUtil.ComponentsToRgba(204, 204, 204);
|
|
||||||
case ChatType.Healing:
|
|
||||||
return ColourUtil.ComponentsToRgba(212, 255, 125);
|
|
||||||
case ChatType.GainBuff:
|
|
||||||
case ChatType.LoseBuff:
|
|
||||||
return ColourUtil.ComponentsToRgba(148, 191, 255);
|
|
||||||
case ChatType.GainDebuff:
|
|
||||||
case ChatType.LoseDebuff:
|
|
||||||
return ColourUtil.ComponentsToRgba(255, 138, 196);
|
|
||||||
case ChatType.BattleSystem:
|
|
||||||
return ColourUtil.ComponentsToRgba(204, 204, 204);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static InputChannel? ToInputChannel(this ChatType type) => type switch
|
|
||||||
{
|
|
||||||
ChatType.TellOutgoing => InputChannel.Tell,
|
|
||||||
ChatType.Say => InputChannel.Say,
|
|
||||||
ChatType.Party => InputChannel.Party,
|
|
||||||
ChatType.Alliance => InputChannel.Alliance,
|
|
||||||
ChatType.Yell => InputChannel.Yell,
|
|
||||||
ChatType.Shout => InputChannel.Shout,
|
|
||||||
ChatType.FreeCompany => InputChannel.FreeCompany,
|
|
||||||
ChatType.PvpTeam => InputChannel.PvpTeam,
|
|
||||||
ChatType.NoviceNetwork => InputChannel.NoviceNetwork,
|
|
||||||
ChatType.CrossLinkshell1 => InputChannel.CrossLinkshell1,
|
|
||||||
ChatType.CrossLinkshell2 => InputChannel.CrossLinkshell2,
|
|
||||||
ChatType.CrossLinkshell3 => InputChannel.CrossLinkshell3,
|
|
||||||
ChatType.CrossLinkshell4 => InputChannel.CrossLinkshell4,
|
|
||||||
ChatType.CrossLinkshell5 => InputChannel.CrossLinkshell5,
|
|
||||||
ChatType.CrossLinkshell6 => InputChannel.CrossLinkshell6,
|
|
||||||
ChatType.CrossLinkshell7 => InputChannel.CrossLinkshell7,
|
|
||||||
ChatType.CrossLinkshell8 => InputChannel.CrossLinkshell8,
|
|
||||||
ChatType.Linkshell1 => InputChannel.Linkshell1,
|
|
||||||
ChatType.Linkshell2 => InputChannel.Linkshell2,
|
|
||||||
ChatType.Linkshell3 => InputChannel.Linkshell3,
|
|
||||||
ChatType.Linkshell4 => InputChannel.Linkshell4,
|
|
||||||
ChatType.Linkshell5 => InputChannel.Linkshell5,
|
|
||||||
ChatType.Linkshell6 => InputChannel.Linkshell6,
|
|
||||||
ChatType.Linkshell7 => InputChannel.Linkshell7,
|
|
||||||
ChatType.Linkshell8 => InputChannel.Linkshell8,
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static bool IsGm(this ChatType type) => type switch
|
|
||||||
{
|
|
||||||
ChatType.GmTell => true,
|
|
||||||
ChatType.GmSay => true,
|
|
||||||
ChatType.GmShout => true,
|
|
||||||
ChatType.GmYell => true,
|
|
||||||
ChatType.GmParty => true,
|
|
||||||
ChatType.GmFreeCompany => true,
|
|
||||||
ChatType.GmLinkshell1 => true,
|
|
||||||
ChatType.GmLinkshell2 => true,
|
|
||||||
ChatType.GmLinkshell3 => true,
|
|
||||||
ChatType.GmLinkshell4 => true,
|
|
||||||
ChatType.GmLinkshell5 => true,
|
|
||||||
ChatType.GmLinkshell6 => true,
|
|
||||||
ChatType.GmLinkshell7 => true,
|
|
||||||
ChatType.GmLinkshell8 => true,
|
|
||||||
ChatType.GmNoviceNetwork => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static bool IsExtraChatLinkshell(this ChatType type) => type switch
|
|
||||||
{
|
|
||||||
ChatType.ExtraChatLinkshell1 => true,
|
|
||||||
ChatType.ExtraChatLinkshell2 => true,
|
|
||||||
ChatType.ExtraChatLinkshell3 => true,
|
|
||||||
ChatType.ExtraChatLinkshell4 => true,
|
|
||||||
ChatType.ExtraChatLinkshell5 => true,
|
|
||||||
ChatType.ExtraChatLinkshell6 => true,
|
|
||||||
ChatType.ExtraChatLinkshell7 => true,
|
|
||||||
ChatType.ExtraChatLinkshell8 => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static UiConfigOption ToConfigEntry(this ChatType type) => type switch
|
|
||||||
{
|
|
||||||
ChatType.Say => UiConfigOption.ColorSay,
|
|
||||||
ChatType.Shout => UiConfigOption.ColorShout,
|
|
||||||
ChatType.TellOutgoing => UiConfigOption.ColorTell,
|
|
||||||
ChatType.Party => UiConfigOption.ColorParty,
|
|
||||||
ChatType.Linkshell1 => UiConfigOption.ColorLS1,
|
|
||||||
ChatType.Linkshell2 => UiConfigOption.ColorLS2,
|
|
||||||
ChatType.Linkshell3 => UiConfigOption.ColorLS3,
|
|
||||||
ChatType.Linkshell4 => UiConfigOption.ColorLS4,
|
|
||||||
ChatType.Linkshell5 => UiConfigOption.ColorLS5,
|
|
||||||
ChatType.Linkshell6 => UiConfigOption.ColorLS6,
|
|
||||||
ChatType.Linkshell7 => UiConfigOption.ColorLS7,
|
|
||||||
ChatType.Linkshell8 => UiConfigOption.ColorLS8,
|
|
||||||
ChatType.FreeCompany => UiConfigOption.ColorFCompany,
|
|
||||||
ChatType.NoviceNetwork => UiConfigOption.ColorBeginner,
|
|
||||||
ChatType.CustomEmote => UiConfigOption.ColorEmoteUser,
|
|
||||||
ChatType.StandardEmote => UiConfigOption.ColorEmote,
|
|
||||||
ChatType.Yell => UiConfigOption.ColorYell,
|
|
||||||
ChatType.GainBuff => UiConfigOption.ColorBuffGive,
|
|
||||||
ChatType.GainDebuff => UiConfigOption.ColorDebuffGive,
|
|
||||||
ChatType.System => UiConfigOption.ColorSysMsg,
|
|
||||||
ChatType.NpcDialogue => UiConfigOption.ColorNpcSay,
|
|
||||||
ChatType.LootRoll => UiConfigOption.ColorLoot,
|
|
||||||
ChatType.FreeCompanyAnnouncement => UiConfigOption.ColorFCAnnounce,
|
|
||||||
ChatType.PvpTeamAnnouncement => UiConfigOption.ColorPvPGroupAnnounce,
|
|
||||||
_ => UiConfigOption.ColorSay,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static bool HasSource(this ChatType type) => type switch
|
|
||||||
{
|
|
||||||
// Battle
|
|
||||||
ChatType.Damage => true,
|
|
||||||
ChatType.Miss => true,
|
|
||||||
ChatType.Action => true,
|
|
||||||
ChatType.Item => true,
|
|
||||||
ChatType.Healing => true,
|
|
||||||
ChatType.GainBuff => true,
|
|
||||||
ChatType.LoseBuff => true,
|
|
||||||
ChatType.GainDebuff => true,
|
|
||||||
ChatType.LoseDebuff => true,
|
|
||||||
|
|
||||||
// Announcements
|
|
||||||
ChatType.System => true,
|
|
||||||
ChatType.BattleSystem => true,
|
|
||||||
ChatType.Error => true,
|
|
||||||
ChatType.LootNotice => true,
|
|
||||||
ChatType.Progress => true,
|
|
||||||
ChatType.LootRoll => true,
|
|
||||||
ChatType.Crafting => true,
|
|
||||||
ChatType.Gathering => true,
|
|
||||||
ChatType.FreeCompanyLoginLogout => true,
|
|
||||||
ChatType.PvpTeamLoginLogout => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static ChatType Parent(this ChatType type) => type switch
|
|
||||||
{
|
|
||||||
ChatType.Say => ChatType.Say,
|
|
||||||
ChatType.GmSay => ChatType.Say,
|
|
||||||
ChatType.Shout => ChatType.Shout,
|
|
||||||
ChatType.GmShout => ChatType.Shout,
|
|
||||||
ChatType.TellOutgoing => ChatType.TellOutgoing,
|
|
||||||
ChatType.TellIncoming => ChatType.TellOutgoing,
|
|
||||||
ChatType.GmTell => ChatType.TellOutgoing,
|
|
||||||
ChatType.Party => ChatType.Party,
|
|
||||||
ChatType.CrossParty => ChatType.Party,
|
|
||||||
ChatType.GmParty => ChatType.Party,
|
|
||||||
ChatType.Linkshell1 => ChatType.Linkshell1,
|
|
||||||
ChatType.GmLinkshell1 => ChatType.Linkshell1,
|
|
||||||
ChatType.Linkshell2 => ChatType.Linkshell2,
|
|
||||||
ChatType.GmLinkshell2 => ChatType.Linkshell2,
|
|
||||||
ChatType.Linkshell3 => ChatType.Linkshell3,
|
|
||||||
ChatType.GmLinkshell3 => ChatType.Linkshell3,
|
|
||||||
ChatType.Linkshell4 => ChatType.Linkshell4,
|
|
||||||
ChatType.GmLinkshell4 => ChatType.Linkshell4,
|
|
||||||
ChatType.Linkshell5 => ChatType.Linkshell5,
|
|
||||||
ChatType.GmLinkshell5 => ChatType.Linkshell5,
|
|
||||||
ChatType.Linkshell6 => ChatType.Linkshell6,
|
|
||||||
ChatType.GmLinkshell6 => ChatType.Linkshell6,
|
|
||||||
ChatType.Linkshell7 => ChatType.Linkshell7,
|
|
||||||
ChatType.GmLinkshell7 => ChatType.Linkshell7,
|
|
||||||
ChatType.Linkshell8 => ChatType.Linkshell8,
|
|
||||||
ChatType.GmLinkshell8 => ChatType.Linkshell8,
|
|
||||||
ChatType.FreeCompany => ChatType.FreeCompany,
|
|
||||||
ChatType.GmFreeCompany => ChatType.FreeCompany,
|
|
||||||
ChatType.NoviceNetwork => ChatType.NoviceNetwork,
|
|
||||||
ChatType.GmNoviceNetwork => ChatType.NoviceNetwork,
|
|
||||||
ChatType.CustomEmote => ChatType.CustomEmote,
|
|
||||||
ChatType.StandardEmote => ChatType.StandardEmote,
|
|
||||||
ChatType.Yell => ChatType.Yell,
|
|
||||||
ChatType.GmYell => ChatType.Yell,
|
|
||||||
ChatType.GainBuff => ChatType.GainBuff,
|
|
||||||
ChatType.LoseBuff => ChatType.GainBuff,
|
|
||||||
ChatType.GainDebuff => ChatType.GainDebuff,
|
|
||||||
ChatType.LoseDebuff => ChatType.GainDebuff,
|
|
||||||
ChatType.System => ChatType.System,
|
|
||||||
ChatType.Alarm => ChatType.System,
|
|
||||||
ChatType.GlamourNotifications => ChatType.System,
|
|
||||||
ChatType.RetainerSale => ChatType.System,
|
|
||||||
ChatType.PeriodicRecruitmentNotification => ChatType.System,
|
|
||||||
ChatType.Sign => ChatType.System,
|
|
||||||
ChatType.Orchestrion => ChatType.System,
|
|
||||||
ChatType.MessageBook => ChatType.System,
|
|
||||||
ChatType.NpcDialogue => ChatType.NpcDialogue,
|
|
||||||
ChatType.NpcAnnouncement => ChatType.NpcDialogue,
|
|
||||||
ChatType.LootRoll => ChatType.LootRoll,
|
|
||||||
ChatType.RandomNumber => ChatType.LootRoll,
|
|
||||||
ChatType.FreeCompanyAnnouncement => ChatType.FreeCompanyAnnouncement,
|
|
||||||
ChatType.FreeCompanyLoginLogout => ChatType.FreeCompanyAnnouncement,
|
|
||||||
ChatType.PvpTeamAnnouncement => ChatType.PvpTeamAnnouncement,
|
|
||||||
ChatType.PvpTeamLoginLogout => ChatType.PvpTeamAnnouncement,
|
|
||||||
_ => type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
using Lumina.Excel.Sheets;
|
|
||||||
|
|
||||||
namespace ChatTwo.Code;
|
|
||||||
|
|
||||||
internal static class InputChannelExt
|
|
||||||
{
|
|
||||||
internal static ChatType ToChatType(this InputChannel input) => input switch
|
|
||||||
{
|
|
||||||
InputChannel.Tell => ChatType.TellOutgoing,
|
|
||||||
InputChannel.Say => ChatType.Say,
|
|
||||||
InputChannel.Party => ChatType.Party,
|
|
||||||
InputChannel.Alliance => ChatType.Alliance,
|
|
||||||
InputChannel.Yell => ChatType.Yell,
|
|
||||||
InputChannel.Shout => ChatType.Shout,
|
|
||||||
InputChannel.FreeCompany => ChatType.FreeCompany,
|
|
||||||
InputChannel.PvpTeam => ChatType.PvpTeam,
|
|
||||||
InputChannel.NoviceNetwork => ChatType.NoviceNetwork,
|
|
||||||
InputChannel.CrossLinkshell1 => ChatType.CrossLinkshell1,
|
|
||||||
InputChannel.CrossLinkshell2 => ChatType.CrossLinkshell2,
|
|
||||||
InputChannel.CrossLinkshell3 => ChatType.CrossLinkshell3,
|
|
||||||
InputChannel.CrossLinkshell4 => ChatType.CrossLinkshell4,
|
|
||||||
InputChannel.CrossLinkshell5 => ChatType.CrossLinkshell5,
|
|
||||||
InputChannel.CrossLinkshell6 => ChatType.CrossLinkshell6,
|
|
||||||
InputChannel.CrossLinkshell7 => ChatType.CrossLinkshell7,
|
|
||||||
InputChannel.CrossLinkshell8 => ChatType.CrossLinkshell8,
|
|
||||||
InputChannel.Linkshell1 => ChatType.Linkshell1,
|
|
||||||
InputChannel.Linkshell2 => ChatType.Linkshell2,
|
|
||||||
InputChannel.Linkshell3 => ChatType.Linkshell3,
|
|
||||||
InputChannel.Linkshell4 => ChatType.Linkshell4,
|
|
||||||
InputChannel.Linkshell5 => ChatType.Linkshell5,
|
|
||||||
InputChannel.Linkshell6 => ChatType.Linkshell6,
|
|
||||||
InputChannel.Linkshell7 => ChatType.Linkshell7,
|
|
||||||
InputChannel.Linkshell8 => ChatType.Linkshell8,
|
|
||||||
InputChannel.ExtraChatLinkshell1 => ChatType.ExtraChatLinkshell1,
|
|
||||||
InputChannel.ExtraChatLinkshell2 => ChatType.ExtraChatLinkshell2,
|
|
||||||
InputChannel.ExtraChatLinkshell3 => ChatType.ExtraChatLinkshell3,
|
|
||||||
InputChannel.ExtraChatLinkshell4 => ChatType.ExtraChatLinkshell4,
|
|
||||||
InputChannel.ExtraChatLinkshell5 => ChatType.ExtraChatLinkshell5,
|
|
||||||
InputChannel.ExtraChatLinkshell6 => ChatType.ExtraChatLinkshell6,
|
|
||||||
InputChannel.ExtraChatLinkshell7 => ChatType.ExtraChatLinkshell7,
|
|
||||||
InputChannel.ExtraChatLinkshell8 => ChatType.ExtraChatLinkshell8,
|
|
||||||
InputChannel.Invalid => ChatType.Echo,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(input), input, null),
|
|
||||||
};
|
|
||||||
|
|
||||||
public static uint LinkshellIndex(this InputChannel channel) => channel switch
|
|
||||||
{
|
|
||||||
InputChannel.Linkshell1 => 0,
|
|
||||||
InputChannel.Linkshell2 => 1,
|
|
||||||
InputChannel.Linkshell3 => 2,
|
|
||||||
InputChannel.Linkshell4 => 3,
|
|
||||||
InputChannel.Linkshell5 => 4,
|
|
||||||
InputChannel.Linkshell6 => 5,
|
|
||||||
InputChannel.Linkshell7 => 6,
|
|
||||||
InputChannel.Linkshell8 => 7,
|
|
||||||
InputChannel.CrossLinkshell1 => 0,
|
|
||||||
InputChannel.CrossLinkshell2 => 1,
|
|
||||||
InputChannel.CrossLinkshell3 => 2,
|
|
||||||
InputChannel.CrossLinkshell4 => 3,
|
|
||||||
InputChannel.CrossLinkshell5 => 4,
|
|
||||||
InputChannel.CrossLinkshell6 => 5,
|
|
||||||
InputChannel.CrossLinkshell7 => 6,
|
|
||||||
InputChannel.CrossLinkshell8 => 7,
|
|
||||||
InputChannel.ExtraChatLinkshell1 => 0,
|
|
||||||
InputChannel.ExtraChatLinkshell2 => 1,
|
|
||||||
InputChannel.ExtraChatLinkshell3 => 2,
|
|
||||||
InputChannel.ExtraChatLinkshell4 => 3,
|
|
||||||
InputChannel.ExtraChatLinkshell5 => 4,
|
|
||||||
InputChannel.ExtraChatLinkshell6 => 5,
|
|
||||||
InputChannel.ExtraChatLinkshell7 => 6,
|
|
||||||
InputChannel.ExtraChatLinkshell8 => 7,
|
|
||||||
_ => uint.MaxValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static string Prefix(this InputChannel channel) => channel switch
|
|
||||||
{
|
|
||||||
InputChannel.Tell => "/t",
|
|
||||||
InputChannel.Say => "/s",
|
|
||||||
InputChannel.Party => "/p",
|
|
||||||
InputChannel.Alliance => "/a",
|
|
||||||
InputChannel.Yell => "/y",
|
|
||||||
InputChannel.Shout => "/sh",
|
|
||||||
InputChannel.FreeCompany => "/fc",
|
|
||||||
InputChannel.PvpTeam => "/pt",
|
|
||||||
InputChannel.NoviceNetwork => "/b",
|
|
||||||
InputChannel.CrossLinkshell1 => "/cwl1",
|
|
||||||
InputChannel.CrossLinkshell2 => "/cwl2",
|
|
||||||
InputChannel.CrossLinkshell3 => "/cwl3",
|
|
||||||
InputChannel.CrossLinkshell4 => "/cwl4",
|
|
||||||
InputChannel.CrossLinkshell5 => "/cwl5",
|
|
||||||
InputChannel.CrossLinkshell6 => "/cwl6",
|
|
||||||
InputChannel.CrossLinkshell7 => "/cwl7",
|
|
||||||
InputChannel.CrossLinkshell8 => "/cwl8",
|
|
||||||
InputChannel.Linkshell1 => "/l1",
|
|
||||||
InputChannel.Linkshell2 => "/l2",
|
|
||||||
InputChannel.Linkshell3 => "/l3",
|
|
||||||
InputChannel.Linkshell4 => "/l4",
|
|
||||||
InputChannel.Linkshell5 => "/l5",
|
|
||||||
InputChannel.Linkshell6 => "/l6",
|
|
||||||
InputChannel.Linkshell7 => "/l7",
|
|
||||||
InputChannel.Linkshell8 => "/l8",
|
|
||||||
InputChannel.ExtraChatLinkshell1 => "/ecl1",
|
|
||||||
InputChannel.ExtraChatLinkshell2 => "/ecl2",
|
|
||||||
InputChannel.ExtraChatLinkshell3 => "/ecl3",
|
|
||||||
InputChannel.ExtraChatLinkshell4 => "/ecl4",
|
|
||||||
InputChannel.ExtraChatLinkshell5 => "/ecl5",
|
|
||||||
InputChannel.ExtraChatLinkshell6 => "/ecl6",
|
|
||||||
InputChannel.ExtraChatLinkshell7 => "/ecl7",
|
|
||||||
InputChannel.ExtraChatLinkshell8 => "/ecl8",
|
|
||||||
_ => "/e",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static IEnumerable<TextCommand>? TextCommands(this InputChannel channel)
|
|
||||||
{
|
|
||||||
uint[] ids = channel switch
|
|
||||||
{
|
|
||||||
InputChannel.Tell => [104, 118],
|
|
||||||
InputChannel.Say => [102],
|
|
||||||
InputChannel.Party => [105],
|
|
||||||
InputChannel.Alliance => [119],
|
|
||||||
InputChannel.Yell => [117],
|
|
||||||
InputChannel.Shout => [103],
|
|
||||||
InputChannel.FreeCompany => [115],
|
|
||||||
InputChannel.PvpTeam => [91],
|
|
||||||
InputChannel.NoviceNetwork => [101],
|
|
||||||
InputChannel.CrossLinkshell1 => [13],
|
|
||||||
InputChannel.CrossLinkshell2 => [14],
|
|
||||||
InputChannel.CrossLinkshell3 => [15],
|
|
||||||
InputChannel.CrossLinkshell4 => [16],
|
|
||||||
InputChannel.CrossLinkshell5 => [17],
|
|
||||||
InputChannel.CrossLinkshell6 => [18],
|
|
||||||
InputChannel.CrossLinkshell7 => [19],
|
|
||||||
InputChannel.CrossLinkshell8 => [20],
|
|
||||||
InputChannel.Linkshell1 => [107],
|
|
||||||
InputChannel.Linkshell2 => [108],
|
|
||||||
InputChannel.Linkshell3 => [109],
|
|
||||||
InputChannel.Linkshell4 => [110],
|
|
||||||
InputChannel.Linkshell5 => [111],
|
|
||||||
InputChannel.Linkshell6 => [112],
|
|
||||||
InputChannel.Linkshell7 => [113],
|
|
||||||
InputChannel.Linkshell8 => [114],
|
|
||||||
_ => [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ids.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ids.Where(id => Sheets.TextCommandSheet.HasRow(id)).Select(id => Sheets.TextCommandSheet.GetRow(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsLinkshell(this InputChannel channel) => channel switch
|
|
||||||
{
|
|
||||||
InputChannel.Linkshell1 => true,
|
|
||||||
InputChannel.Linkshell2 => true,
|
|
||||||
InputChannel.Linkshell3 => true,
|
|
||||||
InputChannel.Linkshell4 => true,
|
|
||||||
InputChannel.Linkshell5 => true,
|
|
||||||
InputChannel.Linkshell6 => true,
|
|
||||||
InputChannel.Linkshell7 => true,
|
|
||||||
InputChannel.Linkshell8 => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static bool IsCrossLinkshell(this InputChannel channel) => channel switch
|
|
||||||
{
|
|
||||||
InputChannel.CrossLinkshell1 => true,
|
|
||||||
InputChannel.CrossLinkshell2 => true,
|
|
||||||
InputChannel.CrossLinkshell3 => true,
|
|
||||||
InputChannel.CrossLinkshell4 => true,
|
|
||||||
InputChannel.CrossLinkshell5 => true,
|
|
||||||
InputChannel.CrossLinkshell6 => true,
|
|
||||||
InputChannel.CrossLinkshell7 => true,
|
|
||||||
InputChannel.CrossLinkshell8 => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static bool IsExtraChatLinkshell(this InputChannel channel) => channel switch
|
|
||||||
{
|
|
||||||
InputChannel.ExtraChatLinkshell1 => true,
|
|
||||||
InputChannel.ExtraChatLinkshell2 => true,
|
|
||||||
InputChannel.ExtraChatLinkshell3 => true,
|
|
||||||
InputChannel.ExtraChatLinkshell4 => true,
|
|
||||||
InputChannel.ExtraChatLinkshell5 => true,
|
|
||||||
InputChannel.ExtraChatLinkshell6 => true,
|
|
||||||
InputChannel.ExtraChatLinkshell7 => true,
|
|
||||||
InputChannel.ExtraChatLinkshell8 => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static bool IsValid(this InputChannel channel) => channel switch
|
|
||||||
{
|
|
||||||
InputChannel.Invalid => false,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,733 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
using ChatTwo.Code;
|
|
||||||
using ChatTwo.GameFunctions.Types;
|
|
||||||
using ChatTwo.Resources;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Dalamud;
|
|
||||||
using Dalamud.Configuration;
|
|
||||||
using Dalamud.Game.ClientState.Keys;
|
|
||||||
using Dalamud.Interface.FontIdentifier;
|
|
||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
|
|
||||||
namespace ChatTwo;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class ConfigKeyBind
|
|
||||||
{
|
|
||||||
public ModifierFlag Modifier;
|
|
||||||
public VirtualKey Key;
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
var modString = "";
|
|
||||||
if (Modifier.HasFlag(ModifierFlag.Ctrl))
|
|
||||||
modString += Language.Keybind_Modifier_Ctrl + " + ";
|
|
||||||
if (Modifier.HasFlag(ModifierFlag.Shift))
|
|
||||||
modString += Language.Keybind_Modifier_Shift + " + ";
|
|
||||||
if (Modifier.HasFlag(ModifierFlag.Alt))
|
|
||||||
modString += Language.Keybind_Modifier_Alt + " + ";
|
|
||||||
return modString+Key.GetFancyName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class Configuration : IPluginConfiguration
|
|
||||||
{
|
|
||||||
private const int LatestVersion = 7;
|
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
|
||||||
|
|
||||||
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
|
||||||
// Master-switch defaults to true; set false to restore upstream behavior.
|
|
||||||
public bool PrivacyFilterEnabled = true;
|
|
||||||
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
|
||||||
public HashSet<ChatType> PrivacyPersistChannels = [];
|
|
||||||
// Failsafe for ChatTypes added by future FFXIV patches we don't know about.
|
|
||||||
public bool PrivacyPersistUnknownChannels;
|
|
||||||
|
|
||||||
public bool IsAllowedForStorage(ChatType type)
|
|
||||||
{
|
|
||||||
if (!PrivacyFilterEnabled)
|
|
||||||
return true;
|
|
||||||
if (PrivacyPersistChannels.Contains(type))
|
|
||||||
return true;
|
|
||||||
return PrivacyPersistUnknownChannels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hellion Chat — Message retention (GDPR data minimization, time axis).
|
|
||||||
// Master switch defaults to false; the plugin will not delete history
|
|
||||||
// until the user explicitly opts in.
|
|
||||||
public bool RetentionEnabled;
|
|
||||||
public int RetentionDefaultDays = 30;
|
|
||||||
public Dictionary<ChatType, int> RetentionPerChannelDays = [];
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Hellion Chat global ImGui theme — applied to every plugin window in
|
|
||||||
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
|
|
||||||
// can flip this off in the Privacy tab.
|
|
||||||
public bool HellionThemeEnabled = true;
|
|
||||||
|
|
||||||
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
|
||||||
// panes more glass-like so the game shines through. Default ~92%.
|
|
||||||
public float HellionThemeWindowOpacity = 0.92f;
|
|
||||||
|
|
||||||
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
|
||||||
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
|
|
||||||
// fresh install gets the Hellion typography out-of-the-box; flip OFF
|
|
||||||
// to fall back to the user's chosen system or Dalamud font.
|
|
||||||
public bool UseHellionFont = true;
|
|
||||||
|
|
||||||
public int GetRetentionDays(ChatType type)
|
|
||||||
{
|
|
||||||
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
|
|
||||||
return userOverride;
|
|
||||||
if (Privacy.PrivacyDefaults.DefaultRetentionDays.TryGetValue(type, out var specDefault))
|
|
||||||
return specDefault;
|
|
||||||
return RetentionDefaultDays;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HideChat = true;
|
|
||||||
public bool HideDuringCutscenes = true;
|
|
||||||
public bool HideWhenNotLoggedIn = true;
|
|
||||||
public bool HideWhenUiHidden = true;
|
|
||||||
public bool HideInLoadingScreens;
|
|
||||||
public bool HideInBattle;
|
|
||||||
public bool HideWhenInactive;
|
|
||||||
public int InactivityHideTimeout = 10;
|
|
||||||
public bool InactivityHideActiveDuringBattle = true;
|
|
||||||
|
|
||||||
[Obsolete("Use InactivityHideChannelsV2 instead")]
|
|
||||||
public Dictionary<ChatType, ChatSource> InactivityHideChannels = [];
|
|
||||||
|
|
||||||
public Dictionary<ChatType, (ChatSource, ChatSource)> InactivityHideChannelsV2 = [];
|
|
||||||
public bool InactivityHideExtraChatAll = true;
|
|
||||||
public HashSet<Guid> InactivityHideExtraChatChannels = [];
|
|
||||||
public bool ShowHideButton = true;
|
|
||||||
public bool NativeItemTooltips = true;
|
|
||||||
public bool PrettierTimestamps = true;
|
|
||||||
public bool MoreCompactPretty;
|
|
||||||
public bool HideSameTimestamps;
|
|
||||||
public bool ShowNoviceNetwork;
|
|
||||||
public bool SidebarTabView;
|
|
||||||
public bool PrintChangelog = true;
|
|
||||||
public bool OnlyPreviewIf;
|
|
||||||
public int PreviewMinimum = 1;
|
|
||||||
public PreviewPosition PreviewPosition = PreviewPosition.Inside;
|
|
||||||
public CommandHelpSide CommandHelpSide = CommandHelpSide.None;
|
|
||||||
public KeybindMode KeybindMode = KeybindMode.Strict;
|
|
||||||
public LanguageOverride LanguageOverride = LanguageOverride.None;
|
|
||||||
public bool CanMove = true;
|
|
||||||
public bool CanResize = true;
|
|
||||||
public bool ShowTitleBar;
|
|
||||||
public bool ShowPopOutTitleBar = true;
|
|
||||||
public bool DatabaseBattleMessages;
|
|
||||||
public bool LoadPreviousSession;
|
|
||||||
public bool FilterIncludePreviousSessions;
|
|
||||||
public bool SortAutoTranslate;
|
|
||||||
public bool CollapseDuplicateMessages;
|
|
||||||
public bool CollapseKeepUniqueLinks;
|
|
||||||
public bool PlaySounds = true;
|
|
||||||
public bool KeepInputFocus = true;
|
|
||||||
public int MaxLinesToRender = 10_000; // 1-10000
|
|
||||||
public bool Use24HourClock;
|
|
||||||
|
|
||||||
public bool ShowEmotes = true;
|
|
||||||
public HashSet<string> BlockedEmotes = [];
|
|
||||||
|
|
||||||
public bool FontsEnabled = true;
|
|
||||||
public ExtraGlyphRanges ExtraGlyphRanges = 0;
|
|
||||||
public float FontSizeV2 = 12.75f;
|
|
||||||
public float SymbolsFontSizeV2 = 12.75f;
|
|
||||||
public SingleFontSpec GlobalFontV2 = new()
|
|
||||||
{
|
|
||||||
// dalamud only ships KR as regular, which chat2 used previously for global fonts
|
|
||||||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
|
|
||||||
SizePt = 12.75f,
|
|
||||||
};
|
|
||||||
public SingleFontSpec JapaneseFontV2 = new()
|
|
||||||
{
|
|
||||||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium),
|
|
||||||
SizePt = 12.75f,
|
|
||||||
};
|
|
||||||
public bool ItalicEnabled;
|
|
||||||
public SingleFontSpec ItalicFontV2 = new()
|
|
||||||
{
|
|
||||||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular),
|
|
||||||
SizePt = 12.75f,
|
|
||||||
};
|
|
||||||
|
|
||||||
public float TooltipOffset;
|
|
||||||
public float WindowAlpha = 100f;
|
|
||||||
public Dictionary<ChatType, uint> ChatColours = new();
|
|
||||||
public List<Tab> Tabs = [];
|
|
||||||
|
|
||||||
public bool OverrideStyle;
|
|
||||||
public string? ChosenStyle;
|
|
||||||
|
|
||||||
public ConfigKeyBind? ChatTabForward;
|
|
||||||
public ConfigKeyBind? ChatTabBackward;
|
|
||||||
|
|
||||||
// Webinterface
|
|
||||||
public bool WebinterfaceEnabled;
|
|
||||||
public bool WebinterfaceAutoStart;
|
|
||||||
public string WebinterfacePassword = WebinterfaceUtil.GenerateSimpleAuthCode();
|
|
||||||
public int WebinterfacePort = 9000;
|
|
||||||
public HashSet<string> AuthStore = [];
|
|
||||||
public int WebinterfaceMaxLinesToSend = 1000; // 1-10000
|
|
||||||
|
|
||||||
public void UpdateFrom(Configuration other, bool backToOriginal)
|
|
||||||
{
|
|
||||||
if (backToOriginal)
|
|
||||||
foreach (var tab in Tabs.Where(t => t.PopOut))
|
|
||||||
tab.PopOut = false;
|
|
||||||
|
|
||||||
HideChat = other.HideChat;
|
|
||||||
HideDuringCutscenes = other.HideDuringCutscenes;
|
|
||||||
HideWhenNotLoggedIn = other.HideWhenNotLoggedIn;
|
|
||||||
HideWhenUiHidden = other.HideWhenUiHidden;
|
|
||||||
HideInLoadingScreens = other.HideInLoadingScreens;
|
|
||||||
HideInBattle = other.HideInBattle;
|
|
||||||
HideWhenInactive = other.HideWhenInactive;
|
|
||||||
InactivityHideTimeout = other.InactivityHideTimeout;
|
|
||||||
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
|
||||||
InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(pair => pair.Key, pair => pair.Value);
|
|
||||||
InactivityHideExtraChatAll = other.InactivityHideExtraChatAll;
|
|
||||||
InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet();
|
|
||||||
ShowHideButton = other.ShowHideButton;
|
|
||||||
NativeItemTooltips = other.NativeItemTooltips;
|
|
||||||
PrettierTimestamps = other.PrettierTimestamps;
|
|
||||||
MoreCompactPretty = other.MoreCompactPretty;
|
|
||||||
HideSameTimestamps = other.HideSameTimestamps;
|
|
||||||
ShowNoviceNetwork = other.ShowNoviceNetwork;
|
|
||||||
SidebarTabView = other.SidebarTabView;
|
|
||||||
PrintChangelog = other.PrintChangelog;
|
|
||||||
OnlyPreviewIf = other.OnlyPreviewIf;
|
|
||||||
PreviewMinimum = other.PreviewMinimum;
|
|
||||||
PreviewPosition = other.PreviewPosition;
|
|
||||||
CommandHelpSide = other.CommandHelpSide;
|
|
||||||
KeybindMode = other.KeybindMode;
|
|
||||||
LanguageOverride = other.LanguageOverride;
|
|
||||||
CanMove = other.CanMove;
|
|
||||||
CanResize = other.CanResize;
|
|
||||||
ShowTitleBar = other.ShowTitleBar;
|
|
||||||
ShowPopOutTitleBar = other.ShowPopOutTitleBar;
|
|
||||||
DatabaseBattleMessages = other.DatabaseBattleMessages;
|
|
||||||
LoadPreviousSession = other.LoadPreviousSession;
|
|
||||||
FilterIncludePreviousSessions = other.FilterIncludePreviousSessions;
|
|
||||||
SortAutoTranslate = other.SortAutoTranslate;
|
|
||||||
CollapseDuplicateMessages = other.CollapseDuplicateMessages;
|
|
||||||
CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks;
|
|
||||||
PlaySounds = other.PlaySounds;
|
|
||||||
KeepInputFocus = other.KeepInputFocus;
|
|
||||||
MaxLinesToRender = other.MaxLinesToRender;
|
|
||||||
Use24HourClock = other.Use24HourClock;
|
|
||||||
ShowEmotes = other.ShowEmotes;
|
|
||||||
BlockedEmotes = other.BlockedEmotes;
|
|
||||||
FontsEnabled = other.FontsEnabled;
|
|
||||||
ItalicEnabled = other.ItalicEnabled;
|
|
||||||
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
|
||||||
FontSizeV2 = other.FontSizeV2;
|
|
||||||
GlobalFontV2 = other.GlobalFontV2;
|
|
||||||
JapaneseFontV2 = other.JapaneseFontV2;
|
|
||||||
ItalicFontV2 = other.ItalicFontV2;
|
|
||||||
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
|
|
||||||
TooltipOffset = other.TooltipOffset;
|
|
||||||
WindowAlpha = other.WindowAlpha;
|
|
||||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
|
||||||
Tabs = other.Tabs.Select(t => t.Clone()).ToList();
|
|
||||||
OverrideStyle = other.OverrideStyle;
|
|
||||||
ChosenStyle = other.ChosenStyle;
|
|
||||||
ChatTabForward = other.ChatTabForward;
|
|
||||||
ChatTabBackward = other.ChatTabBackward;
|
|
||||||
WebinterfaceEnabled = other.WebinterfaceEnabled;
|
|
||||||
WebinterfaceAutoStart = other.WebinterfaceAutoStart;
|
|
||||||
WebinterfacePassword = other.WebinterfacePassword;
|
|
||||||
WebinterfacePort = other.WebinterfacePort;
|
|
||||||
WebinterfaceMaxLinesToSend = other.WebinterfaceMaxLinesToSend;
|
|
||||||
|
|
||||||
PrivacyFilterEnabled = other.PrivacyFilterEnabled;
|
|
||||||
PrivacyPersistChannels = [..other.PrivacyPersistChannels];
|
|
||||||
PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels;
|
|
||||||
|
|
||||||
RetentionEnabled = other.RetentionEnabled;
|
|
||||||
RetentionDefaultDays = other.RetentionDefaultDays;
|
|
||||||
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value);
|
|
||||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
|
||||||
|
|
||||||
FirstRunCompleted = other.FirstRunCompleted;
|
|
||||||
HellionThemeEnabled = other.HellionThemeEnabled;
|
|
||||||
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
|
||||||
UseHellionFont = other.UseHellionFont;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public enum UnreadMode
|
|
||||||
{
|
|
||||||
All,
|
|
||||||
Unseen,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UnreadModeExt
|
|
||||||
{
|
|
||||||
internal static string Name(this UnreadMode mode) => mode switch
|
|
||||||
{
|
|
||||||
UnreadMode.All => Language.UnreadMode_All,
|
|
||||||
UnreadMode.Unseen => Language.UnreadMode_Unseen,
|
|
||||||
UnreadMode.None => Language.UnreadMode_None,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
|
||||||
};
|
|
||||||
|
|
||||||
internal static string? Tooltip(this UnreadMode mode) => mode switch
|
|
||||||
{
|
|
||||||
UnreadMode.All => Language.UnreadMode_All_Tooltip,
|
|
||||||
UnreadMode.Unseen => Language.UnreadMode_Unseen_Tooltip,
|
|
||||||
UnreadMode.None => Language.UnreadMode_None_Tooltip,
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class Tab
|
|
||||||
{
|
|
||||||
public string Name = Language.Tab_DefaultName;
|
|
||||||
|
|
||||||
[Obsolete("Removed in favor of SelectedChannels")]
|
|
||||||
public Dictionary<ChatType, ChatSource> ChatCodes = new();
|
|
||||||
|
|
||||||
public Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels = new();
|
|
||||||
public bool ExtraChatAll;
|
|
||||||
public HashSet<Guid> ExtraChatChannels = [];
|
|
||||||
|
|
||||||
public UnreadMode UnreadMode = UnreadMode.Unseen;
|
|
||||||
public bool UnhideOnActivity;
|
|
||||||
public bool DisplayTimestamp = true;
|
|
||||||
public InputChannel? Channel;
|
|
||||||
public bool PopOut;
|
|
||||||
public bool IndependentOpacity;
|
|
||||||
public float Opacity = 100f;
|
|
||||||
public bool InputDisabled;
|
|
||||||
|
|
||||||
public bool CanMove = true;
|
|
||||||
public bool CanResize = true;
|
|
||||||
|
|
||||||
public bool IndependentHide;
|
|
||||||
public bool HideDuringCutscenes = true;
|
|
||||||
public bool HideWhenNotLoggedIn = true;
|
|
||||||
public bool HideWhenUiHidden = true;
|
|
||||||
public bool HideInLoadingScreens;
|
|
||||||
public bool HideInBattle;
|
|
||||||
public bool HideWhenInactive;
|
|
||||||
|
|
||||||
public bool IsTempTab;
|
|
||||||
public bool AllSenderMessages;
|
|
||||||
public TellTarget TellTarget = TellTarget.Empty();
|
|
||||||
|
|
||||||
[NonSerialized] public uint Unread;
|
|
||||||
[NonSerialized] public uint LastSendUnread;
|
|
||||||
[NonSerialized] public long LastActivity;
|
|
||||||
[NonSerialized] public MessageList Messages = new();
|
|
||||||
|
|
||||||
[NonSerialized] public UsedChannel CurrentChannel = new();
|
|
||||||
|
|
||||||
[NonSerialized] public Guid Identifier = Guid.NewGuid();
|
|
||||||
|
|
||||||
public bool Matches(Message message)
|
|
||||||
{
|
|
||||||
return message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddMessage(Message message, bool unread = true)
|
|
||||||
{
|
|
||||||
Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
|
||||||
if (!unread)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Unread += 1;
|
|
||||||
if (message.Matches(Plugin.Config.InactivityHideChannelsV2, Plugin.Config.InactivityHideExtraChatAll, Plugin.Config.InactivityHideExtraChatChannels))
|
|
||||||
LastActivity = Environment.TickCount64;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
=> Messages.Clear();
|
|
||||||
|
|
||||||
public Tab Clone()
|
|
||||||
{
|
|
||||||
return new Tab
|
|
||||||
{
|
|
||||||
Name = Name,
|
|
||||||
SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value),
|
|
||||||
ExtraChatAll = ExtraChatAll,
|
|
||||||
ExtraChatChannels = ExtraChatChannels.ToHashSet(),
|
|
||||||
UnreadMode = UnreadMode,
|
|
||||||
UnhideOnActivity = UnhideOnActivity,
|
|
||||||
Unread = Unread,
|
|
||||||
LastActivity = LastActivity,
|
|
||||||
DisplayTimestamp = DisplayTimestamp,
|
|
||||||
Channel = Channel,
|
|
||||||
PopOut = PopOut,
|
|
||||||
IndependentOpacity = IndependentOpacity,
|
|
||||||
Opacity = Opacity,
|
|
||||||
Identifier = Identifier,
|
|
||||||
InputDisabled = InputDisabled,
|
|
||||||
CurrentChannel = CurrentChannel,
|
|
||||||
CanMove = CanMove,
|
|
||||||
CanResize = CanResize,
|
|
||||||
IndependentHide = IndependentHide,
|
|
||||||
HideDuringCutscenes = HideDuringCutscenes,
|
|
||||||
HideWhenNotLoggedIn = HideWhenNotLoggedIn,
|
|
||||||
HideWhenUiHidden = HideWhenUiHidden,
|
|
||||||
HideInLoadingScreens = HideInLoadingScreens,
|
|
||||||
HideInBattle = HideInBattle,
|
|
||||||
HideWhenInactive = HideWhenInactive,
|
|
||||||
IsTempTab = IsTempTab,
|
|
||||||
AllSenderMessages = AllSenderMessages,
|
|
||||||
TellTarget = TellTarget.From(TellTarget),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MessageList provides an ordered list of messages with duplicate ID
|
|
||||||
/// tracking, sorting and mutex protection.
|
|
||||||
/// </summary>
|
|
||||||
public class MessageList
|
|
||||||
{
|
|
||||||
private readonly SemaphoreSlim LockSlim = new(1, 1);
|
|
||||||
|
|
||||||
private readonly List<Message> Messages;
|
|
||||||
private readonly HashSet<Guid> TrackedMessageIds;
|
|
||||||
|
|
||||||
public MessageList()
|
|
||||||
{
|
|
||||||
Messages = [];
|
|
||||||
TrackedMessageIds = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageList(int initialCapacity)
|
|
||||||
{
|
|
||||||
Messages = new List<Message>(initialCapacity);
|
|
||||||
TrackedMessageIds = new HashSet<Guid>(initialCapacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddPrune(Message message, int max)
|
|
||||||
{
|
|
||||||
LockSlim.Wait(-1);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
AddLocked(message);
|
|
||||||
PruneMaxLocked(max);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
LockSlim.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddSortPrune(IEnumerable<Message> messages, int max)
|
|
||||||
{
|
|
||||||
LockSlim.Wait(-1);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var message in messages)
|
|
||||||
AddLocked(message);
|
|
||||||
|
|
||||||
SortLocked();
|
|
||||||
PruneMaxLocked(max);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
LockSlim.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddLocked(Message message)
|
|
||||||
{
|
|
||||||
if (TrackedMessageIds.Contains(message.Id))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Messages.Add(message);
|
|
||||||
TrackedMessageIds.Add(message.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
LockSlim.Wait(-1);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Messages.Clear();
|
|
||||||
TrackedMessageIds.Clear();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
LockSlim.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SortLocked()
|
|
||||||
{
|
|
||||||
Messages.Sort((a, b) => a.Date.CompareTo(b.Date));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PruneMaxLocked(int max)
|
|
||||||
{
|
|
||||||
while (Messages.Count > max)
|
|
||||||
{
|
|
||||||
TrackedMessageIds.Remove(Messages[0].Id);
|
|
||||||
Messages.RemoveAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an array copy of the message list for usage outside of main thread
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Message[]> GetCopy(int millisecondsTimeout = -1)
|
|
||||||
{
|
|
||||||
await LockSlim.WaitAsync(millisecondsTimeout);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Messages.ToArray();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
LockSlim.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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)
|
|
||||||
{
|
|
||||||
LockSlim.Wait(millisecondsTimeout);
|
|
||||||
return new RLockedMessageList(LockSlim, Messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RLockedMessageList(SemaphoreSlim lockSlim, List<Message> messages) : IReadOnlyList<Message>, IDisposable
|
|
||||||
{
|
|
||||||
public IEnumerator<Message> GetEnumerator()
|
|
||||||
{
|
|
||||||
return messages.GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Count => messages.Count;
|
|
||||||
|
|
||||||
public Message this[int index] => messages[index];
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
lockSlim.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UsedChannel
|
|
||||||
{
|
|
||||||
public InputChannel Channel = InputChannel.Invalid;
|
|
||||||
public List<Chunk> Name = [];
|
|
||||||
public TellTarget? TellTarget;
|
|
||||||
|
|
||||||
public bool UseTempChannel;
|
|
||||||
public InputChannel TempChannel = InputChannel.Invalid;
|
|
||||||
public TellTarget? TempTellTarget;
|
|
||||||
|
|
||||||
public void ResetTempChannel()
|
|
||||||
{
|
|
||||||
UseTempChannel = false;
|
|
||||||
TempTellTarget = null;
|
|
||||||
TempChannel = InputChannel.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetChannel(InputChannel channel)
|
|
||||||
{
|
|
||||||
Channel = channel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public enum PreviewPosition
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Inside,
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
Tooltip,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PreviewPositionExt
|
|
||||||
{
|
|
||||||
public static string Name(this PreviewPosition position) => position switch
|
|
||||||
{
|
|
||||||
PreviewPosition.None => Language.Options_Preview_None,
|
|
||||||
PreviewPosition.Inside => Language.Options_Preview_Inside,
|
|
||||||
PreviewPosition.Top => Language.Options_Preview_Top,
|
|
||||||
PreviewPosition.Bottom => Language.Options_Preview_Bottom,
|
|
||||||
PreviewPosition.Tooltip => Language.Options_Preview_Tooltip,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(position), position, null),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public enum CommandHelpSide
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CommandHelpSideExt
|
|
||||||
{
|
|
||||||
public static string Name(this CommandHelpSide side) => side switch
|
|
||||||
{
|
|
||||||
CommandHelpSide.None => Language.CommandHelpSide_None,
|
|
||||||
CommandHelpSide.Left => Language.CommandHelpSide_Left,
|
|
||||||
CommandHelpSide.Right => Language.CommandHelpSide_Right,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(side), side, null),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public enum KeybindMode
|
|
||||||
{
|
|
||||||
Flexible,
|
|
||||||
Strict,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KeybindModeExt
|
|
||||||
{
|
|
||||||
public static string Name(this KeybindMode mode) => mode switch
|
|
||||||
{
|
|
||||||
KeybindMode.Flexible => Language.KeybindMode_Flexible_Name,
|
|
||||||
KeybindMode.Strict => Language.KeybindMode_Strict_Name,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
|
||||||
};
|
|
||||||
|
|
||||||
public static string? Tooltip(this KeybindMode mode) => mode switch
|
|
||||||
{
|
|
||||||
KeybindMode.Flexible => Language.KeybindMode_Flexible_Tooltip,
|
|
||||||
KeybindMode.Strict => Language.KeybindMode_Strict_Tooltip,
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public enum LanguageOverride
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
ChineseSimplified,
|
|
||||||
ChineseTraditional,
|
|
||||||
Dutch,
|
|
||||||
English,
|
|
||||||
French,
|
|
||||||
German,
|
|
||||||
Greek,
|
|
||||||
|
|
||||||
// Italian,
|
|
||||||
Japanese,
|
|
||||||
|
|
||||||
// Korean,
|
|
||||||
// Norwegian,
|
|
||||||
PortugueseBrazil,
|
|
||||||
Romanian,
|
|
||||||
Russian,
|
|
||||||
Spanish,
|
|
||||||
Swedish,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LanguageOverrideExt
|
|
||||||
{
|
|
||||||
public static string Name(this LanguageOverride mode) => mode switch
|
|
||||||
{
|
|
||||||
LanguageOverride.None => Language.LanguageOverride_None,
|
|
||||||
LanguageOverride.ChineseSimplified => "简体中文",
|
|
||||||
LanguageOverride.ChineseTraditional => "繁體中文",
|
|
||||||
LanguageOverride.Dutch => "Nederlands",
|
|
||||||
LanguageOverride.English => "English",
|
|
||||||
LanguageOverride.French => "Français",
|
|
||||||
LanguageOverride.German => "Deutsch",
|
|
||||||
LanguageOverride.Greek => "Ελληνικά",
|
|
||||||
// LanguageOverride.Italian => "Italiano",
|
|
||||||
LanguageOverride.Japanese => "日本語",
|
|
||||||
// LanguageOverride.Korean => "한국어 (Korean)",
|
|
||||||
// LanguageOverride.Norwegian => "Norsk",
|
|
||||||
LanguageOverride.PortugueseBrazil => "Português do Brasil",
|
|
||||||
LanguageOverride.Romanian => "Română",
|
|
||||||
LanguageOverride.Russian => "Русский",
|
|
||||||
LanguageOverride.Spanish => "Español",
|
|
||||||
LanguageOverride.Swedish => "Svenska",
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
|
||||||
};
|
|
||||||
|
|
||||||
public static string Code(this LanguageOverride mode) => mode switch
|
|
||||||
{
|
|
||||||
LanguageOverride.None => "",
|
|
||||||
LanguageOverride.ChineseSimplified => "zh-hans",
|
|
||||||
LanguageOverride.ChineseTraditional => "zh-hant",
|
|
||||||
LanguageOverride.Dutch => "nl",
|
|
||||||
LanguageOverride.English => "en",
|
|
||||||
LanguageOverride.French => "fr",
|
|
||||||
LanguageOverride.German => "de",
|
|
||||||
LanguageOverride.Greek => "el",
|
|
||||||
// LanguageOverride.Italian => "it",
|
|
||||||
LanguageOverride.Japanese => "ja",
|
|
||||||
// LanguageOverride.Korean => "ko",
|
|
||||||
// LanguageOverride.Norwegian => "no",
|
|
||||||
LanguageOverride.PortugueseBrazil => "pt-br",
|
|
||||||
LanguageOverride.Romanian => "ro",
|
|
||||||
LanguageOverride.Russian => "ru",
|
|
||||||
LanguageOverride.Spanish => "es",
|
|
||||||
LanguageOverride.Swedish => "sv",
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
[Flags]
|
|
||||||
public enum ExtraGlyphRanges
|
|
||||||
{
|
|
||||||
ChineseFull = 1 << 0,
|
|
||||||
ChineseSimplifiedCommon = 1 << 1,
|
|
||||||
Cyrillic = 1 << 2,
|
|
||||||
Japanese = 1 << 3,
|
|
||||||
Korean = 1 << 4,
|
|
||||||
Thai = 1 << 5,
|
|
||||||
Vietnamese = 1 << 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ExtraGlyphRangesExt
|
|
||||||
{
|
|
||||||
public static string Name(this ExtraGlyphRanges ranges) => ranges switch
|
|
||||||
{
|
|
||||||
ExtraGlyphRanges.ChineseFull => Language.ExtraGlyphRanges_ChineseFull_Name,
|
|
||||||
ExtraGlyphRanges.ChineseSimplifiedCommon => Language.ExtraGlyphRanges_ChineseSimplifiedCommon_Name,
|
|
||||||
ExtraGlyphRanges.Cyrillic => Language.ExtraGlyphRanges_Cyrillic_Name,
|
|
||||||
ExtraGlyphRanges.Japanese => Language.ExtraGlyphRanges_Japanese_Name,
|
|
||||||
ExtraGlyphRanges.Korean => Language.ExtraGlyphRanges_Korean_Name,
|
|
||||||
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
|
|
||||||
{
|
|
||||||
ExtraGlyphRanges.ChineseFull => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseFull(),
|
|
||||||
ExtraGlyphRanges.ChineseSimplifiedCommon => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseSimplifiedCommon(),
|
|
||||||
ExtraGlyphRanges.Cyrillic => (nint)ImGui.GetIO().Fonts.GetGlyphRangesCyrillic(),
|
|
||||||
ExtraGlyphRanges.Japanese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesJapanese(),
|
|
||||||
ExtraGlyphRanges.Korean => (nint)ImGui.GetIO().Fonts.GetGlyphRangesKorean(),
|
|
||||||
ExtraGlyphRanges.Thai => (nint)ImGui.GetIO().Fonts.GetGlyphRangesThai(),
|
|
||||||
ExtraGlyphRanges.Vietnamese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesVietnamese(),
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
using Dalamud;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.FontIdentifier;
|
|
||||||
using Dalamud.Interface.GameFonts;
|
|
||||||
using Dalamud.Interface.ManagedFontAtlas;
|
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
|
|
||||||
namespace ChatTwo;
|
|
||||||
|
|
||||||
public class FontManager
|
|
||||||
{
|
|
||||||
internal IFontHandle Axis = null!;
|
|
||||||
internal IFontHandle AxisItalic = null!;
|
|
||||||
|
|
||||||
internal IFontHandle RegularFont = null!;
|
|
||||||
internal IFontHandle? ItalicFont;
|
|
||||||
|
|
||||||
internal IFontHandle FontAwesome = null!;
|
|
||||||
|
|
||||||
internal readonly byte[] GameSymFont;
|
|
||||||
|
|
||||||
private ushort[] Ranges = [];
|
|
||||||
private ushort[] JpRange = [];
|
|
||||||
|
|
||||||
public static readonly HashSet<float> AxisFontSizeList =
|
|
||||||
[
|
|
||||||
9.6f, 10f, 12f, 14f, 16f,
|
|
||||||
18f, 18.4f, 20f, 23f, 34f,
|
|
||||||
36f, 40f, 45f, 46f, 68f, 90f,
|
|
||||||
];
|
|
||||||
|
|
||||||
public FontManager()
|
|
||||||
{
|
|
||||||
var filePath = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "FFXIV_Lodestone_SSF.ttf");
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
{
|
|
||||||
GameSymFont = File.ReadAllBytes(filePath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GameSymFont = new HttpClient().GetAsync("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")
|
|
||||||
.Result
|
|
||||||
.Content
|
|
||||||
.ReadAsByteArrayAsync()
|
|
||||||
.Result;
|
|
||||||
|
|
||||||
Dalamud.Utility.FilesystemUtil.WriteAllBytesSafe(filePath, GameSymFont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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[] GetHellionFontBytes()
|
|
||||||
{
|
|
||||||
if (HellionFontBytes is not null)
|
|
||||||
return HellionFontBytes;
|
|
||||||
|
|
||||||
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
|
|
||||||
?? throw new FileNotFoundException("Hellion font resource not embedded in the assembly");
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
stream.CopyTo(ms);
|
|
||||||
HellionFontBytes = ms.ToArray();
|
|
||||||
return HellionFontBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void SetUpRanges()
|
|
||||||
{
|
|
||||||
ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
|
|
||||||
{
|
|
||||||
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
|
|
||||||
// text
|
|
||||||
foreach (var range in ranges)
|
|
||||||
builder.AddRanges((ushort*)range);
|
|
||||||
|
|
||||||
// chars
|
|
||||||
if (chars != null)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < chars.Count; i += 2)
|
|
||||||
{
|
|
||||||
if (chars[i] == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
for (var j = (uint) chars[i]; j <= chars[i + 1]; j++)
|
|
||||||
builder.AddChar((ushort) j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ingame supported ranges
|
|
||||||
var reader = new FdtReader(Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data);
|
|
||||||
foreach (var c in reader.Glyphs)
|
|
||||||
builder.AddChar(c.Char);
|
|
||||||
|
|
||||||
// various symbols
|
|
||||||
// French
|
|
||||||
// Romanian
|
|
||||||
// builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─\~");
|
|
||||||
builder.AddText("Œœ");
|
|
||||||
builder.AddText("ĂăÂâÎîȘșȚț");
|
|
||||||
|
|
||||||
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
|
|
||||||
for (var i = 0x2460; i <= 0x24B5; i++)
|
|
||||||
builder.AddChar((char) i);
|
|
||||||
|
|
||||||
builder.AddChar('⓪');
|
|
||||||
return builder.BuildRangesToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
var ranges = new List<nint> { (nint)ImGui.GetIO().Fonts.GetGlyphRangesDefault() };
|
|
||||||
foreach (var extraRange in Enum.GetValues<ExtraGlyphRanges>())
|
|
||||||
if (Plugin.Config.ExtraGlyphRanges.HasFlag(extraRange))
|
|
||||||
ranges.Add(extraRange.Range());
|
|
||||||
|
|
||||||
Ranges = BuildRange(null, ranges.ToArray());
|
|
||||||
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void BuildFonts()
|
|
||||||
{
|
|
||||||
SetUpRanges();
|
|
||||||
|
|
||||||
Axis = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2)));
|
|
||||||
AxisItalic = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
|
|
||||||
{
|
|
||||||
SkewStrength = SizeInPx(Plugin.Config.FontSizeV2) / 6
|
|
||||||
});
|
|
||||||
|
|
||||||
FontAwesome = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
|
||||||
{
|
|
||||||
e.OnPreBuild(tk => tk.AddFontAwesomeIconFont(new SafeFontConfig { SizePx = GetFontSize() }));
|
|
||||||
e.OnPostBuild(tk => tk.FitRatio(tk.Font));
|
|
||||||
});
|
|
||||||
|
|
||||||
RegularFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(
|
|
||||||
e => e.OnPreBuild(
|
|
||||||
tk =>
|
|
||||||
{
|
|
||||||
var config = new SafeFontConfig {SizePt = Plugin.Config.GlobalFontV2.SizePt, 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
|
|
||||||
{
|
|
||||||
return fontId.AddToBuildToolkit(tk, config);
|
|
||||||
}
|
|
||||||
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
|
|
||||||
{
|
|
||||||
Plugin.Log.Warning(e, $"Configured {slot} font unavailable, falling back to NotoSansCjkRegular");
|
|
||||||
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
|
|
||||||
return fallback.AddToBuildToolkit(tk, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
|
|
||||||
namespace ChatTwo.GameFunctions.Types;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class TellTarget
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public uint World { get; set; }
|
|
||||||
public ulong ContentId { get; private set; }
|
|
||||||
public TellReason Reason { get; private set; }
|
|
||||||
|
|
||||||
public TellTarget(string name, uint world, ulong contentId, TellReason reason)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
World = world;
|
|
||||||
ContentId = contentId;
|
|
||||||
Reason = reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSet()
|
|
||||||
=> Name.Length > 0 && World > 0;
|
|
||||||
|
|
||||||
public string ToWorldString()
|
|
||||||
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
|
|
||||||
|
|
||||||
public string ToTargetString()
|
|
||||||
=> $"{Name}@{ToWorldString()}";
|
|
||||||
|
|
||||||
public unsafe void FromTarget(IPlayerCharacter target)
|
|
||||||
{
|
|
||||||
Name = target.Name.TextValue;
|
|
||||||
World = target.HomeWorld.RowId;
|
|
||||||
ContentId = ((Character*)target.Address)->ContentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
name: Hellion Chat
|
|
||||||
author: JonKazama-Hellion
|
|
||||||
punchline: GDPR-compliant, Linux-aware fork of Chat 2
|
|
||||||
description: |-
|
|
||||||
Hellion Chat is a privacy-focused, Linux-aware fork of Chat 2.
|
|
||||||
|
|
||||||
Same chat replacement you know from upstream, with extra controls
|
|
||||||
for what actually gets stored:
|
|
||||||
|
|
||||||
- Channel whitelist for database persistence (GDPR Art. 25)
|
|
||||||
- Privacy-First defaults: only your own conversations are kept
|
|
||||||
- Failsafe for unknown ChatTypes (default OFF)
|
|
||||||
- Independent plugin state (own config + database directory)
|
|
||||||
|
|
||||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
|
||||||
repo_url: https://github.com/JonKazama-Hellion/HellionChat
|
|
||||||
accepts_feedback: true
|
|
||||||
tags:
|
|
||||||
- Social
|
|
||||||
- UI
|
|
||||||
- Chat
|
|
||||||
- Replacement
|
|
||||||
- Privacy
|
|
||||||
changelog: |-
|
|
||||||
**Hellion Chat 0.1.0 — Initial fork release**
|
|
||||||
|
|
||||||
Privacy
|
|
||||||
- Channel whitelist filter in MessageStore.UpsertMessage with a
|
|
||||||
Privacy-First default (own conversations only)
|
|
||||||
- Per-channel retention with a 24-hour idempotent background sweep
|
|
||||||
(default OFF; spec defaults of 365 / 90 / 30 days)
|
|
||||||
- Retroactive cleanup with a Ctrl+Shift confirm and VACUUM
|
|
||||||
- Export to Markdown / JSON / CSV via Dalamud's file dialog
|
|
||||||
(GDPR Art. 15 right of access)
|
|
||||||
|
|
||||||
Onboarding
|
|
||||||
- First-run wizard with three profiles: Privacy-First / Casual /
|
|
||||||
Full History
|
|
||||||
- Configuration v6 → v7 migration that seeds defaults and shows a
|
|
||||||
notification once on update
|
|
||||||
- One-shot migration from upstream Chat 2's pluginConfigs layout
|
|
||||||
so the fork uses pluginConfigs/HellionChat without losing state
|
|
||||||
- Migrate3 idempotency recovery for half-migrated databases
|
|
||||||
|
|
||||||
Look & feel
|
|
||||||
- Localized UI (English and German) with live language switching
|
|
||||||
- Hellion industrial HUD theme with cyan-teal action accents,
|
|
||||||
slate-violet tabs, amber active highlights and a window-opacity
|
|
||||||
slider for combat-friendly transparency
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
node_modules
|
|
||||||
|
|
||||||
# Output
|
|
||||||
.output
|
|
||||||
.vercel
|
|
||||||
.netlify
|
|
||||||
.wrangler
|
|
||||||
/.svelte-kit
|
|
||||||
/build
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Env
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
!.env.test
|
|
||||||
|
|
||||||
# Vite
|
|
||||||
vite.config.js.timestamp-*
|
|
||||||
vite.config.ts.timestamp-*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
engine-strict=true
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# sv
|
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
|
||||||
|
|
||||||
## Creating a project
|
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# create a new project in the current directory
|
|
||||||
npx sv create
|
|
||||||
|
|
||||||
# create a new project in my-app
|
|
||||||
npx sv create my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To create a production version of your app:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
|
||||||
|
|
||||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
|
||||||
Generated
-1573
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite dev",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
|
||||||
"@sveltejs/kit": "^2.22.0",
|
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
|
||||||
"svelte": "^5.39.2",
|
|
||||||
"svelte-check": "^4.0.0",
|
|
||||||
"sveltekit-sse": "^0.14.3",
|
|
||||||
"typescript": "^5.0.0",
|
|
||||||
"vite": "^7.0.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@sveltestrap/sveltestrap": "^7.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
-23
@@ -1,23 +0,0 @@
|
|||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
|
||||||
namespace App {
|
|
||||||
interface Error {
|
|
||||||
code: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
// interface Locals {}
|
|
||||||
// interface PageData {}
|
|
||||||
// interface PageState {}
|
|
||||||
// interface Platform {}
|
|
||||||
|
|
||||||
interface Warning {
|
|
||||||
hasWarning: boolean;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Element { scrollTopMax: number } // Firefox only property
|
|
||||||
}
|
|
||||||
|
|
||||||
export {};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" data-bs-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
|
||||||
%sveltekit.head%
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {isChannelLocked, channelOptions} from "$lib/shared.svelte";
|
|
||||||
|
|
||||||
let selectElement: HTMLSelectElement;
|
|
||||||
|
|
||||||
async function requestChannelSwitch(event: Event) {
|
|
||||||
if (!event.currentTarget)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let element = (event.currentTarget as HTMLSelectElement);
|
|
||||||
let requestedChannel = element.value;
|
|
||||||
|
|
||||||
console.log(element.value)
|
|
||||||
element.value = '0';
|
|
||||||
|
|
||||||
const rawResponse = await fetch('/channel', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ channel: requestedChannel })
|
|
||||||
});
|
|
||||||
// const content = await rawResponse.json();
|
|
||||||
// TODO: use the response
|
|
||||||
}
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | null = null;
|
|
||||||
function getTextWidth(text: string): number {
|
|
||||||
// re-use canvas object for better performance
|
|
||||||
if (canvas === null)
|
|
||||||
canvas = document.createElement("canvas");
|
|
||||||
|
|
||||||
const context: CanvasRenderingContext2D | null = canvas.getContext("2d");
|
|
||||||
if (!context)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
context.font = getCanvasFont(selectElement);
|
|
||||||
const metrics = context.measureText(text);
|
|
||||||
return metrics.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCssStyle(element: Element, prop: string): string {
|
|
||||||
return window.getComputedStyle(element, null).getPropertyValue(prop);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCanvasFont(el = document.body) {
|
|
||||||
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
|
|
||||||
const fontSize = getCssStyle(el, 'font-size') || '16px';
|
|
||||||
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
|
|
||||||
|
|
||||||
return `${fontWeight} ${fontSize} ${fontFamily}`;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<select
|
|
||||||
bind:this={selectElement}
|
|
||||||
id="channel-select"
|
|
||||||
style="pointer-events: {isChannelLocked.locked ? 'none' : 'inherit'}; width: {(channelOptions.length > 1 ? getTextWidth(channelOptions[0].text) : 1) + 40}px"
|
|
||||||
onchange={(e) => requestChannelSwitch(e)}>
|
|
||||||
{#each channelOptions as channelOption}
|
|
||||||
{#if channelOption.preview }
|
|
||||||
<option selected disabled hidden value={channelOption.value}>
|
|
||||||
{channelOption.text}
|
|
||||||
</option>
|
|
||||||
{:else}
|
|
||||||
<option value={channelOption.value}>
|
|
||||||
{channelOption.text}
|
|
||||||
</option>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
select {
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { subscribe } from "$lib/utils.svelte";
|
|
||||||
import { chatInput, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
|
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
|
||||||
|
|
||||||
let skipNextCheck: boolean = $state(false);
|
|
||||||
let requiresResize: boolean = $state(true);
|
|
||||||
|
|
||||||
subscribe(
|
|
||||||
() => chatInput,
|
|
||||||
(v) => {
|
|
||||||
if (skipNextCheck) {
|
|
||||||
skipNextCheck = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input box has been reset to empty, so resize it back to smaller box
|
|
||||||
if (v.content === '') {
|
|
||||||
console.log("Empty chatbox, resize");
|
|
||||||
requiresResize = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove newline characters
|
|
||||||
let original = v.content;
|
|
||||||
v.content = v.content.replace(/(\r\n|\n|\r)/gm,"");
|
|
||||||
|
|
||||||
console.log(`${original.length} vs ${v.content.length}`);
|
|
||||||
let hasChanged = original.length != v.content.length;
|
|
||||||
if (hasChanged) {
|
|
||||||
skipNextCheck = true;
|
|
||||||
requiresResize = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function preventNewlines(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
// Prevent key from creating a newline
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// submit the data
|
|
||||||
const newEvent = new Event('submit', {bubbles: true, cancelable: true});
|
|
||||||
if (e.currentTarget !== null) {
|
|
||||||
(e.currentTarget as HTMLTextAreaElement).closest('form')?.dispatchEvent(newEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
if (!textarea)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const scrolledToBottom = messagesList.scrolledToBottom;
|
|
||||||
textarea.style.height = '1px';
|
|
||||||
textarea.style.height = `${textarea.scrollHeight + 10}px`; // with +10px extra padding
|
|
||||||
if (scrolledToBottom)
|
|
||||||
scrollMessagesToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
console.log(`Checking effect: ${requiresResize}`)
|
|
||||||
if (requiresResize) {
|
|
||||||
requiresResize = false;
|
|
||||||
resize();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
bind:this={textarea}
|
|
||||||
bind:value={chatInput.content}
|
|
||||||
oninput={() => resize()}
|
|
||||||
onkeydown={(e) => preventNewlines(e)}
|
|
||||||
|
|
||||||
id="chat-input"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="Message"
|
|
||||||
enterkeyhint="send"
|
|
||||||
maxlength="500">
|
|
||||||
</textarea>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
textarea {
|
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: var(--bg-input);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--focus-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
min-height: 2.5em;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { selectedTab, knownTabs, tabPaneState, tabPaneAnimationState, closeTabPane, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
|
|
||||||
|
|
||||||
async function selectTab(index: number) {
|
|
||||||
const rawResponse = await fetch('/tab', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ index })
|
|
||||||
});
|
|
||||||
// const content = await rawResponse.json();
|
|
||||||
// TODO: use the response
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
tabPaneAnimationState.noAnimation = false;
|
|
||||||
closeTabPane();
|
|
||||||
}
|
|
||||||
|
|
||||||
let scrolledToBottom = true;
|
|
||||||
function ontransitionstart() {
|
|
||||||
scrolledToBottom = messagesList.scrolledToBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ontransitionend() {
|
|
||||||
if (scrolledToBottom) {
|
|
||||||
scrollMessagesToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<aside
|
|
||||||
id="tabs"
|
|
||||||
class:no-animation={tabPaneAnimationState.noAnimation}
|
|
||||||
class:hidden={!tabPaneState.visible}
|
|
||||||
{ontransitionstart}
|
|
||||||
{ontransitionend}
|
|
||||||
>
|
|
||||||
<div class="inner">
|
|
||||||
<header>
|
|
||||||
<span>Tabs</span>
|
|
||||||
<button type="button" onclick={() => handleClose()}>
|
|
||||||
<!-- "chevron-left" icon from https://github.com/feathericons/feather, under MIT license -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<ol id="tabs-list">
|
|
||||||
{#each knownTabs as tab}
|
|
||||||
<li class:active={selectedTab.index === tab.index}>
|
|
||||||
<button type="button" onclick={() => selectTab(tab.index)}>
|
|
||||||
{ tab.name } {tab.unreadCount > 0 ? `(${tab.unreadCount})`: '' }
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {tabPaneState, tabPaneAnimationState, openTabPane, knownTabs} from "$lib/shared.svelte";
|
|
||||||
|
|
||||||
function onclick() {
|
|
||||||
tabPaneAnimationState.noAnimation = false;
|
|
||||||
openTabPane();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button type="button" aria-label="Open tab pane" class:visible={!tabPaneState.visible} class:unread={knownTabs.some((tab) => tab.unreadCount > 0)} {onclick} disabled={tabPaneState.visible}>
|
|
||||||
<!-- "chevron-right" icon from https://github.com/feathericons/feather, under MIT license -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
button {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
padding: 25px 0;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
z-index: 100;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 250ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.unread svg {
|
|
||||||
stroke: var(--unread-color);
|
|
||||||
filter: drop-shadow(0 0 2px var(--unread-color));
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>%sveltekit.error.message%</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Status: %sveltekit.status%</p>
|
|
||||||
<p>Message: %sveltekit.error.message%</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
import { channelOptions, isChannelLocked, selectedTab, knownTabs, chatInput, messagesList, scrollMessagesToBottom } from "$lib/shared.svelte";
|
|
||||||
import { WebPayloadType } from "$lib/payload";
|
|
||||||
import { source, type Source } from "sveltekit-sse";
|
|
||||||
|
|
||||||
interface ChatElements {
|
|
||||||
messagesContainer: Element | null,
|
|
||||||
messagesList: HTMLElement | null,
|
|
||||||
|
|
||||||
timestampWidthProbe: HTMLElement | null,
|
|
||||||
|
|
||||||
inputForm: Element | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.Messages`
|
|
||||||
interface Messages {
|
|
||||||
messages: MessageResponse[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.MessageResponse`
|
|
||||||
interface MessageResponse {
|
|
||||||
id: string;
|
|
||||||
timestamp: string;
|
|
||||||
templates: Template[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.MessageTemplate`
|
|
||||||
interface Template {
|
|
||||||
payloadType: WebPayloadType;
|
|
||||||
content: string;
|
|
||||||
iconId: number;
|
|
||||||
color: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.SwitchChannel`
|
|
||||||
interface SwitchChannel {
|
|
||||||
channelName: Template[];
|
|
||||||
channelValue: number;
|
|
||||||
channelLocked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.ChannelList`
|
|
||||||
interface ChannelList {
|
|
||||||
channels: {[key: string]: number};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.ChatTab`
|
|
||||||
export interface ChatTab {
|
|
||||||
name: string;
|
|
||||||
index: number;
|
|
||||||
unreadCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.ChatTabList`
|
|
||||||
interface ChatTabList {
|
|
||||||
tabs: ChatTab[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref `DataStructure.ChatTabUnreadState`
|
|
||||||
interface ChatTabUnreadState {
|
|
||||||
index: number;
|
|
||||||
unreadCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChatTwoWeb {
|
|
||||||
elements!: ChatElements;
|
|
||||||
maxTimestampWidth: number = 0;
|
|
||||||
|
|
||||||
sse!: EventSource;
|
|
||||||
connection!: Source;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.setupDOMElements();
|
|
||||||
this.setupSSEConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupDOMElements() {
|
|
||||||
this.elements = {
|
|
||||||
messagesContainer: document.querySelector('#messages > .scroll-container')!,
|
|
||||||
messagesList: document.getElementById('messages-list'),
|
|
||||||
|
|
||||||
timestampWidthProbe: document.getElementById('timestamp-width-probe'),
|
|
||||||
|
|
||||||
inputForm: document.querySelector('#input > form'),
|
|
||||||
};
|
|
||||||
messagesList.element = this.elements.messagesList;
|
|
||||||
|
|
||||||
// add indicator signaling more messages below
|
|
||||||
this.elements.messagesContainer?.addEventListener('scroll', (event) => {
|
|
||||||
if (event.currentTarget === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let parentElement = (event.currentTarget as HTMLDivElement).parentElement;
|
|
||||||
if (!this.messagesAreScrolledToBottom()) {
|
|
||||||
parentElement?.classList.add('more-messages');
|
|
||||||
} else {
|
|
||||||
parentElement?.classList.remove('more-messages');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// adjust scroll when the window size changes; mostly for mobile (opening/closing the keyboard)
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
if (messagesList.scrolledToBottom) {
|
|
||||||
scrollMessagesToBottom();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// handle message sending
|
|
||||||
this.elements.inputForm?.addEventListener('submit', async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (chatInput.content.length > 500) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawResponse = await fetch('/send', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ message: chatInput.content })
|
|
||||||
});
|
|
||||||
// const content = await rawResponse.json();
|
|
||||||
// TODO: use the response
|
|
||||||
|
|
||||||
chatInput.content = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesAreScrolledToBottom() {
|
|
||||||
if (this.elements.messagesContainer === null) {
|
|
||||||
return messagesList.scrolledToBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesList.scrolledToBottom =
|
|
||||||
(
|
|
||||||
this.elements.messagesContainer.scrollHeight -
|
|
||||||
this.elements.messagesContainer.clientHeight -
|
|
||||||
this.elements.messagesContainer.scrollTop
|
|
||||||
) < 1;
|
|
||||||
|
|
||||||
return messagesList.scrolledToBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateChannelHint(channel: SwitchChannel) {
|
|
||||||
// Set storage to the current lock state
|
|
||||||
isChannelLocked.locked = channel.channelLocked;
|
|
||||||
|
|
||||||
const channelElement = this.processTemplate(channel.channelName);
|
|
||||||
if (!channelElement.firstChild)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let channelName = (channelElement.firstChild as HTMLSpanElement).innerText;
|
|
||||||
if (channel.channelLocked)
|
|
||||||
channelName = `(Locked) ${channelName}`;
|
|
||||||
|
|
||||||
channelOptions[0] = {text: channelName, value: 0, preview: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
updateChannels(channelList: ChannelList) {
|
|
||||||
channelOptions.length = 1;
|
|
||||||
|
|
||||||
for (const [ label, channel ] of Object.entries(channelList.channels)) {
|
|
||||||
channelOptions.push( { text: label, value: channel, preview: false } )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate timestamp width to ensure that all timestamps have the same width.
|
|
||||||
// some typefaces have the same width across all number glyphs, others do not.
|
|
||||||
// then there's AM/PM vs 24 hour, and so on
|
|
||||||
calculateTimestampWidth(timestamp: string) {
|
|
||||||
if (this.elements.timestampWidthProbe === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.elements.timestampWidthProbe.innerText = timestamp;
|
|
||||||
if (this.elements.timestampWidthProbe.clientWidth > this.maxTimestampWidth) {
|
|
||||||
this.maxTimestampWidth = this.elements.timestampWidthProbe.clientWidth;
|
|
||||||
document.body.style.setProperty('--timestamp-width', (Math.ceil(this.maxTimestampWidth) + 1) + 'px');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addMessage(messageData: MessageResponse) {
|
|
||||||
if (this.elements.messagesList === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const scrolledToBottom = this.messagesAreScrolledToBottom();
|
|
||||||
this.calculateTimestampWidth(messageData.timestamp);
|
|
||||||
|
|
||||||
const liMessage = document.createElement('li');
|
|
||||||
const spanTimestamp = document.createElement('span');
|
|
||||||
spanTimestamp.classList.add('timestamp');
|
|
||||||
spanTimestamp.innerText = messageData.timestamp;
|
|
||||||
|
|
||||||
const spanMessage = document.createElement('span');
|
|
||||||
spanMessage.classList.add('message');
|
|
||||||
spanMessage.appendChild(this.processTemplate(messageData.templates))
|
|
||||||
|
|
||||||
liMessage.appendChild(spanTimestamp);
|
|
||||||
liMessage.appendChild(spanMessage);
|
|
||||||
this.elements.messagesList.appendChild(liMessage);
|
|
||||||
|
|
||||||
if (scrolledToBottom) {
|
|
||||||
scrollMessagesToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processTemplate(templates: Template[]) {
|
|
||||||
const frag = document.createDocumentFragment();
|
|
||||||
|
|
||||||
for( const template of templates ) {
|
|
||||||
const spanElement = document.createElement('span');
|
|
||||||
switch (template.payloadType) {
|
|
||||||
case WebPayloadType.RawText:
|
|
||||||
this.processTextTemplate(template, spanElement);
|
|
||||||
break;
|
|
||||||
case WebPayloadType.CustomUri:
|
|
||||||
this.processUrlTemplate(template, spanElement);
|
|
||||||
break;
|
|
||||||
case WebPayloadType.CustomEmote:
|
|
||||||
this.processEmote(template, spanElement);
|
|
||||||
break;
|
|
||||||
case WebPayloadType.Icon:
|
|
||||||
this.processIcon(template, spanElement);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
frag.appendChild(spanElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return frag;
|
|
||||||
}
|
|
||||||
|
|
||||||
processTextTemplate(template: Template, spanElement: HTMLSpanElement) {
|
|
||||||
spanElement.innerText = template.content;
|
|
||||||
if (template.color !== 0)
|
|
||||||
{
|
|
||||||
this.processColor(template, spanElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processUrlTemplate(template: Template, spanElement: HTMLSpanElement) {
|
|
||||||
const urlElement = document.createElement('a');
|
|
||||||
let url = template.content;
|
|
||||||
if (!url.startsWith('https://')) {
|
|
||||||
url = `https://${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
urlElement.innerText = template.content;
|
|
||||||
urlElement.href = encodeURI(url);
|
|
||||||
urlElement.target = '_blank'
|
|
||||||
|
|
||||||
if (template.color !== 0)
|
|
||||||
{
|
|
||||||
this.processColor(template, spanElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
spanElement.appendChild(urlElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts a RGBA uint number to components
|
|
||||||
processColor(template: Template, spanElement: HTMLSpanElement) {
|
|
||||||
const r = (template.color & 0xFF000000) >>> 24;
|
|
||||||
const g = (template.color & 0xFF0000) >>> 16;
|
|
||||||
const b = (template.color & 0xFF00) >>> 8;
|
|
||||||
const a = (template.color & 0xFF) / 255.0;
|
|
||||||
|
|
||||||
spanElement.style.color = `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
processEmote(template: Template, spanElement: HTMLSpanElement) {
|
|
||||||
const imgElement = document.createElement('img');
|
|
||||||
imgElement.src = `/emote/${template.content}`;
|
|
||||||
|
|
||||||
spanElement.classList.add('emote-icon');
|
|
||||||
spanElement.appendChild(imgElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
processIcon(template: Template, spanElement: HTMLSpanElement) {
|
|
||||||
spanElement.classList.add('gfd-icon');
|
|
||||||
spanElement.classList.add(`gfd-icon-hq-${template.iconId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAllMessages() {
|
|
||||||
if (this.elements.messagesList === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.elements.messagesList.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSSEConnection() {
|
|
||||||
this.connection = source('/sse')
|
|
||||||
|
|
||||||
this.connection.select('close').subscribe((data: string) => {
|
|
||||||
console.log(`close: ${data}`)
|
|
||||||
if (data) {
|
|
||||||
console.log('Closing SSE connection.');
|
|
||||||
this.connection.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// new messages to be appended to the message list
|
|
||||||
this.connection.select('new-message').subscribe((data: string) => {
|
|
||||||
console.log(`new-message: ${data}`)
|
|
||||||
if (data) {
|
|
||||||
try {
|
|
||||||
let message: MessageResponse = JSON.parse(data);
|
|
||||||
this.addMessage(message);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// a bulk of new messages, with a clear of the message list beforehand
|
|
||||||
this.connection.select('bulk-messages').subscribe((data: string) => {
|
|
||||||
console.log(`bulk-messages: ${data}`)
|
|
||||||
if (data) {
|
|
||||||
this.clearAllMessages();
|
|
||||||
try {
|
|
||||||
let messages: Messages = JSON.parse(data);
|
|
||||||
for (const message of messages.messages) {
|
|
||||||
this.addMessage(message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connection.select('channel-switched').subscribe((data: string) => {
|
|
||||||
console.log(`channel-switched: ${data}`)
|
|
||||||
if (data) {
|
|
||||||
try {
|
|
||||||
let channel: SwitchChannel = JSON.parse(data);
|
|
||||||
this.updateChannelHint(channel);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// list of all channels
|
|
||||||
this.connection.select('channel-list').subscribe((data: string) => {
|
|
||||||
console.log(`channel-list: ${data}`)
|
|
||||||
if (data) {
|
|
||||||
try {
|
|
||||||
let channelList: ChannelList = JSON.parse(data);
|
|
||||||
this.updateChannels(channelList);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// tab switched
|
|
||||||
this.connection.select('tab-switched').subscribe((data: string) => {
|
|
||||||
console.log(`tab-switched: ${data}`)
|
|
||||||
if (data) {
|
|
||||||
try {
|
|
||||||
const chatTab: ChatTab = JSON.parse(data);
|
|
||||||
selectedTab.index = chatTab.index;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// list of all tabs
|
|
||||||
this.connection.select('tab-list').subscribe((data: string) => {
|
|
||||||
console.log(`tab-list: ${data}`)
|
|
||||||
if (data) {
|
|
||||||
try {
|
|
||||||
const chatTabList: ChatTabList = JSON.parse(data);
|
|
||||||
knownTabs.length = 0;
|
|
||||||
for (const tab of chatTabList.tabs) {
|
|
||||||
knownTabs.push(tab);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// the unread state of a specific tab has changed
|
|
||||||
this.connection.select('tab-unread-state').subscribe((data: string) => {
|
|
||||||
console.log(`tab-unread-state`, data)
|
|
||||||
if (data) {
|
|
||||||
try {
|
|
||||||
const chatTabUnreadState: ChatTabUnreadState = JSON.parse(data);
|
|
||||||
let tab = knownTabs.find((tab) => tab.index === chatTabUnreadState.index);
|
|
||||||
if (tab) {
|
|
||||||
tab.unreadCount = chatTabUnreadState.unreadCount;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.error("Unable to find tab!")
|
|
||||||
console.error(chatTabUnreadState)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// from kizer, gfd icons
|
|
||||||
interface GdfEntry {
|
|
||||||
id: number,
|
|
||||||
left: number,
|
|
||||||
top: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
unk0A: number,
|
|
||||||
redirect: number,
|
|
||||||
unk0E: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StylesheetEntry {
|
|
||||||
ids: number[],
|
|
||||||
style1: string,
|
|
||||||
style2: string,
|
|
||||||
width: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addGfdStylesheet(gfdPath: string, texPath: string) {
|
|
||||||
const texPromise = loadTexAsBlob(texPath);
|
|
||||||
const gfdPromise = loadGfd(gfdPath);
|
|
||||||
const texUrl = URL.createObjectURL(await texPromise);
|
|
||||||
const gfd = await gfdPromise;
|
|
||||||
|
|
||||||
const stylesheets: {[id: number]: StylesheetEntry} = [];
|
|
||||||
for (const entry of gfd) {
|
|
||||||
if (entry.width * entry.height <= 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (entry.redirect !== 0) {
|
|
||||||
stylesheets[entry.redirect].ids.push(entry.id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
stylesheets[entry.id] = {
|
|
||||||
ids: [entry.id],
|
|
||||||
style1: [
|
|
||||||
`background-position: -${entry.left}px -${entry.top}px`,
|
|
||||||
`background-image: url('${texUrl}')`,
|
|
||||||
`width: ${entry.width}px`,
|
|
||||||
`height: ${entry.height}px`
|
|
||||||
].join(';'),
|
|
||||||
style2: [
|
|
||||||
`background-position: -${entry.left * 2}px -${entry.top * 2 + 341}px`,
|
|
||||||
`background-image: url('${texUrl}')`,
|
|
||||||
`width: ${entry.width * 2}px`,
|
|
||||||
`height: ${entry.height * 2}px`
|
|
||||||
].join(';'),
|
|
||||||
width: entry.width
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let stylesheet = '';
|
|
||||||
for (const entry of Object.values(stylesheets)) {
|
|
||||||
if (!entry)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-${x}::before`).join(', ')}{${entry.style1};}`;
|
|
||||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-hq-${x}::before`).join(', ')}{${entry.style2};}`;
|
|
||||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-${x}`).join(', ')}{width:${entry.width}px;}`;
|
|
||||||
stylesheet += `\n${entry.ids.map(x => `.gfd-icon.gfd-icon-hq-${x}`).join(', ')}{width:${entry.width * 2}px;}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const styleNode = document.createElement('style');
|
|
||||||
styleNode.appendChild(document.createTextNode(stylesheet));
|
|
||||||
document.head.appendChild(styleNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTexAsBlob(path: string) {
|
|
||||||
const tex = parseTex(await (await fetch(path)).arrayBuffer());
|
|
||||||
if (tex.format !== 0x1450) // B8G8R8A8
|
|
||||||
throw 'Not supported';
|
|
||||||
|
|
||||||
const dataArray = new Uint8ClampedArray(tex.buffer, tex.offsetToSurface[0], tex.width * tex.height * 4);
|
|
||||||
for (let i = 0; i < dataArray.length; i += 4) {
|
|
||||||
const t = dataArray[i];
|
|
||||||
dataArray[i] = dataArray[i + 2];
|
|
||||||
dataArray[i + 2] = t;
|
|
||||||
}
|
|
||||||
const imageData = new ImageData(dataArray, tex.width, tex.height);
|
|
||||||
const bitmap = await createImageBitmap(imageData);
|
|
||||||
|
|
||||||
const canvas = new OffscreenCanvas(tex.width, tex.height);
|
|
||||||
canvas.getContext('bitmaprenderer')?.transferFromImageBitmap(bitmap);
|
|
||||||
return await canvas.convertToBlob();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGfd(path: string) {
|
|
||||||
const buffer = new DataView(await (await fetch(path)).arrayBuffer());
|
|
||||||
const count = buffer.getInt32(8, true);
|
|
||||||
const entries: GdfEntry[] = new Array(count);
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const offset = 0x10 + (i * 0x10);
|
|
||||||
entries[i] = {
|
|
||||||
id: buffer.getInt16(offset, true),
|
|
||||||
left: buffer.getInt16(offset + 2, true),
|
|
||||||
top: buffer.getInt16(offset + 4, true),
|
|
||||||
width: buffer.getInt16(offset + 6, true),
|
|
||||||
height: buffer.getInt16(offset + 8, true),
|
|
||||||
unk0A: buffer.getInt16(offset + 10, true),
|
|
||||||
redirect: buffer.getInt16(offset + 12, true),
|
|
||||||
unk0E: buffer.getInt16(offset + 14, true),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseTex(arrayBuffer: ArrayBuffer) {
|
|
||||||
const buffer = new DataView(arrayBuffer);
|
|
||||||
const type = buffer.getInt32(0, true);
|
|
||||||
const format = buffer.getInt32(4, true);
|
|
||||||
const width = buffer.getInt16(8, true);
|
|
||||||
const height = buffer.getInt16(10, true);
|
|
||||||
const depth = buffer.getInt16(12, true);
|
|
||||||
const mipsAndFlag = buffer.getInt8(14);
|
|
||||||
const arraySize = buffer.getInt8(15);
|
|
||||||
const lodOffsets = [buffer.getInt32(16, true), buffer.getInt32(20, true), buffer.getInt32(24, true)];
|
|
||||||
const offsetToSurface = [buffer.getInt32(28, true), buffer.getInt32(32, true), buffer.getInt32(36, true), buffer.getInt32(40, true), buffer.getInt32(44, true), buffer.getInt32(48, true), buffer.getInt32(52, true), buffer.getInt32(56, true), buffer.getInt32(60, true), buffer.getInt32(64, true), buffer.getInt32(68, true), buffer.getInt32(72, true), buffer.getInt32(76, true)];
|
|
||||||
|
|
||||||
return {
|
|
||||||
buffer: arrayBuffer,
|
|
||||||
type,
|
|
||||||
format,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
depth,
|
|
||||||
mipsAndFlag,
|
|
||||||
arraySize,
|
|
||||||
lodOffsets,
|
|
||||||
offsetToSurface,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export enum WebPayloadType {
|
|
||||||
// Dalamud
|
|
||||||
Unknown,
|
|
||||||
Player,
|
|
||||||
Item,
|
|
||||||
Status,
|
|
||||||
RawText,
|
|
||||||
UIForeground,
|
|
||||||
UIGlow,
|
|
||||||
MapLink,
|
|
||||||
AutoTranslateText,
|
|
||||||
EmphasisItalic,
|
|
||||||
Icon,
|
|
||||||
Quest,
|
|
||||||
DalamudLink,
|
|
||||||
NewLine,
|
|
||||||
SeHyphen,
|
|
||||||
PartyFinder,
|
|
||||||
|
|
||||||
// Custom
|
|
||||||
CustomPartyFinder = 0x50,
|
|
||||||
CustomAchievement = 0x51,
|
|
||||||
CustomUri = 0x52,
|
|
||||||
CustomEmote = 0x53,
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import type { ChatTab } from "./chat.svelte";
|
|
||||||
|
|
||||||
export const isChannelLocked: { locked: boolean } = $state({ locked: false });
|
|
||||||
export const channelOptions: ChannelOption[] = $state([ { text: 'Invalid', value: 0, preview: true } ]);
|
|
||||||
|
|
||||||
export interface ChannelOption {
|
|
||||||
text: string;
|
|
||||||
value: number;
|
|
||||||
preview: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const selectedTab: { index: number } = $state({ index: 0 });
|
|
||||||
export const knownTabs: ChatTab[] = $state([]);
|
|
||||||
export const tabPaneState: { visible: boolean } = $state({ visible: true });
|
|
||||||
export const tabPaneAnimationState: { noAnimation: boolean } = $state({ noAnimation: true });
|
|
||||||
export const persistentTabPabeStateKey = 'chat2_tab_pane_visible';
|
|
||||||
|
|
||||||
export function openTabPane() {
|
|
||||||
tabPaneState.visible = true;
|
|
||||||
window.localStorage.setItem(persistentTabPabeStateKey, 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function closeTabPane() {
|
|
||||||
tabPaneState.visible = false;
|
|
||||||
window.localStorage.setItem(persistentTabPabeStateKey, 'false');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const chatInput: { content: string } = $state({ content: ''} );
|
|
||||||
export const messagesList: {
|
|
||||||
element: HTMLElement | null,
|
|
||||||
scrolledToBottom: boolean
|
|
||||||
} = $state({ element: null, scrolledToBottom: true });
|
|
||||||
|
|
||||||
export function scrollMessagesToBottom() {
|
|
||||||
if (messagesList.element === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
messagesList.element.lastElementChild?.scrollIntoView();
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import {writable} from "svelte/store";
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/79696571
|
|
||||||
export const subscribe = <T>(functionToState: () => T, callback: (v: T) => void) => {
|
|
||||||
let value = writable<T>(functionToState());
|
|
||||||
value.subscribe(callback);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
value.set(functionToState());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let { children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
|
||||||
<link rel="stylesheet" href="/static/start.css">
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
{@render children?.()}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const prerender = true;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { page } from '$app/state'
|
|
||||||
import { Alert } from '@sveltestrap/sveltestrap';
|
|
||||||
|
|
||||||
let data: App.Warning = $state({ hasWarning: false, content: '' });
|
|
||||||
$effect.pre(() => {
|
|
||||||
if (page.url.searchParams.has('message')) {
|
|
||||||
data = {
|
|
||||||
hasWarning: true,
|
|
||||||
content: page.url.searchParams.get('message') ?? '',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
data = {
|
|
||||||
hasWarning: false,
|
|
||||||
content: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<main class="auth">
|
|
||||||
<h1>Authcode</h1>
|
|
||||||
{#if data?.hasWarning }
|
|
||||||
<Alert content={data.content} color="warning" dismissible={true}/>
|
|
||||||
{/if}
|
|
||||||
<form action="/auth" method="POST">
|
|
||||||
<label><input type="password" name="authcode"></label>
|
|
||||||
<button type="submit" class="submitButton">Submit</button>
|
|
||||||
</form>
|
|
||||||
<div data-sveltekit-preload-data="false">
|
|
||||||
<img src="/emote/Sure" alt=":Sure:" data-sveltekit-preload-data="off">
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { page } from '$app/state'
|
|
||||||
import { Alert } from "@sveltestrap/sveltestrap";
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { ChatTwoWeb } from '$lib/chat.svelte'
|
|
||||||
import { tabPaneState, persistentTabPabeStateKey } from "$lib/shared.svelte";
|
|
||||||
import { addGfdStylesheet } from "$lib/gfd";
|
|
||||||
import DynamicTextArea from "../../components/DynamicTextArea.svelte";
|
|
||||||
import ChannelSelector from "../../components/ChannelSelector.svelte";
|
|
||||||
import TabPane from "../../components/TabPane.svelte";
|
|
||||||
import TabPaneOpener from "../../components/TabPaneOpener.svelte";
|
|
||||||
|
|
||||||
let data: App.Warning = $state({ hasWarning: false, content: '' });
|
|
||||||
$effect.pre(() => {
|
|
||||||
if (page.url.searchParams.has('message')) {
|
|
||||||
data = {
|
|
||||||
hasWarning: true,
|
|
||||||
content: page.url.searchParams.get('message') ?? '',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
data = {
|
|
||||||
hasWarning: false,
|
|
||||||
content: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
console.log('the component has mounted');
|
|
||||||
|
|
||||||
// Populate the stylesheet with gfd data
|
|
||||||
addGfdStylesheet('/files/gfdata.gfd', '/files/fonticon_ps5.tex');
|
|
||||||
|
|
||||||
// read saved tab pane state from localStorage
|
|
||||||
try {
|
|
||||||
const tabPaneVisible = window.localStorage.getItem(persistentTabPabeStateKey);
|
|
||||||
if (tabPaneVisible !== null) {
|
|
||||||
tabPaneState.visible = JSON.parse(tabPaneVisible);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// JSON.parse() failed, let's reset what's in localStorage
|
|
||||||
window.localStorage.removeItem(persistentTabPabeStateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all web functions in the background
|
|
||||||
const _ = new ChatTwoWeb();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<main class="chat">
|
|
||||||
<TabPane />
|
|
||||||
|
|
||||||
<div class="main-content">
|
|
||||||
<TabPaneOpener />
|
|
||||||
|
|
||||||
<section id="messages">
|
|
||||||
<div class="scroll-container">
|
|
||||||
<ol id="messages-list"></ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="more-messages-indicator">
|
|
||||||
<!-- "arrow-down" icon from https://github.com/feathericons/feather, under MIT license -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if data?.hasWarning }
|
|
||||||
<section id="warnings">
|
|
||||||
<Alert content={data.content} color="warning" dismissible={true}/>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<section id="input">
|
|
||||||
<form>
|
|
||||||
<div class="input-container">
|
|
||||||
<DynamicTextArea />
|
|
||||||
<ChannelSelector />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit">Send</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<div id="timestamp-width-probe"></div>
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
|||||||
# allow crawling everything by default
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
/* fonts */
|
|
||||||
@font-face {
|
|
||||||
font-family: Lodestone;
|
|
||||||
src: url('/files/FFXIV_Lodestone_SSF.ttf') format('truetype');
|
|
||||||
unicode-range: U+E020-E0DB;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Inter var';
|
|
||||||
font-weight: 100 900;
|
|
||||||
font-style: oblique 0deg 10deg;
|
|
||||||
src: url('/static/Inter.var.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* variables */
|
|
||||||
:root {
|
|
||||||
--fg: white;
|
|
||||||
--fg-faint: #a0a0a0;
|
|
||||||
--fg-scrollbar: #404040;
|
|
||||||
--bg: #101010;
|
|
||||||
--bg-sidebar: #080808;
|
|
||||||
--bg-input: #202020;
|
|
||||||
--bg-input-hover: #282828;
|
|
||||||
--focus-color: #4060a0;
|
|
||||||
--unread-color: #beffa0;
|
|
||||||
|
|
||||||
--gradient-clickable: linear-gradient(to bottom, #404040, var(--bg-input) 65%, var(--bg-input));
|
|
||||||
--gradient-clickable-hover: linear-gradient(to bottom, #505050, var(--bg-input-hover) 65%, var(--bg-input-hover));
|
|
||||||
|
|
||||||
--timestamp-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* reset */
|
|
||||||
*, *::before, *::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
color: var(--fg);
|
|
||||||
font-family: Lodestone, 'Inter var', sans-serif;
|
|
||||||
font-feature-settings: 'tnum', 'calt' 0; /* calt appears to be on by default */
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span > a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* layout and global styles */
|
|
||||||
body {
|
|
||||||
padding: 25px;
|
|
||||||
height: 100dvh;
|
|
||||||
background-color: var(--bg-input-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
main.chat {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: var(--bg);
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.5);
|
|
||||||
|
|
||||||
& > .main-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main.auth {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
input { width: 150px; }
|
|
||||||
|
|
||||||
input, .submitButton {
|
|
||||||
padding: 5px 20px;
|
|
||||||
font-size: 1rem;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: var(--bg-input);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--focus-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submitButton {
|
|
||||||
padding: 5px 15px;
|
|
||||||
border: 3px solid var(--bg-input);
|
|
||||||
background-image: var(--gradient-clickable);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--bg-input-hover);
|
|
||||||
background-color: var(--bg-input-hover);
|
|
||||||
background-image: var(--gradient-clickable-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tab list */
|
|
||||||
aside#tabs {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
scrollbar-color: var(--fg-scrollbar) var(--bg-sidebar);
|
|
||||||
background-color: var(--bg-sidebar);
|
|
||||||
transition: width 250ms ease;
|
|
||||||
|
|
||||||
width: 200px;
|
|
||||||
&.hidden { width: 0px; }
|
|
||||||
|
|
||||||
&.no-animation {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.inner {
|
|
||||||
width: 200px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 550;
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
margin: 0.6rem 0 0.75rem;
|
|
||||||
border-color: var(--fg-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
ol#tabs-list {
|
|
||||||
margin: 0 -5px;
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 3px 5px;
|
|
||||||
color: var(--fg-faint);
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
color: inherit;
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
li + li {
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:has(button:hover) {
|
|
||||||
color: var(--fg);
|
|
||||||
background-color: rgb(from var(--bg-input) r g b / 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
li.active {
|
|
||||||
color: var(--fg);
|
|
||||||
background-color: var(--bg-input);
|
|
||||||
}
|
|
||||||
|
|
||||||
li.unread button {
|
|
||||||
color: var(--unread-color);
|
|
||||||
text-shadow: 0 0 5px var(--unread-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* message list */
|
|
||||||
section#messages {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 20px;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
.scroll-container {
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
scrollbar-color: var(--fg-scrollbar) var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
ol#messages-list {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
flex: 0 0 var(--timestamp-width);
|
|
||||||
color: var(--fg-faint);
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#more-messages-indicator {
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
right: 30px;
|
|
||||||
bottom: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
filter: drop-shadow(0 0 5px #60a0ff) drop-shadow(0 0 15px #60a0ff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.more-messages #more-messages-indicator {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#timestamp-width-probe {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* alerts */
|
|
||||||
section#warnings {
|
|
||||||
flex-grow: 0;
|
|
||||||
padding: 20px 20px 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* input bar, channel selector, ... */
|
|
||||||
section#input {
|
|
||||||
flex-grow: 0;
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, button {
|
|
||||||
font-size: 1rem;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: var(--bg-input);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--focus-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 5px 15px;
|
|
||||||
border: 3px solid var(--bg-input);
|
|
||||||
background-image: var(--gradient-clickable);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--bg-input-hover);
|
|
||||||
background-color: var(--bg-input-hover);
|
|
||||||
background-image: var(--gradient-clickable-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding-left: calc(20px + 1.5rem);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
/* "send" icon from https://github.com/feathericons/feather, under MIT license */
|
|
||||||
mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 15px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 1.3rem;
|
|
||||||
height: 1.3rem;
|
|
||||||
background-color: var(--fg);
|
|
||||||
mask-size: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
#chat-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 5px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#channel-select {
|
|
||||||
position: absolute;
|
|
||||||
top: -1.5em;
|
|
||||||
left: 23px;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 550;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
padding: 5px 15px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--bg-input-hover);
|
|
||||||
background-color: var(--bg-input-hover);
|
|
||||||
background-image: var(--gradient-clickable-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--focus-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* icons, emotes */
|
|
||||||
.gfd-icon {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: middle;
|
|
||||||
zoom: 0.75;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emote-icon {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 2rem;
|
|
||||||
height: 1px;
|
|
||||||
vertical-align: middle;
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** mobile ***/
|
|
||||||
@media ((max-width: 600px) and (orientation: portrait)) or (max-height: 400px) {
|
|
||||||
body {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
main.chat {
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#messages {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
align-items: baseline;
|
|
||||||
|
|
||||||
.timestamp {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#timestamp-width-probe {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
section#input {
|
|
||||||
button {
|
|
||||||
max-width: 0;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
button::before {
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%) translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container #channel-select {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gfd-icon { zoom: 0.65; }
|
|
||||||
.emote-icon {
|
|
||||||
width: 1.5rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aside#tabs {
|
|
||||||
position: fixed;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
div.inner {
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import adapter from '@sveltejs/adapter-static';
|
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
|
||||||
const config = {
|
|
||||||
// Consult https://svelte.dev/docs/kit/integrations
|
|
||||||
// for more information about preprocessors
|
|
||||||
preprocess: vitePreprocess(),
|
|
||||||
kit: {
|
|
||||||
prerender: {
|
|
||||||
handleHttpError: 'warn'
|
|
||||||
},
|
|
||||||
adapter: adapter({
|
|
||||||
// default options are shown. On some platforms
|
|
||||||
// these options are set automatically — see below
|
|
||||||
pages: 'build',
|
|
||||||
assets: 'build',
|
|
||||||
fallback: undefined,
|
|
||||||
precompress: false,
|
|
||||||
strict: true,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"moduleResolution": "bundler"
|
|
||||||
}
|
|
||||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
|
||||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
|
||||||
//
|
|
||||||
// To make changes to top-level options such as include and exclude, we recommend extending
|
|
||||||
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [sveltekit()]
|
|
||||||
});
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
using WatsonWebserver.Core;
|
|
||||||
using WatsonWebserver.Lite;
|
|
||||||
|
|
||||||
namespace ChatTwo.Http;
|
|
||||||
|
|
||||||
public class HostContext
|
|
||||||
{
|
|
||||||
public readonly ServerCore Core;
|
|
||||||
|
|
||||||
public bool IsActive;
|
|
||||||
public bool IsStopping;
|
|
||||||
|
|
||||||
// Initialized at webserver start
|
|
||||||
public WebserverLite Host = null!;
|
|
||||||
public Processing Processing = null!;
|
|
||||||
public RouteController RouteController = null!;
|
|
||||||
|
|
||||||
public readonly List<SSEConnection> EventConnections = [];
|
|
||||||
|
|
||||||
public readonly CancellationTokenSource TokenSource = new();
|
|
||||||
public readonly string StaticDir = Path.Combine(Plugin.Interface.AssemblyLocation.DirectoryName!, "Frontend/");
|
|
||||||
|
|
||||||
public HostContext(ServerCore core)
|
|
||||||
{
|
|
||||||
Core = core;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Start()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Host = new WebserverLite(new WebserverSettings("*", Plugin.Config.WebinterfacePort), DefaultRoute);
|
|
||||||
|
|
||||||
Processing = new Processing(this);
|
|
||||||
RouteController = new RouteController(this);
|
|
||||||
|
|
||||||
Host.Routes.PreAuthentication.Content.BaseDirectory = StaticDir;
|
|
||||||
Host.Routes.AuthenticateRequest = CheckAuthenticationCookie;
|
|
||||||
Host.Events.ExceptionEncountered += ExceptionEncountered;
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
#if DEBUG
|
|
||||||
Host.Settings.Debug.Requests = true;
|
|
||||||
Host.Settings.Debug.Routing = true;
|
|
||||||
Host.Settings.Debug.Responses = true;
|
|
||||||
Host.Settings.Debug.AccessControl = true;
|
|
||||||
#endif
|
|
||||||
Host.Events.Logger = logMessage => Plugin.Log.Debug(logMessage);
|
|
||||||
|
|
||||||
IsActive = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
IsActive = false;
|
|
||||||
Plugin.Log.Error(ex, "Initialization of the webserver failed.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Host.Start(TokenSource.Token);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Webserver failed to boot up.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<bool> Stop()
|
|
||||||
{
|
|
||||||
// Is already stopped
|
|
||||||
if (!IsActive)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IsActive = false;
|
|
||||||
IsStopping = true;
|
|
||||||
Host.Stop();
|
|
||||||
|
|
||||||
// Save our session tokens
|
|
||||||
Core.Plugin.SaveConfig();
|
|
||||||
|
|
||||||
// We get a copy, so that the original can be cleaned up successfully
|
|
||||||
foreach (var eventServer in EventConnections.ToArray())
|
|
||||||
await eventServer.DisposeAsync();
|
|
||||||
|
|
||||||
EventConnections.Clear();
|
|
||||||
Host.Dispose();
|
|
||||||
RouteController.Dispose();
|
|
||||||
IsStopping = false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Webserver failed to stop and dispose all resources.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
await Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region GeneralHandlers
|
|
||||||
private static void ExceptionEncountered(object? _, ExceptionEventArgs args)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(args.Exception, "Webserver threw an exception.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> DefaultRoute(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
return await ctx.Response.Send("Nothing to see here.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CheckAuthenticationCookie(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
if (Plugin.Config.AuthStore.Count == 0)
|
|
||||||
{
|
|
||||||
await RouteController.Redirect(ctx, "/", ("message", "Invalid session token."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cookies = WebserverUtil.GetCookieData(ctx.Request.Headers.Get("Cookie") ?? "");
|
|
||||||
if (!cookies.TryGetValue("ChatTwo-token", out var token) || !Plugin.Config.AuthStore.Contains(token))
|
|
||||||
await RouteController.Redirect(ctx, "/", ("message", "Invalid session token."));
|
|
||||||
|
|
||||||
// Do nothing to let auth pass
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
using ChatTwo.Code;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace ChatTwo.Http.MessageProtocol;
|
|
||||||
|
|
||||||
#region Outgoing SSE
|
|
||||||
/// <summary>
|
|
||||||
/// Contains a valid tab with its assigned index
|
|
||||||
/// </summary>
|
|
||||||
public struct ChatTab(string name, int index, uint unreadCount)
|
|
||||||
{
|
|
||||||
[JsonProperty("name")] public string Name = name;
|
|
||||||
[JsonProperty("index")] public int Index = index;
|
|
||||||
[JsonProperty("unreadCount")] public uint UnreadCount = unreadCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains a number of tabs that are valid for the user to pick from
|
|
||||||
/// </summary>
|
|
||||||
public struct ChatTabList(ChatTab[] tabs)
|
|
||||||
{
|
|
||||||
[JsonProperty("tabs")] public ChatTab[] Tabs = tabs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains a valid tab index and the current unread state as a number unread of messages
|
|
||||||
/// </summary>
|
|
||||||
public struct ChatTabUnreadState(int index, uint unreadCount)
|
|
||||||
{
|
|
||||||
[JsonProperty("index")] public int Index = index;
|
|
||||||
[JsonProperty("unreadCount")] public uint UnreadCount = unreadCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains the current channel name
|
|
||||||
/// </summary>
|
|
||||||
public struct SwitchChannel((MessageTemplate[] Name, bool Locked) channel)
|
|
||||||
{
|
|
||||||
[JsonProperty("channelName")] public MessageTemplate[] ChannelName = channel.Name;
|
|
||||||
[JsonProperty("channelLocked")] public bool Locked = channel.Locked;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains a number of channels that are valid for the user to pick from
|
|
||||||
/// </summary>
|
|
||||||
public struct ChannelList(Dictionary<string, uint> channels)
|
|
||||||
{
|
|
||||||
[JsonProperty("channels")] public Dictionary<string, uint> Channels = channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains one or multiple messages
|
|
||||||
/// </summary>
|
|
||||||
public struct Messages(MessageResponse[] set)
|
|
||||||
{
|
|
||||||
[JsonProperty("messages")] public MessageResponse[] Set = set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains a single message with all its templates and a timestamp
|
|
||||||
/// </summary>
|
|
||||||
public struct MessageResponse()
|
|
||||||
{
|
|
||||||
[JsonProperty("id")] public Guid Id = Guid.Empty;
|
|
||||||
[JsonProperty("timestamp")] public string Timestamp = "";
|
|
||||||
[JsonProperty("templates")] public MessageTemplate[] Templates = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Template that is used for the channel name or any message posted to the chatlog
|
|
||||||
/// </summary>
|
|
||||||
public struct MessageTemplate()
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The type of payload.
|
|
||||||
/// Dalamuds enum is just a baseline, there exists more that are expressed through raw values.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("payloadType")] public WebPayloadType PayloadType = WebPayloadType.Unknown;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used for text and emote.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("content")] public string Content = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used for an icon.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("iconId")] public uint IconId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used for text and url
|
|
||||||
///
|
|
||||||
/// Note:
|
|
||||||
/// 0 is used for invalid colors
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("color")] public uint Color;
|
|
||||||
|
|
||||||
public static MessageTemplate Empty => new();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Outgoing POST
|
|
||||||
public struct OkResponse(string message)
|
|
||||||
{
|
|
||||||
[JsonProperty("message")] public string Message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct ErrorResponse(string reason)
|
|
||||||
{
|
|
||||||
[JsonProperty("reason")] public string Reason = reason;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Incoming POST
|
|
||||||
/// <summary>
|
|
||||||
/// Message must fulfill the posting requirement
|
|
||||||
/// Greater than or equal 2 characters
|
|
||||||
/// Less than or equal 500 characters
|
|
||||||
/// </summary>
|
|
||||||
public struct IncomingMessage()
|
|
||||||
{
|
|
||||||
[JsonProperty("message")] public string Message = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The channel type must be a valid <see cref="InputChannel"/>
|
|
||||||
/// </summary>
|
|
||||||
public struct IncomingChannel()
|
|
||||||
{
|
|
||||||
[JsonProperty("channel")] public InputChannel Channel = InputChannel.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The tabs index must be a valid int
|
|
||||||
/// </summary>
|
|
||||||
public struct IncomingTab()
|
|
||||||
{
|
|
||||||
[JsonProperty("index")] public int Index = -1;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace ChatTwo.Http.MessageProtocol;
|
|
||||||
|
|
||||||
// General
|
|
||||||
public class CloseEvent() : BaseEvent("close");
|
|
||||||
|
|
||||||
// Tab related
|
|
||||||
public class ChatTabListEvent(ChatTabList list) : BaseEvent("tab-list", JsonConvert.SerializeObject(list));
|
|
||||||
public class ChatTabSwitchedEvent(ChatTab chatTab) : BaseEvent("tab-switched", JsonConvert.SerializeObject(chatTab));
|
|
||||||
public class ChatTabUnreadStateEvent(ChatTabUnreadState unreadState) : BaseEvent("tab-unread-state", JsonConvert.SerializeObject(unreadState));
|
|
||||||
|
|
||||||
// Input channel related
|
|
||||||
public class ChannelListEvent(ChannelList channelList) : BaseEvent("channel-list", JsonConvert.SerializeObject(channelList));
|
|
||||||
public class SwitchChannelEvent(SwitchChannel switchChannel) : BaseEvent("channel-switched", JsonConvert.SerializeObject(switchChannel));
|
|
||||||
|
|
||||||
// Chat message related
|
|
||||||
public class BulkMessagesEvent(Messages messages) : BaseEvent("bulk-messages", JsonConvert.SerializeObject(messages));
|
|
||||||
public class NewMessageEvent(MessageResponse message) : BaseEvent("new-message", JsonConvert.SerializeObject(message));
|
|
||||||
|
|
||||||
public class BaseEvent(string eventType, string? data = null)
|
|
||||||
{
|
|
||||||
private string Event = eventType;
|
|
||||||
private string Data = data ?? "0"; // SSE requires data on each response
|
|
||||||
|
|
||||||
public byte[] Build()
|
|
||||||
{
|
|
||||||
// SSE always ends with \n\n
|
|
||||||
return Encoding.UTF8.GetBytes($"event: {Event}\ndata: {Data}\n\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
namespace ChatTwo.Http.MessageProtocol;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Baseline: <see cref="Dalamud.Game.Text.SeStringHandling.PayloadType"/>
|
|
||||||
/// </summary>
|
|
||||||
public enum WebPayloadType
|
|
||||||
{
|
|
||||||
// Dalamud
|
|
||||||
Unknown,
|
|
||||||
Player,
|
|
||||||
Item,
|
|
||||||
Status,
|
|
||||||
RawText,
|
|
||||||
UIForeground,
|
|
||||||
UIGlow,
|
|
||||||
MapLink,
|
|
||||||
AutoTranslateText,
|
|
||||||
EmphasisItalic,
|
|
||||||
Icon,
|
|
||||||
Quest,
|
|
||||||
DalamudLink,
|
|
||||||
NewLine,
|
|
||||||
SeHyphen,
|
|
||||||
PartyFinder,
|
|
||||||
|
|
||||||
// Custom
|
|
||||||
CustomPartyFinder = 0x50,
|
|
||||||
CustomAchievement = 0x51,
|
|
||||||
CustomUri = 0x52,
|
|
||||||
CustomEmote = 0x53,
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
using ChatTwo.Code;
|
|
||||||
using ChatTwo.Http.MessageProtocol;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
||||||
|
|
||||||
namespace ChatTwo.Http;
|
|
||||||
|
|
||||||
public class Processing
|
|
||||||
{
|
|
||||||
private readonly HostContext HostContext;
|
|
||||||
|
|
||||||
public Processing(HostContext hostContext)
|
|
||||||
{
|
|
||||||
HostContext = hostContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal (MessageTemplate[] Name, bool Locked) ReadChannelName(Chunk[] channelName)
|
|
||||||
{
|
|
||||||
var locked = HostContext.Core.Plugin.CurrentTab is not { Channel: null };
|
|
||||||
return (channelName.Select(ProcessChunk).ToArray(), locked);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<MessageResponse[]> ReadMessageList()
|
|
||||||
{
|
|
||||||
var tabMessages = await HostContext.Core.Plugin.CurrentTab.Messages.GetCopy();
|
|
||||||
return tabMessages.TakeLast(Plugin.Config.WebinterfaceMaxLinesToSend).Select(ReadMessageContent).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal MessageResponse ReadMessageContent(Message message)
|
|
||||||
{
|
|
||||||
var response = new MessageResponse
|
|
||||||
{
|
|
||||||
Id = message.Id,
|
|
||||||
Timestamp = message.Date.ToLocalTime().ToString("t", !Plugin.Config.Use24HourClock ? null : CultureInfo.CreateSpecificCulture("es-ES"))
|
|
||||||
};
|
|
||||||
|
|
||||||
var sender = message.Sender.Select(ProcessChunk);
|
|
||||||
var content = message.Content.Select(ProcessChunk);
|
|
||||||
response.Templates = sender.Concat(content).ToArray();
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MessageTemplate ProcessChunk(Chunk chunk)
|
|
||||||
{
|
|
||||||
if (chunk is IconChunk { } icon)
|
|
||||||
{
|
|
||||||
var iconId = (uint)icon.Icon;
|
|
||||||
return IconUtil.GfdFileView.TryGetEntry(iconId, out _) ? new MessageTemplate {PayloadType = WebPayloadType.Icon, IconId = iconId}: MessageTemplate.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk is TextChunk { } text)
|
|
||||||
{
|
|
||||||
if (chunk.Link is EmotePayload emotePayload && Plugin.Config.ShowEmotes)
|
|
||||||
{
|
|
||||||
var image = EmoteCache.GetEmote(emotePayload.Code);
|
|
||||||
|
|
||||||
if (image is { Failed: false })
|
|
||||||
return new MessageTemplate { PayloadType = WebPayloadType.CustomEmote, Color = 0, Content = emotePayload.Code };
|
|
||||||
}
|
|
||||||
|
|
||||||
var color = text.Foreground;
|
|
||||||
if (color == null && text.FallbackColour != null)
|
|
||||||
{
|
|
||||||
var type = text.FallbackColour.Value;
|
|
||||||
color = Plugin.Config.ChatColours.TryGetValue(type, out var col) ? col : type.DefaultColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
color ??= 0;
|
|
||||||
|
|
||||||
var userContent = text.Content;
|
|
||||||
if (HostContext.Core.Plugin.ChatLogWindow.ScreenshotMode)
|
|
||||||
{
|
|
||||||
if (chunk.Link is PlayerPayload playerPayload)
|
|
||||||
userContent = HostContext.Core.Plugin.ChatLogWindow.HidePlayerInString(userContent, playerPayload.PlayerName, playerPayload.World.RowId);
|
|
||||||
else if (Plugin.PlayerState.IsLoaded)
|
|
||||||
userContent = HostContext.Core.Plugin.ChatLogWindow.HidePlayerInString(userContent, Plugin.PlayerState.CharacterName, Plugin.PlayerState.HomeWorld.RowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var isNotUrl = text.Link is not UriPayload;
|
|
||||||
return new MessageTemplate { PayloadType = isNotUrl ? WebPayloadType.RawText : WebPayloadType.CustomUri, Color = color.Value, Content = userContent };
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageTemplate.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Messages> GetAllMessages()
|
|
||||||
{
|
|
||||||
var messages = await WebserverUtil.FrameworkWrapper(ReadMessageList);
|
|
||||||
return new Messages(messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SwitchChannel GetCurrentChannel()
|
|
||||||
{
|
|
||||||
var channel = ReadChannelName(HostContext.Core.Plugin.ChatLogWindow.PreviousChannel);
|
|
||||||
return new SwitchChannel(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelList GetValidChannels()
|
|
||||||
{
|
|
||||||
var channels = HostContext.Core.Plugin.ChatLogWindow.GetValidChannels();
|
|
||||||
return new ChannelList(channels.ToDictionary(pair => pair.Key, pair => (uint)pair.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatTab GetCurrentTab()
|
|
||||||
{
|
|
||||||
var currentTab = HostContext.Core.Plugin.CurrentTab;
|
|
||||||
return new ChatTab(currentTab.Name, HostContext.Core.Plugin.LastTab, currentTab.Unread);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatTabList GetAllTabs()
|
|
||||||
{
|
|
||||||
var tabs = Plugin.Config.Tabs.Select((tab, idx) => new ChatTab(tab.Name, idx, tab.Unread)).ToArray();
|
|
||||||
return new ChatTabList(tabs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Web;
|
|
||||||
using ChatTwo.Http.MessageProtocol;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Lumina.Data.Files;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using WatsonWebserver.Core;
|
|
||||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
|
||||||
using HttpMethod = WatsonWebserver.Core.HttpMethod;
|
|
||||||
|
|
||||||
namespace ChatTwo.Http;
|
|
||||||
|
|
||||||
public class RouteController
|
|
||||||
{
|
|
||||||
private readonly HostContext HostContext;
|
|
||||||
|
|
||||||
private readonly string AuthTemplate;
|
|
||||||
private readonly string ChatBoxTemplate;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, long> RateLimit = [];
|
|
||||||
|
|
||||||
private readonly JsonSerializerSettings JsonSettings = new()
|
|
||||||
{
|
|
||||||
Error = delegate(object? _, ErrorEventArgs args) { args.ErrorContext.Handled = true; }
|
|
||||||
};
|
|
||||||
|
|
||||||
public RouteController(HostContext hostContext)
|
|
||||||
{
|
|
||||||
HostContext = hostContext;
|
|
||||||
|
|
||||||
AuthTemplate = File.ReadAllText(Path.Combine(HostContext.StaticDir, "index.html"));
|
|
||||||
ChatBoxTemplate = File.ReadAllText(Path.Combine(HostContext.StaticDir, "chat.html"));
|
|
||||||
|
|
||||||
// Pre Auth
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/", AuthRoute, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.POST, "/auth", AuthenticateClient, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/gfdata.gfd", GetGfdData, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/fonticon_ps5.tex", GetTexData, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/files/FFXIV_Lodestone_SSF.ttf", GetLodestoneFont, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Static.Add(HttpMethod.GET, "/favicon.ico", GetFavicon, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Parameter.Add(HttpMethod.GET, "/emote/{name}", GetEmote, ExceptionRoute);
|
|
||||||
|
|
||||||
// Post Auth
|
|
||||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.GET, "/chat", ChatBoxRoute, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/send", ReceiveMessage, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/channel", ReceiveChannelSwitch, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/tab", ReceiveTabSwitch, ExceptionRoute);
|
|
||||||
|
|
||||||
// Ship all other static files dynamically
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Content.Add("/_app/", true, ExceptionRoute);
|
|
||||||
HostContext.Host.Routes.PreAuthentication.Content.Add("/static/", true, ExceptionRoute);
|
|
||||||
|
|
||||||
// Server-Sent Events Route
|
|
||||||
HostContext.Host.Routes.PostAuthentication.Static.Add(HttpMethod.POST, "/sse", NewSSEConnection, ExceptionRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExceptionRoute(HttpContextBase ctx, Exception _)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 500;
|
|
||||||
await ctx.Response.Send("Internal Server Error, please try again");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AuthRoute(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
if (Plugin.Config.AuthStore.Count > 0)
|
|
||||||
{
|
|
||||||
var cookies = WebserverUtil.GetCookieData(ctx.Request.Headers.Get("Cookie") ?? "");
|
|
||||||
if (cookies.TryGetValue("ChatTwo-token", out var value) && Plugin.Config.AuthStore.Contains(value))
|
|
||||||
{
|
|
||||||
await Redirect(ctx, "/chat");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.Response.Send(AuthTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#region FileHandlerRoutes
|
|
||||||
private async Task GetTexData(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
var data = Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!.Data;
|
|
||||||
await ctx.Response.Send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task GetGfdData(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
var data = Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data;
|
|
||||||
await ctx.Response.Send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task GetLodestoneFont(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
var data = HostContext.Core.Plugin.FontManager.GameSymFont;
|
|
||||||
await ctx.Response.Send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task GetFavicon(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 404;
|
|
||||||
await ctx.Response.Send();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task GetEmote(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
var name = ctx.Request.Url.Parameters["name"] ?? "";
|
|
||||||
if (name == "" || !EmoteCache.Exists(name))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.Send("Malformed emote name.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var emote = EmoteCache.GetEmote(name);
|
|
||||||
if (emote is null)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.Send("Emote not valid.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the emote to be loaded a maximum of 5 times
|
|
||||||
var timeout = 5;
|
|
||||||
while (!emote.IsLoaded && timeout > 0)
|
|
||||||
{
|
|
||||||
timeout--;
|
|
||||||
await Task.Delay(25);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Response.Headers.Add("Cache-Control", "max-age=86400");
|
|
||||||
await ctx.Response.Send(emote.RawData);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region PreAuthRoutes
|
|
||||||
private async Task<bool> AuthenticateClient(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
var currentTick = Environment.TickCount64;
|
|
||||||
if (RateLimit.TryGetValue(ctx.Request.Source.IpAddress, out var timestamp) && timestamp > currentTick)
|
|
||||||
{
|
|
||||||
_ = ctx.Request.DataAsString; // Temp fix for Watson.Lite bug #155
|
|
||||||
return await Redirect(ctx, "/", ("message", "Rate limit active (10s)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The next request will be rate limited for 10s
|
|
||||||
RateLimit[ctx.Request.Source.IpAddress] = currentTick + 10_000;
|
|
||||||
|
|
||||||
var authcode = HttpUtility.ParseQueryString(ctx.Request.DataAsString ?? "").Get("authcode");
|
|
||||||
if (authcode == null || authcode != Plugin.Config.WebinterfacePassword)
|
|
||||||
return await Redirect(ctx, "/", ("message", "Authentication failed"));
|
|
||||||
|
|
||||||
var token = WebinterfaceUtil.GenerateSimpleToken();
|
|
||||||
Plugin.Config.AuthStore.Add(token);
|
|
||||||
|
|
||||||
ctx.Response.Headers.Add("Set-Cookie", $"ChatTwo-token={token}");
|
|
||||||
return await Redirect(ctx, "/chat");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region PostAuthRoutes
|
|
||||||
private async Task ChatBoxRoute(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
await ctx.Response.Send(ChatBoxTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReceiveMessage(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
if (!await EnforceMediaType(ctx, "application/json"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var content = JsonConvert.DeserializeObject<IncomingMessage>(ctx.Request.DataAsString, JsonSettings);
|
|
||||||
if (content.Message.Length is < 2 or > 500)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Invalid message received.")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Plugin.Framework.RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
HostContext.Core.Plugin.ChatLogWindow.Chat = content.Message;
|
|
||||||
HostContext.Core.Plugin.ChatLogWindow.SendChatBox(HostContext.Core.Plugin.CurrentTab);
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.Response.StatusCode = 201;
|
|
||||||
await ctx.Response.Send(JsonConvert.SerializeObject(new OkResponse("Message was send to the channel.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReceiveChannelSwitch(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
if (!await EnforceMediaType(ctx, "application/json"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var channel = JsonConvert.DeserializeObject<IncomingChannel>(ctx.Request.DataAsString, JsonSettings);
|
|
||||||
if (!Enum.IsDefined(channel.Channel))
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Invalid channel received.")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Plugin.Framework.RunOnFrameworkThread(() => { HostContext.Core.Plugin.ChatLogWindow.SetChannel(channel.Channel); });
|
|
||||||
|
|
||||||
ctx.Response.StatusCode = 201;
|
|
||||||
await ctx.Response.Send(JsonConvert.SerializeObject(new OkResponse("Channel switch was initiated.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReceiveTabSwitch(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
if (!await EnforceMediaType(ctx, "application/json"))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var tab = JsonConvert.DeserializeObject<IncomingTab>(ctx.Request.DataAsString, JsonSettings);
|
|
||||||
if (tab.Index < 0 || tab.Index >= Plugin.Config.Tabs.Count)
|
|
||||||
{
|
|
||||||
ctx.Response.StatusCode = 400;
|
|
||||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Invalid tab received.")));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Plugin.Framework.RunOnFrameworkThread(() => { HostContext.Core.Plugin.WantedTab = tab.Index; });
|
|
||||||
|
|
||||||
ctx.Response.StatusCode = 201;
|
|
||||||
await ctx.Response.Send(JsonConvert.SerializeObject(new OkResponse("Tab switch was initiated.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task NewSSEConnection(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Plugin.Log.Debug($"Client connected: {ctx.Guid}");
|
|
||||||
|
|
||||||
var sse = new SSEConnection(HostContext.TokenSource.Token);
|
|
||||||
await HostContext.Core.PrepareNewClient(sse);
|
|
||||||
HostContext.EventConnections.Add(sse);
|
|
||||||
|
|
||||||
await sse.HandleEventLoop(ctx);
|
|
||||||
|
|
||||||
// It should always be done after return
|
|
||||||
if (sse.Done)
|
|
||||||
HostContext.EventConnections.Remove(sse);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Failed to finish the server event function");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region RedirectHelper
|
|
||||||
public static async Task<bool> Redirect(HttpContextBase ctx, string location, params (string, string)[] parameter)
|
|
||||||
{
|
|
||||||
var query = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var (key, value) in parameter)
|
|
||||||
query.Add(key, value);
|
|
||||||
|
|
||||||
ctx.Response.Headers.Add("Location", $"{location}?{query}");
|
|
||||||
ctx.Response.StatusCode = 303;
|
|
||||||
return await ctx.Response.Send();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region PreChecks
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check that the request has the correct media type that the functions expects.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ctx"></param>
|
|
||||||
/// <param name="requiredMediaType"></param>
|
|
||||||
/// <returns>True if media type is correct, otherwise handled and false</returns>
|
|
||||||
private async Task<bool> EnforceMediaType(HttpContextBase ctx, string requiredMediaType)
|
|
||||||
{
|
|
||||||
if (ctx.Request.ContentType == requiredMediaType)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
ctx.Response.StatusCode = 415;
|
|
||||||
await ctx.Response.Send(JsonConvert.SerializeObject(new ErrorResponse("Request contains wrong media type.")));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using ChatTwo.Http.MessageProtocol;
|
|
||||||
using WatsonWebserver.Core;
|
|
||||||
|
|
||||||
namespace ChatTwo.Http;
|
|
||||||
|
|
||||||
public class SSEConnection
|
|
||||||
{
|
|
||||||
private bool Stopping;
|
|
||||||
private readonly CancellationToken Token;
|
|
||||||
|
|
||||||
public bool Done;
|
|
||||||
public readonly ConcurrentQueue<BaseEvent> OutboundQueue = new();
|
|
||||||
|
|
||||||
public SSEConnection(CancellationToken token)
|
|
||||||
{
|
|
||||||
Token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task HandleEventLoop(HttpContextBase ctx)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ctx.Response.Headers.Add("Content-Type", "text/event-stream");
|
|
||||||
ctx.Response.Headers.Add("Cache-Control", "no-cache");
|
|
||||||
ctx.Response.Headers.Add("Connection", "keep-alive");
|
|
||||||
|
|
||||||
ctx.Response.ChunkedTransfer = true;
|
|
||||||
while (!Token.IsCancellationRequested && !Stopping)
|
|
||||||
{
|
|
||||||
await Task.Delay(10, Token);
|
|
||||||
if (Token.IsCancellationRequested)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!OutboundQueue.TryDequeue(out var outgoingEvent))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!await ctx.Response.SendChunk(outgoingEvent.Build(), false, Token))
|
|
||||||
{
|
|
||||||
Plugin.Log.Debug("SSE connection was unable to send new data");
|
|
||||||
Plugin.Log.Debug($"Client disconnected: {ctx.Guid}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "SSE handler failed.");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// "No Content" (204) didn't work for Firefox, so manually closing the connection on client side
|
|
||||||
await ctx.Response.SendChunk(new CloseEvent().Build(), true, Token);
|
|
||||||
|
|
||||||
// Manually confirm that we have finished our connection, even if the final response failed
|
|
||||||
// This can happen if the client disconnects before the server does
|
|
||||||
ctx.Response.ResponseSent = true;
|
|
||||||
|
|
||||||
Done = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
Stopping = true;
|
|
||||||
|
|
||||||
var timeout = 1000; // 1000ms
|
|
||||||
while (timeout > 0)
|
|
||||||
{
|
|
||||||
if (Done)
|
|
||||||
break;
|
|
||||||
|
|
||||||
timeout -= 100;
|
|
||||||
await Task.Delay(100);
|
|
||||||
Plugin.Log.Debug("Sleeping because EventServer still alive");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
using ChatTwo.Http.MessageProtocol;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
|
|
||||||
namespace ChatTwo.Http;
|
|
||||||
|
|
||||||
public class ServerCore : IAsyncDisposable
|
|
||||||
{
|
|
||||||
public readonly Plugin Plugin;
|
|
||||||
private readonly HostContext HostContext;
|
|
||||||
|
|
||||||
public ServerCore(Plugin plugin)
|
|
||||||
{
|
|
||||||
Plugin = plugin;
|
|
||||||
HostContext = new HostContext(this);
|
|
||||||
|
|
||||||
Plugin.Framework.Update += FrameworkUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
Plugin.Framework.Update -= FrameworkUpdate;
|
|
||||||
await HostContext.DisposeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FrameworkUpdate(IFramework _)
|
|
||||||
{
|
|
||||||
foreach (var (idx, tab) in Plugin.Config.Tabs.Index())
|
|
||||||
{
|
|
||||||
if (tab.Unread == tab.LastSendUnread)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
tab.LastSendUnread = tab.Unread;
|
|
||||||
foreach (var eventServer in HostContext.EventConnections)
|
|
||||||
eventServer.OutboundQueue.Enqueue(new ChatTabUnreadStateEvent(new ChatTabUnreadState(idx, tab.Unread)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region SSE Helper
|
|
||||||
internal async Task PrepareNewClient(SSEConnection sse)
|
|
||||||
{
|
|
||||||
// This takes long, so keep it outside the next frame
|
|
||||||
var messages = await HostContext.Processing.GetAllMessages();
|
|
||||||
|
|
||||||
// Using the bulk message event to clear everything on the client side that may still exist
|
|
||||||
await Plugin.Framework.RunOnTick(() =>
|
|
||||||
{
|
|
||||||
sse.OutboundQueue.Enqueue(new BulkMessagesEvent(messages));
|
|
||||||
|
|
||||||
sse.OutboundQueue.Enqueue(new SwitchChannelEvent(HostContext.Processing.GetCurrentChannel()));
|
|
||||||
sse.OutboundQueue.Enqueue(new ChannelListEvent(HostContext.Processing.GetValidChannels()));
|
|
||||||
|
|
||||||
sse.OutboundQueue.Enqueue(new ChatTabSwitchedEvent(HostContext.Processing.GetCurrentTab()));
|
|
||||||
sse.OutboundQueue.Enqueue(new ChatTabListEvent(HostContext.Processing.GetAllTabs()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void SendNewMessage(Message message)
|
|
||||||
{
|
|
||||||
if (!HostContext.IsActive)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Plugin.Framework.RunOnTick(() =>
|
|
||||||
{
|
|
||||||
var bundledResponse = new NewMessageEvent(HostContext.Processing.ReadMessageContent(message));
|
|
||||||
foreach (var eventServer in HostContext.EventConnections)
|
|
||||||
eventServer.OutboundQueue.Enqueue(bundledResponse);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Sending message over SSE failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void SendBulkMessageList()
|
|
||||||
{
|
|
||||||
if (!HostContext.IsActive)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Plugin.Framework.RunOnTick(() =>
|
|
||||||
{
|
|
||||||
foreach (var eventServer in HostContext.EventConnections)
|
|
||||||
eventServer.OutboundQueue.Enqueue(new BulkMessagesEvent(new Messages(HostContext.Processing.ReadMessageList().Result)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Sending channel switch over SSE failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void SendChannelSwitch(Chunk[] channelName)
|
|
||||||
{
|
|
||||||
if (!HostContext.IsActive)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Plugin.Framework.RunOnTick(() =>
|
|
||||||
{
|
|
||||||
var bundledResponse = new SwitchChannelEvent(new SwitchChannel(HostContext.Processing.ReadChannelName(channelName)));
|
|
||||||
foreach (var eventServer in HostContext.EventConnections)
|
|
||||||
eventServer.OutboundQueue.Enqueue(bundledResponse);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Sending channel switch over SSE failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void SendChannelList()
|
|
||||||
{
|
|
||||||
if (!HostContext.IsActive)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Plugin.Framework.RunOnTick(() =>
|
|
||||||
{
|
|
||||||
var bundledResponse = new ChannelListEvent(HostContext.Processing.GetValidChannels());
|
|
||||||
foreach (var eventServer in HostContext.EventConnections)
|
|
||||||
eventServer.OutboundQueue.Enqueue(bundledResponse);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Sending channel switch over SSE failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void SendNewLogin()
|
|
||||||
{
|
|
||||||
if (!HostContext.IsActive)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Plugin.Framework.RunOnTick(async () =>
|
|
||||||
{
|
|
||||||
foreach (var eventServer in HostContext.EventConnections)
|
|
||||||
await HostContext.Core.PrepareNewClient(eventServer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Plugin.Log.Error(ex, "Preparing all clients after login failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public void InvalidateSessions()
|
|
||||||
{
|
|
||||||
if (!HostContext.IsActive)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Plugin.Config.AuthStore.Clear();
|
|
||||||
Plugin.SaveConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsActive()
|
|
||||||
{
|
|
||||||
return HostContext is { IsActive: true, Host.IsListening: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsStopping()
|
|
||||||
{
|
|
||||||
return HostContext is { IsActive: false, IsStopping: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public bool Start()
|
|
||||||
{
|
|
||||||
return HostContext.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Run()
|
|
||||||
{
|
|
||||||
HostContext.Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask<bool> Stop()
|
|
||||||
{
|
|
||||||
return await HostContext.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
namespace ChatTwo.Http;
|
|
||||||
|
|
||||||
public static class WebserverUtil
|
|
||||||
{
|
|
||||||
public static async Task<T> FrameworkWrapper<T>(Func<Task<T>> func)
|
|
||||||
{
|
|
||||||
return await Plugin.Framework.RunOnTick(func).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// From: https://github.com/NancyFx/Nancy/blob/master/src/Nancy/Request.cs#L176
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the cookie data from the provided string if it exists
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cookieHeader">The string containing cookie data</param>
|
|
||||||
/// <returns>Cookies dictionary</returns>
|
|
||||||
public static Dictionary<string, string> GetCookieData(string cookieHeader)
|
|
||||||
{
|
|
||||||
var cookieDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
if (cookieHeader.Length == 0)
|
|
||||||
return cookieDictionary;
|
|
||||||
|
|
||||||
var values = cookieHeader.TrimEnd(';').Split(';');
|
|
||||||
foreach (var parts in values.Select(c => c.Split(['='], 2)))
|
|
||||||
{
|
|
||||||
var cookieName = parts[0].Trim();
|
|
||||||
var cookieValue = parts.Length == 1 ? string.Empty : parts[1]; //Cookie attribute
|
|
||||||
|
|
||||||
cookieDictionary[cookieName] = cookieValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cookieDictionary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
|
|
||||||
namespace ChatTwo.Ipc;
|
|
||||||
|
|
||||||
public sealed class ExtraChat : IDisposable
|
|
||||||
{
|
|
||||||
#pragma warning disable CS0649 // Assigned through IPC
|
|
||||||
[Serializable]
|
|
||||||
private struct OverrideInfo
|
|
||||||
{
|
|
||||||
public string? Channel;
|
|
||||||
public ushort UiColour;
|
|
||||||
public uint Rgba;
|
|
||||||
}
|
|
||||||
#pragma warning restore CS0649
|
|
||||||
|
|
||||||
private ICallGateSubscriber<OverrideInfo, object> OverrideChannelGate { get; }
|
|
||||||
private ICallGateSubscriber<Dictionary<string, uint>, Dictionary<string, uint>> ChannelCommandColoursGate { get; }
|
|
||||||
private ICallGateSubscriber<Dictionary<Guid, string>, Dictionary<Guid, string>> ChannelNamesGate { get; }
|
|
||||||
|
|
||||||
internal (string, uint)? ChannelOverride { get; set; }
|
|
||||||
|
|
||||||
private Dictionary<string, uint> ChannelCommandColoursInternal { get; set; } = new();
|
|
||||||
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
|
|
||||||
|
|
||||||
private Dictionary<Guid, string> ChannelNamesInternal { get; set; } = new();
|
|
||||||
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
|
||||||
|
|
||||||
internal ExtraChat()
|
|
||||||
{
|
|
||||||
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>("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);
|
|
||||||
ChannelCommandColoursGate.Subscribe(OnChannelCommandColours);
|
|
||||||
ChannelNamesGate.Subscribe(OnChannelNames);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
|
|
||||||
ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
OverrideChannelGate.Unsubscribe(OnOverrideChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnOverrideChannel(OverrideInfo info)
|
|
||||||
{
|
|
||||||
if (info.Channel == null)
|
|
||||||
{
|
|
||||||
ChannelOverride = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChannelOverride = (info.Channel, info.Rgba);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnChannelCommandColours(Dictionary<string, uint> obj)
|
|
||||||
{
|
|
||||||
ChannelCommandColoursInternal = obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnChannelNames(Dictionary<Guid, string> obj)
|
|
||||||
{
|
|
||||||
ChannelNamesInternal = obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
using ChatTwo.Code;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
|
|
||||||
namespace ChatTwo.Ipc;
|
|
||||||
|
|
||||||
using ChatInputState = (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType);
|
|
||||||
|
|
||||||
internal sealed class TypingIpc : IDisposable
|
|
||||||
{
|
|
||||||
private Plugin Plugin { get; }
|
|
||||||
|
|
||||||
private ICallGateProvider<ChatInputState> StateQueryGate { get; }
|
|
||||||
private ICallGateProvider<ChatInputState, object?> StateChangedGate { get; }
|
|
||||||
|
|
||||||
private ChatInputState LastState;
|
|
||||||
private bool HasState;
|
|
||||||
|
|
||||||
internal TypingIpc(Plugin plugin)
|
|
||||||
{
|
|
||||||
Plugin = plugin;
|
|
||||||
|
|
||||||
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>("ChatTwo.GetChatInputState");
|
|
||||||
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("ChatTwo.ChatInputStateChanged");
|
|
||||||
|
|
||||||
StateQueryGate.RegisterFunc(GetState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChatInputState BuildState()
|
|
||||||
{
|
|
||||||
var log = Plugin.ChatLogWindow;
|
|
||||||
|
|
||||||
var usedChannel = Plugin.CurrentTab.CurrentChannel;
|
|
||||||
var inputChannel = usedChannel.UseTempChannel ? usedChannel.TempChannel : usedChannel.Channel;
|
|
||||||
var channelType = inputChannel.ToChatType();
|
|
||||||
|
|
||||||
return (InputVisible: !log.IsHidden,
|
|
||||||
log.InputFocused,
|
|
||||||
HasText: log.Chat.Length > 0,
|
|
||||||
IsTyping: log is { InputFocused: true, Chat.Length: > 0 },
|
|
||||||
TextLength: log.Chat.Length,
|
|
||||||
ChannelType: channelType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChatInputState GetState()
|
|
||||||
=> BuildState();
|
|
||||||
|
|
||||||
internal void Update()
|
|
||||||
{
|
|
||||||
var state = BuildState();
|
|
||||||
if (HasState && state.Equals(LastState))
|
|
||||||
return;
|
|
||||||
|
|
||||||
HasState = true;
|
|
||||||
LastState = state;
|
|
||||||
StateChangedGate.SendMessage(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
StateQueryGate.UnregisterFunc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
|
|
||||||
namespace ChatTwo;
|
|
||||||
|
|
||||||
internal sealed class IpcManager : IDisposable
|
|
||||||
{
|
|
||||||
private ICallGateProvider<string> RegisterGate { get; }
|
|
||||||
private ICallGateProvider<string, object?> UnregisterGate { get; }
|
|
||||||
private ICallGateProvider<object?> AvailableGate { get; }
|
|
||||||
private ICallGateProvider<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?> InvokeGate { get; }
|
|
||||||
|
|
||||||
internal List<string> Registered { get; } = [];
|
|
||||||
|
|
||||||
public IpcManager()
|
|
||||||
{
|
|
||||||
RegisterGate = Plugin.Interface.GetIpcProvider<string>("ChatTwo.Register");
|
|
||||||
RegisterGate.RegisterFunc(Register);
|
|
||||||
|
|
||||||
AvailableGate = Plugin.Interface.GetIpcProvider<object?>("ChatTwo.Available");
|
|
||||||
|
|
||||||
UnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>("ChatTwo.Unregister");
|
|
||||||
UnregisterGate.RegisterAction(Unregister);
|
|
||||||
|
|
||||||
InvokeGate = Plugin.Interface.GetIpcProvider<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?>("ChatTwo.Invoke");
|
|
||||||
|
|
||||||
AvailableGate.SendMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Invoke(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content)
|
|
||||||
{
|
|
||||||
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string Register()
|
|
||||||
{
|
|
||||||
var id = Guid.NewGuid().ToString();
|
|
||||||
Registered.Add(id);
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Unregister(string id)
|
|
||||||
{
|
|
||||||
Registered.Remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
UnregisterGate.UnregisterFunc();
|
|
||||||
RegisterGate.UnregisterFunc();
|
|
||||||
Registered.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,800 +0,0 @@
|
|||||||
using System.Buffers;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Data.Common;
|
|
||||||
using ChatTwo.Code;
|
|
||||||
using ChatTwo.Ui;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
using MessagePack;
|
|
||||||
using MessagePack.Formatters;
|
|
||||||
using MessagePack.Resolvers;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
|
|
||||||
using DalamudUtil = Dalamud.Utility.Util;
|
|
||||||
using Encoding = System.Text.Encoding;
|
|
||||||
|
|
||||||
namespace ChatTwo;
|
|
||||||
|
|
||||||
internal static class DbExtensions
|
|
||||||
{
|
|
||||||
internal static void Execute(this DbConnection conn, string sql)
|
|
||||||
{
|
|
||||||
using var cmd = conn.CreateCommand();
|
|
||||||
cmd.CommandText = sql;
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum PayloadMessagePackType : byte
|
|
||||||
{
|
|
||||||
Achievement,
|
|
||||||
PartyFinder,
|
|
||||||
Uri,
|
|
||||||
Emote,
|
|
||||||
Other = 255,
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PayloadMessagePackFormatter : IMessagePackFormatter<Payload?>
|
|
||||||
{
|
|
||||||
public void Serialize(ref MessagePackWriter writer, Payload? value, MessagePackSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNil();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteArrayHeader(2);
|
|
||||||
switch (value)
|
|
||||||
{
|
|
||||||
case AchievementPayload achievementPayload:
|
|
||||||
writer.WriteUInt8((byte)PayloadMessagePackType.Achievement);
|
|
||||||
writer.WriteUInt32(achievementPayload.Id);
|
|
||||||
break;
|
|
||||||
case PartyFinderPayload partyFinderPayload:
|
|
||||||
writer.WriteUInt8((byte)PayloadMessagePackType.PartyFinder);
|
|
||||||
writer.WriteUInt32(partyFinderPayload.Id);
|
|
||||||
break;
|
|
||||||
case UriPayload uriPayload:
|
|
||||||
writer.WriteUInt8((byte)PayloadMessagePackType.Uri);
|
|
||||||
writer.WriteString(Encoding.UTF8.GetBytes(uriPayload.Uri.ToString()));
|
|
||||||
break;
|
|
||||||
case EmotePayload emotePayload:
|
|
||||||
writer.WriteUInt8((byte)PayloadMessagePackType.Emote);
|
|
||||||
writer.WriteString(Encoding.UTF8.GetBytes(emotePayload.Code));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
writer.WriteUInt8((byte)PayloadMessagePackType.Other);
|
|
||||||
writer.Write(value.Encode());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Payload? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TryReadNil())
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (reader.ReadArrayHeader() != 2)
|
|
||||||
throw new InvalidOperationException("Invalid array count for Payload object");
|
|
||||||
|
|
||||||
var type = (PayloadMessagePackType)reader.ReadByte();
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case PayloadMessagePackType.Achievement:
|
|
||||||
return new AchievementPayload(reader.ReadUInt32());
|
|
||||||
case PayloadMessagePackType.PartyFinder:
|
|
||||||
return new PartyFinderPayload(reader.ReadUInt32());
|
|
||||||
case PayloadMessagePackType.Uri:
|
|
||||||
return new UriPayload(new Uri(reader.ReadString() ?? ""));
|
|
||||||
case PayloadMessagePackType.Emote:
|
|
||||||
return EmotePayload.ResolveEmote(reader.ReadString() ?? "");
|
|
||||||
case PayloadMessagePackType.Other:
|
|
||||||
default:
|
|
||||||
var bytes = reader.ReadBytes() ?? new ReadOnlySequence<byte>();
|
|
||||||
var binReader = new BinaryReader(new MemoryStream(bytes.ToArray()));
|
|
||||||
return Payload.Decode(binReader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SeStringMessagePackFormatter : IMessagePackFormatter<SeString?>
|
|
||||||
{
|
|
||||||
public void Serialize(ref MessagePackWriter writer, SeString? value, MessagePackSerializerOptions options)
|
|
||||||
{
|
|
||||||
options.Resolver.GetFormatter<List<Payload>>()!.Serialize(ref writer, value?.Payloads ?? [], options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SeString Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
|
||||||
{
|
|
||||||
return new SeString(options.Resolver.GetFormatter<List<Payload>>()!.Deserialize(ref reader, options));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MessageStore : IDisposable
|
|
||||||
{
|
|
||||||
private const int MessageQueryLimit = 10_000;
|
|
||||||
|
|
||||||
private string DbPath { get; }
|
|
||||||
|
|
||||||
private SqliteConnection Connection { get; set; }
|
|
||||||
|
|
||||||
internal static readonly MessagePackSerializerOptions MsgPackOptions = MessagePackSerializerOptions.Standard
|
|
||||||
.WithResolver(CompositeResolver.Create([new PayloadMessagePackFormatter(), new SeStringMessagePackFormatter()], [StandardResolver.Instance]));
|
|
||||||
|
|
||||||
internal MessageStore(string dbPath)
|
|
||||||
{
|
|
||||||
DbPath = dbPath;
|
|
||||||
Connection = Connect();
|
|
||||||
Migrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Connection.Close();
|
|
||||||
Connection.Dispose();
|
|
||||||
// Closing the connection doesn't immediately release the file.
|
|
||||||
GC.Collect();
|
|
||||||
GC.WaitForPendingFinalizers();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SqliteConnection Connect()
|
|
||||||
{
|
|
||||||
var uriBuilder = new SqliteConnectionStringBuilder
|
|
||||||
{
|
|
||||||
DataSource = DbPath,
|
|
||||||
DefaultTimeout = 5,
|
|
||||||
Pooling = false,
|
|
||||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
|
||||||
};
|
|
||||||
|
|
||||||
var conn = new SqliteConnection(uriBuilder.ToString());
|
|
||||||
conn.Open();
|
|
||||||
conn.Execute(@"PRAGMA journal_mode=WAL;");
|
|
||||||
conn.Execute(@"PRAGMA synchronous=NORMAL;");
|
|
||||||
if (DalamudUtil.IsWine())
|
|
||||||
conn.Execute(@"PRAGMA cache_size = 32768;");
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Migrate()
|
|
||||||
{
|
|
||||||
// Get current user_version.
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
cmd.CommandText = "PRAGMA user_version;";
|
|
||||||
var userVersion = Convert.ToInt32(cmd.ExecuteScalar());
|
|
||||||
|
|
||||||
var migrationsToDo = new List<Action>();
|
|
||||||
switch (userVersion)
|
|
||||||
{
|
|
||||||
case <= 0:
|
|
||||||
migrationsToDo.Add(Migrate0);
|
|
||||||
|
|
||||||
// Migration support was only added in version 1. Migrate 0 is
|
|
||||||
// idempotent.
|
|
||||||
migrationsToDo.Add(Migrate1);
|
|
||||||
migrationsToDo.Add(Migrate2);
|
|
||||||
migrationsToDo.Add(Migrate3);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
migrationsToDo.Add(Migrate2);
|
|
||||||
migrationsToDo.Add(Migrate3);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
migrationsToDo.Add(Migrate3);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var migration in migrationsToDo)
|
|
||||||
migration();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Migrate0()
|
|
||||||
{
|
|
||||||
Plugin.Log.Information("Running migration 0: Creating tables");
|
|
||||||
Connection.Execute(@"
|
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
|
||||||
Id BLOB PRIMARY KEY NOT NULL, -- Guid
|
|
||||||
Receiver INTEGER NOT NULL, -- uint64 (first bits are always 0)
|
|
||||||
ContentId INTEGER NOT NULL, -- uint64 (first bits are always 0)
|
|
||||||
Date INTEGER NOT NULL, -- unix timestamp with millisecond precision
|
|
||||||
Code INTEGER NOT NULL, -- ChatCode encoding
|
|
||||||
Sender BLOB NOT NULL, -- Chunk[] msgpack
|
|
||||||
Content BLOB NOT NULL, -- Chunk[] msgpack
|
|
||||||
SenderSource BLOB NOT NULL, -- SeString
|
|
||||||
ContentSource BLOB NOT NULL, -- SeString
|
|
||||||
SortCode INTEGER NOT NULL, -- SortCode encoding
|
|
||||||
ExtraChatChannel BLOB NOT NULL -- Guid
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages (Receiver);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_date ON messages (Date);
|
|
||||||
");
|
|
||||||
|
|
||||||
SetMigrationVersion(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Migrate1()
|
|
||||||
{
|
|
||||||
Plugin.Log.Information("Running migration 1: Adding Deleted column");
|
|
||||||
Connection.Execute(@"
|
|
||||||
-- Migration 1: Add Deleted column
|
|
||||||
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
|
||||||
");
|
|
||||||
|
|
||||||
SetMigrationVersion(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Migrate2()
|
|
||||||
{
|
|
||||||
Plugin.Log.Information("Running migration 2: Adding Channel generated column");
|
|
||||||
Connection.Execute(@"
|
|
||||||
-- Migration 2: Add Channel generated column
|
|
||||||
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages (Channel);
|
|
||||||
");
|
|
||||||
|
|
||||||
SetMigrationVersion(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ColumnExists(string table, string column)
|
|
||||||
{
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
cmd.CommandText = $"PRAGMA table_info({table});";
|
|
||||||
using var reader = cmd.ExecuteReader();
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (reader.GetString(1) == column)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Migrate3()
|
|
||||||
{
|
|
||||||
Plugin.Log.Information("Running migration 3: Fix log kinds to fit the new format");
|
|
||||||
|
|
||||||
// Recovery for partially-applied Migrate3: if the schema is already
|
|
||||||
// in its target shape (new columns exist, old Code column gone) but
|
|
||||||
// user_version was never bumped, just record the version and exit.
|
|
||||||
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
|
|
||||||
{
|
|
||||||
Plugin.Log.Information("Migration 3: schema already migrated, only bumping user_version");
|
|
||||||
SetMigrationVersion(3);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connection.Execute(@"
|
|
||||||
-- Migration 3: Fix log kinds to fit the new format
|
|
||||||
-- Add new ChatType, SourceKind, TargetKind (byte), SortCodeV2
|
|
||||||
-- Migrate OldChatColumn
|
|
||||||
-- ChatType = OldChatColumn & 0x7f
|
|
||||||
-- SourceKind = log2(1 << ((OldChatColumn >> 11) & 0xF))
|
|
||||||
-- TargetKind = trunc(log2(1 << ((OldChatColumn >> 7) & 0xF)))
|
|
||||||
-- Virtual SortCodeV2 = ChatType << 16 | SourceKind << 8 | TargetKind
|
|
||||||
-- Delete OldChatColumn, Virtual Channel
|
|
||||||
|
|
||||||
ALTER TABLE messages ADD COLUMN ChatType INTEGER;
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_chat_type ON messages (ChatType);
|
|
||||||
ALTER TABLE messages ADD COLUMN SourceKind INTEGER;
|
|
||||||
ALTER TABLE messages ADD COLUMN TargetKind INTEGER;
|
|
||||||
|
|
||||||
UPDATE messages SET
|
|
||||||
ChatType = Code & 0x7f,
|
|
||||||
SourceKind = trunc(log2(1 << ((Code >> 11) & 0xF))),
|
|
||||||
TargetKind = trunc(log2(1 << ((Code >> 7) & 0xF)))
|
|
||||||
WHERE true;
|
|
||||||
|
|
||||||
DROP INDEX idx_messages_channel;
|
|
||||||
ALTER TABLE messages DROP COLUMN Channel;
|
|
||||||
ALTER TABLE messages DROP COLUMN Code;
|
|
||||||
ALTER TABLE messages DROP COLUMN SortCode;
|
|
||||||
");
|
|
||||||
|
|
||||||
SetMigrationVersion(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetMigrationVersion(int version)
|
|
||||||
{
|
|
||||||
Plugin.Log.Information($"Setting version {version}");
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
// Parameters aren't supported for PRAGMA queries, and you can't set the
|
|
||||||
// version with a pragma_ function.
|
|
||||||
cmd.CommandText = $"PRAGMA user_version = {version};";
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ClearMessages()
|
|
||||||
{
|
|
||||||
Connection.Execute("DELETE FROM messages;");
|
|
||||||
PerformMaintenance();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a (ChatType, count) snapshot over non-deleted messages.
|
|
||||||
/// Used by the Privacy tab to preview the impact of a retroactive
|
|
||||||
/// cleanup before the user confirms.
|
|
||||||
/// </summary>
|
|
||||||
internal Dictionary<int, long> GetMessageCountsByChatType()
|
|
||||||
{
|
|
||||||
var result = new Dictionary<int, long>();
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
cmd.CommandText = "SELECT ChatType, COUNT(*) FROM messages WHERE deleted = false GROUP BY ChatType;";
|
|
||||||
cmd.CommandTimeout = 120;
|
|
||||||
using var reader = cmd.ExecuteReader();
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
var chatType = reader.GetInt32(0);
|
|
||||||
var count = reader.GetInt64(1);
|
|
||||||
result[chatType] = count;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes messages older than the per-channel retention window, with a
|
|
||||||
/// global default for channels not listed explicitly. Cutoffs are
|
|
||||||
/// computed from "now" at call time. Runs VACUUM only if anything was
|
|
||||||
/// removed. Returns the number of rows deleted.
|
|
||||||
/// </summary>
|
|
||||||
internal long DeleteByRetentionPolicy(IReadOnlyDictionary<int, int> chatTypeDaysMap, int defaultDays)
|
|
||||||
{
|
|
||||||
if (defaultDays < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(defaultDays), "Negative retention is not allowed.");
|
|
||||||
foreach (var (_, days) in chatTypeDaysMap)
|
|
||||||
if (days < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(chatTypeDaysMap), "Negative retention is not allowed.");
|
|
||||||
|
|
||||||
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
||||||
var clauses = new List<string>();
|
|
||||||
foreach (var (type, days) in chatTypeDaysMap)
|
|
||||||
{
|
|
||||||
var cutoff = nowMs - days * 86400000L;
|
|
||||||
clauses.Add($"(ChatType = {type} AND Date < {cutoff})");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catch-all for channels without an explicit override. "0" is treated
|
|
||||||
// as "do not delete by default" — without an explicit user override,
|
|
||||||
// unmapped channels stay forever instead of getting wiped immediately.
|
|
||||||
if (defaultDays > 0)
|
|
||||||
{
|
|
||||||
var cutoff = nowMs - defaultDays * 86400000L;
|
|
||||||
var explicitTypes = chatTypeDaysMap.Count > 0
|
|
||||||
? string.Join(",", chatTypeDaysMap.Keys)
|
|
||||||
: "-1"; // empty list would produce invalid SQL
|
|
||||||
clauses.Add($"(ChatType NOT IN ({explicitTypes}) AND Date < {cutoff})");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clauses.Count == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
long deleted;
|
|
||||||
using (var cmd = Connection.CreateCommand())
|
|
||||||
{
|
|
||||||
cmd.CommandText = $"DELETE FROM messages WHERE {string.Join(" OR ", clauses)};";
|
|
||||||
cmd.CommandTimeout = 600;
|
|
||||||
deleted = cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleted > 0)
|
|
||||||
PerformMaintenance();
|
|
||||||
return deleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hard-deletes every message whose ChatType is not in the supplied
|
|
||||||
/// allowlist, then VACUUMs the database to reclaim disk space.
|
|
||||||
/// Returns the number of rows deleted.
|
|
||||||
/// </summary>
|
|
||||||
internal long CleanupRetainOnly(IReadOnlyCollection<int> allowedTypes)
|
|
||||||
{
|
|
||||||
if (allowedTypes.Count == 0)
|
|
||||||
{
|
|
||||||
// Defensive: refuse a "delete everything" disguised as a filter.
|
|
||||||
// Use ClearMessages() if a full wipe is actually intended.
|
|
||||||
throw new InvalidOperationException("CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var inList = string.Join(",", allowedTypes);
|
|
||||||
long deleted;
|
|
||||||
using (var cmd = Connection.CreateCommand())
|
|
||||||
{
|
|
||||||
cmd.CommandText = $"DELETE FROM messages WHERE ChatType NOT IN ({inList});";
|
|
||||||
cmd.CommandTimeout = 600;
|
|
||||||
deleted = cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
PerformMaintenance();
|
|
||||||
return deleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PerformMaintenance()
|
|
||||||
{
|
|
||||||
Connection.Execute(@"
|
|
||||||
VACUUM;
|
|
||||||
REINDEX messages;
|
|
||||||
ANALYZE;
|
|
||||||
");
|
|
||||||
}
|
|
||||||
|
|
||||||
private string LogPath => DbPath + "-wal";
|
|
||||||
internal long DatabaseSize() => !File.Exists(DbPath) ? 0 : new FileInfo(DbPath).Length;
|
|
||||||
internal long DatabaseLogSize() => !File.Exists(LogPath) ? 0 : new FileInfo(LogPath).Length;
|
|
||||||
|
|
||||||
internal int MessageCount()
|
|
||||||
{
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
cmd.CommandText = "SELECT COUNT(*) FROM messages;";
|
|
||||||
return Convert.ToInt32(cmd.ExecuteScalar());
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void UpsertMessage(Message message)
|
|
||||||
{
|
|
||||||
// Hellion Chat privacy filter — drop disallowed ChatTypes before
|
|
||||||
// they reach the storage layer (single source of truth, also
|
|
||||||
// covers any future write paths e.g. webinterface backfill).
|
|
||||||
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
|
||||||
{
|
|
||||||
Plugin.Log.Debug($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
cmd.CommandText = @"
|
|
||||||
INSERT INTO messages (
|
|
||||||
Id,
|
|
||||||
Receiver,
|
|
||||||
ContentId,
|
|
||||||
Date,
|
|
||||||
ChatType,
|
|
||||||
SourceKind,
|
|
||||||
TargetKind,
|
|
||||||
Sender,
|
|
||||||
Content,
|
|
||||||
SenderSource,
|
|
||||||
ContentSource,
|
|
||||||
ExtraChatChannel,
|
|
||||||
Deleted
|
|
||||||
) VALUES (
|
|
||||||
$Id,
|
|
||||||
$Receiver,
|
|
||||||
$ContentId,
|
|
||||||
$Date,
|
|
||||||
$ChatType,
|
|
||||||
$SourceKind,
|
|
||||||
$TargetKind,
|
|
||||||
$Sender,
|
|
||||||
$Content,
|
|
||||||
$SenderSource,
|
|
||||||
$ContentSource,
|
|
||||||
$ExtraChatChannel,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
|
||||||
Receiver = excluded.Receiver,
|
|
||||||
ContentId = excluded.ContentId,
|
|
||||||
Date = excluded.Date,
|
|
||||||
ChatType = excluded.ChatType,
|
|
||||||
SourceKind = excluded.SourceKind,
|
|
||||||
TargetKind = excluded.TargetKind,
|
|
||||||
Sender = excluded.Sender,
|
|
||||||
Content = excluded.Content,
|
|
||||||
SenderSource = excluded.SenderSource,
|
|
||||||
ContentSource = excluded.ContentSource,
|
|
||||||
ExtraChatChannel = excluded.ExtraChatChannel,
|
|
||||||
Deleted = false;
|
|
||||||
";
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$Id", message.Id);
|
|
||||||
cmd.Parameters.AddWithValue("$Receiver", message.Receiver);
|
|
||||||
cmd.Parameters.AddWithValue("$ContentId", message.ContentId);
|
|
||||||
cmd.Parameters.AddWithValue("$Date", message.Date.ToUnixTimeMilliseconds());
|
|
||||||
cmd.Parameters.AddWithValue("$ChatType", message.Code.Type);
|
|
||||||
cmd.Parameters.AddWithValue("$SourceKind", message.Code.Source);
|
|
||||||
cmd.Parameters.AddWithValue("$TargetKind", message.Code.Target);
|
|
||||||
cmd.Parameters.AddWithValue("$Sender", MessagePackSerializer.Serialize(message.Sender, MsgPackOptions));
|
|
||||||
cmd.Parameters.AddWithValue("$Content", MessagePackSerializer.Serialize(message.Content, MsgPackOptions));
|
|
||||||
cmd.Parameters.AddWithValue("$SenderSource", MessagePackSerializer.Serialize(message.SenderSource, MsgPackOptions));
|
|
||||||
cmd.Parameters.AddWithValue("$ContentSource", MessagePackSerializer.Serialize(message.ContentSource, MsgPackOptions));
|
|
||||||
cmd.Parameters.AddWithValue("$ExtraChatChannel", message.ExtraChatChannel);
|
|
||||||
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Streams messages for export. Optional filters:
|
|
||||||
/// - <paramref name="chatTypes"/>: limit to these ChatTypes
|
|
||||||
/// - <paramref name="from"/> / <paramref name="to"/>: inclusive date range
|
|
||||||
/// Result is sorted ascending by Date and excludes soft-deleted rows.
|
|
||||||
/// Caller is responsible for disposing the enumerator.
|
|
||||||
/// </summary>
|
|
||||||
internal MessageEnumerator StreamForExport(
|
|
||||||
IReadOnlyCollection<int>? chatTypes,
|
|
||||||
DateTimeOffset? from,
|
|
||||||
DateTimeOffset? to)
|
|
||||||
{
|
|
||||||
var clauses = new List<string> { "deleted = false" };
|
|
||||||
if (chatTypes is { Count: > 0 })
|
|
||||||
clauses.Add($"ChatType IN ({string.Join(",", chatTypes)})");
|
|
||||||
if (from is not null)
|
|
||||||
clauses.Add("Date >= $From");
|
|
||||||
if (to is not null)
|
|
||||||
clauses.Add("Date <= $To");
|
|
||||||
|
|
||||||
var cmd = Connection.CreateCommand();
|
|
||||||
cmd.CommandText = @"
|
|
||||||
SELECT
|
|
||||||
Id,
|
|
||||||
Receiver,
|
|
||||||
ContentId,
|
|
||||||
Date,
|
|
||||||
ChatType,
|
|
||||||
SourceKind,
|
|
||||||
TargetKind,
|
|
||||||
Sender,
|
|
||||||
Content,
|
|
||||||
SenderSource,
|
|
||||||
ContentSource,
|
|
||||||
ExtraChatChannel
|
|
||||||
FROM messages
|
|
||||||
WHERE " + string.Join(" AND ", clauses) + @"
|
|
||||||
ORDER BY Date ASC;";
|
|
||||||
cmd.CommandTimeout = 600;
|
|
||||||
|
|
||||||
if (from is not null)
|
|
||||||
cmd.Parameters.AddWithValue("$From", from.Value.ToUnixTimeMilliseconds());
|
|
||||||
if (to is not null)
|
|
||||||
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
|
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the most recent messages.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="receiver">The receiver content ID to filter by. If null, no filtering is performed.</param>
|
|
||||||
/// <param name="since">Only show messages since this date. If null, no filtering is performed.</param>
|
|
||||||
/// <param name="count">The amount to return. Defaults to 10,000.</param>
|
|
||||||
internal MessageEnumerator GetMostRecentMessages(ulong? receiver = null, DateTimeOffset? since = null, int count = MessageQueryLimit)
|
|
||||||
{
|
|
||||||
List<string> whereClauses = ["deleted = false"];
|
|
||||||
if (receiver != null)
|
|
||||||
whereClauses.Add("Receiver = $Receiver");
|
|
||||||
if (since != null)
|
|
||||||
whereClauses.Add("Date >= $Since");
|
|
||||||
|
|
||||||
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
|
||||||
|
|
||||||
var cmd = Connection.CreateCommand();
|
|
||||||
// Select last N messages by date DESC, but reverse the order to get
|
|
||||||
// them in ascending order.
|
|
||||||
cmd.CommandText = @"
|
|
||||||
SELECT *
|
|
||||||
FROM (
|
|
||||||
SELECT
|
|
||||||
Id,
|
|
||||||
Receiver,
|
|
||||||
ContentId,
|
|
||||||
Date,
|
|
||||||
ChatType,
|
|
||||||
SourceKind,
|
|
||||||
TargetKind,
|
|
||||||
Sender,
|
|
||||||
Content,
|
|
||||||
SenderSource,
|
|
||||||
ContentSource,
|
|
||||||
ExtraChatChannel
|
|
||||||
FROM messages
|
|
||||||
" + whereClause + @"
|
|
||||||
ORDER BY Date DESC
|
|
||||||
LIMIT $Count
|
|
||||||
)
|
|
||||||
ORDER BY Date ASC;
|
|
||||||
";
|
|
||||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
|
||||||
|
|
||||||
if (receiver != null)
|
|
||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
|
||||||
if (since != null)
|
|
||||||
cmd.Parameters.AddWithValue("$Since", since.Value.ToUnixTimeMilliseconds());
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$Count", count);
|
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marks a message as deleted so it won't get returned in queries.
|
|
||||||
/// </summary>
|
|
||||||
internal void DeleteMessage(Guid id)
|
|
||||||
{
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
cmd.CommandText = "UPDATE messages SET Deleted = true WHERE Id = $Id;";
|
|
||||||
cmd.Parameters.AddWithValue("$Id", id);
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal long CountDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
|
|
||||||
{
|
|
||||||
List<string> whereClauses = ["deleted = false"];
|
|
||||||
if (receiver != null)
|
|
||||||
whereClauses.Add("Receiver = $Receiver");
|
|
||||||
|
|
||||||
whereClauses.Add("Date BETWEEN $After AND $Before");
|
|
||||||
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})");
|
|
||||||
|
|
||||||
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
|
||||||
|
|
||||||
using var cmd = Connection.CreateCommand();
|
|
||||||
// Select last N messages by date DESC, but reverse the order to get
|
|
||||||
// them in ascending order.
|
|
||||||
cmd.CommandText = @"
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM messages
|
|
||||||
" + whereClause;
|
|
||||||
|
|
||||||
if (receiver != null)
|
|
||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds());
|
|
||||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds());
|
|
||||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
|
||||||
|
|
||||||
return (long) cmd.ExecuteScalar()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal MessageEnumerator GetDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
|
|
||||||
{
|
|
||||||
List<string> whereClauses = ["deleted = false"];
|
|
||||||
if (receiver != null)
|
|
||||||
whereClauses.Add("Receiver = $Receiver");
|
|
||||||
|
|
||||||
whereClauses.Add("Date BETWEEN $After AND $Before");
|
|
||||||
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})");
|
|
||||||
|
|
||||||
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
|
||||||
|
|
||||||
var cmd = Connection.CreateCommand();
|
|
||||||
// Select last N messages by date DESC, but reverse the order to get
|
|
||||||
// them in ascending order.
|
|
||||||
cmd.CommandText = @"
|
|
||||||
SELECT
|
|
||||||
Id,
|
|
||||||
Receiver,
|
|
||||||
ContentId,
|
|
||||||
Date,
|
|
||||||
ChatType,
|
|
||||||
SourceKind,
|
|
||||||
TargetKind,
|
|
||||||
Sender,
|
|
||||||
Content,
|
|
||||||
SenderSource,
|
|
||||||
ContentSource,
|
|
||||||
ExtraChatChannel
|
|
||||||
FROM messages
|
|
||||||
" + whereClause;
|
|
||||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
|
||||||
|
|
||||||
if (receiver != null)
|
|
||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds());
|
|
||||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds());
|
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
|
||||||
}
|
|
||||||
|
|
||||||
internal MessageEnumerator GetPagedDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null, int page = 0)
|
|
||||||
{
|
|
||||||
List<string> whereClauses = ["deleted = false"];
|
|
||||||
if (receiver != null)
|
|
||||||
whereClauses.Add("Receiver = $Receiver");
|
|
||||||
|
|
||||||
whereClauses.Add("Date BETWEEN $After AND $Before");
|
|
||||||
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})");
|
|
||||||
|
|
||||||
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
|
||||||
|
|
||||||
var cmd = Connection.CreateCommand();
|
|
||||||
// Select last N messages by date DESC, but reverse the order to get
|
|
||||||
// them in ascending order.
|
|
||||||
cmd.CommandText = @"
|
|
||||||
SELECT
|
|
||||||
Id,
|
|
||||||
Receiver,
|
|
||||||
ContentId,
|
|
||||||
Date,
|
|
||||||
ChatType,
|
|
||||||
SourceKind,
|
|
||||||
TargetKind,
|
|
||||||
Sender,
|
|
||||||
Content,
|
|
||||||
SenderSource,
|
|
||||||
ContentSource,
|
|
||||||
ExtraChatChannel
|
|
||||||
FROM messages
|
|
||||||
" + whereClause + @"
|
|
||||||
ORDER BY Date
|
|
||||||
LIMIT $Offset, $OffsetCount;
|
|
||||||
";
|
|
||||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
|
||||||
|
|
||||||
if (receiver != null)
|
|
||||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset) after).ToUnixTimeMilliseconds());
|
|
||||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset) before).ToUnixTimeMilliseconds());
|
|
||||||
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
|
||||||
cmd.Parameters.AddWithValue("OffsetCount", DbViewer.RowPerPage);
|
|
||||||
|
|
||||||
return new MessageEnumerator(cmd.ExecuteReader());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MessageEnumerator(DbDataReader reader) : IEnumerable<Message>, IDisposable, IAsyncDisposable
|
|
||||||
{
|
|
||||||
private const int MaxErrorLogs = 10;
|
|
||||||
|
|
||||||
// FailedIds and FailedCount are separate, because messages might fail to
|
|
||||||
// even parse the ID field.
|
|
||||||
private readonly List<Guid> FailedIds = [];
|
|
||||||
private int FailedCount;
|
|
||||||
public bool DidError => FailedCount > 0;
|
|
||||||
|
|
||||||
public IEnumerator<Message> GetEnumerator()
|
|
||||||
{
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
var id = Guid.Empty;
|
|
||||||
Message msg;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
id = reader.GetGuid(0);
|
|
||||||
msg = new Message(
|
|
||||||
id,
|
|
||||||
(ulong)reader.GetInt64(1),
|
|
||||||
(ulong)reader.GetInt64(2),
|
|
||||||
DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64(3)),
|
|
||||||
new ChatCode((byte)reader.GetInt32(4), (byte)reader.GetInt32(5), (byte)reader.GetInt32(6)),
|
|
||||||
MessagePackSerializer.Deserialize<List<Chunk>>(reader.GetFieldValue<byte[]>(7), MessageStore.MsgPackOptions),
|
|
||||||
MessagePackSerializer.Deserialize<List<Chunk>>(reader.GetFieldValue<byte[]>(8), MessageStore.MsgPackOptions),
|
|
||||||
MessagePackSerializer.Deserialize<SeString>(reader.GetFieldValue<byte[]>(9), MessageStore.MsgPackOptions),
|
|
||||||
MessagePackSerializer.Deserialize<SeString>(reader.GetFieldValue<byte[]>(10), MessageStore.MsgPackOptions),
|
|
||||||
reader.GetGuid(11)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
if (FailedCount < MaxErrorLogs)
|
|
||||||
Plugin.Log.Error($"Exception while reading message '{id}' from database: {e}");
|
|
||||||
FailedCount++;
|
|
||||||
if (FailedCount == MaxErrorLogs)
|
|
||||||
Plugin.Log.Error("Further parsing errors will not be logged");
|
|
||||||
if (id != Guid.Empty)
|
|
||||||
FailedIds.Add(id);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
|
||||||
{
|
|
||||||
return GetEnumerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<Guid> FailedMessageIds()
|
|
||||||
{
|
|
||||||
return FailedIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
reader.Dispose();
|
|
||||||
}
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
await reader.DisposeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user