diff --git a/.gitattributes b/.gitattributes index 7dc4840..aa781c8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,8 @@ # Generated files -HellionChat/Resources/Language.*.resx linguist-generated=true \ No newline at end of file +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 \ No newline at end of file diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml index 922c9dc..838eaf7 100644 --- a/.gitea/workflows/security.yml +++ b/.gitea/workflows/security.yml @@ -1,21 +1,20 @@ name: Security on: - push: - branches: [main, master] - pull_request: - schedule: - - cron: '0 6 * * 1' - workflow_dispatch: + 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' - + 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" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 41b654d..5c9fc4a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 296f27d..132da67 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index f106809..fe74fd4 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -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 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6912be3..52c58ab 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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). diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ef49340..2fff09b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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)" diff --git a/.github/forge-posts/v1.1.0.md b/.github/forge-posts/v1.1.0.md index 533464b..888aca7 100644 --- a/.github/forge-posts/v1.1.0.md +++ b/.github/forge-posts/v1.1.0.md @@ -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` diff --git a/.github/forge-posts/v1.2.0.md b/.github/forge-posts/v1.2.0.md index 1797966..5275957 100644 --- a/.github/forge-posts/v1.2.0.md +++ b/.github/forge-posts/v1.2.0.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 diff --git a/.github/forge-posts/v1.2.1.md b/.github/forge-posts/v1.2.1.md index 7b563c8..e98f68f 100644 --- a/.github/forge-posts/v1.2.1.md +++ b/.github/forge-posts/v1.2.1.md @@ -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. diff --git a/.github/forge-posts/v1.2.3.md b/.github/forge-posts/v1.2.3.md index d1e0541..f52a523 100644 --- a/.github/forge-posts/v1.2.3.md +++ b/.github/forge-posts/v1.2.3.md @@ -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. diff --git a/.github/forge-posts/v1.3.0.md b/.github/forge-posts/v1.3.0.md index 5b24203..a64d2c9 100644 --- a/.github/forge-posts/v1.3.0.md +++ b/.github/forge-posts/v1.3.0.md @@ -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 diff --git a/.github/forge-posts/v1.4.0.md b/.github/forge-posts/v1.4.0.md index 8e91459..bad8c82 100644 --- a/.github/forge-posts/v1.4.0.md +++ b/.github/forge-posts/v1.4.0.md @@ -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. diff --git a/.github/forge-posts/v1.4.1.md b/.github/forge-posts/v1.4.1.md index 4304507..4d30828 100644 --- a/.github/forge-posts/v1.4.1.md +++ b/.github/forge-posts/v1.4.1.md @@ -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. diff --git a/.github/forge-posts/v1.4.2.md b/.github/forge-posts/v1.4.2.md index f037278..dc9f0f6 100644 --- a/.github/forge-posts/v1.4.2.md +++ b/.github/forge-posts/v1.4.2.md @@ -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. diff --git a/.github/forge-posts/v1.4.3.md b/.github/forge-posts/v1.4.3.md index da59d6a..cd3d181 100644 --- a/.github/forge-posts/v1.4.3.md +++ b/.github/forge-posts/v1.4.3.md @@ -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. diff --git a/.github/release-footer.md b/.github/release-footer.md index 5976477..f673ded 100644 --- a/.github/release-footer.md +++ b/.github/release-footer.md @@ -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. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d29a76e..4795537 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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@v6 - - name: Setup .NET 10 - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x + - name: Setup .NET 10 + uses: actions/setup-dotnet@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 diff --git a/.github/workflows/forge-announce.yml b/.github/workflows/forge-announce.yml index ff0f20f..af87077 100644 --- a/.github/workflows/forge-announce.yml +++ b/.github/workflows/forge-announce.yml @@ -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" + } + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index edd8855..3a8ff11 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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@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@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 }} diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..8f989cf --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,7 @@ +{ + "MD007": { "indent": 4 }, + "MD013": false, + "MD029": false, + "MD033": false, + "MD041": false +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..19bce32 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +bin/ +obj/ +node_modules/ +*.Designer.cs \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..42bc65f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "proseWrap": "always", + "singleQuote": false, + "endOfLine": "lf" +} diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000..d93399f --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,8 @@ +extends: default +rules: + line-length: disable + document-start: disable + truthy: + allowed-values: ["true", "false", "on"] + empty-lines: + max: 1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 1c9ebbb..ea90fb5 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -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/). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ba0500..8333b20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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..resx` (per-language). These are accepted as -direct pull requests. +Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx` (English source) and +`HellionStrings..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. diff --git a/COPYRIGHT b/COPYRIGHT index 4a43297..7759702 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -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: 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. ═══════════════════════════════════════════════════════════════════ diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index 4d16d27..7ee6456 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -1,12 +1,12 @@ 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; @@ -76,7 +76,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; } @@ -89,10 +92,11 @@ internal sealed class AutoTellTabsService : IDisposable // (FFXIV changing tell payload shape, new edge cases) findable // without having to crank up debug logging at the source. 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; } @@ -124,8 +128,9 @@ internal sealed class AutoTellTabsService : IDisposable // 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); + var fromSender = + ChunkUtil.TryGetPlayerPayload(message.Sender) + ?? ChunkUtil.TryGetPlayerPayload(message.SenderSource); if (fromSender != null) { return (fromSender.PlayerName, fromSender.World.RowId); @@ -137,17 +142,19 @@ internal sealed class AutoTellTabsService : IDisposable // 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); + 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,7 +169,8 @@ 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() @@ -171,8 +179,8 @@ internal sealed class AutoTellTabsService : IDisposable // "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)) + 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) @@ -191,8 +199,9 @@ internal sealed class AutoTellTabsService : IDisposable // popped tab is now a routine code path. 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; @@ -286,7 +295,8 @@ internal sealed class AutoTellTabsService : IDisposable _messageManager.CurrentContentId, senderName, senderWorld, - preloadCount + 1); + preloadCount + 1 + ); var historicMessages = history .Where(m => m.Id != currentMessageId) @@ -314,7 +324,8 @@ internal sealed class AutoTellTabsService : IDisposable // after the historical messages but before the current one. tab.Messages.AddPrune( MakeSystemMarker(HellionStrings.AutoTellTabs_HistorySeparator), - MessageManager.MessageDisplayLimit); + MessageManager.MessageDisplayLimit + ); } catch (Exception ex) { @@ -324,7 +335,8 @@ internal sealed class AutoTellTabsService : IDisposable Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed"); tab.Messages.AddPrune( MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError), - MessageManager.MessageDisplayLimit); + MessageManager.MessageDisplayLimit + ); } } @@ -388,16 +400,18 @@ internal sealed class AutoTellTabsService : IDisposable // 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) + 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; } diff --git a/HellionChat/ChatTwoConflictDetector.cs b/HellionChat/ChatTwoConflictDetector.cs index ad53edb..357f1d4 100644 --- a/HellionChat/ChatTwoConflictDetector.cs +++ b/HellionChat/ChatTwoConflictDetector.cs @@ -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); } diff --git a/HellionChat/Chunk.cs b/HellionChat/Chunk.cs index e155ed1..9f943d0 100755 --- a/HellionChat/Chunk.cs +++ b/HellionChat/Chunk.cs @@ -1,5 +1,5 @@ -using HellionChat.Code; using Dalamud.Game.Text.SeStringHandling; +using HellionChat.Code; using MessagePack; namespace HellionChat; @@ -25,13 +25,14 @@ 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, + }; /// /// Get some basic text for use in generating hashes. @@ -42,7 +43,7 @@ public abstract class Chunk { TextChunk text => text.Content, IconChunk icon => icon.Icon.ToString(), - _ => "" + _ => "", }; } } @@ -57,18 +58,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 +88,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; @@ -122,7 +143,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; } diff --git a/HellionChat/Code/ChatCode.cs b/HellionChat/Code/ChatCode.cs index 32b717a..09ad788 100755 --- a/HellionChat/Code/ChatCode.cs +++ b/HellionChat/Code/ChatCode.cs @@ -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() { diff --git a/HellionChat/Code/ChatSourceExt.cs b/HellionChat/Code/ChatSourceExt.cs index d3fa5d3..f105c4d 100755 --- a/HellionChat/Code/ChatSourceExt.cs +++ b/HellionChat/Code/ChatSourceExt.cs @@ -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), + }; } diff --git a/HellionChat/Code/ChatTypeExt.cs b/HellionChat/Code/ChatTypeExt.cs index 962ef40..76128b4 100755 --- a/HellionChat/Code/ChatTypeExt.cs +++ b/HellionChat/Code/ChatTypeExt.cs @@ -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, + }; } diff --git a/HellionChat/Code/InputChannelExt.cs b/HellionChat/Code/InputChannelExt.cs index 6fd89e5..3e9e88b 100755 --- a/HellionChat/Code/InputChannelExt.cs +++ b/HellionChat/Code/InputChannelExt.cs @@ -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? 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, + }; } diff --git a/HellionChat/Commands.cs b/HellionChat/Commands.cs index 20d6aa9..7d618d5 100755 --- a/HellionChat/Commands.cs +++ b/HellionChat/Commands.cs @@ -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)) { diff --git a/HellionChat/Configuration.cs b/HellionChat/Configuration.cs index b5cc5bb..0465041 100755 --- a/HellionChat/Configuration.cs +++ b/HellionChat/Configuration.cs @@ -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(); } } @@ -45,10 +45,10 @@ public class Configuration : IPluginConfiguration // HellionThemeWindowOpacity beim Bump v13 → v14. 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. 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 @@ -60,8 +60,10 @@ public class Configuration : IPluginConfiguration // Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default). // Master-switch defaults to true; set false to restore upstream behavior. public bool PrivacyFilterEnabled = true; + // Empty set means the migration has not run yet — see Plugin.cs v6→v7. public HashSet PrivacyPersistChannels = []; + // Failsafe for ChatTypes added by future FFXIV patches we don't know about. public bool PrivacyPersistUnknownChannels; @@ -103,15 +105,19 @@ public class Configuration : IPluginConfiguration // /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 @@ -160,6 +166,7 @@ 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- @@ -179,11 +186,13 @@ 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 @@ -209,11 +218,13 @@ 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 @@ -246,6 +257,7 @@ 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 @@ -257,7 +269,9 @@ public class Configuration : IPluginConfiguration private static Dictionary BuildDefaultChatColours() { var defaults = new Dictionary(); - 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 +298,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; @@ -349,32 +366,37 @@ public class Configuration : IPluginConfiguration // settings get a fresh empty MessageList; deleted tabs lose their // history (intended). var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList(); - var livePersistentSession = Tabs - .Where(t => !t.IsTempTab) + 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 +432,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] @@ -468,29 +492,50 @@ 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; + + [NonSerialized] + public MessageList Messages = new(); + + [NonSerialized] + public UsedChannel CurrentChannel = new(); + + [NonSerialized] + public Guid Identifier = Guid.NewGuid(); // 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 bool IsGreeted; // 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] + 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) { @@ -517,12 +562,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() { @@ -660,8 +710,14 @@ public class Tab get { LockSlim.Wait(-1); - try { return Messages.Count; } - finally { LockSlim.Release(); } + try + { + return Messages.Count; + } + finally + { + LockSlim.Release(); + } } } @@ -691,7 +747,9 @@ public class Tab return new RLockedMessageList(LockSlim, Messages); } - public class RLockedMessageList(SemaphoreSlim lockSlim, List messages) : IReadOnlyList, IDisposable + public class RLockedMessageList(SemaphoreSlim lockSlim, List messages) + : IReadOnlyList, + IDisposable { public IEnumerator GetEnumerator() { @@ -750,15 +808,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 +830,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 +849,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 +892,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 +954,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), + }; } diff --git a/HellionChat/EmoteCache.cs b/HellionChat/EmoteCache.cs index eff10ad..e47143d 100644 --- a/HellionChat/EmoteCache.cs +++ b/HellionChat/EmoteCache.cs @@ -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,7 +76,7 @@ public static class EmoteCache { Unloaded, Loading, - Done + Done, } // All of this data is uninitalized while State is not `LoadingState.Done` @@ -80,11 +100,16 @@ public static class EmoteCache 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() @@ -121,7 +146,10 @@ public static class EmoteCache // load. Skip them defensively so a single bad row no longer // breaks the cache for everyone else. 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; @@ -225,13 +253,19 @@ public static class EmoteCache // 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")); + 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 +276,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 +310,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; @@ -363,7 +403,11 @@ public static class EmoteCache 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)); } @@ -385,4 +429,4 @@ public static class EmoteCache } } } -} \ No newline at end of file +} diff --git a/HellionChat/Export/MessageExporter.cs b/HellionChat/Export/MessageExporter.cs index d7e9ea2..fbc6b01 100644 --- a/HellionChat/Export/MessageExporter.cs +++ b/HellionChat/Export/MessageExporter.cs @@ -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", + }; } /// @@ -42,13 +44,15 @@ internal static class MessageExporter IReadOnlyCollection? ChatTypes, DateTimeOffset? From, DateTimeOffset? To, - string? SenderSubstring); + string? SenderSubstring + ); internal static int ExportToFile( string path, ExportFormat format, IEnumerable 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 messages, FilterDescription filter) + private static int WriteMarkdown( + StreamWriter w, + IEnumerable 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 messages, FilterDescription filter) + private static int WriteJson( + StreamWriter w, + IEnumerable 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 messages, FilterDescription filter) + private static int WriteCsv( + StreamWriter w, + IEnumerable 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}"); diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index 07e94d5..80d0535 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -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,9 +23,22 @@ public class FontManager public static readonly HashSet 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, ]; /// @@ -41,8 +54,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(); @@ -66,8 +82,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 +101,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(); @@ -117,66 +133,84 @@ 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 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font) + // wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet. + // Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem + // Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei + // UseHellionFont=true wirkungslos, was 4K-User mit größerer + // Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten). + var basePt = Plugin.Config.UseHellionFont + ? Plugin.Config.FontSizeV2 + : Plugin.Config.GlobalFontV2.SizePt; + var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges }; + config.MergeFont = Plugin.Config.UseHellionFont + ? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2") + : AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global"); + + config.SizePt = Plugin.Config.JapaneseFontV2.SizePt; + config.GlyphRanges = JpRange; + AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese"); + + config.SizePt = Plugin.Config.SymbolsFontSizeV2; + tk.AddGameSymbol(config); + + tk.Font = config.MergeFont; + }) + ); + + if (Plugin.Config.ItalicEnabled) + { + ItalicFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e => + e.OnPreBuild(tk => { - // 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 { @@ -191,21 +225,35 @@ public class FontManager /// pointing at a font the user uninstalled or that never existed on /// Linux (e.g. "Crimson Text") tears down the entire font atlas build. /// - private static ImFontPtr AddFontWithFallback(IFontAtlasBuildToolkitPreBuild tk, IFontId fontId, SafeFontConfig config, string slot) + 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); } diff --git a/HellionChat/GameFunctions/Chat.cs b/HellionChat/GameFunctions/Chat.cs index 4c714c7..35e328d 100755 --- a/HellionChat/GameFunctions/Chat.cs +++ b/HellionChat/GameFunctions/Chat.cs @@ -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 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 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? 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? 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? ChangeChannelNameHook; private readonly Hook? 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.MemberFunctionPointers.ChangeChannelName, ChangeChannelNameDetour); + ChangeChannelNameHook = + Plugin.GameInteropProvider.HookFromAddress( + AgentChatLog.MemberFunctionPointers.ChangeChannelName, + ChangeChannelNameDetour + ); ChangeChannelNameHook.Enable(); - ReplyInSelectedChatModeHook = Plugin.GameInteropProvider.HookFromAddress(RaptureShellModule.MemberFunctionPointers.ReplyInSelectedChatMode, ReplyInSelectedChatModeDetour); + ReplyInSelectedChatModeHook = + Plugin.GameInteropProvider.HookFromAddress( + RaptureShellModule.MemberFunctionPointers.ReplyInSelectedChatMode, + ReplyInSelectedChatModeDetour + ); ReplyInSelectedChatModeHook.Enable(); - SetChatLogTellTargetHook = Plugin.GameInteropProvider.HookFromAddress(RaptureShellModule.MemberFunctionPointers.SetContextTellTarget, SetContextTellTarget); + SetChatLogTellTargetHook = + Plugin.GameInteropProvider.HookFromAddress( + 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 + ); } /// @@ -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 validFn) + private static uint? RotateLinkshell( + uint currentIndex, + RotateMode rotate, + Func 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); } } diff --git a/HellionChat/GameFunctions/ChatBox.cs b/HellionChat/GameFunctions/ChatBox.cs index f70536c..011867d 100644 --- a/HellionChat/GameFunctions/ChatBox.cs +++ b/HellionChat/GameFunctions/ChatBox.cs @@ -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? sanitiserOverride = null) + internal static byte[] ValidateMessage( + string message, + Func? 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; } -} \ No newline at end of file +} diff --git a/HellionChat/GameFunctions/Context.cs b/HellionChat/GameFunctions/Context.cs index e1d3cb6..3a8a16d 100755 --- a/HellionChat/GameFunctions/Context.cs +++ b/HellionChat/GameFunctions/Context.cs @@ -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) diff --git a/HellionChat/GameFunctions/GameFunctions.cs b/HellionChat/GameFunctions/GameFunctions.cs index b85bb72..80ecbb1 100755 --- a/HellionChat/GameFunctions/GameFunctions.cs +++ b/HellionChat/GameFunctions/GameFunctions.cs @@ -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? 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(string name) where T : unmanaged + private static T* GetAddon(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) atkModuleVtbl[27]; + var atkModuleVtbl = (void**)atkModule->AtkModule.VirtualTable; + var vf27 = (delegate* unmanaged) + 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*) agent->VirtualTable; + var vf0 = *(delegate* unmanaged*) + 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); } diff --git a/HellionChat/GameFunctions/KeybindManager.cs b/HellionChat/GameFunctions/KeybindManager.cs index 2aa202d..4b1bc78 100644 --- a/HellionChat/GameFunctions/KeybindManager.cs +++ b/HellionChat/GameFunctions/KeybindManager.cs @@ -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 Keybinds = new(); - private static readonly IReadOnlyDictionary KeybindsToIntercept = new Dictionary - { - ["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 KeybindsToIntercept = + new Dictionary + { + ["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, }; } -} \ No newline at end of file +} diff --git a/HellionChat/GameFunctions/Party.cs b/HellionChat/GameFunctions/Party.cs index 8326fb8..22d0ec1 100755 --- a/HellionChat/GameFunctions/Party.cs +++ b/HellionChat/GameFunctions/Party.cs @@ -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); } } diff --git a/HellionChat/GameFunctions/Types/ChannelSwitchInfo.cs b/HellionChat/GameFunctions/Types/ChannelSwitchInfo.cs index d419715..3f7105d 100755 --- a/HellionChat/GameFunctions/Types/ChannelSwitchInfo.cs +++ b/HellionChat/GameFunctions/Types/ChannelSwitchInfo.cs @@ -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; diff --git a/HellionChat/GameFunctions/Types/TellTarget.cs b/HellionChat/GameFunctions/Types/TellTarget.cs index 554dbd2..e792e24 100755 --- a/HellionChat/GameFunctions/Types/TellTarget.cs +++ b/HellionChat/GameFunctions/Types/TellTarget.cs @@ -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); } diff --git a/HellionChat/HellionChat.csproj b/HellionChat/HellionChat.csproj index 76c8de2..50d8d5d 100644 --- a/HellionChat/HellionChat.csproj +++ b/HellionChat/HellionChat.csproj @@ -80,7 +80,6 @@ -