Compare commits
15 Commits
v1.4.3
..
734703eddc
| Author | SHA1 | Date | |
|---|---|---|---|
| 734703eddc | |||
| 4000bbd199 | |||
| 3cabdf3e15 | |||
| 05c28f7e92 | |||
| 699d4ede1d | |||
| 31673fdff6 | |||
| 07337108bc | |||
| fd82033666 | |||
| cd01fa63a1 | |||
| b81c50b433 | |||
| 355a57089b | |||
| cf7ab6226c | |||
| 03da6d58a4 | |||
| 90a4544ab2 | |||
| 9b4557f197 |
+13
-2
@@ -7,12 +7,21 @@ insert_final_newline=false
|
||||
|
||||
# JetBrains Rider custom properties for code formatting styles
|
||||
resharper_csharp_brace_style=next_line
|
||||
# Allman für standard Tooling (VS Code)
|
||||
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
|
||||
|
||||
# Switch-Einrückung
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
resharper_csharp_braces_for_foreach=not_required
|
||||
resharper_csharp_braces_for_for=not_required
|
||||
resharper_csharp_braces_for_while=not_required
|
||||
charset=utf-8
|
||||
end_of_line=crlf
|
||||
end_of_line=lf
|
||||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers=false
|
||||
@@ -142,7 +151,7 @@ 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}]
|
||||
[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.prettierrc.json,.markdownlint.json,.yamllint.json,.stylelintrc,bowerrc,jest.config}]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
|
||||
@@ -154,3 +163,5 @@ indent_size=2
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
tab_width=4
|
||||
[*.md]
|
||||
trim_trailing_whitespace=false
|
||||
+7
-1
@@ -1,2 +1,8 @@
|
||||
# Generated files
|
||||
HellionChat/Resources/Language.*.resx linguist-generated=true
|
||||
HellionChat/Resources/Language.*.resx linguist-generated=true
|
||||
* text=auto eol=lf
|
||||
*.cs text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.md text eol=lf
|
||||
*.json text eol=lf
|
||||
@@ -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"
|
||||
@@ -1,73 +1,73 @@
|
||||
name: Bug report
|
||||
description: Something in HellionChat is broken or behaves wrong
|
||||
labels:
|
||||
- bug
|
||||
- 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: 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
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: HellionChat version
|
||||
description: From Settings → Information → Version
|
||||
placeholder: "0.5.4"
|
||||
validations:
|
||||
required: true
|
||||
- label: I have searched existing issues for duplicates
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
options:
|
||||
- Windows (XIVLauncher)
|
||||
- Linux (XIVLauncher Core)
|
||||
- macOS (XIVLauncher Core / wine)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened
|
||||
description: Plain description, no log dumps yet
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What you expected
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: Step-by-step from "open settings" or "log in" through to the broken behaviour
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Relevant /xllog excerpt
|
||||
description: Filter for "HellionChat" if the log is huge
|
||||
render: text
|
||||
|
||||
- type: checkboxes
|
||||
id: confirm
|
||||
attributes:
|
||||
label: Pre-flight
|
||||
options:
|
||||
- label: I am running the latest version of HellionChat
|
||||
required: true
|
||||
- label: I have searched existing issues for duplicates
|
||||
required: true
|
||||
|
||||
@@ -1,14 +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: 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: 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.
|
||||
- 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.
|
||||
|
||||
@@ -1,55 +1,57 @@
|
||||
name: Feature request
|
||||
description: Suggest a feature or enhancement for HellionChat
|
||||
labels:
|
||||
- enhancement
|
||||
- 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: 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
|
||||
- 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
|
||||
- label: I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat 2
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: What you would like HellionChat to do
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives you have considered
|
||||
description: Other plugins, manual workarounds, settings combinations
|
||||
|
||||
- type: dropdown
|
||||
id: scope
|
||||
attributes:
|
||||
label: Scope estimate from your side
|
||||
options:
|
||||
- "Small (one tab, one toggle, one filter)"
|
||||
- "Medium (a settings section, persistent state, one new file)"
|
||||
- "Large (architectural, touches the message pipeline or the database)"
|
||||
- "I don't know"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: confirm
|
||||
attributes:
|
||||
label: Pre-flight
|
||||
options:
|
||||
- label: I have searched existing issues for similar requests
|
||||
required: true
|
||||
- label:
|
||||
I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat
|
||||
2
|
||||
required: true
|
||||
|
||||
@@ -18,8 +18,7 @@ mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||
|
||||
- [ ] 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)
|
||||
- [ ] Breaking change (config migration, removed feature, or behaviour change that user-visible defaults rely on)
|
||||
- [ ] Documentation only
|
||||
- [ ] Translation update
|
||||
- [ ] Build, CI or tooling change
|
||||
@@ -56,15 +55,11 @@ new commands, new translations, removed behaviour. If none, write
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and
|
||||
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
|
||||
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
|
||||
- [ ] My change matches the existing code style (`.editorconfig`).
|
||||
- [ ] 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).
|
||||
- [ ] 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).
|
||||
|
||||
+38
-38
@@ -1,42 +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
|
||||
# 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)"
|
||||
# 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)"
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
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
|
||||
|
||||
- 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
|
||||
- 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
|
||||
- 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`
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
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
|
||||
|
||||
- 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
|
||||
- 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
|
||||
|
||||
|
||||
@@ -2,15 +2,28 @@
|
||||
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)
|
||||
|
||||
- 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
|
||||
- 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.
|
||||
Pure UX-Polish, keine neuen Features. Nächster Cycle (v1.3.0): Animation-Polish (Lerps, Theme-Crossfade, Quick-Picker)
|
||||
wie ursprünglich geplant.
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
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
|
||||
|
||||
- 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.
|
||||
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.
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
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
|
||||
- **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
|
||||
- **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
|
||||
|
||||
@@ -5,28 +5,19 @@ 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.
|
||||
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
|
||||
- **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
|
||||
- **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
|
||||
- **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.
|
||||
Keine Schema-Bumps, keine User-sichtbaren Funktions-Änderungen außer dass Reload und Shutdown spürbar sauberer laufen.
|
||||
|
||||
@@ -5,35 +5,23 @@ 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.
|
||||
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
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **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.
|
||||
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.
|
||||
|
||||
@@ -5,39 +5,25 @@ 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.
|
||||
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
|
||||
- **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-
|
||||
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
|
||||
Keine Schema-Bumps, keine User-sichtbaren Funktions- Änderungen außer dass die Frames im Chat-Log und in der
|
||||
Settings-Statusleiste merklich glatter laufen.
|
||||
|
||||
@@ -5,41 +5,25 @@ 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.
|
||||
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
|
||||
- **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
|
||||
- **Plugin-Load-Zeit ehrlich.** Median 3,7 s über fünf Reloads, vergleichbar mit v1.4.2. Der Async-Refactor ist
|
||||
Foundation für künftige Lazy-Init-Optimierungen (v1.4.4) und Code-Architektur-Hygiene, kein direkter User-spürbarer
|
||||
Speed-Win in dieser Release
|
||||
|
||||
Keine User-sichtbaren Funktions-Änderungen außer dem
|
||||
Repo-URL-Update. Settings, Themes und Tabs bleiben
|
||||
unangetastet.
|
||||
Keine User-sichtbaren Funktions-Änderungen außer dem Repo-URL-Update. Settings, Themes und Tabs bleiben unangetastet.
|
||||
|
||||
+14
-13
@@ -1,26 +1,27 @@
|
||||
|
||||
---
|
||||
|
||||
## How to install
|
||||
|
||||
This release is distributed via the HellionChat custom repository, not the
|
||||
Dalamud main plugin repo. To install:
|
||||
This release is distributed via the HellionChat custom repository, not the Dalamud main plugin repo. To install:
|
||||
|
||||
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
||||
2. Add the URL:
|
||||
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||
2. Add the URL: `https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||
3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install
|
||||
|
||||
## 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
|
||||
- [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.
|
||||
[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.
|
||||
|
||||
+27
-27
@@ -11,43 +11,43 @@ name: Build
|
||||
# Dalamud SDK 15 uses on Linux).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
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
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (Release)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
build:
|
||||
name: Build (Release)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
- 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: 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: Restore
|
||||
run: dotnet restore HellionChat/HellionChat.csproj
|
||||
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||
|
||||
@@ -17,209 +17,209 @@ name: Forge Announce
|
||||
# (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
|
||||
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
|
||||
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
|
||||
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 }}
|
||||
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(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$header = "**Hellion Chat $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**Hellion Chat ", 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()
|
||||
}
|
||||
|
||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
||||
$title = "Hellion Chat $version — $subtitle"
|
||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
||||
$footerText = "Hellion Forge · $versionsnatur"
|
||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
||||
if ($totalChars -gt 5500) {
|
||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
||||
}
|
||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
||||
|
||||
# ---------- Embed-Payload bauen ----------
|
||||
$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 = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
color = 12730636
|
||||
description = $description
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
# 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)
|
||||
|
||||
$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))
|
||||
# ---------- 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
|
||||
|
||||
Write-Host "Payload size: $($payloadJson.Length) chars"
|
||||
Write-Host "Embed title: $title"
|
||||
Write-Host "Embed footer: $footerText"
|
||||
# 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()
|
||||
|
||||
# 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."
|
||||
}
|
||||
$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" }
|
||||
|
||||
$payloadFile = "$PWD/embed-payload.json"
|
||||
if (-not (Test-Path $payloadFile)) {
|
||||
throw "Embed payload file missing — previous step did not produce embed-payload.json"
|
||||
}
|
||||
# ---------- 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(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$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"
|
||||
$header = "**Hellion Chat $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**Hellion Chat ", 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()
|
||||
}
|
||||
|
||||
if ($status -ge 200 -and $status -lt 300) {
|
||||
Write-Host "Forge announce POST succeeded."
|
||||
exit 0
|
||||
}
|
||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
||||
$title = "Hellion Chat $version — $subtitle"
|
||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
||||
$footerText = "Hellion Forge · $versionsnatur"
|
||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
||||
if ($totalChars -gt 5500) {
|
||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
||||
}
|
||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
||||
|
||||
$bodySnippet = ""
|
||||
if (Test-Path $tmpResp) {
|
||||
$bodySnippet = (Get-Content $tmpResp -Raw -ErrorAction SilentlyContinue)
|
||||
if ($bodySnippet.Length -gt 500) { $bodySnippet = $bodySnippet.Substring(0, 500) + " …" }
|
||||
}
|
||||
# ---------- Embed-Payload bauen ----------
|
||||
$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 = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
color = 12730636
|
||||
description = $description
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
$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))
|
||||
|
||||
# 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"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
+131
-131
@@ -17,154 +17,154 @@ name: Release
|
||||
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
||||
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
||||
# The tag input is validated against the same semver regex as the
|
||||
# auto-trigger before any string interpolation happens.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Existing tag to (re)release, e.g. v0.6.1'
|
||||
required: true
|
||||
type: string
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
||||
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
||||
# The tag input is validated against the same semver regex as the
|
||||
# auto-trigger before any string interpolation happens.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing tag to (re)release, e.g. v0.6.1"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and attach release ZIP
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
release:
|
||||
name: Build and attach release ZIP
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
# On push:tags, github.ref_name is the tag — checkout default works.
|
||||
# On workflow_dispatch, ref defaults to the branch the action was
|
||||
# invoked from; we need to explicitly check out the tag the user
|
||||
# supplied so the build comes from the tagged commit, not main.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
steps:
|
||||
# On push:tags, github.ref_name is the tag — checkout default works.
|
||||
# On workflow_dispatch, ref defaults to the branch the action was
|
||||
# invoked from; we need to explicitly check out the tag the user
|
||||
# supplied so the build comes from the tagged commit, not main.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
- 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: 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: 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"
|
||||
- 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:
|
||||
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
||||
# push:tags carries it in github.ref_name. Either way the value
|
||||
# is treated as a PowerShell variable (env-var pass), not as
|
||||
# inline shell text, and validated against the semver regex
|
||||
# below before any string interpolation.
|
||||
TAG_NAME: ${{ github.event.inputs.tag || 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)
|
||||
# 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:
|
||||
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
||||
# push:tags carries it in github.ref_name. Either way the value
|
||||
# is treated as a PowerShell variable (env-var pass), not as
|
||||
# inline shell text, and validated against the semver regex
|
||||
# below before any string interpolation.
|
||||
TAG_NAME: ${{ github.event.inputs.tag || 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
|
||||
$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" }
|
||||
$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
|
||||
# 2-space yaml indent from each line.
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
# changelog: is the last top-level key in the manifest, so
|
||||
# everything after the marker is the literal block. Strip the
|
||||
# 2-space yaml indent from each line.
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$header = "**Hellion Chat $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."
|
||||
}
|
||||
$header = "**Hellion Chat $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**Hellion Chat ", 1)
|
||||
$trailer = $rest.IndexOf("`n`n---")
|
||||
$rest = $changelogBody.Substring($start)
|
||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 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()
|
||||
}
|
||||
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
|
||||
# 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
|
||||
$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 "----------------------------------------"
|
||||
Write-Host "Generated release body for $tag :"
|
||||
Write-Host "----------------------------------------"
|
||||
Write-Host $body
|
||||
Write-Host "----------------------------------------"
|
||||
|
||||
# Gitea-native release action. Creates the release if the tag has no
|
||||
# release yet, or updates the existing one. body_path provides the
|
||||
# generated release body, files attaches latest.zip. 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:
|
||||
# Explicit tag_name so the action targets the correct release in
|
||||
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
||||
# modes. Without this, dispatch runs would default to the branch
|
||||
# ref (main) and fail to find the release.
|
||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
body_path: release-body.md
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Gitea-native release action. Creates the release if the tag has no
|
||||
# release yet, or updates the existing one. body_path provides the
|
||||
# generated release body, files attaches latest.zip. 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:
|
||||
# Explicit tag_name so the action targets the correct release in
|
||||
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
||||
# modes. Without this, dispatch runs would default to the branch
|
||||
# ref (main) and fail to find the release.
|
||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
body_path: release-body.md
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"MD007": { "indent": 4 },
|
||||
"MD013": false,
|
||||
"MD029": false,
|
||||
"MD033": false,
|
||||
"MD041": false
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
bin/
|
||||
obj/
|
||||
node_modules/
|
||||
*.Designer.cs
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always",
|
||||
"singleQuote": false,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
extends: default
|
||||
rules:
|
||||
line-length: disable
|
||||
document-start: disable
|
||||
truthy:
|
||||
allowed-values: ["true", "false", "on"]
|
||||
empty-lines:
|
||||
max: 1
|
||||
+54
-77
@@ -2,16 +2,14 @@
|
||||
|
||||
## 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.
|
||||
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.
|
||||
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.
|
||||
Please also keep discussions on topic. This project is about a Dalamud chat plugin. Off-topic arguments belong
|
||||
elsewhere.
|
||||
|
||||
---
|
||||
|
||||
@@ -19,88 +17,72 @@ chat plugin. Off-topic arguments belong elsewhere.
|
||||
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
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.
|
||||
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**.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
If something here is being broken, contact me directly. Do not open a public issue.
|
||||
|
||||
| Channel | Address |
|
||||
| ---------- | ------------------------ |
|
||||
| Channel | Address |
|
||||
| ---------- | -------------------------- |
|
||||
| Email | `kontakt@hellion-media.de` |
|
||||
| Discord DM | `@j.j_kazama` |
|
||||
| Discord DM | `@j.j_kazama` |
|
||||
|
||||
Reports stay private. I will acknowledge within a few weekdays
|
||||
(European business hours) and tell you what I plan to do.
|
||||
Reports stay private. I will acknowledge within a few weekdays (European business hours) and tell you what I plan to do.
|
||||
|
||||
## Enforcement
|
||||
|
||||
I am the sole maintainer, so enforcement is a single-person process.
|
||||
I will pick the lightest measure that actually resolves the situation:
|
||||
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.
|
||||
@@ -109,25 +91,20 @@ I will pick the lightest measure that actually resolves the situation:
|
||||
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.
|
||||
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.
|
||||
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
|
||||
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
|
||||
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/).
|
||||
|
||||
+72
-107
@@ -1,91 +1,69 @@
|
||||
# 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.
|
||||
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.
|
||||
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 [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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
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
|
||||
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.
|
||||
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:
|
||||
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)
|
||||
- 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
|
||||
@@ -93,68 +71,56 @@ 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.
|
||||
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`.
|
||||
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 |
|
||||
| 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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
- 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.
|
||||
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 |
|
||||
| ------------- | -------------------------- |
|
||||
| Channel | Address |
|
||||
| ------------- | --------------------------------------- |
|
||||
| GitHub Issues | Preferred for bugs and feature requests |
|
||||
| Discord DM | `@j.j_kazama` |
|
||||
| Email | `kontakt@hellion-media.de` |
|
||||
| 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.
|
||||
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
|
||||
|
||||
@@ -164,10 +130,9 @@ After cloning, run once:
|
||||
./scripts/setup-hooks.sh
|
||||
```
|
||||
|
||||
This wires `core.hooksPath` to `.githooks/`. The pre-push hook runs preflight
|
||||
(versions/manifest/changelog/build).
|
||||
This wires `core.hooksPath` to `.githooks/`. The pre-push hook runs preflight (versions/manifest/changelog/build).
|
||||
|
||||
### Test suite
|
||||
|
||||
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.
|
||||
The plugin's test suite lives in a separate local repository and is not part of this codebase. If you need access for
|
||||
development, contact the maintainer.
|
||||
|
||||
@@ -4,22 +4,22 @@ HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
||||
Source code
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Copyright (c) 2022-2026 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
||||
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) 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.
|
||||
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/
|
||||
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
|
||||
@@ -42,8 +42,8 @@ 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.
|
||||
SIL Open Font License 1.1, full text in HellionFont-OFL.txt.
|
||||
Bundled with permission per the OFL terms.
|
||||
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs.
|
||||
//
|
||||
// Spawns a session-only tab per /tell partner so a club greeter can track
|
||||
// multiple parallel conversations without losing context. Subscribes to
|
||||
// MessageManager.MessageProcessed for live tells and to ClientState.Logout
|
||||
// for the cleanup pass; everything else hangs off these two entry points.
|
||||
//
|
||||
// See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian vault).
|
||||
// Auto-Tell-Tabs: spawns session-only tabs per /tell partner.
|
||||
// Subscribes to MessageManager.MessageProcessed and ClientState.Logout.
|
||||
internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
private readonly Plugin _plugin;
|
||||
@@ -76,7 +70,10 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Code.Type != ChatType.TellIncoming && message.Code.Type != ChatType.TellOutgoing)
|
||||
if (
|
||||
message.Code.Type != ChatType.TellIncoming
|
||||
&& message.Code.Type != ChatType.TellOutgoing
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -84,15 +81,13 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
var partner = ExtractTellPartner(message);
|
||||
if (partner == null)
|
||||
{
|
||||
// Real message without a player payload — e.g. GM tells, which
|
||||
// we deliberately skip. The diagnostics make future regressions
|
||||
// (FFXIV changing tell payload shape, new edge cases) findable
|
||||
// without having to crank up debug logging at the source.
|
||||
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
||||
Plugin.Log.Warning(
|
||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " +
|
||||
$"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " +
|
||||
$"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " +
|
||||
$"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}");
|
||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
||||
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
||||
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
||||
+ $"contentSourcePayloads={message.ContentSource?.Payloads?.Count ?? 0}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,9 +96,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||
if (existing != null)
|
||||
{
|
||||
// Tab already exists; Tab.Matches has already routed this
|
||||
// message via the MessageManager pipeline (see Task 2 sender
|
||||
// filter).
|
||||
// Already routed via MessageManager pipeline
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,12 +113,10 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
if (message.Code.Type == ChatType.TellIncoming)
|
||||
{
|
||||
// Incoming tell: the sender is the conversation partner. The
|
||||
// PlayerPayload normally rides on a chunk's Link slot, but for
|
||||
// some tell types FFXIV only puts it in the raw SeString —
|
||||
// fall back to that before giving up.
|
||||
var fromSender = ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||
// Sender is the partner; check chunks first, then raw SeString as fallback
|
||||
var fromSender =
|
||||
ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||
if (fromSender != null)
|
||||
{
|
||||
return (fromSender.PlayerName, fromSender.World.RowId);
|
||||
@@ -133,21 +124,20 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
// Outgoing tell: the local player is the sender, the partner shows
|
||||
// up either as a payload in the content (for tells typed via the
|
||||
// Chat 2 input bar) or as the channel's tracked tell target (set by
|
||||
// the SetContextTellTarget game hook). Same SeString fallback.
|
||||
var fromContent = ChunkUtil.TryGetPlayerPayload(message.Content)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||
// Outgoing tell: check content first, then channels's TellTarget as fallback
|
||||
var fromContent =
|
||||
ChunkUtil.TryGetPlayerPayload(message.Content)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.ContentSource)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.Sender)
|
||||
?? ChunkUtil.TryGetPlayerPayload(message.SenderSource);
|
||||
if (fromContent != null)
|
||||
{
|
||||
return (fromContent.PlayerName, fromContent.World.RowId);
|
||||
}
|
||||
|
||||
var current = _plugin.CurrentTab.CurrentChannel.TellTarget
|
||||
?? _plugin.CurrentTab.CurrentChannel.TempTellTarget;
|
||||
var current =
|
||||
_plugin.CurrentTab.CurrentChannel.TellTarget
|
||||
?? _plugin.CurrentTab.CurrentChannel.TempTellTarget;
|
||||
if (current != null && current.IsSet())
|
||||
{
|
||||
return (current.Name, current.World);
|
||||
@@ -162,17 +152,15 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
t.IsTempTab
|
||||
&& t.TellTarget != null
|
||||
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
||||
&& t.TellTarget.World == world);
|
||||
&& t.TellTarget.World == world
|
||||
);
|
||||
}
|
||||
|
||||
private void DropOldestTempTab()
|
||||
{
|
||||
// Greeted tabs are dropped before un-greeted ones (the user said
|
||||
// "I'm done with that conversation"), and within each bucket we
|
||||
// pick the oldest LastActivity. This protects active conversations
|
||||
// and unfinished greetings while still freeing up a slot.
|
||||
var victim = Plugin.Config.Tabs
|
||||
.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||
// Prioritize greeted tabs for drop; within each bucket, drop by oldest LastActivity
|
||||
var victim = Plugin
|
||||
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||
.Where(t => t.Tab.IsTempTab)
|
||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||
.ThenBy(t => t.Tab.LastActivity)
|
||||
@@ -183,16 +171,12 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// v0.6.1 — if the victim is currently popped out, tear down the
|
||||
// matching Popout window first. Otherwise the window stays in
|
||||
// PopOutWindows + WindowSystem and renders empty / re-spawns on the
|
||||
// next AddPopOutsToDraw tick. Latent since pop-outs were introduced;
|
||||
// becomes visible with AutoTellTabsOpenAsPopout where dropping a
|
||||
// popped tab is now a routine code path.
|
||||
// Clean up pop-out window if tab is popped out
|
||||
if (victim.Tab.PopOut)
|
||||
{
|
||||
var popout = _plugin.ChatLogWindow.ActivePopouts
|
||||
.FirstOrDefault(p => p.TabIdentifier == victim.Tab.Identifier);
|
||||
var popout = _plugin.ChatLogWindow.ActivePopouts.FirstOrDefault(p =>
|
||||
p.TabIdentifier == victim.Tab.Identifier
|
||||
);
|
||||
if (popout != null)
|
||||
{
|
||||
popout.IsOpen = false;
|
||||
@@ -201,8 +185,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||
|
||||
// Re-anchor the active tab so the user does not silently end up on
|
||||
// a different conversation when their tab gets dropped or shifted.
|
||||
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||
if (victim.Index <= _plugin.LastTab)
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
@@ -213,22 +196,12 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
var tab = BuildTempTab(partner.Name, partner.World);
|
||||
|
||||
// Preload first so the tab opens with chronological history above
|
||||
// the current message — and so a slow DB query never causes a
|
||||
// visible "empty tab, then history pops in" effect on screen.
|
||||
// The current message is already persisted in the store by the
|
||||
// time MessageProcessed fires (see MessageManager.cs: UpsertMessage
|
||||
// runs before the event), so we have to exclude it explicitly to
|
||||
// avoid the separator landing below the live tell.
|
||||
// Preload history: chronological order with current message already persisted
|
||||
PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);
|
||||
|
||||
tab.AddMessage(currentMessage, unread: true);
|
||||
|
||||
// Hellion Chat v0.6.1 — opt-in: open new /tell tabs directly as a
|
||||
// pop-out window. Set BEFORE Tabs.Add so the next render-tick's
|
||||
// AddPopOutsToDraw() sees PopOut=true and spawns the Popout window
|
||||
// alongside the tab going into the list. No SaveConfig() because
|
||||
// auto-tell tabs are IsTempTab (session-only, never persisted).
|
||||
// Open as pop-out if configured (set before Tabs.Add for next render-tick)
|
||||
if (Plugin.Config.AutoTellTabsOpenAsPopout)
|
||||
{
|
||||
tab.PopOut = true;
|
||||
@@ -263,9 +236,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
return $"{playerName}@{worldRow.Name}";
|
||||
}
|
||||
// World sheet lookup miss is rare (only for FFXIV worlds Dalamud has
|
||||
// not yet seen). Fall back to the raw RowId so the user still has a
|
||||
// unique, readable label.
|
||||
// Fallback if world lookup misses (rare; only for unseen worlds)
|
||||
return $"{playerName}@World{worldRowId}";
|
||||
}
|
||||
|
||||
@@ -279,14 +250,13 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
// Pull one extra row because the live tell that triggered this
|
||||
// spawn is already in the store and would otherwise eat one of
|
||||
// the user's preload-budget slots.
|
||||
// Pull one extra row: current message is already in store and would eat a preload slot
|
||||
var history = _store.GetTellHistoryWithSender(
|
||||
_messageManager.CurrentContentId,
|
||||
senderName,
|
||||
senderWorld,
|
||||
preloadCount + 1);
|
||||
preloadCount + 1
|
||||
);
|
||||
|
||||
var historicMessages = history
|
||||
.Where(m => m.Id != currentMessageId)
|
||||
@@ -295,36 +265,30 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
if (historicMessages.Count == 0)
|
||||
{
|
||||
// No prior tells with this player — leave the tab to start
|
||||
// empty so the user does not see a "history loaded" marker
|
||||
// sitting alone above the very first message.
|
||||
// No prior tells; leave tab empty to avoid orphaned "history loaded" marker
|
||||
return;
|
||||
}
|
||||
|
||||
// The history list is already oldest-first, so a plain AddPrune
|
||||
// loop produces the chronological order the user expects to see
|
||||
// when the tab opens.
|
||||
// History is oldest-first; add in order for chronological display
|
||||
foreach (var message in historicMessages)
|
||||
{
|
||||
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
|
||||
}
|
||||
|
||||
// Visible separator between the loaded history and the live
|
||||
// tell that triggered this spawn. Goes in last so it sorts
|
||||
// after the historical messages but before the current one.
|
||||
// Separator between history and live tell (sorts after history but before current)
|
||||
tab.Messages.AddPrune(
|
||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator),
|
||||
MessageManager.MessageDisplayLimit);
|
||||
MessageManager.MessageDisplayLimit
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Non-fatal: the tab still spawns, but the user gets a visible
|
||||
// notice instead of silently missing history. The error logs
|
||||
// once with full stack trace for diagnosis.
|
||||
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
||||
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
|
||||
tab.Messages.AddPrune(
|
||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||
MessageManager.MessageDisplayLimit);
|
||||
MessageManager.MessageDisplayLimit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,9 +324,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
// Frame-race guard (E5): the sidebar might still render a tab
|
||||
// that has already been removed by LRU drop or logout cleanup.
|
||||
// Silently skip the toggle so we don't mutate stale state.
|
||||
// Guard against frame-race: sidebar might render a tab already removed by LRU or logout
|
||||
if (!Plugin.Config.Tabs.Contains(tab))
|
||||
{
|
||||
return;
|
||||
@@ -376,28 +338,24 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
// Snapshot whether the active tab is about to be removed, BEFORE
|
||||
// we mutate the list — index lookups would lie to us afterwards.
|
||||
// Snapshot active tab index before mutating list
|
||||
var lastIndex = _plugin.LastTab;
|
||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
|
||||
|
||||
// v0.6.1 — symmetric to DropOldestTempTab cleanup: tear down any
|
||||
// popped-out temp tab windows before removing the tabs themselves,
|
||||
// otherwise PopOutWindows + WindowSystem keep ghost entries until
|
||||
// the next plugin reload. Especially relevant once Auto-Pop-Out is
|
||||
// enabled — every logout would otherwise leak as many ghosts as
|
||||
// there were active /tell pop-outs.
|
||||
var poppedTempTabIds = Plugin.Config.Tabs
|
||||
.Where(t => t.IsTempTab && t.PopOut)
|
||||
// Clean up pop-out windows before removing temp tabs
|
||||
var poppedTempTabIds = Plugin
|
||||
.Config.Tabs.Where(t => t.IsTempTab && t.PopOut)
|
||||
.Select(t => t.Identifier)
|
||||
.ToList();
|
||||
if (poppedTempTabIds.Count > 0)
|
||||
{
|
||||
var poppedSet = poppedTempTabIds.ToHashSet();
|
||||
foreach (var popout in _plugin.ChatLogWindow.ActivePopouts
|
||||
.Where(p => poppedSet.Contains(p.TabIdentifier))
|
||||
.ToList())
|
||||
foreach (
|
||||
var popout in _plugin
|
||||
.ChatLogWindow.ActivePopouts.Where(p => poppedSet.Contains(p.TabIdentifier))
|
||||
.ToList()
|
||||
)
|
||||
{
|
||||
popout.IsOpen = false;
|
||||
}
|
||||
@@ -405,9 +363,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
|
||||
// Force a switch to tab 0 if the active tab was a temp tab OR
|
||||
// if drops before the active index pushed LastTab out of range.
|
||||
// Otherwise the user keeps their current persistent tab.
|
||||
// Force switch to tab 0 if active tab was temp or index is now out of range
|
||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
if (currentWasTempTab || !stillValid)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// HellionChat/Branding/BrandingLinks.cs
|
||||
namespace HellionChat.Branding;
|
||||
|
||||
// Centralised so a future invite rotation only touches one file. The same
|
||||
// link is currently hard-coded in repo.json, README.md, SUPPORT.md,
|
||||
// CONTRIBUTORS.md and HellionChat.yaml — those will be migrated to consume
|
||||
// this constant in a separate housekeeping sweep
|
||||
// Centralised — a future invite/URL rotation only touches this file.
|
||||
internal static class BrandingLinks
|
||||
{
|
||||
public const string HellionForgeDiscordInvite = "https://discord.gg/X9V7Kcv5gR";
|
||||
public const string HellionForgeGitea = "https://gitea.hellion-forge.cloud/Hellion-Forge";
|
||||
public const string HellionChatRepo =
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
||||
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
||||
public const string HellionMediaWebsite = "https://hellion-media.de/de";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Linq;
|
||||
using HellionChat.Resources;
|
||||
using Dalamud.Plugin;
|
||||
using HellionChat.Resources;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -10,17 +10,19 @@ internal static class ChatTwoConflictDetector
|
||||
|
||||
public static void ThrowIfChatTwoIsLoaded(IDalamudPluginInterface pluginInterface)
|
||||
{
|
||||
var conflict = pluginInterface.InstalledPlugins
|
||||
.FirstOrDefault(p =>
|
||||
p.InternalName == UpstreamInternalName &&
|
||||
p.IsLoaded);
|
||||
var conflict = pluginInterface.InstalledPlugins.FirstOrDefault(p =>
|
||||
p.InternalName == UpstreamInternalName && p.IsLoaded
|
||||
);
|
||||
|
||||
if (conflict is null)
|
||||
return;
|
||||
|
||||
var message = HellionStrings.ChatTwoConflictTitle + "\n\n" +
|
||||
HellionStrings.ChatTwoConflictBody + "\n\n" +
|
||||
HellionStrings.ChatTwoConflictAction;
|
||||
var message =
|
||||
HellionStrings.ChatTwoConflictTitle
|
||||
+ "\n\n"
|
||||
+ HellionStrings.ChatTwoConflictBody
|
||||
+ "\n\n"
|
||||
+ HellionStrings.ChatTwoConflictAction;
|
||||
|
||||
throw new System.InvalidOperationException(message);
|
||||
}
|
||||
|
||||
+41
-27
@@ -1,5 +1,5 @@
|
||||
using HellionChat.Code;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using HellionChat.Code;
|
||||
using MessagePack;
|
||||
|
||||
namespace HellionChat;
|
||||
@@ -25,24 +25,23 @@ public abstract class Chunk
|
||||
Link = link;
|
||||
}
|
||||
|
||||
internal SeString? GetSeString() => Source switch
|
||||
{
|
||||
ChunkSource.None => null,
|
||||
ChunkSource.Sender => Message?.SenderSource,
|
||||
ChunkSource.Content => Message?.ContentSource,
|
||||
_ => null,
|
||||
};
|
||||
internal SeString? GetSeString() =>
|
||||
Source switch
|
||||
{
|
||||
ChunkSource.None => null,
|
||||
ChunkSource.Sender => Message?.SenderSource,
|
||||
ChunkSource.Content => Message?.ContentSource,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get some basic text for use in generating hashes.
|
||||
/// </summary>
|
||||
// Returns basic text for hashing (content for TextChunk, icon name for IconChunk)
|
||||
internal string StringValue()
|
||||
{
|
||||
return this switch
|
||||
{
|
||||
TextChunk text => text.Content,
|
||||
IconChunk icon => icon.Icon.ToString(),
|
||||
_ => ""
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -57,18 +56,29 @@ public enum ChunkSource
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
public class TextChunk : Chunk
|
||||
{
|
||||
[Key(2)] public ChatType? FallbackColour;
|
||||
[Key(3)] public uint? Foreground;
|
||||
[Key(4)] public uint? Glow;
|
||||
[Key(5)] public bool Italic;
|
||||
[Key(6)] public string Content;
|
||||
[Key(2)]
|
||||
public ChatType? FallbackColour;
|
||||
|
||||
private TextChunk(Chunk chunk, string content) : base(chunk.Source, chunk.Link)
|
||||
[Key(3)]
|
||||
public uint? Foreground;
|
||||
|
||||
[Key(4)]
|
||||
public uint? Glow;
|
||||
|
||||
[Key(5)]
|
||||
public bool Italic;
|
||||
|
||||
[Key(6)]
|
||||
public string Content;
|
||||
|
||||
private TextChunk(Chunk chunk, string content)
|
||||
: base(chunk.Source, chunk.Link)
|
||||
{
|
||||
Content = content;
|
||||
}
|
||||
|
||||
internal TextChunk(ChunkSource source, Payload? link, string content) : base(source, link)
|
||||
internal TextChunk(ChunkSource source, Payload? link, string content)
|
||||
: base(source, link)
|
||||
{
|
||||
// This has been null in the past, and it broke rendering code.
|
||||
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
|
||||
@@ -76,7 +86,16 @@ public class TextChunk : Chunk
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global // Used by MessagePack
|
||||
public TextChunk(ChunkSource source, Payload? link, ChatType? fallbackColour, uint? foreground, uint? glow, bool italic, string content) : base(source, link)
|
||||
public TextChunk(
|
||||
ChunkSource source,
|
||||
Payload? link,
|
||||
ChatType? fallbackColour,
|
||||
uint? foreground,
|
||||
uint? glow,
|
||||
bool italic,
|
||||
string content
|
||||
)
|
||||
: base(source, link)
|
||||
{
|
||||
FallbackColour = fallbackColour;
|
||||
Foreground = foreground;
|
||||
@@ -87,9 +106,6 @@ public class TextChunk : Chunk
|
||||
Content = content ?? "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextChunk with identical styling to this one.
|
||||
/// </summary>
|
||||
public TextChunk NewWithStyle(ChunkSource source, Payload? link, string content)
|
||||
{
|
||||
return new TextChunk(source, link, content)
|
||||
@@ -101,9 +117,6 @@ public class TextChunk : Chunk
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextChunk with identical styling to this one.
|
||||
/// </summary>
|
||||
public TextChunk NewWithStyle(Chunk chunk, string content)
|
||||
{
|
||||
return new TextChunk(chunk, content)
|
||||
@@ -122,7 +135,8 @@ public class IconChunk : Chunk
|
||||
[Key(2)]
|
||||
public BitmapFontIcon Icon { get; set; }
|
||||
|
||||
public IconChunk(ChunkSource source, Payload? link, BitmapFontIcon icon) : base(source, link)
|
||||
public IconChunk(ChunkSource source, Payload? link, BitmapFontIcon icon)
|
||||
: base(source, link)
|
||||
{
|
||||
Icon = icon;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class ChatCode
|
||||
}
|
||||
|
||||
public ChatCode(byte type, byte source, byte target)
|
||||
: this((XivChatType)type, (XivChatRelationKind)source, (XivChatRelationKind)target) {}
|
||||
: this((XivChatType)type, (XivChatRelationKind)source, (XivChatRelationKind)target) { }
|
||||
|
||||
public bool IsBattle()
|
||||
{
|
||||
|
||||
@@ -5,24 +5,32 @@ namespace HellionChat.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;
|
||||
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),
|
||||
};
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
+265
-252
@@ -1,92 +1,98 @@
|
||||
using Dalamud.Game.Config;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Game.Config;
|
||||
|
||||
namespace HellionChat.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.
|
||||
];
|
||||
(
|
||||
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)
|
||||
{
|
||||
@@ -143,7 +149,8 @@ internal static class ChatTypeExt
|
||||
ChatType.FreeCompanyAnnouncement => Language.ChatType_FreeCompanyAnnouncement,
|
||||
ChatType.FreeCompanyLoginLogout => Language.ChatType_FreeCompanyLoginLogout,
|
||||
ChatType.RetainerSale => Language.ChatType_RetainerSale,
|
||||
ChatType.PeriodicRecruitmentNotification => Language.ChatType_PeriodicRecruitmentNotification,
|
||||
ChatType.PeriodicRecruitmentNotification =>
|
||||
Language.ChatType_PeriodicRecruitmentNotification,
|
||||
ChatType.Sign => Language.ChatType_Sign,
|
||||
ChatType.RandomNumber => Language.ChatType_RandomNumber,
|
||||
ChatType.NoviceNetworkSystem => Language.ChatType_NoviceNetworkSystem,
|
||||
@@ -306,181 +313,187 @@ internal static class ChatTypeExt
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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,
|
||||
};
|
||||
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,
|
||||
};
|
||||
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,
|
||||
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,
|
||||
};
|
||||
// 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,
|
||||
};
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
+153
-145
@@ -4,111 +4,114 @@ namespace HellionChat.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),
|
||||
};
|
||||
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 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 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)
|
||||
{
|
||||
@@ -145,51 +148,56 @@ internal static class InputChannelExt
|
||||
if (ids.Length == 0)
|
||||
return null;
|
||||
|
||||
return ids.Where(id => Sheets.TextCommandSheet.HasRow(id)).Select(id => Sheets.TextCommandSheet.GetRow(id));
|
||||
return ids.Where(id => Sheets.TextCommandSheet.HasRow(id))
|
||||
.Select(id => Sheets.TextCommandSheet.GetRow(id));
|
||||
}
|
||||
|
||||
internal static bool IsLinkshell(this InputChannel channel) => channel switch
|
||||
{
|
||||
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 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 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 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,
|
||||
};
|
||||
internal static bool IsValid(this InputChannel channel) =>
|
||||
channel switch
|
||||
{
|
||||
InputChannel.Invalid => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
+13
-6
@@ -16,15 +16,22 @@ internal sealed class Commands : IDisposable
|
||||
{
|
||||
foreach (var wrapper in Registered.Values)
|
||||
{
|
||||
Plugin.CommandManager.AddHandler(wrapper.Name, new CommandInfo(Invoke)
|
||||
{
|
||||
HelpMessage = wrapper.Description ?? string.Empty,
|
||||
ShowInHelp = wrapper.ShowInHelp,
|
||||
});
|
||||
Plugin.CommandManager.AddHandler(
|
||||
wrapper.Name,
|
||||
new CommandInfo(Invoke)
|
||||
{
|
||||
HelpMessage = wrapper.Description ?? string.Empty,
|
||||
ShowInHelp = wrapper.ShowInHelp,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
internal CommandWrapper Register(string name, string? description = null, bool? showInHelp = null)
|
||||
internal CommandWrapper Register(
|
||||
string name,
|
||||
string? description = null,
|
||||
bool? showInHelp = null
|
||||
)
|
||||
{
|
||||
if (Registered.TryGetValue(name, out var wrapper))
|
||||
{
|
||||
|
||||
+233
-293
@@ -1,14 +1,14 @@
|
||||
using System.Collections;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ConfigKeyBind
|
||||
modString += Language.Keybind_Modifier_Shift + " + ";
|
||||
if (Modifier.HasFlag(ModifierFlag.Alt))
|
||||
modString += Language.Keybind_Modifier_Alt + " + ";
|
||||
return modString+Key.GetFancyName();
|
||||
return modString + Key.GetFancyName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,31 +38,26 @@ public class Configuration : IPluginConfiguration
|
||||
|
||||
public int Version { get; set; } = LatestVersion;
|
||||
|
||||
// v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
|
||||
// Slug-based; ThemeRegistry resolves the object at runtime.
|
||||
public string Theme = "hellion-arctic";
|
||||
|
||||
// v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
|
||||
// HellionThemeWindowOpacity beim Bump v13 → v14.
|
||||
// Global window opacity, applied across all themes.
|
||||
public float WindowOpacity = 0.85f;
|
||||
|
||||
|
||||
// v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden
|
||||
// vorab angelegt, damit später keine Migration nötig ist.
|
||||
// Reserved for future UI toggles; pre-declared to avoid a migration later.
|
||||
public bool ReduceMotion;
|
||||
// v1.2.1 — Default geflippt von false → true. Card-Rows-Layout aus
|
||||
// v1.2.0 wurde als zu dicht empfunden; Single-Line `[HH:mm] Sender:
|
||||
// Text` ist besser lesbar und platzsparender. Bestand-User mit aktiv
|
||||
// false werden durch die v15→v16-Migration auf den neuen Default
|
||||
// gehoben (Heuristik: wer in v1.2.0 false hatte, hatte den damals
|
||||
// neu eingeführten Default — kaum jemand hat aktiv abgeschaltet).
|
||||
|
||||
// v1.2.1: default flipped false → true. Compact single-line layout is
|
||||
// more readable than the card-rows layout introduced in v1.2.0.
|
||||
public bool UseCompactDensity = true;
|
||||
|
||||
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
||||
// Master-switch defaults to true; set false to restore upstream behavior.
|
||||
// Privacy by Default master switch. Set false to restore upstream behaviour.
|
||||
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.
|
||||
|
||||
// Failsafe for ChatTypes added by future FFXIV patches.
|
||||
public bool PrivacyPersistUnknownChannels;
|
||||
|
||||
public bool IsAllowedForStorage(ChatType type)
|
||||
@@ -74,75 +69,23 @@ public class Configuration : IPluginConfiguration
|
||||
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.
|
||||
// Retention master switch defaults to false — 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// Cycle 1 of the plugin-integration roadmap. When Honorific is installed
|
||||
// and reports a custom title, render it in the chat header above the
|
||||
// message log. Auto-hides regardless when Honorific is missing or the
|
||||
// active title is original/empty, so leaving this on is safe even for
|
||||
// users who don't run Honorific.
|
||||
public bool ShowHonorificTitleInHeader = true;
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing
|
||||
// /tell spawns a session-only tab dedicated to that conversation
|
||||
// partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian).
|
||||
public bool EnableAutoTellTabs = true;
|
||||
// Hard cap on simultaneously open auto tell tabs. Range enforced by the
|
||||
// settings slider (1–50). LRU drop favors greeted tabs first.
|
||||
public int AutoTellTabsLimit = 15;
|
||||
// When true the sidebar shows only a thin separator before the temp
|
||||
// tabs; when false a section header "Active Tells (n)" is rendered.
|
||||
public bool AutoTellTabsCompactDisplay;
|
||||
// Number of prior tells to preload from the message store when an
|
||||
// auto tell tab is spawned. Range 0–100; 0 disables preload.
|
||||
public int AutoTellTabsHistoryPreload = 20;
|
||||
// Show the greeter "marked-as-greeted" toggle button next to each
|
||||
// temp tab and dim the tab name when set. Off by default because the
|
||||
// workflow is specific to club-greeter use cases — most users just
|
||||
// want the auto tabs themselves without the extra UI affordance.
|
||||
public bool AutoTellTabsShowGreetedToggle;
|
||||
|
||||
// Hellion Chat — One-Time-Hint-Banner that introduces the v0.6.0 pop-out
|
||||
// input feature. Set to true once the user dismisses the banner from a
|
||||
// pop-out window; never reset after that.
|
||||
public bool SeenPopOutInputHint;
|
||||
|
||||
// Hellion Chat — v0.6.0 master switch for the pop-out input bar.
|
||||
// Global on purpose: per-tab makes no sense for Auto-Tell-Tabs which
|
||||
// are session-only and would force the user to re-enable it for every
|
||||
// new conversation. Default flipped to ON in v0.6.1 (was OFF in v0.6.0)
|
||||
// because tester feedback called the manual toggle "umständlich, wirkt
|
||||
// unfertig". v11 → v12 migration applies the same flip to existing users.
|
||||
public bool PopOutInputEnabled = true;
|
||||
|
||||
// Hellion Chat — v0.6.1 One-Time-Hint-Banner that introduces the
|
||||
// chat-header pop-out toolbar button and reminds about the pop-out
|
||||
// input default flip. Set to true once the user dismisses the banner
|
||||
// from the main chat window; never reset after that.
|
||||
public bool SeenPopOutHeaderHint;
|
||||
|
||||
// Hellion Chat — v0.6.1 opt-in: when true, AutoTellTabsService.SpawnTempTab
|
||||
// sets tab.PopOut = true on every new auto-tell tab so the conversation
|
||||
// pops out as its own window directly. Closing the pop-out returns the
|
||||
// tab to the sidebar via the standard Popout.OnClose() flow. Default OFF
|
||||
// because the existing sidebar workflow is what most users (especially
|
||||
// club greeters tracking many parallel tells) expect by default.
|
||||
public bool AutoTellTabsOpenAsPopout;
|
||||
|
||||
public int GetRetentionDays(ChatType type)
|
||||
@@ -160,10 +103,8 @@ public class Configuration : IPluginConfiguration
|
||||
public bool HideWhenUiHidden = true;
|
||||
public bool HideInLoadingScreens;
|
||||
public bool HideInBattle;
|
||||
// v1.2.1 — Default geflippt false → true. Hellion-UI im NG+-Menü
|
||||
// versteckt zu halten ist konsistent mit den anderen Hide-Defaults
|
||||
// (Cutscenes, Logged-out, UI-Hidden) — UI-out-of-the-way bei Story-
|
||||
// Sequenzen.
|
||||
|
||||
// v1.2.1: default flipped false → true for consistency with other hide defaults.
|
||||
public bool HideInNewGamePlusMenu = true;
|
||||
public bool HideWhenInactive;
|
||||
public int InactivityHideTimeout = 10;
|
||||
@@ -179,16 +120,8 @@ public class Configuration : IPluginConfiguration
|
||||
public bool NativeItemTooltips = true;
|
||||
public bool PrettierTimestamps = true;
|
||||
public bool MoreCompactPretty;
|
||||
// v1.2.1 — Default geflippt false → true. Wiederholte Zeitstempel
|
||||
// innerhalb derselben Minute lesen sich als Rauschen; ein einziger
|
||||
// Timestamp pro Minute reicht aus um die Konversation zu verorten.
|
||||
public bool HideSameTimestamps = true;
|
||||
public bool ShowNoviceNetwork;
|
||||
// Hellion Chat — vertical sidebar tab layout reads better than the
|
||||
// horizontal tab strip in the company of Auto-Tell-Tabs (a club
|
||||
// greeter typically tracks 5–15 simultaneous conversations). Bestand
|
||||
// users keep their saved value untouched — only fresh installs pick
|
||||
// up the new default.
|
||||
public bool SidebarTabView = true;
|
||||
public bool PrintChangelog = true;
|
||||
public bool OnlyPreviewIf;
|
||||
@@ -209,20 +142,10 @@ public class Configuration : IPluginConfiguration
|
||||
public bool CollapseKeepUniqueLinks;
|
||||
public bool PlaySounds = true;
|
||||
public bool KeepInputFocus = true;
|
||||
// v1.2.1 — Default gesenkt 5000 → 2500. 5000 ist auf Mid-Range-
|
||||
// Hardware bei langen Sessions spürbar langsamer (Card-Layout
|
||||
// re-Layout pro Frame), 2500 deckt eine typische Stunde Chat ab
|
||||
// und bleibt smooth. User die mehr brauchen können bis 10000 hoch.
|
||||
public int MaxLinesToRender = 2_500; // 1-10000
|
||||
// Default ON to match a German / European 24h locale. The
|
||||
// ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via
|
||||
// CultureInfo.InvariantCulture so the result is consistent across
|
||||
// host locales.
|
||||
public bool Use24HourClock = true;
|
||||
|
||||
public bool ShowEmotes = true;
|
||||
public HashSet<string> BlockedEmotes = [];
|
||||
|
||||
public bool FontsEnabled = true;
|
||||
public ExtraGlyphRanges ExtraGlyphRanges = 0;
|
||||
public float FontSizeV2 = 12.75f;
|
||||
@@ -246,18 +169,15 @@ public class Configuration : IPluginConfiguration
|
||||
};
|
||||
|
||||
public float TooltipOffset;
|
||||
// v1.2.1 — Default-Chat-Farben sind das Hellion-Brand-Preset. Der
|
||||
// First-Run-Wizard bietet keine Theme-/Preset-Wahl an, daher kriegen
|
||||
// neue User die Hellion-Brand-Farben out-of-the-box (Cyan-Familie für
|
||||
// Standard/Tell, Ember/Warning für laute Channels). Bestand-User mit
|
||||
// leerem ChatColours-Dict werden durch die v15→v16-Migration auf das
|
||||
// Preset gehoben; User die bereits Custom-Farben haben, bleiben.
|
||||
|
||||
public Dictionary<ChatType, uint> ChatColours = BuildDefaultChatColours();
|
||||
|
||||
private static Dictionary<ChatType, uint> BuildDefaultChatColours()
|
||||
{
|
||||
var defaults = new Dictionary<ChatType, uint>();
|
||||
foreach (var (channel, colour) in HellionChat.Resources.ChatColourPresets.All["Hellion"].Colours)
|
||||
foreach (
|
||||
var (channel, colour) in HellionChat.Resources.ChatColourPresets.All["Hellion"].Colours
|
||||
)
|
||||
defaults[channel] = colour;
|
||||
return defaults;
|
||||
}
|
||||
@@ -284,7 +204,10 @@ public class Configuration : IPluginConfiguration
|
||||
HideWhenInactive = other.HideWhenInactive;
|
||||
InactivityHideTimeout = other.InactivityHideTimeout;
|
||||
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
||||
InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(
|
||||
pair => pair.Key,
|
||||
pair => pair.Value
|
||||
);
|
||||
InactivityHideExtraChatAll = other.InactivityHideExtraChatAll;
|
||||
InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet();
|
||||
ShowHideButton = other.ShowHideButton;
|
||||
@@ -316,9 +239,7 @@ public class Configuration : IPluginConfiguration
|
||||
MaxLinesToRender = other.MaxLinesToRender;
|
||||
Use24HourClock = other.Use24HourClock;
|
||||
ShowEmotes = other.ShowEmotes;
|
||||
// Deep-copy the set so the live and mutable Configuration instances don't share state
|
||||
// — a HashSet reference assignment would cause edits in the settings window to leak
|
||||
// into the live config before the user clicks Save.
|
||||
// Deep-copy so settings window edits don't leak into live config before Save.
|
||||
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
|
||||
FontsEnabled = other.FontsEnabled;
|
||||
ItalicEnabled = other.ItalicEnabled;
|
||||
@@ -332,49 +253,43 @@ public class Configuration : IPluginConfiguration
|
||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
||||
// never present in a disk-loaded copy. Keep the live temp tabs of
|
||||
// *this* configuration alive across an UpdateFrom so a settings
|
||||
// save (or sidebar-mode toggle) does not silently destroy the
|
||||
// user's open tell conversations.
|
||||
//
|
||||
// For persistent tabs we go through Tab.Clone() which intentionally
|
||||
// does NOT copy the NonSerialized Messages list (avoids shared
|
||||
// mutable state on disk-load). On a settings save that means the
|
||||
// chat history for every persistent tab would be wiped — bug
|
||||
// reported by Flo 2026-05-05. We work around it by capturing the
|
||||
// live MessageList (and LastSendUnread counter) by Identifier
|
||||
// before the replace, then restoring it onto the freshly cloned
|
||||
// tabs whose Identifier survives Tab.Clone(). New tabs added in
|
||||
// settings get a fresh empty MessageList; deleted tabs lose their
|
||||
// history (intended).
|
||||
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||
// not destroy open tell conversations. For persistent tabs, capture
|
||||
// the live MessageList and LastSendUnread by Identifier before the
|
||||
// replace and restore them onto the freshly cloned tabs; new tabs
|
||||
// get an empty MessageList, deleted tabs lose their history (intended).
|
||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
||||
var livePersistentSession = Tabs
|
||||
.Where(t => !t.IsTempTab)
|
||||
var livePersistentSession = Tabs.Where(t => !t.IsTempTab)
|
||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
|
||||
|
||||
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t =>
|
||||
{
|
||||
var clone = t.Clone();
|
||||
if (livePersistentSession.TryGetValue(clone.Identifier, out var live))
|
||||
Tabs = other
|
||||
.Tabs.Where(t => !t.IsTempTab)
|
||||
.Select(t =>
|
||||
{
|
||||
clone.Messages = live.Messages;
|
||||
clone.LastSendUnread = live.LastSendUnread;
|
||||
}
|
||||
return clone;
|
||||
}).ToList();
|
||||
var clone = t.Clone();
|
||||
if (livePersistentSession.TryGetValue(clone.Identifier, out var live))
|
||||
{
|
||||
clone.Messages = live.Messages;
|
||||
clone.LastSendUnread = live.LastSendUnread;
|
||||
}
|
||||
return clone;
|
||||
})
|
||||
.ToList();
|
||||
Tabs.AddRange(liveTempTabs);
|
||||
|
||||
ChatTabForward = other.ChatTabForward;
|
||||
ChatTabBackward = other.ChatTabBackward;
|
||||
|
||||
PrivacyFilterEnabled = other.PrivacyFilterEnabled;
|
||||
PrivacyPersistChannels = [..other.PrivacyPersistChannels];
|
||||
PrivacyPersistChannels = [.. other.PrivacyPersistChannels];
|
||||
PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels;
|
||||
|
||||
RetentionEnabled = other.RetentionEnabled;
|
||||
RetentionDefaultDays = other.RetentionDefaultDays;
|
||||
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value);
|
||||
RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(
|
||||
p => p.Key,
|
||||
p => p.Value
|
||||
);
|
||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||
|
||||
FirstRunCompleted = other.FirstRunCompleted;
|
||||
@@ -410,21 +325,23 @@ public enum UnreadMode
|
||||
|
||||
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 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,
|
||||
};
|
||||
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]
|
||||
@@ -432,9 +349,7 @@ public class Tab
|
||||
{
|
||||
public string Name = Language.Tab_DefaultName;
|
||||
|
||||
// v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet:
|
||||
// Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name).
|
||||
// User können hier per Settings → Tabs einen eigenen Glyph setzen.
|
||||
// Optional FontAwesome glyph name; null falls back to TabIconMapping default.
|
||||
public string? Icon = null;
|
||||
|
||||
[Obsolete("Removed in favor of SelectedChannels")]
|
||||
@@ -468,44 +383,57 @@ public class Tab
|
||||
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 uint Unread;
|
||||
|
||||
[NonSerialized] public UsedChannel CurrentChannel = new();
|
||||
[NonSerialized]
|
||||
public uint LastSendUnread;
|
||||
|
||||
[NonSerialized] public Guid Identifier = Guid.NewGuid();
|
||||
[NonSerialized]
|
||||
public long LastActivity;
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the
|
||||
// sidebar to mark a tell partner as already greeted in the current
|
||||
// session. NonSerialized because the temp tab itself is session-only.
|
||||
[NonSerialized] public bool IsGreeted;
|
||||
[NonSerialized]
|
||||
public MessageList Messages = new();
|
||||
|
||||
// v1.4.2 — TabTintCache uses separate validation keys per cache so a
|
||||
// TellTarget change picked up by GetTint can't strand GetIcon (or vice
|
||||
// versa) with a stale entry that looks fresh on the shared key.
|
||||
[NonSerialized] internal string? _cachedTintTellName;
|
||||
[NonSerialized] internal uint _cachedTintTellWorld;
|
||||
[NonSerialized] internal uint _cachedTellTint;
|
||||
[NonSerialized] internal string? _cachedIconTellName;
|
||||
[NonSerialized] internal uint _cachedIconTellWorld;
|
||||
[NonSerialized] internal string? _cachedTellIcon;
|
||||
[NonSerialized]
|
||||
public UsedChannel CurrentChannel = new();
|
||||
|
||||
[NonSerialized]
|
||||
public Guid Identifier = Guid.NewGuid();
|
||||
|
||||
// Session-only greeted flag for club-greeter workflows.
|
||||
[NonSerialized]
|
||||
public bool IsGreeted;
|
||||
|
||||
// Separate validation keys per cache so TellTarget changes don't
|
||||
// cause GetTint and GetIcon to strand each other with stale entries.
|
||||
[NonSerialized]
|
||||
internal string? _cachedTintTellName;
|
||||
|
||||
[NonSerialized]
|
||||
internal uint _cachedTintTellWorld;
|
||||
|
||||
[NonSerialized]
|
||||
internal uint _cachedTellTint;
|
||||
|
||||
[NonSerialized]
|
||||
internal string? _cachedIconTellName;
|
||||
|
||||
[NonSerialized]
|
||||
internal uint _cachedIconTellWorld;
|
||||
|
||||
[NonSerialized]
|
||||
internal string? _cachedTellIcon;
|
||||
|
||||
public bool Matches(Message message)
|
||||
{
|
||||
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Auto-tell temp tabs are bound to a single conversation partner;
|
||||
// every other tell that matches the channel filter must NOT land
|
||||
// here, otherwise all temp tabs would mirror "Tell Exclusive".
|
||||
// Temp tabs are bound to a single conversation partner — other tells
|
||||
// matching the channel filter must not land here.
|
||||
if (IsTempTab && TellTarget?.IsSet() == true)
|
||||
{
|
||||
return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -517,12 +445,17 @@ public class Tab
|
||||
return;
|
||||
|
||||
Unread += 1;
|
||||
if (message.Matches(Plugin.Config.InactivityHideChannelsV2, Plugin.Config.InactivityHideExtraChatAll, Plugin.Config.InactivityHideExtraChatChannels))
|
||||
if (
|
||||
message.Matches(
|
||||
Plugin.Config.InactivityHideChannelsV2,
|
||||
Plugin.Config.InactivityHideExtraChatAll,
|
||||
Plugin.Config.InactivityHideExtraChatChannels
|
||||
)
|
||||
)
|
||||
LastActivity = Environment.TickCount64;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
=> Messages.Clear();
|
||||
public void Clear() => Messages.Clear();
|
||||
|
||||
public Tab Clone()
|
||||
{
|
||||
@@ -560,10 +493,7 @@ public class Tab
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MessageList provides an ordered list of messages with duplicate ID
|
||||
/// tracking, sorting and mutex protection.
|
||||
/// </summary>
|
||||
/// Ordered message list with duplicate ID tracking, sorting and mutex protection.
|
||||
public class MessageList
|
||||
{
|
||||
private readonly SemaphoreSlim LockSlim = new(1, 1);
|
||||
@@ -651,23 +581,24 @@ public class Tab
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read
|
||||
/// ist OK für 1×/sec Status-Bar-Polling (v1.2.0).
|
||||
/// </summary>
|
||||
/// Current message count. Lock-per-read is acceptable for 1×/sec status bar polling.
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
LockSlim.Wait(-1);
|
||||
try { return Messages.Count; }
|
||||
finally { LockSlim.Release(); }
|
||||
try
|
||||
{
|
||||
return Messages.Count;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LockSlim.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array copy of the message list for usage outside of main thread
|
||||
/// </summary>
|
||||
/// Returns an array copy of the message list for usage outside of main thread.
|
||||
public async Task<Message[]> GetCopy(int millisecondsTimeout = -1)
|
||||
{
|
||||
await LockSlim.WaitAsync(millisecondsTimeout);
|
||||
@@ -681,17 +612,16 @@ public class Tab
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GetReadOnly returns a read-only list of messages while holding a
|
||||
/// reader lock. The list should be used with a using statement.
|
||||
/// </summary>
|
||||
/// Returns a read-only list while holding a reader lock. Use with a using statement.
|
||||
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 class RLockedMessageList(SemaphoreSlim lockSlim, List<Message> messages)
|
||||
: IReadOnlyList<Message>,
|
||||
IDisposable
|
||||
{
|
||||
public IEnumerator<Message> GetEnumerator()
|
||||
{
|
||||
@@ -750,15 +680,16 @@ public enum PreviewPosition
|
||||
|
||||
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),
|
||||
};
|
||||
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]
|
||||
@@ -771,13 +702,14 @@ public enum CommandHelpSide
|
||||
|
||||
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),
|
||||
};
|
||||
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]
|
||||
@@ -789,19 +721,21 @@ public enum KeybindMode
|
||||
|
||||
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 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,
|
||||
};
|
||||
public static string? Tooltip(this KeybindMode mode) =>
|
||||
mode switch
|
||||
{
|
||||
KeybindMode.Flexible => Language.KeybindMode_Flexible_Tooltip,
|
||||
KeybindMode.Strict => Language.KeybindMode_Strict_Tooltip,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -830,49 +764,51 @@ public enum LanguageOverride
|
||||
|
||||
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 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),
|
||||
};
|
||||
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]
|
||||
@@ -890,27 +826,31 @@ public enum ExtraGlyphRanges
|
||||
|
||||
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 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),
|
||||
};
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
+79
-52
@@ -2,10 +2,10 @@
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Textures;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
@@ -15,11 +15,31 @@ public static class EmoteCache
|
||||
{
|
||||
private static readonly string[] NotWorking =
|
||||
[
|
||||
":tf:", "(ditto)", "c!", "h!", "l!", "M&Mjc", "LUL3D", "p!",
|
||||
"POLICE2", "r!", "Pussy", "s!", "v!", "w!", "x0r6ztGiggle",
|
||||
"z!", "xar2EDM", "iron95Pls", "Clap2", "AlienPls3", "Life",
|
||||
"peepoPogClimbingTreeHard4House", "monkaGIGAftRobertDowneyJr",
|
||||
"DogLookingSussyAndCold", "DICKS"
|
||||
":tf:",
|
||||
"(ditto)",
|
||||
"c!",
|
||||
"h!",
|
||||
"l!",
|
||||
"M&Mjc",
|
||||
"LUL3D",
|
||||
"p!",
|
||||
"POLICE2",
|
||||
"r!",
|
||||
"Pussy",
|
||||
"s!",
|
||||
"v!",
|
||||
"w!",
|
||||
"x0r6ztGiggle",
|
||||
"z!",
|
||||
"xar2EDM",
|
||||
"iron95Pls",
|
||||
"Clap2",
|
||||
"AlienPls3",
|
||||
"Life",
|
||||
"peepoPogClimbingTreeHard4House",
|
||||
"monkaGIGAftRobertDowneyJr",
|
||||
"DogLookingSussyAndCold",
|
||||
"DICKS",
|
||||
];
|
||||
|
||||
private static readonly HttpClient Client = new();
|
||||
@@ -56,10 +76,10 @@ public static class EmoteCache
|
||||
{
|
||||
Unloaded,
|
||||
Loading,
|
||||
Done
|
||||
Done,
|
||||
}
|
||||
|
||||
// All of this data is uninitalized while State is not `LoadingState.Done`
|
||||
// All fields below are uninitialised while State != Done.
|
||||
public static LoadingState State = LoadingState.Unloaded;
|
||||
|
||||
private static readonly Dictionary<string, Emote> Cache = new();
|
||||
@@ -67,24 +87,25 @@ public static class EmoteCache
|
||||
|
||||
public static string[] SortedCodeArray = [];
|
||||
|
||||
// Plugin-scoped cancellation source for in-flight emote loads. Dispose
|
||||
// cancels every running download/texture-create so the workers don't
|
||||
// touch a torn-down TextureProvider on plugin reload. Replaced with a
|
||||
// fresh source on the next LoadData() call so a re-enable still works.
|
||||
// Cancelled on Dispose to stop in-flight downloads; replaced on re-enable.
|
||||
private static CancellationTokenSource Cts = new();
|
||||
internal static CancellationToken Token => Cts.Token;
|
||||
|
||||
// Drain target for in-flight loads on Dispose; without this an orphan
|
||||
// continuation could still write to a torn-down Texture/Frames field.
|
||||
// Tracks in-flight loads so Dispose can drain them before teardown.
|
||||
private static readonly ConcurrentBag<Task> PendingLoads = new();
|
||||
|
||||
internal static void TrackLoad(Task loadTask, string emoteCode)
|
||||
{
|
||||
PendingLoads.Add(loadTask.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Plugin.Log.Error(t.Exception!, $"EmoteCache load failed for {emoteCode}");
|
||||
}, TaskScheduler.Default));
|
||||
PendingLoads.Add(
|
||||
loadTask.ContinueWith(
|
||||
t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Plugin.Log.Error(t.Exception!, $"EmoteCache load failed for {emoteCode}");
|
||||
},
|
||||
TaskScheduler.Default
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task LoadData()
|
||||
@@ -92,8 +113,7 @@ public static class EmoteCache
|
||||
if (State is not LoadingState.Unloaded)
|
||||
return;
|
||||
|
||||
// Refresh the CTS in case Dispose was called and we're being re-enabled
|
||||
// in the same process (Dalamud /xlplugins toggle).
|
||||
// Reset CTS if Dispose was called and the plugin is being re-enabled.
|
||||
if (Cts.IsCancellationRequested)
|
||||
Cts = new CancellationTokenSource();
|
||||
|
||||
@@ -115,13 +135,13 @@ public static class EmoteCache
|
||||
var topList = await top.Content.ReadAsStringAsync(ct);
|
||||
|
||||
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
||||
// BetterTTV occasionally returns entries with a null Code; the
|
||||
// upstream code passed those straight into Dictionary.TryAdd
|
||||
// and tripped ArgumentNullException, killing the whole emote
|
||||
// load. Skip them defensively so a single bad row no longer
|
||||
// breaks the cache for everyone else.
|
||||
// BetterTTV occasionally returns entries with a null Code;
|
||||
// skip them so a single bad row doesn't break the whole cache.
|
||||
foreach (var emote in jsonList)
|
||||
if (!string.IsNullOrEmpty(emote.Emote.Code) && !NotWorking.Contains(emote.Emote.Code))
|
||||
if (
|
||||
!string.IsNullOrEmpty(emote.Emote.Code)
|
||||
&& !NotWorking.Contains(emote.Emote.Code)
|
||||
)
|
||||
Cache.TryAdd(emote.Emote.Code, emote.Emote);
|
||||
|
||||
lastId = jsonList.Last().Id;
|
||||
@@ -132,16 +152,11 @@ public static class EmoteCache
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Plugin disposed while the cache was loading; leave State on
|
||||
// Loading so a subsequent re-enable can re-issue LoadData with
|
||||
// a fresh CTS (handled above).
|
||||
// Plugin disposed mid-load; State stays on Loading so re-enable can retry.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Reset to Unloaded so a later trigger (e.g. the user reopening
|
||||
// the Emotes tab after the network recovers) can retry. Without
|
||||
// this the State stays on Loading and the early-out at the top
|
||||
// of LoadData blocks every further attempt until plugin reload.
|
||||
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
||||
State = LoadingState.Unloaded;
|
||||
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
|
||||
}
|
||||
@@ -220,18 +235,21 @@ public static class EmoteCache
|
||||
|
||||
internal async Task<byte[]> LoadAsync(Emote emote, CancellationToken ct)
|
||||
{
|
||||
// BetterTTV-supplied Id and ImageType are interpolated straight
|
||||
// into the filename. HTTPS protects the wire, but a compromised
|
||||
// upstream could still hand us "../foo" and write into the
|
||||
// pluginConfigs root (or worse). Resolve the candidate path and
|
||||
// refuse anything that escapes the cache directory.
|
||||
var dir = Path.GetFullPath(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1"));
|
||||
// Path-traversal guard: resolve and verify the candidate path stays
|
||||
// inside the cache directory before reading or writing.
|
||||
var dir = Path.GetFullPath(
|
||||
Path.Join(Plugin.Interface.ConfigDirectory.FullName, "EmoteCacheV1")
|
||||
);
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var dirPrefix = dir.EndsWith(Path.DirectorySeparatorChar) ? dir : dir + Path.DirectorySeparatorChar;
|
||||
var dirPrefix = dir.EndsWith(Path.DirectorySeparatorChar)
|
||||
? dir
|
||||
: dir + Path.DirectorySeparatorChar;
|
||||
var filePath = Path.GetFullPath(Path.Join(dir, $"{emote.Id}.{emote.ImageType}"));
|
||||
if (!filePath.StartsWith(dirPrefix, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException($"Emote path escapes cache directory: id={emote.Id}, type={emote.ImageType}");
|
||||
throw new InvalidOperationException(
|
||||
$"Emote path escapes cache directory: id={emote.Id}, type={emote.ImageType}"
|
||||
);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
@@ -242,7 +260,12 @@ public static class EmoteCache
|
||||
var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct);
|
||||
RawData = await content.Content.ReadAsByteArrayAsync(ct);
|
||||
|
||||
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
await using var stream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read
|
||||
);
|
||||
await stream.WriteAsync(RawData, ct);
|
||||
}
|
||||
|
||||
@@ -271,12 +294,13 @@ public static class EmoteCache
|
||||
return;
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image, cancellationToken: ct);
|
||||
Texture = await Plugin.TextureProvider.CreateFromImageAsync(
|
||||
image,
|
||||
cancellationToken: ct
|
||||
);
|
||||
IsLoaded = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Failed = true;
|
||||
@@ -357,13 +381,17 @@ public static class EmoteCache
|
||||
|
||||
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
||||
|
||||
// Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
|
||||
// Match browser behaviour: anything under 20ms rounds up to 100ms.
|
||||
if (delay < 0.02f)
|
||||
delay = 0.1f;
|
||||
|
||||
var buffer = new byte[4 * frame.Width * frame.Height];
|
||||
frame.CopyPixelDataTo(buffer);
|
||||
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer, cancellationToken: ct);
|
||||
var tex = await Plugin.TextureProvider.CreateFromRawAsync(
|
||||
RawImageSpecification.Rgba32(frame.Width, frame.Height),
|
||||
buffer,
|
||||
cancellationToken: ct
|
||||
);
|
||||
frames.Add((tex, delay));
|
||||
}
|
||||
|
||||
@@ -372,8 +400,7 @@ public static class EmoteCache
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Plugin disposed mid-load; partial frames are released by
|
||||
// InnerDispose on the next dispose pass.
|
||||
// Plugin disposed mid-load; release any partial frames.
|
||||
foreach (var f in Frames)
|
||||
f.Texture.Dispose();
|
||||
Frames = [];
|
||||
@@ -385,4 +412,4 @@ public static class EmoteCache
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,23 @@ internal enum ExportFormat
|
||||
|
||||
internal static class ExportFormatExt
|
||||
{
|
||||
internal static string Extension(this ExportFormat fmt) => fmt switch
|
||||
{
|
||||
ExportFormat.Markdown => "md",
|
||||
ExportFormat.Json => "json",
|
||||
ExportFormat.Csv => "csv",
|
||||
_ => "txt",
|
||||
};
|
||||
internal static string Extension(this ExportFormat fmt) =>
|
||||
fmt switch
|
||||
{
|
||||
ExportFormat.Markdown => "md",
|
||||
ExportFormat.Json => "json",
|
||||
ExportFormat.Csv => "csv",
|
||||
_ => "txt",
|
||||
};
|
||||
|
||||
internal static string Filter(this ExportFormat fmt) => fmt switch
|
||||
{
|
||||
ExportFormat.Markdown => ".md",
|
||||
ExportFormat.Json => ".json",
|
||||
ExportFormat.Csv => ".csv",
|
||||
_ => ".txt",
|
||||
};
|
||||
internal static string Filter(this ExportFormat fmt) =>
|
||||
fmt switch
|
||||
{
|
||||
ExportFormat.Markdown => ".md",
|
||||
ExportFormat.Json => ".json",
|
||||
ExportFormat.Csv => ".csv",
|
||||
_ => ".txt",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,13 +44,15 @@ internal static class MessageExporter
|
||||
IReadOnlyCollection<int>? ChatTypes,
|
||||
DateTimeOffset? From,
|
||||
DateTimeOffset? To,
|
||||
string? SenderSubstring);
|
||||
string? SenderSubstring
|
||||
);
|
||||
|
||||
internal static int ExportToFile(
|
||||
string path,
|
||||
ExportFormat format,
|
||||
IEnumerable<Message> messages,
|
||||
FilterDescription filter)
|
||||
FilterDescription filter
|
||||
)
|
||||
{
|
||||
var matching = filter.SenderSubstring is { Length: > 0 } needle
|
||||
? messages.Where(m => MatchesSender(m, needle))
|
||||
@@ -64,10 +68,14 @@ internal static class MessageExporter
|
||||
};
|
||||
}
|
||||
|
||||
private static bool MatchesSender(Message m, string needle)
|
||||
=> m.SenderSource.TextValue.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
private static bool MatchesSender(Message m, string needle) =>
|
||||
m.SenderSource.TextValue.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static int WriteMarkdown(StreamWriter w, IEnumerable<Message> messages, FilterDescription filter)
|
||||
private static int WriteMarkdown(
|
||||
StreamWriter w,
|
||||
IEnumerable<Message> messages,
|
||||
FilterDescription filter
|
||||
)
|
||||
{
|
||||
w.WriteLine("# Hellion Chat Export");
|
||||
w.WriteLine();
|
||||
@@ -107,7 +115,9 @@ internal static class MessageExporter
|
||||
private static void WriteFilterSummaryMarkdown(StreamWriter w, FilterDescription filter)
|
||||
{
|
||||
if (filter.ChatTypes is { Count: > 0 })
|
||||
w.WriteLine($"ChatTypes: {string.Join(", ", filter.ChatTypes.Select(t => $"{(ChatType)(ushort)t}({t})"))}");
|
||||
w.WriteLine(
|
||||
$"ChatTypes: {string.Join(", ", filter.ChatTypes.Select(t => $"{(ChatType)(ushort)t}({t})"))}"
|
||||
);
|
||||
if (filter.From is not null)
|
||||
w.WriteLine($"From: {filter.From.Value.ToLocalTime():yyyy-MM-dd HH:mm}");
|
||||
if (filter.To is not null)
|
||||
@@ -116,7 +126,11 @@ internal static class MessageExporter
|
||||
w.WriteLine($"Sender contains: \"{filter.SenderSubstring}\"");
|
||||
}
|
||||
|
||||
private static int WriteJson(StreamWriter w, IEnumerable<Message> messages, FilterDescription filter)
|
||||
private static int WriteJson(
|
||||
StreamWriter w,
|
||||
IEnumerable<Message> messages,
|
||||
FilterDescription filter
|
||||
)
|
||||
{
|
||||
// Manual JSON to avoid pulling in System.Text.Json policy choices.
|
||||
// Output is a single object with metadata and an array of messages.
|
||||
@@ -130,9 +144,17 @@ internal static class MessageExporter
|
||||
else
|
||||
w.Write("null");
|
||||
w.Write(",\n \"from\": ");
|
||||
w.Write(filter.From is null ? "null" : "\"" + filter.From.Value.ToString("O", CultureInfo.InvariantCulture) + "\"");
|
||||
w.Write(
|
||||
filter.From is null
|
||||
? "null"
|
||||
: "\"" + filter.From.Value.ToString("O", CultureInfo.InvariantCulture) + "\""
|
||||
);
|
||||
w.Write(",\n \"to\": ");
|
||||
w.Write(filter.To is null ? "null" : "\"" + filter.To.Value.ToString("O", CultureInfo.InvariantCulture) + "\"");
|
||||
w.Write(
|
||||
filter.To is null
|
||||
? "null"
|
||||
: "\"" + filter.To.Value.ToString("O", CultureInfo.InvariantCulture) + "\""
|
||||
);
|
||||
w.Write(",\n \"sender_substring\": ");
|
||||
w.Write(filter.SenderSubstring is null ? "null" : JsonString(filter.SenderSubstring));
|
||||
w.Write("\n },\n \"messages\": [\n");
|
||||
@@ -166,7 +188,11 @@ internal static class MessageExporter
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int WriteCsv(StreamWriter w, IEnumerable<Message> messages, FilterDescription filter)
|
||||
private static int WriteCsv(
|
||||
StreamWriter w,
|
||||
IEnumerable<Message> messages,
|
||||
FilterDescription filter
|
||||
)
|
||||
{
|
||||
// Header line always written so empty exports are still importable.
|
||||
w.WriteLine("Date,ChatType,ChatTypeName,Sender,Content,Receiver,ContentId");
|
||||
@@ -201,13 +227,27 @@ internal static class MessageExporter
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\b': sb.Append("\\b"); break;
|
||||
case '\f': sb.Append("\\f"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
case '"':
|
||||
sb.Append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
sb.Append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
sb.Append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
sb.Append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
sb.Append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
sb.Append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
sb.Append("\\t");
|
||||
break;
|
||||
default:
|
||||
if (c < 0x20)
|
||||
sb.Append($"\\u{(int)c:x4}");
|
||||
|
||||
+106
-82
@@ -1,10 +1,10 @@
|
||||
using Dalamud;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -23,17 +23,25 @@ public class FontManager
|
||||
|
||||
public static readonly HashSet<float> AxisFontSizeList =
|
||||
[
|
||||
9.6f, 10f, 12f, 14f, 16f,
|
||||
18f, 18.4f, 20f, 23f, 34f,
|
||||
36f, 40f, 45f, 46f, 68f, 90f,
|
||||
9.6f,
|
||||
10f,
|
||||
12f,
|
||||
14f,
|
||||
16f,
|
||||
18f,
|
||||
18.4f,
|
||||
20f,
|
||||
23f,
|
||||
34f,
|
||||
36f,
|
||||
40f,
|
||||
45f,
|
||||
46f,
|
||||
68f,
|
||||
90f,
|
||||
];
|
||||
|
||||
/// <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>
|
||||
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
||||
private static byte[]? HellionFontBytes;
|
||||
|
||||
private static byte[] GetHellionFontBytes()
|
||||
@@ -41,8 +49,11 @@ public class FontManager
|
||||
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 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();
|
||||
@@ -54,11 +65,9 @@ public class FontManager
|
||||
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)
|
||||
@@ -66,8 +75,8 @@ public class FontManager
|
||||
if (chars[i] == 0)
|
||||
break;
|
||||
|
||||
for (var j = (uint) chars[i]; j <= chars[i + 1]; j++)
|
||||
builder.AddChar((ushort) j);
|
||||
for (var j = (uint)chars[i]; j <= chars[i + 1]; j++)
|
||||
builder.AddChar((ushort)j);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +94,7 @@ public class FontManager
|
||||
|
||||
// "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((char)i);
|
||||
|
||||
builder.AddChar('⓪');
|
||||
return builder.BuildRangesToArray();
|
||||
@@ -100,13 +109,7 @@ public class FontManager
|
||||
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async wrapper around <see cref="BuildFonts"/> for the Phase-1 LoadAsync
|
||||
/// path. The font-atlas build is CPU-bound, so we offload via Task.Run and
|
||||
/// honour the cancellation token at the scheduling boundary; this lets the
|
||||
/// font build run in parallel with the theme init without blocking the
|
||||
/// loader. Settings-driven manual rebuilds keep using the sync entry point.
|
||||
/// </summary>
|
||||
// CPU-bound build offloaded to Task.Run; runs parallel with theme init
|
||||
public async Task BuildFontsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -117,66 +120,79 @@ public class FontManager
|
||||
{
|
||||
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
|
||||
});
|
||||
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.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 =>
|
||||
RegularFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
||||
e.OnPreBuild(tk =>
|
||||
{
|
||||
// v1.2.0: UseHellionFont controls font size selection
|
||||
var basePt = Plugin.Config.UseHellionFont
|
||||
? Plugin.Config.FontSizeV2
|
||||
: Plugin.Config.GlobalFontV2.SizePt;
|
||||
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
||||
config.MergeFont = Plugin.Config.UseHellionFont
|
||||
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||
|
||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||
config.GlyphRanges = JpRange;
|
||||
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
|
||||
|
||||
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
|
||||
tk.AddGameSymbol(config);
|
||||
|
||||
tk.Font = config.MergeFont;
|
||||
})
|
||||
);
|
||||
|
||||
if (Plugin.Config.ItalicEnabled)
|
||||
{
|
||||
ItalicFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
||||
e.OnPreBuild(tk =>
|
||||
{
|
||||
// v1.2.0 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font)
|
||||
// wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet.
|
||||
// Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
|
||||
// Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei
|
||||
// UseHellionFont=true wirkungslos, was 4K-User mit größerer
|
||||
// Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten).
|
||||
var basePt = Plugin.Config.UseHellionFont
|
||||
? Plugin.Config.FontSizeV2
|
||||
: Plugin.Config.GlobalFontV2.SizePt;
|
||||
var config = new SafeFontConfig {SizePt = basePt, GlyphRanges = Ranges};
|
||||
config.MergeFont = Plugin.Config.UseHellionFont
|
||||
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||
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");
|
||||
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
|
||||
{
|
||||
@@ -184,28 +200,36 @@ public class FontManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
// Add font with fallback to NotoSansCjkRegular if unavailable
|
||||
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)
|
||||
catch (Exception e)
|
||||
when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
|
||||
{
|
||||
Plugin.Log.Warning(e, $"Configured {slot} font unavailable, falling back to NotoSansCjkRegular");
|
||||
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);
|
||||
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,8 +1,4 @@
|
||||
using System.Text;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Game.Config;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
@@ -17,9 +13,12 @@ using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using InteropGenerator.Runtime;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
@@ -28,20 +27,55 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
// Functions
|
||||
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")]
|
||||
private readonly delegate* unmanaged<RaptureLogModule*, ushort, Utf8String*, Utf8String*, ulong, ulong, ushort, byte, int, byte, void> PrintTellNative = null!;
|
||||
private readonly delegate* unmanaged<
|
||||
RaptureLogModule*,
|
||||
ushort,
|
||||
Utf8String*,
|
||||
Utf8String*,
|
||||
ulong,
|
||||
ulong,
|
||||
ushort,
|
||||
byte,
|
||||
int,
|
||||
byte,
|
||||
void> PrintTellNative = null!;
|
||||
|
||||
[Signature("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8C 24 ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 ?? 48 8B 8C 24")]
|
||||
private readonly delegate* unmanaged<NetworkModule*, ulong, ushort, Utf8String*, Utf8String*, ushort, ushort, byte> SendTellNative = null!;
|
||||
[Signature(
|
||||
"E8 ?? ?? ?? ?? 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8C 24 ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 ?? 48 8B 8C 24"
|
||||
)]
|
||||
private readonly delegate* unmanaged<
|
||||
NetworkModule*,
|
||||
ulong,
|
||||
ushort,
|
||||
Utf8String*,
|
||||
Utf8String*,
|
||||
ushort,
|
||||
ushort,
|
||||
byte> SendTellNative = null!;
|
||||
|
||||
// Client::UI::AddonChatLog.OnRefresh
|
||||
[Signature("40 53 57 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4D 8B F8", DetourName = nameof(ChatLogRefreshDetour))]
|
||||
[Signature(
|
||||
"40 53 57 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 4D 8B F8",
|
||||
DetourName = nameof(ChatLogRefreshDetour)
|
||||
)]
|
||||
private Hook<ChatLogRefreshDelegate>? ChatLogRefreshHook = null!;
|
||||
private delegate byte ChatLogRefreshDelegate(nint log, ushort eventId, AtkValue* value);
|
||||
|
||||
// Replace with CS version later
|
||||
[Signature("48 89 5C 24 ?? 55 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 83 B9", DetourName = nameof(ContextMenuTellInForayDetour))]
|
||||
[Signature(
|
||||
"48 89 5C 24 ?? 55 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 83 B9",
|
||||
DetourName = nameof(ContextMenuTellInForayDetour)
|
||||
)]
|
||||
private Hook<ContextMenuTellInForayDelegate>? ContextMenuTellInForayHook = null!;
|
||||
private delegate void ContextMenuTellInForayDelegate(RaptureShellModule* module, Utf8String* playerName, Utf8String* worldName, ushort worldId, ulong accountId, ulong contentId, ushort reason);
|
||||
private delegate void ContextMenuTellInForayDelegate(
|
||||
RaptureShellModule* module,
|
||||
Utf8String* playerName,
|
||||
Utf8String* worldName,
|
||||
ushort worldId,
|
||||
ulong accountId,
|
||||
ulong contentId,
|
||||
ushort reason
|
||||
);
|
||||
|
||||
private readonly Hook<AgentChatLog.Delegates.ChangeChannelName>? ChangeChannelNameHook;
|
||||
private readonly Hook<RaptureShellModule.Delegates.ReplyInSelectedChatMode>? ReplyInSelectedChatModeHook;
|
||||
@@ -58,7 +92,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
FullName = 0,
|
||||
SurnameAbbreviated = 1,
|
||||
ForenameAbbreviated = 2,
|
||||
Initials = 3
|
||||
Initials = 3,
|
||||
}
|
||||
|
||||
private long LastPlayerNameDisplayTypeRefresh;
|
||||
@@ -72,13 +106,25 @@ internal sealed unsafe class Chat : IDisposable
|
||||
ChatLogRefreshHook?.Enable();
|
||||
ContextMenuTellInForayHook?.Enable();
|
||||
|
||||
ChangeChannelNameHook = Plugin.GameInteropProvider.HookFromAddress<AgentChatLog.Delegates.ChangeChannelName>(AgentChatLog.MemberFunctionPointers.ChangeChannelName, ChangeChannelNameDetour);
|
||||
ChangeChannelNameHook =
|
||||
Plugin.GameInteropProvider.HookFromAddress<AgentChatLog.Delegates.ChangeChannelName>(
|
||||
AgentChatLog.MemberFunctionPointers.ChangeChannelName,
|
||||
ChangeChannelNameDetour
|
||||
);
|
||||
ChangeChannelNameHook.Enable();
|
||||
|
||||
ReplyInSelectedChatModeHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.ReplyInSelectedChatMode>(RaptureShellModule.MemberFunctionPointers.ReplyInSelectedChatMode, ReplyInSelectedChatModeDetour);
|
||||
ReplyInSelectedChatModeHook =
|
||||
Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.ReplyInSelectedChatMode>(
|
||||
RaptureShellModule.MemberFunctionPointers.ReplyInSelectedChatMode,
|
||||
ReplyInSelectedChatModeDetour
|
||||
);
|
||||
ReplyInSelectedChatModeHook.Enable();
|
||||
|
||||
SetChatLogTellTargetHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTarget>(RaptureShellModule.MemberFunctionPointers.SetContextTellTarget, SetContextTellTarget);
|
||||
SetChatLogTellTargetHook =
|
||||
Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTarget>(
|
||||
RaptureShellModule.MemberFunctionPointers.SetContextTellTarget,
|
||||
SetContextTellTarget
|
||||
);
|
||||
SetChatLogTellTargetHook.Enable();
|
||||
|
||||
Plugin.ClientState.Login += Login;
|
||||
@@ -108,12 +154,13 @@ internal sealed unsafe class Chat : IDisposable
|
||||
return utf == null ? null : utf->ToString();
|
||||
}
|
||||
|
||||
private static int GetRotateIdx(RotateMode mode) => mode switch
|
||||
{
|
||||
RotateMode.Forward => 1,
|
||||
RotateMode.Reverse => -1,
|
||||
_ => 0,
|
||||
};
|
||||
private static int GetRotateIdx(RotateMode mode) =>
|
||||
mode switch
|
||||
{
|
||||
RotateMode.Forward => 1,
|
||||
RotateMode.Reverse => -1,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
internal static void RotateLinkshellHistory(RotateMode mode)
|
||||
{
|
||||
@@ -174,7 +221,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
string? input = null;
|
||||
|
||||
var utf8Bytes = MemoryHelper.ReadRaw((nint)LastTypedCharacter+0x4, 2);
|
||||
var utf8Bytes = MemoryHelper.ReadRaw((nint)LastTypedCharacter + 0x4, 2);
|
||||
var chars = Encoding.UTF8.GetString(utf8Bytes).ToCharArray();
|
||||
if (chars.Length == 0)
|
||||
return;
|
||||
@@ -185,7 +232,9 @@ internal sealed unsafe class Chat : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, });
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input }
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -197,7 +246,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
string? addIfNotPresent = null;
|
||||
|
||||
var str = value + 2;
|
||||
if (str != null && ((int) str->Type & 0xF) == (int) ValueType.String && str->String.HasValue)
|
||||
if (str != null && ((int)str->Type & 0xF) == (int)ValueType.String && str->String.HasValue)
|
||||
{
|
||||
var add = str->String.ToString();
|
||||
if (add.Length > 0)
|
||||
@@ -214,7 +263,12 @@ internal sealed unsafe class Chat : IDisposable
|
||||
return ChatLogRefreshHook!.Original(log, eventId, value);
|
||||
}
|
||||
|
||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, });
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(new ChannelSwitchInfo(null))
|
||||
{
|
||||
AddIfNotPresent = addIfNotPresent,
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -231,9 +285,9 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (agent == null)
|
||||
return ret;
|
||||
|
||||
var channel = (uint) RaptureShellModule.Instance()->ChatType;
|
||||
var channel = (uint)RaptureShellModule.Instance()->ChatType;
|
||||
if (channel is 17 or 18)
|
||||
channel = (uint) InputChannel.Tell;
|
||||
channel = (uint)InputChannel.Tell;
|
||||
|
||||
var name = SeString.Parse(agent->ChannelLabel);
|
||||
if (name.Payloads.Count == 0)
|
||||
@@ -248,7 +302,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
|
||||
string? playerName = null;
|
||||
ushort worldId = 0;
|
||||
if (channel == (uint) InputChannel.Tell)
|
||||
if (channel == (uint)InputChannel.Tell)
|
||||
{
|
||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||
worldId = agent->TellWorldId;
|
||||
@@ -257,9 +311,9 @@ internal sealed unsafe class Chat : IDisposable
|
||||
|
||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||
{
|
||||
Channel = (InputChannel) channel,
|
||||
Channel = (InputChannel)channel,
|
||||
Name = nameChunks,
|
||||
TellTarget = playerName != null ? new TellTarget(playerName, worldId, 0, 0) : null
|
||||
TellTarget = playerName != null ? new TellTarget(playerName, worldId, 0, 0) : null,
|
||||
};
|
||||
|
||||
return ret;
|
||||
@@ -274,22 +328,40 @@ internal sealed unsafe class Chat : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
SetChannel((InputChannel) replyMode);
|
||||
SetChannel((InputChannel)replyMode);
|
||||
ReplyInSelectedChatModeHook!.Original(agent);
|
||||
}
|
||||
|
||||
private bool SetContextTellTarget(RaptureShellModule* a1, Utf8String* playerName, Utf8String* worldName, ushort worldId, ulong accountId, ulong contentId, ushort reason, bool setChatType)
|
||||
private bool SetContextTellTarget(
|
||||
RaptureShellModule* a1,
|
||||
Utf8String* playerName,
|
||||
Utf8String* worldName,
|
||||
ushort worldId,
|
||||
ulong accountId,
|
||||
ulong contentId,
|
||||
ushort reason,
|
||||
bool setChatType
|
||||
)
|
||||
{
|
||||
if (playerName != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason);
|
||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell, permanent: setChatType))
|
||||
{
|
||||
TellReason = (TellReason) reason,
|
||||
TellTarget = target,
|
||||
});
|
||||
var target = new TellTarget(
|
||||
playerName->ToString(),
|
||||
worldId,
|
||||
contentId,
|
||||
(TellReason)reason
|
||||
);
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(
|
||||
new ChannelSwitchInfo(InputChannel.Tell, permanent: setChatType)
|
||||
)
|
||||
{
|
||||
TellReason = (TellReason)reason,
|
||||
TellTarget = target,
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -297,10 +369,27 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
return SetChatLogTellTargetHook!.Original(a1, playerName, worldName, worldId, accountId, contentId, reason, setChatType);
|
||||
return SetChatLogTellTargetHook!.Original(
|
||||
a1,
|
||||
playerName,
|
||||
worldName,
|
||||
worldId,
|
||||
accountId,
|
||||
contentId,
|
||||
reason,
|
||||
setChatType
|
||||
);
|
||||
}
|
||||
|
||||
private void ContextMenuTellInForayDetour(RaptureShellModule* a1, Utf8String* playerName, Utf8String* worldName, ushort worldId, ulong accountId, ulong contentId, ushort reason)
|
||||
private void ContextMenuTellInForayDetour(
|
||||
RaptureShellModule* a1,
|
||||
Utf8String* playerName,
|
||||
Utf8String* worldName,
|
||||
ushort worldId,
|
||||
ulong accountId,
|
||||
ulong contentId,
|
||||
ushort reason
|
||||
)
|
||||
{
|
||||
if (!Plugin.CurrentTab.CurrentChannel.UseTempChannel)
|
||||
Plugin.CurrentTab.CurrentChannel.UseTempChannel = true;
|
||||
@@ -309,13 +398,20 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason);
|
||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell))
|
||||
{
|
||||
TellReason = (TellReason) reason,
|
||||
TellTarget = target,
|
||||
TellSpecial = Sheets.IsInForay(), // Handle Eureka/Bozja special
|
||||
});
|
||||
var target = new TellTarget(
|
||||
playerName->ToString(),
|
||||
worldId,
|
||||
contentId,
|
||||
(TellReason)reason
|
||||
);
|
||||
Plugin.ChatLogWindow.Activated(
|
||||
new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell))
|
||||
{
|
||||
TellReason = (TellReason)reason,
|
||||
TellTarget = target,
|
||||
TellSpecial = Sheets.IsInForay(), // Handle Eureka/Bozja special
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -323,7 +419,15 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenuTellInForayHook!.Original(a1, playerName, worldName, worldId, accountId, contentId, reason);
|
||||
ContextMenuTellInForayHook!.Original(
|
||||
a1,
|
||||
playerName,
|
||||
worldName,
|
||||
worldId,
|
||||
accountId,
|
||||
contentId,
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -346,17 +450,22 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
if (idx > 7)
|
||||
return false;
|
||||
return InfoProxyLinkshell.Instance()->LinkShells[(int) idx].Id != 0;
|
||||
return InfoProxyLinkshell.Instance()->LinkShells[(int)idx].Id != 0;
|
||||
}
|
||||
|
||||
internal static bool ValidCrossLinkshell(uint idx)
|
||||
{
|
||||
if (idx > 7)
|
||||
return false;
|
||||
return InfoProxyCrossWorldLinkshell.Instance()->CrossWorldLinkshells[(int) idx].Name.Length > 0;
|
||||
return InfoProxyCrossWorldLinkshell.Instance()->CrossWorldLinkshells[(int)idx].Name.Length
|
||||
> 0;
|
||||
}
|
||||
|
||||
private static uint? RotateLinkshell(uint currentIndex, RotateMode rotate, Func<uint, bool> validFn)
|
||||
private static uint? RotateLinkshell(
|
||||
uint currentIndex,
|
||||
RotateMode rotate,
|
||||
Func<uint, bool> validFn
|
||||
)
|
||||
{
|
||||
if (rotate == RotateMode.None)
|
||||
return null;
|
||||
@@ -365,13 +474,13 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
RotateMode.Forward => 1,
|
||||
RotateMode.Reverse => -1,
|
||||
_ => 1
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
// Iterate up to 8 times to find a valid linkshell.
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
currentIndex = (uint) ((8 + currentIndex + delta) % 8);
|
||||
currentIndex = (uint)((8 + currentIndex + delta) % 8);
|
||||
if (validFn(currentIndex))
|
||||
return currentIndex;
|
||||
}
|
||||
@@ -379,27 +488,40 @@ internal sealed unsafe class Chat : IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static InputChannel? ResolveTempInputChannel(InputChannel? currentTempChannel, InputChannel channel, RotateMode rotate)
|
||||
internal static InputChannel? ResolveTempInputChannel(
|
||||
InputChannel? currentTempChannel,
|
||||
InputChannel channel,
|
||||
RotateMode rotate
|
||||
)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case InputChannel.Linkshell1 or InputChannel.CrossLinkshell1 when rotate != RotateMode.None:
|
||||
case InputChannel.Linkshell1
|
||||
or InputChannel.CrossLinkshell1 when rotate != RotateMode.None:
|
||||
{
|
||||
var module = UIModule.Instance();
|
||||
|
||||
var currentIndex = channel is InputChannel.Linkshell1 ? (uint) module->LinkshellCycle : (uint) module->CrossWorldLinkshellCycle;
|
||||
var currentIndex =
|
||||
channel is InputChannel.Linkshell1
|
||||
? (uint)module->LinkshellCycle
|
||||
: (uint)module->CrossWorldLinkshellCycle;
|
||||
if (currentTempChannel != null)
|
||||
{
|
||||
switch (channel)
|
||||
{
|
||||
case InputChannel.Linkshell1 when currentTempChannel.Value.IsLinkshell():
|
||||
case InputChannel.CrossLinkshell1 when currentTempChannel.Value.IsCrossLinkshell():
|
||||
case InputChannel.CrossLinkshell1
|
||||
when currentTempChannel.Value.IsCrossLinkshell():
|
||||
currentIndex = currentTempChannel.Value.LinkshellIndex();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell);
|
||||
var idx = RotateLinkshell(
|
||||
currentIndex,
|
||||
rotate,
|
||||
channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell
|
||||
);
|
||||
// RotateLinkshell returns null when no valid linkshell is found within 8 iterations.
|
||||
// Forward the null so the caller can keep the existing channel instead of crashing on nullable arithmetic.
|
||||
return idx is null ? null : channel + idx.Value;
|
||||
@@ -427,11 +549,21 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (!ValidAnyLinkshell(channel))
|
||||
return;
|
||||
|
||||
RaptureShellModule.Instance()->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||
RaptureShellModule
|
||||
.Instance()
|
||||
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||
target->Dtor(true);
|
||||
}
|
||||
|
||||
internal void SetEurekaTellChannel(string name, string worldName, ushort worldId, ulong accountId, ulong objectId, ushort reason, bool setChatType)
|
||||
internal void SetEurekaTellChannel(
|
||||
string name,
|
||||
string worldName,
|
||||
ushort worldId,
|
||||
ulong accountId,
|
||||
ulong objectId,
|
||||
ushort reason,
|
||||
bool setChatType
|
||||
)
|
||||
{
|
||||
// param6 is 0 for contentId and 1 for objectId
|
||||
// param7 is always 0 ?
|
||||
@@ -446,7 +578,17 @@ internal sealed unsafe class Chat : IDisposable
|
||||
var utfName = Utf8String.FromString(name);
|
||||
var utfWorld = Utf8String.FromString(worldName);
|
||||
|
||||
RaptureShellModule.Instance()->SetTellTargetInForay(utfName, utfWorld, worldId, accountId, objectId, reason, setChatType);
|
||||
RaptureShellModule
|
||||
.Instance()
|
||||
->SetTellTargetInForay(
|
||||
utfName,
|
||||
utfWorld,
|
||||
worldId,
|
||||
accountId,
|
||||
objectId,
|
||||
reason,
|
||||
setChatType
|
||||
);
|
||||
|
||||
utfName->Dtor(true);
|
||||
utfWorld->Dtor(true);
|
||||
@@ -475,19 +617,30 @@ internal sealed unsafe class Chat : IDisposable
|
||||
mes->Dtor(true);
|
||||
}
|
||||
|
||||
internal void SendTell(TellReason reason, ulong contentId, string name, ushort homeWorld, byte[] message, string rawText)
|
||||
internal void SendTell(
|
||||
TellReason reason,
|
||||
ulong contentId,
|
||||
string name,
|
||||
ushort homeWorld,
|
||||
byte[] message,
|
||||
string rawText
|
||||
)
|
||||
{
|
||||
if (contentId == 0)
|
||||
{
|
||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||
Plugin.Log.Warning("Tried to send a tell with ContentId being 0, sorry this is an internal error.");
|
||||
Plugin.Log.Warning(
|
||||
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var uName = Utf8String.FromString(name);
|
||||
var uMessage = Utf8String.FromSequence(message.NullTerminate());
|
||||
|
||||
var encoded = Utf8String.FromUtf8String(PronounModule.Instance()->ProcessString(uMessage, true));
|
||||
var encoded = Utf8String.FromUtf8String(
|
||||
PronounModule.Instance()->ProcessString(uMessage, true)
|
||||
);
|
||||
var decoded = EncodeMessage(rawText);
|
||||
AutoTranslate.ReplaceWithPayload(ref decoded);
|
||||
|
||||
@@ -500,9 +653,28 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (reason == TellReason.Direct)
|
||||
reason = TellReason.Friend;
|
||||
|
||||
var ok = SendTellNative(networkModule, contentId, homeWorld, uName, encoded, (ushort) reason, homeWorld);
|
||||
var ok = SendTellNative(
|
||||
networkModule,
|
||||
contentId,
|
||||
homeWorld,
|
||||
uName,
|
||||
encoded,
|
||||
(ushort)reason,
|
||||
homeWorld
|
||||
);
|
||||
if (ok == 1)
|
||||
PrintTellNative(logModule, 33, uName, &decodedUtf8String, 0, contentId, homeWorld, 255, 0, 0);
|
||||
PrintTellNative(
|
||||
logModule,
|
||||
33,
|
||||
uName,
|
||||
&decodedUtf8String,
|
||||
0,
|
||||
contentId,
|
||||
homeWorld,
|
||||
255,
|
||||
0,
|
||||
0
|
||||
);
|
||||
else
|
||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||
|
||||
@@ -511,7 +683,8 @@ internal sealed unsafe class Chat : IDisposable
|
||||
uMessage->Dtor(true);
|
||||
}
|
||||
|
||||
private static byte[] EncodeMessage(string str) {
|
||||
private static byte[] EncodeMessage(string str)
|
||||
{
|
||||
using var input = new Utf8String(str);
|
||||
using var output = new Utf8String();
|
||||
|
||||
@@ -524,7 +697,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
var uC = Utf8String.FromString(c.ToString());
|
||||
|
||||
uC->SanitizeString((AllowedEntities) 0x27F);
|
||||
uC->SanitizeString((AllowedEntities)0x27F);
|
||||
var wasValid = uC->ToString().Length > 0;
|
||||
|
||||
uC->Dtor(true);
|
||||
@@ -537,7 +710,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
var ok = Plugin.GameConfig.TryGet(UiConfigOption.LogNameType, out uint type);
|
||||
if (!ok || !Enum.IsDefined(typeof(PlayerNameDisplayType), type))
|
||||
return PlayerNameDisplayType.FullName;
|
||||
return (PlayerNameDisplayType) type;
|
||||
return (PlayerNameDisplayType)type;
|
||||
}
|
||||
|
||||
internal string AbbreviatePlayerName(string playerName)
|
||||
@@ -557,10 +730,13 @@ internal sealed unsafe class Chat : IDisposable
|
||||
|
||||
return CurrentPlayerNameDisplayType switch
|
||||
{
|
||||
PlayerNameDisplayType.SurnameAbbreviated => $"{split.First()} {split.Last().FirstOrDefault('A')}.",
|
||||
PlayerNameDisplayType.ForenameAbbreviated => $"{split.First().FirstOrDefault('A')}. {split.Last()}",
|
||||
PlayerNameDisplayType.Initials => $"{split.First().FirstOrDefault('A')}. {split.Last().FirstOrDefault('A')}.",
|
||||
_ => playerName
|
||||
PlayerNameDisplayType.SurnameAbbreviated =>
|
||||
$"{split.First()} {split.Last().FirstOrDefault('A')}.",
|
||||
PlayerNameDisplayType.ForenameAbbreviated =>
|
||||
$"{split.First().FirstOrDefault('A')}. {split.Last()}",
|
||||
PlayerNameDisplayType.Initials =>
|
||||
$"{split.First().FirstOrDefault('A')}. {split.Last().FirstOrDefault('A')}.",
|
||||
_ => playerName,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -571,6 +747,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
// second before the cutscene actually starts, because the game sets
|
||||
// the cutscene conditions before processing the skip.
|
||||
var raptureAtkUnitManager = RaptureAtkUnitManager.Instance();
|
||||
return raptureAtkUnitManager == null || raptureAtkUnitManager->UiFlags.HasFlag(UiFlags.Chat);
|
||||
return raptureAtkUnitManager == null
|
||||
|| raptureAtkUnitManager->UiFlags.HasFlag(UiFlags.Chat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Text;
|
||||
using HellionChat.Resources;
|
||||
using Dalamud.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using HellionChat.Resources;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
|
||||
@@ -27,7 +27,10 @@ public unsafe class ChatBox
|
||||
// Utf8String->SanitizeString, which only resolves in-process. Returns the
|
||||
// already-encoded bytes so SendMessage doesn't pay GetBytes twice.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/GameFunctions/ChatBoxTests.cs
|
||||
internal static byte[] ValidateMessage(string message, Func<string, string>? sanitiserOverride = null)
|
||||
internal static byte[] ValidateMessage(
|
||||
string message,
|
||||
Func<string, string>? sanitiserOverride = null
|
||||
)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
if (bytes.Length == 0)
|
||||
@@ -47,10 +50,10 @@ public unsafe class ChatBox
|
||||
{
|
||||
var uText = Utf8String.FromString(text);
|
||||
|
||||
uText->SanitizeString((AllowedEntities) 0x27F);
|
||||
uText->SanitizeString((AllowedEntities)0x27F);
|
||||
var sanitised = uText->ToString();
|
||||
uText->Dtor(true);
|
||||
|
||||
return sanitised;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using HellionChat.Util;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
|
||||
@@ -10,7 +10,9 @@ internal sealed unsafe class Context
|
||||
internal static void InviteToNoviceNetwork(string name, ushort world)
|
||||
{
|
||||
// can specify content id if we have it, but there's no need
|
||||
InfoProxyNoviceNetwork.Instance()->InviteToNoviceNetwork(0, 0, world, name.ToTerminatedBytes());
|
||||
InfoProxyNoviceNetwork
|
||||
.Instance()
|
||||
->InviteToNoviceNetwork(0, 0, world, name.ToTerminatedBytes());
|
||||
}
|
||||
|
||||
internal static void TryOn(uint itemId, byte stainId)
|
||||
|
||||
@@ -23,9 +23,17 @@ internal unsafe class GameFunctions : IDisposable
|
||||
internal const string NewGamePlusAddonName = "QuestRedo";
|
||||
|
||||
#region Hooks
|
||||
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
|
||||
[Signature(
|
||||
"E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F",
|
||||
DetourName = nameof(ResolveTextCommandPlaceholderDetour)
|
||||
)]
|
||||
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
|
||||
private delegate nint ResolveTextCommandPlaceholderDelegate(nint a1, byte* placeholderText, byte a3, byte a4);
|
||||
private delegate nint ResolveTextCommandPlaceholderDelegate(
|
||||
nint a1,
|
||||
byte* placeholderText,
|
||||
byte a3,
|
||||
byte a4
|
||||
);
|
||||
#endregion
|
||||
|
||||
private Plugin Plugin { get; }
|
||||
@@ -81,7 +89,8 @@ internal unsafe class GameFunctions : IDisposable
|
||||
ChatBox.SendMessage($"/{commandName} add {Placeholder}");
|
||||
}
|
||||
|
||||
private static T* GetAddon<T>(string name) where T : unmanaged
|
||||
private static T* GetAddon<T>(string name)
|
||||
where T : unmanaged
|
||||
{
|
||||
var addon = RaptureAtkModule.Instance()->RaptureAtkUnitManager.GetAddonByName(name);
|
||||
return addon != null && addon->IsReady ? (T*)addon : null;
|
||||
@@ -164,14 +173,15 @@ internal unsafe class GameFunctions : IDisposable
|
||||
{
|
||||
var addonId = lfg->GetAddonId();
|
||||
var atkModule = RaptureAtkModule.Instance();
|
||||
var atkModuleVtbl = (void**) atkModule->AtkModule.VirtualTable;
|
||||
var vf27 = (delegate* unmanaged<RaptureAtkModule*, ulong, ulong, byte>) atkModuleVtbl[27];
|
||||
var atkModuleVtbl = (void**)atkModule->AtkModule.VirtualTable;
|
||||
var vf27 = (delegate* unmanaged<RaptureAtkModule*, ulong, ulong, byte>)
|
||||
atkModuleVtbl[27];
|
||||
vf27(atkModule, addonId, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 6.05: 8443DD
|
||||
if (*(uint*) ((nint) lfg + 0x2C20) > 0)
|
||||
if (*(uint*)((nint)lfg + 0x2C20) > 0)
|
||||
lfg->Hide();
|
||||
else
|
||||
lfg->Show();
|
||||
@@ -197,7 +207,14 @@ internal unsafe class GameFunctions : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(splits[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var questId))
|
||||
if (
|
||||
!uint.TryParse(
|
||||
splits[1],
|
||||
NumberStyles.Any,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var questId
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.ChatGui.Print("Unable to parse quest id");
|
||||
return;
|
||||
@@ -239,9 +256,10 @@ internal unsafe class GameFunctions : IDisposable
|
||||
{
|
||||
var agent = AgentChatLog.Instance();
|
||||
// case 3
|
||||
var value = new AtkValue { Type = ValueType.Int, Int = 3, };
|
||||
var value = new AtkValue { Type = ValueType.Int, Int = 3 };
|
||||
var result = 0;
|
||||
var vf0 = *(delegate* unmanaged<AgentChatLog*, int*, AtkValue*, ulong, ulong, int*>*) agent->VirtualTable;
|
||||
var vf0 = *(delegate* unmanaged<AgentChatLog*, int*, AtkValue*, ulong, ulong, int*>*)
|
||||
agent->VirtualTable;
|
||||
vf0(agent, &result, &value, 0, 0);
|
||||
}
|
||||
|
||||
@@ -250,7 +268,12 @@ internal unsafe class GameFunctions : IDisposable
|
||||
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
|
||||
private string? ReplacementName;
|
||||
|
||||
private nint ResolveTextCommandPlaceholderDetour(nint a1, byte* placeholderText, byte a3, byte a4)
|
||||
private nint ResolveTextCommandPlaceholderDetour(
|
||||
nint a1,
|
||||
byte* placeholderText,
|
||||
byte a3,
|
||||
byte a4
|
||||
)
|
||||
{
|
||||
// The detour is only invoked through the hook, so the hook should
|
||||
// never be null here, but the nullable field declaration forces us
|
||||
@@ -258,7 +281,7 @@ internal unsafe class GameFunctions : IDisposable
|
||||
if (ResolveTextCommandPlaceholderHook is null)
|
||||
return nint.Zero;
|
||||
|
||||
var placeholder = MemoryHelper.ReadStringNullTerminated((nint) placeholderText);
|
||||
var placeholder = MemoryHelper.ReadStringNullTerminated((nint)placeholderText);
|
||||
if (ReplacementName == null || placeholder != Placeholder)
|
||||
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
||||
|
||||
@@ -268,7 +291,9 @@ internal unsafe class GameFunctions : IDisposable
|
||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||
if (byteCount >= PlaceholderBufferSize)
|
||||
{
|
||||
Plugin.Log.Warning($"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original.");
|
||||
Plugin.Log.Warning(
|
||||
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
||||
);
|
||||
ReplacementName = null;
|
||||
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
using System.Numerics;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Config;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Util;
|
||||
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
|
||||
internal enum KeyboardSource {
|
||||
internal enum KeyboardSource
|
||||
{
|
||||
Game,
|
||||
ImGui
|
||||
ImGui,
|
||||
}
|
||||
|
||||
internal unsafe class KeybindManager : IDisposable {
|
||||
internal unsafe class KeybindManager : IDisposable
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
|
||||
internal bool DirectChat;
|
||||
@@ -26,70 +28,79 @@ internal unsafe class KeybindManager : IDisposable {
|
||||
private bool VanillaTextInputHasFocus;
|
||||
|
||||
private readonly Dictionary<string, Keybind> Keybinds = new();
|
||||
private static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept = new Dictionary<string, ChannelSwitchInfo>
|
||||
{
|
||||
["CMD_CHAT"] = new(null),
|
||||
["CMD_COMMAND"] = new(null, text: "/"),
|
||||
["CMD_REPLY"] = new(InputChannel.Tell, rotate: RotateMode.Forward),
|
||||
["CMD_REPLY_REV"] = new(InputChannel.Tell, rotate: RotateMode.Reverse),
|
||||
["CMD_SAY"] = new(InputChannel.Say),
|
||||
["CMD_YELL"] = new(InputChannel.Yell),
|
||||
["CMD_SHOUT"] = new(InputChannel.Shout),
|
||||
["CMD_PARTY"] = new(InputChannel.Party),
|
||||
["CMD_ALLIANCE"] = new(InputChannel.Alliance),
|
||||
["CMD_FREECOM"] = new(InputChannel.FreeCompany),
|
||||
["PVPTEAM_CHAT"] = new(InputChannel.PvpTeam),
|
||||
["CMD_CWLINKSHELL"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Forward),
|
||||
["CMD_CWLINKSHELL_REV"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Reverse),
|
||||
["CMD_CWLINKSHELL_1"] = new(InputChannel.CrossLinkshell1),
|
||||
["CMD_CWLINKSHELL_2"] = new(InputChannel.CrossLinkshell2),
|
||||
["CMD_CWLINKSHELL_3"] = new(InputChannel.CrossLinkshell3),
|
||||
["CMD_CWLINKSHELL_4"] = new(InputChannel.CrossLinkshell4),
|
||||
["CMD_CWLINKSHELL_5"] = new(InputChannel.CrossLinkshell5),
|
||||
["CMD_CWLINKSHELL_6"] = new(InputChannel.CrossLinkshell6),
|
||||
["CMD_CWLINKSHELL_7"] = new(InputChannel.CrossLinkshell7),
|
||||
["CMD_CWLINKSHELL_8"] = new(InputChannel.CrossLinkshell8),
|
||||
["CMD_LINKSHELL"] = new(InputChannel.Linkshell1, rotate: RotateMode.Forward),
|
||||
["CMD_LINKSHELL_REV"] = new(InputChannel.Linkshell1, rotate: RotateMode.Reverse),
|
||||
["CMD_LINKSHELL_1"] = new(InputChannel.Linkshell1),
|
||||
["CMD_LINKSHELL_2"] = new(InputChannel.Linkshell2),
|
||||
["CMD_LINKSHELL_3"] = new(InputChannel.Linkshell3),
|
||||
["CMD_LINKSHELL_4"] = new(InputChannel.Linkshell4),
|
||||
["CMD_LINKSHELL_5"] = new(InputChannel.Linkshell5),
|
||||
["CMD_LINKSHELL_6"] = new(InputChannel.Linkshell6),
|
||||
["CMD_LINKSHELL_7"] = new(InputChannel.Linkshell7),
|
||||
["CMD_LINKSHELL_8"] = new(InputChannel.Linkshell8),
|
||||
["CMD_BEGINNER"] = new(InputChannel.NoviceNetwork),
|
||||
["CMD_REPLY_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Forward),
|
||||
["CMD_REPLY_REV_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Reverse),
|
||||
["CMD_SAY_ALWAYS"] = new(InputChannel.Say, true),
|
||||
["CMD_YELL_ALWAYS"] = new(InputChannel.Yell, true),
|
||||
["CMD_PARTY_ALWAYS"] = new(InputChannel.Party, true),
|
||||
["CMD_ALLIANCE_ALWAYS"] = new(InputChannel.Alliance, true),
|
||||
["CMD_FREECOM_ALWAYS"] = new(InputChannel.FreeCompany, true),
|
||||
["PVPTEAM_CHAT_ALWAYS"] = new(InputChannel.PvpTeam, true),
|
||||
["CMD_CWLINKSHELL_ALWAYS"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Forward),
|
||||
["CMD_CWLINKSHELL_ALWAYS_REV"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Reverse),
|
||||
["CMD_CWLINKSHELL_1_ALWAYS"] = new(InputChannel.CrossLinkshell1, true),
|
||||
["CMD_CWLINKSHELL_2_ALWAYS"] = new(InputChannel.CrossLinkshell2, true),
|
||||
["CMD_CWLINKSHELL_3_ALWAYS"] = new(InputChannel.CrossLinkshell3, true),
|
||||
["CMD_CWLINKSHELL_4_ALWAYS"] = new(InputChannel.CrossLinkshell4, true),
|
||||
["CMD_CWLINKSHELL_5_ALWAYS"] = new(InputChannel.CrossLinkshell5, true),
|
||||
["CMD_CWLINKSHELL_6_ALWAYS"] = new(InputChannel.CrossLinkshell6, true),
|
||||
["CMD_CWLINKSHELL_7_ALWAYS"] = new(InputChannel.CrossLinkshell7, true),
|
||||
["CMD_CWLINKSHELL_8_ALWAYS"] = new(InputChannel.CrossLinkshell8, true),
|
||||
["CMD_LINKSHELL_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Forward),
|
||||
["CMD_LINKSHELL_REV_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Reverse),
|
||||
["CMD_LINKSHELL_1_ALWAYS"] = new(InputChannel.Linkshell1, true),
|
||||
["CMD_LINKSHELL_2_ALWAYS"] = new(InputChannel.Linkshell2, true),
|
||||
["CMD_LINKSHELL_3_ALWAYS"] = new(InputChannel.Linkshell3, true),
|
||||
["CMD_LINKSHELL_4_ALWAYS"] = new(InputChannel.Linkshell4, true),
|
||||
["CMD_LINKSHELL_5_ALWAYS"] = new(InputChannel.Linkshell5, true),
|
||||
["CMD_LINKSHELL_6_ALWAYS"] = new(InputChannel.Linkshell6, true),
|
||||
["CMD_LINKSHELL_7_ALWAYS"] = new(InputChannel.Linkshell7, true),
|
||||
["CMD_LINKSHELL_8_ALWAYS"] = new(InputChannel.Linkshell8, true),
|
||||
["CMD_BEGINNER_ALWAYS"] = new(InputChannel.NoviceNetwork, true)
|
||||
};
|
||||
private static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept =
|
||||
new Dictionary<string, ChannelSwitchInfo>
|
||||
{
|
||||
["CMD_CHAT"] = new(null),
|
||||
["CMD_COMMAND"] = new(null, text: "/"),
|
||||
["CMD_REPLY"] = new(InputChannel.Tell, rotate: RotateMode.Forward),
|
||||
["CMD_REPLY_REV"] = new(InputChannel.Tell, rotate: RotateMode.Reverse),
|
||||
["CMD_SAY"] = new(InputChannel.Say),
|
||||
["CMD_YELL"] = new(InputChannel.Yell),
|
||||
["CMD_SHOUT"] = new(InputChannel.Shout),
|
||||
["CMD_PARTY"] = new(InputChannel.Party),
|
||||
["CMD_ALLIANCE"] = new(InputChannel.Alliance),
|
||||
["CMD_FREECOM"] = new(InputChannel.FreeCompany),
|
||||
["PVPTEAM_CHAT"] = new(InputChannel.PvpTeam),
|
||||
["CMD_CWLINKSHELL"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Forward),
|
||||
["CMD_CWLINKSHELL_REV"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Reverse),
|
||||
["CMD_CWLINKSHELL_1"] = new(InputChannel.CrossLinkshell1),
|
||||
["CMD_CWLINKSHELL_2"] = new(InputChannel.CrossLinkshell2),
|
||||
["CMD_CWLINKSHELL_3"] = new(InputChannel.CrossLinkshell3),
|
||||
["CMD_CWLINKSHELL_4"] = new(InputChannel.CrossLinkshell4),
|
||||
["CMD_CWLINKSHELL_5"] = new(InputChannel.CrossLinkshell5),
|
||||
["CMD_CWLINKSHELL_6"] = new(InputChannel.CrossLinkshell6),
|
||||
["CMD_CWLINKSHELL_7"] = new(InputChannel.CrossLinkshell7),
|
||||
["CMD_CWLINKSHELL_8"] = new(InputChannel.CrossLinkshell8),
|
||||
["CMD_LINKSHELL"] = new(InputChannel.Linkshell1, rotate: RotateMode.Forward),
|
||||
["CMD_LINKSHELL_REV"] = new(InputChannel.Linkshell1, rotate: RotateMode.Reverse),
|
||||
["CMD_LINKSHELL_1"] = new(InputChannel.Linkshell1),
|
||||
["CMD_LINKSHELL_2"] = new(InputChannel.Linkshell2),
|
||||
["CMD_LINKSHELL_3"] = new(InputChannel.Linkshell3),
|
||||
["CMD_LINKSHELL_4"] = new(InputChannel.Linkshell4),
|
||||
["CMD_LINKSHELL_5"] = new(InputChannel.Linkshell5),
|
||||
["CMD_LINKSHELL_6"] = new(InputChannel.Linkshell6),
|
||||
["CMD_LINKSHELL_7"] = new(InputChannel.Linkshell7),
|
||||
["CMD_LINKSHELL_8"] = new(InputChannel.Linkshell8),
|
||||
["CMD_BEGINNER"] = new(InputChannel.NoviceNetwork),
|
||||
["CMD_REPLY_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Forward),
|
||||
["CMD_REPLY_REV_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Reverse),
|
||||
["CMD_SAY_ALWAYS"] = new(InputChannel.Say, true),
|
||||
["CMD_YELL_ALWAYS"] = new(InputChannel.Yell, true),
|
||||
["CMD_PARTY_ALWAYS"] = new(InputChannel.Party, true),
|
||||
["CMD_ALLIANCE_ALWAYS"] = new(InputChannel.Alliance, true),
|
||||
["CMD_FREECOM_ALWAYS"] = new(InputChannel.FreeCompany, true),
|
||||
["PVPTEAM_CHAT_ALWAYS"] = new(InputChannel.PvpTeam, true),
|
||||
["CMD_CWLINKSHELL_ALWAYS"] = new(
|
||||
InputChannel.CrossLinkshell1,
|
||||
true,
|
||||
RotateMode.Forward
|
||||
),
|
||||
["CMD_CWLINKSHELL_ALWAYS_REV"] = new(
|
||||
InputChannel.CrossLinkshell1,
|
||||
true,
|
||||
RotateMode.Reverse
|
||||
),
|
||||
["CMD_CWLINKSHELL_1_ALWAYS"] = new(InputChannel.CrossLinkshell1, true),
|
||||
["CMD_CWLINKSHELL_2_ALWAYS"] = new(InputChannel.CrossLinkshell2, true),
|
||||
["CMD_CWLINKSHELL_3_ALWAYS"] = new(InputChannel.CrossLinkshell3, true),
|
||||
["CMD_CWLINKSHELL_4_ALWAYS"] = new(InputChannel.CrossLinkshell4, true),
|
||||
["CMD_CWLINKSHELL_5_ALWAYS"] = new(InputChannel.CrossLinkshell5, true),
|
||||
["CMD_CWLINKSHELL_6_ALWAYS"] = new(InputChannel.CrossLinkshell6, true),
|
||||
["CMD_CWLINKSHELL_7_ALWAYS"] = new(InputChannel.CrossLinkshell7, true),
|
||||
["CMD_CWLINKSHELL_8_ALWAYS"] = new(InputChannel.CrossLinkshell8, true),
|
||||
["CMD_LINKSHELL_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Forward),
|
||||
["CMD_LINKSHELL_REV_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Reverse),
|
||||
["CMD_LINKSHELL_1_ALWAYS"] = new(InputChannel.Linkshell1, true),
|
||||
["CMD_LINKSHELL_2_ALWAYS"] = new(InputChannel.Linkshell2, true),
|
||||
["CMD_LINKSHELL_3_ALWAYS"] = new(InputChannel.Linkshell3, true),
|
||||
["CMD_LINKSHELL_4_ALWAYS"] = new(InputChannel.Linkshell4, true),
|
||||
["CMD_LINKSHELL_5_ALWAYS"] = new(InputChannel.Linkshell5, true),
|
||||
["CMD_LINKSHELL_6_ALWAYS"] = new(InputChannel.Linkshell6, true),
|
||||
["CMD_LINKSHELL_7_ALWAYS"] = new(InputChannel.Linkshell7, true),
|
||||
["CMD_LINKSHELL_8_ALWAYS"] = new(InputChannel.Linkshell8, true),
|
||||
["CMD_BEGINNER_ALWAYS"] = new(InputChannel.NoviceNetwork, true),
|
||||
};
|
||||
|
||||
// List of keys that can be used as a part of keybinds while the chat is
|
||||
// focused WITHOUT modifiers. All other keys can only be used if their
|
||||
@@ -353,12 +364,22 @@ internal unsafe class KeybindManager : IDisposable {
|
||||
return key.TryToImGui(out var imguiKey) && ImGui.IsKeyPressed(imguiKey);
|
||||
}
|
||||
|
||||
private static bool ComboPressed(KeyboardSource source, VirtualKey key, ModifierFlag modifier, ModifierFlag? modifierState = null, bool modifiersOnly = false)
|
||||
private static bool ComboPressed(
|
||||
KeyboardSource source,
|
||||
VirtualKey key,
|
||||
ModifierFlag modifier,
|
||||
ModifierFlag? modifierState = null,
|
||||
bool modifiersOnly = false
|
||||
)
|
||||
{
|
||||
// When we're in an input, we don't want to process any keybinds that
|
||||
// don't have a modifier (or only use shift) and are not explicitly
|
||||
// whitelisted.
|
||||
if (modifiersOnly && !ModifierlessChatKeys.Contains(key) && modifier is ModifierFlag.None or ModifierFlag.Shift)
|
||||
if (
|
||||
modifiersOnly
|
||||
&& !ModifierlessChatKeys.Contains(key)
|
||||
&& modifier is ModifierFlag.None or ModifierFlag.Shift
|
||||
)
|
||||
return false;
|
||||
|
||||
modifierState ??= GetModifiers(source);
|
||||
@@ -366,26 +387,43 @@ internal unsafe class KeybindManager : IDisposable {
|
||||
{
|
||||
KeybindMode.Strict => modifier == modifierState.Value,
|
||||
KeybindMode.Flexible => modifierState.Value.HasFlag(modifier),
|
||||
_ => false
|
||||
_ => false,
|
||||
};
|
||||
|
||||
return KeyPressed(source, key) && modifierPressed;
|
||||
}
|
||||
|
||||
private static bool ConfigKeybindPressed(KeyboardSource source, ConfigKeyBind? bind, ModifierFlag? modifierState = null, bool modifiersOnly = false)
|
||||
private static bool ConfigKeybindPressed(
|
||||
KeyboardSource source,
|
||||
ConfigKeyBind? bind,
|
||||
ModifierFlag? modifierState = null,
|
||||
bool modifiersOnly = false
|
||||
)
|
||||
{
|
||||
return bind != null && ComboPressed(source, bind.Key, bind.Modifier, modifierState: modifierState, modifiersOnly: modifiersOnly);
|
||||
return bind != null
|
||||
&& ComboPressed(
|
||||
source,
|
||||
bind.Key,
|
||||
bind.Modifier,
|
||||
modifierState: modifierState,
|
||||
modifiersOnly: modifiersOnly
|
||||
);
|
||||
}
|
||||
|
||||
private void HandleKeybinds(IFramework _ ) => HandleKeybinds(KeyboardSource.Game);
|
||||
private void HandleKeybinds(IFramework _) => HandleKeybinds(KeyboardSource.Game);
|
||||
|
||||
internal void HandleKeybinds(KeyboardSource source, bool ignoreChatOpen = false, bool modifiersOnly = false)
|
||||
internal void HandleKeybinds(
|
||||
KeyboardSource source,
|
||||
bool ignoreChatOpen = false,
|
||||
bool modifiersOnly = false
|
||||
)
|
||||
{
|
||||
// Refresh current keybinds every 5s
|
||||
if (LastRefresh + 5 * 1000 < Environment.TickCount64)
|
||||
{
|
||||
UpdateKeybinds();
|
||||
DirectChat = Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option;
|
||||
DirectChat =
|
||||
Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option;
|
||||
LastRefresh = Environment.TickCount64;
|
||||
}
|
||||
|
||||
@@ -433,10 +471,18 @@ internal unsafe class KeybindManager : IDisposable {
|
||||
|
||||
void Intercept(VirtualKey vk, ModifierFlag modifier)
|
||||
{
|
||||
if (!ComboPressed(source, vk, modifier, modifierState: modifierState, modifiersOnly: modifiersOnly))
|
||||
if (
|
||||
!ComboPressed(
|
||||
source,
|
||||
vk,
|
||||
modifier,
|
||||
modifierState: modifierState,
|
||||
modifiersOnly: modifiersOnly
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
var bits = BitOperations.PopCount((uint) modifier);
|
||||
var bits = BitOperations.PopCount((uint)modifier);
|
||||
if (bits < currentBest.Item3)
|
||||
return;
|
||||
|
||||
@@ -457,7 +503,7 @@ internal unsafe class KeybindManager : IDisposable {
|
||||
try
|
||||
{
|
||||
TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null;
|
||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(info) { TellReason = reason, });
|
||||
Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(info) { TellReason = reason });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -494,11 +540,11 @@ internal unsafe class KeybindManager : IDisposable {
|
||||
var key2 = outData.KeySettings[1];
|
||||
return new Keybind
|
||||
{
|
||||
Key1 = RemapInvalidVirtualKey((VirtualKey) key1.Key),
|
||||
Modifier1 = (ModifierFlag) key1.KeyModifier,
|
||||
Key1 = RemapInvalidVirtualKey((VirtualKey)key1.Key),
|
||||
Modifier1 = (ModifierFlag)key1.KeyModifier,
|
||||
|
||||
Key2 = RemapInvalidVirtualKey((VirtualKey) key2.Key),
|
||||
Modifier2 = (ModifierFlag) key2.KeyModifier,
|
||||
Key2 = RemapInvalidVirtualKey((VirtualKey)key2.Key),
|
||||
Modifier2 = (ModifierFlag)key2.KeyModifier,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -506,9 +552,9 @@ internal unsafe class KeybindManager : IDisposable {
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
VirtualKey.F23 => VirtualKey.OEM_2, // /?
|
||||
(VirtualKey) 140 => VirtualKey.OEM_7, // '"
|
||||
_ => key
|
||||
VirtualKey.F23 => VirtualKey.OEM_2, // /?
|
||||
(VirtualKey)140 => VirtualKey.OEM_7, // '"
|
||||
_ => key,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
|
||||
@@ -11,7 +11,8 @@ internal static unsafe class Party
|
||||
internal static void InviteSameWorld(string name, ushort world, ulong contentId)
|
||||
{
|
||||
// this only works if target is on the same world
|
||||
fixed (byte* namePtr = name.ToTerminatedBytes()) {
|
||||
fixed (byte* namePtr = name.ToTerminatedBytes())
|
||||
{
|
||||
InfoProxyPartyInvite.Instance()->InviteToParty(contentId, namePtr, world);
|
||||
}
|
||||
}
|
||||
@@ -44,14 +45,16 @@ internal static unsafe class Party
|
||||
|
||||
internal static void Kick(string name, ulong contentId)
|
||||
{
|
||||
fixed (byte* namePtr = name.ToTerminatedBytes()) {
|
||||
fixed (byte* namePtr = name.ToTerminatedBytes())
|
||||
{
|
||||
AgentPartyMember.Instance()->Kick(namePtr, 0, contentId);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Promote(string name, ulong contentId)
|
||||
{
|
||||
fixed (byte* namePtr = name.ToTerminatedBytes()) {
|
||||
fixed (byte* namePtr = name.ToTerminatedBytes())
|
||||
{
|
||||
AgentPartyMember.Instance()->Promote(namePtr, 0, contentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ using HellionChat.Code;
|
||||
|
||||
namespace HellionChat.GameFunctions.Types;
|
||||
|
||||
internal class ChannelSwitchInfo {
|
||||
internal class ChannelSwitchInfo
|
||||
{
|
||||
internal InputChannel? Channel { get; }
|
||||
internal bool Permanent { get; }
|
||||
internal RotateMode Rotate { get; }
|
||||
internal string? Text { get; }
|
||||
|
||||
internal ChannelSwitchInfo(InputChannel? channel, bool permanent = false, RotateMode rotate = RotateMode.None, string? text = null)
|
||||
internal ChannelSwitchInfo(
|
||||
InputChannel? channel,
|
||||
bool permanent = false,
|
||||
RotateMode rotate = RotateMode.None,
|
||||
string? text = null
|
||||
)
|
||||
{
|
||||
Channel = channel;
|
||||
Permanent = permanent;
|
||||
|
||||
@@ -19,14 +19,14 @@ public class TellTarget
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
public bool IsSet()
|
||||
=> !string.IsNullOrEmpty(Name) && World > 0;
|
||||
public bool IsSet() => !string.IsNullOrEmpty(Name) && World > 0;
|
||||
|
||||
public string ToWorldString()
|
||||
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
|
||||
public string ToWorldString() =>
|
||||
Sheets.WorldSheet.TryGetRow(World, out var worldRow)
|
||||
? worldRow.Name.ToString()
|
||||
: string.Empty;
|
||||
|
||||
public string ToTargetString()
|
||||
=> $"{Name}@{ToWorldString()}";
|
||||
public string ToTargetString() => $"{Name}@{ToWorldString()}";
|
||||
|
||||
public unsafe void FromTarget(IPlayerCharacter target)
|
||||
{
|
||||
@@ -39,5 +39,6 @@ public class TellTarget
|
||||
}
|
||||
|
||||
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,36 +1,21 @@
|
||||
<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. -->
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.4.3</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Honor packages.lock.json on restore so floating version ranges
|
||||
don't silently drift between machines or CI runs. -->
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<!-- v1.0.0 standalone cut — both AssemblyName and RootNamespace
|
||||
are HellionChat. The plugin no longer maintains source-level
|
||||
cherry-pick compatibility with upstream Infiziert90/ChatTwo;
|
||||
upstream changes are integrated manually if at all. -->
|
||||
<!-- v1.0.0+: standalone fork, no upstream cherry-pick compatibility -->
|
||||
<AssemblyName>HellionChat</AssemblyName>
|
||||
<RootNamespace>HellionChat</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Closed ranges on packages with breaking-change history block a
|
||||
surprise major bump when the lock file is regenerated. The
|
||||
lock file pins the exact version per build; the upper bound
|
||||
keeps the unlock path from drifting across major lines. -->
|
||||
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
||||
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||
<!-- Override the transitively-referenced native SQLite build to one
|
||||
that ships SQLite >= 3.50.3 (CVE-2025-6965 memory corruption,
|
||||
CVE-2025-7709 fixed in 3.50.x). Microsoft.Data.Sqlite 10.0.7
|
||||
pulls SQLitePCLRaw 2.1.11 which carries the older lib; pinning
|
||||
the lib package directly forces the newer native binary
|
||||
without a major bump on the managed wrapper. -->
|
||||
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
|
||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
|
||||
@@ -38,9 +23,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Pure-function test suites in HellionChat.Tests need access to
|
||||
the internal helper classes (StringUtil, UriPayload, Tokenizer
|
||||
etc.). Test assembly does not get redistributed. -->
|
||||
<!-- Test assembly needs access to internal helpers (not redistributed) -->
|
||||
<InternalsVisibleTo Include="HellionChat.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -59,15 +42,7 @@
|
||||
</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. -->
|
||||
<!-- Embedded resources: Hellion font (Exo 2, OFL-1.1) + manifest resource -->
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\HellionFont.ttf">
|
||||
<LogicalName>HellionFont.ttf</LogicalName>
|
||||
@@ -80,19 +55,10 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<!-- Plugin icon. Copy images/* into the build output so Dalamud
|
||||
finds the icon next to the DLL, and let the SDK default
|
||||
DalamudPackager pipeline include the same path in the
|
||||
release ZIP. Earlier we shipped a custom DalamudPackager
|
||||
targets override that explicitly set HandleImages and
|
||||
ImagesPath; that override conflicted with the SDK 15
|
||||
default and the resulting manifest carried no IconUrl.
|
||||
Removed in v0.5.2. -->
|
||||
<!-- Plugin icon: copy images/* to output for Dalamud discovery -->
|
||||
<ItemGroup>
|
||||
<None Include="images\**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+134
-186
@@ -2,220 +2,168 @@ name: Hellion Chat
|
||||
author: JonKazama-Hellion
|
||||
punchline: Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)
|
||||
description: |-
|
||||
Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV
|
||||
based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally
|
||||
removed (the optional webinterface) and a stack of privacy controls is
|
||||
added on top. Tabs, channel filters, RGB colours, emotes, screenshot
|
||||
mode, IPC integration and the chat replacement window itself work the
|
||||
same. The webinterface is intentionally not part of Hellion Chat because
|
||||
it serves a different use case from the smaller default footprint this
|
||||
plugin is built around.
|
||||
Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV
|
||||
based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally
|
||||
removed (the optional webinterface) and a stack of privacy controls is
|
||||
added on top. Tabs, channel filters, RGB colours, emotes, screenshot
|
||||
mode, IPC integration and the chat replacement window itself work the
|
||||
same. The webinterface is intentionally not part of Hellion Chat because
|
||||
it serves a different use case from the smaller default footprint this
|
||||
plugin is built around.
|
||||
|
||||
On top of that, Hellion Chat adds privacy and data-handling controls
|
||||
designed to align with the modern data protection rules that apply
|
||||
across the EU, the United States and Japan. By default only your own
|
||||
conversations are stored; messages from strangers, NPCs and system
|
||||
spam stay out of the database. Retention windows are configurable per
|
||||
channel, history can be wiped retroactively, and stored data can be
|
||||
exported on demand.
|
||||
On top of that, Hellion Chat adds privacy and data-handling controls
|
||||
designed to align with the modern data protection rules that apply
|
||||
across the EU, the United States and Japan. By default only your own
|
||||
conversations are stored; messages from strangers, NPCs and system
|
||||
spam stay out of the database. Retention windows are configurable per
|
||||
channel, history can be wiped retroactively, and stored data can be
|
||||
exported on demand.
|
||||
|
||||
Key privacy and data-handling features:
|
||||
Key privacy and data-handling features:
|
||||
|
||||
- Channel whitelist with a Privacy-First default
|
||||
- Per-channel retention with a daily background sweep
|
||||
- Retroactive cleanup with a Ctrl+Shift confirm
|
||||
- Export to Markdown, JSON or CSV
|
||||
- First-run wizard with three preset profiles (Privacy-First, Casual,
|
||||
Full History)
|
||||
- Bilingual UI (English and German) with live language switching
|
||||
- Independent plugin state — own config file and database directory,
|
||||
so Hellion Chat does not share state with upstream Chat 2
|
||||
- Channel whitelist with a Privacy-First default
|
||||
- Per-channel retention with a daily background sweep
|
||||
- Retroactive cleanup with a Ctrl+Shift confirm
|
||||
- Export to Markdown, JSON or CSV
|
||||
- First-run wizard with three preset profiles (Privacy-First, Casual,
|
||||
Full History)
|
||||
- Bilingual UI (English and German) with live language switching
|
||||
- Independent plugin state — own config file and database directory,
|
||||
so Hellion Chat does not share state with upstream Chat 2
|
||||
|
||||
v1.3.0 First plugin integration cycle. Honorific custom titles
|
||||
are shown in the chat header above the message log, with auto-detect
|
||||
and silent fallback when Honorific is not installed.
|
||||
v1.4.2 — ChatLog Frame-Hot-Path. Three per-frame allocation
|
||||
patterns gone from the chat-log render path: card-mode borders
|
||||
hoist invariants out of the per-message loop, auto-tell tab
|
||||
tint and icon get a per-tab cache, and the status bar gates
|
||||
its tab aggregation behind the same one-second cache it uses
|
||||
for the format strings.
|
||||
|
||||
v1.4.0 — Critical Lifecycle Fixes. Plugin reload and shutdown
|
||||
are cleaner: SQLite no longer leans on GC pressure to release
|
||||
its file, worker threads are explicitly background, deferred
|
||||
config saves no longer get lost mid-disable, and pre-v13 config
|
||||
backups carry the user's custom theme opacity into the v14 schema
|
||||
instead of falling back to the default.
|
||||
v1.4.3 — Plugin-Load Async-Init plus Repo-Cutover. Plugin
|
||||
migrated to Dalamud's IAsyncDalamudPlugin so the heavy work
|
||||
(migrations, service allocations, window construction, hook
|
||||
subscription) runs in LoadAsync without blocking Dalamud's
|
||||
UI. Schema-gate replaces the v9 → v16 migration chain;
|
||||
configs on schema v16+ load directly. Custom-repo URL moves
|
||||
to gitea.hellion-forge.cloud, the GitHub repo stays as a
|
||||
frozen v1.4.2 snapshot.
|
||||
|
||||
v1.4.1 — Theme Engine Performance plus a tenth built-in.
|
||||
HellionStyle.PushGlobal reads pre-computed ABGR values from a
|
||||
per-theme cache instead of converting RGBA per slot per frame
|
||||
(~13 % render-time recovery in typical scenes). Custom-theme
|
||||
hot-reload survives transient file locks (editor mid-save
|
||||
keeps the last-known-good snapshot). Synthwave Sunset joins
|
||||
as the tenth built-in theme — Hot Magenta + Cyan on midnight
|
||||
violet, 80s neon-grid vibes.
|
||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
||||
|
||||
v1.4.2 — ChatLog Frame-Hot-Path. Three per-frame allocation
|
||||
patterns gone from the chat-log render path: card-mode borders
|
||||
hoist invariants out of the per-message loop, auto-tell tab
|
||||
tint and icon get a per-tab cache, and the status bar gates
|
||||
its tab aggregation behind the same one-second cache it uses
|
||||
for the format strings.
|
||||
|
||||
v1.4.3 — Plugin-Load Async-Init plus Repo-Cutover. Plugin
|
||||
migrated to Dalamud's IAsyncDalamudPlugin so the heavy work
|
||||
(migrations, service allocations, window construction, hook
|
||||
subscription) runs in LoadAsync without blocking Dalamud's
|
||||
UI. Schema-gate replaces the v9 → v16 migration chain;
|
||||
configs on schema v16+ load directly. Custom-repo URL moves
|
||||
to gitea.hellion-forge.cloud, the GitHub repo stays as a
|
||||
frozen v1.4.2 snapshot.
|
||||
|
||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
||||
|
||||
Modding & support: join the Hellion Forge Discord at
|
||||
https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
|
||||
other Hellion Online Media plugins/tools.
|
||||
Modding & support: join the Hellion Forge Discord at
|
||||
https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
|
||||
other Hellion Online Media plugins/tools.
|
||||
repo_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat
|
||||
accepts_feedback: true
|
||||
icon_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png
|
||||
image_urls:
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png
|
||||
tags:
|
||||
- Social
|
||||
- UI
|
||||
- Chat
|
||||
- Replacement
|
||||
- Privacy
|
||||
- Social
|
||||
- UI
|
||||
- Chat
|
||||
- Replacement
|
||||
- Privacy
|
||||
changelog: |-
|
||||
**Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover (2026-05-08)**
|
||||
**Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover (2026-05-08)**
|
||||
|
||||
Plugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin`
|
||||
API. The constructor now does only the bootstrap-essentials
|
||||
(config load, language init, conflict detection); migrations,
|
||||
service allocations, window construction and hook subscription
|
||||
move to LoadAsync. Dalamud can keep its UI responsive while the
|
||||
heavy work runs.
|
||||
Plugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin`
|
||||
API. The constructor now does only the bootstrap-essentials
|
||||
(config load, language init, conflict detection); migrations,
|
||||
service allocations, window construction and hook subscription
|
||||
move to LoadAsync. Dalamud can keep its UI responsive while the
|
||||
heavy work runs.
|
||||
|
||||
- IAsyncDalamudPlugin two-phase load with per-line CaptureFailure
|
||||
in DisposeAsync (mirrors LightlessSync's pattern); idempotency
|
||||
guard protects against reload races
|
||||
- Schema-gate replaces the v9 → v16 migration chain. Configs
|
||||
on schema v16+ load directly; older configs trigger an
|
||||
"install v1.4.2 first" error so the historic migration
|
||||
path stays intact
|
||||
- AutoTranslate.PreloadCache moved off the load path. First
|
||||
use may have a sub-second hitch instead of every-load; the
|
||||
upstream chose differently, we accept first-use latency
|
||||
- FontManager.BuildFonts is called sync at the start of
|
||||
LoadAsync; Dalamud rebuilds the font atlas on its own
|
||||
pipeline so the custom Hellion-Exo2 font appears with a
|
||||
brief font-pop after load (matches ChatTwo's behaviour)
|
||||
- Custom-repo URL moved to gitea.hellion-forge.cloud/
|
||||
JonKazama-Hellion/HellionChat. GitHub repo stays as a
|
||||
frozen v1.4.2 snapshot; new releases ship from Gitea.
|
||||
Existing testers need to update the custom-repo URL once
|
||||
- Plugin-load time in this release sits at ~3.7 s median
|
||||
(5 reloads), comparable to v1.4.2. Async migration is
|
||||
foundational for v1.4.4 Lazy-Init optimisations rather
|
||||
than an immediate user-perceived win
|
||||
- IAsyncDalamudPlugin two-phase load with per-line CaptureFailure
|
||||
in DisposeAsync (mirrors LightlessSync's pattern); idempotency
|
||||
guard protects against reload races
|
||||
- Schema-gate replaces the v9 → v16 migration chain. Configs
|
||||
on schema v16+ load directly; older configs trigger an
|
||||
"install v1.4.2 first" error so the historic migration
|
||||
path stays intact
|
||||
- AutoTranslate.PreloadCache moved off the load path. First
|
||||
use may have a sub-second hitch instead of every-load; the
|
||||
upstream chose differently, we accept first-use latency
|
||||
- FontManager.BuildFonts is called sync at the start of
|
||||
LoadAsync; Dalamud rebuilds the font atlas on its own
|
||||
pipeline so the custom Hellion-Exo2 font appears with a
|
||||
brief font-pop after load (matches ChatTwo's behaviour)
|
||||
- Custom-repo URL moved to gitea.hellion-forge.cloud/
|
||||
JonKazama-Hellion/HellionChat. GitHub repo stays as a
|
||||
frozen v1.4.2 snapshot; new releases ship from Gitea.
|
||||
Existing testers need to update the custom-repo URL once
|
||||
- Plugin-load time in this release sits at ~3.7 s median
|
||||
(5 reloads), comparable to v1.4.2. Async migration is
|
||||
foundational for v1.4.4 Lazy-Init optimisations rather
|
||||
than an immediate user-perceived win
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**
|
||||
**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**
|
||||
|
||||
Third sub-patch of the v1.4.x Polish Sweep series. Per-frame
|
||||
allocations from the chat-log render path eliminated.
|
||||
Third sub-patch of the v1.4.x Polish Sweep series. Per-frame
|
||||
allocations from the chat-log render path eliminated.
|
||||
|
||||
- DrawMessages card-mode hoists theme/drawList/winLeft/winRight/
|
||||
borderColorAbgr out of the per-message loop. About 500
|
||||
redundant calls per frame at 100 visible messages, multiplied
|
||||
by every pop-out window
|
||||
- Auto-tell tab tint and icon use a per-tab cache. Hash
|
||||
computation and string allocation only happen when the tell
|
||||
target name or world drifts. AutoTellTabTint stays a pure
|
||||
hash helper; cache lives in a thin TabTintCache wrapper
|
||||
- Status bar gates its tab aggregation behind the same
|
||||
one-second cache it already used for the format strings.
|
||||
LINQ Sum and Count replaced with a single foreach pass
|
||||
that runs on roughly 1% of frames
|
||||
- DrawMessages card-mode hoists theme/drawList/winLeft/winRight/
|
||||
borderColorAbgr out of the per-message loop. About 500
|
||||
redundant calls per frame at 100 visible messages, multiplied
|
||||
by every pop-out window
|
||||
- Auto-tell tab tint and icon use a per-tab cache. Hash
|
||||
computation and string allocation only happen when the tell
|
||||
target name or world drifts. AutoTellTabTint stays a pure
|
||||
hash helper; cache lives in a thin TabTintCache wrapper
|
||||
- Status bar gates its tab aggregation behind the same
|
||||
one-second cache it already used for the format strings.
|
||||
LINQ Sum and Count replaced with a single foreach pass
|
||||
that runs on roughly 1% of frames
|
||||
|
||||
Realistic frame-time recovery: 2-5% in typical scenes, more
|
||||
on pop-out-heavy setups because the card-border hoist scales
|
||||
per window.
|
||||
Realistic frame-time recovery: 2-5% in typical scenes, more
|
||||
on pop-out-heavy setups because the card-border hoist scales
|
||||
per window.
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 1.4.1 — Theme Engine Performance**
|
||||
**Hellion Chat 1.4.1 — Theme Engine Performance**
|
||||
|
||||
Second sub-patch of the v1.4.x Polish Sweep series. Heap
|
||||
pressure from the theme engine's per-frame render path
|
||||
removed, plus a tenth built-in theme and hardening for
|
||||
the custom-theme hot-reload.
|
||||
Second sub-patch of the v1.4.x Polish Sweep series. Heap
|
||||
pressure from the theme engine's per-frame render path
|
||||
removed, plus a tenth built-in theme and hardening for
|
||||
the custom-theme hot-reload.
|
||||
|
||||
- Theme records carry a pre-computed ABGR-packed cache
|
||||
for every color slot; cache is filled when the theme
|
||||
is registered and refreshed defensively on every
|
||||
Switch()
|
||||
- HellionStyle.PushGlobal reads ABGR values from the
|
||||
cache instead of calling ColourUtil.RgbaToAbgr per
|
||||
slot per frame; ~13 % render-time recovery measured
|
||||
in typical scenes (plan estimate was 2–6 %, real
|
||||
~10–15 %)
|
||||
- ThemeRegistry custom-theme reload distinguishes a
|
||||
recoverable file lock (editor mid-save) from a
|
||||
permanent IO failure; locked themes keep their
|
||||
last-known-good snapshot and retry on the next
|
||||
lookup instead of dropping out of the picker
|
||||
- New built-in: Synthwave Sunset — Hot Magenta + Cyan
|
||||
on midnight violet, 80s neon-grid vibes; tenth theme
|
||||
in the picker
|
||||
- Author credits refreshed: brand themes are credited
|
||||
as "Hellion Forge"; Mint Grove and Forge Merchantman
|
||||
now credited to Carla Beleandis as a community thanks
|
||||
- Theme records carry a pre-computed ABGR-packed cache
|
||||
for every color slot; cache is filled when the theme
|
||||
is registered and refreshed defensively on every
|
||||
Switch()
|
||||
- HellionStyle.PushGlobal reads ABGR values from the
|
||||
cache instead of calling ColourUtil.RgbaToAbgr per
|
||||
slot per frame; ~13 % render-time recovery measured
|
||||
in typical scenes (plan estimate was 2–6 %, real
|
||||
~10–15 %)
|
||||
- ThemeRegistry custom-theme reload distinguishes a
|
||||
recoverable file lock (editor mid-save) from a
|
||||
permanent IO failure; locked themes keep their
|
||||
last-known-good snapshot and retry on the next
|
||||
lookup instead of dropping out of the picker
|
||||
- New built-in: Synthwave Sunset — Hot Magenta + Cyan
|
||||
on midnight violet, 80s neon-grid vibes; tenth theme
|
||||
in the picker
|
||||
- Author credits refreshed: brand themes are credited
|
||||
as "Hellion Forge"; Mint Grove and Forge Merchantman
|
||||
now credited to Carla Beleandis as a community thanks
|
||||
|
||||
No schema bump, no user-visible behaviour change other
|
||||
than smoother frames on GC-sensitive setups and one
|
||||
additional colour option.
|
||||
No schema bump, no user-visible behaviour change other
|
||||
than smoother frames on GC-sensitive setups and one
|
||||
additional colour option.
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 1.4.0 — Critical Lifecycle Fixes**
|
||||
---
|
||||
|
||||
First sub-patch of the v1.4.x Polish Sweep series. Seven
|
||||
known lifecycle and race bugs eliminated before any
|
||||
performance refactor sits on top.
|
||||
|
||||
- MessageStore disposal no longer triggers GC.Collect
|
||||
globally; Pooling=false on the SQLite connection means
|
||||
there's nothing left to clean up by hand
|
||||
- PendingMessage and RetentionSweep worker threads are
|
||||
explicitly marked IsBackground=true so the plugin domain
|
||||
can unload during XIVLauncher reload without waiting
|
||||
for them
|
||||
- EmoteCache image and gif loaders moved from async-void
|
||||
to async Task with a shared task tracker, draining
|
||||
on Dispose so an in-flight load can no longer write
|
||||
to a disposed EmoteImages entry
|
||||
- DisposeAsync 10s timeout now warns loudly instead of
|
||||
silently leaving the worker behind
|
||||
- Plugin.Dispose flushes any pending DeferredSaveFrames
|
||||
before tearing services down, so settings changes
|
||||
made in the last few frames before disable are no
|
||||
longer lost
|
||||
- The v13→v14 config migration now reads the pre-v13
|
||||
backup and carries HellionThemeWindowOpacity into the
|
||||
new WindowOpacity field instead of falling back to
|
||||
the default 0.85
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||
Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||
|
||||
@@ -2,14 +2,8 @@ using System.Collections.Generic;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
// Hellion Chat — v0.6.0 shared input history. Replaces the embedded
|
||||
// ChatLogWindow.InputBacklog so that pop-out windows with their own
|
||||
// ChatInputBar can navigate the same Up/Down history as the main window.
|
||||
// Index semantics are kept identical to the v0.5.x InputBacklog:
|
||||
// index 0 = oldest entry
|
||||
// index Count - 1 = newest entry
|
||||
// Push performs move-to-newest deduplication: existing entries are
|
||||
// removed before the new one is appended at the end.
|
||||
// Shared input history for all ChatInputBars (main and pop-out windows).
|
||||
// Push deduplicates: existing entries are moved to the end when re-added.
|
||||
public static class InputHistoryService
|
||||
{
|
||||
private const int MaxSize = 30;
|
||||
@@ -26,8 +20,7 @@ public static class InputHistoryService
|
||||
|
||||
var trimmed = entry.Trim();
|
||||
|
||||
// Move-to-newest: existing entries are removed before the append
|
||||
// so the same line typed twice does not occupy two history slots.
|
||||
// Move-to-newest: remove existing entry before adding at the end
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
if (_entries[i] == trimmed)
|
||||
|
||||
@@ -6,25 +6,17 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
// We pull Newtonsoft.Json into this single file for IPC compatibility:
|
||||
// Honorific serialises its TitleData with Newtonsoft (see
|
||||
// Honorific-master/IpcProvider.cs:9 and CustomTitle.cs:12). Using the
|
||||
// same library guarantees identical handling of System.Numerics.Vector3?
|
||||
// and the enum fields we ignore. Newtonsoft is a transitive dependency
|
||||
// via Dalamud, so no new NuGet entry is needed. The rest of HellionChat
|
||||
// keeps using System.Text.Json.
|
||||
// Newtonsoft.Json is used here for IPC compatibility with Honorific, which
|
||||
// serialises TitleData with it. It's a transitive Dalamud dependency — no
|
||||
// new NuGet entry needed. The rest of HellionChat uses System.Text.Json.
|
||||
internal sealed class HonorificService : IDisposable
|
||||
{
|
||||
private const string IpcNamespace = "Honorific";
|
||||
|
||||
// Major version of the Honorific IPC contract HellionChat is built against.
|
||||
// Used both by the runtime compatibility check and by the settings tab when
|
||||
// it tells the user which major version we expected, so the literal lives
|
||||
// in exactly one place.
|
||||
// Major version of the Honorific IPC contract we're built against.
|
||||
internal const uint ExpectedApiMajor = 3;
|
||||
|
||||
// IPC gates we subscribe to. Keep them as fields so Dispose can
|
||||
// unsubscribe the same instances we subscribed in the constructor.
|
||||
// IPC gates — kept as fields so Dispose can unsubscribe the same instances.
|
||||
private readonly ICallGateSubscriber<(uint, uint)> _apiVersion;
|
||||
private readonly ICallGateSubscriber<string> _getLocalCharacterTitle;
|
||||
private readonly ICallGateSubscriber<string, object> _localCharacterTitleChanged;
|
||||
@@ -39,38 +31,29 @@ internal sealed class HonorificService : IDisposable
|
||||
public bool IsAvailable { get; private set; }
|
||||
public (uint Major, uint Minor)? DetectedApiVersion { get; private set; }
|
||||
|
||||
public HonorificService(IDalamudPluginInterface pluginInterface, IPluginLog log, IFramework framework)
|
||||
public HonorificService(
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
IPluginLog log,
|
||||
IFramework framework
|
||||
)
|
||||
{
|
||||
_framework = framework;
|
||||
_log = log;
|
||||
|
||||
// Dalamud caches gate objects per-name for the lifetime of the
|
||||
// plugin interface, so we can register subscribers even when
|
||||
// Honorific isn't loaded yet — the gate just won't fire. Calling
|
||||
// InvokeFunc before Honorific is up will throw, which is why the
|
||||
// initial pull below is wrapped in try-catch.
|
||||
//
|
||||
// Thread-context: plugin constructors run on Dalamud's plugin-loader
|
||||
// thread, NOT the framework thread. Honorific's IPC handlers read
|
||||
// ObjectTable.LocalPlayer (Honorific IpcProvider.cs:61), which throws
|
||||
// "Not on main thread!" outside the framework thread. If Honorific is
|
||||
// already loaded when HellionChat starts, a synchronous InvokeFunc
|
||||
// here would surface that exception, the broad catch below would
|
||||
// mark IsAvailable=false, and OnTitleChanged's `if (!IsAvailable)`
|
||||
// gate would block every subsequent title update. We therefore
|
||||
// schedule the initial pull onto the framework thread via
|
||||
// IFramework.RunOnFrameworkThread so the IPC call sees the right
|
||||
// thread context.
|
||||
_apiVersion = pluginInterface
|
||||
.GetIpcSubscriber<(uint, uint)>($"{IpcNamespace}.ApiVersion");
|
||||
_getLocalCharacterTitle = pluginInterface
|
||||
.GetIpcSubscriber<string>($"{IpcNamespace}.GetLocalCharacterTitle");
|
||||
_localCharacterTitleChanged = pluginInterface
|
||||
.GetIpcSubscriber<string, object>($"{IpcNamespace}.LocalCharacterTitleChanged");
|
||||
_ready = pluginInterface
|
||||
.GetIpcSubscriber<object>($"{IpcNamespace}.Ready");
|
||||
_disposing = pluginInterface
|
||||
.GetIpcSubscriber<object>($"{IpcNamespace}.Disposing");
|
||||
// Gate objects are cached per-name by Dalamud and safe to register
|
||||
// before Honorific loads — they just won't fire until it does.
|
||||
// Initial pull is scheduled on the framework thread because plugin
|
||||
// constructors run on the loader thread, and Honorific's IPC handlers
|
||||
// read ObjectTable.LocalPlayer which throws off the framework thread.
|
||||
_apiVersion = pluginInterface.GetIpcSubscriber<(uint, uint)>($"{IpcNamespace}.ApiVersion");
|
||||
_getLocalCharacterTitle = pluginInterface.GetIpcSubscriber<string>(
|
||||
$"{IpcNamespace}.GetLocalCharacterTitle"
|
||||
);
|
||||
_localCharacterTitleChanged = pluginInterface.GetIpcSubscriber<string, object>(
|
||||
$"{IpcNamespace}.LocalCharacterTitleChanged"
|
||||
);
|
||||
_ready = pluginInterface.GetIpcSubscriber<object>($"{IpcNamespace}.Ready");
|
||||
_disposing = pluginInterface.GetIpcSubscriber<object>($"{IpcNamespace}.Disposing");
|
||||
|
||||
_localCharacterTitleChanged.Subscribe(OnTitleChanged);
|
||||
_ready.Subscribe(OnReady);
|
||||
@@ -81,11 +64,8 @@ internal sealed class HonorificService : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Honorific may already be gone by the time we dispose. Wrap each
|
||||
// unsubscribe so a missing gate doesn't prevent the others from
|
||||
// unsubscribing — leaking even one subscription leaves a callback
|
||||
// alive that captures `this`, which keeps the whole service alive
|
||||
// and breaks plugin reload.
|
||||
// Wrap each unsubscribe — a missing gate must not block the others.
|
||||
// Leaking a subscription keeps this service alive across plugin reloads.
|
||||
TryUnsubscribe(() => _localCharacterTitleChanged.Unsubscribe(OnTitleChanged));
|
||||
TryUnsubscribe(() => _ready.Unsubscribe(OnReady));
|
||||
TryUnsubscribe(() => _disposing.Unsubscribe(OnDisposing));
|
||||
@@ -103,9 +83,11 @@ internal sealed class HonorificService : IDisposable
|
||||
if (!_versionWarningLogged)
|
||||
{
|
||||
_log.Warning(
|
||||
"Honorific API version mismatch — expected major 3, " +
|
||||
"found {Major}.{Minor}. Disabling Honorific integration.",
|
||||
version.Item1, version.Item2);
|
||||
"Honorific API version mismatch — expected major 3, "
|
||||
+ "found {Major}.{Minor}. Disabling Honorific integration.",
|
||||
version.Item1,
|
||||
version.Item2
|
||||
);
|
||||
_versionWarningLogged = true;
|
||||
}
|
||||
IsAvailable = false;
|
||||
@@ -114,62 +96,38 @@ internal sealed class HonorificService : IDisposable
|
||||
|
||||
IsAvailable = true;
|
||||
_versionWarningLogged = false;
|
||||
// Pull the current title once at startup; from here on we rely
|
||||
// on LocalCharacterTitleChanged events.
|
||||
var json = _getLocalCharacterTitle.InvokeFunc();
|
||||
CurrentTitle = ParseTitleJson(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Honorific isn't installed or hasn't initialised yet. The Ready
|
||||
// event will give us a second chance later. Log at Debug so
|
||||
// users without Honorific don't see noise on every reload.
|
||||
// Honorific not installed or not yet initialised — Ready will retry.
|
||||
_log.Debug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
|
||||
IsAvailable = false;
|
||||
CurrentTitle = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Honorific fires LocalCharacterTitleChanged through its nameplate hook
|
||||
// (Honorific-master/Plugin.cs:665), which means we get title updates on
|
||||
// character switches automatically as soon as the new character is
|
||||
// rendered. While the user is in the character-select menu, HellionChat's
|
||||
// window is hidden by default via HideWhenNotLoggedIn (Configuration.cs:152),
|
||||
// so the stale-title window between logout and login isn't user-visible.
|
||||
private void OnTitleChanged(string json)
|
||||
{
|
||||
// Don't update cached state when we've already decided we can't trust
|
||||
// Honorific (e.g. version mismatch). Subscription stays live in case a
|
||||
// compatible Honorific reloads, in which case Ready triggers TryInitialPull
|
||||
// and sets IsAvailable back to true.
|
||||
if (!IsAvailable) return;
|
||||
// Skip updates on version mismatch; subscription stays live for reload.
|
||||
if (!IsAvailable)
|
||||
return;
|
||||
CurrentTitle = ParseTitleJson(json);
|
||||
}
|
||||
|
||||
private void OnReady()
|
||||
{
|
||||
// Honorific loaded after HellionChat; redo the version check and
|
||||
// initial pull. Idempotent on purpose — Honorific can fire Ready
|
||||
// more than once across reloads.
|
||||
//
|
||||
// Honorific's NotifyReady may dispatch from any thread, and
|
||||
// TryInitialPull eventually calls IPC handlers that read
|
||||
// ObjectTable.LocalPlayer — same "Not on main thread!" hazard as
|
||||
// the constructor path. Schedule onto the framework thread.
|
||||
// Schedule on framework thread — NotifyReady can dispatch from any thread.
|
||||
_framework.RunOnFrameworkThread(TryInitialPull);
|
||||
}
|
||||
|
||||
private void OnDisposing()
|
||||
{
|
||||
// Honorific is unloading. Drop our cached state so the header
|
||||
// hides on the next frame; subscriptions stay registered because
|
||||
// the gates may come back later (Honorific reload).
|
||||
//
|
||||
// Race-note: Honorific's NotifyDisposing calls ChangedLocalCharacterTitle(null)
|
||||
// BEFORE SendMessage on the Disposing gate (IpcProvider.cs:109-111),
|
||||
// so OnTitleChanged is expected to fire first and already null out
|
||||
// CurrentTitle. We re-clear here as belt-and-braces; should the
|
||||
// ordering ever flip, ShouldRenderSlot would still gate on IsAvailable.
|
||||
// Honorific unloading — clear cached state so the header hides next frame.
|
||||
// Subscriptions stay registered in case Honorific reloads.
|
||||
// CurrentTitle is already nulled by OnTitleChanged before this fires,
|
||||
// re-clearing here is belt-and-braces.
|
||||
CurrentTitle = null;
|
||||
IsAvailable = false;
|
||||
DetectedApiVersion = null;
|
||||
@@ -187,28 +145,15 @@ internal sealed class HonorificService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Threading note: Dalamud fires IPC events on the framework thread and
|
||||
// ImGui renders on the framework thread, so OnTitleChanged and the
|
||||
// render path that reads CurrentTitle never race — OnTitleChanged is
|
||||
// safe to keep direct (no RunOnFrameworkThread wrap needed) because
|
||||
// LocalCharacterTitleChanged delivery is framework-thread by Dalamud
|
||||
// contract. If a future change moves either side onto a worker thread,
|
||||
// switch to volatile/Interlocked for the CurrentTitle field.
|
||||
// Threading: IPC events and ImGui both run on the framework thread, so
|
||||
// OnTitleChanged and the render path never race — no volatile/Interlocked
|
||||
// needed as long as Dalamud's framework-thread delivery contract holds.
|
||||
//
|
||||
// The constructor's initial pull and OnReady, on the other hand, are
|
||||
// explicitly scheduled via IFramework.RunOnFrameworkThread because
|
||||
// they run outside that contract: the constructor executes on the
|
||||
// plugin-loader thread, and Honorific's NotifyReady can dispatch from
|
||||
// any thread. Both call paths eventually invoke IPC handlers that read
|
||||
// ObjectTable.LocalPlayer, which throws "Not on main thread!" off the
|
||||
// framework thread — see the constructor comment block for context.
|
||||
//
|
||||
// Divergence from ChatTwo/Ipc/ExtraChat.cs: that file uses `volatile`
|
||||
// on its state fields out of caution. We don't, because the framework-
|
||||
// thread delivery is the documented Dalamud contract. If the two files
|
||||
// ever need to share a threading audit, this is the place to revisit.
|
||||
// Constructor and OnReady are exceptions: they run outside that contract
|
||||
// (plugin-loader thread and Honorific's NotifyReady respectively), so both
|
||||
// use RunOnFrameworkThread to safely reach ObjectTable.LocalPlayer.
|
||||
|
||||
// --- Pure-logic helpers below; tested via HellionChat.Tests/Integrations. ---
|
||||
// --- Pure-logic helpers; tested via HellionChat.Tests/Integrations. ---
|
||||
|
||||
internal static HonorificTitleData? ParseTitleJson(string json)
|
||||
{
|
||||
@@ -233,13 +178,19 @@ internal sealed class HonorificService : IDisposable
|
||||
internal static bool ShouldRenderSlot(
|
||||
bool toggleEnabled,
|
||||
bool isAvailable,
|
||||
HonorificTitleData? title)
|
||||
HonorificTitleData? title
|
||||
)
|
||||
{
|
||||
if (!toggleEnabled) return false;
|
||||
if (!isAvailable) return false;
|
||||
if (title is null) return false;
|
||||
if (title.IsOriginal) return false;
|
||||
if (string.IsNullOrEmpty(title.Title)) return false;
|
||||
if (!toggleEnabled)
|
||||
return false;
|
||||
if (!isAvailable)
|
||||
return false;
|
||||
if (title is null)
|
||||
return false;
|
||||
if (title.IsOriginal)
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(title.Title))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,9 @@ using System.Numerics;
|
||||
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
// Local DTO mirroring Honorific's TitleData shape. We replicate the structure
|
||||
// instead of referencing Honorific.dll because a hard build-time dependency
|
||||
// would couple the two assemblies and break HellionChat at load time when
|
||||
// Honorific is missing. Glow, Color3, GradientColourSet and GradientAnimationStyle
|
||||
// are intentionally omitted — Cycle 1 renders text in the primary Color only;
|
||||
// the "Honorific Full Fidelity" backlog item adds them later as a pure
|
||||
// extension that won't break this DTO's existing consumers.
|
||||
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||
// so HellionChat loads cleanly when Honorific is absent.
|
||||
// Glow/gradient fields omitted; Cycle 1 renders primary Color only.
|
||||
internal sealed record HonorificTitleData(
|
||||
string? Title,
|
||||
bool IsPrefix,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
// External URLs for the third-party plugins HellionChat integrates with.
|
||||
// Kept separate from BrandingLinks (which is for Hellion-owned URLs) so
|
||||
// future cycles can extend this file with maintainer attribution links
|
||||
// for Moodles, NotificationMaster, ExtraChat, etc. without polluting the
|
||||
// brand-links class.
|
||||
// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
|
||||
internal static class IntegrationLinks
|
||||
{
|
||||
public const string HonorificRepo = "https://github.com/Caraxi/Honorific";
|
||||
|
||||
@@ -15,8 +15,14 @@ public sealed class ExtraChat : IDisposable
|
||||
#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; }
|
||||
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; }
|
||||
|
||||
@@ -25,16 +31,25 @@ public sealed class ExtraChat : IDisposable
|
||||
// Reference assignment is atomic on x64, but the JIT (especially Mono on Wine/Linux) needs
|
||||
// the volatile barrier to guarantee visibility across threads. See AUDIT-2026-05-05 [SEC-01].
|
||||
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
|
||||
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
|
||||
internal IReadOnlyDictionary<string, uint> ChannelCommandColours =>
|
||||
ChannelCommandColoursInternal;
|
||||
|
||||
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
||||
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 = 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);
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using HellionChat.Code;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using HellionChat.Code;
|
||||
|
||||
namespace HellionChat.Ipc;
|
||||
|
||||
using ChatInputState = (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType);
|
||||
using ChatInputState = (
|
||||
bool InputVisible,
|
||||
bool InputFocused,
|
||||
bool HasText,
|
||||
bool IsTyping,
|
||||
int TextLength,
|
||||
ChatType ChannelType
|
||||
);
|
||||
|
||||
internal sealed class TypingIpc : IDisposable
|
||||
{
|
||||
@@ -19,8 +26,12 @@ internal sealed class TypingIpc : IDisposable
|
||||
{
|
||||
Plugin = plugin;
|
||||
|
||||
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>("HellionChat.GetChatInputState");
|
||||
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("HellionChat.ChatInputStateChanged");
|
||||
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||
"HellionChat.GetChatInputState"
|
||||
);
|
||||
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>(
|
||||
"HellionChat.ChatInputStateChanged"
|
||||
);
|
||||
|
||||
StateQueryGate.RegisterFunc(GetState);
|
||||
}
|
||||
@@ -30,19 +41,22 @@ internal sealed class TypingIpc : IDisposable
|
||||
var log = Plugin.ChatLogWindow;
|
||||
|
||||
var usedChannel = Plugin.CurrentTab.CurrentChannel;
|
||||
var inputChannel = usedChannel.UseTempChannel ? usedChannel.TempChannel : usedChannel.Channel;
|
||||
var inputChannel = usedChannel.UseTempChannel
|
||||
? usedChannel.TempChannel
|
||||
: usedChannel.Channel;
|
||||
var channelType = inputChannel.ToChatType();
|
||||
|
||||
return (InputVisible: !log.IsHidden,
|
||||
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);
|
||||
ChannelType: channelType
|
||||
);
|
||||
}
|
||||
|
||||
private ChatInputState GetState()
|
||||
=> BuildState();
|
||||
private ChatInputState GetState() => BuildState();
|
||||
|
||||
internal void Update()
|
||||
{
|
||||
|
||||
@@ -9,7 +9,15 @@ 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; }
|
||||
private ICallGateProvider<
|
||||
string,
|
||||
PlayerPayload?,
|
||||
ulong,
|
||||
Payload?,
|
||||
SeString?,
|
||||
SeString?,
|
||||
object?
|
||||
> InvokeGate { get; }
|
||||
|
||||
internal List<string> Registered { get; } = [];
|
||||
|
||||
@@ -23,12 +31,27 @@ internal sealed class IpcManager : IDisposable
|
||||
UnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>("HellionChat.Unregister");
|
||||
UnregisterGate.RegisterAction(Unregister);
|
||||
|
||||
InvokeGate = Plugin.Interface.GetIpcProvider<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?>("HellionChat.Invoke");
|
||||
InvokeGate = Plugin.Interface.GetIpcProvider<
|
||||
string,
|
||||
PlayerPayload?,
|
||||
ulong,
|
||||
Payload?,
|
||||
SeString?,
|
||||
SeString?,
|
||||
object?
|
||||
>("HellionChat.Invoke");
|
||||
|
||||
AvailableGate.SendMessage();
|
||||
}
|
||||
|
||||
internal void Invoke(string id, PlayerPayload? sender, ulong contentId, Payload? payload, SeString? senderString, SeString? content)
|
||||
internal void Invoke(
|
||||
string id,
|
||||
PlayerPayload? sender,
|
||||
ulong contentId,
|
||||
Payload? payload,
|
||||
SeString? senderString,
|
||||
SeString? content
|
||||
)
|
||||
{
|
||||
InvokeGate.SendMessage(id, sender, contentId, payload, senderString, content);
|
||||
}
|
||||
|
||||
+120
-36
@@ -1,12 +1,12 @@
|
||||
using System.Text;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -33,7 +33,16 @@ public partial class Message
|
||||
public Dictionary<Guid, float?> Height { get; } = new();
|
||||
public Dictionary<Guid, bool> IsVisible { get; } = new();
|
||||
|
||||
public Message(ulong receiver, ulong contentId, ulong accountId, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource)
|
||||
public Message(
|
||||
ulong receiver,
|
||||
ulong contentId,
|
||||
ulong accountId,
|
||||
ChatCode code,
|
||||
List<Chunk> sender,
|
||||
List<Chunk> content,
|
||||
SeString senderSource,
|
||||
SeString contentSource
|
||||
)
|
||||
{
|
||||
var extraChatChannel = ExtractExtraChatChannel(contentSource);
|
||||
Receiver = receiver;
|
||||
@@ -56,7 +65,18 @@ public partial class Message
|
||||
chunk.Message = this;
|
||||
}
|
||||
|
||||
public Message(Guid id, ulong receiver, ulong contentId, DateTimeOffset date, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource, Guid extraChatChannel)
|
||||
public Message(
|
||||
Guid id,
|
||||
ulong receiver,
|
||||
ulong contentId,
|
||||
DateTimeOffset date,
|
||||
ChatCode code,
|
||||
List<Chunk> sender,
|
||||
List<Chunk> content,
|
||||
SeString senderSource,
|
||||
SeString contentSource,
|
||||
Guid extraChatChannel
|
||||
)
|
||||
{
|
||||
Id = id;
|
||||
Receiver = receiver;
|
||||
@@ -82,7 +102,11 @@ public partial class Message
|
||||
return new Message(0, 0, 0, code, [], content, new SeString(), new SeString());
|
||||
}
|
||||
|
||||
public bool Matches(Dictionary<ChatType, (ChatSource Source, ChatSource Target)> channels, bool allExtraChatChannels, HashSet<Guid> extraChatChannels)
|
||||
public bool Matches(
|
||||
Dictionary<ChatType, (ChatSource Source, ChatSource Target)> channels,
|
||||
bool allExtraChatChannels,
|
||||
HashSet<Guid> extraChatChannels
|
||||
)
|
||||
{
|
||||
if (ExtraChatChannel != Guid.Empty)
|
||||
return allExtraChatChannels || extraChatChannels.Contains(ExtraChatChannel);
|
||||
@@ -90,16 +114,21 @@ public partial class Message
|
||||
var source = (ChatSource)(1 << (int)Code.Source);
|
||||
var target = (ChatSource)(1 << (int)Code.Target);
|
||||
return Code.Type.IsGm()
|
||||
|| channels.TryGetValue(Code.Type, out var sources)
|
||||
&& (Code.Source is 0 || sources.Source.HasFlag(source) || sources.Target.HasFlag(target));
|
||||
|| channels.TryGetValue(Code.Type, out var sources)
|
||||
&& (
|
||||
Code.Source is 0
|
||||
|| sources.Source.HasFlag(source)
|
||||
|| sources.Target.HasFlag(target)
|
||||
);
|
||||
}
|
||||
|
||||
private int GenerateHash()
|
||||
{
|
||||
var hash = SortCodeV2.GetHashCode()
|
||||
^ ExtraChatChannel.GetHashCode()
|
||||
^ string.Join("", Sender.Select(c => c.StringValue())).GetHashCode()
|
||||
^ string.Join("", Content.Select(c => c.StringValue())).GetHashCode();
|
||||
var hash =
|
||||
SortCodeV2.GetHashCode()
|
||||
^ ExtraChatChannel.GetHashCode()
|
||||
^ string.Join("", Sender.Select(c => c.StringValue())).GetHashCode()
|
||||
^ string.Join("", Content.Select(c => c.StringValue())).GetHashCode();
|
||||
|
||||
if (Plugin.Config.CollapseKeepUniqueLinks)
|
||||
{
|
||||
@@ -146,13 +175,15 @@ public partial class Message
|
||||
}
|
||||
|
||||
var nextIsAutoTranslate = false;
|
||||
var checkForEmotes = (Code.IsPlayerMessage() || extraChatChannel != Guid.Empty) && Plugin.Config.ShowEmotes;
|
||||
var checkForEmotes =
|
||||
(Code.IsPlayerMessage() || extraChatChannel != Guid.Empty) && Plugin.Config.ShowEmotes;
|
||||
foreach (var chunk in oldChunks)
|
||||
{
|
||||
// Use as is if it's not a text chunk, it already has a payload, or is auto translate
|
||||
if (chunk is not TextChunk text || chunk.Link != null || nextIsAutoTranslate)
|
||||
{
|
||||
nextIsAutoTranslate = chunk is IconChunk { Icon: BitmapFontIcon.AutoTranslateBegin };
|
||||
nextIsAutoTranslate =
|
||||
chunk is IconChunk { Icon: BitmapFontIcon.AutoTranslateBegin };
|
||||
|
||||
// No need to call AddChunkWithMessage here since the chunk
|
||||
// already has the Message field set.
|
||||
@@ -173,15 +204,23 @@ public partial class Message
|
||||
var word = wordBuilder.ToString();
|
||||
wordBuilder.Clear();
|
||||
|
||||
|
||||
var wordUsed = false;
|
||||
var tokenUsed = false;
|
||||
|
||||
if (checkForEmotes && EmoteCache.Exists(word) && !Plugin.Config.BlockedEmotes.Contains(word))
|
||||
if (
|
||||
checkForEmotes
|
||||
&& EmoteCache.Exists(word)
|
||||
&& !Plugin.Config.BlockedEmotes.Contains(word)
|
||||
)
|
||||
{
|
||||
// Add the previous sentence before adding the emote
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk, sentenceBuilder.ToString()));
|
||||
AddChunkWithMessage(new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), word) { FallbackColour = text.FallbackColour });
|
||||
AddChunkWithMessage(
|
||||
new TextChunk(chunk.Source, EmotePayload.ResolveEmote(word), word)
|
||||
{
|
||||
FallbackColour = text.FallbackColour,
|
||||
}
|
||||
);
|
||||
|
||||
wordUsed = true;
|
||||
sentenceBuilder.Clear();
|
||||
@@ -190,15 +229,31 @@ public partial class Message
|
||||
if (token.TokenType == Tokenizer.TokenType.UrlString)
|
||||
{
|
||||
// Add the previous sentence before adding the url
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, sentenceBuilder.Append(!wordUsed ? word : "").ToString()));
|
||||
AddChunkWithMessage(
|
||||
text.NewWithStyle(
|
||||
chunk.Source,
|
||||
chunk.Link,
|
||||
sentenceBuilder.Append(!wordUsed ? word : "").ToString()
|
||||
)
|
||||
);
|
||||
try
|
||||
{
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, UriPayload.ResolveUri(token.Value), token.Value));
|
||||
AddChunkWithMessage(
|
||||
text.NewWithStyle(
|
||||
chunk.Source,
|
||||
UriPayload.ResolveUri(token.Value),
|
||||
token.Value
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, token.Value));
|
||||
Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'");
|
||||
AddChunkWithMessage(
|
||||
text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
|
||||
);
|
||||
Plugin.Log.Debug(
|
||||
$"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
|
||||
);
|
||||
}
|
||||
|
||||
wordUsed = true;
|
||||
@@ -215,7 +270,12 @@ public partial class Message
|
||||
}
|
||||
|
||||
// End of string reached, we add our leftover
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk, sentenceBuilder.Append(!wordUsed ? word : "").ToString()));
|
||||
AddChunkWithMessage(
|
||||
text.NewWithStyle(
|
||||
chunk,
|
||||
sentenceBuilder.Append(!wordUsed ? word : "").ToString()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,20 +341,30 @@ public partial class Message
|
||||
< 500_000 => ItemKind.Normal,
|
||||
< 1_000_000 => ItemKind.Collectible,
|
||||
< 2_000_000 => ItemKind.Hq,
|
||||
_ => ItemKind.EventItem
|
||||
_ => ItemKind.EventItem,
|
||||
};
|
||||
|
||||
var name = kind != ItemKind.EventItem
|
||||
? Sheets.ItemSheet.GetRow(item.ItemId).Name.ToString()
|
||||
: Sheets.EventItemSheet.GetRow(item.ItemId).Name.ToString();
|
||||
var name =
|
||||
kind != ItemKind.EventItem
|
||||
? Sheets.ItemSheet.GetRow(item.ItemId).Name.ToString()
|
||||
: Sheets.EventItemSheet.GetRow(item.ItemId).Name.ToString();
|
||||
|
||||
var link = new ItemPayload(item.ItemId, kind, $"{SeIconChar.LinkMarker.ToIconChar()}{name}");
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, link.DisplayName ?? "Unknown"));
|
||||
var link = new ItemPayload(
|
||||
item.ItemId,
|
||||
kind,
|
||||
$"{SeIconChar.LinkMarker.ToIconChar()}{name}"
|
||||
);
|
||||
AddChunkWithMessage(
|
||||
text.NewWithStyle(chunk.Source, link, link.DisplayName ?? "Unknown")
|
||||
);
|
||||
}
|
||||
else if (split == "<status>")
|
||||
{
|
||||
var statusId = AgentChatLog.Instance()->ContextStatusId;
|
||||
if (statusId == 0 || !Sheets.StatusSheet.TryGetRow(statusId, out var statusRow))
|
||||
if (
|
||||
statusId == 0
|
||||
|| !Sheets.StatusSheet.TryGetRow(statusId, out var statusRow)
|
||||
)
|
||||
{
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
||||
continue;
|
||||
@@ -305,7 +375,7 @@ public partial class Message
|
||||
{
|
||||
1 => $"{SeIconChar.Buff.ToIconString()}{nameValue}",
|
||||
2 => $"{SeIconChar.Debuff.ToIconString()}{nameValue}",
|
||||
_ => nameValue
|
||||
_ => nameValue,
|
||||
};
|
||||
|
||||
var link = new StatusPayload(statusId);
|
||||
@@ -321,13 +391,27 @@ public partial class Message
|
||||
}
|
||||
|
||||
var mapCoords = agentMap->FlagMapMarkers[0];
|
||||
var rawX = (int)(MathF.Round(mapCoords.XFloat, 3, MidpointRounding.AwayFromZero) * 1000);
|
||||
var rawY = (int)(MathF.Round(mapCoords.YFloat, 3, MidpointRounding.AwayFromZero) * 1000);
|
||||
var rawX = (int)(
|
||||
MathF.Round(mapCoords.XFloat, 3, MidpointRounding.AwayFromZero) * 1000
|
||||
);
|
||||
var rawY = (int)(
|
||||
MathF.Round(mapCoords.YFloat, 3, MidpointRounding.AwayFromZero) * 1000
|
||||
);
|
||||
|
||||
var link = new MapLinkPayload(mapCoords.TerritoryId, mapCoords.MapId, rawX, rawY);
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, $"{SeIconChar.LinkMarker.ToIconChar()}{link.PlaceName} {link.CoordinateString}"));
|
||||
var link = new MapLinkPayload(
|
||||
mapCoords.TerritoryId,
|
||||
mapCoords.MapId,
|
||||
rawX,
|
||||
rawY
|
||||
);
|
||||
AddChunkWithMessage(
|
||||
text.NewWithStyle(
|
||||
chunk.Source,
|
||||
link,
|
||||
$"{SeIconChar.LinkMarker.ToIconChar()}{link.PlaceName} {link.CoordinateString}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Game.Chat;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
@@ -11,6 +8,9 @@ using Dalamud.Hooking;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Lumina.Text.Expressions;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
@@ -27,16 +27,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
|
||||
private ulong LastContentId { get; set; }
|
||||
|
||||
// Messages go into the PendingSync queue first, which will be consumed one
|
||||
// at a time in the main thread. This is to delay the async processing until
|
||||
// after we've received the content ID from the ContentIdResolver hook.
|
||||
//
|
||||
// After that, the message is enqueued in the PendingAsync queue, which will
|
||||
// be consumed in a separate thread and perform more processing (emotes,
|
||||
// URLs) as well as inserting the message into the database.
|
||||
// LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last
|
||||
// every hook call. Queue<T>.Last() is the LINQ extension and walks the
|
||||
// whole queue (O(n)); LinkedList<T>.Last is an O(1) node reference.
|
||||
// PendingSync (main thread) → PendingAsync (worker thread); LinkedList for O(1) Last access
|
||||
private LinkedList<PendingMessage> PendingSync { get; } = [];
|
||||
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
|
||||
private readonly Thread PendingMessageThread;
|
||||
@@ -53,11 +44,8 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs hook. Fires after a fully processed
|
||||
// message has been routed to all matching persistent tabs and stored
|
||||
// in the database. The AutoTellTabsService subscribes to spawn or
|
||||
// refresh temp tabs without having to wedge itself into ProcessMessage
|
||||
// directly.
|
||||
// Auto-Tell-Tabs hook: fires after a message is processed and stored, allowing
|
||||
// AutoTellTabsService to spawn or refresh temp tabs without coupling.
|
||||
public event Action<Message>? MessageProcessed;
|
||||
|
||||
internal unsafe MessageManager(Plugin plugin)
|
||||
@@ -66,15 +54,19 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
Store = new MessageStore(DatabasePath());
|
||||
|
||||
// IsBackground so a stuck worker never blocks plugin unload.
|
||||
// Cooperative cancel via PendingThreadCancellationToken first, background flag is the safety net.
|
||||
PendingMessageThread = new Thread(() => ProcessPendingMessages(PendingThreadCancellationToken.Token))
|
||||
PendingMessageThread = new Thread(() =>
|
||||
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||
)
|
||||
{
|
||||
IsBackground = true,
|
||||
};
|
||||
PendingMessageThread.Start();
|
||||
|
||||
ContentIdResolverHook = Plugin.GameInteropProvider.HookFromAddress<RaptureLogModule.Delegates.AddMsgSourceEntry>(RaptureLogModule.MemberFunctionPointers.AddMsgSourceEntry, ContentIdResolver);
|
||||
ContentIdResolverHook =
|
||||
Plugin.GameInteropProvider.HookFromAddress<RaptureLogModule.Delegates.AddMsgSourceEntry>(
|
||||
RaptureLogModule.MemberFunctionPointers.AddMsgSourceEntry,
|
||||
ContentIdResolver
|
||||
);
|
||||
ContentIdResolverHook.Enable();
|
||||
|
||||
Plugin.ChatGui.ChatMessageUnhandled += ChatMessage;
|
||||
@@ -100,12 +92,10 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
if (PendingMessageThread.IsAlive)
|
||||
Plugin.Log.Warning(
|
||||
"PendingMessageThread did not observe cancellation within 10s. " +
|
||||
"Worker remains on a background thread; next plugin reload releases it. " +
|
||||
"If this recurs, file a bug with /xllog after the previous reload.");
|
||||
"PendingMessageThread did not observe cancellation within 10s. "
|
||||
+ "Worker remains on background thread; next plugin reload releases it."
|
||||
);
|
||||
|
||||
// CTS owns an unmanaged WaitHandle; dispose even if the worker is
|
||||
// alive — it checks IsCancellationRequested via the linked token.
|
||||
PendingThreadCancellationToken.Dispose();
|
||||
|
||||
Store.Dispose();
|
||||
@@ -159,12 +149,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
internal void ClearAllTabs()
|
||||
{
|
||||
// Hellion Chat — TempTabs haben keine DB-Persistenz (session-only,
|
||||
// direkt vom AutoTellTabsService befüllt). Ein Clear+Refilter würde
|
||||
// sie leer hinterlassen weil FilterAllTabs nichts aus der DB
|
||||
// findet — Tells sind oft durch Privacy-Filter blockiert oder
|
||||
// schlicht session-flüchtig. TempTabs vom Clear-Pfad ausschließen
|
||||
// damit Settings-Save den Tell-Verlauf nicht zerstört.
|
||||
// TempTabs are session-only (not persisted); exclude them to preserve Tell history
|
||||
foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
|
||||
tab.Clear();
|
||||
}
|
||||
@@ -177,27 +162,25 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
using var messages = Store.GetMostRecentMessages(CurrentContentId, since);
|
||||
|
||||
// We store the pending messages to be added to the chat log in a
|
||||
// temporary list, and apply them all at once after filtering.
|
||||
// TempTabs werden ausgeschlossen — sie bleiben live-state aus dem
|
||||
// AutoTellTabsService, ein DB-Refilter würde sie nur partial
|
||||
// wiederherstellen falls Tells in DB liegen, oder leer lassen wenn
|
||||
// Privacy-Filter sie blockiert hat.
|
||||
var pendingTabs = Plugin.Config.Tabs.Where(t => !t.IsTempTab).Select(tab => (tab, new List<Message>())).ToList();
|
||||
// TempTabs are excluded; they maintain live state from AutoTellTabsService
|
||||
var pendingTabs = Plugin
|
||||
.Config.Tabs.Where(t => !t.IsTempTab)
|
||||
.Select(tab => (tab, new List<Message>()))
|
||||
.ToList();
|
||||
foreach (var message in messages)
|
||||
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
|
||||
pendingMessages.Add(message);
|
||||
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
|
||||
pendingMessages.Add(message);
|
||||
|
||||
// Apply the messages to the chat log in one go.
|
||||
// Apply messages to chat log all at once.
|
||||
foreach (var (tab, pendingMessages) in pendingTabs)
|
||||
tab.Messages.AddSortPrune(pendingMessages, MessageDisplayLimit);
|
||||
|
||||
if (!messages.DidError) return;
|
||||
if (!messages.DidError)
|
||||
return;
|
||||
|
||||
WrapperUtil.AddNotification(Language.LoadMessages_Error, NotificationType.Error);
|
||||
|
||||
// Mark the failed messages as deleted so we don't try to load them
|
||||
// again.
|
||||
// Mark failed messages as deleted to prevent retry attempts
|
||||
var failedIds = messages.FailedMessageIds();
|
||||
Plugin.Log.Info($"Marking {failedIds.Count} messages as deleted due to parse failures");
|
||||
foreach (var msgId in messages.FailedMessageIds())
|
||||
@@ -226,6 +209,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
|
||||
public (SeString? Sender, SeString? Message) LastMessage = (null, null);
|
||||
|
||||
private void ChatMessage(IChatMessage message)
|
||||
{
|
||||
LastMessage = (message.Sender, message.Message);
|
||||
@@ -244,21 +228,29 @@ internal class MessageManager : IAsyncDisposable
|
||||
// Update colour codes.
|
||||
GlobalParametersCache.Refresh();
|
||||
|
||||
// We delay messages to be handed off to the async processing thread
|
||||
// in the next tick, otherwise we can't get the content ID from the hook
|
||||
// below.
|
||||
// Delay to next tick to get content ID from ContentIdResolver hook
|
||||
PendingSync.AddLast(pendingMessage);
|
||||
}
|
||||
|
||||
// This hook is called immediately after receiving a message with the
|
||||
// message's content ID. If multiple messages are received in the same tick,
|
||||
// this will be called for each message immediately after ChatMessage is
|
||||
// called for each message.
|
||||
private unsafe void ContentIdResolver(RaptureLogModule* agent, ulong contentId, ulong accountId, int messageIndex, ushort worldId, ushort chatType)
|
||||
private unsafe void ContentIdResolver(
|
||||
RaptureLogModule* agent,
|
||||
ulong contentId,
|
||||
ulong accountId,
|
||||
int messageIndex,
|
||||
ushort worldId,
|
||||
ushort chatType
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
|
||||
ContentIdResolverHook?.Original(
|
||||
agent,
|
||||
contentId,
|
||||
accountId,
|
||||
messageIndex,
|
||||
worldId,
|
||||
chatType
|
||||
);
|
||||
if (PendingSync.Last is not { } last)
|
||||
return;
|
||||
|
||||
@@ -273,7 +265,11 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
private void ProcessMessage(PendingMessage pendingMessage)
|
||||
{
|
||||
var chatCode = new ChatCode(pendingMessage.LogKind, pendingMessage.SourceKind, pendingMessage.TargetKind);
|
||||
var chatCode = new ChatCode(
|
||||
pendingMessage.LogKind,
|
||||
pendingMessage.SourceKind,
|
||||
pendingMessage.TargetKind
|
||||
);
|
||||
|
||||
NameFormatting? formatting = null;
|
||||
if (pendingMessage.Sender.Payloads.Count > 0)
|
||||
@@ -282,13 +278,36 @@ internal class MessageManager : IAsyncDisposable
|
||||
var senderChunks = new List<Chunk>();
|
||||
if (formatting is { IsPresent: true })
|
||||
{
|
||||
senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.Before) { FallbackColour = chatCode.Type });
|
||||
senderChunks.AddRange(ChunkUtil.ToChunks(pendingMessage.Sender, ChunkSource.Sender, chatCode.Type));
|
||||
senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.After) { FallbackColour = chatCode.Type });
|
||||
senderChunks.Add(
|
||||
new TextChunk(ChunkSource.None, null, formatting.Before)
|
||||
{
|
||||
FallbackColour = chatCode.Type,
|
||||
}
|
||||
);
|
||||
senderChunks.AddRange(
|
||||
ChunkUtil.ToChunks(pendingMessage.Sender, ChunkSource.Sender, chatCode.Type)
|
||||
);
|
||||
senderChunks.Add(
|
||||
new TextChunk(ChunkSource.None, null, formatting.After)
|
||||
{
|
||||
FallbackColour = chatCode.Type,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var contentChunks = ChunkUtil.ToChunks(pendingMessage.Content, ChunkSource.Content, chatCode.Type).ToList();
|
||||
var message = new Message(CurrentContentId, pendingMessage.ContentId, pendingMessage.AccountId, chatCode, senderChunks, contentChunks, pendingMessage.Sender, pendingMessage.Content);
|
||||
var contentChunks = ChunkUtil
|
||||
.ToChunks(pendingMessage.Content, ChunkSource.Content, chatCode.Type)
|
||||
.ToList();
|
||||
var message = new Message(
|
||||
CurrentContentId,
|
||||
pendingMessage.ContentId,
|
||||
pendingMessage.AccountId,
|
||||
chatCode,
|
||||
senderChunks,
|
||||
contentChunks,
|
||||
pendingMessage.Sender,
|
||||
pendingMessage.Content
|
||||
);
|
||||
|
||||
if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle())
|
||||
Store.UpsertMessage(message);
|
||||
@@ -296,7 +315,9 @@ internal class MessageManager : IAsyncDisposable
|
||||
var currentMatches = Plugin.CurrentTab.Matches(message);
|
||||
foreach (var tab in Plugin.Config.Tabs)
|
||||
{
|
||||
var unread = !(tab.UnreadMode == UnreadMode.Unseen && Plugin.CurrentTab != tab && currentMatches);
|
||||
var unread = !(
|
||||
tab.UnreadMode == UnreadMode.Unseen && Plugin.CurrentTab != tab && currentMatches
|
||||
);
|
||||
|
||||
if (tab.Matches(message))
|
||||
tab.AddMessage(message, unread);
|
||||
@@ -313,16 +334,12 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
internal static NameFormatting Empty()
|
||||
{
|
||||
return new NameFormatting { IsPresent = false, };
|
||||
return new NameFormatting { IsPresent = false };
|
||||
}
|
||||
|
||||
internal static NameFormatting Of(string before, string after)
|
||||
{
|
||||
return new NameFormatting
|
||||
{
|
||||
Before = before,
|
||||
After = after,
|
||||
};
|
||||
return new NameFormatting { Before = before, After = after };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +374,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
var after = formats
|
||||
.GetRange(firstStringParam + 1, secondStringParam - firstStringParam)
|
||||
.Where(payload => payload.Type == ReadOnlySePayloadType.Text)
|
||||
.Select(text => Encoding.UTF8.GetString(text.Body.Span)); // Can't use `ToString()` as it defaults to macro
|
||||
.Select(text => Encoding.UTF8.GetString(text.Body.Span));
|
||||
|
||||
var nameFormatting = NameFormatting.Of(string.Join("", before), string.Join("", after));
|
||||
Formats[type] = nameFormatting;
|
||||
|
||||
+173
-61
@@ -1,15 +1,14 @@
|
||||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Data.Common;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.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;
|
||||
|
||||
@@ -36,7 +35,11 @@ internal enum PayloadMessagePackType : byte
|
||||
|
||||
public class PayloadMessagePackFormatter : IMessagePackFormatter<Payload?>
|
||||
{
|
||||
public void Serialize(ref MessagePackWriter writer, Payload? value, MessagePackSerializerOptions options)
|
||||
public void Serialize(
|
||||
ref MessagePackWriter writer,
|
||||
Payload? value,
|
||||
MessagePackSerializerOptions options
|
||||
)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
@@ -100,14 +103,22 @@ public class PayloadMessagePackFormatter : IMessagePackFormatter<Payload?>
|
||||
|
||||
public class SeStringMessagePackFormatter : IMessagePackFormatter<SeString?>
|
||||
{
|
||||
public void Serialize(ref MessagePackWriter writer, SeString? value, MessagePackSerializerOptions options)
|
||||
public void Serialize(
|
||||
ref MessagePackWriter writer,
|
||||
SeString? value,
|
||||
MessagePackSerializerOptions options
|
||||
)
|
||||
{
|
||||
options.Resolver.GetFormatter<List<Payload>>()!.Serialize(ref writer, value?.Payloads ?? [], 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));
|
||||
return new SeString(
|
||||
options.Resolver.GetFormatter<List<Payload>>()!.Deserialize(ref reader, options)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +130,13 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private SqliteConnection Connection { get; set; }
|
||||
|
||||
internal static readonly MessagePackSerializerOptions MsgPackOptions = MessagePackSerializerOptions.Standard
|
||||
.WithResolver(CompositeResolver.Create([new PayloadMessagePackFormatter(), new SeStringMessagePackFormatter()], [StandardResolver.Instance]));
|
||||
internal static readonly MessagePackSerializerOptions MsgPackOptions =
|
||||
MessagePackSerializerOptions.Standard.WithResolver(
|
||||
CompositeResolver.Create(
|
||||
[new PayloadMessagePackFormatter(), new SeStringMessagePackFormatter()],
|
||||
[StandardResolver.Instance]
|
||||
)
|
||||
);
|
||||
|
||||
internal MessageStore(string dbPath)
|
||||
{
|
||||
@@ -193,7 +209,8 @@ internal class MessageStore : IDisposable
|
||||
private void Migrate0()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 0: Creating tables");
|
||||
Connection.Execute(@"
|
||||
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)
|
||||
@@ -210,7 +227,8 @@ internal class MessageStore : IDisposable
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages (Receiver);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_date ON messages (Date);
|
||||
");
|
||||
"
|
||||
);
|
||||
|
||||
SetMigrationVersion(0);
|
||||
}
|
||||
@@ -218,10 +236,12 @@ internal class MessageStore : IDisposable
|
||||
private void Migrate1()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 1: Adding Deleted column");
|
||||
Connection.Execute(@"
|
||||
Connection.Execute(
|
||||
@"
|
||||
-- Migration 1: Add Deleted column
|
||||
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
||||
");
|
||||
"
|
||||
);
|
||||
|
||||
SetMigrationVersion(1);
|
||||
}
|
||||
@@ -229,11 +249,13 @@ internal class MessageStore : IDisposable
|
||||
private void Migrate2()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 2: Adding Channel generated column");
|
||||
Connection.Execute(@"
|
||||
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);
|
||||
}
|
||||
@@ -263,12 +285,15 @@ internal class MessageStore : IDisposable
|
||||
// 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");
|
||||
Plugin.Log.Information(
|
||||
"Migration 3: schema already migrated, only bumping user_version"
|
||||
);
|
||||
SetMigrationVersion(3);
|
||||
return;
|
||||
}
|
||||
|
||||
Connection.Execute(@"
|
||||
Connection.Execute(
|
||||
@"
|
||||
-- Migration 3: Fix log kinds to fit the new format
|
||||
-- Add new ChatType, SourceKind, TargetKind (byte), SortCodeV2
|
||||
-- Migrate OldChatColumn
|
||||
@@ -293,7 +318,8 @@ internal class MessageStore : IDisposable
|
||||
ALTER TABLE messages DROP COLUMN Channel;
|
||||
ALTER TABLE messages DROP COLUMN Code;
|
||||
ALTER TABLE messages DROP COLUMN SortCode;
|
||||
");
|
||||
"
|
||||
);
|
||||
|
||||
SetMigrationVersion(3);
|
||||
}
|
||||
@@ -325,7 +351,8 @@ internal class MessageStore : IDisposable
|
||||
{
|
||||
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.CommandText =
|
||||
"SELECT ChatType, COUNT(*) FROM messages WHERE deleted = false GROUP BY ChatType;";
|
||||
cmd.CommandTimeout = 120;
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
@@ -343,13 +370,22 @@ internal class MessageStore : IDisposable
|
||||
/// 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)
|
||||
internal long DeleteByRetentionPolicy(
|
||||
IReadOnlyDictionary<int, int> chatTypeDaysMap,
|
||||
int defaultDays
|
||||
)
|
||||
{
|
||||
if (defaultDays < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(defaultDays), "Negative retention is not allowed.");
|
||||
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.");
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(chatTypeDaysMap),
|
||||
"Negative retention is not allowed."
|
||||
);
|
||||
|
||||
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
@@ -381,10 +417,13 @@ internal class MessageStore : IDisposable
|
||||
var defaultCutoff = nowMs - defaultDays * 86400000L;
|
||||
cmd.Parameters.AddWithValue("$defaultCutoff", defaultCutoff);
|
||||
|
||||
var explicitPlaceholders = chatTypeDaysMap.Count > 0
|
||||
? BindIntList(cmd, "explicit", chatTypeDaysMap.Keys)
|
||||
: "-1"; // empty list would produce invalid SQL
|
||||
clauses.Add($"(ChatType NOT IN ({explicitPlaceholders}) AND Date < $defaultCutoff)");
|
||||
var explicitPlaceholders =
|
||||
chatTypeDaysMap.Count > 0
|
||||
? BindIntList(cmd, "explicit", chatTypeDaysMap.Keys)
|
||||
: "-1"; // empty list would produce invalid SQL
|
||||
clauses.Add(
|
||||
$"(ChatType NOT IN ({explicitPlaceholders}) AND Date < $defaultCutoff)"
|
||||
);
|
||||
}
|
||||
|
||||
if (clauses.Count == 0)
|
||||
@@ -411,7 +450,9 @@ internal class MessageStore : IDisposable
|
||||
{
|
||||
// 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.");
|
||||
throw new InvalidOperationException(
|
||||
"CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe."
|
||||
);
|
||||
}
|
||||
|
||||
long deleted;
|
||||
@@ -428,15 +469,19 @@ internal class MessageStore : IDisposable
|
||||
|
||||
internal void PerformMaintenance()
|
||||
{
|
||||
Connection.Execute(@"
|
||||
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()
|
||||
@@ -461,7 +506,8 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
INSERT INTO messages (
|
||||
Id,
|
||||
Receiver,
|
||||
@@ -513,10 +559,22 @@ internal class MessageStore : IDisposable
|
||||
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(
|
||||
"$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();
|
||||
@@ -532,7 +590,8 @@ internal class MessageStore : IDisposable
|
||||
internal MessageEnumerator StreamForExport(
|
||||
IReadOnlyCollection<int>? chatTypes,
|
||||
DateTimeOffset? from,
|
||||
DateTimeOffset? to)
|
||||
DateTimeOffset? to
|
||||
)
|
||||
{
|
||||
var cmd = Connection.CreateCommand();
|
||||
|
||||
@@ -544,7 +603,8 @@ internal class MessageStore : IDisposable
|
||||
if (to is not null)
|
||||
clauses.Add("Date <= $To");
|
||||
|
||||
cmd.CommandText = @"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
@@ -559,7 +619,9 @@ internal class MessageStore : IDisposable
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
FROM messages
|
||||
WHERE " + string.Join(" AND ", clauses) + @"
|
||||
WHERE "
|
||||
+ string.Join(" AND ", clauses)
|
||||
+ @"
|
||||
ORDER BY Date ASC;";
|
||||
cmd.CommandTimeout = 600;
|
||||
|
||||
@@ -577,7 +639,11 @@ internal class MessageStore : IDisposable
|
||||
/// <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)
|
||||
internal MessageEnumerator GetMostRecentMessages(
|
||||
ulong? receiver = null,
|
||||
DateTimeOffset? since = null,
|
||||
int count = MessageQueryLimit
|
||||
)
|
||||
{
|
||||
List<string> whereClauses = ["deleted = false"];
|
||||
if (receiver != null)
|
||||
@@ -590,7 +656,8 @@ internal class MessageStore : IDisposable
|
||||
var cmd = Connection.CreateCommand();
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText = @"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
@@ -607,7 +674,9 @@ internal class MessageStore : IDisposable
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
FROM messages
|
||||
" + whereClause + @"
|
||||
"
|
||||
+ whereClause
|
||||
+ @"
|
||||
ORDER BY Date DESC
|
||||
LIMIT $Count
|
||||
)
|
||||
@@ -645,7 +714,8 @@ internal class MessageStore : IDisposable
|
||||
string senderName,
|
||||
uint senderWorld,
|
||||
int limit,
|
||||
int sqlScanLimit = 500)
|
||||
int sqlScanLimit = 500
|
||||
)
|
||||
{
|
||||
if (limit <= 0)
|
||||
{
|
||||
@@ -653,7 +723,8 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
@@ -714,7 +785,12 @@ internal class MessageStore : IDisposable
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
internal long CountDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
|
||||
internal long CountDateRange(
|
||||
DateTime after,
|
||||
DateTime before,
|
||||
IEnumerable<byte> channels,
|
||||
ulong? receiver = null
|
||||
)
|
||||
{
|
||||
using var cmd = Connection.CreateCommand();
|
||||
|
||||
@@ -729,7 +805,8 @@ internal class MessageStore : IDisposable
|
||||
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText = @"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT COUNT(*)
|
||||
FROM messages
|
||||
" + whereClause;
|
||||
@@ -737,14 +814,19 @@ internal class MessageStore : IDisposable
|
||||
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("$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()!;
|
||||
return (long)cmd.ExecuteScalar()!;
|
||||
}
|
||||
|
||||
internal MessageEnumerator GetDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
|
||||
internal MessageEnumerator GetDateRange(
|
||||
DateTime after,
|
||||
DateTime before,
|
||||
IEnumerable<byte> channels,
|
||||
ulong? receiver = null
|
||||
)
|
||||
{
|
||||
var cmd = Connection.CreateCommand();
|
||||
|
||||
@@ -759,7 +841,8 @@ internal class MessageStore : IDisposable
|
||||
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText = @"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
@@ -780,13 +863,19 @@ internal class MessageStore : IDisposable
|
||||
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("$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)
|
||||
internal MessageEnumerator GetPagedDateRange(
|
||||
DateTime after,
|
||||
DateTime before,
|
||||
IEnumerable<byte> channels,
|
||||
ulong? receiver = null,
|
||||
int page = 0
|
||||
)
|
||||
{
|
||||
var cmd = Connection.CreateCommand();
|
||||
|
||||
@@ -801,7 +890,8 @@ internal class MessageStore : IDisposable
|
||||
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText = @"
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
@@ -816,7 +906,9 @@ internal class MessageStore : IDisposable
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
FROM messages
|
||||
" + whereClause + @"
|
||||
"
|
||||
+ whereClause
|
||||
+ @"
|
||||
ORDER BY Date
|
||||
LIMIT $Offset, $OffsetCount;
|
||||
";
|
||||
@@ -825,8 +917,8 @@ internal class MessageStore : IDisposable
|
||||
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("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
|
||||
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
||||
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
|
||||
|
||||
@@ -852,7 +944,10 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal class MessageEnumerator(DbDataReader reader) : IEnumerable<Message>, IDisposable, IAsyncDisposable
|
||||
internal class MessageEnumerator(DbDataReader reader)
|
||||
: IEnumerable<Message>,
|
||||
IDisposable,
|
||||
IAsyncDisposable
|
||||
{
|
||||
private const int MaxErrorLogs = 10;
|
||||
|
||||
@@ -876,11 +971,27 @@ internal class MessageEnumerator(DbDataReader reader) : IEnumerable<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),
|
||||
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)
|
||||
);
|
||||
}
|
||||
@@ -915,6 +1026,7 @@ internal class MessageEnumerator(DbDataReader reader) : IEnumerable<Message>, ID
|
||||
{
|
||||
reader.Dispose();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await reader.DisposeAsync();
|
||||
|
||||
+182
-50
@@ -1,8 +1,5 @@
|
||||
using System.Numerics;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
@@ -18,11 +15,14 @@ using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Util;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Action = System.Action;
|
||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -107,7 +107,9 @@ public sealed class PayloadHandler
|
||||
ImGui.Separator();
|
||||
|
||||
var contentId = chunk.Message?.ContentId ?? 0;
|
||||
var sender = chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload) as PlayerPayload;
|
||||
var sender =
|
||||
chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload)
|
||||
as PlayerPayload;
|
||||
|
||||
using var menu = ImRaii.Menu(Language.Context_Integrations);
|
||||
if (!menu.Success)
|
||||
@@ -118,7 +120,14 @@ public sealed class PayloadHandler
|
||||
{
|
||||
try
|
||||
{
|
||||
LogWindow.Plugin.Ipc.Invoke(id, sender, contentId, payload, chunk.Message?.SenderSource, chunk.Message?.ContentSource);
|
||||
LogWindow.Plugin.Ipc.Invoke(
|
||||
id,
|
||||
sender,
|
||||
contentId,
|
||||
payload,
|
||||
chunk.Message?.SenderSource,
|
||||
chunk.Message?.ContentSource
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -128,7 +137,10 @@ public sealed class PayloadHandler
|
||||
|
||||
if (cursor == ImGui.GetCursorPos())
|
||||
{
|
||||
using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]);
|
||||
using var pushedColor = ImRaii.PushColor(
|
||||
ImGuiCol.Text,
|
||||
ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]
|
||||
);
|
||||
ImGui.Text("No integrations available");
|
||||
}
|
||||
}
|
||||
@@ -168,10 +180,16 @@ public sealed class PayloadHandler
|
||||
if (message.Sender.Count > 0 && ImGui.Selectable(Language.Context_CopyContent))
|
||||
{
|
||||
ImGui.SetClipboardText(StringifyMessage(message));
|
||||
WrapperUtil.AddNotification(Language.Context_CopyContentSuccess, NotificationType.Info);
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Context_CopyContentSuccess,
|
||||
NotificationType.Info
|
||||
);
|
||||
}
|
||||
|
||||
using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
|
||||
using var pushedColor = ImRaii.PushColor(
|
||||
ImGuiCol.Text,
|
||||
ImGui.GetStyle().Colors[(int)ImGuiCol.TextDisabled]
|
||||
);
|
||||
ImGui.TextUnformatted(message.Code.Type.Name());
|
||||
}
|
||||
|
||||
@@ -184,7 +202,8 @@ public sealed class PayloadHandler
|
||||
return string.Empty;
|
||||
|
||||
var chunks = withSender ? message.Sender.Concat(message.Content) : message.Content;
|
||||
return chunks.Where(chunk => chunk is TextChunk)
|
||||
return chunks
|
||||
.Where(chunk => chunk is TextChunk)
|
||||
.Cast<TextChunk>()
|
||||
.Select(text => text.Content)
|
||||
.Aggregate(string.Concat);
|
||||
@@ -255,7 +274,10 @@ public sealed class PayloadHandler
|
||||
public unsafe void MoveTooltip(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
// Only move if the user has the "Next to Cursor" option selected
|
||||
if (!Plugin.GameConfig.TryGet(UiControlOption.DetailTrackingType, out uint selected) || selected != 0)
|
||||
if (
|
||||
!Plugin.GameConfig.TryGet(UiControlOption.DetailTrackingType, out uint selected)
|
||||
|| selected != 0
|
||||
)
|
||||
return;
|
||||
|
||||
if (LogWindow.LastViewport != ImGuiHelpers.MainViewport.Handle)
|
||||
@@ -274,7 +296,10 @@ public sealed class PayloadHandler
|
||||
|
||||
var component = atkBase->WindowNode->AtkResNode;
|
||||
var atkPos = new Vector2(component.ScreenX, component.ScreenY);
|
||||
var atkSize = new Vector2(component.GetWidth() * component.ScaleX, component.GetHeight() * component.GetScaleY());
|
||||
var atkSize = new Vector2(
|
||||
component.GetWidth() * component.ScaleX,
|
||||
component.GetHeight() * component.GetScaleY()
|
||||
);
|
||||
|
||||
var chatRect = new MathUtil.Rectangle(LogWindow.LastWindowPos, LogWindow.LastWindowSize);
|
||||
var addonRect = new MathUtil.Rectangle(atkPos, atkSize);
|
||||
@@ -302,7 +327,7 @@ public sealed class PayloadHandler
|
||||
|
||||
if (!chatRect.HasOverlap(addonRect))
|
||||
{
|
||||
atkBase->SetPosition((short) addonRect.X, (short) addonRect.Y);
|
||||
atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,7 +345,7 @@ public sealed class PayloadHandler
|
||||
|
||||
if (!chatRect.HasOverlap(addonRect))
|
||||
{
|
||||
atkBase->SetPosition((short) addonRect.X, (short) addonRect.Y);
|
||||
atkBase->SetPosition((short)addonRect.X, (short)addonRect.Y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -329,7 +354,7 @@ public sealed class PayloadHandler
|
||||
var y = Math.Clamp(chatRect.SizeY - atkSize.Y, 0, float.MaxValue);
|
||||
y -= isTop ? 0 : Plugin.Config.TooltipOffset; // offset to prevent cut-off on the bottom
|
||||
|
||||
atkBase->SetPosition((short) x, (short) y);
|
||||
atkBase->SetPosition((short)x, (short)y);
|
||||
}
|
||||
|
||||
private const float MaxInlineIconSize = 32f;
|
||||
@@ -339,20 +364,25 @@ public sealed class PayloadHandler
|
||||
if (icon.Size.X <= 0 || icon.Size.Y <= 0)
|
||||
return;
|
||||
|
||||
var width = (float) icon.Size.X;
|
||||
var height = (float) icon.Size.Y;
|
||||
var width = (float)icon.Size.X;
|
||||
var height = (float)icon.Size.Y;
|
||||
var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height));
|
||||
var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale);
|
||||
|
||||
var cursor = ImGui.GetCursorPos();
|
||||
ImGui.Image(icon.Handle, size);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
|
||||
ImGui.SetCursorPos(
|
||||
cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing())
|
||||
);
|
||||
}
|
||||
|
||||
private void HoverStatus(StatusPayload status)
|
||||
{
|
||||
if (Plugin.TextureProvider.GetFromGameIcon(status.Status.Value.Icon).GetWrapOrDefault() is { } icon)
|
||||
if (
|
||||
Plugin.TextureProvider.GetFromGameIcon(status.Status.Value.Icon).GetWrapOrDefault() is
|
||||
{ } icon
|
||||
)
|
||||
InlineIcon(icon);
|
||||
|
||||
var builder = new SeStringBuilder();
|
||||
@@ -374,7 +404,11 @@ public sealed class PayloadHandler
|
||||
LogWindow.DrawChunks(name.ToList());
|
||||
ImGui.Separator();
|
||||
|
||||
var desc = ChunkUtil.ToChunks(status.Status.Value.Description.ToDalamudString(), ChunkSource.None, null);
|
||||
var desc = ChunkUtil.ToChunks(
|
||||
status.Status.Value.Description.ToDalamudString(),
|
||||
ChunkSource.None,
|
||||
null
|
||||
);
|
||||
LogWindow.DrawChunks(desc.ToList());
|
||||
}
|
||||
|
||||
@@ -389,14 +423,23 @@ public sealed class PayloadHandler
|
||||
if (!item.Item.TryGetValue(out Item resolvedItem))
|
||||
return;
|
||||
|
||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(resolvedItem.Icon, item.IsHQ)).GetWrapOrDefault() is { } icon)
|
||||
if (
|
||||
Plugin
|
||||
.TextureProvider.GetFromGameIcon(new GameIconLookup(resolvedItem.Icon, item.IsHQ))
|
||||
.GetWrapOrDefault() is
|
||||
{ } icon
|
||||
)
|
||||
InlineIcon(icon);
|
||||
|
||||
var name = ChunkUtil.ToChunks(resolvedItem.Name.ToDalamudString(), ChunkSource.None, null);
|
||||
LogWindow.DrawChunks(name.ToList());
|
||||
ImGui.Separator();
|
||||
|
||||
var desc = ChunkUtil.ToChunks(resolvedItem.Description.ToDalamudString(), ChunkSource.None, null);
|
||||
var desc = ChunkUtil.ToChunks(
|
||||
resolvedItem.Description.ToDalamudString(),
|
||||
ChunkSource.None,
|
||||
null
|
||||
);
|
||||
LogWindow.DrawChunks(desc.ToList());
|
||||
}
|
||||
|
||||
@@ -405,7 +448,12 @@ public sealed class PayloadHandler
|
||||
if (!Sheets.EventItemSheet.TryGetRow(payload.RawItemId, out var itemRow))
|
||||
return;
|
||||
|
||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon)).GetWrapOrDefault() is { } icon)
|
||||
if (
|
||||
Plugin
|
||||
.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon))
|
||||
.GetWrapOrDefault() is
|
||||
{ } icon
|
||||
)
|
||||
InlineIcon(icon);
|
||||
|
||||
var name = ChunkUtil.ToChunks(itemRow.Name.ToDalamudString(), ChunkSource.None, null);
|
||||
@@ -415,7 +463,11 @@ public sealed class PayloadHandler
|
||||
if (!Sheets.EventItemHelpSheet.TryGetRow(payload.RawItemId, out var itemHelpRow))
|
||||
return;
|
||||
|
||||
LogWindow.DrawChunks(ChunkUtil.ToChunks(itemHelpRow.Description.ToDalamudString(), ChunkSource.None, null).ToList());
|
||||
LogWindow.DrawChunks(
|
||||
ChunkUtil
|
||||
.ToChunks(itemHelpRow.Description.ToDalamudString(), ChunkSource.None, null)
|
||||
.ToList()
|
||||
);
|
||||
}
|
||||
|
||||
private void HoverUri(UriPayload uri)
|
||||
@@ -438,7 +490,10 @@ public sealed class PayloadHandler
|
||||
ClickLinkPayload(chunk, payload, link);
|
||||
break;
|
||||
case DalamudPartyFinderPayload pf:
|
||||
if (pf.LinkType == DalamudPartyFinderPayload.PartyFinderLinkType.PartyFinderNotification)
|
||||
if (
|
||||
pf.LinkType
|
||||
== DalamudPartyFinderPayload.PartyFinderLinkType.PartyFinderNotification
|
||||
)
|
||||
GameFunctions.GameFunctions.OpenPartyFinder();
|
||||
else
|
||||
GameFunctions.GameFunctions.OpenPartyFinder(pf.ListingId);
|
||||
@@ -473,7 +528,12 @@ public sealed class PayloadHandler
|
||||
return;
|
||||
|
||||
var payloads = source.Payloads.Skip(start).Take(end - start + 1).ToList();
|
||||
if (!Plugin.ChatGui.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
|
||||
if (
|
||||
!Plugin.ChatGui.RegisteredLinkHandlers.TryGetValue(
|
||||
(link.Plugin, link.CommandId),
|
||||
out var value
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Could not find DalamudLinkHandlers");
|
||||
return;
|
||||
@@ -508,7 +568,12 @@ public sealed class PayloadHandler
|
||||
return;
|
||||
|
||||
var hq = payload.Kind == ItemKind.Hq;
|
||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon, hq)).GetWrapOrDefault() is { } icon)
|
||||
if (
|
||||
Plugin
|
||||
.TextureProvider.GetFromGameIcon(new GameIconLookup(itemRow.Icon, hq))
|
||||
.GetWrapOrDefault() is
|
||||
{ } icon
|
||||
)
|
||||
InlineIcon(icon);
|
||||
|
||||
var name = itemRow.Name.ToDalamudString();
|
||||
@@ -554,10 +619,18 @@ public sealed class PayloadHandler
|
||||
return;
|
||||
|
||||
var item = Sheets.EventItemSheet.GetRow(payload.ItemId);
|
||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(item.Icon)).GetWrapOrDefault() is { } icon)
|
||||
if (
|
||||
Plugin
|
||||
.TextureProvider.GetFromGameIcon(new GameIconLookup(item.Icon))
|
||||
.GetWrapOrDefault() is
|
||||
{ } icon
|
||||
)
|
||||
InlineIcon(icon);
|
||||
|
||||
LogWindow.DrawChunks(ChunkUtil.ToChunks(item.Name.ToDalamudString(), ChunkSource.None, null).ToList(), false);
|
||||
LogWindow.DrawChunks(
|
||||
ChunkUtil.ToChunks(item.Name.ToDalamudString(), ChunkSource.None, null).ToList(),
|
||||
false
|
||||
);
|
||||
ImGui.Separator();
|
||||
|
||||
var realItemId = payload.RawItemId;
|
||||
@@ -585,7 +658,7 @@ public sealed class PayloadHandler
|
||||
{
|
||||
name.AddRange([
|
||||
new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld),
|
||||
new TextChunk(ChunkSource.None, null, world.Value.Name.ExtractText())
|
||||
new TextChunk(ChunkSource.None, null, world.Value.Name.ExtractText()),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -606,7 +679,15 @@ public sealed class PayloadHandler
|
||||
}
|
||||
else if (validContentId)
|
||||
{
|
||||
LogWindow.Plugin.Functions.Chat.SetEurekaTellChannel(player.PlayerName, world.Value.Name.ToString(), (ushort) world.RowId, 0, chunk.Message!.ContentId, 0, false);
|
||||
LogWindow.Plugin.Functions.Chat.SetEurekaTellChannel(
|
||||
player.PlayerName,
|
||||
world.Value.Name.ToString(),
|
||||
(ushort)world.RowId,
|
||||
0,
|
||||
chunk.Message!.ContentId,
|
||||
0,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
LogWindow.Activate = true;
|
||||
@@ -615,12 +696,18 @@ public sealed class PayloadHandler
|
||||
if (world.Value.IsPublic)
|
||||
{
|
||||
var party = Plugin.PartyList;
|
||||
var leader = party[(int) party.PartyLeaderIndex]?.ContentId;
|
||||
var leader = party[(int)party.PartyLeaderIndex]?.ContentId;
|
||||
var isLeader = party.Length == 0 || Plugin.PlayerState.ContentId == leader;
|
||||
var member = party.FirstOrDefault(member => member.Name.TextValue == player.PlayerName && member.World.RowId == world.RowId);
|
||||
var member = party.FirstOrDefault(member =>
|
||||
member.Name.TextValue == player.PlayerName && member.World.RowId == world.RowId
|
||||
);
|
||||
var isInParty = member != null;
|
||||
var inInstance = GameFunctions.GameFunctions.IsInInstance();
|
||||
var inPartyInstance = Sheets.TerritorySheet.GetRow(Plugin.ClientState.TerritoryType).TerritoryIntendedUse.RowId is (41 or 47 or 48 or 52 or 53 or 61);
|
||||
var inPartyInstance =
|
||||
Sheets
|
||||
.TerritorySheet.GetRow(Plugin.ClientState.TerritoryType)
|
||||
.TerritoryIntendedUse.RowId
|
||||
is (41 or 47 or 48 or 52 or 53 or 61);
|
||||
if (isLeader)
|
||||
{
|
||||
if (!isInParty)
|
||||
@@ -636,10 +723,20 @@ public sealed class PayloadHandler
|
||||
if (menu.Success)
|
||||
{
|
||||
if (ImGui.Selectable(Language.Context_InviteToParty_SameWorld))
|
||||
GameFunctions.Party.InviteSameWorld(player.PlayerName, (ushort)world.RowId, chunk.Message?.ContentId ?? 0);
|
||||
GameFunctions.Party.InviteSameWorld(
|
||||
player.PlayerName,
|
||||
(ushort)world.RowId,
|
||||
chunk.Message?.ContentId ?? 0
|
||||
);
|
||||
|
||||
if (validContentId && ImGui.Selectable(Language.Context_InviteToParty_DifferentWorld))
|
||||
GameFunctions.Party.InviteOtherWorld(chunk.Message!.ContentId, (ushort)world.RowId);
|
||||
if (
|
||||
validContentId
|
||||
&& ImGui.Selectable(Language.Context_InviteToParty_DifferentWorld)
|
||||
)
|
||||
GameFunctions.Party.InviteOtherWorld(
|
||||
chunk.Message!.ContentId,
|
||||
(ushort)world.RowId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,23 +751,41 @@ public sealed class PayloadHandler
|
||||
}
|
||||
}
|
||||
|
||||
var isFriend = GameFunctions.GameFunctions.GetFriends().Any(friend => friend.NameString == player.PlayerName && friend.HomeWorld == world.RowId);
|
||||
var isFriend = GameFunctions
|
||||
.GameFunctions.GetFriends()
|
||||
.Any(friend =>
|
||||
friend.NameString == player.PlayerName && friend.HomeWorld == world.RowId
|
||||
);
|
||||
if (!isFriend && ImGui.Selectable(Language.Context_SendFriendRequest))
|
||||
LogWindow.Plugin.Functions.SendFriendRequest(player.PlayerName, (ushort) world.RowId);
|
||||
LogWindow.Plugin.Functions.SendFriendRequest(
|
||||
player.PlayerName,
|
||||
(ushort)world.RowId
|
||||
);
|
||||
|
||||
using (var menuBlockFunctions = ImRaii.Menu(Language.Context_BlockFunctions))
|
||||
{
|
||||
if (menuBlockFunctions.Success)
|
||||
{
|
||||
if (ImGui.Selectable(Language.Context_AddToBlacklist))
|
||||
LogWindow.Plugin.Functions.AddToBlacklist(player.PlayerName, (ushort)world.RowId);
|
||||
LogWindow.Plugin.Functions.AddToBlacklist(
|
||||
player.PlayerName,
|
||||
(ushort)world.RowId
|
||||
);
|
||||
|
||||
if (chunk.Message != null)
|
||||
{
|
||||
var message = chunk.Message;
|
||||
|
||||
if (message.AccountId != 0 && ImGui.Selectable(Language.Context_AddToMuteList))
|
||||
LogWindow.Plugin.Functions.AddToMuteList(message.AccountId, message.ContentId, player.PlayerName, (short) world.RowId);
|
||||
if (
|
||||
message.AccountId != 0
|
||||
&& ImGui.Selectable(Language.Context_AddToMuteList)
|
||||
)
|
||||
LogWindow.Plugin.Functions.AddToMuteList(
|
||||
message.AccountId,
|
||||
message.ContentId,
|
||||
player.PlayerName,
|
||||
(short)world.RowId
|
||||
);
|
||||
|
||||
if (ImGui.Selectable(Language.Context_AddToTermsFilter))
|
||||
LogWindow.Plugin.Functions.AddToTermsList(message.ContentSource);
|
||||
@@ -678,8 +793,11 @@ public sealed class PayloadHandler
|
||||
}
|
||||
}
|
||||
|
||||
if (GameFunctions.GameFunctions.IsMentor() && ImGui.Selectable(Language.Context_InviteToNoviceNetwork))
|
||||
GameFunctions.Context.InviteToNoviceNetwork(player.PlayerName, (ushort) world.RowId);
|
||||
if (
|
||||
GameFunctions.GameFunctions.IsMentor()
|
||||
&& ImGui.Selectable(Language.Context_InviteToNoviceNetwork)
|
||||
)
|
||||
GameFunctions.Context.InviteToNoviceNetwork(player.PlayerName, (ushort)world.RowId);
|
||||
}
|
||||
|
||||
var inputChannel = chunk.Message?.Code.Type.ToInputChannel();
|
||||
@@ -694,7 +812,10 @@ public sealed class PayloadHandler
|
||||
|
||||
if (validContentId && ImGui.Selectable(Language.Context_AdventurerPlate))
|
||||
if (!GameFunctions.GameFunctions.TryOpenAdventurerPlate(chunk.Message!.ContentId))
|
||||
WrapperUtil.AddNotification(Language.Context_AdventurerPlateError, NotificationType.Warning);
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Context_AdventurerPlateError,
|
||||
NotificationType.Warning
|
||||
);
|
||||
}
|
||||
|
||||
private IPlayerCharacter? FindCharacterForPayload(PlayerPayload payload)
|
||||
@@ -728,13 +849,21 @@ public sealed class PayloadHandler
|
||||
if (ImGui.Selectable(Language.Context_CopyLink))
|
||||
{
|
||||
ImGui.SetClipboardText(uri.Uri.ToString());
|
||||
WrapperUtil.AddNotification(Language.Context_CopyLinkNotification, NotificationType.Info);
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Context_CopyLinkNotification,
|
||||
NotificationType.Info
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawStatusPopup(StatusPayload status)
|
||||
{
|
||||
if (Plugin.TextureProvider.GetFromGameIcon(new GameIconLookup(status.Status.Value.Icon)).GetWrapOrDefault() is { } icon)
|
||||
if (
|
||||
Plugin
|
||||
.TextureProvider.GetFromGameIcon(new GameIconLookup(status.Status.Value.Icon))
|
||||
.GetWrapOrDefault() is
|
||||
{ } icon
|
||||
)
|
||||
InlineIcon(icon);
|
||||
|
||||
var builder = new SeStringBuilder();
|
||||
@@ -752,7 +881,10 @@ public sealed class PayloadHandler
|
||||
break;
|
||||
}
|
||||
|
||||
LogWindow.DrawChunks(ChunkUtil.ToChunks(builder.BuiltString, ChunkSource.None, null).ToList(), false);
|
||||
LogWindow.DrawChunks(
|
||||
ChunkUtil.ToChunks(builder.BuiltString, ChunkSource.None, null).ToList(),
|
||||
false
|
||||
);
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.Selectable(Language.Context_Link))
|
||||
|
||||
+256
-259
@@ -3,17 +3,17 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using HellionChat.Ipc;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using HellionChat.Ipc;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -22,37 +22,76 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
{
|
||||
public const string PluginName = "Hellion Chat";
|
||||
|
||||
[PluginService] public static IPluginLog Log { get; private set; } = null!;
|
||||
[PluginService] public static IDalamudPluginInterface Interface { get; private set; } = null!;
|
||||
[PluginService] public static IChatGui ChatGui { get; private set; } = null!;
|
||||
[PluginService] public static IClientState ClientState { get; private set; } = null!;
|
||||
[PluginService] public static ICommandManager CommandManager { get; private set; } = null!;
|
||||
[PluginService] public static ICondition Condition { get; private set; } = null!;
|
||||
[PluginService] public static IDataManager DataManager { get; private set; } = null!;
|
||||
[PluginService] public static IFramework Framework { get; private set; } = null!;
|
||||
[PluginService] public static IGameGui GameGui { get; private set; } = null!;
|
||||
[PluginService] public static IKeyState KeyState { get; private set; } = null!;
|
||||
[PluginService] public static IObjectTable ObjectTable { get; private set; } = null!;
|
||||
[PluginService] public static IPartyList PartyList { get; private set; } = null!;
|
||||
[PluginService] public static ITargetManager TargetManager { get; private set; } = null!;
|
||||
[PluginService] public static ITextureProvider TextureProvider { get; private set; } = null!;
|
||||
[PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; } = null!;
|
||||
[PluginService] public static IGameConfig GameConfig { get; private set; } = null!;
|
||||
[PluginService] public static INotificationManager Notification { get; private set; } = null!;
|
||||
[PluginService] public static IAddonLifecycle AddonLifecycle { get; private set; } = null!;
|
||||
[PluginService] public static IPlayerState PlayerState { get; private set; } = null!;
|
||||
[PluginService] public static ISeStringEvaluator Evaluator { get; private set; } = null!;
|
||||
[PluginService] public static ISelfTestRegistry SelfTestRegistry { get; private set; } = null!;
|
||||
[PluginService]
|
||||
public static IPluginLog Log { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IDalamudPluginInterface Interface { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IChatGui ChatGui { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IClientState ClientState { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static ICommandManager CommandManager { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static ICondition Condition { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IDataManager DataManager { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IFramework Framework { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IGameGui GameGui { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IKeyState KeyState { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IObjectTable ObjectTable { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IPartyList PartyList { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static ITargetManager TargetManager { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static ITextureProvider TextureProvider { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IGameInteropProvider GameInteropProvider { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IGameConfig GameConfig { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static INotificationManager Notification { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IAddonLifecycle AddonLifecycle { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static IPlayerState PlayerState { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static ISeStringEvaluator Evaluator { get; private set; } = null!;
|
||||
|
||||
[PluginService]
|
||||
public static ISelfTestRegistry SelfTestRegistry { get; private set; } = null!;
|
||||
|
||||
public static Configuration Config = null!;
|
||||
public static FileDialogManager FileDialogManager { get; private set; } = null!;
|
||||
|
||||
public readonly WindowSystem WindowSystem = new(PluginName);
|
||||
|
||||
// v1.4.3: properties moved from { get; } to { get; private set; } = null!;
|
||||
// because LoadAsync now owns construction of the Phase-2 services.
|
||||
// Phase-1 services use the same shape for consistency, even though
|
||||
// they're still allocated in the ctor.
|
||||
// Phase-2 services are constructed in LoadAsync; null! shape is kept
|
||||
// consistent across all properties for clarity.
|
||||
public SettingsWindow SettingsWindow { get; private set; } = null!;
|
||||
public ChatLogWindow ChatLogWindow { get; private set; } = null!;
|
||||
public DbViewer DbViewer { get; private set; } = null!;
|
||||
@@ -74,27 +113,20 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
||||
|
||||
// (B3) Lightless idempotency guard — Dalamud may fire DisposeAsync twice
|
||||
// in a reload race; second call short-circuits.
|
||||
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||
private int _disposeStarted;
|
||||
|
||||
internal int DeferredSaveFrames = -1;
|
||||
|
||||
// Serialises retention sweeps. The 24h auto-sweep on plugin load and
|
||||
// the manual button in the Privacy tab both run on background threads;
|
||||
// without this gate, hitting the manual button moments after a fresh
|
||||
// plugin start would launch two sweeps in parallel and the second one
|
||||
// would just re-do work the first one already finished. The lock guards
|
||||
// the flag — the flag check itself bails before we touch the database.
|
||||
// Volatile because the ImGui thread reads the flag outside the lock to
|
||||
// gate the manual button; without it the JIT may cache the value in a
|
||||
// register and miss the background-thread update.
|
||||
// Serialises retention sweeps so a manual trigger and the 24h auto-sweep
|
||||
// can't run in parallel. Volatile because the ImGui thread reads it outside
|
||||
// the lock to gate the manual button.
|
||||
internal readonly object RetentionSweepLock = new();
|
||||
internal volatile bool RetentionSweepRunning;
|
||||
|
||||
internal DateTime GameStarted { get; }
|
||||
|
||||
// Tab management needs to happen outside the chatlog window class for access reasons
|
||||
// Tab management lives here rather than in ChatLogWindow for access reasons.
|
||||
internal int LastTab { get; set; }
|
||||
internal int? WantedTab { get; set; }
|
||||
internal Tab CurrentTab
|
||||
@@ -108,51 +140,37 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
public Plugin()
|
||||
{
|
||||
// Phase-1 ctor stays minimal: bootstrap-essentials only (conflict
|
||||
// gate, config load, language + ImGui init, WindowSystem skeleton).
|
||||
// Schema migrations and every service / window allocation moved to
|
||||
// LoadAsync so the sync ctor returns fast. On failure here nothing
|
||||
// is initialized yet, so just throw — there is nothing to clean up.
|
||||
// Phase-1 ctor: bootstrap-essentials only (conflict gate, config load,
|
||||
// language + ImGui init). All service/window allocation lives in LoadAsync.
|
||||
|
||||
// Refuse to start if upstream Chat 2 is loaded — prevents IPC
|
||||
// channel collisions and double-replacement of the in-game chat
|
||||
// window. Throwing here makes Dalamud abort the load cleanly with
|
||||
// our localized message instead of crashing FFXIV mid-frame.
|
||||
// Block load if upstream Chat 2 is active — prevents IPC collisions
|
||||
// and double-replacement of the in-game chat window.
|
||||
ChatTwoConflictDetector.ThrowIfChatTwoIsLoaded(Interface);
|
||||
|
||||
GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime();
|
||||
|
||||
// Hellion Chat: take over config + database from upstream ChatTwo
|
||||
// before Dalamud loads our plugin config. Idempotent: only acts on
|
||||
// the first start where the legacy paths exist and ours don't.
|
||||
// Migrate config + database from upstream ChatTwo on first start.
|
||||
MigrateFromChatTwoLayout();
|
||||
|
||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
|
||||
// Schema-gate: v1.4.3 only supports config schema v16. Older configs
|
||||
// went through their migrations in v1.2.1 (v15→v16) and earlier; users
|
||||
// who skipped past those releases need to install v1.4.2 first to run
|
||||
// the migration chain, then upgrade to v1.4.3.
|
||||
// Schema gate: v1.4.3 requires config v16. Users on older schemas
|
||||
// must install v1.4.2 first to run the migration chain.
|
||||
if (Config.Version < 16)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"HellionChat v1.4.3 requires config schema v16, got v{Config.Version}. " +
|
||||
"Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.3.");
|
||||
$"HellionChat v1.4.3 requires config schema v16, got v{Config.Version}. "
|
||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.3."
|
||||
);
|
||||
}
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs Defense-in-Depth. SaveConfig
|
||||
// already strips temp tabs before persistence, but a previous
|
||||
// crash or external write could have left them in the JSON.
|
||||
// Drop them on load to guarantee the session-only invariant.
|
||||
// Drop session-only Auto-Tell-Tabs that a previous crash may have persisted.
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
|
||||
LanguageChanged(Interface.UiLanguage);
|
||||
ImGuiUtil.Initialize(this);
|
||||
|
||||
DeferredSaveFrames = -1;
|
||||
|
||||
// WindowSystem skeleton is initialised by the readonly field above —
|
||||
// no AddWindow yet; window construction lives in LoadAsync.
|
||||
}
|
||||
|
||||
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||
@@ -161,14 +179,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
try
|
||||
{
|
||||
// Hellion v1.0.0 default tab layout. Five thematically separated
|
||||
// tabs: General catches the immediate-surroundings public chat
|
||||
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
||||
// and gameplay-event noise; FreeCompany, Group and Linkshell each
|
||||
// own their respective channel set. Tells are not in a static
|
||||
// tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation
|
||||
// tabs on demand. Novice-Network gets no preset tab; users who
|
||||
// want it can add HellionBeginner from Settings → Tabs.
|
||||
// Default tab layout on fresh install. Tells are handled by
|
||||
// Auto-Tell-Tabs; Novice Network has no preset tab by design.
|
||||
if (Config.Tabs.Count == 0)
|
||||
{
|
||||
Config.Tabs.Add(TabsUtil.VanillaGeneral);
|
||||
@@ -180,19 +192,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Sync allocation + handle registration. BuildFonts() registers
|
||||
// IFontHandles with Dalamud's UiBuilder.FontAtlas — registration
|
||||
// itself is non-blocking (handles stored, lambdas queued). Dalamud
|
||||
// rebuilds the atlas on its own pipeline a few frames later; first
|
||||
// frames render with the default font until the rebuild lands and
|
||||
// ImGui switches to Hellion-Exo2 / NotoSans (visible "font-pop").
|
||||
// Mirrors ChatTwo Plugin.cs:152.
|
||||
// BuildFonts registers handles with Dalamud's FontAtlas; the atlas
|
||||
// rebuilds async a few frames later (visible "font-pop" on first load).
|
||||
FontManager = new FontManager();
|
||||
FontManager.BuildFonts();
|
||||
|
||||
// Theme init stays sync on the LoadAsync continuation — cheap,
|
||||
// and Active is read every Draw frame, so the registry must be
|
||||
// wired before the first hook fires.
|
||||
// ThemeRegistry must be wired before the first Draw tick.
|
||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||
Directory.CreateDirectory(customThemesDir);
|
||||
SeedExampleThemeIfEmpty(customThemesDir);
|
||||
@@ -201,11 +206,9 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Service allocations: order encodes dependencies. Commands is
|
||||
// alloc-only here; Initialise() runs after windows exist so the
|
||||
// slash-commands can toggle their visibility. HonorificService
|
||||
// registers IPC subscribers up-front so Ready/Disposing events
|
||||
// are caught from the very first frame.
|
||||
// Service allocations — order encodes dependencies.
|
||||
// HonorificService registers IPC subscribers early to catch
|
||||
// Ready/Disposing events from the first frame.
|
||||
FileDialogManager = new FileDialogManager();
|
||||
Commands = new Commands();
|
||||
Functions = new GameFunctions.GameFunctions(this);
|
||||
@@ -216,16 +219,14 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
StatusBar = new Ui.StatusBar();
|
||||
MessageManager = new MessageManager(this);
|
||||
|
||||
// Auto-Tell-Tabs subscribes to MessageManager.MessageProcessed for
|
||||
// live tells and to ClientState.Logout for cleanup; needs the live
|
||||
// store handed in at construction.
|
||||
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
|
||||
AutoTellTabsService = new AutoTellTabsService(
|
||||
this,
|
||||
MessageManager,
|
||||
MessageManager.Store
|
||||
);
|
||||
AutoTellTabsService.Initialize();
|
||||
|
||||
// SelfTest steps poll Active per frame and need the registry wired.
|
||||
SelfTestRegistry.RegisterTestSteps([
|
||||
new SelfTests.ThemeSwitchSelfTestStep(this),
|
||||
]);
|
||||
SelfTestRegistry.RegisterTestSteps([new SelfTests.ThemeSwitchSelfTestStep(this)]);
|
||||
|
||||
ChatLogWindow = new ChatLogWindow(this);
|
||||
SettingsWindow = new SettingsWindow(this);
|
||||
@@ -245,22 +246,19 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
WindowSystem.AddWindow(DebuggerWindow);
|
||||
WindowSystem.AddWindow(FirstRunWizard);
|
||||
|
||||
// Open the wizard on a fresh install. Existing ChatTwo users have
|
||||
// FirstRunCompleted set to true by the v6→v7 migration above.
|
||||
if (!Config.FirstRunCompleted)
|
||||
FirstRunWizard.IsOpen = true;
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// let all the other components register, then initialize commands
|
||||
Commands.Initialise();
|
||||
|
||||
// Daily retention sweep, fire-and-forget. Skips itself when
|
||||
// disabled or when it already ran within the past 24 hours.
|
||||
// Daily retention sweep — fire-and-forget, skips when disabled
|
||||
// or already ran within the past 24 hours.
|
||||
RunRetentionSweepIfDue();
|
||||
|
||||
if (Config.ShowEmotes)
|
||||
_ = EmoteCache.LoadData(); // Fire-and-forget, exceptions caught inside
|
||||
_ = EmoteCache.LoadData();
|
||||
|
||||
if (Interface.Reason is not PluginLoadReason.Boot)
|
||||
MessageManager.FilterAllTabsAsync();
|
||||
@@ -268,126 +266,109 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
||||
Interface.UiBuilder.DisableGposeUiHide = true;
|
||||
|
||||
#if !DEBUG
|
||||
// Fire-and-forget on a worker thread. The first auto-translate use of
|
||||
// a session may have a sub-second hitch if the cache hasn't filled yet,
|
||||
// but that's preferable to making every user wait ~300 ms during
|
||||
// plugin load for a cache they may never touch. ChatTwo (upstream)
|
||||
// does this sync; we trade load-time for first-use latency.
|
||||
#if !DEBUG
|
||||
// Fire-and-forget — first auto-translate use may have a sub-second
|
||||
// hitch if the cache hasn't filled yet, but avoids blocking load.
|
||||
_ = Task.Run(AutoTranslate.PreloadCache, cancellationToken);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// (B1) Hooks last: every service and window must be live before
|
||||
// Dalamud fires our first Draw / FrameworkUpdate tick. Anything
|
||||
// earlier risks rendering against null FontManager / ThemeRegistry.
|
||||
// Hooks last — all services and windows must be live before
|
||||
// the first Draw / FrameworkUpdate tick fires.
|
||||
Framework.Update += FrameworkUpdate;
|
||||
Interface.UiBuilder.Draw += Draw;
|
||||
Interface.LanguageChanged += LanguageChanged;
|
||||
// Hellion Chat — surface a "main UI" entry point so Dalamud's
|
||||
// plugin list shows the Open-Plugin button. Settings is the
|
||||
// most useful landing place; OpenConfigUi is already wired to
|
||||
// the same toggle inside SettingsWindow.
|
||||
Interface.UiBuilder.OpenMainUi += OpenMainUi;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Mirror the v1.4.0 load-failure recovery: hand off to DisposeAsync
|
||||
// so partially-built services are torn down. Swallow the cleanup
|
||||
// exception so the original load failure stays the visible cause.
|
||||
try { await DisposeAsync().ConfigureAwait(false); }
|
||||
catch { /* keep original failure */ }
|
||||
try
|
||||
{
|
||||
await DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{ /* keep original failure */
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Suppressing this warning because DisposeAsync may run after a partial
|
||||
// LoadAsync, so some properties may not be initialized.
|
||||
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract")]
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// (B3) Idempotency guard — Dalamud may reload-race us; second
|
||||
// call short-circuits so we don't double-dispose services.
|
||||
// Idempotency guard — second call short-circuits on reload race.
|
||||
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
||||
return;
|
||||
|
||||
Exception? failure = null;
|
||||
|
||||
// Hooks unsubscribe FIRST so no Draw / FrameworkUpdate / LanguageChanged
|
||||
// tick can fire while we're tearing services down. Mirrors the
|
||||
// hooks-last subscribe order in LoadAsync.
|
||||
// Unsubscribe hooks first — mirrors the hooks-last subscribe order in LoadAsync.
|
||||
failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi);
|
||||
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
|
||||
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
|
||||
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
|
||||
|
||||
// v1.4.0 F5.3 — flush a pending DeferredSave before service teardown,
|
||||
// since FrameworkUpdate just got unsubscribed and won't fire it.
|
||||
failure = CaptureFailure(failure, () =>
|
||||
{
|
||||
if (DeferredSaveFrames >= 0)
|
||||
// Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore.
|
||||
failure = CaptureFailure(
|
||||
failure,
|
||||
() =>
|
||||
{
|
||||
SaveConfig();
|
||||
DeferredSaveFrames = -1;
|
||||
if (DeferredSaveFrames >= 0)
|
||||
{
|
||||
SaveConfig();
|
||||
DeferredSaveFrames = -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// Auto-Tell-Tabs unsubscribes from MessageProcessed before MessageManager
|
||||
// goes away. Pure-memory cleanup, no framework-thread requirement.
|
||||
// Unsubscribe AutoTellTabs before MessageManager goes away.
|
||||
failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose());
|
||||
|
||||
// v1.4.0 F6.2 — MessageManager has its own async dispose path
|
||||
// (DB flush, pending-message thread shutdown). Run it before the
|
||||
// framework-block so the worker threads are quiesced first.
|
||||
// MessageManager has its own async dispose path (DB flush, thread shutdown).
|
||||
if (MessageManager is not null)
|
||||
{
|
||||
failure = await CaptureFailureAsync(failure, () => MessageManager.DisposeAsync().AsTask())
|
||||
failure = await CaptureFailureAsync(
|
||||
failure,
|
||||
() => MessageManager.DisposeAsync().AsTask()
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// (B4) Game-Function / IPC / UI-Window cleanup MUST run on the
|
||||
// framework thread. WindowSystem mutations and IPC subscriber
|
||||
// disposes touch Dalamud state that's only safe from the framework.
|
||||
// Worker-thread DisposeAsync would race the next Draw tick.
|
||||
// Per-line CaptureFailure so a single throw can't strand the lines
|
||||
// behind it; mirrors Lightless DisposeFrameworkBoundServicesAsync.
|
||||
// Game-function / IPC / window cleanup must run on the framework thread.
|
||||
try
|
||||
{
|
||||
await Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
// Game-Functions first — other services may still query
|
||||
// chat-interactable state during their Dispose.
|
||||
failure = CaptureFailure(failure, () => GameFunctions.GameFunctions.SetChatInteractable(true));
|
||||
await Framework
|
||||
.RunOnFrameworkThread(() =>
|
||||
{
|
||||
failure = CaptureFailure(
|
||||
failure,
|
||||
() => GameFunctions.GameFunctions.SetChatInteractable(true)
|
||||
);
|
||||
|
||||
// IPC subscribers — dispose before windows so any final
|
||||
// event firing from the IPC source can't reach a half-torn
|
||||
// ChatLogWindow.
|
||||
failure = CaptureFailure(failure, () => HonorificService?.Dispose());
|
||||
failure = CaptureFailure(failure, () => TypingIpc?.Dispose());
|
||||
failure = CaptureFailure(failure, () => ExtraChat?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Ipc?.Dispose());
|
||||
// IPC subscribers before windows — prevents a final IPC event
|
||||
// from reaching a half-torn ChatLogWindow.
|
||||
failure = CaptureFailure(failure, () => HonorificService?.Dispose());
|
||||
failure = CaptureFailure(failure, () => TypingIpc?.Dispose());
|
||||
failure = CaptureFailure(failure, () => ExtraChat?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Ipc?.Dispose());
|
||||
|
||||
// Windows — RemoveAllWindows first, then per-window Dispose.
|
||||
// Order matches the pre-v1.4.3 Dispose body byte-for-byte.
|
||||
// CommandHelpWindow and FirstRunWizard don't implement
|
||||
// IDisposable; their resources are reclaimed via WindowSystem.
|
||||
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
|
||||
failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => DbViewer?.Dispose());
|
||||
failure = CaptureFailure(failure, () => InputPreview?.Dispose());
|
||||
failure = CaptureFailure(failure, () => SettingsWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => DebuggerWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => SeStringDebugger?.Dispose());
|
||||
}).ConfigureAwait(false);
|
||||
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
|
||||
failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => DbViewer?.Dispose());
|
||||
failure = CaptureFailure(failure, () => InputPreview?.Dispose());
|
||||
failure = CaptureFailure(failure, () => SettingsWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => DebuggerWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => SeStringDebugger?.Dispose());
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure ??= ex;
|
||||
}
|
||||
|
||||
// Pure-memory cleanups — no Framework / UI / IPC touch, so they
|
||||
// run on whatever thread DisposeAsync resumes on.
|
||||
// Pure-memory cleanups — no Framework / UI / IPC touch.
|
||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
||||
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
||||
@@ -396,20 +377,34 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||
}
|
||||
|
||||
// Lightless-pattern capture helpers: run cleanup, remember the FIRST
|
||||
// exception, keep going. Without these one mid-teardown failure would
|
||||
// skip every cleanup behind it and leave services half-torn.
|
||||
// Run cleanup actions individually so a single failure doesn't strand
|
||||
// the remaining teardown steps.
|
||||
private static Exception? CaptureFailure(Exception? failure, Action action)
|
||||
{
|
||||
try { action(); }
|
||||
catch (Exception ex) { failure ??= ex; }
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure ??= ex;
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static async ValueTask<Exception?> CaptureFailureAsync(Exception? failure, Func<Task> action)
|
||||
private static async ValueTask<Exception?> CaptureFailureAsync(
|
||||
Exception? failure,
|
||||
Func<Task> action
|
||||
)
|
||||
{
|
||||
try { await action().ConfigureAwait(false); }
|
||||
catch (Exception ex) { failure ??= ex; }
|
||||
try
|
||||
{
|
||||
await action().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failure ??= ex;
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
@@ -424,9 +419,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
var ourConfigFile = Path.Combine(pluginConfigsDir, "HellionChat.json");
|
||||
var ourConfigDir = Interface.ConfigDirectory.FullName;
|
||||
|
||||
// Track whether anything legitimately blocked us. The most common
|
||||
// cause is upstream Chat 2 still being loaded — its SQLite handle
|
||||
// keeps chat-sqlite.db locked and File.Move throws IOException.
|
||||
var lockedBlocker = false;
|
||||
|
||||
try
|
||||
@@ -434,22 +426,20 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
if (!File.Exists(ourConfigFile) && File.Exists(legacyConfigFile))
|
||||
{
|
||||
File.Move(legacyConfigFile, ourConfigFile);
|
||||
Log.Information($"HellionChat: migrated config file {legacyConfigFile} → {ourConfigFile}");
|
||||
Log.Information(
|
||||
$"HellionChat: migrated config file {legacyConfigFile} → {ourConfigFile}"
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: config file move blocked, leaving {legacyConfigFile} in place");
|
||||
Log.Warning(
|
||||
e,
|
||||
$"HellionChat: config file move blocked, leaving {legacyConfigFile} in place"
|
||||
);
|
||||
lockedBlocker = true;
|
||||
}
|
||||
|
||||
// The plugin's ConfigDirectory may already exist on first load
|
||||
// (Dalamud creates it), so check at the file level instead of
|
||||
// skipping when the directory is present. Move every legacy
|
||||
// entry whose target name is not occupied yet, then remove the
|
||||
// source dir if it ends up empty. Each move is wrapped on its
|
||||
// own so a single locked file (the SQLite db while ChatTwo still
|
||||
// runs) does not abandon the rest of the migration.
|
||||
if (!Directory.Exists(legacyConfigDir))
|
||||
return;
|
||||
|
||||
@@ -457,6 +447,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
{
|
||||
Directory.CreateDirectory(ourConfigDir);
|
||||
|
||||
// Move each file individually so a single locked file (e.g. the
|
||||
// SQLite db while ChatTwo is still loaded) doesn't abort the rest.
|
||||
foreach (var file in Directory.EnumerateFiles(legacyConfigDir))
|
||||
{
|
||||
var target = Path.Combine(ourConfigDir, Path.GetFileName(file));
|
||||
@@ -469,7 +461,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: file move blocked for {file}, will retry on next load");
|
||||
Log.Warning(
|
||||
e,
|
||||
$"HellionChat: file move blocked for {file}, will retry on next load"
|
||||
);
|
||||
lockedBlocker = true;
|
||||
}
|
||||
}
|
||||
@@ -486,7 +481,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Log.Warning(e, $"HellionChat: subdir move blocked for {dir}, will retry on next load");
|
||||
Log.Warning(
|
||||
e,
|
||||
$"HellionChat: subdir move blocked for {dir}, will retry on next load"
|
||||
);
|
||||
lockedBlocker = true;
|
||||
}
|
||||
}
|
||||
@@ -504,27 +502,23 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
if (lockedBlocker)
|
||||
{
|
||||
// Surface the most common cause to the user as a notification
|
||||
// so they don't think Hellion Chat lost their history when in
|
||||
// fact upstream Chat 2 was still holding the database file.
|
||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = "Hellion Chat",
|
||||
Content = "Could not migrate the Chat 2 database — the file appears to be in use. " +
|
||||
"Disable Chat 2, fully close the game, then start it again. " +
|
||||
"See the README troubleshooting section if the issue persists.",
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
InitialDuration = TimeSpan.FromSeconds(30),
|
||||
});
|
||||
Notification.AddNotification(
|
||||
new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = "Hellion Chat",
|
||||
Content =
|
||||
"Could not migrate the Chat 2 database — the file appears to be in use. "
|
||||
+ "Disable Chat 2, fully close the game, then start it again. "
|
||||
+ "See the README troubleshooting section if the issue persists.",
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
InitialDuration = TimeSpan.FromSeconds(30),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenMainUi()
|
||||
{
|
||||
// Settings is the most useful landing surface — same target as the
|
||||
// Configure button. SettingsWindow.Toggle is internal and already
|
||||
// wired to OpenConfigUi, so re-using IsOpen keeps both entry points
|
||||
// behaviourally identical.
|
||||
SettingsWindow.IsOpen = !SettingsWindow.IsOpen;
|
||||
}
|
||||
|
||||
@@ -535,8 +529,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
if (DateTimeOffset.UtcNow - Config.RetentionLastRunAt < TimeSpan.FromHours(24))
|
||||
return;
|
||||
|
||||
// Snapshot the policy so the user can edit settings while we run.
|
||||
// Spec defaults form the baseline; explicit user overrides win.
|
||||
// Snapshot the policy so the user can edit settings while the sweep runs.
|
||||
var policy = new Dictionary<int, int>();
|
||||
foreach (var (type, days) in Privacy.PrivacyDefaults.DefaultRetentionDays)
|
||||
policy[(int)(ushort)type] = days;
|
||||
@@ -544,16 +537,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
policy[(int)(ushort)type] = days;
|
||||
var defaultDays = Config.RetentionDefaultDays;
|
||||
|
||||
// IsBackground = true for the same reason as PendingMessageThread:
|
||||
// a stuck sweep must never block plugin unload. RunRetentionSweepIfDue
|
||||
// guards the run-frequency, and the sweep itself uses the framework's
|
||||
// cooperative cancellation pattern. The background flag is the safety
|
||||
// net if the sweep ever takes longer than expected.
|
||||
// IsBackground = true so a stuck sweep never blocks plugin unload.
|
||||
new Thread(() =>
|
||||
{
|
||||
// Bail out cheaply if a manual sweep is already in flight; the
|
||||
// lock around the actual work would queue us up otherwise and
|
||||
// we would just re-do whatever the manual run already did.
|
||||
// Bail early if a manual sweep is already in flight.
|
||||
lock (RetentionSweepLock)
|
||||
{
|
||||
if (RetentionSweepRunning)
|
||||
@@ -570,16 +557,15 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
if (deleted > 0)
|
||||
{
|
||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||
// Run the clear+refilter synchronously on the framework thread.
|
||||
// Earlier this called FilterAllTabsAsync(), which is fire-and-forget
|
||||
// — the .Wait() here would return as soon as the inner Task.Run was
|
||||
// dispatched, racing the next sweep cycle against the still-running
|
||||
// filter pass. See AUDIT-2026-05-05 [QUAL-02].
|
||||
Framework.Run(() =>
|
||||
{
|
||||
MessageManager.ClearAllTabs();
|
||||
MessageManager.FilterAllTabs();
|
||||
}).Wait();
|
||||
// Run clear+refilter on the framework thread — FilterAllTabsAsync
|
||||
// is fire-and-forget and would race the next sweep cycle.
|
||||
Framework
|
||||
.Run(() =>
|
||||
{
|
||||
MessageManager.ClearAllTabs();
|
||||
MessageManager.FilterAllTabs();
|
||||
})
|
||||
.Wait();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -595,15 +581,19 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
lock (RetentionSweepLock)
|
||||
RetentionSweepRunning = false;
|
||||
}
|
||||
}) { IsBackground = true }.Start();
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
}.Start();
|
||||
}
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
// Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes
|
||||
// Theme statt einem deaktivierten Hellion-Theme. Active wird einmal
|
||||
// pro Frame aus der Registry gelesen.
|
||||
using IDisposable _style = HellionStyle.PushGlobal(ThemeRegistry.Active, Config.WindowOpacity);
|
||||
// Theme engine is always active; Classic is a theme, not a disabled state.
|
||||
using IDisposable _style = HellionStyle.PushGlobal(
|
||||
ThemeRegistry.Active,
|
||||
Config.WindowOpacity
|
||||
);
|
||||
|
||||
ChatLogWindow.BeginFrame();
|
||||
|
||||
@@ -614,10 +604,13 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
return;
|
||||
}
|
||||
|
||||
// v1.0.2 — global skip while the New Game+ menu (QuestRedo addon) is
|
||||
// open. Hides every plugin window in one shot (chat log, pop-outs,
|
||||
// settings, db viewer, etc.), matching the LoadingScreens pattern.
|
||||
if (Config.HideInNewGamePlusMenu && GameFunctions.GameFunctions.IsAddonInteractable(GameFunctions.GameFunctions.NewGamePlusAddonName))
|
||||
// Hide all plugin windows while the New Game+ menu is open.
|
||||
if (
|
||||
Config.HideInNewGamePlusMenu
|
||||
&& GameFunctions.GameFunctions.IsAddonInteractable(
|
||||
GameFunctions.GameFunctions.NewGamePlusAddonName
|
||||
)
|
||||
)
|
||||
{
|
||||
ChatLogWindow.FinalizeFrame();
|
||||
TypingIpc.Update();
|
||||
@@ -627,7 +620,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
ChatLogWindow.HideStateCheck();
|
||||
|
||||
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
||||
ChatLogWindow.DefaultText = ImGui.GetStyle().Colors[(int) ImGuiCol.Text];
|
||||
ChatLogWindow.DefaultText = ImGui.GetStyle().Colors[(int)ImGuiCol.Text];
|
||||
|
||||
using ((Config.FontsEnabled ? FontManager.RegularFont : FontManager.Axis).Push())
|
||||
WindowSystem.Draw();
|
||||
@@ -640,10 +633,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
internal void SaveConfig()
|
||||
{
|
||||
// Hellion Chat — Auto-Tell-Tabs are session-only. Strip them out
|
||||
// before serialization so a crash mid-session can never persist
|
||||
// them. We snapshot the full tab list first and restore it after
|
||||
// the save, preserving the user's order and open conversations.
|
||||
// Strip session-only Auto-Tell-Tabs before serialization; restore after.
|
||||
var snapshot = Config.Tabs.ToList();
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
|
||||
@@ -655,9 +645,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
internal void LanguageChanged(string langCode)
|
||||
{
|
||||
var info = Config.LanguageOverride is LanguageOverride.None
|
||||
? new CultureInfo(langCode)
|
||||
: new CultureInfo(Config.LanguageOverride.Code());
|
||||
var info =
|
||||
Config.LanguageOverride is LanguageOverride.None
|
||||
? new CultureInfo(langCode)
|
||||
: new CultureInfo(Config.LanguageOverride.Code());
|
||||
|
||||
Language.Culture = info;
|
||||
HellionStrings.Culture = info;
|
||||
@@ -669,7 +660,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
"ChatLogPanel_0",
|
||||
"ChatLogPanel_1",
|
||||
"ChatLogPanel_2",
|
||||
"ChatLogPanel_3"
|
||||
"ChatLogPanel_3",
|
||||
];
|
||||
|
||||
private void FrameworkUpdate(IFramework framework)
|
||||
@@ -687,18 +678,21 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
public static bool InBattle => Condition[ConditionFlag.InCombat];
|
||||
public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene];
|
||||
public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78];
|
||||
public static bool CutsceneActive =>
|
||||
Condition[ConditionFlag.OccupiedInCutSceneEvent]
|
||||
|| Condition[ConditionFlag.WatchingCutscene78];
|
||||
|
||||
// v1.1.0 — wenn der themes/-Ordner leer ist, schreiben wir die embedded
|
||||
// example-theme.json als Vorlage rein. Bestehende User-Customs werden
|
||||
// nicht angefasst (existing JSONs lassen den Block überspringen).
|
||||
// Seeds example-theme.json into the themes dir on first run.
|
||||
// Skipped if any custom JSON already exists.
|
||||
private static void SeedExampleThemeIfEmpty(string dir)
|
||||
{
|
||||
if (Directory.EnumerateFiles(dir, "*.json").Any())
|
||||
return;
|
||||
|
||||
var examplePath = Path.Combine(dir, "example-theme.json");
|
||||
var resourceStream = typeof(Plugin).Assembly.GetManifestResourceStream("HellionChat.Themes.Builtin.example-theme.json");
|
||||
var resourceStream = typeof(Plugin).Assembly.GetManifestResourceStream(
|
||||
"HellionChat.Themes.Builtin.example-theme.json"
|
||||
);
|
||||
if (resourceStream is null)
|
||||
{
|
||||
Log.Warning("Themes example template not found in assembly resources; skipping seed.");
|
||||
@@ -713,7 +707,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to seed example-theme.json; user can create custom themes manually.");
|
||||
Log.Warning(
|
||||
ex,
|
||||
"Failed to seed example-theme.json; user can create custom themes manually."
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -46,50 +46,53 @@ internal static class PrivacyDefaults
|
||||
// here fall back to Configuration.RetentionDefaultDays. Reflects the
|
||||
// design spec: Tells 365, own-conversation channels 90, everything else
|
||||
// shorter via the global default.
|
||||
internal static readonly IReadOnlyDictionary<ChatType, int> DefaultRetentionDays = new Dictionary<ChatType, int>
|
||||
{
|
||||
[ChatType.TellIncoming] = 365,
|
||||
[ChatType.TellOutgoing] = 365,
|
||||
internal static readonly IReadOnlyDictionary<ChatType, int> DefaultRetentionDays =
|
||||
new Dictionary<ChatType, int>
|
||||
{
|
||||
[ChatType.TellIncoming] = 365,
|
||||
[ChatType.TellOutgoing] = 365,
|
||||
|
||||
[ChatType.Party] = 90,
|
||||
[ChatType.CrossParty] = 90,
|
||||
[ChatType.Alliance] = 90,
|
||||
[ChatType.PvpTeam] = 90,
|
||||
[ChatType.FreeCompany] = 90,
|
||||
[ChatType.Party] = 90,
|
||||
[ChatType.CrossParty] = 90,
|
||||
[ChatType.Alliance] = 90,
|
||||
[ChatType.PvpTeam] = 90,
|
||||
[ChatType.FreeCompany] = 90,
|
||||
|
||||
[ChatType.Linkshell1] = 90,
|
||||
[ChatType.Linkshell2] = 90,
|
||||
[ChatType.Linkshell3] = 90,
|
||||
[ChatType.Linkshell4] = 90,
|
||||
[ChatType.Linkshell5] = 90,
|
||||
[ChatType.Linkshell6] = 90,
|
||||
[ChatType.Linkshell7] = 90,
|
||||
[ChatType.Linkshell8] = 90,
|
||||
[ChatType.Linkshell1] = 90,
|
||||
[ChatType.Linkshell2] = 90,
|
||||
[ChatType.Linkshell3] = 90,
|
||||
[ChatType.Linkshell4] = 90,
|
||||
[ChatType.Linkshell5] = 90,
|
||||
[ChatType.Linkshell6] = 90,
|
||||
[ChatType.Linkshell7] = 90,
|
||||
[ChatType.Linkshell8] = 90,
|
||||
|
||||
[ChatType.CrossLinkshell1] = 90,
|
||||
[ChatType.CrossLinkshell2] = 90,
|
||||
[ChatType.CrossLinkshell3] = 90,
|
||||
[ChatType.CrossLinkshell4] = 90,
|
||||
[ChatType.CrossLinkshell5] = 90,
|
||||
[ChatType.CrossLinkshell6] = 90,
|
||||
[ChatType.CrossLinkshell7] = 90,
|
||||
[ChatType.CrossLinkshell8] = 90,
|
||||
[ChatType.CrossLinkshell1] = 90,
|
||||
[ChatType.CrossLinkshell2] = 90,
|
||||
[ChatType.CrossLinkshell3] = 90,
|
||||
[ChatType.CrossLinkshell4] = 90,
|
||||
[ChatType.CrossLinkshell5] = 90,
|
||||
[ChatType.CrossLinkshell6] = 90,
|
||||
[ChatType.CrossLinkshell7] = 90,
|
||||
[ChatType.CrossLinkshell8] = 90,
|
||||
|
||||
[ChatType.ExtraChatLinkshell1] = 90,
|
||||
[ChatType.ExtraChatLinkshell2] = 90,
|
||||
[ChatType.ExtraChatLinkshell3] = 90,
|
||||
[ChatType.ExtraChatLinkshell4] = 90,
|
||||
[ChatType.ExtraChatLinkshell5] = 90,
|
||||
[ChatType.ExtraChatLinkshell6] = 90,
|
||||
[ChatType.ExtraChatLinkshell7] = 90,
|
||||
[ChatType.ExtraChatLinkshell8] = 90,
|
||||
};
|
||||
[ChatType.ExtraChatLinkshell1] = 90,
|
||||
[ChatType.ExtraChatLinkshell2] = 90,
|
||||
[ChatType.ExtraChatLinkshell3] = 90,
|
||||
[ChatType.ExtraChatLinkshell4] = 90,
|
||||
[ChatType.ExtraChatLinkshell5] = 90,
|
||||
[ChatType.ExtraChatLinkshell6] = 90,
|
||||
[ChatType.ExtraChatLinkshell7] = 90,
|
||||
[ChatType.ExtraChatLinkshell8] = 90,
|
||||
};
|
||||
|
||||
// Casual profile = Privacy-First plus public chat (Say/Shout/Yell, both
|
||||
// emote types, Novice Network), kept for a short 24-hour window so the
|
||||
// last RP scene or shout trade is still searchable but third-party data
|
||||
// doesn't accumulate forever.
|
||||
internal static readonly IReadOnlySet<ChatType> CasualWhitelist = new HashSet<ChatType>(PrivacyFirstWhitelist)
|
||||
internal static readonly IReadOnlySet<ChatType> CasualWhitelist = new HashSet<ChatType>(
|
||||
PrivacyFirstWhitelist
|
||||
)
|
||||
{
|
||||
ChatType.Say,
|
||||
ChatType.Shout,
|
||||
@@ -99,13 +102,14 @@ internal static class PrivacyDefaults
|
||||
ChatType.NoviceNetwork,
|
||||
};
|
||||
|
||||
internal static readonly IReadOnlyDictionary<ChatType, int> CasualRetentionOverrides = new Dictionary<ChatType, int>
|
||||
{
|
||||
[ChatType.Say] = 1,
|
||||
[ChatType.Shout] = 1,
|
||||
[ChatType.Yell] = 1,
|
||||
[ChatType.CustomEmote] = 1,
|
||||
[ChatType.StandardEmote] = 1,
|
||||
[ChatType.NoviceNetwork] = 1,
|
||||
};
|
||||
internal static readonly IReadOnlyDictionary<ChatType, int> CasualRetentionOverrides =
|
||||
new Dictionary<ChatType, int>
|
||||
{
|
||||
[ChatType.Say] = 1,
|
||||
[ChatType.Shout] = 1,
|
||||
[ChatType.Yell] = 1,
|
||||
[ChatType.CustomEmote] = 1,
|
||||
[ChatType.StandardEmote] = 1,
|
||||
[ChatType.NoviceNetwork] = 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,16 +4,14 @@ using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Resources;
|
||||
|
||||
// Hellion Chat — v0.6.0 built-in colour presets for the ChatColours
|
||||
// settings section. Read-only static data; users apply a preset via the
|
||||
// settings UI which overwrites Configuration.ChatColours immediately.
|
||||
// Battle-channel types are intentionally NOT covered by the stylistic
|
||||
// presets so that combat-log tuning the user has done stays intact.
|
||||
// Built-in colour presets applied via Settings UI → ChatColours.
|
||||
// Battle-channel types are intentionally excluded to preserve combat-log tuning.
|
||||
public sealed record ChatColourPreset(
|
||||
string DisplayName,
|
||||
string LocalizationKey,
|
||||
bool IsBrandPreset,
|
||||
IReadOnlyDictionary<ChatType, uint> Colours);
|
||||
IReadOnlyDictionary<ChatType, uint> Colours
|
||||
);
|
||||
|
||||
public static class ChatColourPresets
|
||||
{
|
||||
@@ -27,43 +25,48 @@ public static class ChatColourPresets
|
||||
DisplayName: "ChatTwo Default",
|
||||
LocalizationKey: "ChatColourPresets_Default",
|
||||
IsBrandPreset: false,
|
||||
Colours: BuildDefault()),
|
||||
Colours: BuildDefault()
|
||||
),
|
||||
["HighContrast"] = new(
|
||||
DisplayName: "High-Contrast",
|
||||
LocalizationKey: "ChatColourPresets_HighContrast",
|
||||
IsBrandPreset: false,
|
||||
Colours: BuildHighContrast()),
|
||||
Colours: BuildHighContrast()
|
||||
),
|
||||
["Pastell"] = new(
|
||||
DisplayName: "Pastell",
|
||||
LocalizationKey: "ChatColourPresets_Pastell",
|
||||
IsBrandPreset: false,
|
||||
Colours: BuildPastell()),
|
||||
Colours: BuildPastell()
|
||||
),
|
||||
["DarkModeTuned"] = new(
|
||||
DisplayName: "Dark-Mode-Tuned",
|
||||
LocalizationKey: "ChatColourPresets_DarkModeTuned",
|
||||
IsBrandPreset: false,
|
||||
Colours: BuildDarkModeTuned()),
|
||||
Colours: BuildDarkModeTuned()
|
||||
),
|
||||
["Hellion"] = new(
|
||||
DisplayName: "Hellion",
|
||||
LocalizationKey: "ChatColourPresets_Hellion",
|
||||
IsBrandPreset: true,
|
||||
Colours: BuildHellion()),
|
||||
Colours: BuildHellion()
|
||||
),
|
||||
["NightBlue"] = new(
|
||||
DisplayName: "Night Blue",
|
||||
LocalizationKey: "ChatColourPresets_NightBlue",
|
||||
IsBrandPreset: false,
|
||||
Colours: BuildNightBlue()),
|
||||
Colours: BuildNightBlue()
|
||||
),
|
||||
["IndigoViolet"] = new(
|
||||
DisplayName: "Indigo Violet",
|
||||
LocalizationKey: "ChatColourPresets_IndigoViolet",
|
||||
IsBrandPreset: false,
|
||||
Colours: BuildIndigoViolet()),
|
||||
Colours: BuildIndigoViolet()
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// The Default preset spiegelt 1:1 die Werte aus ChatTypeExt.DefaultColor.
|
||||
// Channels ohne Default-Wert (return null) werden ausgelassen — wer sie
|
||||
// anwenden will, behält seine aktuelle Farbe.
|
||||
// Mirrors ChatTypeExt.DefaultColor; channels without a default are skipped.
|
||||
private static IReadOnlyDictionary<ChatType, uint> BuildDefault()
|
||||
{
|
||||
var dict = new Dictionary<ChatType, uint>();
|
||||
@@ -175,79 +178,55 @@ public static class ChatColourPresets
|
||||
};
|
||||
}
|
||||
|
||||
// Hellion brand preset — Arctic Cyan + Ember Orange palette aus
|
||||
// /mnt/ssd-fast/Projekte/hellion-media/hellion-media-website/BRANDING.md
|
||||
// (Schema-Stand 2026-04-16). Channels sind über das ganze Brand-Spektrum
|
||||
// verteilt damit jede Zeile auf einen Glance unterscheidbar ist:
|
||||
// Cyan-Familie für Standard/Tell, Ember + Warning für laute Channels,
|
||||
// Status-Farben (Success, Danger) für Linkshells. CrossLinkshells
|
||||
// nutzen die dunkleren/sattersten Varianten derselben Hue-Familien.
|
||||
// Hellion brand preset — Arctic Cyan + Ember Orange palette.
|
||||
// Cyan family for Standard/Tell, Ember/Warning for loud channels,
|
||||
// Status colours for Linkshells, darker variants for CrossLinkshells.
|
||||
private static IReadOnlyDictionary<ChatType, uint> BuildHellion()
|
||||
{
|
||||
return new Dictionary<ChatType, uint>
|
||||
{
|
||||
// Standard / Tell — Cyan-Familie (Brand-Primary)
|
||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light #4DD9E8
|
||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light #4DD9E8
|
||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan #00BED2
|
||||
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark #0097A7
|
||||
|
||||
// Laute Channels — Ember/Warning
|
||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning #F0AD4E
|
||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember #F97316
|
||||
|
||||
// Gruppen-Channels — Success/Ember-dark/Cyan
|
||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success #5CB85C
|
||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark #E85D04
|
||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(77, 217, 232),// Cyan-light
|
||||
|
||||
// Linkshells 1-8 — über das ganze Brand-Spektrum verteilt
|
||||
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(251, 146, 60), // Ember-light #FB923C
|
||||
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning
|
||||
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success
|
||||
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
|
||||
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember
|
||||
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(217, 83, 79), // Danger #D9534F
|
||||
|
||||
// CrossWorld-Linkshells 1-8 — dunklere/sattersere Varianten
|
||||
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark
|
||||
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(200, 140, 50), // Warning-dark
|
||||
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(60, 140, 60), // Success-dark
|
||||
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(0, 110, 130), // Cyan-darker
|
||||
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(220, 90, 30), // Ember-medium
|
||||
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(170, 60, 60), // Danger-dark
|
||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning #F0AD4E
|
||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember #F97316
|
||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success #5CB85C
|
||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark #E85D04
|
||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
|
||||
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(251, 146, 60), // Ember-light #FB923C
|
||||
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning
|
||||
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success
|
||||
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
|
||||
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember
|
||||
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(217, 83, 79), // Danger #D9534F
|
||||
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark
|
||||
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(200, 140, 50), // Warning-dark
|
||||
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(60, 140, 60), // Success-dark
|
||||
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
|
||||
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
|
||||
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(0, 110, 130), // Cyan-darker
|
||||
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(220, 90, 30), // Ember-medium
|
||||
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(170, 60, 60), // Danger-dark
|
||||
};
|
||||
}
|
||||
|
||||
// Bonus preset — Night Blue, KAZAMA-Stimmungs-Theme aus
|
||||
// /mnt/HDD-Data1/Obsidian/Vault/Systeme/KAZAMA/Theming/Night Blue + Indigo Violet Themes.md
|
||||
// Klassisch, kühl, technisch — Marineblau-Tiefe ohne Lila-Anteil.
|
||||
// Bewusst NICHT als Brand-Preset markiert (Vault-Boundary): die KAZAMA-Themes
|
||||
// sind persönliche Stimmungs-Themes, nicht Teil des Hellion-Brand-Systems.
|
||||
// Night Blue — cool nautical theme, deep navy without purple.
|
||||
private static IReadOnlyDictionary<ChatType, uint> BuildNightBlue()
|
||||
{
|
||||
return new Dictionary<ChatType, uint>
|
||||
{
|
||||
// Standard / Tell — Royal Blue Akzent-Familie
|
||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(230, 237, 247), // text-primary
|
||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(106, 176, 255),// akzent-hot
|
||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(230, 237, 247), // text-primary
|
||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(106, 176, 255), // akzent-hot
|
||||
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
||||
|
||||
// Laute Channels — Warning/Danger Status-Töne
|
||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74), // warning
|
||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122), // danger
|
||||
|
||||
// Gruppen — Success/Akzent-Variations
|
||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151), // success
|
||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100), // warm-orange-light
|
||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(140, 160, 191),// text-dim
|
||||
|
||||
// Linkshells 1-8 — über Spektrum verteilt
|
||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74), // warning
|
||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122), // danger
|
||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151), // success
|
||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100), // warm-orange-light
|
||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
|
||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(140, 160, 191), // text-dim
|
||||
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
||||
@@ -256,8 +235,6 @@ public static class ChatColourPresets
|
||||
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(100, 200, 220),
|
||||
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(106, 176, 255),
|
||||
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(140, 160, 191),
|
||||
|
||||
// CrossWorld-Linkshells — gedämpfte Variants
|
||||
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
||||
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
||||
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
||||
@@ -269,30 +246,20 @@ public static class ChatColourPresets
|
||||
};
|
||||
}
|
||||
|
||||
// Bonus preset — Indigo Violet, KAZAMA-Stimmungs-Theme aus demselben
|
||||
// Vault-Doc. Warm-mystisch, "Galaxy/Glitter/Nordlicht" — tiefes Indigo
|
||||
// mit kräftigem Violet-Akzent. Persönlicher Favorit (siehe Vault).
|
||||
// Auch nicht als Brand-Preset (siehe NightBlue-Note oben).
|
||||
// Indigo Violet — warm-mystic theme, deep indigo with violet accent.
|
||||
private static IReadOnlyDictionary<ChatType, uint> BuildIndigoViolet()
|
||||
{
|
||||
return new Dictionary<ChatType, uint>
|
||||
{
|
||||
// Standard / Tell — Royal Violet Akzent-Familie
|
||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary (light lavender)
|
||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(176, 124, 255),// akzent-hot
|
||||
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary
|
||||
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(176, 124, 255), // akzent-hot
|
||||
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
||||
|
||||
// Laute Channels — geteilt mit Night Blue (Status-Farben)
|
||||
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122),
|
||||
|
||||
// Gruppen
|
||||
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151),
|
||||
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(168, 144, 208),// text-dim
|
||||
|
||||
// Linkshells 1-8
|
||||
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
|
||||
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(168, 144, 208), // text-dim
|
||||
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
|
||||
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
|
||||
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
|
||||
@@ -301,8 +268,6 @@ public static class ChatColourPresets
|
||||
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(139, 77, 222),
|
||||
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(130, 90, 200),
|
||||
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(168, 144, 208),
|
||||
|
||||
// CrossWorld-Linkshells
|
||||
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
|
||||
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
|
||||
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
|
||||
|
||||
@@ -4,13 +4,9 @@ using HellionChat.Themes;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// Validates the runtime theme-switch contract from the user side. The
|
||||
// caller toggles the active theme via Settings -> Theme & Layout, the
|
||||
// step polls ThemeRegistry.Active per frame and only passes once the
|
||||
// slug has moved away from the initial value and back. The ABGR cache
|
||||
// is sanity-checked on every frame: a freshly switched theme must carry
|
||||
// a populated cache, otherwise Switch() forgot the recompute and the UI
|
||||
// would still draw, just with all-transparent slots.
|
||||
// Validates the runtime theme-switch contract: polls ThemeRegistry.Active
|
||||
// per frame until the slug moves away and back, then sanity-checks that
|
||||
// the ABGR cache was recomputed on switch.
|
||||
internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep
|
||||
{
|
||||
private readonly Plugin plugin;
|
||||
@@ -40,7 +36,9 @@ internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep
|
||||
if (this.initialSlug is null)
|
||||
{
|
||||
this.initialSlug = active.Slug;
|
||||
ImGui.Text($"Initial theme: \"{this.initialSlug}\". Open Settings -> Theme & Layout and pick a different theme.");
|
||||
ImGui.Text(
|
||||
$"Initial theme: \"{this.initialSlug}\". Open Settings -> Theme & Layout and pick a different theme."
|
||||
);
|
||||
return SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
@@ -71,15 +69,11 @@ internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep
|
||||
this.switchedAway = false;
|
||||
}
|
||||
|
||||
// Any non-zero slot proves the cache was actually recomputed for the
|
||||
// current theme. We don't compare against a reference, because custom
|
||||
// themes can legitimately share slot values with a built-in.
|
||||
// Any non-zero slot confirms the cache was recomputed — no reference
|
||||
// comparison since custom themes can share slot values with built-ins.
|
||||
private static bool HasPopulatedCache(Theme theme)
|
||||
{
|
||||
var cache = theme.AbgrCache;
|
||||
return (cache.Primary
|
||||
| cache.WindowBg
|
||||
| cache.TextPrimary
|
||||
| cache.Border) != 0u;
|
||||
return (cache.Primary | cache.WindowBg | cache.TextPrimary | cache.Border) != 0u;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ public static class Sheets
|
||||
}
|
||||
|
||||
public static bool IsInForay() =>
|
||||
TerritorySheet.TryGetRow(Plugin.ClientState.TerritoryType, out var row) &&
|
||||
row.TerritoryIntendedUse.RowId is 41 or 61;
|
||||
TerritorySheet.TryGetRow(Plugin.ClientState.TerritoryType, out var row)
|
||||
&& row.TerritoryIntendedUse.RowId is 41 or 61;
|
||||
|
||||
public static IEnumerable<World> WorldsOnDatacenter(IPlayerCharacter character)
|
||||
{
|
||||
var dcRow = character.HomeWorld.Value.DataCenter.RowId;
|
||||
return WorldSheet.Where(world => world.IsPublic && world.DataCenter.RowId == dcRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,45 +6,47 @@ internal static class Chat2Classic
|
||||
{
|
||||
public const string Slug = "chat2-classic";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Chat 2 Klassik",
|
||||
Author: "Upstream (Infi & Anna)",
|
||||
Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||
Primary: ColourUtil.HexToRgba("#4682B4"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#4682B466"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||
Accent: ColourUtil.HexToRgba("#4682B4"),
|
||||
AccentLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#4682B4"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"),
|
||||
ChildBg: ColourUtil.HexToRgba("#141414"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1A1A1A"),
|
||||
Surface: ColourUtil.HexToRgba("#202020"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"),
|
||||
Border: ColourUtil.HexToRgba("#404040"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6E6E6"),
|
||||
TextMuted: ColourUtil.HexToRgba("#999999"),
|
||||
TextDim: ColourUtil.HexToRgba("#666666"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#4682B4")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 0f, ChildRounding: 0f, PopupRounding: 0f,
|
||||
FrameRounding: 0f, GrabRounding: 0f, TabRounding: 0f,
|
||||
ScrollbarRounding: 0f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Chat 2 Klassik",
|
||||
Author: "Upstream (Infi & Anna)",
|
||||
Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||
Primary: ColourUtil.HexToRgba("#4682B4"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#4682B466"),
|
||||
AccentDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||
Accent: ColourUtil.HexToRgba("#4682B4"),
|
||||
AccentLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||
Identity: ColourUtil.HexToRgba("#4682B4"),
|
||||
WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"),
|
||||
ChildBg: ColourUtil.HexToRgba("#141414"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1A1A1A"),
|
||||
Surface: ColourUtil.HexToRgba("#202020"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"),
|
||||
Border: ColourUtil.HexToRgba("#404040"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6E6E6"),
|
||||
TextMuted: ColourUtil.HexToRgba("#999999"),
|
||||
TextDim: ColourUtil.HexToRgba("#666666"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#4682B4")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 0f,
|
||||
ChildRounding: 0f,
|
||||
PopupRounding: 0f,
|
||||
FrameRounding: 0f,
|
||||
GrabRounding: 0f,
|
||||
TabRounding: 0f,
|
||||
ScrollbarRounding: 0f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,72 +6,76 @@ internal static class EventHorizon
|
||||
{
|
||||
public const string Slug = "event-horizon";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Event Horizon",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"),
|
||||
Primary: ColourUtil.HexToRgba("#9D5CFF"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#B585FF"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#C9982E"),
|
||||
Accent: ColourUtil.HexToRgba("#E0AB36"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F2C25C"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#9D5CFF"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#040308"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0A081A"),
|
||||
FrameBg: ColourUtil.HexToRgba("#140F23"),
|
||||
Surface: ColourUtil.HexToRgba("#1B1530"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#251D40"),
|
||||
Border: ColourUtil.HexToRgba("#9D5CFF44"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6E0F5"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9890B5"),
|
||||
TextDim: ColourUtil.HexToRgba("#5A5570"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#26A269"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#ED333B"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E0AB36"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#9D5CFF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
|
||||
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
|
||||
// Lila. Channel-Identität bleibt klar erkennbar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Event Horizon",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"),
|
||||
Primary: ColourUtil.HexToRgba("#9D5CFF"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#B585FF"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"),
|
||||
AccentDark: ColourUtil.HexToRgba("#C9982E"),
|
||||
Accent: ColourUtil.HexToRgba("#E0AB36"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F2C25C"),
|
||||
Identity: ColourUtil.HexToRgba("#9D5CFF"),
|
||||
WindowBg: ColourUtil.HexToRgba("#040308"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0A081A"),
|
||||
FrameBg: ColourUtil.HexToRgba("#140F23"),
|
||||
Surface: ColourUtil.HexToRgba("#1B1530"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#251D40"),
|
||||
Border: ColourUtil.HexToRgba("#9D5CFF44"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6E0F5"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9890B5"),
|
||||
TextDim: ColourUtil.HexToRgba("#5A5570"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#26A269"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#ED333B"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E0AB36"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#9D5CFF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f,
|
||||
ChildRounding: 5f,
|
||||
PopupRounding: 5f,
|
||||
FrameRounding: 4f,
|
||||
GrabRounding: 4f,
|
||||
TabRounding: 4f,
|
||||
ScrollbarRounding: 4f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
|
||||
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
|
||||
// Lila. Channel-Identität bleibt klar erkennbar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,71 +6,75 @@ internal static class ForgeMerchantman
|
||||
{
|
||||
public const string Slug = "forge-merchantman";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Forge Merchantman",
|
||||
Author: "Carla Beleandis",
|
||||
Description: "Patina Bronze auf Workshop-Slate — Hellion Forge im Plugin.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#1F8A82"),
|
||||
Primary: ColourUtil.HexToRgba("#2DB39E"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#4FC9B0"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#2DB39E99"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#B86A20"),
|
||||
Accent: ColourUtil.HexToRgba("#D9892C"),
|
||||
AccentLight: ColourUtil.HexToRgba("#E8A04A"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#1F8A82"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#050B0A"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0B1413"),
|
||||
FrameBg: ColourUtil.HexToRgba("#11201D"),
|
||||
Surface: ColourUtil.HexToRgba("#182925"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#213631"),
|
||||
Border: ColourUtil.HexToRgba("#2DB39E66"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#D8EFE8"),
|
||||
TextMuted: ColourUtil.HexToRgba("#8FA39B"),
|
||||
TextDim: ColourUtil.HexToRgba("#5A6E66"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#2DB39E")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 4f, ChildRounding: 3f, PopupRounding: 3f,
|
||||
FrameRounding: 2f, GrabRounding: 2f, TabRounding: 2f,
|
||||
ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Forge Merchantman — Patina-Tinte in Party/FC, Bernstein-Tinte in
|
||||
// Yell/Alliance/CustomEmote. Channel-identity bleibt voll erhalten.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0C060"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#E8902C"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#6AC9B0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#E8A04A"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4FB8A0"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#6AC9B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#E8A04A"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0C060"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#6AC9B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#8FA39B"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Forge Merchantman",
|
||||
Author: "Carla Beleandis",
|
||||
Description: "Patina Bronze auf Workshop-Slate — Hellion Forge im Plugin.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#1F8A82"),
|
||||
Primary: ColourUtil.HexToRgba("#2DB39E"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#4FC9B0"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#2DB39E99"),
|
||||
AccentDark: ColourUtil.HexToRgba("#B86A20"),
|
||||
Accent: ColourUtil.HexToRgba("#D9892C"),
|
||||
AccentLight: ColourUtil.HexToRgba("#E8A04A"),
|
||||
Identity: ColourUtil.HexToRgba("#1F8A82"),
|
||||
WindowBg: ColourUtil.HexToRgba("#050B0A"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0B1413"),
|
||||
FrameBg: ColourUtil.HexToRgba("#11201D"),
|
||||
Surface: ColourUtil.HexToRgba("#182925"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#213631"),
|
||||
Border: ColourUtil.HexToRgba("#2DB39E66"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#D8EFE8"),
|
||||
TextMuted: ColourUtil.HexToRgba("#8FA39B"),
|
||||
TextDim: ColourUtil.HexToRgba("#5A6E66"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#2DB39E")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 4f,
|
||||
ChildRounding: 3f,
|
||||
PopupRounding: 3f,
|
||||
FrameRounding: 2f,
|
||||
GrabRounding: 2f,
|
||||
TabRounding: 2f,
|
||||
ScrollbarRounding: 2f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Forge Merchantman — Patina-Tinte in Party/FC, Bernstein-Tinte in
|
||||
// Yell/Alliance/CustomEmote. Channel-identity bleibt voll erhalten.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0C060"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#E8902C"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#6AC9B0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#E8A04A"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4FB8A0"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#6AC9B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#E8A04A"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0C060"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#6AC9B0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#8FA39B"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,71 +6,75 @@ internal static class HellionArctic
|
||||
{
|
||||
public const string Slug = "hellion-arctic";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Hellion Arctic",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Arctic Cyan + Ember Glow on industrial slate. Plugin default.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#0097A7"),
|
||||
Primary: ColourUtil.HexToRgba("#00BED2"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#4DD9E8"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#00BED299"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#E85D04"),
|
||||
Accent: ColourUtil.HexToRgba("#F97316"),
|
||||
AccentLight: ColourUtil.HexToRgba("#FB923C"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#0097A7"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#070B12"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0C1220"),
|
||||
FrameBg: ColourUtil.HexToRgba("#141E30"),
|
||||
Surface: ColourUtil.HexToRgba("#1A2538"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#22303F"),
|
||||
Border: ColourUtil.HexToRgba("#00BED266"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6F4F1"),
|
||||
TextMuted: ColourUtil.HexToRgba("#8FA3B5"),
|
||||
TextDim: ColourUtil.HexToRgba("#566273"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#00BED2")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 4f, ChildRounding: 3f, PopupRounding: 3f,
|
||||
FrameRounding: 2f, GrabRounding: 2f, TabRounding: 2f,
|
||||
ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den
|
||||
// blauen Channels (Party/FC). Channel-Identität bleibt klar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80C0E8"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFB870"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4DD9E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80C0E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FFC080"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFE066"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80C0E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Hellion Arctic",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Arctic Cyan + Ember Glow on industrial slate. Plugin default.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#0097A7"),
|
||||
Primary: ColourUtil.HexToRgba("#00BED2"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#4DD9E8"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#00BED299"),
|
||||
AccentDark: ColourUtil.HexToRgba("#E85D04"),
|
||||
Accent: ColourUtil.HexToRgba("#F97316"),
|
||||
AccentLight: ColourUtil.HexToRgba("#FB923C"),
|
||||
Identity: ColourUtil.HexToRgba("#0097A7"),
|
||||
WindowBg: ColourUtil.HexToRgba("#070B12"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0C1220"),
|
||||
FrameBg: ColourUtil.HexToRgba("#141E30"),
|
||||
Surface: ColourUtil.HexToRgba("#1A2538"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#22303F"),
|
||||
Border: ColourUtil.HexToRgba("#00BED266"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6F4F1"),
|
||||
TextMuted: ColourUtil.HexToRgba("#8FA3B5"),
|
||||
TextDim: ColourUtil.HexToRgba("#566273"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#00BED2")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 4f,
|
||||
ChildRounding: 3f,
|
||||
PopupRounding: 3f,
|
||||
FrameRounding: 2f,
|
||||
GrabRounding: 2f,
|
||||
TabRounding: 2f,
|
||||
ScrollbarRounding: 2f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den
|
||||
// blauen Channels (Party/FC). Channel-Identität bleibt klar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80C0E8"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFB870"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4DD9E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80C0E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FFC080"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFE066"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80C0E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,72 +12,76 @@ internal static class HellionSpectrum
|
||||
{
|
||||
public const string Slug = "hellion-spectrum";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Hellion Spectrum",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Deuteran/Protan-safe channels — Wong palette tones, channel identity preserved.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#005983"),
|
||||
Primary: ColourUtil.HexToRgba("#0072B2"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#3E9BD0"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#0072B299"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#B07F00"),
|
||||
Accent: ColourUtil.HexToRgba("#E69F00"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F0B73A"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#005983"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#0A0F14"),
|
||||
ChildBg: ColourUtil.HexToRgba("#101620"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1A222E"),
|
||||
Surface: ColourUtil.HexToRgba("#22303F"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#2D3E51"),
|
||||
Border: ColourUtil.HexToRgba("#0072B266"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#F0F4F8"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9AA8B5"),
|
||||
TextDim: ColourUtil.HexToRgba("#5E6B78"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#009E73"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D55E00"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0E442"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#56B4E9")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Hellion Spectrum — Wong/Okabe-Ito tones within FFXIV channel
|
||||
// identity. FC pulled slightly greener than vanilla cyan-teal so
|
||||
// Party-blue and FC-green stay separable under deuteran sim.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0E442"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#D55E00"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#CC79A7"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#CC79A7"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#56B4E9"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#E69F00"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#009E73"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#94CC4A"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#56B4E9"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#94CC4A"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#E69F00"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0E442"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#66D9A8"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#56B4E9"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#8B7DD0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E0A0C0"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#DAA0DA"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#C9A56F"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#C9A56F"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Hellion Spectrum",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Deuteran/Protan-safe channels — Wong palette tones, channel identity preserved.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#005983"),
|
||||
Primary: ColourUtil.HexToRgba("#0072B2"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#3E9BD0"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#0072B299"),
|
||||
AccentDark: ColourUtil.HexToRgba("#B07F00"),
|
||||
Accent: ColourUtil.HexToRgba("#E69F00"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F0B73A"),
|
||||
Identity: ColourUtil.HexToRgba("#005983"),
|
||||
WindowBg: ColourUtil.HexToRgba("#0A0F14"),
|
||||
ChildBg: ColourUtil.HexToRgba("#101620"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1A222E"),
|
||||
Surface: ColourUtil.HexToRgba("#22303F"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#2D3E51"),
|
||||
Border: ColourUtil.HexToRgba("#0072B266"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#F0F4F8"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9AA8B5"),
|
||||
TextDim: ColourUtil.HexToRgba("#5E6B78"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#009E73"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D55E00"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#F0E442"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#56B4E9")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f,
|
||||
ChildRounding: 5f,
|
||||
PopupRounding: 5f,
|
||||
FrameRounding: 4f,
|
||||
GrabRounding: 4f,
|
||||
TabRounding: 4f,
|
||||
ScrollbarRounding: 4f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Hellion Spectrum — Wong/Okabe-Ito tones within FFXIV channel
|
||||
// identity. FC pulled slightly greener than vanilla cyan-teal so
|
||||
// Party-blue and FC-green stay separable under deuteran sim.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0E442"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#D55E00"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#CC79A7"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#CC79A7"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#56B4E9"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#E69F00"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#009E73"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#94CC4A"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#56B4E9"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#94CC4A"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#E69F00"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0E442"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#66D9A8"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#56B4E9"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#8B7DD0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E0A0C0"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#DAA0DA"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#C9A56F"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#C9A56F"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,72 +6,76 @@ internal static class IndigoViolet
|
||||
{
|
||||
public const string Slug = "indigo-violet";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Indigo Violet",
|
||||
Author: "Julia Moon",
|
||||
Description: "Royal Violet auf Deep Indigo — Glitter-Galaxy mit Türkis-Mint-Aurora.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#6B3AB0"),
|
||||
Primary: ColourUtil.HexToRgba("#8B4DDE"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#B07CFF"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#8B4DDE99"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#36A89C"),
|
||||
Accent: ColourUtil.HexToRgba("#4FC9B8"),
|
||||
AccentLight: ColourUtil.HexToRgba("#7AE0CF"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#6B3AB0"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#0D061F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#1A0D3D"),
|
||||
FrameBg: ColourUtil.HexToRgba("#2A1556"),
|
||||
Surface: ColourUtil.HexToRgba("#3D1F78"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#5B2A9A"),
|
||||
Border: ColourUtil.HexToRgba("#8B4DDE66"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#F0E6FF"),
|
||||
TextMuted: ColourUtil.HexToRgba("#A890D0"),
|
||||
TextDim: ColourUtil.HexToRgba("#7560A0"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#3DDC97"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#FF5C7A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FFB84A"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#8B4DDE")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Indigo Violet — Lavender-Pink-Drift in Tell und LS6/7. Türkis-
|
||||
// Mint-Aurora-Counter in Party/FC und LS4. Glitter-Gold in Yell.
|
||||
// Differenzierung zu Event Horizon: dunkler, dichter, Türkis statt Gold.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0E6FF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D880"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#6AB8D0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0A878"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4FC9B8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#6AB8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D880"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0C0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#6AB8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B07CFF"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#C098D8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A890D0"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Indigo Violet",
|
||||
Author: "Julia Moon",
|
||||
Description: "Royal Violet auf Deep Indigo — Glitter-Galaxy mit Türkis-Mint-Aurora.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#6B3AB0"),
|
||||
Primary: ColourUtil.HexToRgba("#8B4DDE"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#B07CFF"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#8B4DDE99"),
|
||||
AccentDark: ColourUtil.HexToRgba("#36A89C"),
|
||||
Accent: ColourUtil.HexToRgba("#4FC9B8"),
|
||||
AccentLight: ColourUtil.HexToRgba("#7AE0CF"),
|
||||
Identity: ColourUtil.HexToRgba("#6B3AB0"),
|
||||
WindowBg: ColourUtil.HexToRgba("#0D061F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#1A0D3D"),
|
||||
FrameBg: ColourUtil.HexToRgba("#2A1556"),
|
||||
Surface: ColourUtil.HexToRgba("#3D1F78"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#5B2A9A"),
|
||||
Border: ColourUtil.HexToRgba("#8B4DDE66"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#F0E6FF"),
|
||||
TextMuted: ColourUtil.HexToRgba("#A890D0"),
|
||||
TextDim: ColourUtil.HexToRgba("#7560A0"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#3DDC97"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#FF5C7A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FFB84A"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#8B4DDE")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f,
|
||||
ChildRounding: 5f,
|
||||
PopupRounding: 5f,
|
||||
FrameRounding: 4f,
|
||||
GrabRounding: 4f,
|
||||
TabRounding: 4f,
|
||||
ScrollbarRounding: 4f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Indigo Violet — Lavender-Pink-Drift in Tell und LS6/7. Türkis-
|
||||
// Mint-Aurora-Counter in Party/FC und LS4. Glitter-Gold in Yell.
|
||||
// Differenzierung zu Event Horizon: dunkler, dichter, Türkis statt Gold.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0E6FF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D880"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#6AB8D0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0A878"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4FC9B8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#6AB8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D880"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0C0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#6AB8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B07CFF"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#C098D8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A890D0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,72 +6,76 @@ internal static class MintGrove
|
||||
{
|
||||
public const string Slug = "mint-grove";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Mint Grove",
|
||||
Author: "Carla Beleandis",
|
||||
Description: "Mint Green + Honey Amber auf Deep Forest. Naturthemen-tauglich.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#3CB371"),
|
||||
Primary: ColourUtil.HexToRgba("#5DD39E"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#8FE0B8"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#5DD39E99"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#F4C870"),
|
||||
Accent: ColourUtil.HexToRgba("#F9D580"),
|
||||
AccentLight: ColourUtil.HexToRgba("#FCDD93"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#5DD39E"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#0A1410"),
|
||||
ChildBg: ColourUtil.HexToRgba("#10201A"),
|
||||
FrameBg: ColourUtil.HexToRgba("#162B22"),
|
||||
Surface: ColourUtil.HexToRgba("#1E372B"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#284335"),
|
||||
Border: ColourUtil.HexToRgba("#5DD39E55"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#E8F5EA"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9BB5A5"),
|
||||
TextDim: ColourUtil.HexToRgba("#5C6F65"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5DD39E"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#5DA9C7")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 5f, ChildRounding: 4f, PopupRounding: 4f,
|
||||
FrameRounding: 3f, GrabRounding: 3f, TabRounding: 3f,
|
||||
ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie,
|
||||
// Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität
|
||||
// bleibt erhalten für Erkennbarkeit.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F098C8"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F098C8"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80B8D0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B070"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#80C8B0"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80B8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC80"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F9D580"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0A0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80B8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A89DC0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#F098C8"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A8C8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9BB5A5"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Mint Grove",
|
||||
Author: "Carla Beleandis",
|
||||
Description: "Mint Green + Honey Amber auf Deep Forest. Naturthemen-tauglich.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#3CB371"),
|
||||
Primary: ColourUtil.HexToRgba("#5DD39E"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#8FE0B8"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#5DD39E99"),
|
||||
AccentDark: ColourUtil.HexToRgba("#F4C870"),
|
||||
Accent: ColourUtil.HexToRgba("#F9D580"),
|
||||
AccentLight: ColourUtil.HexToRgba("#FCDD93"),
|
||||
Identity: ColourUtil.HexToRgba("#5DD39E"),
|
||||
WindowBg: ColourUtil.HexToRgba("#0A1410"),
|
||||
ChildBg: ColourUtil.HexToRgba("#10201A"),
|
||||
FrameBg: ColourUtil.HexToRgba("#162B22"),
|
||||
Surface: ColourUtil.HexToRgba("#1E372B"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#284335"),
|
||||
Border: ColourUtil.HexToRgba("#5DD39E55"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#E8F5EA"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9BB5A5"),
|
||||
TextDim: ColourUtil.HexToRgba("#5C6F65"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#5DD39E"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#5DA9C7")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 5f,
|
||||
ChildRounding: 4f,
|
||||
PopupRounding: 4f,
|
||||
FrameRounding: 3f,
|
||||
GrabRounding: 3f,
|
||||
TabRounding: 3f,
|
||||
ScrollbarRounding: 3f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie,
|
||||
// Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität
|
||||
// bleibt erhalten für Erkennbarkeit.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F098C8"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F098C8"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80B8D0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B070"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#80C8B0"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80B8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC80"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F9D580"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0A0"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80B8D0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A89DC0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#F098C8"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A8C8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9BB5A5"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,71 +6,75 @@ internal static class MoonlitBloom
|
||||
{
|
||||
public const string Slug = "moonlit-bloom";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Moonlit Bloom",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
|
||||
Primary: ColourUtil.HexToRgba("#E374E8"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
Accent: ColourUtil.HexToRgba("#9CCB7C"),
|
||||
AccentLight: ColourUtil.HexToRgba("#B6E297"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#E374E8"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#15122B"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
|
||||
Surface: ColourUtil.HexToRgba("#28224A"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
|
||||
Border: ColourUtil.HexToRgba("#E374E844"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
|
||||
TextDim: ColourUtil.HexToRgba("#554B6E"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#6278FF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
|
||||
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Moonlit Bloom",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
|
||||
Primary: ColourUtil.HexToRgba("#E374E8"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
|
||||
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
Accent: ColourUtil.HexToRgba("#9CCB7C"),
|
||||
AccentLight: ColourUtil.HexToRgba("#B6E297"),
|
||||
Identity: ColourUtil.HexToRgba("#E374E8"),
|
||||
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#15122B"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
|
||||
Surface: ColourUtil.HexToRgba("#28224A"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
|
||||
Border: ColourUtil.HexToRgba("#E374E844"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
|
||||
TextDim: ColourUtil.HexToRgba("#554B6E"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#6278FF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f,
|
||||
ChildRounding: 5f,
|
||||
PopupRounding: 5f,
|
||||
FrameRounding: 4f,
|
||||
GrabRounding: 4f,
|
||||
TabRounding: 4f,
|
||||
ScrollbarRounding: 4f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
|
||||
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,71 +6,75 @@ internal static class NightBlue
|
||||
{
|
||||
public const string Slug = "night-blue";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Night Blue",
|
||||
Author: "Julia Moon",
|
||||
Description: "Royal Blue auf Marineblau — kühles Tech-Dashboard-Mood.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#3576C0"),
|
||||
Primary: ColourUtil.HexToRgba("#4A90E2"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#6AB0FF"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#4A90E299"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#C97A2E"),
|
||||
Accent: ColourUtil.HexToRgba("#E8A040"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F4B968"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#3576C0"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#050B18"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0A1628"),
|
||||
FrameBg: ColourUtil.HexToRgba("#122039"),
|
||||
Surface: ColourUtil.HexToRgba("#1A2D4F"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#234070"),
|
||||
Border: ColourUtil.HexToRgba("#4A90E266"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6EDF7"),
|
||||
TextMuted: ColourUtil.HexToRgba("#8CA0BF"),
|
||||
TextDim: ColourUtil.HexToRgba("#5A6F8F"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#3DDC97"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#FF5C7A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FFB84A"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#4A90E2")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Night Blue — Royal-Blue-Tinte in Party/FC, Bronze-Gold in Yell/
|
||||
// Alliance. Channel-identity (Tell-Pink, NN-Lime) bleibt erhalten.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFD060"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#6AA8E8"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4FA8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#6AA8E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFD060"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#6AA8E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#8CA0BF"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Night Blue",
|
||||
Author: "Julia Moon",
|
||||
Description: "Royal Blue auf Marineblau — kühles Tech-Dashboard-Mood.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#3576C0"),
|
||||
Primary: ColourUtil.HexToRgba("#4A90E2"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#6AB0FF"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#4A90E299"),
|
||||
AccentDark: ColourUtil.HexToRgba("#C97A2E"),
|
||||
Accent: ColourUtil.HexToRgba("#E8A040"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F4B968"),
|
||||
Identity: ColourUtil.HexToRgba("#3576C0"),
|
||||
WindowBg: ColourUtil.HexToRgba("#050B18"),
|
||||
ChildBg: ColourUtil.HexToRgba("#0A1628"),
|
||||
FrameBg: ColourUtil.HexToRgba("#122039"),
|
||||
Surface: ColourUtil.HexToRgba("#1A2D4F"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#234070"),
|
||||
Border: ColourUtil.HexToRgba("#4A90E266"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#E6EDF7"),
|
||||
TextMuted: ColourUtil.HexToRgba("#8CA0BF"),
|
||||
TextDim: ColourUtil.HexToRgba("#5A6F8F"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#3DDC97"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#FF5C7A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FFB84A"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#4A90E2")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f,
|
||||
ChildRounding: 5f,
|
||||
PopupRounding: 5f,
|
||||
FrameRounding: 4f,
|
||||
GrabRounding: 4f,
|
||||
TabRounding: 4f,
|
||||
ScrollbarRounding: 4f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Night Blue — Royal-Blue-Tinte in Party/FC, Bronze-Gold in Yell/
|
||||
// Alliance. Channel-identity (Tell-Pink, NN-Lime) bleibt erhalten.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFD060"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#6AA8E8"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4FA8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#6AA8E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFD060"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#6AA8E8"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B070"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#8CA0BF"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,71 +6,75 @@ internal static class SynthwaveSunset
|
||||
{
|
||||
public const string Slug = "synthwave-sunset";
|
||||
|
||||
public static Theme Build() => new(
|
||||
Slug: Slug,
|
||||
Name: "Synthwave Sunset",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Hot Magenta + Cyan on midnight violet. 80s neon-grid vibes for late-night raids.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#C71585"),
|
||||
Primary: ColourUtil.HexToRgba("#FF2D95"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#FF6BB6"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#FF2D9599"),
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba("#0098B8"),
|
||||
Accent: ColourUtil.HexToRgba("#00F0FF"),
|
||||
AccentLight: ColourUtil.HexToRgba("#5CFFFE"),
|
||||
|
||||
Identity: ColourUtil.HexToRgba("#FF2D95"),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba("#13041F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#1E0A35"),
|
||||
FrameBg: ColourUtil.HexToRgba("#2A1247"),
|
||||
Surface: ColourUtil.HexToRgba("#3A1860"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#4A2475"),
|
||||
Border: ColourUtil.HexToRgba("#FF2D9566"),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba("#F0DFFF"),
|
||||
TextMuted: ColourUtil.HexToRgba("#A88BC4"),
|
||||
TextDim: ColourUtil.HexToRgba("#6F4D8E"),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba("#39FF14"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#FF3838"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FFD700"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#00F0FF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 5f, ChildRounding: 4f, PopupRounding: 4f,
|
||||
FrameRounding: 3f, GrabRounding: 3f, TabRounding: 3f,
|
||||
ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Synthwave Sunset — Magenta dominiert die warmen Channels (Yell/Shout/FC),
|
||||
// Cyan dominiert die kühlen (Tell/Party). Neon-Akzente für Status-nahe Channels.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0DFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FF2D95"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF6BB6"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#00F0FF"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FF8C00"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#FF2D95"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#39FF14"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#39FF14"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FF8C00"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFD700"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#00F0FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#FF6BB6"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#FF2D95"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#A88BC4"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#FF6BB6"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#A88BC4"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A88BC4"),
|
||||
})
|
||||
);
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Synthwave Sunset",
|
||||
Author: "Zoe Moon",
|
||||
Description: "Hot Magenta + Cyan on midnight violet. 80s neon-grid vibes for late-night raids.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#C71585"),
|
||||
Primary: ColourUtil.HexToRgba("#FF2D95"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#FF6BB6"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#FF2D9599"),
|
||||
AccentDark: ColourUtil.HexToRgba("#0098B8"),
|
||||
Accent: ColourUtil.HexToRgba("#00F0FF"),
|
||||
AccentLight: ColourUtil.HexToRgba("#5CFFFE"),
|
||||
Identity: ColourUtil.HexToRgba("#FF2D95"),
|
||||
WindowBg: ColourUtil.HexToRgba("#13041F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#1E0A35"),
|
||||
FrameBg: ColourUtil.HexToRgba("#2A1247"),
|
||||
Surface: ColourUtil.HexToRgba("#3A1860"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#4A2475"),
|
||||
Border: ColourUtil.HexToRgba("#FF2D9566"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#F0DFFF"),
|
||||
TextMuted: ColourUtil.HexToRgba("#A88BC4"),
|
||||
TextDim: ColourUtil.HexToRgba("#6F4D8E"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#39FF14"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#FF3838"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FFD700"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#00F0FF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 5f,
|
||||
ChildRounding: 4f,
|
||||
PopupRounding: 4f,
|
||||
FrameRounding: 3f,
|
||||
GrabRounding: 3f,
|
||||
TabRounding: 3f,
|
||||
ScrollbarRounding: 3f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Synthwave Sunset — Magenta dominiert die warmen Channels (Yell/Shout/FC),
|
||||
// Cyan dominiert die kühlen (Tell/Party). Neon-Akzente für Status-nahe Channels.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F0DFFF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FF2D95"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF6BB6"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#00F0FF"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FF8C00"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#FF2D95"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#39FF14"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#39FF14"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FF8C00"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFD700"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#00F0FF"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#FF6BB6"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#FF2D95"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#A88BC4"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#5CFFFE"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#FF6BB6"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#A88BC4"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A88BC4"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "example-custom",
|
||||
"name": "Example Custom",
|
||||
"author": "You",
|
||||
"description": "Starting template — duplicate, rename, edit colors and reload.",
|
||||
"colors": {
|
||||
"primaryDark": "#0097A7",
|
||||
"primary": "#00BED2",
|
||||
"primaryLight": "#4DD9E8",
|
||||
"primaryGlow": "#00BED299",
|
||||
"accentDark": "#E85D04",
|
||||
"accent": "#F97316",
|
||||
"accentLight": "#FB923C",
|
||||
"identity": "#0097A7",
|
||||
"windowBg": "#070B12",
|
||||
"childBg": "#0C1220",
|
||||
"frameBg": "#141E30",
|
||||
"surface": "#1A2538",
|
||||
"surfaceHover": "#22303F",
|
||||
"border": "#00BED266",
|
||||
"textPrimary": "#E6F4F1",
|
||||
"textMuted": "#8FA3B5",
|
||||
"textDim": "#566273",
|
||||
"statusSuccess": "#5CB85C",
|
||||
"statusDanger": "#D9534F",
|
||||
"statusWarning": "#F0AD4E",
|
||||
"statusInfo": "#00BED2"
|
||||
},
|
||||
"layout": {
|
||||
"windowRounding": 4,
|
||||
"childRounding": 3,
|
||||
"popupRounding": 3,
|
||||
"frameRounding": 2,
|
||||
"grabRounding": 2,
|
||||
"tabRounding": 2,
|
||||
"scrollbarRounding": 2,
|
||||
"windowBorderSize": 1,
|
||||
"frameBorderSize": 1
|
||||
}
|
||||
"schemaVersion": 1,
|
||||
"slug": "example-custom",
|
||||
"name": "Example Custom",
|
||||
"author": "You",
|
||||
"description": "Starting template — duplicate, rename, edit colors and reload.",
|
||||
"colors": {
|
||||
"primaryDark": "#0097A7",
|
||||
"primary": "#00BED2",
|
||||
"primaryLight": "#4DD9E8",
|
||||
"primaryGlow": "#00BED299",
|
||||
"accentDark": "#E85D04",
|
||||
"accent": "#F97316",
|
||||
"accentLight": "#FB923C",
|
||||
"identity": "#0097A7",
|
||||
"windowBg": "#070B12",
|
||||
"childBg": "#0C1220",
|
||||
"frameBg": "#141E30",
|
||||
"surface": "#1A2538",
|
||||
"surfaceHover": "#22303F",
|
||||
"border": "#00BED266",
|
||||
"textPrimary": "#E6F4F1",
|
||||
"textMuted": "#8FA3B5",
|
||||
"textDim": "#566273",
|
||||
"statusSuccess": "#5CB85C",
|
||||
"statusDanger": "#D9534F",
|
||||
"statusWarning": "#F0AD4E",
|
||||
"statusInfo": "#00BED2"
|
||||
},
|
||||
"layout": {
|
||||
"windowRounding": 4,
|
||||
"childRounding": 3,
|
||||
"popupRounding": 3,
|
||||
"frameRounding": 2,
|
||||
"grabRounding": 2,
|
||||
"tabRounding": 2,
|
||||
"scrollbarRounding": 2,
|
||||
"windowBorderSize": 1,
|
||||
"frameBorderSize": 1
|
||||
}
|
||||
}
|
||||
|
||||
+40
-25
@@ -21,38 +21,53 @@ public sealed record Theme(
|
||||
public void RecomputeAbgrCache()
|
||||
{
|
||||
AbgrCache = new ThemeAbgrCache(
|
||||
PrimaryDark: ColourUtil.RgbaToAbgr(Colors.PrimaryDark),
|
||||
Primary: ColourUtil.RgbaToAbgr(Colors.Primary),
|
||||
PrimaryLight: ColourUtil.RgbaToAbgr(Colors.PrimaryLight),
|
||||
PrimaryGlow: ColourUtil.RgbaToAbgr(Colors.PrimaryGlow),
|
||||
AccentDark: ColourUtil.RgbaToAbgr(Colors.AccentDark),
|
||||
Accent: ColourUtil.RgbaToAbgr(Colors.Accent),
|
||||
AccentLight: ColourUtil.RgbaToAbgr(Colors.AccentLight),
|
||||
Identity: ColourUtil.RgbaToAbgr(Colors.Identity),
|
||||
WindowBg: ColourUtil.RgbaToAbgr(Colors.WindowBg),
|
||||
ChildBg: ColourUtil.RgbaToAbgr(Colors.ChildBg),
|
||||
FrameBg: ColourUtil.RgbaToAbgr(Colors.FrameBg),
|
||||
Surface: ColourUtil.RgbaToAbgr(Colors.Surface),
|
||||
SurfaceHover: ColourUtil.RgbaToAbgr(Colors.SurfaceHover),
|
||||
Border: ColourUtil.RgbaToAbgr(Colors.Border),
|
||||
TextPrimary: ColourUtil.RgbaToAbgr(Colors.TextPrimary),
|
||||
TextMuted: ColourUtil.RgbaToAbgr(Colors.TextMuted),
|
||||
TextDim: ColourUtil.RgbaToAbgr(Colors.TextDim),
|
||||
PrimaryDark: ColourUtil.RgbaToAbgr(Colors.PrimaryDark),
|
||||
Primary: ColourUtil.RgbaToAbgr(Colors.Primary),
|
||||
PrimaryLight: ColourUtil.RgbaToAbgr(Colors.PrimaryLight),
|
||||
PrimaryGlow: ColourUtil.RgbaToAbgr(Colors.PrimaryGlow),
|
||||
AccentDark: ColourUtil.RgbaToAbgr(Colors.AccentDark),
|
||||
Accent: ColourUtil.RgbaToAbgr(Colors.Accent),
|
||||
AccentLight: ColourUtil.RgbaToAbgr(Colors.AccentLight),
|
||||
Identity: ColourUtil.RgbaToAbgr(Colors.Identity),
|
||||
WindowBg: ColourUtil.RgbaToAbgr(Colors.WindowBg),
|
||||
ChildBg: ColourUtil.RgbaToAbgr(Colors.ChildBg),
|
||||
FrameBg: ColourUtil.RgbaToAbgr(Colors.FrameBg),
|
||||
Surface: ColourUtil.RgbaToAbgr(Colors.Surface),
|
||||
SurfaceHover: ColourUtil.RgbaToAbgr(Colors.SurfaceHover),
|
||||
Border: ColourUtil.RgbaToAbgr(Colors.Border),
|
||||
TextPrimary: ColourUtil.RgbaToAbgr(Colors.TextPrimary),
|
||||
TextMuted: ColourUtil.RgbaToAbgr(Colors.TextMuted),
|
||||
TextDim: ColourUtil.RgbaToAbgr(Colors.TextDim),
|
||||
StatusSuccess: ColourUtil.RgbaToAbgr(Colors.StatusSuccess),
|
||||
StatusDanger: ColourUtil.RgbaToAbgr(Colors.StatusDanger),
|
||||
StatusDanger: ColourUtil.RgbaToAbgr(Colors.StatusDanger),
|
||||
StatusWarning: ColourUtil.RgbaToAbgr(Colors.StatusWarning),
|
||||
StatusInfo: ColourUtil.RgbaToAbgr(Colors.StatusInfo));
|
||||
StatusInfo: ColourUtil.RgbaToAbgr(Colors.StatusInfo)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors ThemeColors slot-for-slot. The FillsAll21Slots test pins the
|
||||
// contract — a new slot without its mirror fails the build.
|
||||
public readonly record struct ThemeAbgrCache(
|
||||
uint PrimaryDark, uint Primary, uint PrimaryLight, uint PrimaryGlow,
|
||||
uint AccentDark, uint Accent, uint AccentLight,
|
||||
uint PrimaryDark,
|
||||
uint Primary,
|
||||
uint PrimaryLight,
|
||||
uint PrimaryGlow,
|
||||
uint AccentDark,
|
||||
uint Accent,
|
||||
uint AccentLight,
|
||||
uint Identity,
|
||||
uint WindowBg, uint ChildBg, uint FrameBg,
|
||||
uint Surface, uint SurfaceHover, uint Border,
|
||||
uint TextPrimary, uint TextMuted, uint TextDim,
|
||||
uint StatusSuccess, uint StatusDanger, uint StatusWarning, uint StatusInfo
|
||||
uint WindowBg,
|
||||
uint ChildBg,
|
||||
uint FrameBg,
|
||||
uint Surface,
|
||||
uint SurfaceHover,
|
||||
uint Border,
|
||||
uint TextPrimary,
|
||||
uint TextMuted,
|
||||
uint TextDim,
|
||||
uint StatusSuccess,
|
||||
uint StatusDanger,
|
||||
uint StatusWarning,
|
||||
uint StatusInfo
|
||||
);
|
||||
|
||||
@@ -2,10 +2,6 @@ using HellionChat.Code;
|
||||
|
||||
namespace HellionChat.Themes;
|
||||
|
||||
// Optional pro Theme. Wenn ein Theme ChatColors mitliefert, kann der
|
||||
// User sie per Klick im Themes-Tab auf Configuration.ChatColours anwenden.
|
||||
// Ein Theme ohne ChatColors (z.B. chat2-classic) lässt die User-Channel-
|
||||
// Farben unverändert.
|
||||
public sealed record ThemeChatColors(
|
||||
IReadOnlyDictionary<ChatType, uint> Channels
|
||||
);
|
||||
// Optional per-theme chat colours applied to Configuration.ChatColours on user request.
|
||||
// Themes without this leave channel colours untouched.
|
||||
public sealed record ThemeChatColors(IReadOnlyDictionary<ChatType, uint> Channels);
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
namespace HellionChat.Themes;
|
||||
|
||||
// Color-Werte als 0xRRGGBBAA, RgbaToAbgr handled den Byte-Swap zu ImGui.
|
||||
// Colour values as 0xRRGGBBAA — RgbaToAbgr handles the byte-swap for ImGui.
|
||||
public sealed record ThemeColors(
|
||||
uint PrimaryDark,
|
||||
uint Primary,
|
||||
uint PrimaryLight,
|
||||
uint PrimaryGlow,
|
||||
|
||||
uint AccentDark,
|
||||
uint Accent,
|
||||
uint AccentLight,
|
||||
|
||||
uint Identity,
|
||||
|
||||
uint WindowBg,
|
||||
uint ChildBg,
|
||||
uint FrameBg,
|
||||
uint Surface,
|
||||
uint SurfaceHover,
|
||||
uint Border,
|
||||
|
||||
uint TextPrimary,
|
||||
uint TextMuted,
|
||||
uint TextDim,
|
||||
|
||||
uint StatusSuccess,
|
||||
uint StatusDanger,
|
||||
uint StatusWarning,
|
||||
|
||||
@@ -13,8 +13,14 @@ internal static class ThemeJsonLoader
|
||||
throw new FormatException("Theme JSON is empty");
|
||||
|
||||
JsonDocument doc;
|
||||
try { doc = JsonDocument.Parse(json); }
|
||||
catch (JsonException ex) { throw new FormatException("Theme JSON is not valid JSON", ex); }
|
||||
try
|
||||
{
|
||||
doc = JsonDocument.Parse(json);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new FormatException("Theme JSON is not valid JSON", ex);
|
||||
}
|
||||
|
||||
using (doc)
|
||||
{
|
||||
@@ -22,21 +28,36 @@ internal static class ThemeJsonLoader
|
||||
|
||||
var schemaVersion = ReadInt(root, "schemaVersion");
|
||||
if (schemaVersion != SupportedSchemaVersion)
|
||||
throw new FormatException($"Unsupported schemaVersion {schemaVersion}; expected {SupportedSchemaVersion}");
|
||||
throw new FormatException(
|
||||
$"Unsupported schemaVersion {schemaVersion}; expected {SupportedSchemaVersion}"
|
||||
);
|
||||
|
||||
var slug = ReadString(root, "slug");
|
||||
var name = ReadString(root, "name");
|
||||
var author = ReadString(root, "author");
|
||||
var slug = ReadString(root, "slug");
|
||||
var name = ReadString(root, "name");
|
||||
var author = ReadString(root, "author");
|
||||
var description = ReadString(root, "description");
|
||||
|
||||
var colors = ReadColors(root.GetProperty("colors"));
|
||||
var layout = ReadLayout(root.GetProperty("layout"));
|
||||
|
||||
ThemeChatColors? chatColors = null;
|
||||
if (root.TryGetProperty("chatChannels", out var ch) && ch.ValueKind == JsonValueKind.Object)
|
||||
if (
|
||||
root.TryGetProperty("chatChannels", out var ch)
|
||||
&& ch.ValueKind == JsonValueKind.Object
|
||||
)
|
||||
chatColors = ReadChatColors(ch);
|
||||
|
||||
return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false, ChatColors: chatColors);
|
||||
return new Theme(
|
||||
slug,
|
||||
name,
|
||||
author,
|
||||
description,
|
||||
colors,
|
||||
layout,
|
||||
new ThemeTypography(),
|
||||
IsBuiltIn: false,
|
||||
ChatColors: chatColors
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +66,15 @@ internal static class ThemeJsonLoader
|
||||
var dict = new Dictionary<HellionChat.Code.ChatType, uint>();
|
||||
foreach (var prop in el.EnumerateObject())
|
||||
{
|
||||
// Property-Name ist der ChatType-Name als String (z.B. "Say", "Tell"),
|
||||
// Value ist Hex wie bei den Theme-Colors. Unbekannte Channel-Names
|
||||
// werden still übersprungen — Forward-Compat falls SE neue Channels
|
||||
// einführt.
|
||||
if (!Enum.TryParse<HellionChat.Code.ChatType>(prop.Name, ignoreCase: true, out var channel))
|
||||
// Property name is the ChatType name (e.g. "Say", "Tell"), value is hex like theme colours.
|
||||
// Unknown channel names are silently skipped for forward-compat with future SE channels.
|
||||
if (
|
||||
!Enum.TryParse<HellionChat.Code.ChatType>(
|
||||
prop.Name,
|
||||
ignoreCase: true,
|
||||
out var channel
|
||||
)
|
||||
)
|
||||
continue;
|
||||
if (prop.Value.ValueKind != JsonValueKind.String)
|
||||
continue;
|
||||
@@ -71,46 +96,43 @@ internal static class ThemeJsonLoader
|
||||
return LoadFromString(json);
|
||||
}
|
||||
|
||||
private static ThemeColors ReadColors(JsonElement el) => new(
|
||||
PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")),
|
||||
Primary: ColourUtil.HexToRgba(ReadString(el, "primary")),
|
||||
PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")),
|
||||
PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")),
|
||||
private static ThemeColors ReadColors(JsonElement el) =>
|
||||
new(
|
||||
PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")),
|
||||
Primary: ColourUtil.HexToRgba(ReadString(el, "primary")),
|
||||
PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")),
|
||||
PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")),
|
||||
AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")),
|
||||
Accent: ColourUtil.HexToRgba(ReadString(el, "accent")),
|
||||
AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")),
|
||||
Identity: ColourUtil.HexToRgba(ReadString(el, "identity")),
|
||||
WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")),
|
||||
ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")),
|
||||
FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")),
|
||||
Surface: ColourUtil.HexToRgba(ReadString(el, "surface")),
|
||||
SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")),
|
||||
Border: ColourUtil.HexToRgba(ReadString(el, "border")),
|
||||
TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")),
|
||||
TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")),
|
||||
TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")),
|
||||
StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")),
|
||||
StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")),
|
||||
StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")),
|
||||
StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo"))
|
||||
);
|
||||
|
||||
AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")),
|
||||
Accent: ColourUtil.HexToRgba(ReadString(el, "accent")),
|
||||
AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")),
|
||||
|
||||
Identity: ColourUtil.HexToRgba(ReadString(el, "identity")),
|
||||
|
||||
WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")),
|
||||
ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")),
|
||||
FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")),
|
||||
Surface: ColourUtil.HexToRgba(ReadString(el, "surface")),
|
||||
SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")),
|
||||
Border: ColourUtil.HexToRgba(ReadString(el, "border")),
|
||||
|
||||
TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")),
|
||||
TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")),
|
||||
TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")),
|
||||
|
||||
StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")),
|
||||
StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")),
|
||||
StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")),
|
||||
StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo"))
|
||||
);
|
||||
|
||||
private static ThemeLayout ReadLayout(JsonElement el) => new(
|
||||
WindowRounding: ReadFloat(el, "windowRounding"),
|
||||
ChildRounding: ReadFloat(el, "childRounding"),
|
||||
PopupRounding: ReadFloat(el, "popupRounding"),
|
||||
FrameRounding: ReadFloat(el, "frameRounding"),
|
||||
GrabRounding: ReadFloat(el, "grabRounding"),
|
||||
TabRounding: ReadFloat(el, "tabRounding"),
|
||||
ScrollbarRounding: ReadFloat(el, "scrollbarRounding"),
|
||||
WindowBorderSize: ReadFloat(el, "windowBorderSize"),
|
||||
FrameBorderSize: ReadFloat(el, "frameBorderSize")
|
||||
);
|
||||
private static ThemeLayout ReadLayout(JsonElement el) =>
|
||||
new(
|
||||
WindowRounding: ReadFloat(el, "windowRounding"),
|
||||
ChildRounding: ReadFloat(el, "childRounding"),
|
||||
PopupRounding: ReadFloat(el, "popupRounding"),
|
||||
FrameRounding: ReadFloat(el, "frameRounding"),
|
||||
GrabRounding: ReadFloat(el, "grabRounding"),
|
||||
TabRounding: ReadFloat(el, "tabRounding"),
|
||||
ScrollbarRounding: ReadFloat(el, "scrollbarRounding"),
|
||||
WindowBorderSize: ReadFloat(el, "windowBorderSize"),
|
||||
FrameBorderSize: ReadFloat(el, "frameBorderSize")
|
||||
);
|
||||
|
||||
private static string ReadString(JsonElement el, string name)
|
||||
{
|
||||
|
||||
@@ -17,39 +17,39 @@ internal static class ThemeJsonWriter
|
||||
writer.WriteString("description", theme.Description);
|
||||
|
||||
writer.WriteStartObject("colors");
|
||||
WriteColor(writer, "primaryDark", theme.Colors.PrimaryDark);
|
||||
WriteColor(writer, "primary", theme.Colors.Primary);
|
||||
WriteColor(writer, "primaryLight", theme.Colors.PrimaryLight);
|
||||
WriteColor(writer, "primaryGlow", theme.Colors.PrimaryGlow);
|
||||
WriteColor(writer, "accentDark", theme.Colors.AccentDark);
|
||||
WriteColor(writer, "accent", theme.Colors.Accent);
|
||||
WriteColor(writer, "accentLight", theme.Colors.AccentLight);
|
||||
WriteColor(writer, "identity", theme.Colors.Identity);
|
||||
WriteColor(writer, "windowBg", theme.Colors.WindowBg);
|
||||
WriteColor(writer, "childBg", theme.Colors.ChildBg);
|
||||
WriteColor(writer, "frameBg", theme.Colors.FrameBg);
|
||||
WriteColor(writer, "surface", theme.Colors.Surface);
|
||||
WriteColor(writer, "surfaceHover", theme.Colors.SurfaceHover);
|
||||
WriteColor(writer, "border", theme.Colors.Border);
|
||||
WriteColor(writer, "textPrimary", theme.Colors.TextPrimary);
|
||||
WriteColor(writer, "textMuted", theme.Colors.TextMuted);
|
||||
WriteColor(writer, "textDim", theme.Colors.TextDim);
|
||||
WriteColor(writer, "primaryDark", theme.Colors.PrimaryDark);
|
||||
WriteColor(writer, "primary", theme.Colors.Primary);
|
||||
WriteColor(writer, "primaryLight", theme.Colors.PrimaryLight);
|
||||
WriteColor(writer, "primaryGlow", theme.Colors.PrimaryGlow);
|
||||
WriteColor(writer, "accentDark", theme.Colors.AccentDark);
|
||||
WriteColor(writer, "accent", theme.Colors.Accent);
|
||||
WriteColor(writer, "accentLight", theme.Colors.AccentLight);
|
||||
WriteColor(writer, "identity", theme.Colors.Identity);
|
||||
WriteColor(writer, "windowBg", theme.Colors.WindowBg);
|
||||
WriteColor(writer, "childBg", theme.Colors.ChildBg);
|
||||
WriteColor(writer, "frameBg", theme.Colors.FrameBg);
|
||||
WriteColor(writer, "surface", theme.Colors.Surface);
|
||||
WriteColor(writer, "surfaceHover", theme.Colors.SurfaceHover);
|
||||
WriteColor(writer, "border", theme.Colors.Border);
|
||||
WriteColor(writer, "textPrimary", theme.Colors.TextPrimary);
|
||||
WriteColor(writer, "textMuted", theme.Colors.TextMuted);
|
||||
WriteColor(writer, "textDim", theme.Colors.TextDim);
|
||||
WriteColor(writer, "statusSuccess", theme.Colors.StatusSuccess);
|
||||
WriteColor(writer, "statusDanger", theme.Colors.StatusDanger);
|
||||
WriteColor(writer, "statusDanger", theme.Colors.StatusDanger);
|
||||
WriteColor(writer, "statusWarning", theme.Colors.StatusWarning);
|
||||
WriteColor(writer, "statusInfo", theme.Colors.StatusInfo);
|
||||
WriteColor(writer, "statusInfo", theme.Colors.StatusInfo);
|
||||
writer.WriteEndObject();
|
||||
|
||||
writer.WriteStartObject("layout");
|
||||
writer.WriteNumber("windowRounding", theme.Layout.WindowRounding);
|
||||
writer.WriteNumber("childRounding", theme.Layout.ChildRounding);
|
||||
writer.WriteNumber("popupRounding", theme.Layout.PopupRounding);
|
||||
writer.WriteNumber("frameRounding", theme.Layout.FrameRounding);
|
||||
writer.WriteNumber("grabRounding", theme.Layout.GrabRounding);
|
||||
writer.WriteNumber("tabRounding", theme.Layout.TabRounding);
|
||||
writer.WriteNumber("windowRounding", theme.Layout.WindowRounding);
|
||||
writer.WriteNumber("childRounding", theme.Layout.ChildRounding);
|
||||
writer.WriteNumber("popupRounding", theme.Layout.PopupRounding);
|
||||
writer.WriteNumber("frameRounding", theme.Layout.FrameRounding);
|
||||
writer.WriteNumber("grabRounding", theme.Layout.GrabRounding);
|
||||
writer.WriteNumber("tabRounding", theme.Layout.TabRounding);
|
||||
writer.WriteNumber("scrollbarRounding", theme.Layout.ScrollbarRounding);
|
||||
writer.WriteNumber("windowBorderSize", theme.Layout.WindowBorderSize);
|
||||
writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize);
|
||||
writer.WriteNumber("windowBorderSize", theme.Layout.WindowBorderSize);
|
||||
writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize);
|
||||
writer.WriteEndObject();
|
||||
|
||||
if (theme.ChatColors is { Channels.Count: > 0 } cc)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace HellionChat.Themes;
|
||||
|
||||
// Layout-Werte spiegeln die ImGuiStyleVar-Slots, die HellionStyle pusht.
|
||||
// Layout values mirror the ImGuiStyleVar slots pushed by HellionStyle.
|
||||
public sealed record ThemeLayout(
|
||||
float WindowRounding,
|
||||
float ChildRounding,
|
||||
|
||||
@@ -7,7 +7,9 @@ public sealed class ThemeRegistry
|
||||
public const string DefaultSlug = HellionArctic.Slug;
|
||||
|
||||
private readonly Dictionary<string, Theme> _builtIns;
|
||||
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(
|
||||
StringComparer.OrdinalIgnoreCase
|
||||
);
|
||||
private readonly string? _customThemesDir;
|
||||
private Theme _active;
|
||||
|
||||
@@ -15,19 +17,19 @@ public sealed class ThemeRegistry
|
||||
{
|
||||
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
||||
{ NightBlue.Slug, NightBlue.Build() },
|
||||
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||
{ MintGrove.Slug, MintGrove.Build() },
|
||||
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
||||
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
||||
{ NightBlue.Slug, NightBlue.Build() },
|
||||
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||
{ MintGrove.Slug, MintGrove.Build() },
|
||||
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
||||
};
|
||||
|
||||
// Centralised so the ten .Build() factories stay free of cache plumbing.
|
||||
// Centralised so Build() factories stay free of cache plumbing.
|
||||
foreach (var theme in _builtIns.Values)
|
||||
theme.RecomputeAbgrCache();
|
||||
|
||||
@@ -39,10 +41,12 @@ public sealed class ThemeRegistry
|
||||
|
||||
public Theme Get(string slug)
|
||||
{
|
||||
if (_builtIns.TryGetValue(slug, out var b)) return b;
|
||||
if (_builtIns.TryGetValue(slug, out var b))
|
||||
return b;
|
||||
|
||||
var custom = LoadCustomBySlug(slug);
|
||||
if (custom != null) return custom;
|
||||
if (custom != null)
|
||||
return custom;
|
||||
|
||||
return _builtIns[DefaultSlug];
|
||||
}
|
||||
@@ -54,14 +58,13 @@ public sealed class ThemeRegistry
|
||||
public void Switch(string slug)
|
||||
{
|
||||
var theme = Get(slug);
|
||||
// Defensive — idempotent and cheap, so any future theme source
|
||||
// that forgets the cache fill still ends up with a populated one.
|
||||
// Defensive — ensures any future theme source always gets a populated cache.
|
||||
theme.RecomputeAbgrCache();
|
||||
_active = theme;
|
||||
}
|
||||
|
||||
// 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION. Other
|
||||
// IO failures are permanent and get the theme dropped instead of retried.
|
||||
// 0x80070020 = SHARING_VIOLATION, 0x80070021 = LOCK_VIOLATION.
|
||||
// Other IO failures are permanent — theme is dropped instead of retried.
|
||||
internal static bool IsRecoverableFileLock(Exception? ex)
|
||||
{
|
||||
if (ex is not IOException io)
|
||||
@@ -70,13 +73,14 @@ public sealed class ThemeRegistry
|
||||
return code == 0x80070020u || code == 0x80070021u;
|
||||
}
|
||||
|
||||
// Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit
|
||||
// LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup
|
||||
// neu eingelesen.
|
||||
// Custom themes are loaded lazily, cached by LastWriteTime.
|
||||
// A changed JSON is reloaded on the next lookup.
|
||||
private Theme? LoadCustomBySlug(string slug)
|
||||
{
|
||||
if (_customThemesDir is null) return null;
|
||||
if (!Directory.Exists(_customThemesDir)) return null;
|
||||
if (_customThemesDir is null)
|
||||
return null;
|
||||
if (!Directory.Exists(_customThemesDir))
|
||||
return null;
|
||||
|
||||
foreach (var theme in RefreshCustomCache())
|
||||
if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -109,9 +113,10 @@ public sealed class ThemeRegistry
|
||||
}
|
||||
catch (Exception ex) when (IsRecoverableFileLock(ex))
|
||||
{
|
||||
// Editor mid-save: keep the cached snapshot, leave the stamp
|
||||
// alone so the next refresh retries automatically.
|
||||
Plugin.Log.Debug($"Custom theme {Path.GetFileName(path)} is locked, keeping last known good");
|
||||
// Editor mid-save: keep last known good, retry on next refresh.
|
||||
Plugin.Log.Debug(
|
||||
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
|
||||
);
|
||||
if (cached.Theme is not null)
|
||||
theme = cached.Theme;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace HellionChat.Themes;
|
||||
|
||||
// Optional pro Theme. v1.1.0 nutzt das nicht aktiv; ist als Erweiterungspunkt
|
||||
// für zukünftige Theme-Slots vorbereitet.
|
||||
// Optional per-theme; reserved as an extension point for future theme slots.
|
||||
public sealed record ThemeTypography(
|
||||
float? OverrideGlobalFontSizePt = null,
|
||||
float? OverrideSymbolsFontSizePt = null
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -36,9 +36,7 @@ public sealed class ChatInputBar
|
||||
public bool IsFocused { get; private set; }
|
||||
|
||||
// Stub. v0.6.0 belässt den Hauptfenster-Input wie er ist.
|
||||
public void Render()
|
||||
{
|
||||
}
|
||||
public void Render() { }
|
||||
|
||||
// Compact rendering for pop-out windows.
|
||||
//
|
||||
@@ -79,12 +77,20 @@ public sealed class ChatInputBar
|
||||
// window does it: via IsItemDeactivated + Enter, NOT EnterReturnsTrue
|
||||
// (matching v0.5.x ChatLogWindow.cs behavior).
|
||||
const ImGuiInputTextFlags flags = ImGuiInputTextFlags.CallbackHistory;
|
||||
ImGui.InputText($"##chat-compact-input-{tab.Identifier}", ref _state.Buffer, 500, flags, CompactCallback);
|
||||
ImGui.InputText(
|
||||
$"##chat-compact-input-{tab.Identifier}",
|
||||
ref _state.Buffer,
|
||||
500,
|
||||
flags,
|
||||
CompactCallback
|
||||
);
|
||||
|
||||
IsFocused = ImGui.IsItemActive();
|
||||
|
||||
if (ImGui.IsItemDeactivated()
|
||||
&& (ImGui.IsKeyDown(ImGuiKey.Enter) || ImGui.IsKeyDown(ImGuiKey.KeypadEnter)))
|
||||
if (
|
||||
ImGui.IsItemDeactivated()
|
||||
&& (ImGui.IsKeyDown(ImGuiKey.Enter) || ImGui.IsKeyDown(ImGuiKey.KeypadEnter))
|
||||
)
|
||||
{
|
||||
SubmitCompact(tab);
|
||||
}
|
||||
@@ -118,7 +124,8 @@ public sealed class ChatInputBar
|
||||
_state.Buffer,
|
||||
() => InputHistoryService.Count,
|
||||
InputHistoryService.Push,
|
||||
InputHistoryService.GetByCursor);
|
||||
InputHistoryService.GetByCursor
|
||||
);
|
||||
|
||||
_state.HistoryCursor = cursor;
|
||||
if (replacement is null)
|
||||
@@ -156,7 +163,10 @@ public sealed class ChatInputBar
|
||||
// Single-letter glyph derived from the channel — quick visual cue
|
||||
// until we have a proper icon font available in the compact bar.
|
||||
var label = ChannelGlyph(inputType);
|
||||
if (ImGui.Button($"{label}##chan-compact", new Vector2(buttonSize, buttonSize)) && tab.Channel is null)
|
||||
if (
|
||||
ImGui.Button($"{label}##chan-compact", new Vector2(buttonSize, buttonSize))
|
||||
&& tab.Channel is null
|
||||
)
|
||||
ImGui.OpenPopup(popupId);
|
||||
}
|
||||
|
||||
@@ -181,34 +191,35 @@ public sealed class ChatInputBar
|
||||
}
|
||||
}
|
||||
|
||||
private static string ChannelGlyph(ChatType type) => type switch
|
||||
{
|
||||
ChatType.Say => "S",
|
||||
ChatType.Yell => "Y",
|
||||
ChatType.Shout => "!",
|
||||
ChatType.TellIncoming or ChatType.TellOutgoing => "T",
|
||||
ChatType.Party or ChatType.CrossParty => "P",
|
||||
ChatType.Alliance => "A",
|
||||
ChatType.FreeCompany => "F",
|
||||
ChatType.NoviceNetwork => "N",
|
||||
ChatType.Linkshell1 => "1",
|
||||
ChatType.Linkshell2 => "2",
|
||||
ChatType.Linkshell3 => "3",
|
||||
ChatType.Linkshell4 => "4",
|
||||
ChatType.Linkshell5 => "5",
|
||||
ChatType.Linkshell6 => "6",
|
||||
ChatType.Linkshell7 => "7",
|
||||
ChatType.Linkshell8 => "8",
|
||||
ChatType.CrossLinkshell1 => "①",
|
||||
ChatType.CrossLinkshell2 => "②",
|
||||
ChatType.CrossLinkshell3 => "③",
|
||||
ChatType.CrossLinkshell4 => "④",
|
||||
ChatType.CrossLinkshell5 => "⑤",
|
||||
ChatType.CrossLinkshell6 => "⑥",
|
||||
ChatType.CrossLinkshell7 => "⑦",
|
||||
ChatType.CrossLinkshell8 => "⑧",
|
||||
_ => "?",
|
||||
};
|
||||
private static string ChannelGlyph(ChatType type) =>
|
||||
type switch
|
||||
{
|
||||
ChatType.Say => "S",
|
||||
ChatType.Yell => "Y",
|
||||
ChatType.Shout => "!",
|
||||
ChatType.TellIncoming or ChatType.TellOutgoing => "T",
|
||||
ChatType.Party or ChatType.CrossParty => "P",
|
||||
ChatType.Alliance => "A",
|
||||
ChatType.FreeCompany => "F",
|
||||
ChatType.NoviceNetwork => "N",
|
||||
ChatType.Linkshell1 => "1",
|
||||
ChatType.Linkshell2 => "2",
|
||||
ChatType.Linkshell3 => "3",
|
||||
ChatType.Linkshell4 => "4",
|
||||
ChatType.Linkshell5 => "5",
|
||||
ChatType.Linkshell6 => "6",
|
||||
ChatType.Linkshell7 => "7",
|
||||
ChatType.Linkshell8 => "8",
|
||||
ChatType.CrossLinkshell1 => "①",
|
||||
ChatType.CrossLinkshell2 => "②",
|
||||
ChatType.CrossLinkshell3 => "③",
|
||||
ChatType.CrossLinkshell4 => "④",
|
||||
ChatType.CrossLinkshell5 => "⑤",
|
||||
ChatType.CrossLinkshell6 => "⑥",
|
||||
ChatType.CrossLinkshell7 => "⑦",
|
||||
ChatType.CrossLinkshell8 => "⑧",
|
||||
_ => "?",
|
||||
};
|
||||
|
||||
// Forwards a tab-cycle keybind delta to the host so all windows
|
||||
// navigate the same active-tab pointer (single source of truth).
|
||||
|
||||
+469
-136
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,30 @@
|
||||
using System.Numerics;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Util;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
public class CommandHelpWindow : Window {
|
||||
public class CommandHelpWindow : Window
|
||||
{
|
||||
private ChatLogWindow LogWindow { get; }
|
||||
private ReadOnlySeString? CommandDescription { get; set; }
|
||||
|
||||
internal CommandHelpWindow(ChatLogWindow logWindow) : base("command help##chat2-commandhelp")
|
||||
internal CommandHelpWindow(ChatLogWindow logWindow)
|
||||
: base("command help##chat2-commandhelp")
|
||||
{
|
||||
LogWindow = logWindow;
|
||||
|
||||
Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize;
|
||||
Flags =
|
||||
ImGuiWindowFlags.NoSavedSettings
|
||||
| ImGuiWindowFlags.NoTitleBar
|
||||
| ImGuiWindowFlags.NoMove
|
||||
| ImGuiWindowFlags.NoResize
|
||||
| ImGuiWindowFlags.NoFocusOnAppearing
|
||||
| ImGuiWindowFlags.AlwaysAutoResize;
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
@@ -31,7 +38,8 @@ public class CommandHelpWindow : Window {
|
||||
var width = 350;
|
||||
var scaledWidth = width * ImGuiHelpers.GlobalScale;
|
||||
var pos = LogWindow.LastWindowPos;
|
||||
switch (Plugin.Config.CommandHelpSide) {
|
||||
switch (Plugin.Config.CommandHelpSide)
|
||||
{
|
||||
case CommandHelpSide.Right:
|
||||
pos.X += LogWindow.LastWindowSize.X;
|
||||
break;
|
||||
@@ -51,7 +59,7 @@ public class CommandHelpWindow : Window {
|
||||
// coordinate space as Position above; otherwise the help window
|
||||
// ends up the wrong width at non-100% DPI.
|
||||
MinimumSize = new Vector2(scaledWidth, 0),
|
||||
MaximumSize = LogWindow.LastWindowSize with { X = scaledWidth }
|
||||
MaximumSize = LogWindow.LastWindowSize with { X = scaledWidth },
|
||||
};
|
||||
|
||||
IsOpen = true;
|
||||
@@ -62,6 +70,10 @@ public class CommandHelpWindow : Window {
|
||||
if (CommandDescription == null)
|
||||
return;
|
||||
|
||||
LogWindow.DrawChunks(ChunkUtil.ToChunks(CommandDescription.Value.ToDalamudString(), ChunkSource.None, null).ToList());
|
||||
LogWindow.DrawChunks(
|
||||
ChunkUtil
|
||||
.ToChunks(CommandDescription.Value.ToDalamudString(), ChunkSource.None, null)
|
||||
.ToList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+192
-51
@@ -2,18 +2,18 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Lumina.Data.Files;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using MoreLinq;
|
||||
@@ -47,8 +47,8 @@ public class DbViewer : Window
|
||||
private readonly string DateTimeFormat;
|
||||
|
||||
private long Count;
|
||||
private Message[] Messages = []; // Messages are only touched while processing is false
|
||||
private ConcurrentStack<Message> Filtered = []; // Is used every frame, so ConcurrentStack for safety
|
||||
private Message[] Messages = []; // Messages are only touched while processing is false
|
||||
private ConcurrentStack<Message> Filtered = []; // Is used every frame, so ConcurrentStack for safety
|
||||
|
||||
private bool IsExporting;
|
||||
private string InputPath = string.Empty;
|
||||
@@ -56,7 +56,8 @@ public class DbViewer : Window
|
||||
|
||||
private bool NeedsScrollReset;
|
||||
|
||||
public DbViewer(Plugin plugin) : base("DBViewer###chat2-dbviewer")
|
||||
public DbViewer(Plugin plugin)
|
||||
: base("DBViewer###chat2-dbviewer")
|
||||
{
|
||||
Plugin = plugin;
|
||||
SelectedChannels = TabsUtil.MostlyPlayer;
|
||||
@@ -64,24 +65,42 @@ public class DbViewer : Window
|
||||
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
||||
DateTimeFormat = "ddd, dd MMM yyy HH:mm:ss";
|
||||
|
||||
LastProcessed = (AfterDate, BeforeDate, CurrentPage, OnlyCurrentCharacter, SelectedChannels.Count);
|
||||
LastProcessed = (
|
||||
AfterDate,
|
||||
BeforeDate,
|
||||
CurrentPage,
|
||||
OnlyCurrentCharacter,
|
||||
SelectedChannels.Count
|
||||
);
|
||||
DateReset();
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(475, 600),
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
||||
};
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
|
||||
Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute += Toggle;
|
||||
Plugin
|
||||
.Commands.Register(
|
||||
"/hellionView",
|
||||
"Get access to your message history, with simple filter options.",
|
||||
true
|
||||
)
|
||||
.Execute += Toggle;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Plugin.Commands.Register("/hellionView", "Get access to your message history, with simple filter options.", true).Execute -= Toggle;
|
||||
Plugin
|
||||
.Commands.Register(
|
||||
"/hellionView",
|
||||
"Get access to your message history, with simple filter options.",
|
||||
true
|
||||
)
|
||||
.Execute -= Toggle;
|
||||
}
|
||||
|
||||
private void Toggle(string _, string __) => Toggle();
|
||||
@@ -102,8 +121,21 @@ public class DbViewer : Window
|
||||
ImGui.SameLine();
|
||||
|
||||
var spacing = 3.0f * ImGuiHelpers.GlobalScale;
|
||||
DateWidget.DatePickerWithInput("##FromDate", 1, ref MinDateString, ref AfterDate, DateFormat);
|
||||
DateWidget.DatePickerWithInput("##ToDate", 2, ref MaxDateString, ref BeforeDate, DateFormat, true);
|
||||
DateWidget.DatePickerWithInput(
|
||||
"##FromDate",
|
||||
1,
|
||||
ref MinDateString,
|
||||
ref AfterDate,
|
||||
DateFormat
|
||||
);
|
||||
DateWidget.DatePickerWithInput(
|
||||
"##ToDate",
|
||||
2,
|
||||
ref MaxDateString,
|
||||
ref BeforeDate,
|
||||
DateFormat,
|
||||
true
|
||||
);
|
||||
|
||||
ImGui.SameLine(0, spacing);
|
||||
|
||||
@@ -118,7 +150,11 @@ public class DbViewer : Window
|
||||
ChannelSelection();
|
||||
|
||||
var skipText = Language.DbViewer_CharacterOption;
|
||||
var textLength = ImGui.GetTextLineHeight() + ImGui.CalcTextSize(skipText).X + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
var textLength =
|
||||
ImGui.GetTextLineHeight()
|
||||
+ ImGui.CalcTextSize(skipText).X
|
||||
+ ImGui.GetStyle().ItemInnerSpacing.X
|
||||
+ ImGui.GetStyle().FramePadding.X * 2;
|
||||
ImGui.SameLine(ImGui.GetContentRegionMax().X - textLength);
|
||||
ImGui.Checkbox(skipText, ref OnlyCurrentCharacter);
|
||||
|
||||
@@ -139,7 +175,16 @@ public class DbViewer : Window
|
||||
using (var innerPopup = ImRaii.Popup("InputPathDialog"))
|
||||
{
|
||||
if (innerPopup.Success)
|
||||
Plugin.FileDialogManager.OpenFolderDialog(Language.Folder_Selection_Header, (b, s) => { if (b) InputPath = s; }, null, true);
|
||||
Plugin.FileDialogManager.OpenFolderDialog(
|
||||
Language.Folder_Selection_Header,
|
||||
(b, s) =>
|
||||
{
|
||||
if (b)
|
||||
InputPath = s;
|
||||
},
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
ImGui.SameLine(0, spacing);
|
||||
@@ -157,7 +202,8 @@ public class DbViewer : Window
|
||||
UserDismissable = false,
|
||||
InitialDuration = TimeSpan.FromSeconds(10000),
|
||||
Progress = 0.0f,
|
||||
});
|
||||
}
|
||||
);
|
||||
CreateTxtBackup();
|
||||
}
|
||||
}
|
||||
@@ -176,12 +222,34 @@ public class DbViewer : Window
|
||||
var loadingIndicator = IsProcessing && ProcessingStart < Environment.TickCount64;
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(string.Format(Language.DbViewer_Page, CurrentPage, totalPages, Count, loadingIndicator ? Language.DbViewer_LoadingIndicator : ""));
|
||||
ImGuiUtil.DrawArrows(ref CurrentPage, 1, totalPages, spacing, tooltipLeft: Language.Page_ArrowLeft_Tooltip, tooltipRight: Language.Page_ArrowRight_Tooltip);
|
||||
ImGui.TextUnformatted(
|
||||
string.Format(
|
||||
Language.DbViewer_Page,
|
||||
CurrentPage,
|
||||
totalPages,
|
||||
Count,
|
||||
loadingIndicator ? Language.DbViewer_LoadingIndicator : ""
|
||||
)
|
||||
);
|
||||
ImGuiUtil.DrawArrows(
|
||||
ref CurrentPage,
|
||||
1,
|
||||
totalPages,
|
||||
spacing,
|
||||
tooltipLeft: Language.Page_ArrowLeft_Tooltip,
|
||||
tooltipRight: Language.Page_ArrowRight_Tooltip
|
||||
);
|
||||
|
||||
ImGui.SameLine(ImGui.GetContentRegionMax().X - width);
|
||||
ImGui.SetNextItemWidth(width);
|
||||
if (ImGui.InputTextWithHint("##searchbar", Language.DbViewer_SearcHint, ref SimpleSearchTerm, 30))
|
||||
if (
|
||||
ImGui.InputTextWithHint(
|
||||
"##searchbar",
|
||||
Language.DbViewer_SearcHint,
|
||||
ref SimpleSearchTerm,
|
||||
30
|
||||
)
|
||||
)
|
||||
Filtered = Filter(Messages);
|
||||
|
||||
// Third row
|
||||
@@ -189,7 +257,17 @@ public class DbViewer : Window
|
||||
if (DateWidget.Validate(MinimalDate, ref AfterDate, ref BeforeDate))
|
||||
DateRefresh();
|
||||
|
||||
if (!IsProcessing && LastProcessed != (AfterDate, BeforeDate, CurrentPage, OnlyCurrentCharacter, SelectedChannels.Count))
|
||||
if (
|
||||
!IsProcessing
|
||||
&& LastProcessed
|
||||
!= (
|
||||
AfterDate,
|
||||
BeforeDate,
|
||||
CurrentPage,
|
||||
OnlyCurrentCharacter,
|
||||
SelectedChannels.Count
|
||||
)
|
||||
)
|
||||
{
|
||||
// Page hasn't changed, so we reset it back to 1
|
||||
if (LastProcessed.Page == CurrentPage)
|
||||
@@ -198,19 +276,37 @@ public class DbViewer : Window
|
||||
AdjustDates();
|
||||
IsProcessing = true;
|
||||
ProcessingStart = Environment.TickCount64 + 1_000; // + 1 second
|
||||
LastProcessed = (AfterDate, BeforeDate, CurrentPage, OnlyCurrentCharacter, SelectedChannels.Count);
|
||||
LastProcessed = (
|
||||
AfterDate,
|
||||
BeforeDate,
|
||||
CurrentPage,
|
||||
OnlyCurrentCharacter,
|
||||
SelectedChannels.Count
|
||||
);
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ulong? character = OnlyCurrentCharacter ? Plugin.PlayerState.ContentId : null;
|
||||
var channels = SelectedChannels.Select(pair => (byte) pair.Key).ToArray();
|
||||
var channels = SelectedChannels.Select(pair => (byte)pair.Key).ToArray();
|
||||
|
||||
// We only want to fetch count if this is the first page
|
||||
if (CurrentPage == 1)
|
||||
Count = Plugin.MessageManager.Store.CountDateRange(AfterDate, BeforeDate, channels, character);
|
||||
Count = Plugin.MessageManager.Store.CountDateRange(
|
||||
AfterDate,
|
||||
BeforeDate,
|
||||
channels,
|
||||
character
|
||||
);
|
||||
|
||||
using var rangeMessageEnumerator = Plugin.MessageManager.Store.GetPagedDateRange(AfterDate, BeforeDate, channels, character, CurrentPage - 1);
|
||||
using var rangeMessageEnumerator =
|
||||
Plugin.MessageManager.Store.GetPagedDateRange(
|
||||
AfterDate,
|
||||
BeforeDate,
|
||||
channels,
|
||||
character,
|
||||
CurrentPage - 1
|
||||
);
|
||||
Messages = rangeMessageEnumerator.ToArray();
|
||||
|
||||
Filtered = Filter(Messages);
|
||||
@@ -231,7 +327,11 @@ public class DbViewer : Window
|
||||
|
||||
if (Filtered.IsEmpty)
|
||||
{
|
||||
ImGui.TextUnformatted(SimpleSearchTerm == "" ? Language.DbViewer_Status_NothingFound : Language.DbViewer_Status_NoSearchResult);
|
||||
ImGui.TextUnformatted(
|
||||
SimpleSearchTerm == ""
|
||||
? Language.DbViewer_Status_NothingFound
|
||||
: Language.DbViewer_Status_NoSearchResult
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,13 +345,24 @@ public class DbViewer : Window
|
||||
ImGui.SetScrollY(0.0f);
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table("##messageHistory", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable);
|
||||
using var table = ImRaii.Table(
|
||||
"##messageHistory",
|
||||
4,
|
||||
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable
|
||||
);
|
||||
if (!table.Success)
|
||||
return;
|
||||
|
||||
var columnWidth = ImGui.CalcTextSize(Language.DbViewer_TableField_Type);
|
||||
ImGui.TableSetupColumn(Language.DbViewer_TableField_Date, ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize);
|
||||
ImGui.TableSetupColumn(Language.DbViewer_TableField_Type, ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize, columnWidth.X);
|
||||
ImGui.TableSetupColumn(
|
||||
Language.DbViewer_TableField_Date,
|
||||
ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize
|
||||
);
|
||||
ImGui.TableSetupColumn(
|
||||
Language.DbViewer_TableField_Type,
|
||||
ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize,
|
||||
columnWidth.X
|
||||
);
|
||||
ImGui.TableSetupColumn(Language.DbViewer_TableField_Sender);
|
||||
ImGui.TableSetupColumn(Language.DbViewer_TableField_Content);
|
||||
|
||||
@@ -349,10 +460,18 @@ public class DbViewer : Window
|
||||
return new ConcurrentStack<Message>(messages.Reverse().OrderByDescending(m => m.Date));
|
||||
|
||||
return new ConcurrentStack<Message>(
|
||||
messages.Reverse().Where(m =>
|
||||
ChunkUtil.ToRawString(m.Sender).Contains(SimpleSearchTerm, StringComparison.InvariantCultureIgnoreCase) ||
|
||||
ChunkUtil.ToRawString(m.Content).Contains(SimpleSearchTerm, StringComparison.InvariantCultureIgnoreCase)
|
||||
).OrderByDescending(m => m.Date));
|
||||
messages
|
||||
.Reverse()
|
||||
.Where(m =>
|
||||
ChunkUtil
|
||||
.ToRawString(m.Sender)
|
||||
.Contains(SimpleSearchTerm, StringComparison.InvariantCultureIgnoreCase)
|
||||
|| ChunkUtil
|
||||
.ToRawString(m.Content)
|
||||
.Contains(SimpleSearchTerm, StringComparison.InvariantCultureIgnoreCase)
|
||||
)
|
||||
.OrderByDescending(m => m.Date)
|
||||
);
|
||||
}
|
||||
|
||||
private void DateRefresh()
|
||||
@@ -386,7 +505,12 @@ public class DbViewer : Window
|
||||
ulong? character = OnlyCurrentCharacter ? Plugin.PlayerState.ContentId : null;
|
||||
var channels = SelectedChannels.Select(pair => (byte)pair.Key).ToArray();
|
||||
|
||||
var rangeMessageEnumerator = Plugin.MessageManager.Store.GetDateRange(AfterDate, BeforeDate, channels, character);
|
||||
var rangeMessageEnumerator = Plugin.MessageManager.Store.GetDateRange(
|
||||
AfterDate,
|
||||
BeforeDate,
|
||||
channels,
|
||||
character
|
||||
);
|
||||
var messageHistory = rangeMessageEnumerator.ToArray();
|
||||
await rangeMessageEnumerator.DisposeAsync();
|
||||
|
||||
@@ -397,28 +521,46 @@ public class DbViewer : Window
|
||||
var totalCount = filteredHistory.Count;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
await using var stream = new StreamWriter(Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.txt"));
|
||||
await using var stream = new StreamWriter(
|
||||
Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.txt")
|
||||
);
|
||||
|
||||
var batch = 0;
|
||||
foreach (var messages in filteredHistory.Batch(5000))
|
||||
{
|
||||
await Plugin.Framework.RunOnTick(() =>
|
||||
{
|
||||
foreach (var message in messages)
|
||||
await Plugin.Framework.RunOnTick(
|
||||
() =>
|
||||
{
|
||||
if (!Sheets.LogKindSheet.TryGetRow((uint)message.Code.Type, out var logKind))
|
||||
logKind = Sheets.LogKindSheet.GetRow(10); // default to say
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (
|
||||
!Sheets.LogKindSheet.TryGetRow(
|
||||
(uint)message.Code.Type,
|
||||
out var logKind
|
||||
)
|
||||
)
|
||||
logKind = Sheets.LogKindSheet.GetRow(10); // default to say
|
||||
|
||||
var rossSender = new ReadOnlySeString(message.SenderSource.Encode());
|
||||
var rossMessage = new ReadOnlySeString(message.ContentSource.Encode());
|
||||
var rossSender = new ReadOnlySeString(
|
||||
message.SenderSource.Encode()
|
||||
);
|
||||
var rossMessage = new ReadOnlySeString(
|
||||
message.ContentSource.Encode()
|
||||
);
|
||||
|
||||
var timestamp = message.Date.ToLocalTime().ToString(DateTimeFormat);
|
||||
var text = Plugin.Evaluator.Evaluate(logKind.Format, [rossSender, rossMessage]).ToString();
|
||||
sb.AppendLine($"[{timestamp}][{message.Code.Type.Name()}] {text}");
|
||||
var timestamp = message.Date.ToLocalTime().ToString(DateTimeFormat);
|
||||
var text = Plugin
|
||||
.Evaluator.Evaluate(logKind.Format, [rossSender, rossMessage])
|
||||
.ToString();
|
||||
sb.AppendLine(
|
||||
$"[{timestamp}][{message.Code.Type.Name()}] {text}"
|
||||
);
|
||||
|
||||
batch++;
|
||||
}
|
||||
}, delayTicks: 5);
|
||||
batch++;
|
||||
}
|
||||
},
|
||||
delayTicks: 5
|
||||
);
|
||||
|
||||
Notification.Progress = (float)batch / totalCount;
|
||||
Notification.Content = $"Exported {batch} of {totalCount} messages";
|
||||
@@ -447,5 +589,4 @@ public class DbViewer : Window
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+27
-12
@@ -1,10 +1,10 @@
|
||||
using System.Numerics;
|
||||
using HellionChat.Code;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Code;
|
||||
using Lumina.Text.ReadOnly;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
@@ -14,7 +14,8 @@ public class DebuggerWindow : Window, IDisposable
|
||||
private readonly Plugin Plugin;
|
||||
private readonly ChatLogWindow ChatLogWindow;
|
||||
|
||||
public DebuggerWindow(Plugin plugin) : base("Debugger###chat2-debugger")
|
||||
public DebuggerWindow(Plugin plugin)
|
||||
: base("Debugger###chat2-debugger")
|
||||
{
|
||||
Plugin = plugin;
|
||||
ChatLogWindow = plugin.ChatLogWindow;
|
||||
@@ -22,7 +23,7 @@ public class DebuggerWindow : Window, IDisposable
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(475, 600),
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
||||
};
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
@@ -40,7 +41,7 @@ public class DebuggerWindow : Window, IDisposable
|
||||
|
||||
public override unsafe void Draw()
|
||||
{
|
||||
var agent = (nint) AgentItemDetail.Instance();
|
||||
var agent = (nint)AgentItemDetail.Instance();
|
||||
ImGui.TextUnformatted($"Current Cursor Pos: {ChatLogWindow.CursorPos}");
|
||||
if (ImGui.Selectable($"Agent Address: {agent:X}"))
|
||||
ImGui.SetClipboardText(agent.ToString("X"));
|
||||
@@ -50,23 +51,37 @@ public class DebuggerWindow : Window, IDisposable
|
||||
ImGui.TextUnformatted($"Handle Tooltips: {ChatLogWindow.PayloadHandler.HandleTooltips}");
|
||||
ImGui.TextUnformatted($"Hovered Item: {ChatLogWindow.PayloadHandler.HoveredItem}");
|
||||
ImGui.TextUnformatted($"Hover Counter: {ChatLogWindow.PayloadHandler.HoverCounter}");
|
||||
ImGui.TextUnformatted($"Last Hover Counter: {ChatLogWindow.PayloadHandler.LastHoverCounter}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Last Hover Counter: {ChatLogWindow.PayloadHandler.LastHoverCounter}"
|
||||
);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
ImGui.TextColored(ImGuiColors.DalamudOrange, "Current Tab");
|
||||
ImGui.TextUnformatted($"Name: {Plugin.CurrentTab.Name}");
|
||||
ImGui.TextUnformatted($"Channel: {Plugin.CurrentTab.CurrentChannel.Channel.ToChatType().Name()}");
|
||||
ImGui.TextUnformatted($"Tell Target: {Plugin.CurrentTab.CurrentChannel.TellTarget?.ToTargetString() ?? "Null"}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Channel: {Plugin.CurrentTab.CurrentChannel.Channel.ToChatType().Name()}"
|
||||
);
|
||||
ImGui.TextUnformatted(
|
||||
$"Tell Target: {Plugin.CurrentTab.CurrentChannel.TellTarget?.ToTargetString() ?? "Null"}"
|
||||
);
|
||||
ImGui.TextUnformatted($"Use Temp? {Plugin.CurrentTab.CurrentChannel.UseTempChannel}");
|
||||
ImGui.TextUnformatted($"Temp Channel: {Plugin.CurrentTab.CurrentChannel.TempChannel.ToChatType().Name()}");
|
||||
ImGui.TextUnformatted($"Temp Tell Target: {Plugin.CurrentTab.CurrentChannel.TempTellTarget?.ToTargetString() ?? "Null"}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Temp Channel: {Plugin.CurrentTab.CurrentChannel.TempChannel.ToChatType().Name()}"
|
||||
);
|
||||
ImGui.TextUnformatted(
|
||||
$"Temp Tell Target: {Plugin.CurrentTab.CurrentChannel.TempTellTarget?.ToTargetString() ?? "Null"}"
|
||||
);
|
||||
ImGui.TextUnformatted($"Name Set? {Plugin.CurrentTab.CurrentChannel.Name.Count > 0}");
|
||||
ImGui.TextUnformatted($"Name {string.Join(" ", Plugin.CurrentTab.CurrentChannel.Name.Select(c => c.StringValue()))}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Name {string.Join(" ", Plugin.CurrentTab.CurrentChannel.Name.Select(c => c.StringValue()))}"
|
||||
);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5.0f);
|
||||
|
||||
ImGui.TextColored(ImGuiColors.DalamudOrange, "Vanilla Chat");
|
||||
ImGui.TextUnformatted($"Channel: {new ReadOnlySeString(AgentChatLog.Instance()->ChannelLabel).ExtractText()}");
|
||||
ImGui.TextUnformatted(
|
||||
$"Channel: {new ReadOnlySeString(AgentChatLog.Instance()->ChannelLabel).ExtractText()}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Privacy;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -13,7 +13,8 @@ public sealed class FirstRunWizard : Window
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
|
||||
internal FirstRunWizard(Plugin plugin) : base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
|
||||
internal FirstRunWizard(Plugin plugin)
|
||||
: base($"{HellionStrings.Wizard_Title}###hellion-firstrun")
|
||||
{
|
||||
Plugin = plugin;
|
||||
|
||||
@@ -50,33 +51,54 @@ public sealed class FirstRunWizard : Window
|
||||
var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f;
|
||||
var cardHeight = avail.Y - ImGui.GetTextLineHeightWithSpacing();
|
||||
|
||||
DrawCard("privacy-first", cardWidth, cardHeight,
|
||||
DrawCard(
|
||||
"privacy-first",
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
HellionStrings.Wizard_Profile_PrivacyFirst_Heading,
|
||||
HellionStrings.Wizard_Profile_PrivacyFirst_Description,
|
||||
null,
|
||||
HellionStrings.Wizard_Profile_PrivacyFirst_Apply,
|
||||
ApplyPrivacyFirst);
|
||||
ApplyPrivacyFirst
|
||||
);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
DrawCard("casual", cardWidth, cardHeight,
|
||||
DrawCard(
|
||||
"casual",
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
HellionStrings.Wizard_Profile_Casual_Heading,
|
||||
HellionStrings.Wizard_Profile_Casual_Description,
|
||||
null,
|
||||
HellionStrings.Wizard_Profile_Casual_Apply,
|
||||
ApplyCasual);
|
||||
ApplyCasual
|
||||
);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
DrawCard("full-history", cardWidth, cardHeight,
|
||||
DrawCard(
|
||||
"full-history",
|
||||
cardWidth,
|
||||
cardHeight,
|
||||
HellionStrings.Wizard_Profile_FullHistory_Heading,
|
||||
HellionStrings.Wizard_Profile_FullHistory_Description,
|
||||
HellionStrings.Wizard_Profile_FullHistory_GdprWarning,
|
||||
HellionStrings.Wizard_Profile_FullHistory_Apply,
|
||||
ApplyFullHistory);
|
||||
ApplyFullHistory
|
||||
);
|
||||
}
|
||||
|
||||
private void DrawCard(string id, float width, float height, string heading, string description, string? warning, string buttonLabel, Action onApply)
|
||||
private void DrawCard(
|
||||
string id,
|
||||
float width,
|
||||
float height,
|
||||
string heading,
|
||||
string description,
|
||||
string? warning,
|
||||
string buttonLabel,
|
||||
Action onApply
|
||||
)
|
||||
{
|
||||
using var child = ImRaii.Child($"##wizard-card-{id}", new Vector2(width, height), true);
|
||||
if (!child.Success)
|
||||
@@ -113,19 +135,21 @@ public sealed class FirstRunWizard : Window
|
||||
private void ApplyPrivacyFirst()
|
||||
{
|
||||
Plugin.Config.PrivacyFilterEnabled = true;
|
||||
Plugin.Config.PrivacyPersistChannels = [..PrivacyDefaults.PrivacyFirstWhitelist];
|
||||
Plugin.Config.PrivacyPersistChannels = [.. PrivacyDefaults.PrivacyFirstWhitelist];
|
||||
Plugin.Config.PrivacyPersistUnknownChannels = false;
|
||||
|
||||
Plugin.Config.RetentionEnabled = true;
|
||||
Plugin.Config.RetentionDefaultDays = 30;
|
||||
Plugin.Config.RetentionPerChannelDays =
|
||||
PrivacyDefaults.DefaultRetentionDays.ToDictionary(p => p.Key, p => p.Value);
|
||||
Plugin.Config.RetentionPerChannelDays = PrivacyDefaults.DefaultRetentionDays.ToDictionary(
|
||||
p => p.Key,
|
||||
p => p.Value
|
||||
);
|
||||
}
|
||||
|
||||
private void ApplyCasual()
|
||||
{
|
||||
Plugin.Config.PrivacyFilterEnabled = true;
|
||||
Plugin.Config.PrivacyPersistChannels = [..PrivacyDefaults.CasualWhitelist];
|
||||
Plugin.Config.PrivacyPersistChannels = [.. PrivacyDefaults.CasualWhitelist];
|
||||
Plugin.Config.PrivacyPersistUnknownChannels = false;
|
||||
|
||||
Plugin.Config.RetentionEnabled = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -21,18 +21,18 @@ internal static class HellionStyle
|
||||
{
|
||||
var a = theme.AbgrCache;
|
||||
var stack = new StackHandle();
|
||||
stack.PushColorAbgr(ImGuiCol.Button, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.ButtonHovered, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.ButtonActive, a.PrimaryDark);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBg, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBgHovered, a.SurfaceHover);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBgActive, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.Border, a.Border);
|
||||
stack.PushColorAbgr(ImGuiCol.Header, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.HeaderHovered, a.SurfaceHover);
|
||||
stack.PushColorAbgr(ImGuiCol.HeaderActive, a.Identity);
|
||||
stack.PushColorAbgr(ImGuiCol.CheckMark, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.SliderGrab, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.Button, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.ButtonHovered, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.ButtonActive, a.PrimaryDark);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBg, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBgHovered, a.SurfaceHover);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBgActive, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.Border, a.Border);
|
||||
stack.PushColorAbgr(ImGuiCol.Header, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.HeaderHovered, a.SurfaceHover);
|
||||
stack.PushColorAbgr(ImGuiCol.HeaderActive, a.Identity);
|
||||
stack.PushColorAbgr(ImGuiCol.CheckMark, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.SliderGrab, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.SliderGrabActive, a.PrimaryLight);
|
||||
return stack;
|
||||
}
|
||||
@@ -67,71 +67,71 @@ internal static class HellionStyle
|
||||
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | childBgAlpha;
|
||||
|
||||
// Layout
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, l.ScrollbarRounding);
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize);
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize);
|
||||
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize);
|
||||
|
||||
// Surfaces — WindowBg/ChildBg use the per-push opacity-modulated value,
|
||||
// so they go through the RGBA path; everything else reads from cache.
|
||||
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
|
||||
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
|
||||
stack.PushColorAbgr(ImGuiCol.PopupBg, a.ChildBg);
|
||||
stack.PushColorAbgr(ImGuiCol.Border, a.Border);
|
||||
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
|
||||
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
|
||||
stack.PushColorAbgr(ImGuiCol.PopupBg, a.ChildBg);
|
||||
stack.PushColorAbgr(ImGuiCol.Border, a.Border);
|
||||
stack.PushColorAbgr(ImGuiCol.BorderShadow, 0u);
|
||||
|
||||
// Frames
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBg, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBg, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBgHovered, a.SurfaceHover);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBgActive, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.FrameBgActive, a.Surface);
|
||||
|
||||
// Title bars
|
||||
stack.PushColorAbgr(ImGuiCol.TitleBg, a.WindowBg);
|
||||
stack.PushColorAbgr(ImGuiCol.TitleBgActive, a.Identity);
|
||||
stack.PushColorAbgr(ImGuiCol.TitleBg, a.WindowBg);
|
||||
stack.PushColorAbgr(ImGuiCol.TitleBgActive, a.Identity);
|
||||
stack.PushColorAbgr(ImGuiCol.TitleBgCollapsed, a.WindowBg);
|
||||
|
||||
// Buttons
|
||||
stack.PushColorAbgr(ImGuiCol.Button, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.Button, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.ButtonHovered, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.ButtonActive, a.PrimaryDark);
|
||||
stack.PushColorAbgr(ImGuiCol.ButtonActive, a.PrimaryDark);
|
||||
|
||||
// Headers / selectables
|
||||
stack.PushColorAbgr(ImGuiCol.Header, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.Header, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.HeaderHovered, a.SurfaceHover);
|
||||
stack.PushColorAbgr(ImGuiCol.HeaderActive, a.Identity);
|
||||
stack.PushColorAbgr(ImGuiCol.HeaderActive, a.Identity);
|
||||
|
||||
// Tabs
|
||||
stack.PushColorAbgr(ImGuiCol.Tab, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.TabHovered, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.TabActive, a.Identity);
|
||||
stack.PushColorAbgr(ImGuiCol.TabUnfocused, a.ChildBg);
|
||||
stack.PushColorAbgr(ImGuiCol.Tab, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.TabHovered, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.TabActive, a.Identity);
|
||||
stack.PushColorAbgr(ImGuiCol.TabUnfocused, a.ChildBg);
|
||||
stack.PushColorAbgr(ImGuiCol.TabUnfocusedActive, a.PrimaryDark);
|
||||
|
||||
// Scrollbar
|
||||
stack.PushColorAbgr(ImGuiCol.ScrollbarBg, a.WindowBg);
|
||||
stack.PushColorAbgr(ImGuiCol.ScrollbarGrab, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.ScrollbarBg, a.WindowBg);
|
||||
stack.PushColorAbgr(ImGuiCol.ScrollbarGrab, a.Surface);
|
||||
stack.PushColorAbgr(ImGuiCol.ScrollbarGrabHovered, a.AccentLight);
|
||||
stack.PushColorAbgr(ImGuiCol.ScrollbarGrabActive, a.Accent);
|
||||
stack.PushColorAbgr(ImGuiCol.ScrollbarGrabActive, a.Accent);
|
||||
|
||||
// Resize grip
|
||||
stack.PushColorAbgr(ImGuiCol.ResizeGrip, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.ResizeGrip, a.FrameBg);
|
||||
stack.PushColorAbgr(ImGuiCol.ResizeGripHovered, a.AccentLight);
|
||||
stack.PushColorAbgr(ImGuiCol.ResizeGripActive, a.Accent);
|
||||
stack.PushColorAbgr(ImGuiCol.ResizeGripActive, a.Accent);
|
||||
|
||||
// Check mark + slider grab
|
||||
stack.PushColorAbgr(ImGuiCol.CheckMark, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.SliderGrab, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.SliderGrabActive, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.CheckMark, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.SliderGrab, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.SliderGrabActive, a.PrimaryLight);
|
||||
|
||||
// Separator
|
||||
stack.PushColorAbgr(ImGuiCol.Separator, a.Border);
|
||||
stack.PushColorAbgr(ImGuiCol.SeparatorHovered, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.SeparatorActive, a.Primary);
|
||||
stack.PushColorAbgr(ImGuiCol.Separator, a.Border);
|
||||
stack.PushColorAbgr(ImGuiCol.SeparatorHovered, a.PrimaryLight);
|
||||
stack.PushColorAbgr(ImGuiCol.SeparatorActive, a.Primary);
|
||||
|
||||
return stack;
|
||||
}
|
||||
@@ -140,14 +140,14 @@ internal static class HellionStyle
|
||||
{
|
||||
private readonly List<IDisposable> _items = new(64);
|
||||
|
||||
internal void PushColor(ImGuiCol slot, uint rgba)
|
||||
=> _items.Add(ImRaii.PushColor(slot, ColourUtil.RgbaToAbgr(rgba)));
|
||||
internal void PushColor(ImGuiCol slot, uint rgba) =>
|
||||
_items.Add(ImRaii.PushColor(slot, ColourUtil.RgbaToAbgr(rgba)));
|
||||
|
||||
internal void PushColorAbgr(ImGuiCol slot, uint abgr)
|
||||
=> _items.Add(ImRaii.PushColor(slot, abgr));
|
||||
internal void PushColorAbgr(ImGuiCol slot, uint abgr) =>
|
||||
_items.Add(ImRaii.PushColor(slot, abgr));
|
||||
|
||||
internal void PushStyleVar(ImGuiStyleVar var, float value)
|
||||
=> _items.Add(ImRaii.PushStyle(var, value));
|
||||
internal void PushStyleVar(ImGuiStyleVar var, float value) =>
|
||||
_items.Add(ImRaii.PushStyle(var, value));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -30,12 +30,18 @@ public partial class InputPreview : Window
|
||||
|
||||
internal int SelectedCursorPos = -1;
|
||||
|
||||
internal InputPreview(ChatLogWindow logWindow) : base("##chat2-inputpreview")
|
||||
internal InputPreview(ChatLogWindow logWindow)
|
||||
: base("##chat2-inputpreview")
|
||||
{
|
||||
LogWindow = logWindow;
|
||||
|
||||
Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoScrollbar;
|
||||
Flags =
|
||||
ImGuiWindowFlags.NoSavedSettings
|
||||
| ImGuiWindowFlags.NoTitleBar
|
||||
| ImGuiWindowFlags.NoMove
|
||||
| ImGuiWindowFlags.NoResize
|
||||
| ImGuiWindowFlags.NoFocusOnAppearing
|
||||
| ImGuiWindowFlags.NoScrollbar;
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
@@ -49,7 +55,10 @@ public partial class InputPreview : Window
|
||||
Plugin.Framework.Update -= UpdateConditionCheck;
|
||||
}
|
||||
|
||||
private bool ValidDraw => !string.IsNullOrEmpty(LogWindow.Chat) && LogWindow.Chat.Length >= Plugin.Config.PreviewMinimum;
|
||||
private bool ValidDraw =>
|
||||
!string.IsNullOrEmpty(LogWindow.Chat)
|
||||
&& LogWindow.Chat.Length >= Plugin.Config.PreviewMinimum;
|
||||
|
||||
private void UpdateConditionCheck(IFramework framework)
|
||||
{
|
||||
Drawing = ValidDraw;
|
||||
@@ -70,7 +79,9 @@ public partial class InputPreview : Window
|
||||
var bytes = Encoding.UTF8.GetBytes(LogWindow.Chat.Trim());
|
||||
AutoTranslate.ReplaceWithPayload(ref bytes);
|
||||
|
||||
var chunks = ChunkUtil.ToChunks(SeString.Parse(bytes), ChunkSource.Content, ChatType.Say).ToList();
|
||||
var chunks = ChunkUtil
|
||||
.ToChunks(SeString.Parse(bytes), ChunkSource.Content, ChatType.Say)
|
||||
.ToList();
|
||||
PreviewMessage = Message.FakeMessage(chunks, new ChatCode(XivChatType.Say, 0, 0));
|
||||
PreviewMessage.DecodeTextParam();
|
||||
}
|
||||
@@ -79,7 +90,9 @@ public partial class InputPreview : Window
|
||||
|
||||
internal bool IsDrawable => ValidDraw && HasEvaluation;
|
||||
|
||||
private static bool IsWindowMode => Plugin.Config.PreviewPosition is PreviewPosition.Top or PreviewPosition.Bottom;
|
||||
private static bool IsWindowMode =>
|
||||
Plugin.Config.PreviewPosition is PreviewPosition.Top or PreviewPosition.Bottom;
|
||||
|
||||
public override bool DrawConditions()
|
||||
{
|
||||
return IsWindowMode && IsDrawable;
|
||||
@@ -96,7 +109,11 @@ public partial class InputPreview : Window
|
||||
{
|
||||
PreviewPosition.Top => pos.Y - PreviewHeight,
|
||||
PreviewPosition.Bottom => pos.Y + size.Y,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(Plugin.Config.PreviewPosition), Plugin.Config.PreviewPosition, null)
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(Plugin.Config.PreviewPosition),
|
||||
Plugin.Config.PreviewPosition,
|
||||
null
|
||||
),
|
||||
};
|
||||
|
||||
Position = pos with { Y = y };
|
||||
@@ -141,7 +158,12 @@ public partial class InputPreview : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawChunksPreview(IReadOnlyList<Chunk> chunks, PayloadHandler? handler = null, float lineWidth = 0f, int unique = 0)
|
||||
private void DrawChunksPreview(
|
||||
IReadOnlyList<Chunk> chunks,
|
||||
PayloadHandler? handler = null,
|
||||
float lineWidth = 0f,
|
||||
int unique = 0
|
||||
)
|
||||
{
|
||||
CursorPosition = 0;
|
||||
|
||||
@@ -168,7 +190,12 @@ public partial class InputPreview : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawChunkPreview(Chunk chunk, PayloadHandler? handler = null, float lineWidth = 0f, int unique = 0)
|
||||
private void DrawChunkPreview(
|
||||
Chunk chunk,
|
||||
PayloadHandler? handler = null,
|
||||
float lineWidth = 0f,
|
||||
int unique = 0
|
||||
)
|
||||
{
|
||||
if (chunk is IconChunk icon)
|
||||
{
|
||||
@@ -248,7 +275,14 @@ public partial class InputPreview : Window
|
||||
var letterSize = ImGui.CalcTextSize(letter.ToString());
|
||||
|
||||
CursorPosition++;
|
||||
if (ImGui.Selectable($"{letter}##{CursorPosition + unique}", false, ImGuiSelectableFlags.None, letterSize))
|
||||
if (
|
||||
ImGui.Selectable(
|
||||
$"{letter}##{CursorPosition + unique}",
|
||||
false,
|
||||
ImGuiSelectableFlags.None,
|
||||
letterSize
|
||||
)
|
||||
)
|
||||
{
|
||||
SelectedCursorPos = CursorPosition;
|
||||
LogWindow.FocusedPreview = true;
|
||||
|
||||
+34
-10
@@ -1,8 +1,8 @@
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -27,7 +27,8 @@ internal class Popout : Window
|
||||
// matching pop-out window when an LRU temp tab gets evicted.
|
||||
internal Guid TabIdentifier => Tab.Identifier;
|
||||
|
||||
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) : base($"{tab.Name}##popout")
|
||||
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx)
|
||||
: base($"{tab.Name}##popout")
|
||||
{
|
||||
ChatLogWindow = chatLogWindow;
|
||||
Tab = tab;
|
||||
@@ -59,7 +60,11 @@ internal class Popout : Window
|
||||
if (Tab.IndependentHide ? HideStateCheck() : ChatLogWindow.IsHidden)
|
||||
return false;
|
||||
|
||||
if (!Plugin.Config.HideWhenInactive || (!Plugin.Config.InactivityHideActiveDuringBattle && Plugin.InBattle) || !Tab.UnhideOnActivity)
|
||||
if (
|
||||
!Plugin.Config.HideWhenInactive
|
||||
|| (!Plugin.Config.InactivityHideActiveDuringBattle && Plugin.InBattle)
|
||||
|| !Tab.UnhideOnActivity
|
||||
)
|
||||
{
|
||||
LastActivityTime = FrameTime;
|
||||
return true;
|
||||
@@ -169,7 +174,13 @@ internal class Popout : Window
|
||||
|
||||
var dismiss = false;
|
||||
var openSettings = false;
|
||||
using (var child = ImRaii.Child("##v060-pop-out-hint", new System.Numerics.Vector2(0f, 64f), true))
|
||||
using (
|
||||
var child = ImRaii.Child(
|
||||
"##v060-pop-out-hint",
|
||||
new System.Numerics.Vector2(0f, 64f),
|
||||
true
|
||||
)
|
||||
)
|
||||
{
|
||||
if (child)
|
||||
{
|
||||
@@ -222,7 +233,7 @@ internal class Popout : Window
|
||||
Cutscene,
|
||||
CutsceneOverride,
|
||||
User,
|
||||
Battle
|
||||
Battle,
|
||||
}
|
||||
|
||||
private HideState CurrentHideState = HideState.None;
|
||||
@@ -244,7 +255,11 @@ internal class Popout : Window
|
||||
}
|
||||
|
||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||
if (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
||||
if (
|
||||
Tab.HideDuringCutscenes
|
||||
&& CurrentHideState == HideState.None
|
||||
&& (Plugin.CutsceneActive || Plugin.GposeActive)
|
||||
)
|
||||
{
|
||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||
{
|
||||
@@ -254,9 +269,15 @@ internal class Popout : Window
|
||||
}
|
||||
|
||||
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
||||
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
||||
if (
|
||||
CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride
|
||||
&& !Plugin.CutsceneActive
|
||||
&& !Plugin.GposeActive
|
||||
)
|
||||
{
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||
Plugin.Log.Verbose(
|
||||
$"Popout HideState [{Tab.Name}]: {CurrentHideState} → None (cutscene/gpose ended)"
|
||||
);
|
||||
CurrentHideState = HideState.None;
|
||||
}
|
||||
|
||||
@@ -264,7 +285,9 @@ internal class Popout : Window
|
||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||
{
|
||||
CurrentHideState = HideState.CutsceneOverride;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Cutscene → CutsceneOverride (user activate)");
|
||||
Plugin.Log.Verbose(
|
||||
$"Popout HideState [{Tab.Name}]: Cutscene → CutsceneOverride (user activate)"
|
||||
);
|
||||
}
|
||||
|
||||
// if the user hid the chat and is now activating chat, reset the hide state
|
||||
@@ -274,6 +297,7 @@ internal class Popout : Window
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User → None (activate)");
|
||||
}
|
||||
|
||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
|
||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
|
||||
|| (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
|
||||
}
|
||||
}
|
||||
|
||||
+174
-107
@@ -1,13 +1,13 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Utility;
|
||||
using HellionChat.Util;
|
||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
@@ -16,29 +16,30 @@ public class SeStringDebugger : Window
|
||||
{
|
||||
private readonly Plugin Plugin;
|
||||
|
||||
public SeStringDebugger(Plugin plugin) : base("SeString Debugger###chat2-sestringdebugger")
|
||||
public SeStringDebugger(Plugin plugin)
|
||||
: base("SeString Debugger###chat2-sestringdebugger")
|
||||
{
|
||||
Plugin = plugin;
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(475, 600),
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
||||
};
|
||||
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute += Toggle;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
#if DEBUG
|
||||
#if DEBUG
|
||||
Plugin.Commands.Register("/hellionSeString", showInHelp: false).Execute -= Toggle;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Toggle(string _, string __) => Toggle();
|
||||
@@ -75,99 +76,143 @@ public class SeStringDebugger : Window
|
||||
{
|
||||
case UIForegroundPayload color:
|
||||
{
|
||||
RenderMetadataDictionary("Link ForegroundColor", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Enabled?", color.IsEnabled.ToString() },
|
||||
{ "ColorKey", color.IsEnabled ? color.ColorKey.ToString() : "Color Ended" },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link ForegroundColor",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Enabled?", color.IsEnabled.ToString() },
|
||||
{
|
||||
"ColorKey",
|
||||
color.IsEnabled ? color.ColorKey.ToString() : "Color Ended"
|
||||
},
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case MapLinkPayload map:
|
||||
{
|
||||
RenderMetadataDictionary("Link MapLinkPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Map.RowId", map.Map.RowId.ToString() },
|
||||
{ "Map.PlaceName", map.Map.Value.PlaceName.Value.Name.ToString() },
|
||||
{ "Map.PlaceNameRegion", map.Map.Value.PlaceNameRegion.Value.Name.ToString() },
|
||||
{ "Map.PlaceNameSub", map.Map.Value.PlaceNameSub.Value.Name.ToString() },
|
||||
{ "TerritoryType.RowId", map.TerritoryType.RowId.ToString() },
|
||||
{ "RawX", map.RawX.ToString() },
|
||||
{ "RawY", map.RawY.ToString() },
|
||||
{ "XCoord", map.XCoord.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "YCoord", map.YCoord.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "CoordinateString", map.CoordinateString },
|
||||
{ "DataString", map.DataString },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link MapLinkPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Map.RowId", map.Map.RowId.ToString() },
|
||||
{ "Map.PlaceName", map.Map.Value.PlaceName.Value.Name.ToString() },
|
||||
{
|
||||
"Map.PlaceNameRegion",
|
||||
map.Map.Value.PlaceNameRegion.Value.Name.ToString()
|
||||
},
|
||||
{
|
||||
"Map.PlaceNameSub",
|
||||
map.Map.Value.PlaceNameSub.Value.Name.ToString()
|
||||
},
|
||||
{ "TerritoryType.RowId", map.TerritoryType.RowId.ToString() },
|
||||
{ "RawX", map.RawX.ToString() },
|
||||
{ "RawY", map.RawY.ToString() },
|
||||
{ "XCoord", map.XCoord.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "YCoord", map.YCoord.ToString(CultureInfo.InvariantCulture) },
|
||||
{ "CoordinateString", map.CoordinateString },
|
||||
{ "DataString", map.DataString },
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case QuestPayload quest:
|
||||
{
|
||||
RenderMetadataDictionary("Link QuestPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Quest.RowId", quest.Quest.RowId.ToString() },
|
||||
{ "Quest.Name", quest.Quest.Value.Name.ToString() },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link QuestPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Quest.RowId", quest.Quest.RowId.ToString() },
|
||||
{ "Quest.Name", quest.Quest.Value.Name.ToString() },
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case DalamudLinkPayload link:
|
||||
{
|
||||
RenderMetadataDictionary("Link DalamudLinkPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "CommandId", link.CommandId.ToString() },
|
||||
{ "Plugin", link.Plugin },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link DalamudLinkPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "CommandId", link.CommandId.ToString() },
|
||||
{ "Plugin", link.Plugin },
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case DalamudPartyFinderPayload pf:
|
||||
{
|
||||
RenderMetadataDictionary("Link PartyFinderPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "ListingId", pf.ListingId.ToString() },
|
||||
{ "LinkType", EnumName(pf.LinkType) },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link PartyFinderPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "ListingId", pf.ListingId.ToString() },
|
||||
{ "LinkType", EnumName(pf.LinkType) },
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PlayerPayload player:
|
||||
{
|
||||
RenderMetadataDictionary("Link PlayerPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Displayed", player.DisplayedName },
|
||||
{ "Player Name", player.PlayerName },
|
||||
{ "World Name", player.World.Value.Name.ExtractText() },
|
||||
{ "Data", string.Join(" ", player.Encode().Select(b => b.ToString("X2"))) },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link PlayerPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Displayed", player.DisplayedName },
|
||||
{ "Player Name", player.PlayerName },
|
||||
{ "World Name", player.World.Value.Name.ExtractText() },
|
||||
{
|
||||
"Data",
|
||||
string.Join(" ", player.Encode().Select(b => b.ToString("X2")))
|
||||
},
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ItemPayload item:
|
||||
{
|
||||
RenderMetadataDictionary("Link ItemPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "ItemId", item.ItemId.ToString() },
|
||||
{ "RawItemId", item.RawItemId.ToString() },
|
||||
{ "Kind", EnumName(item.Kind) },
|
||||
{ "IsHQ", item.IsHQ.ToString() },
|
||||
{ "Item.Name", item.Kind == ItemKind.EventItem ? Sheets.EventItemSheet.GetRow(item.ItemId).Name.ExtractText() : Sheets.ItemSheet.GetRow(item.ItemId).Name.ExtractText() },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link ItemPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "ItemId", item.ItemId.ToString() },
|
||||
{ "RawItemId", item.RawItemId.ToString() },
|
||||
{ "Kind", EnumName(item.Kind) },
|
||||
{ "IsHQ", item.IsHQ.ToString() },
|
||||
{
|
||||
"Item.Name",
|
||||
item.Kind == ItemKind.EventItem
|
||||
? Sheets.EventItemSheet.GetRow(item.ItemId).Name.ExtractText()
|
||||
: Sheets.ItemSheet.GetRow(item.ItemId).Name.ExtractText()
|
||||
},
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AutoTranslatePayload at:
|
||||
{
|
||||
RenderMetadataDictionary("Link AutoTranslatePayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Text", at.Text },
|
||||
{ "Key/Group", $"{at.Key}/{at.Group}" },
|
||||
{ "Data", string.Join(" ", at.Encode().Select(b => b.ToString("X2"))) },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link AutoTranslatePayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Text", at.Text },
|
||||
{ "Key/Group", $"{at.Key}/{at.Group}" },
|
||||
{ "Data", string.Join(" ", at.Encode().Select(b => b.ToString("X2"))) },
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case IconPayload icon:
|
||||
{
|
||||
var found = IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out _);
|
||||
RenderMetadataDictionary("Link IconPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Found", found.ToString() },
|
||||
{ "Icon ID", ((uint) icon.Icon).ToString() },
|
||||
});
|
||||
var found = IconUtil.GfdFileView.TryGetEntry((uint)icon.Icon, out _);
|
||||
RenderMetadataDictionary(
|
||||
"Link IconPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Found", found.ToString() },
|
||||
{ "Icon ID", ((uint)icon.Icon).ToString() },
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
case RawPayload raw:
|
||||
@@ -175,48 +220,60 @@ public class SeStringDebugger : Window
|
||||
var colorPayload = ColorPayload.From(raw.Data);
|
||||
if (colorPayload != null)
|
||||
{
|
||||
RenderMetadataDictionary("Link ColorPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Unshifted", colorPayload.UnshiftedColor.ToString("X8") },
|
||||
{ "Color", colorPayload.Color.ToString("X8") },
|
||||
{ "Enabled?", colorPayload.Enabled.ToString() },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link ColorPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Unshifted", colorPayload.UnshiftedColor.ToString("X8") },
|
||||
{ "Color", colorPayload.Color.ToString("X8") },
|
||||
{ "Enabled?", colorPayload.Enabled.ToString() },
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderMetadataDictionary("Link RawPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Data", string.Join(" ", raw.Data.Select(b => b.ToString("X2"))) },
|
||||
{ "Type", EnumName(raw.Type) },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link RawPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{
|
||||
"Data",
|
||||
string.Join(" ", raw.Data.Select(b => b.ToString("X2")))
|
||||
},
|
||||
{ "Type", EnumName(raw.Type) },
|
||||
}
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case StatusPayload status:
|
||||
{
|
||||
RenderMetadataDictionary("Link StatusPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Status.RowId", status.Status.RowId.ToString() },
|
||||
{ "Status.Name", status.Status.Value.Name.ExtractText() },
|
||||
{ "Status.Icon", status.Status.Value.Icon.ToString() }
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link StatusPayload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Status.RowId", status.Status.RowId.ToString() },
|
||||
{ "Status.Name", status.Status.Value.Name.ExtractText() },
|
||||
{ "Status.Icon", status.Status.Value.Icon.ToString() },
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case Util.PartyFinderPayload pf:
|
||||
{
|
||||
RenderMetadataDictionary("Link PartyFinderPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Id", pf.Id.ToString() }
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link PartyFinderPayload",
|
||||
new Dictionary<string, string?> { { "Id", pf.Id.ToString() } }
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AchievementPayload achievement:
|
||||
{
|
||||
RenderMetadataDictionary("Link AchievementPayload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Id", achievement.Id.ToString() }
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link AchievementPayload",
|
||||
new Dictionary<string, string?> { { "Id", achievement.Id.ToString() } }
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -224,35 +281,45 @@ public class SeStringDebugger : Window
|
||||
|
||||
if (payloadData.Length == 0)
|
||||
{
|
||||
RenderMetadataDictionary("Empty Payload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Type", payload.GetType().Name },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Empty Payload",
|
||||
new Dictionary<string, string?> { { "Type", payload.GetType().Name } }
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
var initialByte = payloadData[0];
|
||||
if (initialByte != 0x02)
|
||||
{
|
||||
RenderMetadataDictionary("Text Payload", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Content", Encoding.UTF8.GetString(payloadData) },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Text Payload",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{ "Content", Encoding.UTF8.GetString(payloadData) },
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unknown = new RawPayload(payloadData);
|
||||
RenderMetadataDictionary("Link Unknown", new Dictionary<string, string?>
|
||||
{
|
||||
{ "Unknown", string.Join(" ", unknown.Data.Select(b => b.ToString("X2"))) },
|
||||
});
|
||||
RenderMetadataDictionary(
|
||||
"Link Unknown",
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
{
|
||||
"Unknown",
|
||||
string.Join(" ", unknown.Data.Select(b => b.ToString("X2")))
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? EnumName<T>(T? value) where T : Enum
|
||||
private static string? EnumName<T>(T? value)
|
||||
where T : Enum
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
|
||||
+61
-31
@@ -1,11 +1,11 @@
|
||||
using System.Numerics;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui.SettingsTabs;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui.SettingsTabs;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -25,7 +25,8 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
private SettingsView View = SettingsView.Overview;
|
||||
private readonly SettingsOverview Overview;
|
||||
|
||||
internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
||||
internal SettingsWindow(Plugin plugin)
|
||||
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
||||
{
|
||||
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
|
||||
|
||||
@@ -33,7 +34,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(475, 600),
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
|
||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
||||
};
|
||||
|
||||
Plugin = plugin;
|
||||
@@ -60,7 +61,9 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
|
||||
Initialise();
|
||||
|
||||
Plugin.Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute += Command;
|
||||
Plugin
|
||||
.Commands.Register("/hellion", "Perform various actions with Hellion Chat.")
|
||||
.Execute += Command;
|
||||
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
|
||||
}
|
||||
|
||||
@@ -93,9 +96,11 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
// Pflicht — sonst triggert ESC auch wenn der User ein anderes Fenster
|
||||
// fokussiert hat und ESC fürs Game-Menü drückt (Codebase-Pattern siehe
|
||||
// Util/SearchSelector.cs:37).
|
||||
if (View == SettingsView.Detail
|
||||
if (
|
||||
View == SettingsView.Detail
|
||||
&& ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)
|
||||
&& ImGui.IsKeyPressed(ImGuiKey.Escape))
|
||||
&& ImGui.IsKeyPressed(ImGuiKey.Escape)
|
||||
)
|
||||
{
|
||||
View = SettingsView.Overview;
|
||||
return;
|
||||
@@ -150,7 +155,12 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
// der User in eine andere Section will, geht er zurück zur Overview
|
||||
// (Breadcrumb / ESC).
|
||||
var style = ImGui.GetStyle();
|
||||
var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y;
|
||||
var height =
|
||||
ImGui.GetContentRegionAvail().Y
|
||||
- style.FramePadding.Y * 2
|
||||
- style.ItemSpacing.Y
|
||||
- style.ItemInnerSpacing.Y * 2
|
||||
- ImGui.CalcTextSize("A").Y;
|
||||
|
||||
using var child = ImRaii.Child("##chat2-settings-detail", new Vector2(-1, height));
|
||||
if (child.Success)
|
||||
@@ -184,9 +194,16 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.RgbaToAbgr(0xFF4542FF)))
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, 0xFFFFFFFF))
|
||||
{
|
||||
var buttonWidth = ImGui.CalcTextSize(buttonLabel).X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
var buttonWidth2 = ImGui.CalcTextSize(buttonLabel2).X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonWidth - buttonWidth2 - ImGui.GetStyle().ItemSpacing.X);
|
||||
var buttonWidth =
|
||||
ImGui.CalcTextSize(buttonLabel).X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
var buttonWidth2 =
|
||||
ImGui.CalcTextSize(buttonLabel2).X + ImGui.GetStyle().FramePadding.X * 2;
|
||||
ImGui.SameLine(
|
||||
ImGui.GetContentRegionAvail().X
|
||||
- buttonWidth
|
||||
- buttonWidth2
|
||||
- ImGui.GetStyle().ItemSpacing.X
|
||||
);
|
||||
|
||||
if (ImGui.Button(buttonLabel2))
|
||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
|
||||
@@ -203,13 +220,15 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
// calculate all conditions before updating config
|
||||
var hideChanged = !Mutable.HideChat && Mutable.HideChat != Plugin.Config.HideChat;
|
||||
var languageChanged = Mutable.LanguageOverride != Plugin.Config.LanguageOverride;
|
||||
var fontChanged = Mutable.GlobalFontV2 != Plugin.Config.GlobalFontV2
|
||||
|| Mutable.JapaneseFontV2 != Plugin.Config.JapaneseFontV2
|
||||
|| Mutable.ItalicFontV2 != Plugin.Config.ItalicFontV2
|
||||
|| Mutable.ExtraGlyphRanges != Plugin.Config.ExtraGlyphRanges
|
||||
|| Mutable.UseHellionFont != Plugin.Config.UseHellionFont;
|
||||
var fontSizeChanged = Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|
||||
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
|
||||
var fontChanged =
|
||||
Mutable.GlobalFontV2 != Plugin.Config.GlobalFontV2
|
||||
|| Mutable.JapaneseFontV2 != Plugin.Config.JapaneseFontV2
|
||||
|| Mutable.ItalicFontV2 != Plugin.Config.ItalicFontV2
|
||||
|| Mutable.ExtraGlyphRanges != Plugin.Config.ExtraGlyphRanges
|
||||
|| Mutable.UseHellionFont != Plugin.Config.UseHellionFont;
|
||||
var fontSizeChanged =
|
||||
Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|
||||
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
|
||||
var italicStateChanged = Mutable.ItalicEnabled != Plugin.Config.ItalicEnabled;
|
||||
// v1.2.0 — Refilter only if a filter-relevant setting actually
|
||||
// changed. The Clear+Refilter cycle reloads messages from the DB,
|
||||
@@ -258,21 +277,26 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
private bool HasFilterRelevantChanges()
|
||||
{
|
||||
// Top-level privacy controls.
|
||||
if (Mutable.PrivacyFilterEnabled != Plugin.Config.PrivacyFilterEnabled) return true;
|
||||
if (Mutable.PrivacyPersistUnknownChannels != Plugin.Config.PrivacyPersistUnknownChannels) return true;
|
||||
if (!Mutable.PrivacyPersistChannels.SetEquals(Plugin.Config.PrivacyPersistChannels)) return true;
|
||||
if (Mutable.PrivacyFilterEnabled != Plugin.Config.PrivacyFilterEnabled)
|
||||
return true;
|
||||
if (Mutable.PrivacyPersistUnknownChannels != Plugin.Config.PrivacyPersistUnknownChannels)
|
||||
return true;
|
||||
if (!Mutable.PrivacyPersistChannels.SetEquals(Plugin.Config.PrivacyPersistChannels))
|
||||
return true;
|
||||
|
||||
// FilterIncludePreviousSessions changes the GetMostRecentMessages
|
||||
// window in MessageManager.FilterAllTabs and is therefore filter-
|
||||
// relevant even though it lives outside the Privacy block.
|
||||
if (Mutable.FilterIncludePreviousSessions != Plugin.Config.FilterIncludePreviousSessions) return true;
|
||||
if (Mutable.FilterIncludePreviousSessions != Plugin.Config.FilterIncludePreviousSessions)
|
||||
return true;
|
||||
|
||||
// Per-tab channel selection. Compare persistent tabs only —
|
||||
// TempTabs are session-only and never refiltered anyway.
|
||||
var origPersistent = Plugin.Config.Tabs.Where(t => !t.IsTempTab).ToList();
|
||||
var newPersistent = Mutable.Tabs.Where(t => !t.IsTempTab).ToList();
|
||||
|
||||
if (origPersistent.Count != newPersistent.Count) return true; // add or delete
|
||||
if (origPersistent.Count != newPersistent.Count)
|
||||
return true; // add or delete
|
||||
|
||||
for (var i = 0; i < origPersistent.Count; i++)
|
||||
{
|
||||
@@ -282,18 +306,24 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
// Identifier mismatch at the same index means reorder or
|
||||
// a slot got swapped — treat as filter-relevant so the new
|
||||
// channel-selection layout actually applies.
|
||||
if (orig.Identifier != neu.Identifier) return true;
|
||||
if (orig.Identifier != neu.Identifier)
|
||||
return true;
|
||||
|
||||
if (orig.ExtraChatAll != neu.ExtraChatAll) return true;
|
||||
if (!orig.ExtraChatChannels.SetEquals(neu.ExtraChatChannels)) return true;
|
||||
if (orig.ExtraChatAll != neu.ExtraChatAll)
|
||||
return true;
|
||||
if (!orig.ExtraChatChannels.SetEquals(neu.ExtraChatChannels))
|
||||
return true;
|
||||
|
||||
// SelectedChannels is a Dictionary<ChatType, (ChatSource, ChatSource)>
|
||||
// — value-tuple equality already does the right thing per-pair.
|
||||
if (orig.SelectedChannels.Count != neu.SelectedChannels.Count) return true;
|
||||
if (orig.SelectedChannels.Count != neu.SelectedChannels.Count)
|
||||
return true;
|
||||
foreach (var pair in orig.SelectedChannels)
|
||||
{
|
||||
if (!neu.SelectedChannels.TryGetValue(pair.Key, out var nv)) return true;
|
||||
if (!pair.Value.Equals(nv)) return true;
|
||||
if (!neu.SelectedChannels.TryGetValue(pair.Key, out var nv))
|
||||
return true;
|
||||
if (!pair.Value.Equals(nv))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,16 +18,40 @@ internal sealed class SettingsOverview
|
||||
// + Export + DB-Viewer + Advanced.
|
||||
private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs =
|
||||
[
|
||||
(FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"),
|
||||
(FontAwesomeIcon.Palette, "Settings_Card_ThemeAndLayout_Title", "Settings_Card_ThemeAndLayout_Subtext"),
|
||||
(FontAwesomeIcon.Font, "Settings_Card_FontsAndColours_Title", "Settings_Card_FontsAndColours_Subtext"),
|
||||
(FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"),
|
||||
(FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"),
|
||||
(FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"),
|
||||
(FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"),
|
||||
(FontAwesomeIcon.Database, "Settings_Card_DataManagement_Title", "Settings_Card_DataManagement_Subtext"),
|
||||
(FontAwesomeIcon.Plug, "Settings_Card_Integrations_Title", "Settings_Card_Integrations_Subtext"),
|
||||
(FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"),
|
||||
(FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"),
|
||||
(
|
||||
FontAwesomeIcon.Palette,
|
||||
"Settings_Card_ThemeAndLayout_Title",
|
||||
"Settings_Card_ThemeAndLayout_Subtext"
|
||||
),
|
||||
(
|
||||
FontAwesomeIcon.Font,
|
||||
"Settings_Card_FontsAndColours_Title",
|
||||
"Settings_Card_FontsAndColours_Subtext"
|
||||
),
|
||||
(
|
||||
FontAwesomeIcon.WindowMaximize,
|
||||
"Settings_Card_Window_Title",
|
||||
"Settings_Card_Window_Subtext"
|
||||
),
|
||||
(FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"),
|
||||
(FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"),
|
||||
(FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"),
|
||||
(
|
||||
FontAwesomeIcon.Database,
|
||||
"Settings_Card_DataManagement_Title",
|
||||
"Settings_Card_DataManagement_Subtext"
|
||||
),
|
||||
(
|
||||
FontAwesomeIcon.Plug,
|
||||
"Settings_Card_Integrations_Title",
|
||||
"Settings_Card_Integrations_Subtext"
|
||||
),
|
||||
(
|
||||
FontAwesomeIcon.InfoCircle,
|
||||
"Settings_Card_Information_Title",
|
||||
"Settings_Card_Information_Subtext"
|
||||
),
|
||||
];
|
||||
|
||||
public SettingsOverview(SettingsWindow window)
|
||||
@@ -57,7 +81,14 @@ internal sealed class SettingsOverview
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCard(int index, FontAwesomeIcon icon, string title, string subtext, float w, float h)
|
||||
private void DrawCard(
|
||||
int index,
|
||||
FontAwesomeIcon icon,
|
||||
string title,
|
||||
string subtext,
|
||||
float w,
|
||||
float h
|
||||
)
|
||||
{
|
||||
// BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item.
|
||||
// Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die
|
||||
@@ -94,7 +125,14 @@ internal sealed class SettingsOverview
|
||||
// erweitern und das SameLine-Wrapping in der Card-Reihe brechen, daher
|
||||
// bleibt der Subtext bewusst beim DrawList-Overlay-Pattern.
|
||||
var subtextWrapWidth = w - 32f;
|
||||
draw.AddText(ImGui.GetFont(), ImGui.GetFontSize(), subtextPos, subtextColor, subtext, subtextWrapWidth);
|
||||
draw.AddText(
|
||||
ImGui.GetFont(),
|
||||
ImGui.GetFontSize(),
|
||||
subtextPos,
|
||||
subtextColor,
|
||||
subtext,
|
||||
subtextWrapWidth
|
||||
);
|
||||
|
||||
ImGui.EndGroup();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Numerics;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.SettingsTabs;
|
||||
|
||||
@@ -21,6 +21,7 @@ internal sealed class Chat : ISettingsTab
|
||||
public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat";
|
||||
|
||||
private SearchSelector.SelectorPopupOptions WordPopupOptions;
|
||||
|
||||
// Snapshot of EmoteCache.State for which we last built WordPopupOptions.
|
||||
// Without this, an empty FilteredSheet (e.g., the user blocked every emote)
|
||||
// would trigger a refill every frame the settings tab is open.
|
||||
@@ -39,7 +40,9 @@ internal sealed class Chat : ISettingsTab
|
||||
{
|
||||
return new SearchSelector.SelectorPopupOptions
|
||||
{
|
||||
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
|
||||
FilteredSheet = EmoteCache
|
||||
.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w))
|
||||
.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +67,10 @@ internal sealed class Chat : ISettingsTab
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Enable_Name, ref Mutable.EnableAutoTellTabs);
|
||||
ImGui.Checkbox(
|
||||
HellionStrings.ChatLog_AutoTellTabs_Enable_Name,
|
||||
ref Mutable.EnableAutoTellTabs
|
||||
);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Enable_Description);
|
||||
|
||||
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
|
||||
@@ -75,13 +81,22 @@ internal sealed class Chat : ISettingsTab
|
||||
}
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Limit_Description);
|
||||
|
||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay);
|
||||
ImGui.Checkbox(
|
||||
HellionStrings.ChatLog_AutoTellTabs_Compact_Name,
|
||||
ref Mutable.AutoTellTabsCompactDisplay
|
||||
);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description);
|
||||
|
||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Name, ref Mutable.AutoTellTabsOpenAsPopout);
|
||||
ImGui.Checkbox(
|
||||
HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Name,
|
||||
ref Mutable.AutoTellTabsOpenAsPopout
|
||||
);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Description);
|
||||
|
||||
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle);
|
||||
ImGui.Checkbox(
|
||||
HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name,
|
||||
ref Mutable.AutoTellTabsShowGreetedToggle
|
||||
);
|
||||
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
|
||||
|
||||
ImGui.Spacing();
|
||||
@@ -96,7 +111,14 @@ internal sealed class Chat : ISettingsTab
|
||||
|
||||
var preload = Mutable.AutoTellTabsHistoryPreload;
|
||||
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.SliderInt(HellionStrings.Privacy_AutoTellTabs_Preload_Name, ref preload, 0, 100))
|
||||
if (
|
||||
ImGui.SliderInt(
|
||||
HellionStrings.Privacy_AutoTellTabs_Preload_Name,
|
||||
ref preload,
|
||||
0,
|
||||
100
|
||||
)
|
||||
)
|
||||
{
|
||||
Mutable.AutoTellTabsHistoryPreload = preload;
|
||||
}
|
||||
@@ -117,12 +139,18 @@ internal sealed class Chat : ISettingsTab
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_CollapseDuplicateMessages_Name, ref Mutable.CollapseDuplicateMessages);
|
||||
ImGui.Checkbox(
|
||||
Language.Options_CollapseDuplicateMessages_Name,
|
||||
ref Mutable.CollapseDuplicateMessages
|
||||
);
|
||||
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMessages_Description);
|
||||
|
||||
if (Mutable.CollapseDuplicateMessages)
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_CollapseDuplicateMsgUniqueLink_Name, ref Mutable.CollapseKeepUniqueLinks);
|
||||
ImGui.Checkbox(
|
||||
Language.Options_CollapseDuplicateMsgUniqueLink_Name,
|
||||
ref Mutable.CollapseKeepUniqueLinks
|
||||
);
|
||||
ImGuiUtil.HelpMarker(Language.Options_CollapseDuplicateMsgUniqueLink_Description);
|
||||
}
|
||||
}
|
||||
@@ -138,7 +166,12 @@ internal sealed class Chat : ISettingsTab
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Preview_Name, Mutable.PreviewPosition.Name()))
|
||||
using (
|
||||
var combo = ImGuiUtil.BeginComboVertical(
|
||||
Language.Options_Preview_Name,
|
||||
Mutable.PreviewPosition.Name()
|
||||
)
|
||||
)
|
||||
{
|
||||
if (combo)
|
||||
{
|
||||
@@ -153,7 +186,13 @@ internal sealed class Chat : ISettingsTab
|
||||
}
|
||||
ImGuiUtil.HelpMarker(Language.Options_Preview_Description);
|
||||
|
||||
if (ImGuiUtil.InputIntVertical(Language.Options_PreviewMinimum_Name, Language.Options_PreviewMinimum_Description, ref Mutable.PreviewMinimum))
|
||||
if (
|
||||
ImGuiUtil.InputIntVertical(
|
||||
Language.Options_PreviewMinimum_Name,
|
||||
Language.Options_PreviewMinimum_Description,
|
||||
ref Mutable.PreviewMinimum
|
||||
)
|
||||
)
|
||||
{
|
||||
Mutable.PreviewMinimum = Math.Clamp(Mutable.PreviewMinimum, 1, 250);
|
||||
}
|
||||
@@ -180,9 +219,11 @@ internal sealed class Chat : ISettingsTab
|
||||
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (EmoteCache.State is EmoteCache.LoadingState.Done
|
||||
if (
|
||||
EmoteCache.State is EmoteCache.LoadingState.Done
|
||||
&& WordPopupOptions.FilteredSheet.Length == 0
|
||||
&& WordPopupOptionsBuiltFor != EmoteCache.LoadingState.Done)
|
||||
&& WordPopupOptionsBuiltFor != EmoteCache.LoadingState.Done
|
||||
)
|
||||
{
|
||||
WordPopupOptions = RefillSheet();
|
||||
WordPopupOptionsBuiltFor = EmoteCache.LoadingState.Done;
|
||||
@@ -204,7 +245,13 @@ internal sealed class Chat : ISettingsTab
|
||||
Mutable.BlockedEmotes.Add(newWord);
|
||||
}
|
||||
|
||||
using (var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
|
||||
using (
|
||||
var table = ImRaii.Table(
|
||||
"##BlockedWords",
|
||||
2,
|
||||
ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner
|
||||
)
|
||||
)
|
||||
{
|
||||
if (table)
|
||||
{
|
||||
@@ -220,7 +267,13 @@ internal sealed class Chat : ISettingsTab
|
||||
ImGui.TextUnformatted(word);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl))
|
||||
if (
|
||||
ImGuiUtil.Button(
|
||||
$"##{word}Del",
|
||||
FontAwesomeIcon.Trash,
|
||||
!ImGui.GetIO().KeyCtrl
|
||||
)
|
||||
)
|
||||
{
|
||||
Mutable.BlockedEmotes.Remove(word);
|
||||
}
|
||||
@@ -244,8 +297,16 @@ internal sealed class Chat : ISettingsTab
|
||||
ImGui.TextColored(ImGuiColors.DPSRed, Language.Options_Emote_NotReady);
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted($"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}");
|
||||
using (var emoteTable = ImRaii.Table("##LoadedEmotes", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
|
||||
ImGui.TextUnformatted(
|
||||
$"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}"
|
||||
);
|
||||
using (
|
||||
var emoteTable = ImRaii.Table(
|
||||
"##LoadedEmotes",
|
||||
5,
|
||||
ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner
|
||||
)
|
||||
)
|
||||
{
|
||||
if (emoteTable)
|
||||
{
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using System.Diagnostics;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Export;
|
||||
using HellionChat.Privacy;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Export;
|
||||
using HellionChat.Privacy;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Ui.SettingsTabs;
|
||||
|
||||
@@ -19,7 +19,8 @@ internal sealed class DataManagement : ISettingsTab
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
|
||||
public string Name => HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement";
|
||||
public string Name =>
|
||||
HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement";
|
||||
|
||||
// Cleanup state (was in Privacy.cs)
|
||||
private Dictionary<int, long>? CleanupCounts;
|
||||
@@ -49,28 +50,90 @@ internal sealed class DataManagement : ISettingsTab
|
||||
// call updates the labels immediately. 1:1 from Privacy.cs Groups.
|
||||
private static readonly (Func<string> Heading, ChatType[] Types)[] Groups =
|
||||
[
|
||||
(() => HellionStrings.Privacy_Group_DirectMessages, [ChatType.TellIncoming, ChatType.TellOutgoing]),
|
||||
(() => HellionStrings.Privacy_Group_PartyAlliance, [ChatType.Party, ChatType.CrossParty, ChatType.Alliance, ChatType.PvpTeam]),
|
||||
(() => HellionStrings.Privacy_Group_FreeCompany, [ChatType.FreeCompany, ChatType.FreeCompanyAnnouncement, ChatType.FreeCompanyLoginLogout]),
|
||||
(() => HellionStrings.Privacy_Group_Linkshells, [
|
||||
ChatType.Linkshell1, ChatType.Linkshell2, ChatType.Linkshell3, ChatType.Linkshell4,
|
||||
ChatType.Linkshell5, ChatType.Linkshell6, ChatType.Linkshell7, ChatType.Linkshell8,
|
||||
]),
|
||||
(() => HellionStrings.Privacy_Group_CrossLinkshells, [
|
||||
ChatType.CrossLinkshell1, ChatType.CrossLinkshell2, ChatType.CrossLinkshell3, ChatType.CrossLinkshell4,
|
||||
ChatType.CrossLinkshell5, ChatType.CrossLinkshell6, ChatType.CrossLinkshell7, ChatType.CrossLinkshell8,
|
||||
]),
|
||||
(() => HellionStrings.Privacy_Group_ExtraChat, [
|
||||
ChatType.ExtraChatLinkshell1, ChatType.ExtraChatLinkshell2, ChatType.ExtraChatLinkshell3, ChatType.ExtraChatLinkshell4,
|
||||
ChatType.ExtraChatLinkshell5, ChatType.ExtraChatLinkshell6, ChatType.ExtraChatLinkshell7, ChatType.ExtraChatLinkshell8,
|
||||
]),
|
||||
(() => HellionStrings.Privacy_Group_PublicChat, [ChatType.Say, ChatType.Shout, ChatType.Yell, ChatType.NoviceNetwork, ChatType.CustomEmote, ChatType.StandardEmote]),
|
||||
(() => HellionStrings.Privacy_Group_SystemLogs, [
|
||||
ChatType.System, ChatType.Notice, ChatType.Urgent, ChatType.Echo,
|
||||
ChatType.NpcDialogue, ChatType.NpcAnnouncement,
|
||||
ChatType.LootNotice, ChatType.LootRoll, ChatType.RetainerSale,
|
||||
ChatType.Crafting, ChatType.Gathering, ChatType.Sign, ChatType.RandomNumber,
|
||||
]),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_DirectMessages,
|
||||
[ChatType.TellIncoming, ChatType.TellOutgoing]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_PartyAlliance,
|
||||
[ChatType.Party, ChatType.CrossParty, ChatType.Alliance, ChatType.PvpTeam]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_FreeCompany,
|
||||
[
|
||||
ChatType.FreeCompany,
|
||||
ChatType.FreeCompanyAnnouncement,
|
||||
ChatType.FreeCompanyLoginLogout,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_Linkshells,
|
||||
[
|
||||
ChatType.Linkshell1,
|
||||
ChatType.Linkshell2,
|
||||
ChatType.Linkshell3,
|
||||
ChatType.Linkshell4,
|
||||
ChatType.Linkshell5,
|
||||
ChatType.Linkshell6,
|
||||
ChatType.Linkshell7,
|
||||
ChatType.Linkshell8,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_CrossLinkshells,
|
||||
[
|
||||
ChatType.CrossLinkshell1,
|
||||
ChatType.CrossLinkshell2,
|
||||
ChatType.CrossLinkshell3,
|
||||
ChatType.CrossLinkshell4,
|
||||
ChatType.CrossLinkshell5,
|
||||
ChatType.CrossLinkshell6,
|
||||
ChatType.CrossLinkshell7,
|
||||
ChatType.CrossLinkshell8,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_ExtraChat,
|
||||
[
|
||||
ChatType.ExtraChatLinkshell1,
|
||||
ChatType.ExtraChatLinkshell2,
|
||||
ChatType.ExtraChatLinkshell3,
|
||||
ChatType.ExtraChatLinkshell4,
|
||||
ChatType.ExtraChatLinkshell5,
|
||||
ChatType.ExtraChatLinkshell6,
|
||||
ChatType.ExtraChatLinkshell7,
|
||||
ChatType.ExtraChatLinkshell8,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_PublicChat,
|
||||
[
|
||||
ChatType.Say,
|
||||
ChatType.Shout,
|
||||
ChatType.Yell,
|
||||
ChatType.NoviceNetwork,
|
||||
ChatType.CustomEmote,
|
||||
ChatType.StandardEmote,
|
||||
]
|
||||
),
|
||||
(
|
||||
() => HellionStrings.Privacy_Group_SystemLogs,
|
||||
[
|
||||
ChatType.System,
|
||||
ChatType.Notice,
|
||||
ChatType.Urgent,
|
||||
ChatType.Echo,
|
||||
ChatType.NpcDialogue,
|
||||
ChatType.NpcAnnouncement,
|
||||
ChatType.LootNotice,
|
||||
ChatType.LootRoll,
|
||||
ChatType.RetainerSale,
|
||||
ChatType.Crafting,
|
||||
ChatType.Gathering,
|
||||
ChatType.Sign,
|
||||
ChatType.RandomNumber,
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
internal DataManagement(Plugin plugin, Configuration mutable)
|
||||
@@ -107,21 +170,36 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
{
|
||||
ImGui.Checkbox(Language.Options_DatabaseBattleMessages_Name, ref Mutable.DatabaseBattleMessages);
|
||||
ImGui.Checkbox(
|
||||
Language.Options_DatabaseBattleMessages_Name,
|
||||
ref Mutable.DatabaseBattleMessages
|
||||
);
|
||||
ImGuiUtil.HelpMarker(Language.Options_DatabaseBattleMessages_Description);
|
||||
|
||||
if (ImGui.Checkbox(Language.Options_LoadPreviousSession_Name, ref Mutable.LoadPreviousSession))
|
||||
if (
|
||||
ImGui.Checkbox(
|
||||
Language.Options_LoadPreviousSession_Name,
|
||||
ref Mutable.LoadPreviousSession
|
||||
)
|
||||
)
|
||||
if (Mutable.LoadPreviousSession)
|
||||
Mutable.FilterIncludePreviousSessions = true;
|
||||
ImGuiUtil.HelpMarker(Language.Options_LoadPreviousSession_Description);
|
||||
|
||||
if (ImGui.Checkbox(Language.Options_FilterIncludePreviousSessions_Name, ref Mutable.FilterIncludePreviousSessions))
|
||||
if (
|
||||
ImGui.Checkbox(
|
||||
Language.Options_FilterIncludePreviousSessions_Name,
|
||||
ref Mutable.FilterIncludePreviousSessions
|
||||
)
|
||||
)
|
||||
if (!Mutable.FilterIncludePreviousSessions)
|
||||
Mutable.LoadPreviousSession = false;
|
||||
ImGuiUtil.HelpMarker(Language.Options_FilterIncludePreviousSessions_Description);
|
||||
|
||||
var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db"));
|
||||
var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db"));
|
||||
var migratedOld = new FileInfo(
|
||||
Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db")
|
||||
);
|
||||
if (old.Exists || migratedOld.Exists)
|
||||
{
|
||||
ImGui.Spacing();
|
||||
@@ -131,7 +209,12 @@ internal sealed class DataManagement : ISettingsTab
|
||||
ImGui.TextUnformatted(Language.Options_Database_Old_Heading);
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGuiUtil.CtrlShiftButton(Language.Options_Database_Old_Delete, Language.Options_Database_Old_Delete_Tooltip))
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
Language.Options_Database_Old_Delete,
|
||||
Language.Options_Database_Old_Delete_Tooltip
|
||||
)
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -139,12 +222,18 @@ internal sealed class DataManagement : ISettingsTab
|
||||
old.Delete();
|
||||
if (migratedOld.Exists)
|
||||
migratedOld.Delete();
|
||||
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success);
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Old_Delete_Success,
|
||||
NotificationType.Success
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Unable to delete old database");
|
||||
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error);
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Old_Delete_Error,
|
||||
NotificationType.Error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +251,8 @@ internal sealed class DataManagement : ISettingsTab
|
||||
ImGuiUtil.OptionCheckbox(
|
||||
ref Mutable.RetentionEnabled,
|
||||
HellionStrings.Retention_Enabled_Name,
|
||||
HellionStrings.Retention_Enabled_Description);
|
||||
HellionStrings.Retention_Enabled_Description
|
||||
);
|
||||
|
||||
using (ImRaii.Disabled(!Mutable.RetentionEnabled))
|
||||
{
|
||||
@@ -191,39 +281,57 @@ internal sealed class DataManagement : ISettingsTab
|
||||
if (perChannelTree.Success)
|
||||
{
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
foreach (var (heading, types) in Groups)
|
||||
{
|
||||
using var subTree = ImRaii.TreeNode(heading());
|
||||
if (!subTree.Success)
|
||||
continue;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
foreach (var type in types)
|
||||
foreach (var (heading, types) in Groups)
|
||||
{
|
||||
var hasOverride = Mutable.RetentionPerChannelDays.TryGetValue(type, out var days);
|
||||
var hasSpecDefault = PrivacyDefaults.DefaultRetentionDays.TryGetValue(type, out var specDays);
|
||||
if (!hasOverride)
|
||||
days = hasSpecDefault ? specDays : Mutable.RetentionDefaultDays;
|
||||
using var subTree = ImRaii.TreeNode(heading());
|
||||
if (!subTree.Success)
|
||||
continue;
|
||||
|
||||
var tag = hasOverride
|
||||
? HellionStrings.Retention_Tag_Override
|
||||
: hasSpecDefault
|
||||
? HellionStrings.Retention_Tag_Spec
|
||||
: HellionStrings.Retention_Tag_Global;
|
||||
if (ImGui.InputInt($"{type} {tag}##retention-{(int)type}", ref days))
|
||||
{
|
||||
days = Math.Max(0, days);
|
||||
Mutable.RetentionPerChannelDays[type] = days;
|
||||
}
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
foreach (var type in types)
|
||||
{
|
||||
var hasOverride =
|
||||
Mutable.RetentionPerChannelDays.TryGetValue(
|
||||
type,
|
||||
out var days
|
||||
);
|
||||
var hasSpecDefault =
|
||||
PrivacyDefaults.DefaultRetentionDays.TryGetValue(
|
||||
type,
|
||||
out var specDays
|
||||
);
|
||||
if (!hasOverride)
|
||||
days = hasSpecDefault
|
||||
? specDays
|
||||
: Mutable.RetentionDefaultDays;
|
||||
|
||||
if (hasOverride)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button($"{HellionStrings.Retention_Reset_Button}##retention-reset-{(int)type}"))
|
||||
Mutable.RetentionPerChannelDays.Remove(type);
|
||||
}
|
||||
var tag =
|
||||
hasOverride ? HellionStrings.Retention_Tag_Override
|
||||
: hasSpecDefault ? HellionStrings.Retention_Tag_Spec
|
||||
: HellionStrings.Retention_Tag_Global;
|
||||
if (
|
||||
ImGui.InputInt(
|
||||
$"{type} {tag}##retention-{(int)type}",
|
||||
ref days
|
||||
)
|
||||
)
|
||||
{
|
||||
days = Math.Max(0, days);
|
||||
Mutable.RetentionPerChannelDays[type] = days;
|
||||
}
|
||||
|
||||
if (hasOverride)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if (
|
||||
ImGui.Button(
|
||||
$"{HellionStrings.Retention_Reset_Button}##retention-reset-{(int)type}"
|
||||
)
|
||||
)
|
||||
Mutable.RetentionPerChannelDays.Remove(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +342,12 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
using (ImRaii.Disabled(RetentionRunning))
|
||||
{
|
||||
if (ImGuiUtil.CtrlShiftButton(HellionStrings.Retention_Apply_Label, HellionStrings.Retention_Apply_Tooltip))
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
HellionStrings.Retention_Apply_Label,
|
||||
HellionStrings.Retention_Apply_Tooltip
|
||||
)
|
||||
)
|
||||
StartRetentionRun();
|
||||
}
|
||||
|
||||
@@ -243,9 +356,11 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
ImGui.Spacing();
|
||||
var lastRun = Plugin.Config.RetentionLastRunAt;
|
||||
ImGuiUtil.HelpText(lastRun == DateTimeOffset.MinValue
|
||||
? HellionStrings.Retention_LastRun_Never
|
||||
: string.Format(HellionStrings.Retention_LastRun_At, lastRun.ToLocalTime()));
|
||||
ImGuiUtil.HelpText(
|
||||
lastRun == DateTimeOffset.MinValue
|
||||
? HellionStrings.Retention_LastRun_Never
|
||||
: string.Format(HellionStrings.Retention_LastRun_At, lastRun.ToLocalTime())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,14 +374,20 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.RetentionSweepRunning = true;
|
||||
}
|
||||
|
||||
var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(p => (int)(ushort)p.Key, p => p.Value);
|
||||
var policy = Plugin.Config.RetentionPerChannelDays.ToDictionary(
|
||||
p => (int)(ushort)p.Key,
|
||||
p => p.Value
|
||||
);
|
||||
var defaultDays = Plugin.Config.RetentionDefaultDays;
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = Plugin.MessageManager.Store.DeleteByRetentionPolicy(policy, defaultDays);
|
||||
var deleted = Plugin.MessageManager.Store.DeleteByRetentionPolicy(
|
||||
policy,
|
||||
defaultDays
|
||||
);
|
||||
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||
Plugin.SaveConfig();
|
||||
|
||||
@@ -274,17 +395,26 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
if (!Plugin.Framework.Run(() =>
|
||||
{
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
Plugin.MessageManager.FilterAllTabsAsync();
|
||||
}).Wait(TimeSpan.FromSeconds(5)))
|
||||
if (
|
||||
!Plugin
|
||||
.Framework.Run(() =>
|
||||
{
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
Plugin.MessageManager.FilterAllTabsAsync();
|
||||
})
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Retention sweep: framework refresh timed out after 5s.");
|
||||
Plugin.Log.Warning(
|
||||
"Retention sweep: framework refresh timed out after 5s."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WrapperUtil.AddNotification(string.Format(HellionStrings.Retention_Success, deleted), NotificationType.Success);
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(HellionStrings.Retention_Success, deleted),
|
||||
NotificationType.Success
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -296,7 +426,10 @@ internal sealed class DataManagement : ISettingsTab
|
||||
lock (Plugin.RetentionSweepLock)
|
||||
Plugin.RetentionSweepRunning = false;
|
||||
}
|
||||
}) { IsBackground = true }.Start();
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
}.Start();
|
||||
}
|
||||
|
||||
private void DrawCleanupSection()
|
||||
@@ -312,15 +445,19 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
if (CleanupPreviewSnapshot is not null
|
||||
&& !CleanupPreviewSnapshot.SetEquals(Mutable.PrivacyPersistChannels))
|
||||
if (
|
||||
CleanupPreviewSnapshot is not null
|
||||
&& !CleanupPreviewSnapshot.SetEquals(Mutable.PrivacyPersistChannels)
|
||||
)
|
||||
{
|
||||
CleanupPreviewStale = true;
|
||||
}
|
||||
|
||||
using (var emphasis = CleanupPreviewStale
|
||||
? ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.6f })
|
||||
: null)
|
||||
using (
|
||||
var emphasis = CleanupPreviewStale
|
||||
? ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.6f })
|
||||
: null
|
||||
)
|
||||
using (ImRaii.Disabled(CleanupRunning))
|
||||
{
|
||||
if (ImGui.Button(HellionStrings.Cleanup_RefreshPreview))
|
||||
@@ -341,13 +478,24 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
ImGui.Spacing();
|
||||
|
||||
using (var staleColor = CleanupPreviewStale
|
||||
? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)
|
||||
: null)
|
||||
using (
|
||||
var staleColor = CleanupPreviewStale
|
||||
? ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)
|
||||
: null
|
||||
)
|
||||
{
|
||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_TotalStored, CleanupKeepCount + CleanupDeleteCount));
|
||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount));
|
||||
ImGuiUtil.HelpText(string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount));
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(
|
||||
HellionStrings.Cleanup_TotalStored,
|
||||
CleanupKeepCount + CleanupDeleteCount
|
||||
)
|
||||
);
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(HellionStrings.Cleanup_WillKeep, CleanupKeepCount)
|
||||
);
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(HellionStrings.Cleanup_WillDelete, CleanupDeleteCount)
|
||||
);
|
||||
}
|
||||
|
||||
using (var breakdownTree = ImRaii.TreeNode(HellionStrings.Cleanup_Breakdown))
|
||||
@@ -355,15 +503,19 @@ internal sealed class DataManagement : ISettingsTab
|
||||
if (breakdownTree.Success)
|
||||
{
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
foreach (var (chatType, count) in CleanupCounts.OrderByDescending(p => p.Value))
|
||||
{
|
||||
var name = Enum.IsDefined(typeof(ChatType), (ushort)chatType)
|
||||
? ((ChatType)(ushort)chatType).ToString()
|
||||
: $"Unknown({chatType})";
|
||||
var keeps = WouldBeKept(chatType);
|
||||
var marker = keeps ? HellionStrings.Cleanup_Marker_Keep : HellionStrings.Cleanup_Marker_Delete;
|
||||
ImGuiUtil.HelpText($"{marker} {name} — {count:N0}");
|
||||
}
|
||||
foreach (
|
||||
var (chatType, count) in CleanupCounts.OrderByDescending(p => p.Value)
|
||||
)
|
||||
{
|
||||
var name = Enum.IsDefined(typeof(ChatType), (ushort)chatType)
|
||||
? ((ChatType)(ushort)chatType).ToString()
|
||||
: $"Unknown({chatType})";
|
||||
var keeps = WouldBeKept(chatType);
|
||||
var marker = keeps
|
||||
? HellionStrings.Cleanup_Marker_Keep
|
||||
: HellionStrings.Cleanup_Marker_Delete;
|
||||
ImGuiUtil.HelpText($"{marker} {name} — {count:N0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,8 +523,12 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
using (ImRaii.Disabled(CleanupRunning || CleanupDeleteCount == 0))
|
||||
{
|
||||
if (ImGuiUtil.CtrlShiftButton(HellionStrings.Cleanup_Apply_Label,
|
||||
string.Format(HellionStrings.Cleanup_Apply_Tooltip, CleanupDeleteCount)))
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
HellionStrings.Cleanup_Apply_Label,
|
||||
string.Format(HellionStrings.Cleanup_Apply_Tooltip, CleanupDeleteCount)
|
||||
)
|
||||
)
|
||||
StartCleanup();
|
||||
}
|
||||
|
||||
@@ -411,7 +567,10 @@ internal sealed class DataManagement : ISettingsTab
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Failed to compute cleanup preview");
|
||||
WrapperUtil.AddNotification(HellionStrings.Cleanup_PreviewError, NotificationType.Error);
|
||||
WrapperUtil.AddNotification(
|
||||
HellionStrings.Cleanup_PreviewError,
|
||||
NotificationType.Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,16 +589,23 @@ internal sealed class DataManagement : ISettingsTab
|
||||
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
||||
Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages");
|
||||
|
||||
if (!Plugin.Framework.Run(() =>
|
||||
{
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
Plugin.MessageManager.FilterAllTabs();
|
||||
}).Wait(TimeSpan.FromSeconds(5)))
|
||||
if (
|
||||
!Plugin
|
||||
.Framework.Run(() =>
|
||||
{
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
Plugin.MessageManager.FilterAllTabs();
|
||||
})
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||
}
|
||||
|
||||
WrapperUtil.AddNotification(string.Format(HellionStrings.Cleanup_Success, deleted), NotificationType.Success);
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(HellionStrings.Cleanup_Success, deleted),
|
||||
NotificationType.Success
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -482,22 +648,24 @@ internal sealed class DataManagement : ISettingsTab
|
||||
ImGuiUtil.HelpText(HellionStrings.Export_Channels_AllOff);
|
||||
foreach (var (heading, types) in Groups)
|
||||
{
|
||||
using var subTree = ImRaii.TreeNode($"{heading()}##export-group-{heading()}");
|
||||
using var subTree = ImRaii.TreeNode(
|
||||
$"{heading()}##export-group-{heading()}"
|
||||
);
|
||||
if (!subTree.Success)
|
||||
continue;
|
||||
|
||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||
foreach (var type in types)
|
||||
{
|
||||
var enabled = ExportSelectedChannels.Contains(type);
|
||||
if (ImGui.Checkbox($"{type}##export-{(int)type}", ref enabled))
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (enabled)
|
||||
ExportSelectedChannels.Add(type);
|
||||
else
|
||||
ExportSelectedChannels.Remove(type);
|
||||
var enabled = ExportSelectedChannels.Contains(type);
|
||||
if (ImGui.Checkbox($"{type}##export-{(int)type}", ref enabled))
|
||||
{
|
||||
if (enabled)
|
||||
ExportSelectedChannels.Add(type);
|
||||
else
|
||||
ExportSelectedChannels.Remove(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -507,10 +675,22 @@ internal sealed class DataManagement : ISettingsTab
|
||||
ImGui.TextUnformatted(HellionStrings.Export_Format_Label);
|
||||
ImGui.SameLine();
|
||||
var fmt = (int)ExportFormat;
|
||||
if (ImGui.RadioButton(HellionStrings.Export_Format_Markdown, ref fmt, (int)ExportFormat.Markdown))
|
||||
if (
|
||||
ImGui.RadioButton(
|
||||
HellionStrings.Export_Format_Markdown,
|
||||
ref fmt,
|
||||
(int)ExportFormat.Markdown
|
||||
)
|
||||
)
|
||||
ExportFormat = ExportFormat.Markdown;
|
||||
ImGui.SameLine();
|
||||
if (ImGui.RadioButton(HellionStrings.Export_Format_Json, ref fmt, (int)ExportFormat.Json))
|
||||
if (
|
||||
ImGui.RadioButton(
|
||||
HellionStrings.Export_Format_Json,
|
||||
ref fmt,
|
||||
(int)ExportFormat.Json
|
||||
)
|
||||
)
|
||||
ExportFormat = ExportFormat.Json;
|
||||
ImGui.SameLine();
|
||||
if (ImGui.RadioButton(HellionStrings.Export_Format_Csv, ref fmt, (int)ExportFormat.Csv))
|
||||
@@ -544,7 +724,8 @@ internal sealed class DataManagement : ISettingsTab
|
||||
if (!success || string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
StartExport(path);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void StartExport(string path)
|
||||
@@ -553,15 +734,17 @@ internal sealed class DataManagement : ISettingsTab
|
||||
return;
|
||||
ExportRunning = true;
|
||||
|
||||
var types = ExportSelectedChannels.Count > 0
|
||||
? ExportSelectedChannels.Select(t => (int)(ushort)t).ToList()
|
||||
: null;
|
||||
var types =
|
||||
ExportSelectedChannels.Count > 0
|
||||
? ExportSelectedChannels.Select(t => (int)(ushort)t).ToList()
|
||||
: null;
|
||||
|
||||
DateTimeOffset? from = ExportRangeDays > 0
|
||||
? DateTimeOffset.UtcNow.AddDays(-ExportRangeDays)
|
||||
: null;
|
||||
DateTimeOffset? from =
|
||||
ExportRangeDays > 0 ? DateTimeOffset.UtcNow.AddDays(-ExportRangeDays) : null;
|
||||
|
||||
var senderSubstring = string.IsNullOrWhiteSpace(ExportSenderSubstring) ? null : ExportSenderSubstring.Trim();
|
||||
var senderSubstring = string.IsNullOrWhiteSpace(ExportSenderSubstring)
|
||||
? null
|
||||
: ExportSenderSubstring.Trim();
|
||||
var format = ExportFormat;
|
||||
var filterDesc = new MessageExporter.FilterDescription(types, from, null, senderSubstring);
|
||||
|
||||
@@ -569,11 +752,18 @@ internal sealed class DataManagement : ISettingsTab
|
||||
{
|
||||
try
|
||||
{
|
||||
using var enumerator = Plugin.MessageManager.Store.StreamForExport(types, from, null);
|
||||
using var enumerator = Plugin.MessageManager.Store.StreamForExport(
|
||||
types,
|
||||
from,
|
||||
null
|
||||
);
|
||||
var written = MessageExporter.ExportToFile(path, format, enumerator, filterDesc);
|
||||
|
||||
if (written > 0)
|
||||
WrapperUtil.AddNotification(string.Format(HellionStrings.Export_Success, written, path), NotificationType.Success);
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(HellionStrings.Export_Success, written, path),
|
||||
NotificationType.Success
|
||||
);
|
||||
else
|
||||
WrapperUtil.AddNotification(HellionStrings.Export_Empty, NotificationType.Info);
|
||||
}
|
||||
@@ -586,7 +776,10 @@ internal sealed class DataManagement : ISettingsTab
|
||||
{
|
||||
ExportRunning = false;
|
||||
}
|
||||
}) { IsBackground = true }.Start();
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
}.Start();
|
||||
}
|
||||
|
||||
private void DrawDatabaseViewerSection()
|
||||
@@ -605,12 +798,20 @@ internal sealed class DataManagement : ISettingsTab
|
||||
DatabaseLastRefreshTicks = Environment.TickCount64;
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Path, MessageManager.DatabasePath()));
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(
|
||||
Language.Options_Database_Metadata_Path,
|
||||
MessageManager.DatabasePath()
|
||||
)
|
||||
);
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
var path = Path.GetDirectoryName(MessageManager.DatabasePath());
|
||||
ImGui.SetClipboardText(path);
|
||||
WrapperUtil.AddNotification(Language.Options_Database_Metadata_CopyConfigPathNotification, NotificationType.Info);
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Metadata_CopyConfigPathNotification,
|
||||
NotificationType.Info
|
||||
);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
@@ -619,24 +820,44 @@ internal sealed class DataManagement : ISettingsTab
|
||||
ImGuiUtil.Tooltip(Language.Options_Database_Metadata_CopyConfigPath);
|
||||
}
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Size, StringUtil.BytesToString(DatabaseSize)));
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(
|
||||
Language.Options_Database_Metadata_Size,
|
||||
StringUtil.BytesToString(DatabaseSize)
|
||||
)
|
||||
);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseSize));
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_LogSize, StringUtil.BytesToString(DatabaseLogSize)));
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(
|
||||
Language.Options_Database_Metadata_LogSize,
|
||||
StringUtil.BytesToString(DatabaseLogSize)
|
||||
)
|
||||
);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseLogSize));
|
||||
|
||||
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount));
|
||||
ImGuiUtil.HelpText(
|
||||
string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount)
|
||||
);
|
||||
|
||||
if (ImGuiUtil.CtrlShiftButton(Language.Options_ClearDatabase_Button, Language.Options_ClearDatabase_Tooltip))
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
Language.Options_ClearDatabase_Button,
|
||||
Language.Options_ClearDatabase_Tooltip
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Clearing messages from database");
|
||||
Plugin.MessageManager.Store.ClearMessages();
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
|
||||
DatabaseLastRefreshTicks = 0;
|
||||
WrapperUtil.AddNotification(Language.Options_ClearDatabase_Success, NotificationType.Info);
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_ClearDatabase_Success,
|
||||
NotificationType.Info
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -655,16 +876,31 @@ internal sealed class DataManagement : ISettingsTab
|
||||
using var wrap = ImRaii.TextWrapPos(0.0f);
|
||||
|
||||
ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning);
|
||||
if (ImGuiUtil.CtrlShiftButton("Perform maintenance", "Ctrl+Shift: MessageManager.Store.PerformMaintenance()"))
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
"Perform maintenance",
|
||||
"Ctrl+Shift: MessageManager.Store.PerformMaintenance()"
|
||||
)
|
||||
)
|
||||
Plugin.MessageManager.Store.PerformMaintenance();
|
||||
|
||||
if (ImGuiUtil.CtrlShiftButton("Reload messages from database", "Ctrl+Shift: MessageManager.FilterAllTabs()"))
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
"Reload messages from database",
|
||||
"Ctrl+Shift: MessageManager.FilterAllTabs()"
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
Plugin.MessageManager.FilterAllTabsAsync();
|
||||
}
|
||||
|
||||
if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)"))
|
||||
if (
|
||||
ImGuiUtil.CtrlShiftButton(
|
||||
"Inject 10,000 messages",
|
||||
"Ctrl+Shift: creates 10,000 unique messages (async)"
|
||||
)
|
||||
)
|
||||
new Thread(() => InsertMessages(10_000)).Start();
|
||||
}
|
||||
}
|
||||
@@ -683,7 +919,9 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.Add(RawPayload.LinkTerminator)
|
||||
.AddText(">: ")
|
||||
.Build();
|
||||
var senderChunks = ChunkUtil.ToChunks(senderSource, ChunkSource.Sender, ChatType.Debug).ToList();
|
||||
var senderChunks = ChunkUtil
|
||||
.ToChunks(senderSource, ChunkSource.Sender, ChatType.Debug)
|
||||
.ToList();
|
||||
var messages = new List<Message>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
@@ -691,26 +929,32 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.AddText("Random message payload - ")
|
||||
.AddItalics(Guid.NewGuid().ToString())
|
||||
.Build();
|
||||
var contentChunks = ChunkUtil.ToChunks(contentSource, ChunkSource.Content, ChatType.Debug).ToList();
|
||||
var contentChunks = ChunkUtil
|
||||
.ToChunks(contentSource, ChunkSource.Content, ChatType.Debug)
|
||||
.ToList();
|
||||
|
||||
var chatCode = new ChatCode(XivChatType.Say, 0, 0);
|
||||
messages.Add(new Message(
|
||||
Guid.NewGuid(),
|
||||
Plugin.MessageManager.CurrentContentId,
|
||||
Plugin.MessageManager.CurrentContentId,
|
||||
DateTimeOffset.UtcNow,
|
||||
chatCode,
|
||||
senderChunks,
|
||||
contentChunks,
|
||||
senderSource,
|
||||
contentSource,
|
||||
Guid.Empty
|
||||
));
|
||||
messages.Add(
|
||||
new Message(
|
||||
Guid.NewGuid(),
|
||||
Plugin.MessageManager.CurrentContentId,
|
||||
Plugin.MessageManager.CurrentContentId,
|
||||
DateTimeOffset.UtcNow,
|
||||
chatCode,
|
||||
senderChunks,
|
||||
contentChunks,
|
||||
senderSource,
|
||||
contentSource,
|
||||
Guid.Empty
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info($"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
|
||||
Plugin.Log.Info(
|
||||
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
foreach (var message in messages)
|
||||
@@ -718,24 +962,34 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info($"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
|
||||
Plugin.Log.Info(
|
||||
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
Plugin.Framework.Run(() =>
|
||||
{
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info($"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
|
||||
}).Wait();
|
||||
Plugin
|
||||
.Framework.Run(() =>
|
||||
{
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
})
|
||||
.Wait();
|
||||
|
||||
Plugin.Framework.Run(() =>
|
||||
{
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
Plugin.MessageManager.FilterAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info($"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
|
||||
}).Wait();
|
||||
Plugin
|
||||
.Framework.Run(() =>
|
||||
{
|
||||
stopwatch = Stopwatch.StartNew();
|
||||
Plugin.MessageManager.FilterAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
})
|
||||
.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user