Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c26d1aa67 | |||
| 8b13ba1fdc | |||
| 52da5d5e23 | |||
| 916640fb60 | |||
| feeb1df4eb | |||
| f2086865ce | |||
| 15a89dd6e7 | |||
| 53952717c0 | |||
| fcbbd174b6 | |||
| d41cea0031 | |||
| c943a2cff3 | |||
| abcd0847ef | |||
| 2f52cbb7d4 | |||
| 9103bbb892 | |||
| 8f9c01d322 | |||
| af4651b37e | |||
| 485dc4e1b4 | |||
| c878d24d11 | |||
| cb5c940a84 | |||
| dd3a0ea069 | |||
| 4bf6c3ef1f | |||
| 2378ce6bf2 | |||
| b85db24601 | |||
| cae7d76206 | |||
| 4c6d52e652 | |||
| cbfdfe35be | |||
| 537b96c79f | |||
| d3d28924e6 | |||
| 48f1fb5ba1 | |||
| 0b13efd0b5 | |||
| 289fe2eb78 | |||
| fe9e66b0ff | |||
| 990edd8300 | |||
| db95ec7dff | |||
| 7e036c1d00 | |||
| 1c511a147d | |||
| f093d93761 | |||
| e7c8667497 | |||
| 497197eb2c | |||
| 08b2ffc600 | |||
| 8db3eca46c | |||
| 4d54eabdac | |||
| 698eb01bbe | |||
| a3fbaab173 | |||
| 57291e925d | |||
| 8e9332ac8c | |||
| fcb72e2b78 | |||
| 7012e8c0d8 | |||
| 176474ec2a | |||
| 9fc8749d15 | |||
| 09634b416d | |||
| 393ef175bf | |||
| d63c710836 |
@@ -1,2 +1,2 @@
|
|||||||
# Generated files
|
# Generated files
|
||||||
ChatTwo/Resources/Language.*.resx linguist-generated=true
|
HellionChat/Resources/Language.*.resx linguist-generated=true
|
||||||
@@ -59,7 +59,7 @@ body:
|
|||||||
id: log
|
id: log
|
||||||
attributes:
|
attributes:
|
||||||
label: Relevant /xllog excerpt
|
label: Relevant /xllog excerpt
|
||||||
description: Filter for "HellionChat" or "ChatTwo" if the log is huge
|
description: Filter for "HellionChat" if the log is huge
|
||||||
render: text
|
render: text
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ new commands, new translations, removed behaviour. If none, write
|
|||||||
bump and is it covered by the existing migration tests?
|
bump and is it covered by the existing migration tests?
|
||||||
- Does this change the schema in MessageStore?
|
- Does this change the schema in MessageStore?
|
||||||
- Does this change the repo.json or HellionChat.yaml manifest fields?
|
- Does this change the repo.json or HellionChat.yaml manifest fields?
|
||||||
- Does this affect the upstream cherry-pick path? See UPSTREAM_SYNC.md.
|
- Does this affect the upstream cherry-pick path? See docs/UPSTREAM_SYNC.md.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
@@ -67,6 +67,6 @@ new commands, new translations, removed behaviour. If none, write
|
|||||||
- [ ] I updated the README, in-plugin strings or documentation if my
|
- [ ] I updated the README, in-plugin strings or documentation if my
|
||||||
change is user-visible.
|
change is user-visible.
|
||||||
- [ ] I did not include any AI-generated code without disclosing it
|
- [ ] I did not include any AI-generated code without disclosing it
|
||||||
in the PR description (see [AI_DISCLOSURE.md](../AI_DISCLOSURE.md)).
|
in the PR description (see [AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)).
|
||||||
- [ ] I confirm my contribution is released under the
|
- [ ] I confirm my contribution is released under the
|
||||||
[EUPL-1.2](../LICENSE).
|
[EUPL-1.2](../LICENSE).
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
subtitle: "Theme Foundation"
|
||||||
|
versionsnatur: "Major-UI-Cycle"
|
||||||
|
---
|
||||||
|
- Theme-Engine mit fünf Built-In-Themes: Hellion Arctic (Default), Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove
|
||||||
|
- Settings öffnet jetzt eine Card-Grid-Übersicht — Klick auf eine Card führt in den Detail-View, Breadcrumb und ESC zurück zur Übersicht
|
||||||
|
- Themes-Tab mit Mini-Mockup pro Theme, Live-Switch beim Klick
|
||||||
|
- Eigene Themes als JSON in `pluginConfigs/HellionChat/themes/` — Beispiel-Vorlage wird beim ersten Start automatisch abgelegt
|
||||||
|
- Optional pro Theme eigene Chat-Channel-Farben mit Übernehmen/Behalten-Banner — niemals automatisch überschrieben
|
||||||
|
- Plugin-Icon zum Hellion-Forge-Hammer gewechselt
|
||||||
|
- Migration v13 → v14: alle User landen auf Hellion Arctic. Wer den Upstream-Look will, wählt Chat 2 Klassik in Settings → Themes
|
||||||
|
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
|
||||||
@@ -15,7 +15,7 @@ Dalamud main plugin repo. To install:
|
|||||||
|
|
||||||
- [README](https://github.com/JonKazama-Hellion/HellionChat/blob/main/README.md) — features, architecture, build
|
- [README](https://github.com/JonKazama-Hellion/HellionChat/blob/main/README.md) — features, architecture, build
|
||||||
- [Privacy notice](https://github.com/JonKazama-Hellion/HellionChat/blob/main/PRIVACY.md) — what the plugin stores and sends
|
- [Privacy notice](https://github.com/JonKazama-Hellion/HellionChat/blob/main/PRIVACY.md) — what the plugin stores and sends
|
||||||
- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/THIRD_PARTY_NOTICES.md) — dependencies and licences
|
- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/docs/THIRD_PARTY_NOTICES.md) — dependencies and licences
|
||||||
- [Security policy](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SECURITY.md) — vulnerability reporting
|
- [Security policy](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SECURITY.md) — vulnerability reporting
|
||||||
- [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths
|
- [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: 10.0.x
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: 10.0.x
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
languages: csharp
|
languages: csharp
|
||||||
build-mode: manual
|
build-mode: manual
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
- name: Perform CodeQL analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
category: /language:csharp
|
category: /language:csharp
|
||||||
|
|
||||||
@@ -79,15 +79,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
languages: actions
|
languages: actions
|
||||||
build-mode: none
|
build-mode: none
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
- name: Perform CodeQL analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
category: /language:actions
|
category: /language:actions
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: 10.0.x
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
scripts/
|
scripts/
|
||||||
|
|
||||||
|
# Local test project (stays out of the published plugin repo;
|
||||||
|
# pure-function safety net for refactor cycles)
|
||||||
|
HellionChat.Tests/
|
||||||
|
|
||||||
# Packaging
|
# Packaging
|
||||||
pack/
|
pack/
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ what I am not, and how to make a contribution land smoothly.
|
|||||||
- Read the [README](README.md) so you understand the scope: this is a
|
- Read the [README](README.md) so you understand the scope: this is a
|
||||||
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
|
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
|
||||||
removes the upstream webinterface and ships smaller defaults.
|
removes the upstream webinterface and ships smaller defaults.
|
||||||
- Read [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md). Cherry-picks from upstream
|
- Read [UPSTREAM_SYNC.md](docs/UPSTREAM_SYNC.md). Cherry-picks from upstream
|
||||||
Chat 2 are selective and conscious; not everything that lands there
|
Chat 2 are selective and conscious; not everything that lands there
|
||||||
belongs here.
|
belongs here.
|
||||||
- Read [SECURITY.md](SECURITY.md). Anything security-sensitive goes
|
- Read [SECURITY.md](SECURITY.md). Anything security-sensitive goes
|
||||||
@@ -22,7 +22,7 @@ what I am not, and how to make a contribution land smoothly.
|
|||||||
- Bug fixes for behaviour documented in the README, the in-plugin
|
- Bug fixes for behaviour documented in the README, the in-plugin
|
||||||
settings or the changelog.
|
settings or the changelog.
|
||||||
- Translation contributions for Hellion-specific strings via direct
|
- Translation contributions for Hellion-specific strings via direct
|
||||||
pull requests against `ChatTwo/Resources/HellionStrings.*.resx`.
|
pull requests against `HellionChat/Resources/HellionStrings.*.resx`.
|
||||||
Translations for the upstream Chat 2 strings (`Language.*.resx`) are
|
Translations for the upstream Chat 2 strings (`Language.*.resx`) are
|
||||||
not handled here; they go through the upstream Chat 2 project.
|
not handled here; they go through the upstream Chat 2 project.
|
||||||
- Documentation improvements (README, comments, this file).
|
- Documentation improvements (README, comments, this file).
|
||||||
@@ -40,7 +40,7 @@ what I am not, and how to make a contribution land smoothly.
|
|||||||
They make selective upstream cherry-picks much harder and the
|
They make selective upstream cherry-picks much harder and the
|
||||||
maintenance cost outweighs the benefit for a one-person project.
|
maintenance cost outweighs the benefit for a one-person project.
|
||||||
- AI-generated code dropped in without disclosure or human review. See
|
- AI-generated code dropped in without disclosure or human review. See
|
||||||
[AI_DISCLOSURE.md](AI_DISCLOSURE.md) for how I handle AI assistance
|
[AI_DISCLOSURE.md](docs/AI_DISCLOSURE.md) for how I handle AI assistance
|
||||||
on my side; I expect comparable transparency from contributors.
|
on my side; I expect comparable transparency from contributors.
|
||||||
|
|
||||||
If you are unsure whether an idea fits, open a feature-request issue
|
If you are unsure whether an idea fits, open a feature-request issue
|
||||||
@@ -60,7 +60,7 @@ proposal than to a finished pull request.
|
|||||||
easier to review than one big one. Squash-on-merge happens at the
|
easier to review than one big one. Squash-on-merge happens at the
|
||||||
PR level if needed.
|
PR level if needed.
|
||||||
5. If your change touches user-visible behaviour, update the README
|
5. If your change touches user-visible behaviour, update the README
|
||||||
and/or the changelog block in `ChatTwo/HellionChat.yaml` and
|
and/or the changelog block in `HellionChat/HellionChat.yaml` and
|
||||||
`repo.json` for the next version. I bump the version number myself
|
`repo.json` for the next version. I bump the version number myself
|
||||||
at release time, so you do not need to.
|
at release time, so you do not need to.
|
||||||
6. Open the pull request against `main`. The PR template will ask
|
6. Open the pull request against `main`. The PR template will ask
|
||||||
@@ -79,13 +79,12 @@ locally you need:
|
|||||||
|
|
||||||
```
|
```
|
||||||
dotnet restore
|
dotnet restore
|
||||||
dotnet build ChatTwo.sln -c Release
|
dotnet build HellionChat.sln -c Release
|
||||||
dotnet test ChatTwo.sln -c Release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The test project is `ChatTwo.Tests`. New behaviour should come with a
|
Tests are not part of the current `HellionChat.sln`. If you add a test
|
||||||
test where the existing test infrastructure makes that practical
|
project, point it at the relevant subsystems (privacy filter,
|
||||||
(privacy filter, configuration migration, message store).
|
configuration migration, message store) and mention it in the PR.
|
||||||
|
|
||||||
For a smoke test in-game: build, copy the output into your Dalamud
|
For a smoke test in-game: build, copy the output into your Dalamud
|
||||||
`devPlugins/HellionChat/` directory and load it through `/xlplugins`.
|
`devPlugins/HellionChat/` directory and load it through `/xlplugins`.
|
||||||
@@ -114,11 +113,11 @@ There is no separate CLA.
|
|||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Hellion-specific strings live in `ChatTwo/Resources/HellionStrings.resx`
|
Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx`
|
||||||
(English source) and `HellionStrings.<lang>.resx` (per-language).
|
(English source) and `HellionStrings.<lang>.resx` (per-language).
|
||||||
Translations are accepted as direct pull requests against those files.
|
Translations are accepted as direct pull requests against those files.
|
||||||
|
|
||||||
The upstream Chat 2 strings in `ChatTwo/Resources/Language.*.resx` are
|
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are
|
||||||
**not** translated in this repository. They are owned by the upstream
|
**not** translated in this repository. They are owned by the upstream
|
||||||
Chat 2 project and synced in via cherry-pick. Please contribute
|
Chat 2 project and synced in via cherry-pick. Please contribute
|
||||||
upstream-string translations to
|
upstream-string translations to
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
|
||||||
|
|
||||||
Copyright (c) 2024-2025 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
Copyright (c) 2022-2026 Infiziert90 (Infi) and Anna Clemens (ascclemens)
|
||||||
Original ChatTwo authors and copyright holders of the upstream
|
Original ChatTwo authors and copyright holders of the upstream
|
||||||
plugin this fork is built on. Their work covers the message store,
|
plugin this fork is built on. Their work covers the message store,
|
||||||
the channel filtering, the sidebar tab system, the FFXIV chat
|
the channel filtering, the sidebar tab system, the FFXIV chat
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HellionChat", "HellionChat\HellionChat.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HellionChat", "HellionChat\HellionChat.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -34,10 +34,23 @@ public class ConfigKeyBind
|
|||||||
[Serializable]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration
|
public class Configuration : IPluginConfiguration
|
||||||
{
|
{
|
||||||
private const int LatestVersion = 12;
|
private const int LatestVersion = 14;
|
||||||
|
|
||||||
public int Version { get; set; } = LatestVersion;
|
public int Version { get; set; } = LatestVersion;
|
||||||
|
|
||||||
|
// v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
|
||||||
|
public string Theme = "hellion-arctic";
|
||||||
|
|
||||||
|
// v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
|
||||||
|
// 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;
|
||||||
|
public bool UseCompactDensity;
|
||||||
|
public bool ShowThemeQuickPicker;
|
||||||
|
|
||||||
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
|
||||||
// Master-switch defaults to true; set false to restore upstream behavior.
|
// Master-switch defaults to true; set false to restore upstream behavior.
|
||||||
public bool PrivacyFilterEnabled = true;
|
public bool PrivacyFilterEnabled = true;
|
||||||
@@ -70,12 +83,14 @@ public class Configuration : IPluginConfiguration
|
|||||||
// Hellion Chat global ImGui theme — applied to every plugin window in
|
// Hellion Chat global ImGui theme — applied to every plugin window in
|
||||||
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
|
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
|
||||||
// can flip this off in the Privacy tab.
|
// can flip this off in the Privacy tab.
|
||||||
|
[Obsolete("Replaced by Theme slug + WindowOpacity in v14")]
|
||||||
public bool HellionThemeEnabled = true;
|
public bool HellionThemeEnabled = true;
|
||||||
|
|
||||||
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
// Window background opacity, 0.5–1.0. Lower values make the plugin
|
||||||
// panes more glass-like so the game shines through. Default 0.5
|
// panes more glass-like so the game shines through. Default 0.5
|
||||||
// matches the maintainer's daily-driver preference; users who want
|
// matches the maintainer's daily-driver preference; users who want
|
||||||
// a less translucent look bump it up in Aussehen → Theme.
|
// a less translucent look bump it up in Aussehen → Theme.
|
||||||
|
[Obsolete("Replaced by WindowOpacity in v14")]
|
||||||
public float HellionThemeWindowOpacity = 0.5f;
|
public float HellionThemeWindowOpacity = 0.5f;
|
||||||
|
|
||||||
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
|
||||||
@@ -145,6 +160,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
public bool HideWhenUiHidden = true;
|
public bool HideWhenUiHidden = true;
|
||||||
public bool HideInLoadingScreens;
|
public bool HideInLoadingScreens;
|
||||||
public bool HideInBattle;
|
public bool HideInBattle;
|
||||||
|
public bool HideInNewGamePlusMenu;
|
||||||
public bool HideWhenInactive;
|
public bool HideWhenInactive;
|
||||||
public int InactivityHideTimeout = 10;
|
public int InactivityHideTimeout = 10;
|
||||||
public bool InactivityHideActiveDuringBattle = true;
|
public bool InactivityHideActiveDuringBattle = true;
|
||||||
@@ -221,6 +237,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
public float TooltipOffset;
|
public float TooltipOffset;
|
||||||
public float WindowAlpha = 100f;
|
public float WindowAlpha = 100f;
|
||||||
public Dictionary<ChatType, uint> ChatColours = new();
|
public Dictionary<ChatType, uint> ChatColours = new();
|
||||||
|
public bool ColorSelectedInputChannelButton = true;
|
||||||
public List<Tab> Tabs = [];
|
public List<Tab> Tabs = [];
|
||||||
|
|
||||||
public bool OverrideStyle;
|
public bool OverrideStyle;
|
||||||
@@ -241,6 +258,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
HideWhenUiHidden = other.HideWhenUiHidden;
|
HideWhenUiHidden = other.HideWhenUiHidden;
|
||||||
HideInLoadingScreens = other.HideInLoadingScreens;
|
HideInLoadingScreens = other.HideInLoadingScreens;
|
||||||
HideInBattle = other.HideInBattle;
|
HideInBattle = other.HideInBattle;
|
||||||
|
HideInNewGamePlusMenu = other.HideInNewGamePlusMenu;
|
||||||
HideWhenInactive = other.HideWhenInactive;
|
HideWhenInactive = other.HideWhenInactive;
|
||||||
InactivityHideTimeout = other.InactivityHideTimeout;
|
InactivityHideTimeout = other.InactivityHideTimeout;
|
||||||
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
|
||||||
@@ -276,7 +294,10 @@ public class Configuration : IPluginConfiguration
|
|||||||
MaxLinesToRender = other.MaxLinesToRender;
|
MaxLinesToRender = other.MaxLinesToRender;
|
||||||
Use24HourClock = other.Use24HourClock;
|
Use24HourClock = other.Use24HourClock;
|
||||||
ShowEmotes = other.ShowEmotes;
|
ShowEmotes = other.ShowEmotes;
|
||||||
BlockedEmotes = other.BlockedEmotes;
|
// Deep-copy the set so the live and mutable Configuration instances don't share state
|
||||||
|
// — a HashSet reference assignment would cause edits in the settings window to leak
|
||||||
|
// into the live config before the user clicks Save.
|
||||||
|
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
|
||||||
FontsEnabled = other.FontsEnabled;
|
FontsEnabled = other.FontsEnabled;
|
||||||
ItalicEnabled = other.ItalicEnabled;
|
ItalicEnabled = other.ItalicEnabled;
|
||||||
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
ExtraGlyphRanges = other.ExtraGlyphRanges;
|
||||||
@@ -288,6 +309,7 @@ public class Configuration : IPluginConfiguration
|
|||||||
TooltipOffset = other.TooltipOffset;
|
TooltipOffset = other.TooltipOffset;
|
||||||
WindowAlpha = other.WindowAlpha;
|
WindowAlpha = other.WindowAlpha;
|
||||||
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
|
||||||
|
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
|
||||||
// never present in a disk-loaded copy. Keep the live temp tabs of
|
// never present in a disk-loaded copy. Keep the live temp tabs of
|
||||||
@@ -314,10 +336,19 @@ public class Configuration : IPluginConfiguration
|
|||||||
RetentionLastRunAt = other.RetentionLastRunAt;
|
RetentionLastRunAt = other.RetentionLastRunAt;
|
||||||
|
|
||||||
FirstRunCompleted = other.FirstRunCompleted;
|
FirstRunCompleted = other.FirstRunCompleted;
|
||||||
|
#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten
|
||||||
HellionThemeEnabled = other.HellionThemeEnabled;
|
HellionThemeEnabled = other.HellionThemeEnabled;
|
||||||
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
|
||||||
|
#pragma warning restore CS0612, CS0618
|
||||||
UseHellionFont = other.UseHellionFont;
|
UseHellionFont = other.UseHellionFont;
|
||||||
|
|
||||||
|
// v1.1.0 theme engine fields
|
||||||
|
Theme = other.Theme;
|
||||||
|
WindowOpacity = other.WindowOpacity;
|
||||||
|
ReduceMotion = other.ReduceMotion;
|
||||||
|
UseCompactDensity = other.UseCompactDensity;
|
||||||
|
ShowThemeQuickPicker = other.ShowThemeQuickPicker;
|
||||||
|
|
||||||
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
EnableAutoTellTabs = other.EnableAutoTellTabs;
|
||||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||||
|
|||||||
@@ -66,16 +66,29 @@ public static class EmoteCache
|
|||||||
|
|
||||||
public static string[] SortedCodeArray = [];
|
public static string[] SortedCodeArray = [];
|
||||||
|
|
||||||
|
// Plugin-scoped cancellation source for in-flight emote loads. Dispose
|
||||||
|
// cancels every running download/texture-create so the workers don't
|
||||||
|
// touch a torn-down TextureProvider on plugin reload. Replaced with a
|
||||||
|
// fresh source on the next LoadData() call so a re-enable still works.
|
||||||
|
private static CancellationTokenSource Cts = new();
|
||||||
|
internal static CancellationToken Token => Cts.Token;
|
||||||
|
|
||||||
public static async Task LoadData()
|
public static async Task LoadData()
|
||||||
{
|
{
|
||||||
if (State is not LoadingState.Unloaded)
|
if (State is not LoadingState.Unloaded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Refresh the CTS in case Dispose was called and we're being re-enabled
|
||||||
|
// in the same process (Dalamud /xlplugins toggle).
|
||||||
|
if (Cts.IsCancellationRequested)
|
||||||
|
Cts = new CancellationTokenSource();
|
||||||
|
|
||||||
State = LoadingState.Loading;
|
State = LoadingState.Loading;
|
||||||
|
var ct = Cts.Token;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var global = await Client.GetAsync(GlobalEmotes);
|
var global = await Client.GetAsync(GlobalEmotes, ct);
|
||||||
var globalList = await global.Content.ReadAsStringAsync();
|
var globalList = await global.Content.ReadAsStringAsync(ct);
|
||||||
|
|
||||||
foreach (var emote in JsonSerializer.Deserialize<Emote[]>(globalList)!)
|
foreach (var emote in JsonSerializer.Deserialize<Emote[]>(globalList)!)
|
||||||
if (!string.IsNullOrEmpty(emote.Code) && !NotWorking.Contains(emote.Code))
|
if (!string.IsNullOrEmpty(emote.Code) && !NotWorking.Contains(emote.Code))
|
||||||
@@ -84,8 +97,8 @@ public static class EmoteCache
|
|||||||
var lastId = string.Empty;
|
var lastId = string.Empty;
|
||||||
for (var i = 0; i < 15; i++)
|
for (var i = 0; i < 15; i++)
|
||||||
{
|
{
|
||||||
var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId));
|
var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId), ct);
|
||||||
var topList = await top.Content.ReadAsStringAsync();
|
var topList = await top.Content.ReadAsStringAsync(ct);
|
||||||
|
|
||||||
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
|
||||||
// BetterTTV occasionally returns entries with a null Code; the
|
// BetterTTV occasionally returns entries with a null Code; the
|
||||||
@@ -103,6 +116,12 @@ public static class EmoteCache
|
|||||||
SortedCodeArray = Cache.Keys.Order().ToArray();
|
SortedCodeArray = Cache.Keys.Order().ToArray();
|
||||||
State = LoadingState.Done;
|
State = LoadingState.Done;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Plugin disposed while the cache was loading; leave State on
|
||||||
|
// Loading so a subsequent re-enable can re-issue LoadData with
|
||||||
|
// a fresh CTS (handled above).
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Reset to Unloaded so a later trigger (e.g. the user reopening
|
// Reset to Unloaded so a later trigger (e.g. the user reopening
|
||||||
@@ -116,6 +135,10 @@ public static class EmoteCache
|
|||||||
|
|
||||||
public static void Dispose()
|
public static void Dispose()
|
||||||
{
|
{
|
||||||
|
// Cancel in-flight downloads / texture creates so the async-void
|
||||||
|
// Load methods bail out before they touch a disposed TextureProvider.
|
||||||
|
Cts.Cancel();
|
||||||
|
|
||||||
foreach (var emote in EmoteImages.Values)
|
foreach (var emote in EmoteImages.Values)
|
||||||
emote.InnerDispose();
|
emote.InnerDispose();
|
||||||
}
|
}
|
||||||
@@ -171,7 +194,7 @@ public static class EmoteCache
|
|||||||
ImGui.Image(Texture!.Handle, size);
|
ImGui.Image(Texture!.Handle, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<byte[]> LoadAsync(Emote emote)
|
internal async Task<byte[]> LoadAsync(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// BetterTTV-supplied Id and ImageType are interpolated straight
|
// BetterTTV-supplied Id and ImageType are interpolated straight
|
||||||
// into the filename. HTTPS protects the wire, but a compromised
|
// into the filename. HTTPS protects the wire, but a compromised
|
||||||
@@ -188,15 +211,15 @@ public static class EmoteCache
|
|||||||
|
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
RawData = await File.ReadAllBytesAsync(filePath);
|
RawData = await File.ReadAllBytesAsync(filePath, ct);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var content = await Client.GetAsync(EmotePath.Format(emote.Id));
|
var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct);
|
||||||
RawData = await content.Content.ReadAsByteArrayAsync();
|
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);
|
||||||
stream.Write(RawData, 0, RawData.Length);
|
await stream.WriteAsync(RawData, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RawData;
|
return RawData;
|
||||||
@@ -209,21 +232,28 @@ public static class EmoteCache
|
|||||||
{
|
{
|
||||||
public ImGuiEmote Prepare(Emote emote)
|
public ImGuiEmote Prepare(Emote emote)
|
||||||
{
|
{
|
||||||
Task.Run(() => Load(emote));
|
var ct = EmoteCache.Token;
|
||||||
|
Task.Run(() => Load(emote, ct), ct);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Load(Emote emote)
|
private async void Load(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = await LoadAsync(emote);
|
var image = await LoadAsync(emote, ct);
|
||||||
if (image.Length <= 0)
|
if (image.Length <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image);
|
ct.ThrowIfCancellationRequested();
|
||||||
|
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image, cancellationToken: ct);
|
||||||
IsLoaded = true;
|
IsLoaded = true;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Plugin disposed mid-load; the EmoteImages entry is also
|
||||||
|
// being torn down, no extra cleanup needed.
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
@@ -279,15 +309,16 @@ public static class EmoteCache
|
|||||||
|
|
||||||
public ImGuiGif Prepare(Emote emote)
|
public ImGuiGif Prepare(Emote emote)
|
||||||
{
|
{
|
||||||
Task.Run(() => Load(emote));
|
var ct = EmoteCache.Token;
|
||||||
|
Task.Run(() => Load(emote, ct), ct);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void Load(Emote emote)
|
private async void Load(Emote emote, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = await LoadAsync(emote);
|
var image = await LoadAsync(emote, ct);
|
||||||
if (image.Length <= 0)
|
if (image.Length <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -299,6 +330,8 @@ public static class EmoteCache
|
|||||||
var frames = new List<(IDalamudTextureWrap Tex, float Delay)>();
|
var frames = new List<(IDalamudTextureWrap Tex, float Delay)>();
|
||||||
foreach (var frame in img.Frames)
|
foreach (var frame in img.Frames)
|
||||||
{
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
|
||||||
|
|
||||||
// Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
|
// Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
|
||||||
@@ -307,13 +340,21 @@ public static class EmoteCache
|
|||||||
|
|
||||||
var buffer = new byte[4 * frame.Width * frame.Height];
|
var buffer = new byte[4 * frame.Width * frame.Height];
|
||||||
frame.CopyPixelDataTo(buffer);
|
frame.CopyPixelDataTo(buffer);
|
||||||
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer);
|
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer, cancellationToken: ct);
|
||||||
frames.Add((tex, delay));
|
frames.Add((tex, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
Frames = frames;
|
Frames = frames;
|
||||||
IsLoaded = true;
|
IsLoaded = true;
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Plugin disposed mid-load; partial frames are released by
|
||||||
|
// InnerDispose on the next dispose pass.
|
||||||
|
foreach (var f in Frames)
|
||||||
|
f.Texture.Dispose();
|
||||||
|
Frames = [];
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Failed = true;
|
Failed = true;
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ public class FontManager
|
|||||||
|
|
||||||
internal IFontHandle FontAwesome = null!;
|
internal IFontHandle FontAwesome = null!;
|
||||||
|
|
||||||
internal readonly byte[] GameSymFont;
|
|
||||||
|
|
||||||
private ushort[] Ranges = [];
|
private ushort[] Ranges = [];
|
||||||
private ushort[] JpRange = [];
|
private ushort[] JpRange = [];
|
||||||
|
|
||||||
@@ -30,32 +28,6 @@ public class FontManager
|
|||||||
36f, 40f, 45f, 46f, 68f, 90f,
|
36f, 40f, 45f, 46f, 68f, 90f,
|
||||||
];
|
];
|
||||||
|
|
||||||
public FontManager()
|
|
||||||
{
|
|
||||||
var filePath = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "FFXIV_Lodestone_SSF.ttf");
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
{
|
|
||||||
GameSymFont = File.ReadAllBytes(filePath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Dispose HttpClient and HttpResponseMessage to avoid socket
|
|
||||||
// exhaustion on repeated cold-start downloads. GetAwaiter().GetResult()
|
|
||||||
// unwraps AggregateException so failures surface cleanly. A full
|
|
||||||
// async refactor of the constructor would be cleaner but is out of
|
|
||||||
// scope for v1.0.0 — tracked in the backlog.
|
|
||||||
using var client = new HttpClient();
|
|
||||||
using var response = client
|
|
||||||
.GetAsync("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
GameSymFont = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
Dalamud.Utility.FilesystemUtil.WriteAllBytesSafe(filePath, GameSymFont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
|
/// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
|
||||||
/// extracted from the assembly's manifest resources on first use; the
|
/// extracted from the assembly's manifest resources on first use; the
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
{
|
{
|
||||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||||
worldId = agent->TellWorldId;
|
worldId = agent->TellWorldId;
|
||||||
Plugin.Log.Debug($"Detected tell target '{playerName}'@{worldId}");
|
Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||||
@@ -400,7 +400,9 @@ internal sealed unsafe class Chat : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell);
|
var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell);
|
||||||
return channel + idx;
|
// 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;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return channel;
|
return channel;
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ namespace HellionChat.GameFunctions;
|
|||||||
|
|
||||||
internal unsafe class GameFunctions : IDisposable
|
internal unsafe class GameFunctions : IDisposable
|
||||||
{
|
{
|
||||||
|
internal const string NewGamePlusAddonName = "QuestRedo";
|
||||||
|
|
||||||
#region Hooks
|
#region Hooks
|
||||||
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
|
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
|
||||||
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
|
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
|
||||||
@@ -243,7 +245,8 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
vf0(agent, &result, &value, 0, 0);
|
vf0(agent, &result, &value, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(128);
|
private const int PlaceholderBufferSize = 128;
|
||||||
|
private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(PlaceholderBufferSize);
|
||||||
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
|
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
|
||||||
private string? ReplacementName;
|
private string? ReplacementName;
|
||||||
|
|
||||||
@@ -259,6 +262,17 @@ internal unsafe class GameFunctions : IDisposable
|
|||||||
if (ReplacementName == null || placeholder != Placeholder)
|
if (ReplacementName == null || placeholder != Placeholder)
|
||||||
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
|
||||||
|
|
||||||
|
// The fixed buffer is 128 bytes; UTF-8 + null-terminator must fit.
|
||||||
|
// FFXIV player names plus an @World suffix should never approach this
|
||||||
|
// limit, but a malformed ReplacementName must not overflow the buffer.
|
||||||
|
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||||
|
if (byteCount >= PlaceholderBufferSize)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
|
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
|
||||||
ReplacementName = null;
|
ReplacementName = null;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class TellTarget
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSet()
|
public bool IsSet()
|
||||||
=> Name.Length > 0 && World > 0;
|
=> !string.IsNullOrEmpty(Name) && World > 0;
|
||||||
|
|
||||||
public string ToWorldString()
|
public string ToWorldString()
|
||||||
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
|
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
||||||
called out in the yaml changelog so users can see what it
|
called out in the yaml changelog so users can see what it
|
||||||
derives from. -->
|
derives from. -->
|
||||||
<Version>1.0.0</Version>
|
<Version>1.1.0</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
<!-- Honor packages.lock.json on restore so floating version ranges
|
<!-- Honor packages.lock.json on restore so floating version ranges
|
||||||
don't silently drift between machines or CI runs. -->
|
don't silently drift between machines or CI runs. -->
|
||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
@@ -18,7 +19,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
<!-- Closed ranges on packages with breaking-change history block a
|
||||||
|
surprise major bump when the lock file is regenerated. The
|
||||||
|
lock file pins the exact version per build; the upper bound
|
||||||
|
keeps the unlock path from drifting across major lines. -->
|
||||||
|
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||||
<!-- Override the transitively-referenced native SQLite build to one
|
<!-- Override the transitively-referenced native SQLite build to one
|
||||||
that ships SQLite >= 3.50.3 (CVE-2025-6965 memory corruption,
|
that ships SQLite >= 3.50.3 (CVE-2025-6965 memory corruption,
|
||||||
@@ -28,8 +33,15 @@
|
|||||||
without a major bump on the managed wrapper. -->
|
without a major bump on the managed wrapper. -->
|
||||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||||
<PackageReference Include="Pidgin" Version="3.3.0" />
|
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4.0.0)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Pure-function test suites in HellionChat.Tests need access to
|
||||||
|
the internal helper classes (StringUtil, UriPayload, Tokenizer
|
||||||
|
etc.). Test assembly does not get redistributed. -->
|
||||||
|
<InternalsVisibleTo Include="HellionChat.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -63,6 +75,9 @@
|
|||||||
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
|
||||||
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
<LogicalName>HellionFont-OFL.txt</LogicalName>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Include="Themes\Builtin\example-theme.json">
|
||||||
|
<LogicalName>HellionChat.Themes.Builtin.example-theme.json</LogicalName>
|
||||||
|
</EmbeddedResource>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ description: |-
|
|||||||
- Independent plugin state — own config file and database directory,
|
- Independent plugin state — own config file and database directory,
|
||||||
so Hellion Chat does not share state with upstream Chat 2
|
so Hellion Chat does not share state with upstream Chat 2
|
||||||
|
|
||||||
|
v1.1.0 — Theme engine with five built-in themes (Hellion Arctic,
|
||||||
|
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove) plus
|
||||||
|
JSON-based custom-theme authoring. Settings rebuilt around a card
|
||||||
|
grid with section detail views. See docs/THEME-AUTHORING.md.
|
||||||
|
|
||||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
||||||
|
|
||||||
Modding & support: join the Hellion Forge Discord at
|
Modding & support: join the Hellion Forge Discord at
|
||||||
@@ -41,7 +46,8 @@ accepts_feedback: true
|
|||||||
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png
|
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png
|
||||||
image_urls:
|
image_urls:
|
||||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png
|
||||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/withSimpleTweaks.png
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/settingsOverview.png
|
||||||
|
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/themesPicker.png
|
||||||
tags:
|
tags:
|
||||||
- Social
|
- Social
|
||||||
- UI
|
- UI
|
||||||
@@ -49,6 +55,108 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
|
**Hellion Chat 1.1.0 — Theme Foundation**
|
||||||
|
|
||||||
|
First major UI cycle after the standalone v1.0.0 cut. Theme engine,
|
||||||
|
five built-in themes, customisable JSON themes, modernised settings
|
||||||
|
layout.
|
||||||
|
|
||||||
|
New themes (Settings → Themes):
|
||||||
|
|
||||||
|
- **Hellion Arctic** — the brand default, Arctic Cyan + Ember Glow
|
||||||
|
on industrial slate.
|
||||||
|
- **Chat 2 Klassik** — Steel Blue on neutral grey, eckige Kanten.
|
||||||
|
The upstream Chat 2 look on the new engine.
|
||||||
|
- **Event Horizon** — Cosmic Purple on near-black. Deep-space mood.
|
||||||
|
- **Moonlit Bloom** — Bloom Magenta + Soft Sage on deep-violet
|
||||||
|
night.
|
||||||
|
- **Mint Grove** — Mint Green + Honey Amber on deep forest. First
|
||||||
|
member of the Grove family.
|
||||||
|
|
||||||
|
Theme engine highlights:
|
||||||
|
|
||||||
|
- Slug-based selection in Settings → Themes with mini-mockup
|
||||||
|
previews per theme.
|
||||||
|
- Click a theme card and the whole plugin (chat, settings,
|
||||||
|
pop-outs, viewer) repaints instantly.
|
||||||
|
- Custom themes via JSON in pluginConfigs/HellionChat/themes/.
|
||||||
|
Example template seeded on first launch.
|
||||||
|
- Optional per-theme chat-channel colours. When a theme proposes
|
||||||
|
its own chat colours and yours differ, a dezent banner offers
|
||||||
|
Apply / Keep — never auto-overwriting.
|
||||||
|
- Migration v13 → v14: existing users land on Hellion Arctic. Pick
|
||||||
|
Chat 2 Klassik to keep the upstream look.
|
||||||
|
|
||||||
|
Settings layout:
|
||||||
|
|
||||||
|
- New card-grid overview on Settings open. Click a card to drill
|
||||||
|
into the section.
|
||||||
|
- Breadcrumb back to overview, ESC also returns.
|
||||||
|
- Detail view drops the redundant tab list — section content uses
|
||||||
|
the full width.
|
||||||
|
|
||||||
|
Branding:
|
||||||
|
|
||||||
|
- Plugin icon swapped from the ChatTwo derivative to the Hellion
|
||||||
|
Forge hammer.
|
||||||
|
- New docs/THEME-AUTHORING.md walks you through writing your own
|
||||||
|
themes with the Forge logo on top.
|
||||||
|
|
||||||
|
Technical:
|
||||||
|
|
||||||
|
- HellionStyle.PushGlobal is now theme-driven. Configuration.
|
||||||
|
HellionThemeEnabled is deprecated and will be removed in v1.2.0.
|
||||||
|
- New ThemeRegistry singleton with LastWriteTime-cached custom-
|
||||||
|
theme loader.
|
||||||
|
- 51 local unit tests cover the data model, registry, JSON round-
|
||||||
|
trip and built-in sanity checks.
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
**Hellion Chat 1.0.3 — Polish patch**
|
||||||
|
|
||||||
|
- New: optionally hide chat (and every other plugin window) while the
|
||||||
|
New Game+ menu is open. Toggle in Settings → Window → Frame, default
|
||||||
|
off. Closing the menu restores all windows.
|
||||||
|
- New: optionally tint the channel selector button next to the input
|
||||||
|
field with the currently active channel's colour. Toggle in
|
||||||
|
Settings → Appearance → Colours, default on. Matches the existing
|
||||||
|
input-text tint and respects ExtraChat overrides.
|
||||||
|
- Fix: status, item and other inline hover icons keep their original
|
||||||
|
aspect ratio. Debuff icons with non-square dimensions are no longer
|
||||||
|
visually squished into a 32×32 box.
|
||||||
|
- Diagnostic: hide-state transitions (battle, cutscene, user-hide,
|
||||||
|
cutscene override) are now logged on Verbose level for easier bug
|
||||||
|
reports — off by default, enable with `/xllog set HellionChat verbose`.
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
|
**Hellion Chat 1.0.1 — Window Position Recovery**
|
||||||
|
|
||||||
|
- Automatic bounds check on the first draw after plugin load.
|
||||||
|
When the persisted window position has no overlap with the
|
||||||
|
primary viewport, the window snaps to a safe top-left default.
|
||||||
|
Helpful after a monitor disconnect, resolution change or
|
||||||
|
multi-monitor layout switch between sessions.
|
||||||
|
- New "Reset Window Position" button in Settings → Window → Frame
|
||||||
|
as a manual escape hatch for edge cases the automatic check
|
||||||
|
doesn't catch.
|
||||||
|
|
||||||
|
Tested on Linux/Wayland with a hard-cut three-monitor reduction;
|
||||||
|
window recovers cleanly without manual JSON editing.
|
||||||
|
|
||||||
|
Housekeeping carried over since v1.0.0:
|
||||||
|
|
||||||
|
- Documentation restructured into docs/ folder. New CHANGELOG,
|
||||||
|
CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added
|
||||||
|
- Stale ChatTwo/* paths in repo configs updated to HellionChat/*
|
||||||
|
- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString
|
||||||
|
Unicode fix relevant for non-ASCII channel/tab names)
|
||||||
|
- GitHub Actions: actions/setup-dotnet bumped 4 → 5,
|
||||||
|
github/codeql-action bumped 3 → 4
|
||||||
|
|
||||||
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 1.0.0 — Standalone Major Release**
|
**Hellion Chat 1.0.0 — Standalone Major Release**
|
||||||
|
|
||||||
First fully standalone release. Internal cleanup plus a sweep of
|
First fully standalone release. Internal cleanup plus a sweep of
|
||||||
@@ -188,76 +296,6 @@ changelog: |-
|
|||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||||
|
|
||||||
**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**
|
|
||||||
|
|
||||||
Two opt-in UX features land in the same release. Existing users see
|
|
||||||
no change unless they enable the new toggles.
|
|
||||||
|
|
||||||
Pop-out input bar:
|
|
||||||
|
|
||||||
- New global master switch in Settings → Window → Frame: "Enable input
|
|
||||||
in pop-outs". Default OFF so existing behaviour is preserved
|
|
||||||
- When enabled, every pop-out window grows a compact input bar at the
|
|
||||||
bottom (channel-coloured icon button left, text input right). The
|
|
||||||
auto-translate picker is intentionally not part of the compact bar
|
|
||||||
in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)
|
|
||||||
rarely need it there
|
|
||||||
- Each pop-out keeps an independent text buffer and history cursor;
|
|
||||||
channel changes still apply globally because that is how the FFXIV
|
|
||||||
channel API works
|
|
||||||
- Up/Down navigates a shared input history singleton across the main
|
|
||||||
window and every open pop-out
|
|
||||||
- First pop-out opening after the upgrade shows a one-time hint
|
|
||||||
banner pointing users to the new toggle
|
|
||||||
|
|
||||||
Chat colour presets:
|
|
||||||
|
|
||||||
- Seven built-in presets above the per-channel colour list in
|
|
||||||
Settings → Appearance → Colours: ChatTwo Default, High-Contrast,
|
|
||||||
Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange
|
|
||||||
Arctic Cyan + Ember Glow palette from the Hellion Online Media
|
|
||||||
branding spec), plus two bonus mood presets — Night Blue (royal
|
|
||||||
blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)
|
|
||||||
- Apply is immediate and overwrites the channels covered by the
|
|
||||||
preset; battle-channel colours are left alone so combat tuning
|
|
||||||
stays intact
|
|
||||||
|
|
||||||
Configuration migrates from v10 to v11 with a diagnostic log entry;
|
|
||||||
no data is reset. Bilingual (English/German) for both new sections.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.5.4 — WrapText hardening**
|
|
||||||
|
|
||||||
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
|
|
||||||
Span- and index-based control flow. Closes the persistent CodeQL
|
|
||||||
Critical alert "unvalidated local pointer arithmetic" that kept
|
|
||||||
re-firing on every shape of the previous fix.
|
|
||||||
|
|
||||||
Hardening:
|
|
||||||
|
|
||||||
- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount
|
|
||||||
via ArrayPool, validates the actual encoded length against that
|
|
||||||
ceiling, and threads the rest of the algorithm through int offsets
|
|
||||||
instead of raw byte pointers
|
|
||||||
- Pointer arithmetic only happens inside two small private helpers
|
|
||||||
(CalcWordWrap and DrawText) that take the pinned base pointer plus
|
|
||||||
int offsets sourced from the plugin's own logic, not from any
|
|
||||||
virtual-method return
|
|
||||||
- Added a 16 KiB upper bound on the buffer rent to prevent a
|
|
||||||
pathological input from triggering an unbounded ArrayPool allocation
|
|
||||||
|
|
||||||
No user-visible behaviour change. Word-wrap output is byte-identical
|
|
||||||
to v0.5.3.
|
|
||||||
|
|
||||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
|
||||||
|
|
||||||
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
|
|
||||||
|
|
||||||
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
|
|
||||||
encoded byte buffer length via GetByteCount before pointer
|
|
||||||
arithmetic. Single-fix patch on top of v0.5.2.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ public sealed class ExtraChat : IDisposable
|
|||||||
|
|
||||||
internal (string, uint)? ChannelOverride { get; set; }
|
internal (string, uint)? ChannelOverride { get; set; }
|
||||||
|
|
||||||
private Dictionary<string, uint> ChannelCommandColoursInternal { get; set; } = new();
|
// Volatile reference: IPC callbacks (OnChannelCommandColours/OnChannelNames) fire on a
|
||||||
|
// Dalamud-dispatcher thread while the ImGui thread reads the IReadOnlyDictionary projections.
|
||||||
|
// Reference assignment is atomic on x64, but the JIT (especially Mono on Wine/Linux) needs
|
||||||
|
// the volatile barrier to guarantee visibility across threads. See AUDIT-2026-05-05 [SEC-01].
|
||||||
|
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
|
||||||
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
|
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
|
||||||
|
|
||||||
private Dictionary<Guid, string> ChannelNamesInternal { get; set; } = new();
|
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
||||||
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
||||||
|
|
||||||
internal ExtraChat()
|
internal ExtraChat()
|
||||||
@@ -40,9 +44,10 @@ public sealed class ExtraChat : IDisposable
|
|||||||
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
|
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
|
||||||
ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!);
|
ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// no-op
|
// ExtraChat is optional; missing IPC peer is normal when the plugin isn't loaded.
|
||||||
|
Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// After that, the message is enqueued in the PendingAsync queue, which will
|
// After that, the message is enqueued in the PendingAsync queue, which will
|
||||||
// be consumed in a separate thread and perform more processing (emotes,
|
// be consumed in a separate thread and perform more processing (emotes,
|
||||||
// URLs) as well as inserting the message into the database.
|
// URLs) as well as inserting the message into the database.
|
||||||
private Queue<PendingMessage> PendingSync { get; } = [];
|
// LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last
|
||||||
|
// every hook call. Queue<T>.Last() is the LINQ extension and walks the
|
||||||
|
// whole queue (O(n)); LinkedList<T>.Last is an O(1) node reference.
|
||||||
|
private LinkedList<PendingMessage> PendingSync { get; } = [];
|
||||||
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
|
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
|
||||||
private readonly Thread PendingMessageThread;
|
private readonly Thread PendingMessageThread;
|
||||||
private readonly CancellationTokenSource PendingThreadCancellationToken = new();
|
private readonly CancellationTokenSource PendingThreadCancellationToken = new();
|
||||||
@@ -93,6 +96,10 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
Plugin.Log.Debug("Sleeping because PendingMessageThread thread still alive");
|
Plugin.Log.Debug("Sleeping because PendingMessageThread thread still alive");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CancellationTokenSource owns an unmanaged WaitHandle; dispose after the
|
||||||
|
// worker thread has drained, otherwise it leaks across plugin reloads.
|
||||||
|
PendingThreadCancellationToken.Dispose();
|
||||||
|
|
||||||
Store.Dispose();
|
Store.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +120,11 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
LastContentId = contentId;
|
LastContentId = contentId;
|
||||||
|
|
||||||
// Drain the PendingSync queue into the PendingAsync queue.
|
// Drain the PendingSync queue into the PendingAsync queue.
|
||||||
while (PendingSync.TryDequeue(out var pending))
|
while (PendingSync.First is { } first)
|
||||||
PendingAsync.Enqueue(pending);
|
{
|
||||||
|
PendingSync.RemoveFirst();
|
||||||
|
PendingAsync.Enqueue(first.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessPendingMessages(CancellationToken token)
|
private void ProcessPendingMessages(CancellationToken token)
|
||||||
@@ -219,7 +229,7 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
// We delay messages to be handed off to the async processing thread
|
// We delay messages to be handed off to the async processing thread
|
||||||
// in the next tick, otherwise we can't get the content ID from the hook
|
// in the next tick, otherwise we can't get the content ID from the hook
|
||||||
// below.
|
// below.
|
||||||
PendingSync.Enqueue(pendingMessage);
|
PendingSync.AddLast(pendingMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This hook is called immediately after receiving a message with the
|
// This hook is called immediately after receiving a message with the
|
||||||
@@ -231,11 +241,11 @@ internal class MessageManager : IAsyncDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
|
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
|
||||||
if (PendingSync.Count == 0)
|
if (PendingSync.Last is not { } last)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PendingSync.Last().ContentId = contentId;
|
last.Value.ContentId = contentId;
|
||||||
PendingSync.Last().AccountId = accountId;
|
last.Value.AccountId = accountId;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -452,7 +452,10 @@ internal class MessageStore : IDisposable
|
|||||||
// covers any future write paths e.g. webinterface backfill).
|
// covers any future write paths e.g. webinterface backfill).
|
||||||
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
||||||
{
|
{
|
||||||
Plugin.Log.Debug($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
// Verbose-only: this fires for every dropped message, which is
|
||||||
|
// the common case for users with a tight privacy whitelist. Keep
|
||||||
|
// it for diagnostics but stay out of the default xllog stream.
|
||||||
|
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -332,10 +332,19 @@ public sealed class PayloadHandler
|
|||||||
atkBase->SetPosition((short) x, (short) y);
|
atkBase->SetPosition((short) x, (short) y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const float MaxInlineIconSize = 32f;
|
||||||
|
|
||||||
private static void InlineIcon(IDalamudTextureWrap icon)
|
private static void InlineIcon(IDalamudTextureWrap icon)
|
||||||
{
|
{
|
||||||
|
if (icon.Size.X <= 0 || icon.Size.Y <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var width = (float) icon.Size.X;
|
||||||
|
var height = (float) icon.Size.Y;
|
||||||
|
var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height));
|
||||||
|
var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale);
|
||||||
|
|
||||||
var cursor = ImGui.GetCursorPos();
|
var cursor = ImGui.GetCursorPos();
|
||||||
var size = ImGuiHelpers.ScaledVector2(32, 32);
|
|
||||||
ImGui.Image(icon.Handle, size);
|
ImGui.Image(icon.Handle, size);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
|
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
internal ExtraChat ExtraChat { get; }
|
internal ExtraChat ExtraChat { get; }
|
||||||
internal TypingIpc TypingIpc { get; }
|
internal TypingIpc TypingIpc { get; }
|
||||||
internal FontManager FontManager { get; }
|
internal FontManager FontManager { get; }
|
||||||
|
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
||||||
|
|
||||||
internal int DeferredSaveFrames = -1;
|
internal int DeferredSaveFrames = -1;
|
||||||
|
|
||||||
@@ -237,6 +238,27 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v13 → v14 — theme-engine migration. Alle User landen
|
||||||
|
// auf "hellion-arctic" als neues Default-Theme; die alte
|
||||||
|
// HellionThemeEnabled-Flag wird deprecated und nur noch ein Release
|
||||||
|
// als Safety-Net im JSON behalten. Window-Opacity wandert von
|
||||||
|
// HellionThemeWindowOpacity in das neue WindowOpacity-Feld.
|
||||||
|
if (Config.Version < 14)
|
||||||
|
{
|
||||||
|
Config.Theme = "hellion-arctic";
|
||||||
|
#pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0
|
||||||
|
Config.WindowOpacity = Config.HellionThemeWindowOpacity;
|
||||||
|
#pragma warning restore CS0612, CS0618
|
||||||
|
Config.ReduceMotion = false;
|
||||||
|
Config.UseCompactDensity = false;
|
||||||
|
Config.ShowThemeQuickPicker = false;
|
||||||
|
Config.Version = 14;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " +
|
||||||
|
"pick chat2-classic in Settings → Themes for the upstream look");
|
||||||
|
}
|
||||||
|
|
||||||
// Hellion v1.0.0 default tab layout. Five thematically separated
|
// Hellion v1.0.0 default tab layout. Five thematically separated
|
||||||
// tabs: General catches the immediate-surroundings public chat
|
// tabs: General catches the immediate-surroundings public chat
|
||||||
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
||||||
@@ -266,6 +288,14 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
ExtraChat = new ExtraChat();
|
ExtraChat = new ExtraChat();
|
||||||
FontManager = new FontManager();
|
FontManager = new FontManager();
|
||||||
|
|
||||||
|
// v1.1.0 — Theme-Engine init. Custom-Themes liegen in
|
||||||
|
// pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get.
|
||||||
|
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(customThemesDir);
|
||||||
|
SeedExampleThemeIfEmpty(customThemesDir);
|
||||||
|
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
||||||
|
ThemeRegistry.Switch(Config.Theme);
|
||||||
|
|
||||||
MessageManager = new MessageManager(this); // Does it require UI?
|
MessageManager = new MessageManager(this); // Does it require UI?
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
||||||
@@ -529,10 +559,15 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
if (deleted > 0)
|
if (deleted > 0)
|
||||||
{
|
{
|
||||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||||
|
// Run the clear+refilter synchronously on the framework thread.
|
||||||
|
// Earlier this called FilterAllTabsAsync(), which is fire-and-forget
|
||||||
|
// — the .Wait() here would return as soon as the inner Task.Run was
|
||||||
|
// dispatched, racing the next sweep cycle against the still-running
|
||||||
|
// filter pass. See AUDIT-2026-05-05 [QUAL-02].
|
||||||
Framework.Run(() =>
|
Framework.Run(() =>
|
||||||
{
|
{
|
||||||
MessageManager.ClearAllTabs();
|
MessageManager.ClearAllTabs();
|
||||||
MessageManager.FilterAllTabsAsync();
|
MessageManager.FilterAllTabs();
|
||||||
}).Wait();
|
}).Wait();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -554,13 +589,10 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
private void Draw()
|
private void Draw()
|
||||||
{
|
{
|
||||||
// Hellion theme is pushed once per frame here so every plugin window
|
// Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes
|
||||||
// (chat log, settings, viewers, wizard, file dialog) renders with
|
// Theme statt einem deaktivierten Hellion-Theme. Active wird einmal
|
||||||
// the same palette. Skipping the push leaves the upstream Dalamud
|
// pro Frame aus der Registry gelesen.
|
||||||
// look untouched for users who flipped the toggle off.
|
using IDisposable _style = HellionStyle.PushGlobal(ThemeRegistry.Active, Config.WindowOpacity);
|
||||||
using IDisposable? _style = Config.HellionThemeEnabled
|
|
||||||
? HellionStyle.PushGlobal(Config.HellionThemeWindowOpacity)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
ChatLogWindow.BeginFrame();
|
ChatLogWindow.BeginFrame();
|
||||||
|
|
||||||
@@ -571,6 +603,16 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.0.2 — global skip while the New Game+ menu (QuestRedo addon) is
|
||||||
|
// open. Hides every plugin window in one shot (chat log, pop-outs,
|
||||||
|
// settings, db viewer, etc.), matching the LoadingScreens pattern.
|
||||||
|
if (Config.HideInNewGamePlusMenu && GameFunctions.GameFunctions.IsAddonInteractable(GameFunctions.GameFunctions.NewGamePlusAddonName))
|
||||||
|
{
|
||||||
|
ChatLogWindow.FinalizeFrame();
|
||||||
|
TypingIpc.Update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ChatLogWindow.HideStateCheck();
|
ChatLogWindow.HideStateCheck();
|
||||||
|
|
||||||
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
|
||||||
@@ -635,4 +677,36 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
public static bool InBattle => Condition[ConditionFlag.InCombat];
|
public static bool InBattle => Condition[ConditionFlag.InCombat];
|
||||||
public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene];
|
public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene];
|
||||||
public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78];
|
public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78];
|
||||||
|
|
||||||
|
// v1.1.0 — wenn der themes/-Ordner leer ist, schreiben wir die embedded
|
||||||
|
// example-theme.json als Vorlage rein. Bestehende User-Customs werden
|
||||||
|
// nicht angefasst (existing JSONs lassen den Block überspringen).
|
||||||
|
private static void SeedExampleThemeIfEmpty(string dir)
|
||||||
|
{
|
||||||
|
if (Directory.EnumerateFiles(dir, "*.json").Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var examplePath = Path.Combine(dir, "example-theme.json");
|
||||||
|
var resourceStream = typeof(Plugin).Assembly.GetManifestResourceStream("HellionChat.Themes.Builtin.example-theme.json");
|
||||||
|
if (resourceStream is null)
|
||||||
|
{
|
||||||
|
Log.Warning("Themes example template not found in assembly resources; skipping seed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var fileStream = File.Create(examplePath);
|
||||||
|
resourceStream.CopyTo(fileStream);
|
||||||
|
Log.Information($"Seeded example-theme.json into {dir}");
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "Failed to seed example-theme.json; user can create custom themes manually.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
resourceStream.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,37 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database));
|
internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database));
|
||||||
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
|
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
|
||||||
|
|
||||||
|
// v1.1.0 — Settings card-grid overview
|
||||||
|
internal static string Settings_Card_General_Title => Get(nameof(Settings_Card_General_Title));
|
||||||
|
internal static string Settings_Card_General_Subtext => Get(nameof(Settings_Card_General_Subtext));
|
||||||
|
internal static string Settings_Card_Appearance_Title => Get(nameof(Settings_Card_Appearance_Title));
|
||||||
|
internal static string Settings_Card_Appearance_Subtext => Get(nameof(Settings_Card_Appearance_Subtext));
|
||||||
|
internal static string Settings_Card_Themes_Title => Get(nameof(Settings_Card_Themes_Title));
|
||||||
|
internal static string Settings_Card_Themes_Subtext => Get(nameof(Settings_Card_Themes_Subtext));
|
||||||
|
internal static string Settings_Card_Window_Title => Get(nameof(Settings_Card_Window_Title));
|
||||||
|
internal static string Settings_Card_Window_Subtext => Get(nameof(Settings_Card_Window_Subtext));
|
||||||
|
internal static string Settings_Card_Chat_Title => Get(nameof(Settings_Card_Chat_Title));
|
||||||
|
internal static string Settings_Card_Chat_Subtext => Get(nameof(Settings_Card_Chat_Subtext));
|
||||||
|
internal static string Settings_Card_Tabs_Title => Get(nameof(Settings_Card_Tabs_Title));
|
||||||
|
internal static string Settings_Card_Tabs_Subtext => Get(nameof(Settings_Card_Tabs_Subtext));
|
||||||
|
internal static string Settings_Card_Privacy_Title => Get(nameof(Settings_Card_Privacy_Title));
|
||||||
|
internal static string Settings_Card_Privacy_Subtext => Get(nameof(Settings_Card_Privacy_Subtext));
|
||||||
|
internal static string Settings_Card_Database_Title => Get(nameof(Settings_Card_Database_Title));
|
||||||
|
internal static string Settings_Card_Database_Subtext => Get(nameof(Settings_Card_Database_Subtext));
|
||||||
|
internal static string Settings_Card_Information_Title => Get(nameof(Settings_Card_Information_Title));
|
||||||
|
internal static string Settings_Card_Information_Subtext => Get(nameof(Settings_Card_Information_Subtext));
|
||||||
|
|
||||||
|
// v1.1.0 — Themes-Settings-Tab
|
||||||
|
internal static string Settings_Tab_Themes => Get(nameof(Settings_Tab_Themes));
|
||||||
|
internal static string Settings_Themes_Active => Get(nameof(Settings_Themes_Active));
|
||||||
|
internal static string Settings_Themes_BuiltIns => Get(nameof(Settings_Themes_BuiltIns));
|
||||||
|
internal static string Settings_Themes_Custom => Get(nameof(Settings_Themes_Custom));
|
||||||
|
internal static string Settings_Themes_OpenFolder => Get(nameof(Settings_Themes_OpenFolder));
|
||||||
|
internal static string Settings_Themes_ExportActive => Get(nameof(Settings_Themes_ExportActive));
|
||||||
|
internal static string Settings_Themes_ApplyChatColors_Hint => Get(nameof(Settings_Themes_ApplyChatColors_Hint));
|
||||||
|
internal static string Settings_Themes_ApplyChatColors_Apply => Get(nameof(Settings_Themes_ApplyChatColors_Apply));
|
||||||
|
internal static string Settings_Themes_ApplyChatColors_Keep => Get(nameof(Settings_Themes_ApplyChatColors_Keep));
|
||||||
|
|
||||||
// Hellion Chat — General-Tab section headings
|
// Hellion Chat — General-Tab section headings
|
||||||
internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading));
|
internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading));
|
||||||
internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading));
|
internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading));
|
||||||
@@ -261,6 +292,10 @@ internal class HellionStrings
|
|||||||
internal static string Settings_Window_PopOutInputEnabled_Name => Get(nameof(Settings_Window_PopOutInputEnabled_Name));
|
internal static string Settings_Window_PopOutInputEnabled_Name => Get(nameof(Settings_Window_PopOutInputEnabled_Name));
|
||||||
internal static string Settings_Window_PopOutInputEnabled_Description => Get(nameof(Settings_Window_PopOutInputEnabled_Description));
|
internal static string Settings_Window_PopOutInputEnabled_Description => Get(nameof(Settings_Window_PopOutInputEnabled_Description));
|
||||||
|
|
||||||
|
// Hellion Chat — Window position recovery (off-screen safety net)
|
||||||
|
internal static string Settings_Window_ResetPosition_Name => Get(nameof(Settings_Window_ResetPosition_Name));
|
||||||
|
internal static string Settings_Window_ResetPosition_Description => Get(nameof(Settings_Window_ResetPosition_Description));
|
||||||
|
|
||||||
// Hellion Chat — v0.6.0 one-time hint banner shown inside pop-outs
|
// Hellion Chat — v0.6.0 one-time hint banner shown inside pop-outs
|
||||||
internal static string Popout_v060_HintText => Get(nameof(Popout_v060_HintText));
|
internal static string Popout_v060_HintText => Get(nameof(Popout_v060_HintText));
|
||||||
internal static string Popout_v060_HintAck => Get(nameof(Popout_v060_HintAck));
|
internal static string Popout_v060_HintAck => Get(nameof(Popout_v060_HintAck));
|
||||||
|
|||||||
@@ -591,6 +591,12 @@
|
|||||||
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
|
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
|
||||||
<value>Master-Switch: erlaubt direktes Tippen und Absenden in jedem Pop-Out-Fenster (inkl. Auto-Tell-Tabs). Channel-Wechsel im Pop-Out wirkt global wie im Hauptfenster; Text-Buffer und History-Cursor sind pro Pop-Out unabhängig.</value>
|
<value>Master-Switch: erlaubt direktes Tippen und Absenden in jedem Pop-Out-Fenster (inkl. Auto-Tell-Tabs). Channel-Wechsel im Pop-Out wirkt global wie im Hauptfenster; Text-Buffer und History-Cursor sind pro Pop-Out unabhängig.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Window_ResetPosition_Name" xml:space="preserve">
|
||||||
|
<value>Fenster-Position zurücksetzen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Window_ResetPosition_Description" xml:space="preserve">
|
||||||
|
<value>Holt das Chat-Fenster und alle aktiven Pop-Outs zurück in die linke obere Ecke des Hauptmonitors. Hilfreich wenn ein Fenster nach einem Display-Layout-Wechsel außerhalb des sichtbaren Bereichs gelandet ist (Monitor abgezogen, Auflösung geändert). Das Plugin macht außerdem einmal pro Session einen automatischen Bounds-Check, dieser Button ist der manuelle Notausgang falls trotzdem etwas unerreichbar bleibt.</value>
|
||||||
|
</data>
|
||||||
<data name="Popout_v060_HintText" xml:space="preserve">
|
<data name="Popout_v060_HintText" xml:space="preserve">
|
||||||
<value>Neu in v0.6.0: Du kannst jetzt direkt im Pop-Out tippen. Master-Switch in den Fenster-Settings aktivieren.</value>
|
<value>Neu in v0.6.0: Du kannst jetzt direkt im Pop-Out tippen. Master-Switch in den Fenster-Settings aktivieren.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -618,4 +624,85 @@
|
|||||||
<data name="ChatTwoConflictAction" xml:space="preserve">
|
<data name="ChatTwoConflictAction" xml:space="preserve">
|
||||||
<value>Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren.</value>
|
<value>Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Card_General_Title" xml:space="preserve">
|
||||||
|
<value>Allgemein</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_General_Subtext" xml:space="preserve">
|
||||||
|
<value>Sprache und grundlegendes Verhalten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
|
||||||
|
<value>Erscheinungsbild</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Subtext" xml:space="preserve">
|
||||||
|
<value>Fensterdeckkraft, Schriften, Bewegung</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Title" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Subtext" xml:space="preserve">
|
||||||
|
<value>Theme wählen oder eigenes importieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Title" xml:space="preserve">
|
||||||
|
<value>Fenster</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
|
||||||
|
<value>Fensterposition, Rahmen, Hide-Zustände</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Title" xml:space="preserve">
|
||||||
|
<value>Chat</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
|
||||||
|
<value>Chat-Verhalten, Emotes, Auto-Tells</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
|
||||||
|
<value>Tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
|
||||||
|
<value>Tab-Layout, Kanäle, eigene Tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
|
||||||
|
<value>Datenschutz</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
|
||||||
|
<value>Filter, Aufbewahrung, Bereinigung, Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Title" xml:space="preserve">
|
||||||
|
<value>Datenbank</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Subtext" xml:space="preserve">
|
||||||
|
<value>Speicher, Migration, alte Bereinigung</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Title" xml:space="preserve">
|
||||||
|
<value>Information</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
|
||||||
|
<value>Über, Mitwirkende, Support</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Tab_Themes" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Active" xml:space="preserve">
|
||||||
|
<value>Aktiv: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_BuiltIns" xml:space="preserve">
|
||||||
|
<value>Eingebaute Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Custom" xml:space="preserve">
|
||||||
|
<value>Eigene Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_OpenFolder" xml:space="preserve">
|
||||||
|
<value>Themes-Ordner öffnen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ExportActive" xml:space="preserve">
|
||||||
|
<value>Aktives exportieren...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Hint" xml:space="preserve">
|
||||||
|
<value>Dieses Theme schlägt eigene Channel-Farben vor.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Apply" xml:space="preserve">
|
||||||
|
<value>Übernehmen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
|
||||||
|
<value>Behalten</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -591,6 +591,12 @@
|
|||||||
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
|
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
|
||||||
<value>Master switch: lets you type and send messages directly inside every pop-out window (including auto-tell tabs). Channel changes inside a pop-out apply globally just like in the main window; the text buffer and history cursor stay independent per pop-out.</value>
|
<value>Master switch: lets you type and send messages directly inside every pop-out window (including auto-tell tabs). Channel changes inside a pop-out apply globally just like in the main window; the text buffer and history cursor stay independent per pop-out.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Window_ResetPosition_Name" xml:space="preserve">
|
||||||
|
<value>Reset Window Position</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Window_ResetPosition_Description" xml:space="preserve">
|
||||||
|
<value>Snaps the chat window and every active pop-out back to the primary monitor's top-left corner. Useful when a window has drifted off-screen after a display layout change (monitor disconnected, resolution changed). The plugin also runs an automatic bounds check once per session — this button is the manual backup if anything still ends up unreachable.</value>
|
||||||
|
</data>
|
||||||
<data name="Popout_v060_HintText" xml:space="preserve">
|
<data name="Popout_v060_HintText" xml:space="preserve">
|
||||||
<value>New in v0.6.0: you can type directly inside pop-out windows. Toggle the master switch in the window settings to enable it.</value>
|
<value>New in v0.6.0: you can type directly inside pop-out windows. Toggle the master switch in the window settings to enable it.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -618,4 +624,85 @@
|
|||||||
<data name="ChatTwoConflictAction" xml:space="preserve">
|
<data name="ChatTwoConflictAction" xml:space="preserve">
|
||||||
<value>Disable Chat 2 in /xlplugins, then re-enable Hellion Chat.</value>
|
<value>Disable Chat 2 in /xlplugins, then re-enable Hellion Chat.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Settings_Card_General_Title" xml:space="preserve">
|
||||||
|
<value>General</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_General_Subtext" xml:space="preserve">
|
||||||
|
<value>Language and basic behaviour</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
|
||||||
|
<value>Appearance</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Appearance_Subtext" xml:space="preserve">
|
||||||
|
<value>Window opacity, fonts, motion</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Title" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Themes_Subtext" xml:space="preserve">
|
||||||
|
<value>Choose a theme or import your own</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Title" xml:space="preserve">
|
||||||
|
<value>Window</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
|
||||||
|
<value>Window position, frame, hide states</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Title" xml:space="preserve">
|
||||||
|
<value>Chat</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
|
||||||
|
<value>Chat behaviour, emotes, auto-tells</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
|
||||||
|
<value>Tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
|
||||||
|
<value>Tab layout, channels, custom tabs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
|
||||||
|
<value>Privacy</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
|
||||||
|
<value>Filter, retention, cleanup, export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Title" xml:space="preserve">
|
||||||
|
<value>Database</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Database_Subtext" xml:space="preserve">
|
||||||
|
<value>Storage, migration, legacy cleanup</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Title" xml:space="preserve">
|
||||||
|
<value>Information</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
|
||||||
|
<value>About, credits, support</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Tab_Themes" xml:space="preserve">
|
||||||
|
<value>Themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Active" xml:space="preserve">
|
||||||
|
<value>Active: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_BuiltIns" xml:space="preserve">
|
||||||
|
<value>Built-in themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_Custom" xml:space="preserve">
|
||||||
|
<value>Custom themes</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_OpenFolder" xml:space="preserve">
|
||||||
|
<value>Open themes folder</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ExportActive" xml:space="preserve">
|
||||||
|
<value>Export active...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Hint" xml:space="preserve">
|
||||||
|
<value>This theme suggests its own chat channel colours.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Apply" xml:space="preserve">
|
||||||
|
<value>Apply</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
|
||||||
|
<value>Keep current</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -2148,6 +2148,24 @@ namespace HellionChat.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself..
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_ColorSelectedInputChannelButton_Description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Tint channel selector with channel colour.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_ColorSelectedInputChannelButton_Name {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Name", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Chat colours.
|
/// Looks up a localized string similar to Chat colours.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2661,6 +2679,24 @@ namespace HellionChat.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again..
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_HideInNewGamePlusMenu_Description {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Description", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Hide while New Game+ menu is open.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Options_HideInNewGamePlusMenu_Name {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Name", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Hide {0} during loading screens..
|
/// Looks up a localized string similar to Hide {0} during loading screens..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -208,6 +208,12 @@
|
|||||||
<data name="Options_ChatColours_Import">
|
<data name="Options_ChatColours_Import">
|
||||||
<value>Vom Spiel importieren</value>
|
<value>Vom Spiel importieren</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||||
|
<value>Channel-Auswahl-Knopf in Channel-Farbe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||||
|
<value>Der Channel-Auswahl-Knopf neben dem Eingabefeld bekommt die Farbe des aktuell aktiven Channels. Konsistent zur Färbung des Eingabetextes selbst.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Tabs_Tab">
|
<data name="Options_Tabs_Tab">
|
||||||
<value>Kanäle</value>
|
<value>Kanäle</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1190,6 +1196,12 @@ Sie wurden gewarnt.</value>
|
|||||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||||
<value>Blende den Chat während der Kämpfe aus.</value>
|
<value>Blende den Chat während der Kämpfe aus.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||||
|
<value>Während des New-Game+ Menüs ausblenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||||
|
<value>Blendet den Chat aus, solange das New-Game+ Menü geöffnet ist. Schließen des Menüs blendet den Chat wieder ein.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||||
<value>Emote-Statistik</value>
|
<value>Emote-Statistik</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -208,6 +208,12 @@
|
|||||||
<data name="Options_ChatColours_Import">
|
<data name="Options_ChatColours_Import">
|
||||||
<value>Import from game</value>
|
<value>Import from game</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
|
||||||
|
<value>Tint channel selector with channel colour</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
|
||||||
|
<value>The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Tabs_Tab">
|
<data name="Options_Tabs_Tab">
|
||||||
<value>Tabs</value>
|
<value>Tabs</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1189,6 +1195,12 @@
|
|||||||
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
<data name="Options_HideInBattle_Description" xml:space="preserve">
|
||||||
<value>Hide the chat during battles.</value>
|
<value>Hide the chat during battles.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
|
||||||
|
<value>Hide while New Game+ menu is open</value>
|
||||||
|
</data>
|
||||||
|
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
|
||||||
|
<value>Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again.</value>
|
||||||
|
</data>
|
||||||
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
<data name="Options_Emote_EmoteStats" xml:space="preserve">
|
||||||
<value>Emote Stats</value>
|
<value>Emote Stats</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class Chat2Classic
|
||||||
|
{
|
||||||
|
public const string Slug = "chat2-classic";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Chat 2 Klassik",
|
||||||
|
Author: "Upstream (Infi & Anna)",
|
||||||
|
Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#4682B4"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#4682B466"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#3D6E92"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#4682B4"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#5C9DC8"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#4682B4"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#141414"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#1A1A1A"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#202020"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"),
|
||||||
|
Border: ColourUtil.HexToRgba("#404040"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E6E6E6"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#999999"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#666666"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#4682B4")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 0f, ChildRounding: 0f, PopupRounding: 0f,
|
||||||
|
FrameRounding: 0f, GrabRounding: 0f, TabRounding: 0f,
|
||||||
|
ScrollbarRounding: 0f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class EventHorizon
|
||||||
|
{
|
||||||
|
public const string Slug = "event-horizon";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Event Horizon",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#9D5CFF"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#B585FF"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#C9982E"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#E0AB36"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#F2C25C"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#9D5CFF"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#040308"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#0A081A"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#140F23"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#1B1530"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#251D40"),
|
||||||
|
Border: ColourUtil.HexToRgba("#9D5CFF44"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E6E0F5"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#9890B5"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#5A5570"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#26A269"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#ED333B"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#E0AB36"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#9D5CFF")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||||
|
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||||
|
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
|
||||||
|
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
|
||||||
|
// Lila. Channel-Identität bleibt klar erkennbar.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class HellionArctic
|
||||||
|
{
|
||||||
|
public const string Slug = "hellion-arctic";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Hellion Arctic",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Arctic Cyan + Ember Glow on industrial slate. Plugin default.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#0097A7"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#00BED2"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#4DD9E8"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#00BED299"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#E85D04"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#F97316"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#FB923C"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#0097A7"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#070B12"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#0C1220"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#141E30"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#1A2538"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#22303F"),
|
||||||
|
Border: ColourUtil.HexToRgba("#00BED266"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E6F4F1"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#8FA3B5"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#566273"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#00BED2")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 4f, ChildRounding: 3f, PopupRounding: 3f,
|
||||||
|
FrameRounding: 2f, GrabRounding: 2f, TabRounding: 2f,
|
||||||
|
ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den
|
||||||
|
// blauen Channels (Party/FC). Channel-Identität bleibt klar.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80C0E8"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFB870"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4DD9E8"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80C0E8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FFC080"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFE066"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80C0E8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class MintGrove
|
||||||
|
{
|
||||||
|
public const string Slug = "mint-grove";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Mint Grove",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Mint Green + Honey Amber auf Deep Forest. Naturthemen-tauglich.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#3CB371"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#5DD39E"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#8FE0B8"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#5DD39E99"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#F4C870"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#F9D580"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#FCDD93"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#5DD39E"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#0A1410"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#10201A"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#162B22"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#1E372B"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#284335"),
|
||||||
|
Border: ColourUtil.HexToRgba("#5DD39E55"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#E8F5EA"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#9BB5A5"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#5C6F65"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#5DD39E"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#5DA9C7")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 5f, ChildRounding: 4f, PopupRounding: 4f,
|
||||||
|
FrameRounding: 3f, GrabRounding: 3f, TabRounding: 3f,
|
||||||
|
ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie,
|
||||||
|
// Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität
|
||||||
|
// bleibt erhalten für Erkennbarkeit.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F098C8"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F098C8"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80B8D0"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B070"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#80C8B0"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80B8D0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#8FE0B8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC80"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F9D580"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0A0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80B8D0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A89DC0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#F098C8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A8C8"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C088"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9BB5A5"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
internal static class MoonlitBloom
|
||||||
|
{
|
||||||
|
public const string Slug = "moonlit-bloom";
|
||||||
|
|
||||||
|
public static Theme Build() => new(
|
||||||
|
Slug: Slug,
|
||||||
|
Name: "Moonlit Bloom",
|
||||||
|
Author: "Hellion Online Media",
|
||||||
|
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
|
||||||
|
Colors: new ThemeColors(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
|
||||||
|
Primary: ColourUtil.HexToRgba("#E374E8"),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
|
||||||
|
Accent: ColourUtil.HexToRgba("#9CCB7C"),
|
||||||
|
AccentLight: ColourUtil.HexToRgba("#B6E297"),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba("#E374E8"),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
|
||||||
|
ChildBg: ColourUtil.HexToRgba("#15122B"),
|
||||||
|
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
|
||||||
|
Surface: ColourUtil.HexToRgba("#28224A"),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
|
||||||
|
Border: ColourUtil.HexToRgba("#E374E844"),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
|
||||||
|
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
|
||||||
|
TextDim: ColourUtil.HexToRgba("#554B6E"),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba("#6278FF")
|
||||||
|
),
|
||||||
|
Layout: new ThemeLayout(
|
||||||
|
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
|
||||||
|
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
|
||||||
|
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
|
||||||
|
),
|
||||||
|
Typography: new ThemeTypography(),
|
||||||
|
IsBuiltIn: true,
|
||||||
|
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
|
||||||
|
{
|
||||||
|
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
|
||||||
|
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
|
||||||
|
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
|
||||||
|
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
|
||||||
|
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||||
|
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||||
|
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||||
|
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||||
|
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||||
|
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
|
||||||
|
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||||
|
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"slug": "example-custom",
|
||||||
|
"name": "Example Custom",
|
||||||
|
"author": "You",
|
||||||
|
"description": "Starting template — duplicate, rename, edit colors and reload.",
|
||||||
|
"colors": {
|
||||||
|
"primaryDark": "#0097A7",
|
||||||
|
"primary": "#00BED2",
|
||||||
|
"primaryLight": "#4DD9E8",
|
||||||
|
"primaryGlow": "#00BED299",
|
||||||
|
"accentDark": "#E85D04",
|
||||||
|
"accent": "#F97316",
|
||||||
|
"accentLight": "#FB923C",
|
||||||
|
"identity": "#0097A7",
|
||||||
|
"windowBg": "#070B12",
|
||||||
|
"childBg": "#0C1220",
|
||||||
|
"frameBg": "#141E30",
|
||||||
|
"surface": "#1A2538",
|
||||||
|
"surfaceHover": "#22303F",
|
||||||
|
"border": "#00BED266",
|
||||||
|
"textPrimary": "#E6F4F1",
|
||||||
|
"textMuted": "#8FA3B5",
|
||||||
|
"textDim": "#566273",
|
||||||
|
"statusSuccess": "#5CB85C",
|
||||||
|
"statusDanger": "#D9534F",
|
||||||
|
"statusWarning": "#F0AD4E",
|
||||||
|
"statusInfo": "#00BED2"
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"windowRounding": 4,
|
||||||
|
"childRounding": 3,
|
||||||
|
"popupRounding": 3,
|
||||||
|
"frameRounding": 2,
|
||||||
|
"grabRounding": 2,
|
||||||
|
"tabRounding": 2,
|
||||||
|
"scrollbarRounding": 2,
|
||||||
|
"windowBorderSize": 1,
|
||||||
|
"frameBorderSize": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
public sealed record Theme(
|
||||||
|
string Slug,
|
||||||
|
string Name,
|
||||||
|
string Author,
|
||||||
|
string Description,
|
||||||
|
ThemeColors Colors,
|
||||||
|
ThemeLayout Layout,
|
||||||
|
ThemeTypography Typography,
|
||||||
|
bool IsBuiltIn,
|
||||||
|
ThemeChatColors? ChatColors = null
|
||||||
|
);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using HellionChat.Code;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Optional pro Theme. Wenn ein Theme ChatColors mitliefert, kann der
|
||||||
|
// User sie per Klick im Themes-Tab auf Configuration.ChatColours anwenden.
|
||||||
|
// Ein Theme ohne ChatColors (z.B. chat2-classic) lässt die User-Channel-
|
||||||
|
// Farben unverändert.
|
||||||
|
public sealed record ThemeChatColors(
|
||||||
|
IReadOnlyDictionary<ChatType, uint> Channels
|
||||||
|
);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Color-Werte als 0xRRGGBBAA, RgbaToAbgr handled den Byte-Swap zu ImGui.
|
||||||
|
public sealed record ThemeColors(
|
||||||
|
uint PrimaryDark,
|
||||||
|
uint Primary,
|
||||||
|
uint PrimaryLight,
|
||||||
|
uint PrimaryGlow,
|
||||||
|
|
||||||
|
uint AccentDark,
|
||||||
|
uint Accent,
|
||||||
|
uint AccentLight,
|
||||||
|
|
||||||
|
uint Identity,
|
||||||
|
|
||||||
|
uint WindowBg,
|
||||||
|
uint ChildBg,
|
||||||
|
uint FrameBg,
|
||||||
|
uint Surface,
|
||||||
|
uint SurfaceHover,
|
||||||
|
uint Border,
|
||||||
|
|
||||||
|
uint TextPrimary,
|
||||||
|
uint TextMuted,
|
||||||
|
uint TextDim,
|
||||||
|
|
||||||
|
uint StatusSuccess,
|
||||||
|
uint StatusDanger,
|
||||||
|
uint StatusWarning,
|
||||||
|
uint StatusInfo
|
||||||
|
);
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
internal static class ThemeJsonLoader
|
||||||
|
{
|
||||||
|
public const int SupportedSchemaVersion = 1;
|
||||||
|
|
||||||
|
public static Theme LoadFromString(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
throw new FormatException("Theme JSON is empty");
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
try { doc = JsonDocument.Parse(json); }
|
||||||
|
catch (JsonException ex) { throw new FormatException("Theme JSON is not valid JSON", ex); }
|
||||||
|
|
||||||
|
using (doc)
|
||||||
|
{
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
var schemaVersion = ReadInt(root, "schemaVersion");
|
||||||
|
if (schemaVersion != SupportedSchemaVersion)
|
||||||
|
throw new FormatException($"Unsupported schemaVersion {schemaVersion}; expected {SupportedSchemaVersion}");
|
||||||
|
|
||||||
|
var slug = ReadString(root, "slug");
|
||||||
|
var name = ReadString(root, "name");
|
||||||
|
var author = ReadString(root, "author");
|
||||||
|
var description = ReadString(root, "description");
|
||||||
|
|
||||||
|
var colors = ReadColors(root.GetProperty("colors"));
|
||||||
|
var layout = ReadLayout(root.GetProperty("layout"));
|
||||||
|
|
||||||
|
ThemeChatColors? chatColors = null;
|
||||||
|
if (root.TryGetProperty("chatChannels", out var ch) && ch.ValueKind == JsonValueKind.Object)
|
||||||
|
chatColors = ReadChatColors(ch);
|
||||||
|
|
||||||
|
return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false, ChatColors: chatColors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThemeChatColors ReadChatColors(JsonElement el)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<HellionChat.Code.ChatType, uint>();
|
||||||
|
foreach (var prop in el.EnumerateObject())
|
||||||
|
{
|
||||||
|
// Property-Name ist der ChatType-Name als String (z.B. "Say", "Tell"),
|
||||||
|
// Value ist Hex wie bei den Theme-Colors. Unbekannte Channel-Names
|
||||||
|
// werden still übersprungen — Forward-Compat falls SE neue Channels
|
||||||
|
// einführt.
|
||||||
|
if (!Enum.TryParse<HellionChat.Code.ChatType>(prop.Name, ignoreCase: true, out var channel))
|
||||||
|
continue;
|
||||||
|
if (prop.Value.ValueKind != JsonValueKind.String)
|
||||||
|
continue;
|
||||||
|
var hex = prop.Value.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(hex))
|
||||||
|
continue;
|
||||||
|
dict[channel] = HellionChat.Util.ColourUtil.HexToRgba(hex);
|
||||||
|
}
|
||||||
|
return new ThemeChatColors(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Theme LoadFromFile(string path)
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return LoadFromString(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ThemeColors ReadColors(JsonElement el) => new(
|
||||||
|
PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")),
|
||||||
|
Primary: ColourUtil.HexToRgba(ReadString(el, "primary")),
|
||||||
|
PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")),
|
||||||
|
PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")),
|
||||||
|
|
||||||
|
AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")),
|
||||||
|
Accent: ColourUtil.HexToRgba(ReadString(el, "accent")),
|
||||||
|
AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")),
|
||||||
|
|
||||||
|
Identity: ColourUtil.HexToRgba(ReadString(el, "identity")),
|
||||||
|
|
||||||
|
WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")),
|
||||||
|
ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")),
|
||||||
|
FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")),
|
||||||
|
Surface: ColourUtil.HexToRgba(ReadString(el, "surface")),
|
||||||
|
SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")),
|
||||||
|
Border: ColourUtil.HexToRgba(ReadString(el, "border")),
|
||||||
|
|
||||||
|
TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")),
|
||||||
|
TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")),
|
||||||
|
TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")),
|
||||||
|
|
||||||
|
StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")),
|
||||||
|
StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")),
|
||||||
|
StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")),
|
||||||
|
StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo"))
|
||||||
|
);
|
||||||
|
|
||||||
|
private static ThemeLayout ReadLayout(JsonElement el) => new(
|
||||||
|
WindowRounding: ReadFloat(el, "windowRounding"),
|
||||||
|
ChildRounding: ReadFloat(el, "childRounding"),
|
||||||
|
PopupRounding: ReadFloat(el, "popupRounding"),
|
||||||
|
FrameRounding: ReadFloat(el, "frameRounding"),
|
||||||
|
GrabRounding: ReadFloat(el, "grabRounding"),
|
||||||
|
TabRounding: ReadFloat(el, "tabRounding"),
|
||||||
|
ScrollbarRounding: ReadFloat(el, "scrollbarRounding"),
|
||||||
|
WindowBorderSize: ReadFloat(el, "windowBorderSize"),
|
||||||
|
FrameBorderSize: ReadFloat(el, "frameBorderSize")
|
||||||
|
);
|
||||||
|
|
||||||
|
private static string ReadString(JsonElement el, string name)
|
||||||
|
{
|
||||||
|
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.String)
|
||||||
|
throw new FormatException($"Theme JSON missing string property '{name}'");
|
||||||
|
return v.GetString() ?? throw new FormatException($"Theme JSON property '{name}' is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadInt(JsonElement el, string name)
|
||||||
|
{
|
||||||
|
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number)
|
||||||
|
throw new FormatException($"Theme JSON missing number property '{name}'");
|
||||||
|
return v.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ReadFloat(JsonElement el, string name)
|
||||||
|
{
|
||||||
|
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number)
|
||||||
|
throw new FormatException($"Theme JSON missing number property '{name}'");
|
||||||
|
return (float)v.GetDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
internal static class ThemeJsonWriter
|
||||||
|
{
|
||||||
|
public static string Serialize(Theme theme)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
using (var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = true }))
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
writer.WriteNumber("schemaVersion", ThemeJsonLoader.SupportedSchemaVersion);
|
||||||
|
writer.WriteString("slug", theme.Slug);
|
||||||
|
writer.WriteString("name", theme.Name);
|
||||||
|
writer.WriteString("author", theme.Author);
|
||||||
|
writer.WriteString("description", theme.Description);
|
||||||
|
|
||||||
|
writer.WriteStartObject("colors");
|
||||||
|
WriteColor(writer, "primaryDark", theme.Colors.PrimaryDark);
|
||||||
|
WriteColor(writer, "primary", theme.Colors.Primary);
|
||||||
|
WriteColor(writer, "primaryLight", theme.Colors.PrimaryLight);
|
||||||
|
WriteColor(writer, "primaryGlow", theme.Colors.PrimaryGlow);
|
||||||
|
WriteColor(writer, "accentDark", theme.Colors.AccentDark);
|
||||||
|
WriteColor(writer, "accent", theme.Colors.Accent);
|
||||||
|
WriteColor(writer, "accentLight", theme.Colors.AccentLight);
|
||||||
|
WriteColor(writer, "identity", theme.Colors.Identity);
|
||||||
|
WriteColor(writer, "windowBg", theme.Colors.WindowBg);
|
||||||
|
WriteColor(writer, "childBg", theme.Colors.ChildBg);
|
||||||
|
WriteColor(writer, "frameBg", theme.Colors.FrameBg);
|
||||||
|
WriteColor(writer, "surface", theme.Colors.Surface);
|
||||||
|
WriteColor(writer, "surfaceHover", theme.Colors.SurfaceHover);
|
||||||
|
WriteColor(writer, "border", theme.Colors.Border);
|
||||||
|
WriteColor(writer, "textPrimary", theme.Colors.TextPrimary);
|
||||||
|
WriteColor(writer, "textMuted", theme.Colors.TextMuted);
|
||||||
|
WriteColor(writer, "textDim", theme.Colors.TextDim);
|
||||||
|
WriteColor(writer, "statusSuccess", theme.Colors.StatusSuccess);
|
||||||
|
WriteColor(writer, "statusDanger", theme.Colors.StatusDanger);
|
||||||
|
WriteColor(writer, "statusWarning", theme.Colors.StatusWarning);
|
||||||
|
WriteColor(writer, "statusInfo", theme.Colors.StatusInfo);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
|
||||||
|
writer.WriteStartObject("layout");
|
||||||
|
writer.WriteNumber("windowRounding", theme.Layout.WindowRounding);
|
||||||
|
writer.WriteNumber("childRounding", theme.Layout.ChildRounding);
|
||||||
|
writer.WriteNumber("popupRounding", theme.Layout.PopupRounding);
|
||||||
|
writer.WriteNumber("frameRounding", theme.Layout.FrameRounding);
|
||||||
|
writer.WriteNumber("grabRounding", theme.Layout.GrabRounding);
|
||||||
|
writer.WriteNumber("tabRounding", theme.Layout.TabRounding);
|
||||||
|
writer.WriteNumber("scrollbarRounding", theme.Layout.ScrollbarRounding);
|
||||||
|
writer.WriteNumber("windowBorderSize", theme.Layout.WindowBorderSize);
|
||||||
|
writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
|
||||||
|
if (theme.ChatColors is { Channels.Count: > 0 } cc)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject("chatChannels");
|
||||||
|
foreach (var kvp in cc.Channels)
|
||||||
|
writer.WriteString(kvp.Key.ToString(), $"#{kvp.Value:X8}");
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Text.Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteColor(Utf8JsonWriter writer, string key, uint rgba)
|
||||||
|
{
|
||||||
|
writer.WriteString(key, $"#{rgba:X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Layout-Werte spiegeln die ImGuiStyleVar-Slots, die HellionStyle pusht.
|
||||||
|
public sealed record ThemeLayout(
|
||||||
|
float WindowRounding,
|
||||||
|
float ChildRounding,
|
||||||
|
float PopupRounding,
|
||||||
|
float FrameRounding,
|
||||||
|
float GrabRounding,
|
||||||
|
float TabRounding,
|
||||||
|
float ScrollbarRounding,
|
||||||
|
float WindowBorderSize,
|
||||||
|
float FrameBorderSize
|
||||||
|
);
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using HellionChat.Themes.Builtin;
|
||||||
|
|
||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
public sealed class ThemeRegistry
|
||||||
|
{
|
||||||
|
public const string DefaultSlug = HellionArctic.Slug;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Theme> _builtIns;
|
||||||
|
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly string? _customThemesDir;
|
||||||
|
private Theme _active;
|
||||||
|
|
||||||
|
public ThemeRegistry(string? customThemesDir = null)
|
||||||
|
{
|
||||||
|
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||||
|
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||||
|
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||||
|
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
||||||
|
{ MintGrove.Slug, MintGrove.Build() },
|
||||||
|
};
|
||||||
|
_active = _builtIns[DefaultSlug];
|
||||||
|
_customThemesDir = customThemesDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Theme Active => _active;
|
||||||
|
|
||||||
|
public Theme Get(string slug)
|
||||||
|
{
|
||||||
|
if (_builtIns.TryGetValue(slug, out var b)) return b;
|
||||||
|
|
||||||
|
var custom = LoadCustomBySlug(slug);
|
||||||
|
if (custom != null) return custom;
|
||||||
|
|
||||||
|
return _builtIns[DefaultSlug];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Theme> AllBuiltIns() => _builtIns.Values;
|
||||||
|
|
||||||
|
public IEnumerable<Theme> AllCustom() => RefreshCustomCache();
|
||||||
|
|
||||||
|
public void Switch(string slug) => _active = Get(slug);
|
||||||
|
|
||||||
|
// Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit
|
||||||
|
// LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup
|
||||||
|
// neu eingelesen.
|
||||||
|
private Theme? LoadCustomBySlug(string slug)
|
||||||
|
{
|
||||||
|
if (_customThemesDir is null) return null;
|
||||||
|
if (!Directory.Exists(_customThemesDir)) return null;
|
||||||
|
|
||||||
|
foreach (var theme in RefreshCustomCache())
|
||||||
|
if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return theme;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Theme> RefreshCustomCache()
|
||||||
|
{
|
||||||
|
if (_customThemesDir is null || !Directory.Exists(_customThemesDir))
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
var seenSlugs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var path in Directory.EnumerateFiles(_customThemesDir, "*.json"))
|
||||||
|
{
|
||||||
|
Theme? theme = null;
|
||||||
|
var stamp = File.GetLastWriteTimeUtc(path);
|
||||||
|
var key = path;
|
||||||
|
if (_customCache.TryGetValue(key, out var cached) && cached.Stamp == stamp)
|
||||||
|
{
|
||||||
|
theme = cached.Theme;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
theme = ThemeJsonLoader.LoadFromFile(path);
|
||||||
|
_customCache[key] = (theme, stamp);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Logging passiert in Plugin.cs durch den Aufrufer; hier still
|
||||||
|
// ignorieren, damit ein einzelnes kaputtes JSON nicht alle
|
||||||
|
// Custom-Themes blockt.
|
||||||
|
_ = ex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme is not null && seenSlugs.Add(theme.Slug))
|
||||||
|
yield return theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace HellionChat.Themes;
|
||||||
|
|
||||||
|
// Optional pro Theme. v1.1.0 nutzt das nicht aktiv; ist als Erweiterungspunkt
|
||||||
|
// für zukünftige Theme-Slots vorbereitet.
|
||||||
|
public sealed record ThemeTypography(
|
||||||
|
float? OverrideGlobalFontSizePt = null,
|
||||||
|
float? OverrideSymbolsFontSizePt = null
|
||||||
|
);
|
||||||
@@ -104,7 +104,7 @@ public sealed class ChatInputBar
|
|||||||
// window's logic but operates on _state.HistoryCursor and the shared
|
// window's logic but operates on _state.HistoryCursor and the shared
|
||||||
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
|
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
|
||||||
// 0 = oldest, Count-1 = newest.
|
// 0 = oldest, Count-1 = newest.
|
||||||
private unsafe int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
|
private int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
|
||||||
{
|
{
|
||||||
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
|
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public sealed class ChatLogWindow : Window
|
|||||||
|
|
||||||
internal Plugin Plugin { get; }
|
internal Plugin Plugin { get; }
|
||||||
|
|
||||||
|
private readonly CommandWrapper _clearHellionCommand;
|
||||||
|
private readonly CommandWrapper _hellionCommand;
|
||||||
|
|
||||||
internal bool ScreenshotMode;
|
internal bool ScreenshotMode;
|
||||||
private string Salt { get; }
|
private string Salt { get; }
|
||||||
|
|
||||||
@@ -66,6 +69,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
public Vector2 LastWindowPos { get; private set; } = Vector2.Zero;
|
public Vector2 LastWindowPos { get; private set; } = Vector2.Zero;
|
||||||
public Vector2 LastWindowSize { get; private set; } = Vector2.Zero;
|
public Vector2 LastWindowSize { get; private set; } = Vector2.Zero;
|
||||||
|
|
||||||
|
// Window position recovery: guards against off-screen positions after a
|
||||||
|
// display layout change (monitor disconnected, resolution changed). On
|
||||||
|
// the first draw after plugin load we run a one-shot bounds check to see
|
||||||
|
// whether the stored position still overlaps any visible viewport area.
|
||||||
|
// The manual reset button in the settings forces the position regardless.
|
||||||
|
private bool DidOnLoadBoundsCheck;
|
||||||
|
internal bool RequestPositionReset { get; set; }
|
||||||
|
|
||||||
public unsafe ImGuiViewport* LastViewport;
|
public unsafe ImGuiViewport* LastViewport;
|
||||||
private bool WasDocked;
|
private bool WasDocked;
|
||||||
|
|
||||||
@@ -102,8 +113,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
SetUpTextCommandChannels();
|
SetUpTextCommandChannels();
|
||||||
SetUpAllCommands();
|
SetUpAllCommands();
|
||||||
|
|
||||||
Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log").Execute += ClearLog;
|
// Cache the registered wrapper instances so Dispose can detach the same
|
||||||
Plugin.Commands.Register("/hellion").Execute += ToggleChat;
|
// event objects the constructor attached to, without going through
|
||||||
|
// Register() again (which would re-create the wrapper if the command
|
||||||
|
// happened to be missing from the dictionary).
|
||||||
|
_clearHellionCommand = Plugin.Commands.Register("/clearhellion", "Clear the Hellion Chat log");
|
||||||
|
_hellionCommand = Plugin.Commands.Register("/hellion");
|
||||||
|
_clearHellionCommand.Execute += ClearLog;
|
||||||
|
_hellionCommand.Execute += ToggleChat;
|
||||||
|
|
||||||
Plugin.ClientState.Login += Login;
|
Plugin.ClientState.Login += Login;
|
||||||
Plugin.ClientState.Logout += Logout;
|
Plugin.ClientState.Logout += Logout;
|
||||||
@@ -118,8 +135,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
|
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "ActionDetail", PayloadHandler.MoveTooltip);
|
||||||
Plugin.ClientState.Logout -= Logout;
|
Plugin.ClientState.Logout -= Logout;
|
||||||
Plugin.ClientState.Login -= Login;
|
Plugin.ClientState.Login -= Login;
|
||||||
Plugin.Commands.Register("/hellion").Execute -= ToggleChat;
|
_hellionCommand.Execute -= ToggleChat;
|
||||||
Plugin.Commands.Register("/clearhellion").Execute -= ClearLog;
|
_clearHellionCommand.Execute -= ClearLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Logout(int _, int __)
|
private void Logout(int _, int __)
|
||||||
@@ -270,9 +287,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
case "hide":
|
case "hide":
|
||||||
CurrentHideState = HideState.User;
|
CurrentHideState = HideState.User;
|
||||||
|
Plugin.Log.Verbose("HideState: → User (chat hide command)");
|
||||||
break;
|
break;
|
||||||
case "show":
|
case "show":
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose("HideState: → None (chat show command)");
|
||||||
break;
|
break;
|
||||||
case "toggle":
|
case "toggle":
|
||||||
CurrentHideState = CurrentHideState switch
|
CurrentHideState = CurrentHideState switch
|
||||||
@@ -282,6 +301,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
HideState.None => HideState.User,
|
HideState.None => HideState.User,
|
||||||
_ => CurrentHideState,
|
_ => CurrentHideState,
|
||||||
};
|
};
|
||||||
|
Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,30 +418,48 @@ public sealed class ChatLogWindow : Window
|
|||||||
{
|
{
|
||||||
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
||||||
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
|
Plugin.Log.Verbose("HideState: None → Battle");
|
||||||
|
}
|
||||||
|
|
||||||
// If the chat is hidden because of battle, we reset it here
|
// If the chat is hidden because of battle, we reset it here
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose("HideState: Battle → None");
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||||
if (Plugin.Config.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
if (Plugin.Config.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
||||||
{
|
{
|
||||||
if (Plugin.Functions.Chat.CheckHideFlags())
|
if (Plugin.Functions.Chat.CheckHideFlags())
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
|
Plugin.Log.Verbose("HideState: None → Cutscene");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
||||||
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
||||||
|
{
|
||||||
|
Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
||||||
if (CurrentHideState == HideState.Cutscene && Activate)
|
if (CurrentHideState == HideState.Cutscene && Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
|
Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||||
|
}
|
||||||
|
|
||||||
// if the user hid the chat and is now activating chat, reset the hide state
|
// if the user hid the chat and is now activating chat, reset the hide state
|
||||||
if (CurrentHideState == HideState.User && Activate)
|
if (CurrentHideState == HideState.User && Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose("HideState: User → None (activate)");
|
||||||
|
}
|
||||||
|
|
||||||
if (CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Plugin.Config.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn))
|
if (CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Plugin.Config.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn))
|
||||||
{
|
{
|
||||||
@@ -456,9 +494,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||||
|
|
||||||
if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked)
|
if (LastViewport == ImGuiHelpers.MainViewport.Handle && !WasDocked)
|
||||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
BgAlpha = Plugin.Config.WindowOpacity;
|
||||||
? Plugin.Config.HellionThemeWindowOpacity
|
|
||||||
: Plugin.Config.WindowAlpha / 100f;
|
|
||||||
|
|
||||||
LastViewport = ImGui.GetWindowViewport().Handle;
|
LastViewport = ImGui.GetWindowViewport().Handle;
|
||||||
WasDocked = ImGui.IsWindowDocked();
|
WasDocked = ImGui.IsWindowDocked();
|
||||||
@@ -490,8 +526,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
if (Plugin.Config.KeepInputFocus && Activate)
|
if (Plugin.Config.KeepInputFocus && Activate)
|
||||||
ImGui.SetWindowFocus(WindowName);
|
ImGui.SetWindowFocus(WindowName);
|
||||||
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
// Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
|
// zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw
|
||||||
|
// pusht das aktive Hellion-Theme global; ChatLogWindow zeichnet sich
|
||||||
|
// damit konsistent zu Settings/Pop-Out/Wizard. Wer den Upstream-Look
|
||||||
|
// will, wählt das Built-In-Theme "Chat 2 Klassik" in Settings → Themes.
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PostDraw()
|
public override void PostDraw()
|
||||||
@@ -502,9 +541,6 @@ public sealed class ChatLogWindow : Window
|
|||||||
// doesn't get called if the input is disabled.
|
// doesn't get called if the input is disabled.
|
||||||
if (Plugin.CurrentTab.InputDisabled)
|
if (Plugin.CurrentTab.InputDisabled)
|
||||||
Activate = false;
|
Activate = false;
|
||||||
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
@@ -542,6 +578,22 @@ public sealed class ChatLogWindow : Window
|
|||||||
LastWindowSize = currentSize;
|
LastWindowSize = currentSize;
|
||||||
LastWindowPos = ImGui.GetWindowPos();
|
LastWindowPos = ImGui.GetWindowPos();
|
||||||
|
|
||||||
|
// Window position recovery. Manual reset takes precedence and snaps
|
||||||
|
// the window to the safe default unconditionally; the one-shot
|
||||||
|
// on-load check only fires when the persisted position has no
|
||||||
|
// overlap with any visible viewport area.
|
||||||
|
if (RequestPositionReset)
|
||||||
|
{
|
||||||
|
RequestPositionReset = false;
|
||||||
|
DidOnLoadBoundsCheck = true;
|
||||||
|
ApplySafeDefaultPosition("manual-reset");
|
||||||
|
}
|
||||||
|
else if (!DidOnLoadBoundsCheck)
|
||||||
|
{
|
||||||
|
DidOnLoadBoundsCheck = true;
|
||||||
|
EnsureWindowOnScreen("on-load");
|
||||||
|
}
|
||||||
|
|
||||||
if (resized)
|
if (resized)
|
||||||
LastResize.Restart();
|
LastResize.Restart();
|
||||||
|
|
||||||
@@ -552,10 +604,11 @@ public sealed class ChatLogWindow : Window
|
|||||||
Plugin.InputPreview.CalculatePreview();
|
Plugin.InputPreview.CalculatePreview();
|
||||||
|
|
||||||
// Hellion Chat v0.6.1 — render the one-time hint banner first so it
|
// Hellion Chat v0.6.1 — render the one-time hint banner first so it
|
||||||
// sits above the tab area / sidebar in full window width. Stash the
|
// sits above the tab area / sidebar in full window width. ImGui's
|
||||||
// height for GetRemainingHeightForMessageLog so the message log
|
// GetContentRegionAvail subtracts its height automatically because the
|
||||||
// shrinks accordingly while the banner is visible.
|
// cursor advances past it before the message log calls
|
||||||
_v061HintBannerHeight = DrawV061HintBannerIfNeeded();
|
// GetRemainingHeightForMessageLog, so we don't track the height here.
|
||||||
|
DrawV061HintBannerIfNeeded();
|
||||||
|
|
||||||
if (Plugin.Config.SidebarTabView)
|
if (Plugin.Config.SidebarTabView)
|
||||||
DrawTabSidebar();
|
DrawTabSidebar();
|
||||||
@@ -576,9 +629,40 @@ public sealed class ChatLogWindow : Window
|
|||||||
DrawChannelName(activeTab);
|
DrawChannelName(activeTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v1.0.2 — compute inputColour up front so the channel selector button
|
||||||
|
// can also tint with it (existing input-text push remains below).
|
||||||
|
var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType();
|
||||||
|
var isCommand = Chat.Trim().StartsWith('/');
|
||||||
|
if (isCommand)
|
||||||
|
{
|
||||||
|
var command = Chat.Split(' ')[0];
|
||||||
|
if (TextCommandChannels.TryGetValue(command, out var channel))
|
||||||
|
inputType = channel;
|
||||||
|
|
||||||
|
if (!IsValidCommand(command))
|
||||||
|
inputType = ChatType.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor();
|
||||||
|
|
||||||
|
if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour))
|
||||||
|
inputColour = overrideColour;
|
||||||
|
|
||||||
|
if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour))
|
||||||
|
inputColour = ecColour;
|
||||||
|
|
||||||
var beforeIcon = ImGui.GetCursorPos();
|
var beforeIcon = ImGui.GetCursorPos();
|
||||||
|
|
||||||
|
var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
|
||||||
|
var selectorAbgr = tintSelector ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0u;
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, selectorAbgr, tintSelector))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.AdjustBrightness(selectorAbgr, 1.15f), tintSelector))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.AdjustBrightness(selectorAbgr, 0.85f), tintSelector))
|
||||||
|
{
|
||||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null)
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null)
|
||||||
ImGui.OpenPopup(ChatChannelPicker);
|
ImGui.OpenPopup(ChatChannelPicker);
|
||||||
|
}
|
||||||
|
|
||||||
if (activeTab.Channel is not null && ImGui.IsItemHovered())
|
if (activeTab.Channel is not null && ImGui.IsItemHovered())
|
||||||
ImGuiUtil.Tooltip(Language.ChatLog_SwitcherDisabled);
|
ImGuiUtil.Tooltip(Language.ChatLog_SwitcherDisabled);
|
||||||
@@ -602,27 +686,7 @@ public sealed class ChatLogWindow : Window
|
|||||||
var buttonsRight = (showNovice ? 1 : 0) + (Plugin.Config.ShowHideButton ? 1 : 0);
|
var buttonsRight = (showNovice ? 1 : 0) + (Plugin.Config.ShowHideButton ? 1 : 0);
|
||||||
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth * (1 + buttonsRight);
|
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth * (1 + buttonsRight);
|
||||||
|
|
||||||
var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType();
|
|
||||||
var isCommand = Chat.Trim().StartsWith('/');
|
|
||||||
if (isCommand)
|
|
||||||
{
|
|
||||||
var command = Chat.Split(' ')[0];
|
|
||||||
if (TextCommandChannels.TryGetValue(command, out var channel))
|
|
||||||
inputType = channel;
|
|
||||||
|
|
||||||
if (!IsValidCommand(command))
|
|
||||||
inputType = ChatType.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalColor = ImGui.GetColorU32(ImGuiCol.Text);
|
var normalColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||||
var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor();
|
|
||||||
|
|
||||||
if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour))
|
|
||||||
inputColour = overrideColour;
|
|
||||||
|
|
||||||
if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour))
|
|
||||||
inputColour = ecColour;
|
|
||||||
|
|
||||||
var push = inputColour != null;
|
var push = inputColour != null;
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push))
|
using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push))
|
||||||
{
|
{
|
||||||
@@ -1484,11 +1548,14 @@ public sealed class ChatLogWindow : Window
|
|||||||
var startY = ImGui.GetCursorPosY();
|
var startY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
|
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
|
||||||
ImGui.PushStyleColor(ImGuiCol.ChildBg, bg);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
|
||||||
|
|
||||||
var dismiss = false;
|
var dismiss = false;
|
||||||
var openSettings = false;
|
var openSettings = false;
|
||||||
|
// RAII for the style stack so an early return in this block
|
||||||
|
// (or a later refactor that introduces one) can never leave the
|
||||||
|
// ImGui style stack unbalanced. Matches the convention used
|
||||||
|
// elsewhere in this file.
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ChildBg, bg))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1f))
|
||||||
using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true))
|
using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true))
|
||||||
{
|
{
|
||||||
if (child)
|
if (child)
|
||||||
@@ -1505,8 +1572,6 @@ public sealed class ChatLogWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
if (dismiss)
|
if (dismiss)
|
||||||
@@ -1580,13 +1645,6 @@ public sealed class ChatLogWindow : Window
|
|||||||
internal readonly List<bool> PopOutDocked = [];
|
internal readonly List<bool> PopOutDocked = [];
|
||||||
internal readonly HashSet<Guid> PopOutWindows = [];
|
internal readonly HashSet<Guid> PopOutWindows = [];
|
||||||
|
|
||||||
// Hellion Chat v0.6.1 — height the v0.6.1 hint banner consumed in the
|
|
||||||
// current frame, read by GetRemainingHeightForMessageLog so the message
|
|
||||||
// log can shrink. Unconditionally reassigned at the top of DrawChatLog
|
|
||||||
// (before any tab-area render) so the value is always in sync with the
|
|
||||||
// current frame. Returns 0 once the banner is dismissed.
|
|
||||||
private float _v061HintBannerHeight;
|
|
||||||
|
|
||||||
// v0.6.0 — live enumeration of all active Popout windows so the
|
// v0.6.0 — live enumeration of all active Popout windows so the
|
||||||
// KeybindManager can find a focused ChatInputBar to forward tab-cycle
|
// KeybindManager can find a focused ChatInputBar to forward tab-cycle
|
||||||
// keybinds to. Filter on IsOpen prevents touching closed-but-still-
|
// keybinds to. Filter on IsOpen prevents touching closed-but-still-
|
||||||
@@ -1689,7 +1747,8 @@ public sealed class ChatLogWindow : Window
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper());
|
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper());
|
||||||
|
try
|
||||||
|
{
|
||||||
clipper.Begin(AutoCompleteList.Count);
|
clipper.Begin(AutoCompleteList.Count);
|
||||||
while (clipper.Step())
|
while (clipper.Step())
|
||||||
{
|
{
|
||||||
@@ -1731,6 +1790,13 @@ public sealed class ChatLogWindow : Window
|
|||||||
var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f);
|
var selectedPos = clipper.StartPosY + clipper.ItemsHeight * (AutoCompleteSelection * 1f);
|
||||||
ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y);
|
ImGui.SetScrollFromPosY(selectedPos - ImGui.GetWindowPos().Y);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// ImGuiListClipperPtr wraps an unmanaged ImGuiListClipper allocated above.
|
||||||
|
// Without Destroy() the unmanaged block leaks per autocomplete render.
|
||||||
|
clipper.Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private int AutoCompleteCallback(scoped ref ImGuiInputTextCallbackData data)
|
private int AutoCompleteCallback(scoped ref ImGuiInputTextCallbackData data)
|
||||||
{
|
{
|
||||||
@@ -2035,4 +2101,47 @@ public sealed class ChatLogWindow : Window
|
|||||||
var hashCode = $"{Salt}{playerName}{worldId}".GetHashCode();
|
var hashCode = $"{Salt}{playerName}{worldId}".GetHashCode();
|
||||||
return $"Player {hashCode:X8}";
|
return $"Player {hashCode:X8}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snap threshold in pixels: at least this much of the window must overlap
|
||||||
|
// a visible viewport so the user can still grab the first tab header.
|
||||||
|
// Below the threshold the window is considered off-screen.
|
||||||
|
private const int OnScreenMinOverlapX = 100;
|
||||||
|
private const int OnScreenMinOverlapY = 40;
|
||||||
|
|
||||||
|
// Default snap position relative to the primary viewport (top-left with a
|
||||||
|
// safety margin from the game title bar).
|
||||||
|
private static readonly Vector2 SafeDefaultOffset = new(50, 50);
|
||||||
|
|
||||||
|
private void EnsureWindowOnScreen(string source)
|
||||||
|
{
|
||||||
|
if (LastWindowSize.X < 1 || LastWindowSize.Y < 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var viewport = ImGui.GetMainViewport();
|
||||||
|
var visibleMin = viewport.WorkPos;
|
||||||
|
var visibleMax = viewport.WorkPos + viewport.WorkSize;
|
||||||
|
|
||||||
|
var overlapMin = Vector2.Max(LastWindowPos, visibleMin);
|
||||||
|
var overlapMax = Vector2.Min(LastWindowPos + LastWindowSize, visibleMax);
|
||||||
|
var overlap = overlapMax - overlapMin;
|
||||||
|
|
||||||
|
if (overlap.X >= OnScreenMinOverlapX && overlap.Y >= OnScreenMinOverlapY)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ApplySafeDefaultPosition(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySafeDefaultPosition(string source)
|
||||||
|
{
|
||||||
|
var viewport = ImGui.GetMainViewport();
|
||||||
|
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
||||||
|
Position = safePos;
|
||||||
|
Plugin.Log.Info(
|
||||||
|
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}.");
|
||||||
|
|
||||||
|
// Pop-outs are intentionally non-persistent (cleared on plugin reload),
|
||||||
|
// so an off-screen pop-out can never survive a session boundary. The
|
||||||
|
// main window above is the only persistence target that needs an
|
||||||
|
// explicit recovery path.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,11 @@ public class CommandHelpWindow : Window {
|
|||||||
Position = pos;
|
Position = pos;
|
||||||
SizeConstraints = new WindowSizeConstraints
|
SizeConstraints = new WindowSizeConstraints
|
||||||
{
|
{
|
||||||
MinimumSize = new Vector2(width, 0),
|
// Use scaledWidth here so the size constraints stay in the same
|
||||||
MaximumSize = LogWindow.LastWindowSize with { X = width }
|
// coordinate space as Position above; otherwise the help window
|
||||||
|
// ends up the wrong width at non-100% DPI.
|
||||||
|
MinimumSize = new Vector2(scaledWidth, 0),
|
||||||
|
MaximumSize = LogWindow.LastWindowSize with { X = scaledWidth }
|
||||||
};
|
};
|
||||||
|
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using HellionChat.Themes;
|
||||||
using HellionChat.Util;
|
using HellionChat.Util;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
@@ -5,207 +6,119 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ImGui style override for Hellion Chat. Industrial HUD palette with three
|
/// ImGui style override for Hellion Chat. v1.1.0 ist die Engine
|
||||||
/// distinct accents — cyan-teal as the primary action color, industrial
|
/// theme-getrieben: PushGlobal nimmt eine Theme-Instance + Window-
|
||||||
/// amber for active state highlights, slate-violet for title bars and
|
/// Opacity, die gesamten Color- und Style-Slots werden aus dem Theme
|
||||||
/// active tabs — on a deep-slate frame background with steel borders.
|
/// gelesen statt aus einer fixen Konstanten-Tabelle.
|
||||||
///
|
|
||||||
/// Two entry points:
|
|
||||||
/// Push — local color stack, scoped via using-block. Use inside
|
|
||||||
/// Hellion-only surfaces (Privacy tab, first-run wizard).
|
|
||||||
/// PushGlobal — full color + style variable stack. Pushed once per frame
|
|
||||||
/// in Plugin.Draw so every Hellion-rendered window inherits
|
|
||||||
/// the look. Cheap to pop because ImGui keeps its own stack.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class HellionStyle
|
internal static class HellionStyle
|
||||||
{
|
{
|
||||||
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
|
|
||||||
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
|
|
||||||
// expects. Hex values are sourced from the Hellion Online Media brand
|
|
||||||
// guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo).
|
|
||||||
|
|
||||||
// Primary — Arctic Cyan, used for every interactive control (buttons,
|
|
||||||
// checks, sliders, separators when hovered). Three brand stages plus a
|
|
||||||
// hover that lifts to brand-color-light and a press that drops to
|
|
||||||
// brand-color-dark.
|
|
||||||
private const uint PrimaryRgba = 0x00BED2FF; // brand-color
|
|
||||||
private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light
|
|
||||||
private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark
|
|
||||||
|
|
||||||
// Identity — brand-color-dark teal for window title bars and the
|
|
||||||
// active tab. Sits visibly below the primary cyan on buttons so the
|
|
||||||
// user sees "where am I" (deep teal) versus "what can I click"
|
|
||||||
// (brand cyan) without leaving the cyan family.
|
|
||||||
private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark
|
|
||||||
private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light
|
|
||||||
private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab
|
|
||||||
|
|
||||||
// Accent — Ember Orange for warm highlights on grips and scrollbar
|
|
||||||
// pulls. Replaces the previous industrial amber so the plugin matches
|
|
||||||
// the website's CTA palette. AccentActive is reserved for any future
|
|
||||||
// pressed-state on accent surfaces; the current slots only need
|
|
||||||
// AccentRgba and AccentHoverRgba.
|
|
||||||
private const uint AccentRgba = 0xF97316FF; // accent-color
|
|
||||||
private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light
|
|
||||||
|
|
||||||
// Surfaces — Hellion brand background ladder. Window darkest, frame
|
|
||||||
// hover ladder climbs into surface tones. Matches the website's
|
|
||||||
// background / background-medium / background-light / surface vars.
|
|
||||||
private const uint WindowBgRgba = 0x070B12FF; // background
|
|
||||||
private const uint ChildBgRgba = 0x0C1220FF; // background-medium
|
|
||||||
private const uint PopupBgRgba = 0x0C1220FF; // background-medium
|
|
||||||
private const uint FrameBgRgba = 0x141E30FF; // background-light
|
|
||||||
private const uint FrameBgHoverRgba = 0x1A2538FF; // surface
|
|
||||||
private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover
|
|
||||||
// Cyan-tinted border — matches website --border-brand (cyan @ 40% α).
|
|
||||||
private const uint BorderRgba = 0x00BED266;
|
|
||||||
private const uint BorderShadowRgba = 0x00000000;
|
|
||||||
|
|
||||||
// Headers / collapsing-headers / tree nodes / selectables — same
|
|
||||||
// surface ladder as frames so panels feel consistent.
|
|
||||||
private const uint HeaderRgba = 0x141E30FF;
|
|
||||||
private const uint HeaderHoverRgba = 0x1A2538FF;
|
|
||||||
private const uint HeaderActiveRgba = 0x22303FFF;
|
|
||||||
|
|
||||||
// Title bars — Identity teal on active so the focused window reads
|
|
||||||
// as "yours" without using accent or primary slots.
|
|
||||||
private const uint TitleBgRgba = 0x070B12FF;
|
|
||||||
private const uint TitleBgActiveRgba = IdentityRgba;
|
|
||||||
private const uint TitleBgCollapsedRgba = 0x05080EFF;
|
|
||||||
|
|
||||||
// Tabs — neutral inactive, Identity-light on hover, Identity teal on
|
|
||||||
// active. Unfocused-active uses the deeper Identity stage so an
|
|
||||||
// unfocused window's active tab still reads but does not pull focus.
|
|
||||||
private const uint TabRgba = 0x141E30FF;
|
|
||||||
private const uint TabHoveredRgba = IdentityHoverRgba;
|
|
||||||
private const uint TabActiveRgba = IdentityRgba;
|
|
||||||
private const uint TabUnfocusedRgba = 0x0C1220FF;
|
|
||||||
private const uint TabUnfocusedActiveRgba = IdentityDeepRgba;
|
|
||||||
|
|
||||||
// Scrollbar — Ember on grab so the pull stands out without competing
|
|
||||||
// with the cyan action buttons. Idle grab is a subtle surface tone,
|
|
||||||
// hover/active climb into accent.
|
|
||||||
private const uint ScrollbarBgRgba = 0x070B12FF;
|
|
||||||
private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover
|
|
||||||
private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba;
|
|
||||||
private const uint ScrollbarGrabActiveRgba = AccentRgba;
|
|
||||||
|
|
||||||
// Resize grip — same Ember treatment as the scrollbar.
|
|
||||||
private const uint ResizeGripRgba = 0x141E30FF;
|
|
||||||
private const uint ResizeGripHoveredRgba = AccentHoverRgba;
|
|
||||||
private const uint ResizeGripActiveRgba = AccentRgba;
|
|
||||||
|
|
||||||
// Separator and check mark / slider follow the primary cyan.
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Local color stack for Hellion-only surfaces. Cheap. Use inside a
|
/// Local color stack auf Basis des aktiven Themes. Cheap. Use inside a
|
||||||
/// `using var _ = HellionStyle.Push();` block.
|
/// `using var _ = HellionStyle.Push(theme);` block.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static IDisposable Push()
|
internal static IDisposable Push(Theme theme)
|
||||||
{
|
{
|
||||||
|
var c = theme.Colors;
|
||||||
var stack = new StackHandle();
|
var stack = new StackHandle();
|
||||||
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
|
stack.PushColor(ImGuiCol.Button, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
|
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
|
||||||
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
|
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
|
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
|
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.Border, BorderRgba);
|
stack.PushColor(ImGuiCol.Border, c.Border);
|
||||||
stack.PushColor(ImGuiCol.Header, HeaderRgba);
|
stack.PushColor(ImGuiCol.Header, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
|
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
|
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
|
||||||
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
|
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
|
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Global color and style-variable stack pushed once per frame in
|
/// Global color and style-variable stack pushed once per frame in
|
||||||
/// Plugin.Draw. Covers every ImGui surface the plugin renders so the
|
/// Plugin.Draw. Drives every Hellion-rendered window from the active
|
||||||
/// Hellion look is consistent across upstream and Hellion tabs.
|
/// theme's palette and layout values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="windowOpacity">Window background alpha (0.5–1.0). Lower
|
/// <param name="theme">Active theme from ThemeRegistry.</param>
|
||||||
/// values let the game shine through the plugin panes.</param>
|
/// <param name="windowOpacity">Window background alpha (0.5–1.0).</param>
|
||||||
internal static IDisposable PushGlobal(float windowOpacity = 1.0f)
|
internal static IDisposable PushGlobal(Theme theme, float windowOpacity = 1.0f)
|
||||||
{
|
{
|
||||||
|
var c = theme.Colors;
|
||||||
|
var l = theme.Layout;
|
||||||
var stack = new StackHandle();
|
var stack = new StackHandle();
|
||||||
|
|
||||||
// Mix the configured opacity into both the outer window and the
|
|
||||||
// inner content child backgrounds — without ChildBg following the
|
|
||||||
// slider the chat log stays opaque inside even when the user
|
|
||||||
// wants to see the game behind it during combat. Form fields and
|
|
||||||
// popups (FrameBg, PopupBg) still stay opaque so input is readable.
|
|
||||||
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
||||||
var windowBgWithAlpha = (WindowBgRgba & 0xFFFFFF00u) | alphaByte;
|
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
||||||
var childBgWithAlpha = (ChildBgRgba & 0xFFFFFF00u) | alphaByte;
|
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | alphaByte;
|
||||||
|
|
||||||
// Layout — geometric edges, modest rounding, single-pixel borders.
|
// Layout
|
||||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, 4f);
|
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, 3f);
|
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, l.ChildRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f);
|
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, l.PopupRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, l.FrameRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, l.GrabRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.TabRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.TabRounding, l.TabRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 2f);
|
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, l.ScrollbarRounding);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1f);
|
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, l.WindowBorderSize);
|
||||||
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
|
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, l.FrameBorderSize);
|
||||||
|
|
||||||
// Surfaces.
|
// Surfaces
|
||||||
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
|
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
|
||||||
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
|
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
|
||||||
stack.PushColor(ImGuiCol.PopupBg, PopupBgRgba);
|
stack.PushColor(ImGuiCol.PopupBg, c.ChildBg);
|
||||||
stack.PushColor(ImGuiCol.Border, BorderRgba);
|
stack.PushColor(ImGuiCol.Border, c.Border);
|
||||||
stack.PushColor(ImGuiCol.BorderShadow, BorderShadowRgba);
|
stack.PushColor(ImGuiCol.BorderShadow, 0u);
|
||||||
|
|
||||||
// Frames (input fields, combos, sliders).
|
// Frames
|
||||||
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
|
stack.PushColor(ImGuiCol.FrameBg, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
|
stack.PushColor(ImGuiCol.FrameBgHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
|
stack.PushColor(ImGuiCol.FrameBgActive, c.Surface);
|
||||||
|
|
||||||
// Title bars — tertiary identity on active.
|
// Title bars
|
||||||
stack.PushColor(ImGuiCol.TitleBg, TitleBgRgba);
|
stack.PushColor(ImGuiCol.TitleBg, c.WindowBg);
|
||||||
stack.PushColor(ImGuiCol.TitleBgActive, TitleBgActiveRgba);
|
stack.PushColor(ImGuiCol.TitleBgActive, c.Identity);
|
||||||
stack.PushColor(ImGuiCol.TitleBgCollapsed, TitleBgCollapsedRgba);
|
stack.PushColor(ImGuiCol.TitleBgCollapsed, c.WindowBg);
|
||||||
|
|
||||||
// Buttons — primary cyan.
|
// Buttons
|
||||||
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
|
stack.PushColor(ImGuiCol.Button, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.ButtonHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
|
stack.PushColor(ImGuiCol.ButtonActive, c.PrimaryDark);
|
||||||
|
|
||||||
// Headers / selectables — slate with subtle steps.
|
// Headers / selectables
|
||||||
stack.PushColor(ImGuiCol.Header, HeaderRgba);
|
stack.PushColor(ImGuiCol.Header, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
|
stack.PushColor(ImGuiCol.HeaderHovered, c.SurfaceHover);
|
||||||
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
|
stack.PushColor(ImGuiCol.HeaderActive, c.Identity);
|
||||||
|
|
||||||
// Tabs — tertiary identity for the active tab.
|
// Tabs
|
||||||
stack.PushColor(ImGuiCol.Tab, TabRgba);
|
stack.PushColor(ImGuiCol.Tab, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.TabHovered, TabHoveredRgba);
|
stack.PushColor(ImGuiCol.TabHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.TabActive, TabActiveRgba);
|
stack.PushColor(ImGuiCol.TabActive, c.Identity);
|
||||||
stack.PushColor(ImGuiCol.TabUnfocused, TabUnfocusedRgba);
|
stack.PushColor(ImGuiCol.TabUnfocused, c.ChildBg);
|
||||||
stack.PushColor(ImGuiCol.TabUnfocusedActive, TabUnfocusedActiveRgba);
|
stack.PushColor(ImGuiCol.TabUnfocusedActive, c.PrimaryDark);
|
||||||
|
|
||||||
// Scrollbar.
|
// Scrollbar
|
||||||
stack.PushColor(ImGuiCol.ScrollbarBg, ScrollbarBgRgba);
|
stack.PushColor(ImGuiCol.ScrollbarBg, c.WindowBg);
|
||||||
stack.PushColor(ImGuiCol.ScrollbarGrab, ScrollbarGrabRgba);
|
stack.PushColor(ImGuiCol.ScrollbarGrab, c.Surface);
|
||||||
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, ScrollbarGrabHoveredRgba);
|
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, c.AccentLight);
|
||||||
stack.PushColor(ImGuiCol.ScrollbarGrabActive, ScrollbarGrabActiveRgba);
|
stack.PushColor(ImGuiCol.ScrollbarGrabActive, c.Accent);
|
||||||
|
|
||||||
// Resize grip — secondary amber on active.
|
// Resize grip
|
||||||
stack.PushColor(ImGuiCol.ResizeGrip, ResizeGripRgba);
|
stack.PushColor(ImGuiCol.ResizeGrip, c.FrameBg);
|
||||||
stack.PushColor(ImGuiCol.ResizeGripHovered, ResizeGripHoveredRgba);
|
stack.PushColor(ImGuiCol.ResizeGripHovered, c.AccentLight);
|
||||||
stack.PushColor(ImGuiCol.ResizeGripActive, ResizeGripActiveRgba);
|
stack.PushColor(ImGuiCol.ResizeGripActive, c.Accent);
|
||||||
|
|
||||||
// Check mark + slider grab — primary cyan.
|
// Check mark + slider grab
|
||||||
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
|
stack.PushColor(ImGuiCol.CheckMark, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
|
stack.PushColor(ImGuiCol.SliderGrab, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.SliderGrabActive, c.PrimaryLight);
|
||||||
|
|
||||||
// Separator — primary cyan when hovered/active so the eye
|
// Separator
|
||||||
// immediately sees that splitters are interactive.
|
stack.PushColor(ImGuiCol.Separator, c.Border);
|
||||||
stack.PushColor(ImGuiCol.Separator, BorderRgba);
|
stack.PushColor(ImGuiCol.SeparatorHovered, c.PrimaryLight);
|
||||||
stack.PushColor(ImGuiCol.SeparatorHovered, PrimaryHoverRgba);
|
stack.PushColor(ImGuiCol.SeparatorActive, c.Primary);
|
||||||
stack.PushColor(ImGuiCol.SeparatorActive, PrimaryRgba);
|
|
||||||
|
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,10 @@ public partial class InputPreview : Window
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
NextChunkIsAutoTranslate = true;
|
NextChunkIsAutoTranslate = true;
|
||||||
var payload = (AutoTranslatePayload) chunk.Link!;
|
// Malformed chunks could carry an AutoTranslateBegin icon without the matching
|
||||||
|
// payload; bail out instead of dereferencing a null Link.
|
||||||
|
if (chunk.Link is not AutoTranslatePayload payload)
|
||||||
|
return;
|
||||||
CursorPosition += $"<at:{payload.Group},{payload.Key}>".Length;
|
CursorPosition += $"<at:{payload.Group},{payload.Key}>".Length;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ internal class Popout : Window
|
|||||||
|
|
||||||
public override void PreDraw()
|
public override void PreDraw()
|
||||||
{
|
{
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
// Hellion Chat v1.1.0+ — Theme-Engine ist Source-of-Truth, kein
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
|
// zusätzlicher Dalamud-StyleModel-Override mehr pro Window. Plugin.Draw
|
||||||
|
// pusht das aktive Hellion-Theme global; Pop-Out zeichnet sich damit
|
||||||
|
// konsistent zum Haupt-Chat-Window.
|
||||||
Flags = ImGuiWindowFlags.None;
|
Flags = ImGuiWindowFlags.None;
|
||||||
if (!Plugin.Config.ShowPopOutTitleBar)
|
if (!Plugin.Config.ShowPopOutTitleBar)
|
||||||
Flags |= ImGuiWindowFlags.NoTitleBar;
|
Flags |= ImGuiWindowFlags.NoTitleBar;
|
||||||
@@ -91,9 +92,7 @@ internal class Popout : Window
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
BgAlpha = Plugin.Config.HellionThemeEnabled
|
BgAlpha = Plugin.Config.WindowOpacity;
|
||||||
? Plugin.Config.HellionThemeWindowOpacity
|
|
||||||
: Plugin.Config.WindowAlpha / 100f;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,9 +199,6 @@ internal class Popout : Window
|
|||||||
{
|
{
|
||||||
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count)
|
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count)
|
||||||
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
|
||||||
|
|
||||||
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
|
|
||||||
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
@@ -229,30 +225,48 @@ internal class Popout : Window
|
|||||||
{
|
{
|
||||||
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
|
||||||
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Battle;
|
CurrentHideState = HideState.Battle;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Battle");
|
||||||
|
}
|
||||||
|
|
||||||
// If the chat is hidden because of battle, we reset it here
|
// If the chat is hidden because of battle, we reset it here
|
||||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle → None");
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||||
if (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
if (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
|
||||||
{
|
{
|
||||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||||
|
{
|
||||||
CurrentHideState = HideState.Cutscene;
|
CurrentHideState = HideState.Cutscene;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Cutscene");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
|
||||||
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
|
||||||
|
{
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
}
|
||||||
|
|
||||||
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
|
||||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.CutsceneOverride;
|
CurrentHideState = HideState.CutsceneOverride;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Cutscene → CutsceneOverride (user activate)");
|
||||||
|
}
|
||||||
|
|
||||||
// if the user hid the chat and is now activating chat, reset the hide state
|
// if the user hid the chat and is now activating chat, reset the hide state
|
||||||
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
||||||
|
{
|
||||||
CurrentHideState = HideState.None;
|
CurrentHideState = HideState.None;
|
||||||
|
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User → None (activate)");
|
||||||
|
}
|
||||||
|
|
||||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
|
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,16 @@ public class SeStringDebugger : Window
|
|||||||
default:
|
default:
|
||||||
var payloadData = payload.Encode();
|
var payloadData = payload.Encode();
|
||||||
|
|
||||||
var initialByte = payloadData.First();
|
if (payloadData.Length == 0)
|
||||||
|
{
|
||||||
|
RenderMetadataDictionary("Empty Payload", new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
{ "Type", payload.GetType().Name },
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialByte = payloadData[0];
|
||||||
if (initialByte != 0x02)
|
if (initialByte != 0x02)
|
||||||
{
|
{
|
||||||
RenderMetadataDictionary("Text Payload", new Dictionary<string, string?>
|
RenderMetadataDictionary("Text Payload", new Dictionary<string, string?>
|
||||||
|
|||||||
@@ -9,13 +9,21 @@ using Dalamud.Bindings.ImGui;
|
|||||||
|
|
||||||
namespace HellionChat.Ui;
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
|
internal enum SettingsView
|
||||||
|
{
|
||||||
|
Overview,
|
||||||
|
Detail,
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||||
{
|
{
|
||||||
private readonly Plugin Plugin;
|
internal readonly Plugin Plugin;
|
||||||
|
|
||||||
private Configuration Mutable { get; }
|
private Configuration Mutable { get; }
|
||||||
private List<ISettingsTab> Tabs { get; }
|
private List<ISettingsTab> Tabs { get; }
|
||||||
private int CurrentTab;
|
private int CurrentTab;
|
||||||
|
private SettingsView View = SettingsView.Overview;
|
||||||
|
private readonly SettingsOverview Overview;
|
||||||
|
|
||||||
internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
||||||
{
|
{
|
||||||
@@ -31,10 +39,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
Plugin = plugin;
|
Plugin = plugin;
|
||||||
Mutable = new Configuration();
|
Mutable = new Configuration();
|
||||||
|
|
||||||
|
Overview = new SettingsOverview(this);
|
||||||
|
|
||||||
Tabs =
|
Tabs =
|
||||||
[
|
[
|
||||||
new General(Plugin, Mutable),
|
new General(Plugin, Mutable),
|
||||||
new Appearance(Plugin, Mutable),
|
new Appearance(Plugin, Mutable),
|
||||||
|
new SettingsTabs.Themes(Plugin, Mutable),
|
||||||
new SettingsTabs.Window(Plugin, Mutable),
|
new SettingsTabs.Window(Plugin, Mutable),
|
||||||
new Chat(Plugin, Mutable),
|
new Chat(Plugin, Mutable),
|
||||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||||
@@ -72,40 +83,81 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
|||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
if (ImGui.IsWindowAppearing())
|
if (ImGui.IsWindowAppearing())
|
||||||
|
{
|
||||||
Initialise();
|
Initialise();
|
||||||
|
View = SettingsView.Overview;
|
||||||
using (var table = ImRaii.Table("##chat2-settings-table", 2))
|
|
||||||
{
|
|
||||||
if (table.Success)
|
|
||||||
{
|
|
||||||
ImGui.TableSetupColumn("tab", ImGuiTableColumnFlags.WidthFixed);
|
|
||||||
ImGui.TableSetupColumn("settings", ImGuiTableColumnFlags.WidthStretch);
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
|
|
||||||
var changed = false;
|
|
||||||
for (var i = 0; i < Tabs.Count; i++)
|
|
||||||
{
|
|
||||||
if (!ImGui.Selectable($"{Tabs[i].Name}###tab-{i}", CurrentTab == i))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
CurrentTab = i;
|
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
// ESC im Detail-View kehrt zur Overview zurück. Window-Focus-Check ist
|
||||||
|
// Pflicht — sonst triggert ESC auch wenn der User ein anderes Fenster
|
||||||
|
// fokussiert hat und ESC fürs Game-Menü drückt (Codebase-Pattern siehe
|
||||||
|
// Util/SearchSelector.cs:37).
|
||||||
|
if (View == SettingsView.Detail
|
||||||
|
&& ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows)
|
||||||
|
&& ImGui.IsKeyPressed(ImGuiKey.Escape))
|
||||||
|
{
|
||||||
|
View = SettingsView.Overview;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (View == SettingsView.Overview)
|
||||||
|
Overview.Draw();
|
||||||
|
else
|
||||||
|
DrawDetail();
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
DrawSaveButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OpenSection(int tabIndex)
|
||||||
|
{
|
||||||
|
CurrentTab = tabIndex;
|
||||||
|
View = SettingsView.Detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OpenOverview()
|
||||||
|
{
|
||||||
|
View = SettingsView.Overview;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDetail()
|
||||||
|
{
|
||||||
|
// Breadcrumb-Header — Akzent-Cyan, klickbar, führt zurück zur Overview
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF00BED2u))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, 0u))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, 0x33FFFFFFu))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, 0x55FFFFFFu))
|
||||||
|
{
|
||||||
|
if (ImGui.SmallButton("← Settings"))
|
||||||
|
{
|
||||||
|
View = SettingsView.Overview;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("·");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(Tabs[CurrentTab].Name.Split("###")[0]);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Section-Content in voller Breite. Die Tab-Liste links ist überholt:
|
||||||
|
// der User ist bereits über die Card-Übersicht navigiert, eine zweite
|
||||||
|
// Tab-Liste daneben würde nur den Vanilla-Look zurückbringen. Falls
|
||||||
|
// der User in eine andere Section will, geht er zurück zur Overview
|
||||||
|
// (Breadcrumb / ESC).
|
||||||
var style = ImGui.GetStyle();
|
var style = ImGui.GetStyle();
|
||||||
var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y;
|
var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y;
|
||||||
|
|
||||||
using var child = ImRaii.Child("##chat2-settings", new Vector2(-1, height));
|
using var child = ImRaii.Child("##chat2-settings-detail", new Vector2(-1, height));
|
||||||
if (child.Success)
|
if (child.Success)
|
||||||
Tabs[CurrentTab].Draw(changed);
|
Tabs[CurrentTab].Draw(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
private void DrawSaveButtons()
|
||||||
|
{
|
||||||
var save = ImGui.Button(Language.Settings_Save);
|
var save = ImGui.Button(Language.Settings_Save);
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui;
|
||||||
|
|
||||||
|
internal sealed class SettingsOverview
|
||||||
|
{
|
||||||
|
private readonly SettingsWindow _window;
|
||||||
|
|
||||||
|
// Card-Reihenfolge entspricht 1:1 dem Tabs-Index in SettingsWindow.
|
||||||
|
// Themes ist Card-Index 2, eingeschoben zwischen Appearance und Window.
|
||||||
|
private static readonly (FontAwesomeIcon Icon, string TitleKey, string SubtextKey)[] CardDefs =
|
||||||
|
[
|
||||||
|
(FontAwesomeIcon.SlidersH, "Settings_Card_General_Title", "Settings_Card_General_Subtext"),
|
||||||
|
(FontAwesomeIcon.Palette, "Settings_Card_Appearance_Title", "Settings_Card_Appearance_Subtext"),
|
||||||
|
(FontAwesomeIcon.Swatchbook, "Settings_Card_Themes_Title", "Settings_Card_Themes_Subtext"),
|
||||||
|
(FontAwesomeIcon.WindowMaximize, "Settings_Card_Window_Title", "Settings_Card_Window_Subtext"),
|
||||||
|
(FontAwesomeIcon.Comments, "Settings_Card_Chat_Title", "Settings_Card_Chat_Subtext"),
|
||||||
|
(FontAwesomeIcon.FolderTree, "Settings_Card_Tabs_Title", "Settings_Card_Tabs_Subtext"),
|
||||||
|
(FontAwesomeIcon.ShieldAlt, "Settings_Card_Privacy_Title", "Settings_Card_Privacy_Subtext"),
|
||||||
|
(FontAwesomeIcon.Database, "Settings_Card_Database_Title", "Settings_Card_Database_Subtext"),
|
||||||
|
(FontAwesomeIcon.InfoCircle, "Settings_Card_Information_Title", "Settings_Card_Information_Subtext"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public SettingsOverview(SettingsWindow window)
|
||||||
|
{
|
||||||
|
_window = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
var columns = avail.X >= 700f ? 3 : 2;
|
||||||
|
var cardWidth = (avail.X - (columns - 1) * 8f) / columns;
|
||||||
|
var cardHeight = 96f;
|
||||||
|
|
||||||
|
for (var i = 0; i < CardDefs.Length; i++)
|
||||||
|
{
|
||||||
|
var (icon, titleKey, subtextKey) = CardDefs[i];
|
||||||
|
var title = HellionStrings.ResourceManager.GetString(titleKey) ?? titleKey;
|
||||||
|
var subtext = HellionStrings.ResourceManager.GetString(subtextKey) ?? subtextKey;
|
||||||
|
DrawCard(i, icon, title, subtext, cardWidth, cardHeight);
|
||||||
|
|
||||||
|
if ((i + 1) % columns != 0 && i != CardDefs.Length - 1)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCard(int index, FontAwesomeIcon icon, string title, string subtext, float w, float h)
|
||||||
|
{
|
||||||
|
// BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item.
|
||||||
|
// Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die
|
||||||
|
// einzelnen InvisibleButton/Text-Items separat und das Wrapping bricht.
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
|
||||||
|
var cursorBefore = ImGui.GetCursorScreenPos();
|
||||||
|
var clicked = ImGui.InvisibleButton($"##settings-card-{index}", new Vector2(w, h));
|
||||||
|
var hovered = ImGui.IsItemHovered();
|
||||||
|
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
||||||
|
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||||
|
|
||||||
|
// Inhalts-Overlay: Icon + Title + Subtext direkt mit DrawList in den
|
||||||
|
// Card-Bereich zeichnen, statt Cursor-Hopping mit SetCursorScreenPos.
|
||||||
|
// DrawList-Overlays ändern den Cursor nicht, BeginGroup/EndGroup
|
||||||
|
// hält den Layout-Anker stabil für SameLine.
|
||||||
|
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
||||||
|
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
||||||
|
var subtextPos = cursorBefore + new Vector2(16f, 62f);
|
||||||
|
|
||||||
|
var titleColor = ColourUtil.RgbaToAbgr(0xE6F4F1FFu);
|
||||||
|
var subtextColor = ColourUtil.RgbaToAbgr(0x8FA3B5FFu);
|
||||||
|
|
||||||
|
// Icon via FontAwesome — temporär den Font pushen, mit DrawList zeichnen
|
||||||
|
using (_window.Plugin.FontManager.FontAwesome.Push())
|
||||||
|
{
|
||||||
|
draw.AddText(iconPos, titleColor, icon.ToIconString());
|
||||||
|
}
|
||||||
|
|
||||||
|
draw.AddText(titlePos, titleColor, title);
|
||||||
|
draw.AddText(subtextPos, subtextColor, subtext);
|
||||||
|
|
||||||
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
if (clicked)
|
||||||
|
{
|
||||||
|
_window.OpenSection(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,13 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
|
|
||||||
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
|
||||||
{
|
{
|
||||||
|
// v1.1.0 — Diese Settings-UI wird in Phase J durch den dedizierten
|
||||||
|
// Themes-Tab ersetzt. Bis dahin bleiben die alten Toggles erhalten,
|
||||||
|
// damit die Settings-Seite kompiliert; sie schreiben in die mit
|
||||||
|
// [Obsolete] markierten Felder, die bis v1.2.0 als JSON-Safety-Net
|
||||||
|
// bestehen bleiben. Das pragma unterdrückt die CS0612-Warnungen
|
||||||
|
// gezielt für diesen Übergangs-Block.
|
||||||
|
#pragma warning disable CS0612, CS0618
|
||||||
ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled);
|
ImGui.Checkbox(HellionStrings.Theme_Enabled_Name, ref Mutable.HellionThemeEnabled);
|
||||||
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
|
ImGuiUtil.HelpMarker(HellionStrings.Theme_Enabled_Description);
|
||||||
|
|
||||||
@@ -81,6 +88,7 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
{
|
{
|
||||||
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
|
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
|
||||||
}
|
}
|
||||||
|
#pragma warning restore CS0612, CS0618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +244,10 @@ internal sealed class Appearance : ISettingsTab
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
ImGui.Checkbox(Language.Options_ColorSelectedInputChannelButton_Name, ref Mutable.ColorSelectedInputChannelButton);
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_ColorSelectedInputChannelButton_Description);
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
foreach (var (_, types) in ChatTypeExt.SortOrder)
|
foreach (var (_, types) in ChatTypeExt.SortOrder)
|
||||||
{
|
{
|
||||||
foreach (var type in types)
|
foreach (var type in types)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ internal sealed class Chat : ISettingsTab
|
|||||||
public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat";
|
public string Name => HellionStrings.Settings_Tab_Chat + "###tabs-chat";
|
||||||
|
|
||||||
private SearchSelector.SelectorPopupOptions WordPopupOptions;
|
private SearchSelector.SelectorPopupOptions WordPopupOptions;
|
||||||
|
// Snapshot of EmoteCache.State for which we last built WordPopupOptions.
|
||||||
|
// Without this, an empty FilteredSheet (e.g., the user blocked every emote)
|
||||||
|
// would trigger a refill every frame the settings tab is open.
|
||||||
|
private EmoteCache.LoadingState? WordPopupOptionsBuiltFor;
|
||||||
|
|
||||||
internal Chat(Plugin plugin, Configuration mutable)
|
internal Chat(Plugin plugin, Configuration mutable)
|
||||||
{
|
{
|
||||||
@@ -28,6 +32,7 @@ internal sealed class Chat : ISettingsTab
|
|||||||
Mutable = mutable;
|
Mutable = mutable;
|
||||||
|
|
||||||
WordPopupOptions = RefillSheet();
|
WordPopupOptions = RefillSheet();
|
||||||
|
WordPopupOptionsBuiltFor = EmoteCache.State;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SearchSelector.SelectorPopupOptions RefillSheet()
|
private SearchSelector.SelectorPopupOptions RefillSheet()
|
||||||
@@ -160,9 +165,12 @@ internal sealed class Chat : ISettingsTab
|
|||||||
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
|
||||||
ImGui.Spacing();
|
ImGui.Spacing();
|
||||||
|
|
||||||
if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0)
|
if (EmoteCache.State is EmoteCache.LoadingState.Done
|
||||||
|
&& WordPopupOptions.FilteredSheet.Length == 0
|
||||||
|
&& WordPopupOptionsBuiltFor != EmoteCache.LoadingState.Done)
|
||||||
{
|
{
|
||||||
WordPopupOptions = RefillSheet();
|
WordPopupOptions = RefillSheet();
|
||||||
|
WordPopupOptionsBuiltFor = EmoteCache.LoadingState.Done;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
|
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
|
||||||
|
|||||||
@@ -81,9 +81,11 @@ internal sealed class Database : ISettingsTab
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Delete both legacy files in one click — the previous if/else
|
||||||
|
// left the second file behind when both happened to exist.
|
||||||
if (old.Exists)
|
if (old.Exists)
|
||||||
old.Delete();
|
old.Delete();
|
||||||
else
|
if (migratedOld.Exists)
|
||||||
migratedOld.Delete();
|
migratedOld.Delete();
|
||||||
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success);
|
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
CleanupRunning = true;
|
CleanupRunning = true;
|
||||||
var allowed = Plugin.Config.PrivacyPersistChannels.Select(t => (int)(ushort)t).ToList();
|
var allowed = Plugin.Config.PrivacyPersistChannels.Select(t => (int)(ushort)t).ToList();
|
||||||
|
|
||||||
new Thread(() =>
|
var thread = new Thread(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -625,10 +625,14 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
// Bound the wait so a hung framework tick can't deadlock
|
// Bound the wait so a hung framework tick can't deadlock
|
||||||
// the background cleanup worker. See the matching comment in
|
// the background cleanup worker. See the matching comment in
|
||||||
// the retention path above for rationale.
|
// the retention path above for rationale.
|
||||||
|
// Note: FilterAllTabs() is called synchronously instead of
|
||||||
|
// FilterAllTabsAsync() — the async variant fires-and-forgets
|
||||||
|
// a Task.Run, so the .Wait() would return before the filter
|
||||||
|
// pass actually finishes. See AUDIT-2026-05-05 [QUAL-02].
|
||||||
if (!Plugin.Framework.Run(() =>
|
if (!Plugin.Framework.Run(() =>
|
||||||
{
|
{
|
||||||
Plugin.MessageManager.ClearAllTabs();
|
Plugin.MessageManager.ClearAllTabs();
|
||||||
Plugin.MessageManager.FilterAllTabsAsync();
|
Plugin.MessageManager.FilterAllTabs();
|
||||||
}).Wait(TimeSpan.FromSeconds(5)))
|
}).Wait(TimeSpan.FromSeconds(5)))
|
||||||
{
|
{
|
||||||
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||||
@@ -646,6 +650,9 @@ internal sealed class Privacy : ISettingsTab
|
|||||||
CleanupRunning = false;
|
CleanupRunning = false;
|
||||||
CleanupCounts = null;
|
CleanupCounts = null;
|
||||||
}
|
}
|
||||||
}).Start();
|
});
|
||||||
|
// Background thread so a still-running cleanup doesn't hold up FFXIV exit.
|
||||||
|
thread.IsBackground = true;
|
||||||
|
thread.Start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
|
internal static class ThemeMockup
|
||||||
|
{
|
||||||
|
// Zeichnet ein Mini-Chat-Window-Mockup mit den Theme-Werten direkt
|
||||||
|
// ins WindowDrawList. Keine Texture, keine Allocation pro Frame —
|
||||||
|
// alles via DrawList.AddRectFilled / AddText.
|
||||||
|
public static void Draw(Vector2 origin, Vector2 size, Theme theme)
|
||||||
|
{
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
var c = theme.Colors;
|
||||||
|
|
||||||
|
// Window-Bg
|
||||||
|
draw.AddRectFilled(origin, origin + size, ColourUtil.RgbaToAbgr(c.WindowBg | 0xFFu), theme.Layout.WindowRounding);
|
||||||
|
|
||||||
|
// Title-Bar
|
||||||
|
var titleHeight = 14f;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
origin,
|
||||||
|
new Vector2(origin.X + size.X, origin.Y + titleHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Identity), theme.Layout.WindowRounding);
|
||||||
|
|
||||||
|
// Tab-Bar — 3 Mini-Tabs
|
||||||
|
var tabY = origin.Y + titleHeight + 4f;
|
||||||
|
var tabHeight = 12f;
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var tabX = origin.X + 6f + i * 28f;
|
||||||
|
var color = i == 0 ? c.FrameBg : c.ChildBg;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(tabX, tabY),
|
||||||
|
new Vector2(tabX + 26f, tabY + tabHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(color), theme.Layout.TabRounding);
|
||||||
|
|
||||||
|
if (i == 0) // Active-Pill
|
||||||
|
{
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(tabX, tabY + tabHeight - 2f),
|
||||||
|
new Vector2(tabX + 26f, tabY + tabHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card-Row mit Mock-Sender + Text
|
||||||
|
var rowY = tabY + tabHeight + 6f;
|
||||||
|
var rowHeight = 18f;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(origin.X + 6f, rowY),
|
||||||
|
new Vector2(origin.X + size.X - 6f, rowY + rowHeight),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Surface), 2f);
|
||||||
|
|
||||||
|
// Akzent-Button rechts unten
|
||||||
|
var btnW = 28f;
|
||||||
|
var btnH = 10f;
|
||||||
|
var btnX = origin.X + size.X - btnW - 6f;
|
||||||
|
var btnY = origin.Y + size.Y - btnH - 6f;
|
||||||
|
draw.AddRectFilled(
|
||||||
|
new Vector2(btnX, btnY),
|
||||||
|
new Vector2(btnX + btnW, btnY + btnH),
|
||||||
|
ColourUtil.RgbaToAbgr(c.Accent), theme.Layout.FrameRounding);
|
||||||
|
|
||||||
|
// Border um das gesamte Mockup
|
||||||
|
draw.AddRect(origin, origin + size, ColourUtil.RgbaToAbgr(c.Border), theme.Layout.WindowRounding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using HellionChat.Resources;
|
||||||
|
using HellionChat.Themes;
|
||||||
|
using HellionChat.Util;
|
||||||
|
|
||||||
|
namespace HellionChat.Ui.SettingsTabs;
|
||||||
|
|
||||||
|
internal sealed class Themes : ISettingsTab
|
||||||
|
{
|
||||||
|
private readonly Plugin Plugin;
|
||||||
|
private readonly Configuration Mutable;
|
||||||
|
|
||||||
|
// Tracks ob der User die Apply-Frage für das aktive Theme bereits
|
||||||
|
// beantwortet hat. Banner wird nur angezeigt wenn das Theme ein
|
||||||
|
// ChatColors-Set hat UND noch keine Antwort vorliegt UND die aktuellen
|
||||||
|
// Mutable.ChatColours davon abweichen.
|
||||||
|
private string? _applyDismissedFor;
|
||||||
|
|
||||||
|
public string Name => HellionStrings.ResourceManager.GetString("Settings_Tab_Themes") ?? "Themes" + "###tabs-themes";
|
||||||
|
|
||||||
|
internal Themes(Plugin plugin, Configuration mutable)
|
||||||
|
{
|
||||||
|
Plugin = plugin;
|
||||||
|
Mutable = mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw(bool changed)
|
||||||
|
{
|
||||||
|
var registry = Plugin.ThemeRegistry;
|
||||||
|
var active = registry.Get(Mutable.Theme);
|
||||||
|
|
||||||
|
var activeLabelTemplate = HellionStrings.ResourceManager.GetString("Settings_Themes_Active") ?? "Active: {0}";
|
||||||
|
ImGui.TextUnformatted(string.Format(activeLabelTemplate, active.Name));
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF8FA3B5u))
|
||||||
|
ImGui.TextUnformatted(active.Author);
|
||||||
|
|
||||||
|
DrawChatColorsApplyBanner(active);
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var builtInsLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_BuiltIns") ?? "Built-in themes";
|
||||||
|
ImGui.TextUnformatted(builtInsLabel);
|
||||||
|
ImGui.Spacing();
|
||||||
|
DrawThemeGrid(registry.AllBuiltIns(), active.Slug);
|
||||||
|
|
||||||
|
var customs = registry.AllCustom().ToList();
|
||||||
|
if (customs.Count > 0)
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
var customLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_Custom") ?? "Custom themes";
|
||||||
|
ImGui.TextUnformatted(customLabel);
|
||||||
|
ImGui.Spacing();
|
||||||
|
DrawThemeGrid(customs, active.Slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var openFolderLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_OpenFolder") ?? "Open themes folder";
|
||||||
|
if (ImGui.Button(openFolderLabel))
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
Dalamud.Utility.Util.OpenLink(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var exportLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ExportActive") ?? "Export active...";
|
||||||
|
if (ImGui.Button(exportLabel))
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var fileName = $"{active.Slug}.export.json";
|
||||||
|
var path = Path.Combine(dir, fileName);
|
||||||
|
var json = ThemeJsonWriter.Serialize(active);
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawThemeGrid(IEnumerable<Theme> themes, string activeSlug)
|
||||||
|
{
|
||||||
|
var avail = ImGui.GetContentRegionAvail();
|
||||||
|
var columns = avail.X >= 700f ? 3 : 2;
|
||||||
|
var cardWidth = (avail.X - (columns - 1) * 8f) / columns;
|
||||||
|
var cardHeight = 140f; // Mockup + Name + Author brauchen den Platz
|
||||||
|
|
||||||
|
var list = themes.ToList();
|
||||||
|
for (var i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
DrawThemeCard(list[i], activeSlug, cardWidth, cardHeight);
|
||||||
|
|
||||||
|
// SameLine zwischen den Cards einer Reihe; am Spalten-Ende kein
|
||||||
|
// SameLine, dann beginnt automatisch eine neue Zeile.
|
||||||
|
if ((i + 1) % columns != 0 && i != list.Count - 1)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawThemeCard(Theme theme, string activeSlug, float w, float h)
|
||||||
|
{
|
||||||
|
// BeginGroup macht den Card-Bereich zu einem einzelnen ImGui-Layout-Item.
|
||||||
|
// Damit funktioniert SameLine() im Caller-Loop — sonst tracked ImGui die
|
||||||
|
// einzelnen InvisibleButton-Items separat und das Wrapping bricht.
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
|
||||||
|
var isActive = string.Equals(theme.Slug, activeSlug, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var cursorBefore = ImGui.GetCursorScreenPos();
|
||||||
|
var clicked = ImGui.InvisibleButton($"##theme-card-{theme.Slug}", new Vector2(w, h));
|
||||||
|
var hovered = ImGui.IsItemHovered();
|
||||||
|
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
var bg = ColourUtil.RgbaToAbgr(theme.Colors.WindowBg | 0xFFu);
|
||||||
|
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bg, 4f);
|
||||||
|
|
||||||
|
if (isActive)
|
||||||
|
{
|
||||||
|
var border = ColourUtil.RgbaToAbgr(theme.Colors.Primary);
|
||||||
|
draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 2f);
|
||||||
|
}
|
||||||
|
else if (hovered)
|
||||||
|
{
|
||||||
|
var border = ColourUtil.RgbaToAbgr(theme.Colors.PrimaryLight & 0xFFFFFF99u);
|
||||||
|
draw.AddRect(cursorBefore, cursorBefore + new Vector2(w, h), border, 4f, ImDrawFlags.None, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mini-Mockup oben — DrawList-Operation, kein Cursor-Hopping
|
||||||
|
var mockupOrigin = cursorBefore + new Vector2(12f, 12f);
|
||||||
|
var mockupSize = new Vector2(w - 24f, 60f);
|
||||||
|
ThemeMockup.Draw(mockupOrigin, mockupSize, theme);
|
||||||
|
|
||||||
|
// Name + Author direkt via DrawList (statt SetCursorScreenPos +
|
||||||
|
// TextUnformatted), damit der ImGui-Layout-Cursor stabil bleibt
|
||||||
|
// und die BeginGroup/EndGroup-Klammer den Card-Bereich als ein
|
||||||
|
// Layout-Item führt.
|
||||||
|
var textColor = ColourUtil.RgbaToAbgr(theme.Colors.TextPrimary);
|
||||||
|
var mutedColor = ColourUtil.RgbaToAbgr(theme.Colors.TextMuted);
|
||||||
|
draw.AddText(cursorBefore + new Vector2(12f, 80f), textColor, theme.Name);
|
||||||
|
draw.AddText(cursorBefore + new Vector2(12f, 100f), mutedColor, theme.Author);
|
||||||
|
|
||||||
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
if (clicked)
|
||||||
|
{
|
||||||
|
Mutable.Theme = theme.Slug;
|
||||||
|
Plugin.ThemeRegistry.Switch(theme.Slug);
|
||||||
|
_applyDismissedFor = null; // Banner für neues Theme wieder zeigen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawChatColorsApplyBanner(Theme active)
|
||||||
|
{
|
||||||
|
// Klassik hat per Design keine ChatColors — kein Banner.
|
||||||
|
if (active.ChatColors is not { Channels.Count: > 0 } themeChatColors)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// User hat die Frage bereits für genau dieses Theme beantwortet.
|
||||||
|
if (_applyDismissedFor == active.Slug)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Wenn die aktuellen Channel-Colors bereits exakt mit dem Theme-Vorschlag
|
||||||
|
// übereinstimmen, gibt's nichts zu tun.
|
||||||
|
var alreadyMatching = themeChatColors.Channels.All(kvp =>
|
||||||
|
Mutable.ChatColours.TryGetValue(kvp.Key, out var current) && current == kvp.Value);
|
||||||
|
if (alreadyMatching)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Dezent-Akzent-Banner mit Border in Theme-Primary
|
||||||
|
var border = ColourUtil.RgbaToAbgr(active.Colors.Primary);
|
||||||
|
var bgFill = ColourUtil.RgbaToAbgr((active.Colors.Surface & 0xFFFFFF00u) | 0xCCu);
|
||||||
|
var origin = ImGui.GetCursorScreenPos();
|
||||||
|
var width = ImGui.GetContentRegionAvail().X;
|
||||||
|
var height = 64f;
|
||||||
|
var draw = ImGui.GetWindowDrawList();
|
||||||
|
draw.AddRectFilled(origin, origin + new Vector2(width, height), bgFill, 4f);
|
||||||
|
draw.AddRect(origin, origin + new Vector2(width, height), border, 4f, ImDrawFlags.None, 1f);
|
||||||
|
|
||||||
|
var hint = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Hint")
|
||||||
|
?? "This theme suggests its own chat channel colours.";
|
||||||
|
var applyLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Apply")
|
||||||
|
?? "Apply";
|
||||||
|
var keepLabel = HellionStrings.ResourceManager.GetString("Settings_Themes_ApplyChatColors_Keep")
|
||||||
|
?? "Keep current";
|
||||||
|
|
||||||
|
var textColor = ColourUtil.RgbaToAbgr(active.Colors.TextPrimary);
|
||||||
|
draw.AddText(origin + new Vector2(12f, 10f), textColor, hint);
|
||||||
|
|
||||||
|
// Buttons als InvisibleButton + DrawList-Overlay, damit sie konsistent
|
||||||
|
// zum Banner-Look bleiben.
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, active.Colors.Primary))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, active.Colors.PrimaryLight))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, active.Colors.PrimaryDark))
|
||||||
|
{
|
||||||
|
ImGui.SetCursorScreenPos(origin + new Vector2(12f, 32f));
|
||||||
|
if (ImGui.Button(applyLabel))
|
||||||
|
{
|
||||||
|
foreach (var kvp in themeChatColors.Channels)
|
||||||
|
Mutable.ChatColours[kvp.Key] = kvp.Value;
|
||||||
|
_applyDismissedFor = active.Slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button(keepLabel))
|
||||||
|
{
|
||||||
|
_applyDismissedFor = active.Slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor unter den Banner setzen
|
||||||
|
ImGui.SetCursorScreenPos(origin + new Vector2(0f, height + 8f));
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,9 @@ internal sealed class Window : ISettingsTab
|
|||||||
|
|
||||||
ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle);
|
ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle);
|
||||||
ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description);
|
ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description);
|
||||||
|
|
||||||
|
ImGui.Checkbox(Language.Options_HideInNewGamePlusMenu_Name, ref Mutable.HideInNewGamePlusMenu);
|
||||||
|
ImGuiUtil.HelpMarker(Language.Options_HideInNewGamePlusMenu_Description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +145,15 @@ internal sealed class Window : ISettingsTab
|
|||||||
|
|
||||||
ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView);
|
ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView);
|
||||||
ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName));
|
ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName));
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
// Manual escape hatch for off-screen windows. The plugin already
|
||||||
|
// runs an automatic bounds check once per session, but a button
|
||||||
|
// is the user-friendly fallback after a display layout change.
|
||||||
|
if (ImGui.Button(HellionStrings.Settings_Window_ResetPosition_Name))
|
||||||
|
Plugin.ChatLogWindow.RequestPositionReset = true;
|
||||||
|
ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_ResetPosition_Description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
@@ -233,9 +232,6 @@ internal static class AutoTranslate
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
|
|
||||||
private static extern int memcmp(byte[] b1, byte[] b2, nuint count);
|
|
||||||
|
|
||||||
internal static void ReplaceWithPayload(ref byte[] bytes)
|
internal static void ReplaceWithPayload(ref byte[] bytes)
|
||||||
{
|
{
|
||||||
var search = "<at:"u8.ToArray();
|
var search = "<at:"u8.ToArray();
|
||||||
@@ -279,7 +275,10 @@ internal static class AutoTranslate
|
|||||||
start = -1;
|
start = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i + search.Length < bytes.Length && memcmp(bytes[i..], search, (nuint) search.Length) == 0)
|
// Pure managed comparison via Span avoids the msvcrt.dll P/Invoke,
|
||||||
|
// which is fragile under Wine and triggered an extra managed-to-
|
||||||
|
// unmanaged copy per check.
|
||||||
|
if (i + search.Length < bytes.Length && bytes.AsSpan(i, search.Length).SequenceEqual(search))
|
||||||
start = i;
|
start = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,34 @@ internal static class ColourUtil {
|
|||||||
|
|
||||||
internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF)
|
internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF)
|
||||||
=> alpha | (uint) (red << 24) | (uint) (green << 16) | (uint) (blue << 8);
|
=> alpha | (uint) (red << 24) | (uint) (green << 16) | (uint) (blue << 8);
|
||||||
|
|
||||||
|
internal static uint AdjustBrightness(uint abgr, float factor)
|
||||||
|
{
|
||||||
|
var a = (byte) ((abgr & 0xFF000000) >> 24);
|
||||||
|
var b = (byte) ((abgr & 0x00FF0000) >> 16);
|
||||||
|
var g = (byte) ((abgr & 0x0000FF00) >> 8);
|
||||||
|
var r = (byte) (abgr & 0x000000FF);
|
||||||
|
|
||||||
|
var nr = (byte) Math.Clamp(r * factor, 0f, 255f);
|
||||||
|
var ng = (byte) Math.Clamp(g * factor, 0f, 255f);
|
||||||
|
var nb = (byte) Math.Clamp(b * factor, 0f, 255f);
|
||||||
|
|
||||||
|
return ((uint) a << 24) | ((uint) nb << 16) | ((uint) ng << 8) | nr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint HexToRgba(string hex)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(hex);
|
||||||
|
var s = hex.StartsWith('#') ? hex[1..] : hex;
|
||||||
|
if (s.Length != 6 && s.Length != 8)
|
||||||
|
throw new FormatException($"Hex colour must be 6 or 8 hex digits, got {s.Length}: '{hex}'");
|
||||||
|
|
||||||
|
if (!uint.TryParse(s, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var value))
|
||||||
|
throw new FormatException($"Hex colour '{hex}' is not a valid hexadecimal value");
|
||||||
|
|
||||||
|
if (s.Length == 6)
|
||||||
|
value = (value << 8) | 0xFFu; // RRGGBB → RRGGBBFF
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public class ColorPayload
|
|||||||
return payload;
|
return payload;
|
||||||
case 0xE9:
|
case 0xE9:
|
||||||
var param = stream.ReadByte();
|
var param = stream.ReadByte();
|
||||||
|
if (param == -1)
|
||||||
|
throw new ArgumentException("Encountered premature end of input (unexpected EOF).", nameof(stream));
|
||||||
var globalValue = (uint) GlobalParametersCache.GetValue(param - 2);
|
var globalValue = (uint) GlobalParametersCache.GetValue(param - 2);
|
||||||
payload.Enabled = true;
|
payload.Enabled = true;
|
||||||
payload.UnshiftedColor = globalValue;
|
payload.UnshiftedColor = globalValue;
|
||||||
|
|||||||
@@ -49,9 +49,21 @@ public readonly unsafe ref struct GfdFileView
|
|||||||
var entries = Entries;
|
var entries = Entries;
|
||||||
if (DirectLookup)
|
if (DirectLookup)
|
||||||
{
|
{
|
||||||
if (iconId <= entries.Length)
|
// Resolve redirects on the direct-lookup path too — the binary-search
|
||||||
|
// path follows them, and skipping them here was inconsistent for
|
||||||
|
// contiguous ID sets.
|
||||||
|
var visited = 0;
|
||||||
|
while (iconId <= entries.Length)
|
||||||
{
|
{
|
||||||
entry = entries[(int)(iconId - 1)];
|
entry = entries[(int)(iconId - 1)];
|
||||||
|
if (followRedirect && entry.Redirect != 0 && entry.Redirect != iconId)
|
||||||
|
{
|
||||||
|
if (++visited > entries.Length)
|
||||||
|
break; // cycle guard
|
||||||
|
iconId = entry.Redirect;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return !entry.IsEmpty;
|
return !entry.IsEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +158,17 @@ public readonly unsafe ref struct GfdFileView
|
|||||||
internal static class IconUtil
|
internal static class IconUtil
|
||||||
{
|
{
|
||||||
private static byte[]? GfdFile;
|
private static byte[]? GfdFile;
|
||||||
public static unsafe GfdFileView GfdFileView
|
public static GfdFileView GfdFileView
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
GfdFile ??= Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data;
|
if (GfdFile is null)
|
||||||
return new GfdFileView(new ReadOnlySpan<byte>(Unsafe.AsPointer(ref GfdFile[0]), GfdFile.Length));
|
{
|
||||||
|
var file = Plugin.DataManager.GetFile("common/font/gfdata.gfd")
|
||||||
|
?? throw new FileNotFoundException("Failed to load common/font/gfdata.gfd from the game data.");
|
||||||
|
GfdFile = file.Data;
|
||||||
|
}
|
||||||
|
return new GfdFileView(GfdFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ internal class Lender<T>
|
|||||||
|
|
||||||
internal Lender(Func<T> ctor)
|
internal Lender(Func<T> ctor)
|
||||||
{
|
{
|
||||||
Ctor = ctor;
|
Ctor = ctor ?? throw new ArgumentNullException(nameof(ctor));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ResetCounter()
|
internal void ResetCounter()
|
||||||
|
|||||||
@@ -4,8 +4,21 @@ namespace HellionChat.Util;
|
|||||||
|
|
||||||
public static class MemoryUtil
|
public static class MemoryUtil
|
||||||
{
|
{
|
||||||
|
// Diagnostic helper. Pointer dereferences here would crash on a null/garbage
|
||||||
|
// address and a huge length would log megabytes of raw bytes; both are easy
|
||||||
|
// to trigger from a debugger and pollute the log with potentially sensitive
|
||||||
|
// game-state. Validate the inputs before reading.
|
||||||
|
private const int MaxDumpLength = 4096;
|
||||||
|
|
||||||
public static unsafe void PrintMemoryArea(nint address, int length)
|
public static unsafe void PrintMemoryArea(nint address, int length)
|
||||||
{
|
{
|
||||||
|
if (address == nint.Zero)
|
||||||
|
throw new ArgumentException("Memory address cannot be zero.", nameof(address));
|
||||||
|
if (length <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length), length, "Length must be positive.");
|
||||||
|
if (length > MaxDumpLength)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(length), length, $"Length exceeds the {MaxDumpLength}-byte safety cap.");
|
||||||
|
|
||||||
var ptr = (byte*)address;
|
var ptr = (byte*)address;
|
||||||
var str = new StringBuilder("\n");
|
var str = new StringBuilder("\n");
|
||||||
for(var i = 0; i < length; i++)
|
for(var i = 0; i < length; i++)
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ internal class UriPayload(Uri uri) : Payload
|
|||||||
public static UriPayload ResolveUri(string rawUri)
|
public static UriPayload ResolveUri(string rawUri)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(rawUri);
|
ArgumentNullException.ThrowIfNull(rawUri);
|
||||||
|
if (string.IsNullOrWhiteSpace(rawUri))
|
||||||
|
throw new UriFormatException("URI cannot be empty or whitespace.");
|
||||||
|
|
||||||
// Check for an expected scheme '://', if not add 'https://'
|
// Check for an expected scheme '://', if not add 'https://'
|
||||||
if (ExpectedSchemes.Any(scheme => rawUri.StartsWith($"{scheme}://")))
|
if (ExpectedSchemes.Any(scheme => rawUri.StartsWith($"{scheme}://")))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace HellionChat.Util;
|
namespace HellionChat.Util;
|
||||||
@@ -23,6 +24,9 @@ internal static class StringUtil
|
|||||||
var bytes = Math.Abs(byteCount);
|
var bytes = Math.Abs(byteCount);
|
||||||
var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
|
var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
|
||||||
var num = Math.Round(bytes / Math.Pow(1024, place), 1);
|
var num = Math.Round(bytes / Math.Pow(1024, place), 1);
|
||||||
return (Math.Sign(byteCount) * num).ToString("N0") + suf[place];
|
// "0.#" keeps the rounded fractional digit (1.5 GB stays "1.5GB"); "N0"
|
||||||
|
// would truncate it back to integer. InvariantCulture pins the decimal
|
||||||
|
// separator to '.' so a German locale doesn't render "1,5GB".
|
||||||
|
return (Math.Sign(byteCount) * num).ToString("0.#", CultureInfo.InvariantCulture) + suf[place];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 51 KiB |
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"MessagePack": {
|
"MessagePack": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.1.4, )",
|
"requested": "[3.1.4, 4.0.0)",
|
||||||
"resolved": "3.1.4",
|
"resolved": "3.1.4",
|
||||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
},
|
},
|
||||||
"Pidgin": {
|
"Pidgin": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.3.0, )",
|
"requested": "[3.5.1, 4.0.0)",
|
||||||
"resolved": "3.3.0",
|
"resolved": "3.5.1",
|
||||||
"contentHash": "2rvIoIogQG1+vqvXCuz1xiAVljaiacG/wCz/TNpN74TzWw+9iSCjhBLf7kVg24sBi6tArRdrcklHq49ovW2NLA=="
|
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
|
||||||
},
|
},
|
||||||
"SixLabors.ImageSharp": {
|
"SixLabors.ImageSharp": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[3.1.12, )",
|
"requested": "[3.1.12, 4.0.0)",
|
||||||
"resolved": "3.1.12",
|
"resolved": "3.1.12",
|
||||||
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ because no data ever leaves your machine on the maintainer's
|
|||||||
infrastructure. Independently of that, the plugin is built so that
|
infrastructure. Independently of that, the plugin is built so that
|
||||||
you can act on your own data the way the GDPR expects.
|
you can act on your own data the way the GDPR expects.
|
||||||
|
|
||||||
Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
Last reviewed: 2026-05-05 (HellionChat v1.1.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,10 +23,9 @@ Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
|||||||
- The plugin does not phone home. There is no telemetry, no analytics,
|
- The plugin does not phone home. There is no telemetry, no analytics,
|
||||||
no crash reporter, no usage counter, no remote update check beyond
|
no crash reporter, no usage counter, no remote update check beyond
|
||||||
what Dalamud itself does.
|
what Dalamud itself does.
|
||||||
- Two outbound network calls exist by design: the BetterTTV emote
|
- One outbound network call exists by design: the BetterTTV emote
|
||||||
service (for chat emotes) and the Square Enix Lodestone font CDN
|
service (for chat emotes). It is documented in detail below and
|
||||||
(for the in-game symbol font). Both are documented in detail below
|
can be reasoned about per request.
|
||||||
and both can be reasoned about per request.
|
|
||||||
- You can export every message the plugin has stored, in Markdown,
|
- You can export every message the plugin has stored, in Markdown,
|
||||||
JSON or CSV, and you can wipe stored history per channel, per date
|
JSON or CSV, and you can wipe stored history per channel, per date
|
||||||
range, or globally.
|
range, or globally.
|
||||||
@@ -103,8 +102,17 @@ on your behalf.
|
|||||||
reaches BetterTTV (unavoidable for any HTTPS request); the request
|
reaches BetterTTV (unavoidable for any HTTPS request); the request
|
||||||
itself contains no identifying user data, no character name, no
|
itself contains no identifying user data, no character name, no
|
||||||
message text. Only the emote ID being looked up is in the URL path.
|
message text. Only the emote ID being looked up is in the URL path.
|
||||||
- **When it triggers:** Only when an incoming message contains an
|
- **When it triggers:**
|
||||||
emote token that is on the BetterTTV emote list.
|
- The emote *list* (global emotes plus the top-1500 community emotes
|
||||||
|
over fifteen API pages) is fetched from `api.betterttv.net` once
|
||||||
|
per session at plugin startup, provided the **Show emotes** option
|
||||||
|
is on. This first list-fetch happens before any chat message has
|
||||||
|
arrived; BetterTTV's edge therefore sees your IP as soon as the
|
||||||
|
plugin loads, not only after an emote is mentioned.
|
||||||
|
- The individual emote *images* on `cdn.betterttv.net` are fetched
|
||||||
|
on demand, only when an incoming chat message contains a token
|
||||||
|
matching one of the cached IDs. These are cached locally
|
||||||
|
(`emoteCache/`) and reused across sessions.
|
||||||
- **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once
|
- **Cached:** Yes, in `emoteCache/`. A given emote is downloaded once
|
||||||
per machine and reused.
|
per machine and reused.
|
||||||
- **How to opt out:** Turn off the **Show emotes** option in
|
- **How to opt out:** Turn off the **Show emotes** option in
|
||||||
@@ -112,26 +120,24 @@ on your behalf.
|
|||||||
and no requests to BetterTTV are made for the rest of the session.
|
and no requests to BetterTTV are made for the rest of the session.
|
||||||
- **BetterTTV's privacy policy:** <https://betterttv.com/privacy>
|
- **BetterTTV's privacy policy:** <https://betterttv.com/privacy>
|
||||||
|
|
||||||
Source: `ChatTwo/EmoteCache.cs`.
|
Source: `HellionChat/EmoteCache.cs`.
|
||||||
|
|
||||||
### 2. Square Enix Lodestone font (`img.finalfantasyxiv.com`)
|
### 2. Square Enix Lodestone font — removed in v1.0.4
|
||||||
|
|
||||||
- **What it does:** Downloads the `FFXIV_Lodestone_SSF.ttf` font file
|
Earlier versions of HellionChat (and upstream Chat 2) downloaded
|
||||||
from the official Square Enix Lodestone CDN once during font setup,
|
`FFXIV_Lodestone_SSF.ttf` from `img.finalfantasyxiv.com` once during
|
||||||
so the plugin can render in-game special symbols (job icons, item
|
font setup. That code path was a leftover from upstream's removed
|
||||||
glyphs, etc.) inside ImGui.
|
webinterface feature and was no longer consumed anywhere — the in-game
|
||||||
- **What is sent:** A single HTTPS GET request to the public Square
|
symbol glyphs (job icons, item glyphs, status effects) come from
|
||||||
Enix font URL. Your IP address reaches Square Enix (unavoidable);
|
Dalamud's bundled symbol-font helper, not from the downloaded TTF.
|
||||||
no character data, no plugin identifier, no message content.
|
|
||||||
- **When it triggers:** Once per font initialisation, not per session
|
|
||||||
if the file is already cached locally.
|
|
||||||
- **Cached:** Yes, by Dalamud's font subsystem.
|
|
||||||
- **How to opt out:** This call is part of the font pipeline inherited
|
|
||||||
from upstream Chat 2 and not toggleable from the settings UI today.
|
|
||||||
If a user-facing opt-out for this would be useful for you, please
|
|
||||||
open a feature-request issue.
|
|
||||||
|
|
||||||
Source: `ChatTwo/FontManager.cs`.
|
The download was removed in v1.0.4. As of that version HellionChat
|
||||||
|
makes no automatic network call to Square Enix or to any
|
||||||
|
`finalfantasyxiv.com` host.
|
||||||
|
|
||||||
|
Cached `FFXIV_Lodestone_SSF.ttf` files left over from earlier versions
|
||||||
|
remain in `pluginConfigs/HellionChat/` until manually deleted; they
|
||||||
|
are no longer read.
|
||||||
|
|
||||||
### Links you click yourself (no automatic traffic)
|
### Links you click yourself (no automatic traffic)
|
||||||
|
|
||||||
@@ -149,7 +155,7 @@ traffic.
|
|||||||
- **No telemetry.** Source verified: no calls to AppInsights, Sentry,
|
- **No telemetry.** Source verified: no calls to AppInsights, Sentry,
|
||||||
PostHog, Plausible, Google Analytics, Microsoft Clarity or any
|
PostHog, Plausible, Google Analytics, Microsoft Clarity or any
|
||||||
comparable service exist in the codebase, nor in the direct
|
comparable service exist in the codebase, nor in the direct
|
||||||
dependencies the plugin pulls in. See `THIRD_PARTY_NOTICES.md`.
|
dependencies the plugin pulls in. See `docs/THIRD_PARTY_NOTICES.md`.
|
||||||
- **No crash reporting.** Crashes go to Dalamud's local `xllog`,
|
- **No crash reporting.** Crashes go to Dalamud's local `xllog`,
|
||||||
not to a remote endpoint controlled by HellionChat.
|
not to a remote endpoint controlled by HellionChat.
|
||||||
- **No usage counters.** The plugin does not count installs, sessions,
|
- **No usage counters.** The plugin does not count installs, sessions,
|
||||||
@@ -209,20 +215,19 @@ retroactive cleanup to apply retroactively, by design.
|
|||||||
| Party | Why they appear | What reaches them | Their privacy policy |
|
| Party | Why they appear | What reaches them | Their privacy policy |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| BetterTTV (NightDev LLC) | Optional emote rendering | HTTPS request for an emote ID; your IP | <https://betterttv.com/privacy> |
|
| BetterTTV (NightDev LLC) | Optional emote rendering | HTTPS request for an emote ID; your IP | <https://betterttv.com/privacy> |
|
||||||
| Square Enix | Lodestone font download (once) | HTTPS request for the font file; your IP | <https://www.square-enix.com/privacy> |
|
|
||||||
| GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | <https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement> |
|
| GitHub (Microsoft) | Plugin distribution via custom repo, issue tracker | Whatever GitHub sees from any HTTPS request to a public repo | <https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement> |
|
||||||
| Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | <https://github.com/goatcorp/Dalamud> |
|
| Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | <https://github.com/goatcorp/Dalamud> |
|
||||||
|
|
||||||
Square Enix and GitHub are unavoidable for anyone playing FFXIV
|
GitHub and the Dalamud/XIVLauncher loader are unavoidable for anyone
|
||||||
through Dalamud at all. BetterTTV is the only third party HellionChat
|
playing FFXIV through Dalamud at all. BetterTTV is the only third
|
||||||
introduces on top of the baseline that is not also part of using FFXIV
|
party HellionChat introduces on top of that baseline, and it is
|
||||||
or Dalamud, and BetterTTV is opt-out via settings.
|
opt-out via settings.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dependencies that touch the network
|
## Dependencies that touch the network
|
||||||
|
|
||||||
For a full dependency inventory see `THIRD_PARTY_NOTICES.md`. Of the
|
For a full dependency inventory see `docs/THIRD_PARTY_NOTICES.md`. Of the
|
||||||
direct dependencies the plugin pulls in:
|
direct dependencies the plugin pulls in:
|
||||||
|
|
||||||
- `MessagePack` — local serialisation, no network.
|
- `MessagePack` — local serialisation, no network.
|
||||||
@@ -232,7 +237,7 @@ direct dependencies the plugin pulls in:
|
|||||||
- `SixLabors.ImageSharp` — image decoding (used for the BetterTTV
|
- `SixLabors.ImageSharp` — image decoding (used for the BetterTTV
|
||||||
emote pipeline), no network on its own.
|
emote pipeline), no network on its own.
|
||||||
|
|
||||||
The two network calls listed under "Outbound network calls" are
|
The single network call listed under "Outbound network calls" is
|
||||||
written directly in HellionChat's own source, not delegated to a
|
written directly in HellionChat's own source, not delegated to a
|
||||||
dependency.
|
dependency.
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
[](https://dotnet.microsoft.com/)
|
[](https://dotnet.microsoft.com/)
|
||||||
[](https://www.finalfantasyxiv.com/)
|
[](https://www.finalfantasyxiv.com/)
|
||||||
|
|
||||||
**Version 1.0.0** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
**Version 1.0.3** — DSGVO-bewusster Chat-Ersatz für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||||
|
|
||||||
Hellion Chat ergänzt das ursprüngliche Chat-2-Fundament um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle aus Chat 2 übernommenen Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank.
|
Hellion Chat ergänzt das ursprüngliche Chat-2-Fundament um Datenschutz- und Daten-Handling-Kontrollen, die mit den Datenschutz-Regeln in der EU, den USA und Japan im Einklang sind. Alle aus Chat 2 übernommenen Funktionen, Befehle und Tastenkürzel funktionieren unverändert. Eigenständiger Plugin-Slot, eigene Konfiguration, eigene Datenbank.
|
||||||
|
|
||||||
Eigenständiges Repository, EUPL-1.2-lizenziert. Distribution über Custom-Repo. Selektive Cherry-Picks von Upstream-Chat-2 nach Bedarf, dokumentiert in [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md).
|
Eigenständiges Repository, EUPL-1.2-lizenziert. Mit v1.0.0 ist der Standalone-Cut abgeschlossen: eigener Namespace `HellionChat.*`, eigene IPC-Kanäle, eigene Source-Tree-Struktur. Distribution über Custom-Repo. Selektive Cherry-Picks von Upstream-Chat-2 nach Bedarf, dokumentiert in [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md).
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
@@ -44,12 +44,12 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf
|
|||||||
- **Aufbewahrungsdauer pro Kanal** mit täglicher Background-Bereinigung. Tells 365 Tage, eigene Konversations-Kanäle 90 Tage, globaler Default 30 Tage. Standard ist AUS, das Plugin löscht ohne ausdrückliche Zustimmung nichts.
|
- **Aufbewahrungsdauer pro Kanal** mit täglicher Background-Bereinigung. Tells 365 Tage, eigene Konversations-Kanäle 90 Tage, globaler Default 30 Tage. Standard ist AUS, das Plugin löscht ohne ausdrückliche Zustimmung nichts.
|
||||||
- **Retroaktive Säuberung** mit Vorschau und Strg+Umschalt-Bestätigung. Wendet die aktuelle Whitelist auf eine bestehende Datenbank an, läuft im Hintergrund, ruft danach VACUUM auf.
|
- **Retroaktive Säuberung** mit Vorschau und Strg+Umschalt-Bestätigung. Wendet die aktuelle Whitelist auf eine bestehende Datenbank an, läuft im Hintergrund, ruft danach VACUUM auf.
|
||||||
- **Export** nach Markdown, JSON oder CSV via Dalamud-Datei-Dialog (DSGVO Art. 15 Auskunftsrecht). Filter nach Kanal, Datums-Bereich oder Sender-Substring.
|
- **Export** nach Markdown, JSON oder CSV via Dalamud-Datei-Dialog (DSGVO Art. 15 Auskunftsrecht). Filter nach Kanal, Datums-Bereich oder Sender-Substring.
|
||||||
- **Vollständige Datenschutz-Übersicht** in [`PRIVACY.md`](PRIVACY.md): was gespeichert wird, welche zwei Outbound-Calls existieren (BetterTTV opt-out, Square-Enix-Lodestone-Font), explizite Telemetry-None-Zusage und das Mapping der DSGVO-Rechte (Art. 15/17/18/20/21) auf konkrete Plugin-Funktionen.
|
- **Vollständige Datenschutz-Übersicht** in [`PRIVACY.md`](PRIVACY.md) und Drittanbieter-Komponenten in [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md): was gespeichert wird, welche zwei Outbound-Calls existieren (BetterTTV opt-out, Square-Enix-Lodestone-Font), explizite Telemetry-None-Zusage und das Mapping der DSGVO-Rechte (Art. 15/17/18/20/21) auf konkrete Plugin-Funktionen.
|
||||||
|
|
||||||
### Onboarding
|
### Onboarding
|
||||||
|
|
||||||
- **First-Run-Wizard** mit drei Profilen (Privacy-First, Locker, Volle Historie) und DSGVO-Hinweis bei der "Volle Historie"-Option.
|
- **First-Run-Wizard** mit drei Profilen (Privacy-First, Locker, Volle Historie) und DSGVO-Hinweis bei der "Volle Historie"-Option.
|
||||||
- **Konfigurations-Migration v6→v7** seedet Privacy-Defaults bei Bestand-Usern und zeigt eine Benachrichtigung beim Ersten Plugin-Start nach Update.
|
- **Konfigurations-Migration** seedet Privacy-Defaults bei Bestands-Usern und zeigt eine Benachrichtigung beim ersten Plugin-Start nach Update. Mit v1.0.0 wird zusätzlich für User auf Config-Version 12 oder älter ein einmaliger Tab-Layout-Reset durchgeführt; die alte Tab-Konfiguration wird als `pluginConfigs/HellionChat.json.pre-v13-backup` gesichert.
|
||||||
- **Layout-Migration aus Chat 2** verschiebt Konfiguration und Datenbank in `pluginConfigs/HellionChat/` ohne Datenverlust. Robust gegen blockierte Dateien (Warnung beim User wenn Chat 2 noch geladen ist).
|
- **Layout-Migration aus Chat 2** verschiebt Konfiguration und Datenbank in `pluginConfigs/HellionChat/` ohne Datenverlust. Robust gegen blockierte Dateien (Warnung beim User wenn Chat 2 noch geladen ist).
|
||||||
- **Migrate3-Recovery** heilt halb-migrierte Datenbanken aus alten Chat-2-Installationen.
|
- **Migrate3-Recovery** heilt halb-migrierte Datenbanken aus alten Chat-2-Installationen.
|
||||||
|
|
||||||
@@ -62,6 +62,13 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf
|
|||||||
- **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font.
|
- **Mitgelieferte Hellion-Schrift** (Exo 2, OFL-1.1) als optionaler Default statt System-Font.
|
||||||
- **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste.
|
- **Hellion-Logo** im Plugin-Bundle und in der Dalamud-Plugin-Liste.
|
||||||
|
|
||||||
|
#### Custom Themes (v1.1.0)
|
||||||
|
|
||||||
|
HellionChat 1.1.0 bringt eine Theme-Engine mit fünf eingebauten Themes
|
||||||
|
(Hellion Arctic, Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove)
|
||||||
|
und ein JSON-basiertes Authoring-Format für eigene Themes. Schema und
|
||||||
|
Schritt-für-Schritt-Anleitung in [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md).
|
||||||
|
|
||||||
### Pop-Out Convenience (v0.6.0)
|
### Pop-Out Convenience (v0.6.0)
|
||||||
|
|
||||||
- **Eingabe-Bar in Pop-Out-Fenstern** als globaler Opt-In in Settings → Fenster → Fenster-Rahmen. Wenn aktiv hat jedes Pop-Out-Window unten einen kompakten Input mit kanal-farbigem Icon-Button und Text-Eingabe — kein Wechsel mehr ins Hauptfenster für eine schnelle Antwort.
|
- **Eingabe-Bar in Pop-Out-Fenstern** als globaler Opt-In in Settings → Fenster → Fenster-Rahmen. Wenn aktiv hat jedes Pop-Out-Window unten einen kompakten Input mit kanal-farbigem Icon-Button und Text-Eingabe — kein Wechsel mehr ins Hauptfenster für eine schnelle Antwort.
|
||||||
@@ -83,7 +90,7 @@ Hellion Chat baut auf [Chat 2](https://github.com/Infiziert90/ChatTwo) von **Inf
|
|||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
```
|
```
|
||||||
ChatTwo/
|
HellionChat/
|
||||||
├── Privacy/
|
├── Privacy/
|
||||||
│ └── PrivacyDefaults.cs # Whitelist-Sets, Spec-Retention-Tabelle
|
│ └── PrivacyDefaults.cs # Whitelist-Sets, Spec-Retention-Tabelle
|
||||||
├── Export/
|
├── Export/
|
||||||
@@ -92,6 +99,7 @@ ChatTwo/
|
|||||||
│ ├── HellionStrings.resx # Hellion-eigene UI-Strings (EN)
|
│ ├── HellionStrings.resx # Hellion-eigene UI-Strings (EN)
|
||||||
│ ├── HellionStrings.de.resx # Deutsche Übersetzung
|
│ ├── HellionStrings.de.resx # Deutsche Übersetzung
|
||||||
│ ├── HellionStrings.Designer.cs # Hand-maintained Accessor
|
│ ├── HellionStrings.Designer.cs # Hand-maintained Accessor
|
||||||
|
│ ├── ChatColourPresets.cs # Sieben Built-in-Color-Presets (v0.6.0)
|
||||||
│ ├── HellionFont.ttf # Exo 2 Variable Font
|
│ ├── HellionFont.ttf # Exo 2 Variable Font
|
||||||
│ ├── HellionFont-OFL.txt # OFL-1.1 Lizenztext (mit Font gebundelt)
|
│ ├── HellionFont-OFL.txt # OFL-1.1 Lizenztext (mit Font gebundelt)
|
||||||
│ └── Language*.resx # Upstream-Lokalisierung (Crowdin)
|
│ └── Language*.resx # Upstream-Lokalisierung (Crowdin)
|
||||||
@@ -100,16 +108,19 @@ ChatTwo/
|
|||||||
│ ├── HellionStyle.cs # ImGui-Theme-Push (lokal + global)
|
│ ├── HellionStyle.cs # ImGui-Theme-Push (lokal + global)
|
||||||
│ └── SettingsTabs/
|
│ └── SettingsTabs/
|
||||||
│ └── Privacy.cs # Datenschutz-Tab (Filter, Retention, Cleanup, Export)
|
│ └── Privacy.cs # Datenschutz-Tab (Filter, Retention, Cleanup, Export)
|
||||||
|
├── Ipc/ # IPC-Kanäle, in v1.0.0 auf HellionChat.* migriert
|
||||||
|
├── ChatTwoConflictDetector.cs # Verweigert Plugin-Start wenn Upstream Chat 2 aktiv
|
||||||
├── images/
|
├── images/
|
||||||
│ └── icon.png # Hellion-Logo (256×256)
|
│ └── icon.png # Hellion-Logo (256×256)
|
||||||
├── DalamudPackager.targets # Override für ImagesPath / HandleImages
|
├── HellionChat.csproj # SDK Dalamud.NET.Sdk/15.0.0
|
||||||
└── HellionChat.yaml # Plugin-Manifest (DalamudPackager-Source)
|
└── HellionChat.yaml # Plugin-Manifest (DalamudPackager-Source)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Regeln
|
### Regeln
|
||||||
|
|
||||||
- **Code-Namespace ist `HellionChat.*`** — seit v1.0.0 vollständig konsolidiert auf den Plugin-Namen.
|
- **Code-Namespace ist `HellionChat.*`** — seit v1.0.0 vollständig konsolidiert auf den Plugin-Namen, kein verbleibender `ChatTwo.*`-Bestand im Source-Tree.
|
||||||
- **AssemblyName ist `HellionChat`** — eigener Slot in `pluginConfigs/`, eigene Datei-Manifest, kein Shared State mit Chat 2.
|
- **AssemblyName ist `HellionChat`** — eigener Slot in `pluginConfigs/`, eigenes Datei-Manifest, kein Shared State mit Chat 2. Parallel-Load mit Upstream Chat 2 wird beim Start aktiv geblockt (bilinguale Konflikt-Meldung).
|
||||||
|
- **IPC-Kanäle sind `HellionChat.*`** — sechs Kanäle für Drittplugin-Anbindung (`Register`, `Available`, `Unregister`, `Invoke`, `GetChatInputState`, `ChatInputStateChanged`). Details in [`docs/IPC.md`](docs/IPC.md).
|
||||||
- **Hellion-eigene Strings in `HellionStrings.*.resx`**, übernommene Strings aus dem Chat-2-Bestand in `Language.*.resx` — die Original-`Language.*.resx` bleibt strukturell erhalten, weil die existierenden Übersetzungen aus dem Crowdin-Bestand der Upstream-Community weiter wertvoll sind.
|
- **Hellion-eigene Strings in `HellionStrings.*.resx`**, übernommene Strings aus dem Chat-2-Bestand in `Language.*.resx` — die Original-`Language.*.resx` bleibt strukturell erhalten, weil die existierenden Übersetzungen aus dem Crowdin-Bestand der Upstream-Community weiter wertvoll sind.
|
||||||
- **Kein Direkt-Eingriff in `Plugin.Interface.UiBuilder.FontAtlas`** außerhalb von `FontManager` — Font-Fallback und Hellion-Font laufen zentral.
|
- **Kein Direkt-Eingriff in `Plugin.Interface.UiBuilder.FontAtlas`** außerhalb von `FontManager` — Font-Fallback und Hellion-Font laufen zentral.
|
||||||
|
|
||||||
@@ -193,93 +204,50 @@ Updates erscheinen automatisch in der Plugin-Liste, sobald ein neuer `v0.X.Y`-Ta
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Entwicklung
|
|
||||||
|
|
||||||
### Voraussetzungen
|
|
||||||
|
|
||||||
- .NET 10 SDK (`10.0.104+`) und .NET 9 SDK (`9.0.115+` parallel)
|
|
||||||
- Dalamud-Hooks im XIVLauncher-`addon`-Verzeichnis
|
|
||||||
- VS Code mit C# Dev Kit (oder Rider, JetBrains)
|
|
||||||
- Linux: WireGuard-Mount für Test-Spiel-Setup falls Remote-DB
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone --recurse-submodules https://github.com/JonKazama-Hellion/HellionChat.git
|
|
||||||
cd HellionChat
|
|
||||||
git remote add upstream https://github.com/Infiziert90/ChatTwo.git
|
|
||||||
|
|
||||||
# Linux: DALAMUD_HOME exportieren falls Hooks nicht im Standardpfad
|
|
||||||
cp .env.example .env
|
|
||||||
set -a; source .env; set +a
|
|
||||||
|
|
||||||
dotnet build ChatTwo/ChatTwo.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
Output: `ChatTwo/bin/Debug/HellionChat.dll`. Den Ordner `ChatTwo/bin/Debug` in Dalamud unter Experimental → Dev Plugin Locations eintragen.
|
|
||||||
|
|
||||||
### Build-Konfigurationen
|
|
||||||
|
|
||||||
| Configuration | Output | Zweck |
|
|
||||||
| ------------- | ----------------------------------------------------- | -------------------------------- |
|
|
||||||
| Debug | `bin/Debug/HellionChat.dll` | Dev-Plugin-Loading |
|
|
||||||
| Release | `bin/Release/HellionChat/latest.zip` + Manifest | Custom-Repo / GitHub Release |
|
|
||||||
|
|
||||||
### Upstream-Sync
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git fetch upstream
|
|
||||||
git log --oneline HEAD..upstream/main # Welche Commits gibt es?
|
|
||||||
git cherry-pick -x <commit> # Selektiv übernehmen
|
|
||||||
```
|
|
||||||
|
|
||||||
Konflikte in Upstream-Sprach-Ressourcen (`Language.<lang>.resx`) kommen häufig vor, weil Upstream-Übersetzungen (über das Chat-2-Crowdin-Projekt, nicht unseres) regelmäßig nachkommen. Pragmatisch mit `git checkout --theirs` auflösen, da wir sie selbst nicht editieren.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Distribution
|
## Distribution
|
||||||
|
|
||||||
| Phase | Version | Distribution |
|
Hellion Chat wird über ein eigenes Dalamud-Custom-Repository verteilt
|
||||||
| --------------- | ------------- | -------------------------------------------------- |
|
(`repo.json` im Repo-Root). Tag-Pushes auf `vX.Y.Z` lösen den
|
||||||
| Bootstrap | v0.1.x | Eigenes Custom-Repo (`repo.json` im Repo-Root) |
|
[`release.yml`](.github/workflows/release.yml)-Workflow aus, der den
|
||||||
| Stable | v1.0 | Eigenes Custom-Repo |
|
Build-Output (`HellionChat/bin/Release/HellionChat/latest.zip`) plus den
|
||||||
| Optional | v1.1+ | Submission ans Dalamud-Main-Plugin-Repo (zusätzlich) |
|
passenden Changelog-Block aus `HellionChat.yaml` an das GitHub-Release
|
||||||
|
hängt. Manueller Recovery-Pfad bei verpasstem Auto-Trigger:
|
||||||
|
`gh workflow run release.yml -f tag=vX.Y.Z`.
|
||||||
|
|
||||||
`repo.json` wird beim Versions-Bump per Hand aus dem generierten `HellionChat.json` plus den GitHub-Release-Download-Links zusammengebaut. Skript-Automatisierung via GitHub Actions ist geplant aber noch nicht eingerichtet.
|
Eine optionale Submission ans Dalamud-Main-Plugin-Repo (zusätzlich zum
|
||||||
|
eigenen Custom-Repo) steht in der [Roadmap](docs/ROADMAP.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Projektstatus
|
## Projektstatus
|
||||||
|
|
||||||
**Version 0.6.1** | Stand: 2026-05-03
|
**Version 1.0.0** — Standalone-Cut live (Stand: 2026-05-04).
|
||||||
|
|
||||||
Alle Bootstrap-Phasen abgeschlossen:
|
Mit v1.0.0 ist Hellion Chat ein eigenständiges Plugin, kein Fork mehr im
|
||||||
|
Repository-Sinne. Vollständig abgeschlossen:
|
||||||
|
|
||||||
- [x] Privacy-Filter (Whitelist + Retention + Cleanup + Export)
|
- Privacy-Filter (Whitelist, Retention, retroaktive Cleanup, Export)
|
||||||
- [x] First-Run-Wizard mit drei Profilen
|
- First-Run-Wizard mit drei Profilen
|
||||||
- [x] Plugin-Identity (eigener Slot, Layout-Migration, Recovery)
|
- Plugin-Identity: eigener `HellionChat`-Slot, Layout-Migration aus Chat 2, Migrate3-Recovery
|
||||||
- [x] Bilinguale UI (EN + DE) mit Live-Sprachwechsel
|
- Bilinguale UI (EN + DE) mit Live-Sprachwechsel
|
||||||
- [x] Hellion-Theme + Hellion-Logo + gebündelter Exo-2-Font
|
- Hellion-Theme, Hellion-Logo, gebündelter Exo-2-Font
|
||||||
- [x] Custom-Repo-Pipeline mit GitHub-Release-Distribution
|
- Custom-Repo-Pipeline mit automatisierter GitHub-Release-Distribution
|
||||||
- [x] About-Tab im Hellion-Branding mit License + Disclaimer
|
- Slash-Commands auf die `/hellion`-Familie konsolidiert
|
||||||
- [x] AI-Disclosure dokumentiert (Pair-Klassifikation)
|
- Webinterface entfernt (v0.2.0)
|
||||||
- [x] Webinterface entfernt (Phase 1.5, Audit-Konsequenz aus 2026-05-02)
|
- Audit-Hardening (Path-Traversal, Retention-Race, DbViewer-Konsistenz)
|
||||||
- [x] Audit-Hardening Phase 2 (Path-Traversal, Retention-Race, DbViewer-Konsistenz, Privacy-Filter-Help-Text)
|
- About-Tab im Hellion-Branding, EN + DE lokalisiert, mit License und Disclaimer
|
||||||
- [x] Slash-Commands auf `/hellion`-Familie umbenannt
|
- AI-Disclosure dokumentiert (siehe [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md))
|
||||||
- [x] Theme auf Hellion-Online-Media-Brand-Palette aligned (Arctic Cyan + Ember Orange)
|
- Standalone-Cut: Namespace `HellionChat.*`, IPC-Kanäle `HellionChat.*`, Source-Tree-Restructure, Conflict-Detection gegen Upstream Chat 2, SQLite-CVE-Härtung (3.50.3)
|
||||||
- [x] About-Tab vollständig lokalisiert (EN + DE) mit Mission-Statement und neutraler Tonart
|
|
||||||
|
|
||||||
Phase 3 (offen, kein festes Datum):
|
Was als Nächstes geplant ist und welche Themen langfristig auf der Liste
|
||||||
|
stehen, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md). Konkrete
|
||||||
|
eingeplante Items werden zusätzlich im
|
||||||
|
[GitHub-Issue-Tracker](https://github.com/JonKazama-Hellion/HellionChat/issues)
|
||||||
|
mit dem `roadmap`-Label geführt.
|
||||||
|
|
||||||
- [ ] MySQL/MariaDB-Backend mit Drei-Stufen-Bestätigung
|
### Zur Release-Kadenz
|
||||||
- [ ] PostgreSQL-Backend
|
|
||||||
- [ ] Encryption für sensible Channels (AES-256, lokaler Key)
|
Wer den Repo zum ersten Mal sieht, bemerkt schnell viele Releases und sehr viele Commits in kurzer Zeit. Beides ist eine bewusste Entscheidung, keine KI-Slop-Symptomatik: Vorarbeit vor dem Fork (Issues und Commits gelesen, Chat 2 ingame genutzt), eine sauber strukturierte Upstream-Codebase als Fundament, atomare Commits im Stil des Upstream und AI-gestütztes Review-Sparring, das ich nicht blind übernehme. Die volle Begründung steht in [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md), Sektion "Wie ich so schnell release".
|
||||||
- [ ] WireGuard-Network-Detection (optionaler Filter)
|
|
||||||
- [ ] libnotify-Integration (native Linux-Toasts)
|
|
||||||
- [ ] XDG-Compliance (komplex unter Wine)
|
|
||||||
- [ ] Hand-gezeichnetes Hellion-Logo (Platzhalter aus Hellion-Online-Media-Brand-Repo)
|
|
||||||
- [ ] GitHub-Actions für reproduzierbaren Build und automatischen `repo.json`-Sync
|
|
||||||
- [ ] Submission ans Dalamud-Main-Plugin-Repo
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -310,23 +278,41 @@ FINAL FANTASY XIV © SQUARE ENIX CO., LTD. Alle Rechte vorbehalten. Hellion Chat
|
|||||||
|
|
||||||
### KI-Unterstützung
|
### KI-Unterstützung
|
||||||
|
|
||||||
Siehe [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) für die Pair-Level-Disclosure.
|
Siehe [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) für die Pair-Level-Disclosure.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Projekt-Dokumente
|
## Projekt-Dokumente
|
||||||
|
|
||||||
|
Im Repo-Root liegen die Standard-Repository-Dokumente, vertiefende
|
||||||
|
Dokumentation lebt unter [`docs/`](docs/).
|
||||||
|
|
||||||
|
### Repo-Root
|
||||||
|
|
||||||
| Dokument | Inhalt |
|
| Dokument | Inhalt |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [`PRIVACY.md`](PRIVACY.md) | Datenschutz-Übersicht: lokale Speicherung, Outbound-Calls, Telemetry-Status, DSGVO-Rechte und ihre Plugin-Entsprechungen. |
|
| [`PRIVACY.md`](PRIVACY.md) | Datenschutz-Übersicht: lokale Speicherung, Outbound-Calls, Telemetry-Status, DSGVO-Rechte und ihre Plugin-Entsprechungen. |
|
||||||
| [`SECURITY.md`](SECURITY.md) | Vulnerability-Reporting via Private Advisory, Scope und Disclosure-Fenster. |
|
| [`SECURITY.md`](SECURITY.md) | Vulnerability-Reporting via Private Advisory, Scope und Disclosure-Fenster. |
|
||||||
| [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. |
|
|
||||||
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Was ich akzeptiere bzw. ablehne, Workflow, Build-Anleitung, EUPL-1.2-Bestätigung. |
|
|
||||||
| [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | Verhaltens-Erwartungen und Reporting-Pfad. |
|
|
||||||
| [`SUPPORT.md`](SUPPORT.md) | Wegweiser für Bugs, Security, Privacy, Quick-Questions. |
|
| [`SUPPORT.md`](SUPPORT.md) | Wegweiser für Bugs, Security, Privacy, Quick-Questions. |
|
||||||
| [`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. |
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Was ich akzeptiere bzw. ablehne, Workflow, EUPL-1.2-Bestätigung. |
|
||||||
|
| [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | Verhaltens-Erwartungen und Reporting-Pfad. |
|
||||||
| [`NOTICE.md`](NOTICE.md) | Attribution an Upstream-Maintainer und Komponenten-Credits. |
|
| [`NOTICE.md`](NOTICE.md) | Attribution an Upstream-Maintainer und Komponenten-Credits. |
|
||||||
| [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. |
|
| [`COPYRIGHT`](COPYRIGHT) | Copyright-Notes mit Dual-Holder-Block. |
|
||||||
|
| [`LICENSE`](LICENSE) | EUPL-1.2 Volltext. |
|
||||||
|
|
||||||
|
### `docs/`
|
||||||
|
|
||||||
|
| Dokument | Inhalt |
|
||||||
|
| --- | --- |
|
||||||
|
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Geplante Cycles, mittelfristige und langfristige Themen. |
|
||||||
|
| [`docs/CHANGELOG.md`](docs/CHANGELOG.md) | Kuratierte Versions-Übersicht mit Verweis auf die GitHub-Release-Pages. |
|
||||||
|
| [`docs/CONTRIBUTORS.md`](docs/CONTRIBUTORS.md) | Tester, Übersetzer und Code-Beiträger der Hellion-Seite. |
|
||||||
|
| [`docs/LEARNING-JOURNEY.md`](docs/LEARNING-JOURNEY.md) | Entwicklungsgeschichte, vom Web-Stack zu C# / Dalamud, was ich aus dem Fork gelernt habe. |
|
||||||
|
| [`docs/IPC.md`](docs/IPC.md) | IPC-Kanal-Reference, Tuple-Payload-Felder, Migrations-Diff für Drittplugins. |
|
||||||
|
| [`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md) | Theme-Engine-Authoring-Guide (EN): JSON-Schema, Color-/Layout-Slots, Channel-Identity-Regeln, Validierung. |
|
||||||
|
| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. |
|
||||||
|
| [`docs/THIRD_PARTY_NOTICES.md`](docs/THIRD_PARTY_NOTICES.md) | NuGet-Dependencies mit Lizenzen, Bundled Assets, Network-Status pro Komponente. |
|
||||||
|
| [`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) | Offenlegung der KI-Unterstützung im Entwicklungsprozess. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ comfortable with Dalamud and plugin development in general.
|
|||||||
|
|
||||||
Upstream Chat 2 (by Infi & Anna, EUPL-1.2) is the foundation and was not
|
Upstream Chat 2 (by Infi & Anna, EUPL-1.2) is the foundation and was not
|
||||||
produced with AI assistance. Hellion-specific code lives in
|
produced with AI assistance. Hellion-specific code lives in
|
||||||
`ChatTwo/Privacy/`, `ChatTwo/Export/`, `Resources/HellionStrings*`,
|
`HellionChat/Privacy/`, `HellionChat/Export/`, `HellionChat/Resources/HellionStrings*`,
|
||||||
`Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`,
|
`Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`,
|
||||||
plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs`
|
plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs`
|
||||||
and `Plugin.cs`. These were developed with Pair-level assistance as
|
and `Plugin.cs`. These were developed with Pair-level assistance as
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Changelog — Hellion Chat
|
||||||
|
|
||||||
|
Alle nutzersichtbaren Änderungen an Hellion Chat. Das Format orientiert
|
||||||
|
sich an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/), die
|
||||||
|
Version-Nummern folgen [Semantischer Versionierung](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
Detaillierte Release-Notes pro Version stehen direkt am
|
||||||
|
[GitHub-Release](https://github.com/JonKazama-Hellion/HellionChat/releases)
|
||||||
|
und im Plugin-Changelog-Block (`HellionChat/HellionChat.yaml` →
|
||||||
|
`changelog:`). Diese Datei fasst die Releases als Überblick zusammen
|
||||||
|
und verlinkt für Details auf die Release-Pages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.1.0] — 2026-05-05 — Theme Foundation
|
||||||
|
|
||||||
|
Erster großer UI-Cycle nach v1.0.0. Theme-Engine, fünf Built-In-Themes,
|
||||||
|
Custom-Themes via JSON, Settings-Card-Grid.
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- **Theme-Engine** mit fünf Built-In-Themes: Hellion Arctic (Default),
|
||||||
|
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove.
|
||||||
|
- **Settings → Themes** mit Mini-Mockup-Preview pro Theme. Klick auf
|
||||||
|
eine Card switcht sofort das ganze Plugin (Chat, Settings, Pop-Out).
|
||||||
|
- **Custom-Themes via JSON** in `pluginConfigs/HellionChat/themes/`.
|
||||||
|
Beim ersten Start wird `example-theme.json` als Vorlage abgelegt.
|
||||||
|
- **Optional Theme-Chat-Channel-Colors**: Themes können eigene
|
||||||
|
Channel-Farben mitliefern. Beim Switch erscheint ein Banner mit
|
||||||
|
*Übernehmen / Behalten* — nie automatisch.
|
||||||
|
- **Settings-Card-Grid**: neue Übersicht beim Öffnen, Card-Klick führt
|
||||||
|
in die Detail-Ansicht der Section. Breadcrumb + ESC führen zurück.
|
||||||
|
- **`docs/THEME-AUTHORING.md`** als Anleitung zum Schreiben eigener
|
||||||
|
Themes, mit Hellion-Forge-Branding.
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- **Plugin-Icon** auf Hellion Forge Hammer (vorher ChatTwo-Derivat).
|
||||||
|
- **Settings-Detail-View** verwendet die volle Breite — die zweite
|
||||||
|
Tab-Liste links ist weg, weil die Card-Übersicht den Wechsel
|
||||||
|
übernimmt.
|
||||||
|
- **`HellionStyle.PushGlobal`** ist jetzt theme-driven (`PushGlobal(theme,
|
||||||
|
opacity)`) statt const-palette-driven.
|
||||||
|
- **Configuration v13 → v14**: alle User landen auf `hellion-arctic`.
|
||||||
|
Wer den Upstream-Look will, wählt `chat2-classic` in Settings →
|
||||||
|
Themes.
|
||||||
|
|
||||||
|
### Veraltet
|
||||||
|
|
||||||
|
- `Configuration.HellionThemeEnabled` und `HellionThemeWindowOpacity`
|
||||||
|
bleiben für ein Release lesbar als Safety-Net, werden aber nicht
|
||||||
|
mehr ausgewertet. Entfernung geplant in v1.2.0.
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
|
||||||
|
- Custom-Theme-JSON-Loader prüft `schemaVersion`, Pflichtfelder und
|
||||||
|
Hex-Format. Ungültige Themes werden mit Warning übersprungen, das
|
||||||
|
Plugin lädt mit Built-Ins weiter.
|
||||||
|
|
||||||
|
### Intern
|
||||||
|
|
||||||
|
- 51 lokale Unit-Tests (Theme-Records, Registry, JSON-Round-Trip,
|
||||||
|
Sanity pro Built-In-Theme). Tests sind gitignored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.0.3] — 2026-05-04 — Polish patch
|
||||||
|
|
||||||
|
Vier kleine Polish-Items aus dem Backlog gebündelt:
|
||||||
|
|
||||||
|
- **Hide bei New Game+ Menü**: Optionaler globaler Toggle der Hellion
|
||||||
|
Chat (und alle weiteren Plugin-Fenster wie Settings, DB-Viewer,
|
||||||
|
Pop-Outs) ausblendet, solange das NG+-Menü offen ist. Settings →
|
||||||
|
Fenster → Rahmen, Default aus. Skipt analog zum bestehenden
|
||||||
|
LoadingScreens-Pattern den gesamten `WindowSystem.Draw()`-Pfad.
|
||||||
|
- **Channel-Selector-Färbung**: Optionales Tinting des
|
||||||
|
Channel-Auswahl-Knopfs (Comment-Icon) neben dem Eingabefeld in der
|
||||||
|
aktuellen Channel-Farbe. Settings → Aussehen → Chat-Farben, Default
|
||||||
|
an. Konsistent zur bestehenden Eingabetext-Färbung, ExtraChat-Override
|
||||||
|
wird übernommen.
|
||||||
|
- **(De)Buff-Icon Aspect-Ratio-Fix**: `PayloadHandler.InlineIcon` quetschte
|
||||||
|
alle Hover-Icons auf 32×32. Status-Icons mit nicht-quadratischen
|
||||||
|
Dimensionen (Debuffs mit Pfeil-Indikator) sind jetzt aspekt-erhaltend
|
||||||
|
geshrinkt. Eigenständige Float-Math-Implementierung mit Zero-Size-Guard
|
||||||
|
statt Cherry-Pick aus dem offenen ChatTwo PR #157 (der hatte eine
|
||||||
|
int-Division-Falle).
|
||||||
|
- **HideState-Logging-Sweep**: Alle HideState-Transitions
|
||||||
|
(Battle/Cutscene/User/Override plus die Pop-Out-Spiegelung) loggen sich
|
||||||
|
auf Verbose-Level. Aus by default, Aktivierung via
|
||||||
|
`/xllog set HellionChat verbose` für Bug-Report-Diagnose.
|
||||||
|
|
||||||
|
[Release-Notes 1.0.3](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.3)
|
||||||
|
|
||||||
|
## [1.0.1] — 2026-05-04 — Window Position Recovery
|
||||||
|
|
||||||
|
Fixes an off-screen-window scenario the user could end up in after a
|
||||||
|
monitor disconnect or display layout change between sessions. An
|
||||||
|
automatic one-shot bounds check on the first draw after plugin load
|
||||||
|
snaps the window back into the visible viewport, and a new
|
||||||
|
"Reset Window Position" button in Settings → Window → Frame serves as
|
||||||
|
the manual escape hatch for edge cases.
|
||||||
|
|
||||||
|
Bundled housekeeping since v1.0.0: documentation restructured into
|
||||||
|
`docs/`, stale ChatTwo/* paths in repo configs cleaned up, Pidgin
|
||||||
|
parser library bumped from 3.3.0 to 3.5.1, GitHub Actions bumps for
|
||||||
|
`actions/setup-dotnet` (4 → 5) and `github/codeql-action` (3 → 4).
|
||||||
|
|
||||||
|
[Release-Notes 1.0.1](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.1)
|
||||||
|
|
||||||
|
## [1.0.0] — 2026-05-03 — Standalone Major Release
|
||||||
|
|
||||||
|
Erste vollständig eigenständige Version. Code-Namespace, IPC-Kanäle und
|
||||||
|
Source-Tree-Struktur wurden auf `HellionChat.*` konsolidiert. Plugin
|
||||||
|
verweigert den Start bei aktivem Upstream Chat 2 (bilinguale
|
||||||
|
Konflikt-Meldung). SQLite-Native auf 3.50.3 gepinnt (CVE-2025-6965,
|
||||||
|
CVE-2025-7709). Tab-Layout-Default für neue Installationen und für
|
||||||
|
User auf Config-Version 12 oder älter neu strukturiert (5 thematische
|
||||||
|
Tabs statt 6+ kitchen-sink). Sweep aus Critical- und Major-Findings
|
||||||
|
aus dem Codebase-Audit eingearbeitet.
|
||||||
|
|
||||||
|
[Release-Notes 1.0.0](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v1.0.0)
|
||||||
|
|
||||||
|
## [0.6.1] — 2026-05-03 — Pop-Out Discoverability & /tell Auto-Pop-Out
|
||||||
|
|
||||||
|
Pop-Out-Button im Chat-Header sichtbar, einmaliger Hint-Banner für die
|
||||||
|
Pop-Out-Funktionalität. Neue Einstellung "Neue /tell-Tabs direkt als
|
||||||
|
Pop-Out öffnen". Pop-Out-Input ist jetzt standardmäßig aktiv.
|
||||||
|
Bugfixes: Ghost-Windows bei LRU-Drop / Logout, Dead-Zone unter dem
|
||||||
|
Input-Bar bei aktivem Hint-Banner.
|
||||||
|
|
||||||
|
[Release-Notes 0.6.1](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.6.1)
|
||||||
|
|
||||||
|
## [0.6.0] — 2026-05-03 — UX Polish: Pop-Out Input + Colour Presets
|
||||||
|
|
||||||
|
Zwei opt-in UX-Features. Pop-Out-Fenster bekommen optional eine
|
||||||
|
kompakte Eingabe-Bar mit channel-farbigem Icon-Button und unabhängigem
|
||||||
|
Text-Buffer pro Pop-Out. Sieben Built-in-Color-Presets (Klassik,
|
||||||
|
High-Contrast, Pastell, Dark-Mode-Tuned, Hellion, Night Blue, Indigo
|
||||||
|
Violet) zum One-Click-Apply. Konfigurations-Migration v10 → v11.
|
||||||
|
|
||||||
|
[Release-Notes 0.6.0](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.6.0)
|
||||||
|
|
||||||
|
## [0.5.4] — 2026-05-02 — WrapText Hardening
|
||||||
|
|
||||||
|
`ImGuiUtil.WrapText` von Pointer-Arithmetik auf Span- und
|
||||||
|
Index-basierten Control-Flow umgestellt. Schließt das wiederkehrende
|
||||||
|
CodeQL-Critical-Alert "unvalidated local pointer arithmetic"
|
||||||
|
dauerhaft. Keine nutzersichtbare Verhaltensänderung — Word-Wrap-Output
|
||||||
|
ist byte-identisch zu 0.5.3.
|
||||||
|
|
||||||
|
[Release-Notes 0.5.4](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.5.4)
|
||||||
|
|
||||||
|
## [0.5.3] — 2026-05-02 — Pointer Arithmetic Hardening
|
||||||
|
|
||||||
|
Erster Anlauf zur Schließung des CodeQL-Critical-Alerts in
|
||||||
|
`ImGuiUtil.WrapText`. Encoded-Byte-Buffer-Length wird vor der
|
||||||
|
Pointer-Arithmetik via `GetByteCount` validiert.
|
||||||
|
|
||||||
|
[Release-Notes 0.5.3](https://github.com/JonKazama-Hellion/HellionChat/releases/tag/v0.5.3)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frühere Versionen
|
||||||
|
|
||||||
|
Releases vor 0.5.3 (Bootstrap-Phase 0.1.0 bis 0.5.2) sind direkt am
|
||||||
|
GitHub-Release-Stream einsehbar:
|
||||||
|
|
||||||
|
[Alle Releases](https://github.com/JonKazama-Hellion/HellionChat/releases)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pflege-Hinweis
|
||||||
|
|
||||||
|
Die Source-of-Truth für den nutzersichtbaren Changelog ist der
|
||||||
|
`changelog:`-Block in `HellionChat/HellionChat.yaml`. `repo.json` und
|
||||||
|
der GitHub-Release-Body werden daraus gespeist. Diese Datei
|
||||||
|
(`docs/CHANGELOG.md`) ist eine kuratierte Zusammenfassung mit Verweis
|
||||||
|
auf die Release-Pages und wird beim Versions-Bump manuell ergänzt.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Contributors — Hellion Chat
|
||||||
|
|
||||||
|
Hellion Chat ist von der Code-Seite ein Ein-Personen-Projekt. Aber ohne die Leute auf dieser Seite gäbe es weder die Bug-Fixes noch die UX-Verbesserungen, die seit den frühen Versionen reingelaufen sind. Jeder Eintrag hier hat das Plugin konkret besser gemacht.
|
||||||
|
|
||||||
|
Die Anerkennung an die Upstream-Autoren von Chat 2 (Infi und Anna) liegt bewusst in [`../NOTICE.md`](../NOTICE.md), nicht hier. Diese Datei deckt explizit Beiträge zur Hellion-Chat-Seite ab.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
### JonKazama (Florian Wathling) — Maintainer
|
||||||
|
|
||||||
|
Hellion Chat ist mein erstes FFXIV-Plugin und mein erstes größeres C#-/Dalamud-Projekt. Mein beruflicher Hintergrund ist Webentwicklung (Next.js, React, TypeScript, Prisma). Plugin-Entwicklung in einer fremden Codebase, ImGui, FFXIV-Game-Hooks und der gesamte Dalamud-Stack waren Neuland.
|
||||||
|
|
||||||
|
Privacy-First-Defaults, Per-Channel-Retention, Auto-Tell-Tabs, Pop-Out-Input, ChatColours-Presets, Hellion-Theme plus Exo-2-Font und der v1.0.0-Standalone-Cut sind die Hellion-spezifischen Surface-Areas, die ich auf das Chat-2-Fundament aufgebaut habe. Die Lern-Geschichte dahinter steht in [`LEARNING-JOURNEY.md`](LEARNING-JOURNEY.md).
|
||||||
|
|
||||||
|
Hellion Chat ist Teil von [Hellion Online Media](https://hellion-media.de).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tester
|
||||||
|
|
||||||
|
Eine kurze Notiz vorneweg: Ich teste das Plugin nicht allein. Die Leute hier haben mir Bugs gemeldet, bevor sie bei mehr Nutzern aufgeschlagen wären. Sie haben UX-Probleme angesprochen, die ich blind nicht mehr gesehen habe. Und sie haben Feature-Wünsche eingebracht, die das Plugin in Richtungen geschoben haben, in die ich von alleine nicht gegangen wäre. Das ist nicht selbstverständlich. Externe Tester sind ihre Zeit wert.
|
||||||
|
|
||||||
|
### Carl Beleandis (Carla) — Beta-Tester
|
||||||
|
|
||||||
|
Carl testet seit der Bootstrap-Phase und hat sowohl die Pop-Out-Mechanik als auch die Theme-Richtung geprägt. Sein Feedback kommt direkt und ohne Umschweife und das ist genau, was ich beim Testen brauche.
|
||||||
|
|
||||||
|
Konkrete Beiträge:
|
||||||
|
|
||||||
|
- **Pop-Out-Discoverability** — der Hinweis, dass Pop-Outs nur per Rechtsklick erreichbar waren, hat den Header-Button und den einmaligen Hint-Banner in v0.6.1 ausgelöst. Ich kannte den Rechtsklick-Pfad blind, deshalb hatte ich nicht mehr gesehen, dass neue Nutzer die Funktion gar nicht finden.
|
||||||
|
- **/tell-Pop-Out-Mode** — der Wunsch, /tell-Tabs direkt als Pop-Out zu öffnen statt über den Tab-Umweg, ist in v0.6.1 als opt-in Settings-Toggle gelandet. Bonus: Bei der Implementation ist ein alter Ghost-Window-Bug aufgefallen (LRU-Drop ließ Pop-Out-Fenster als Geister stehen), der gleich mit gefixt wurde.
|
||||||
|
- **Theme-Varianten mit Helligkeits-Abstufungen** — der Wunsch nach einer Grün-Familie hat mein Verständnis von "ein Theme = eine Farbe" auf "Theme-Familien mit Stimmungs-Varianten" verschoben. Steht in der [Roadmap](ROADMAP.md) für einen späteren Cycle.
|
||||||
|
|
||||||
|
### Jin (Jingliu) — Alpha-Tester
|
||||||
|
|
||||||
|
Jin ist der aktive Tester der ersten Stunde und hat den Pop-Out-Workflow architektonisch in eine andere Richtung geschoben.
|
||||||
|
|
||||||
|
Konkrete Beiträge:
|
||||||
|
|
||||||
|
- **Pop-Out-Tab mit Input-Feld** — der Vorschlag, in einem Pop-Out auch tippen zu können (statt nur lesen), hat die v0.6.0 Pop-Out-Input-Bar ausgelöst. Das war ein größerer Refactor: Der Input-Layer aus `ChatLogWindow` musste so geöffnet werden, dass er auch in `Popout.cs` lebt, mit unabhängigem Text-Buffer und History-Cursor pro Pop-Out. Hat den Cycle dominiert, weil das Design erst sauber sein musste, bevor Code passieren konnte.
|
||||||
|
- **TempTell Persistence** — der Wunsch, /tell-Tabs per Pin-Toggle einen Relog überleben zu lassen, steht in der [Roadmap](ROADMAP.md) für einen späteren Cycle. Berührt das Tab-System architektonisch und braucht eigenes Design.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Übersetzungen
|
||||||
|
|
||||||
|
Hellion-eigene UI-Strings werden in `HellionChat/Resources/HellionStrings.<lang>.resx` gepflegt.
|
||||||
|
|
||||||
|
- **Deutsch (DE):** JonKazama (Native Speaker, Hauptsprache des Projekts)
|
||||||
|
|
||||||
|
Die Upstream-Sprach-Dateien (`Language.<lang>.resx`) sind nicht Teil dieser Datei. Sie werden über das [Chat-2-Crowdin-Projekt](https://github.com/Infiziert90/ChatTwo) gepflegt; Crowdin-Übersetzer findest du in den Plugin-Settings unter **Info → "Chat 2 community translators"**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wie du beitragen kannst
|
||||||
|
|
||||||
|
Bug-Reports, Feature-Wünsche und Pull-Requests laufen über [GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues). Workflow und Erwartungen stehen in [`../CONTRIBUTING.md`](../CONTRIBUTING.md), Code of Conduct in [`../CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
Tester-Pool für neue Versionen läuft über den Hellion-Forge-Discord: [discord.gg/X9V7Kcv5gR](https://discord.gg/X9V7Kcv5gR). Wer in den Tester-Channel rein will, einfach im Forge melden.
|
||||||
|
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Entwicklungsgeschichte und Lernprozess
|
||||||
|
|
||||||
|
## Hintergrund
|
||||||
|
|
||||||
|
Ich bin Autodidakt. Hellion Chat ist mein erstes FFXIV-Plugin und mein erstes größeres C#-Projekt. Mein beruflicher Hintergrund ist Webentwicklung (Next.js, React, TypeScript, Prisma, MySQL), also Browser-Welt mit JavaScript-Toolchain. C# kannte ich vor diesem Projekt nur oberflächlich, ImGui gar nicht, Dalamud nur als Endnutzer über andere Plugins.
|
||||||
|
|
||||||
|
Wenn ich an einer Stelle nicht weiterkomme, nutze ich AI-Tools wie Claude Code als Pair-Hilfsmittel. Wie das genau aussieht und welche Klassifikation ich verwende, steht transparent in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Warum überhaupt ein Chat-Plugin?
|
||||||
|
|
||||||
|
Hellion Chat soll Chat 2 nicht ersetzen. Chat 2 liefert ein vollständiges Chat-Erlebnis mit kompletter Historie, Filtern, Suche und Replay. Für die meisten Nutzer ist genau das richtig.
|
||||||
|
|
||||||
|
### Zwei Millionen Nachrichten in zwei Jahren
|
||||||
|
|
||||||
|
Mein Wunsch nach einem engeren Default war ehrlich gesagt erstmal persönlich. Nach zwei Jahren mit Chat 2 lag meine Datenbank bei über zwei Millionen Nachrichten, der Großteil davon /say, /shout und /yell von wildfremden Leuten in Limsa. Genau diese Daten machen Chat 2's Voll-Historie nützlich, und die meisten Nutzer behalten sie auch gerne. Mein eigener Geschmack wollte einen kleineren Default. Also habe ich diesen Fork gebaut.
|
||||||
|
|
||||||
|
### Greeter in mehreren Clubs
|
||||||
|
|
||||||
|
Dazu kam ein zweiter Use-Case: Ich bin in mehreren FFXIV-Clubs als Greeter aktiv. Für die Greeter-Arbeit reicht die Vanilla-Chat-Oberfläche nicht. Parallel laufende /tell-Gespräche schreiben in einem einzigen Tab durcheinander, und ich verliere ständig den Faden, wer mir gerade was geschrieben hat. Auto-Tell-Tabs (eines der frühen Hellion-Chat-Features) ist genau für diesen Workflow entstanden: ein Tab pro Gesprächspartner, automatisch gespawnt, mit manuellem Greeted-Status. Dass das auch der Privacy-Hygiene gut tut, war ein netter Bonus, nicht der Auslöser.
|
||||||
|
|
||||||
|
### Hellion Online Media
|
||||||
|
|
||||||
|
Die Privacy-Defaults sind außerdem eine Position aus meinem Hauptberuf. Hellion Online Media ist mein Einzelunternehmen, und Datenschutz gegenüber Kunden ist da kein Marketing-Slogan, sondern operativ relevant. Dieser Fork ist die Plugin-Form derselben Haltung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Warum nicht beim Original mitarbeiten?
|
||||||
|
|
||||||
|
Drei Gründe, in absteigender Wichtigkeit.
|
||||||
|
|
||||||
|
### Defaults sind nicht verhandelbar, auch nicht meine
|
||||||
|
|
||||||
|
Privacy-First als Standard ist eine Minderheits-Position. Chat 2 bedient zu Recht die breite Masse mit Voll-Historie als Default. Diese Defaults im Upstream zu ändern wäre falsch gewesen. Ich hätte den Standard für eine große Nutzerbasis umgekippt, die ihn so wollte, wie er ist. Saubere Trennung über einen eigenen Plugin-Slot war der respektvollere Weg.
|
||||||
|
|
||||||
|
### Das Webinterface musste weg
|
||||||
|
|
||||||
|
Das ist ein zentrales Chat-2-Feature für Remote-Zugriff vom Zweitgerät. Ein PR der das entfernt, hat in einem gepflegten Upstream-Projekt keine Chance, und das ist auch richtig so. Aber genau das Webinterface kollidiert mit der Privacy-First-These dieses Forks: Ein Chat-Plugin das einen lokalen HTTP-Server startet, ist für mein Threat-Model eine zu große Angriffsfläche. Also raus damit.
|
||||||
|
|
||||||
|
### Tempo
|
||||||
|
|
||||||
|
Ein Solo-Maintainer-Projekt mit kleinem Tester-Pool kann schneller iterieren als ein etabliertes Plugin mit großer Nutzerbasis. Das ist kein Vorwurf an Upstream, sondern eine andere Optimierung. Ich brauche keine Roadmap-Abstimmung, keine Reviewer-Verfügbarkeit, und kann Audit-Konsequenzen wie das Webinterface-Removal in einer einzigen Version durchziehen statt über mehrere Releases.
|
||||||
|
|
||||||
|
EUPL-1.2 erlaubt das alles ausdrücklich, mit klarer Attribution. Der Code liegt offen unter derselben Lizenz wie Chat 2. Infi, Anna oder sonst jemand dürfen reinschauen, Ideen mitnehmen, Fragen stellen oder den Fork einfach ignorieren. Alles drei ist für mich okay.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wie ich so schnell release
|
||||||
|
|
||||||
|
Wer auf den Repo schaut, sieht in kurzer Zeit viele Releases und sehr viele Commits. Beides wird von außen gerne als Red-Flag gelesen: KI-Slop, Salami-Taktik, Code-Spam. Bei Hellion Chat ist beides eine bewusste Entscheidung, und ich erkläre lieber einmal warum, als mich später dafür zu rechtfertigen.
|
||||||
|
|
||||||
|
### Vorarbeit, lange bevor der Fork existierte
|
||||||
|
|
||||||
|
Bevor ich die erste Zeile in `HellionChat/` getippt habe, war ich wochenlang nur Leser. Chat 2 ingame nutzen und damit rumspielen. Issues im Upstream-Tracker durchgehen, vor allem die geschlossenen, weil dort steht, wie Infi und Anna Bugs einkreisen. Commits lesen, gerne auch ältere, um zu verstehen, warum eine Architektur-Entscheidung getroffen wurde, nicht nur, dass sie getroffen wurde. Wenn ich heute weiß, wo im Code was liegt, dann nicht, weil ich besonders schnell durch eine Codebase navigiere, sondern weil ich den Code vorher gelesen habe.
|
||||||
|
|
||||||
|
Klingt nach Selbstverständlichkeit, ist es aber nicht. Die übliche Reihenfolge bei Solo-Forks heißt erst forken, dann verstehen. Ich habe es andersrum gemacht.
|
||||||
|
|
||||||
|
### Die Codebase von Infi und Anna
|
||||||
|
|
||||||
|
Hellion Chat baut auf einem Boden auf, der schon flach ist. Chat 2 ist sauber strukturiert, die Naming-Konventionen sind konsistent, die Trennung zwischen Layern (Storage, UI, Game-Hooks, IPC) ist klar gezogen. Das ist in Open-Source-Plugin-Welten nicht selbstverständlich, und es ist der Hauptgrund, warum sich Hellion-spezifische Features oft "fast nativ" einbauen lassen. Ich muss nicht erst Spaghetti entwirren bevor ich was Eigenes danebenstellen kann.
|
||||||
|
|
||||||
|
Side-Fact: Selbst beim ersten Codebase-Walkthrough mit Claude kam mehrfach der Hinweis, dass die Architektur ungewöhnlich gut aufgeräumt ist und mehrere Erweiterungspunkte vorbereitet. Das hat Gewicht, weil es von außen kommt, aber den eigentlichen Kredit kriegen Infi und Anna, nicht Claude.
|
||||||
|
|
||||||
|
### Atomar arbeiten, kleine Commits
|
||||||
|
|
||||||
|
Ein Commit, eine logische Änderung. Wenn ich einen Bug fixe, parallel eine Variable umbenenne und nebenbei einen Kommentar einbaue, sind das drei Commits, nicht einer. Klingt nach Mikro-Management, ist es aber nicht. Wenn in sechs Monaten ein Bug auftaucht und ich `git bisect` brauche, finde ich die kaputte Änderung in zwei Minuten statt in zwei Stunden. Bei einem 4000-Zeilen-Mega-Commit darf ich raten, welche der hundert Änderungen die kaputte ist.
|
||||||
|
|
||||||
|
Den Stil habe ich bewusst auch deshalb beibehalten, weil Infi im Upstream häufig genauso arbeitet. Manchmal ein Sechs-Zeilen-Commit, manchmal nur ein Typo-Fix. Das ist keine Schwäche, das ist eine Entscheidung für lesbare Git-History. Den Stil im Fork beizubehalten ist ein Respekt-Move: Wer die beiden Repos vergleicht, soll den gleichen Lese-Rhythmus haben.
|
||||||
|
|
||||||
|
Bonus für mich persönlich: Kleine Commits zwingen mich, jeden Schritt einzeln zu durchdenken und zu benennen. Wenn ich nicht in zwei Sätzen erklären kann, was ein Commit macht, ist die Änderung wahrscheinlich noch nicht klar genug. Auf Beginner-Niveau ist das ein eingebauter Sanity-Check, den ich bei einem Big-Bang-Commit nicht hätte.
|
||||||
|
|
||||||
|
### AI als Beschleuniger, ehrlich
|
||||||
|
|
||||||
|
Ja, AI hilft beim Tempo, und nicht zu knapp. Ohne CodeRabbit hätte ich Critical-Bugs der Klasse `Equals/GetHashCode`-Anti-Pattern, Hook-Subscription-Leaks und TOCTOU-Races nicht gefunden. Ich bin schlicht zu unerfahren für diese Klasse von Findings, das schreibe ich genau so hin.
|
||||||
|
|
||||||
|
Was ich aber nicht mache: blind Code übernehmen, weil ein Tool ihn als Fix markiert hat. Bei mehreren CodeRabbit-Findings stand in den Original-Commits von Infi oder Anna sogar ein Stackoverflow-Link mit Begründung dabei, warum eine bestimmte Stelle so aussieht wie sie aussieht. Die habe ich gelesen, bevor ich was geändert habe. Erst verstehen, dann anfassen, dann committen. Das ist der Unterschied zwischen "AI gibt mir Code, ich pushe" und "AI zeigt mir wo's klemmt, ich entscheide".
|
||||||
|
|
||||||
|
Klassifikation und konkrete Beispiele zur AI-Nutzung stehen in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). Hier in dieser Sektion ging es nur um den Tempo-Aspekt: Recherche plus saubere Codebase plus atomare Commits plus AI-gestütztes Review-Sparring sind die vier Faktoren zusammen. Kein einzelner davon erklärt das Tempo allein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vom Web-Stack zu C# / Dalamud
|
||||||
|
|
||||||
|
### Type-System? Weniger Schock als erwartet
|
||||||
|
|
||||||
|
C# nach TypeScript war angenehmer als gedacht. Properties statt getter/setter sind sauber, nullable reference types fühlen sich an wie `strict: true` in TypeScript. Ungewohnt war Wert-Typen vs. Referenz-Typen explizit denken zu müssen (`struct` vs. `class` mit echten Verhaltens-Konsequenzen), und Generics mit Constraints sind syntaktisch anders genug, dass ich beim Lesen kurz stocke. `async`/`await` ist semantisch ähnlich, aber Threading-Modelle sind in C# expliziter: `Task.Run`, `ConfigureAwait`, Synchronization-Contexts. Das hat mich mehrere Bugs gekostet, bevor ich verstanden hatte, wann der Main-Thread (in Plugin-Welt: der Framework-Tick) wirklich kritisch ist.
|
||||||
|
|
||||||
|
### Build-Toolchain: ähnlich, aber anders
|
||||||
|
|
||||||
|
`dotnet` CLI, csproj-XML, NuGet sind funktional nicht weit weg von npm und tsconfig. Aber das XML-Format der csproj ist eine andere Sprache als JSON-Configs. Die Lock-Datei (`packages.lock.json`) musste ich erst aktiv aktivieren (`RestorePackagesWithLockFile=true`); das ist nicht Default. Im Web-Stack ist Lock-File-First Standard, im .NET-Stack offenbar nicht. Das war eine echte Überraschung.
|
||||||
|
|
||||||
|
### ImGui ist eine andere Welt
|
||||||
|
|
||||||
|
Immediate-Mode-Rendering hat mit React-Component-Trees nichts gemein. Es gibt keine virtuelle DOM, keine Reconciliation, keinen "State der Komponente". Pro Frame zeichnet der Code die UI komplett neu, und der State lebt entweder in lokalen Variablen, die ich selbst verwalten muss, oder in der ImGui-eigenen ID-Stack-Logik.
|
||||||
|
|
||||||
|
Was in React zwei Zeilen `useState` sind, ist in ImGui ein Member-Field plus manuelle ID-Stempel auf den Widgets, sonst kollidieren zwei Selectables in derselben Loop, weil sie auf die gleiche ID zurückfallen. Die ID-Stack-Kollision in `SearchSelector` (gefixt in v1.0.0) war genau dieses Symptom: Alle Selectables fielen auf dieselbe ambiguous ID zurück, bis ich den Row-Index in den Push-ID gemixt habe. Klassischer "warum klickt der falsche Eintrag"-Bug, den man nur findet, wenn man verstanden hat, wie ImGui IDs intern handhabt.
|
||||||
|
|
||||||
|
### Dalamud-Spezifika
|
||||||
|
|
||||||
|
Plugin-Lifecycle, IPC-Subscriber-Pattern, Hook-System für Game-Functions, Game-Object-Threading. Viel davon war nur durch Lesen der Upstream-Codebase und durch [dalamud.dev](https://dalamud.dev) zu verstehen. Meine Trainings- und Such-Ergebnisse für "Dalamud" liefern oft veraltete API-Beispiele aus alten Versionen. dalamud.dev ist die zuverlässige Quelle. Wenn jemand neu anfängt: dort hin, nicht zu Stack Overflow.
|
||||||
|
|
||||||
|
### Der Tag, an dem mich der DalamudPackager einen Tag gekostet hat
|
||||||
|
|
||||||
|
Dalamud SDK 15 liefert seinen eigenen Default-Packager mit, der Icons und Image-URLs ins Manifest einträgt. Ich hatte aus dem Upstream-Repo eine eigene `DalamudPackager.targets`-Datei mit `HandleImages`-Override übernommen, und die hat den SDK-Default überschrieben. Resultat: Das Manifest hatte keinen `IconUrl` mehr, und das Plugin tauchte in der Plugin-Liste ohne Icon auf.
|
||||||
|
|
||||||
|
Symptom war einfach zu sehen, Ursache hat einen Tag gekostet. Ich hatte die Override-Datei für eine Pflicht-Datei gehalten, war sie aber nicht. Removal in v0.5.2, seitdem läuft der SDK-Default. Lektion: Erstmal mit Defaults arbeiten, Overrides erst wenn der Default nachweislich nicht passt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was ich aus dem Fork gelernt habe
|
||||||
|
|
||||||
|
### Refactor in einer fremden Codebase
|
||||||
|
|
||||||
|
Der Standalone-Cut in v1.0.0 hat die `ChatTwo.*`-Identität komplett auf `HellionChat.*` migriert. Klingt nach Find-and-Replace. War es nicht.
|
||||||
|
|
||||||
|
Konkret bedeutete das: Code-Namespace über alle 80 Source-Files plus 100 using-Direktiven plus zwei FQN-Aliases plus die Resource-Designer-Strings. Sechs IPC-Channels umbenannt (Breaking Change für Drittplugins, keine bekannten Anbindungen). Repo-Ordner-Struktur (`ChatTwo/` → `HellionChat/`) inklusive csproj, sln, allen GitHub-Workflows und der dependabot.yml. Public-Facing-Branding in README, repo.json, yaml auf Standalone-Framing umformuliert.
|
||||||
|
|
||||||
|
Das war kein Solo-Find-and-Replace, weil Unicode-String-Pfade in Workflow-YAMLs anders quotiert werden müssen als C#-Strings. Weil Resource-Designer-Files generierte Inhalte haben, die nicht jede Toolchain im Blick hat. Und weil die `ChatTwo.*`-IPC-Channel-Namen Strings in `GetIpcSubscriber`-Calls sind: kein Symbol, kein Compile-Error, wenn man einen vergisst. Da merkst du, was alles still bleibt.
|
||||||
|
|
||||||
|
### Sicherheit ist kein abstraktes Thema mehr
|
||||||
|
|
||||||
|
Vor diesem Projekt war Supply-Chain-Sicherheit für mich akademisch. Drei konkrete Lektionen haben das geändert.
|
||||||
|
|
||||||
|
**SQLite-Native-Binary.** Ich musste auf 3.50.3 pinnen (`SQLitePCLRaw.lib.e_sqlite3` Override), weil `Microsoft.Data.Sqlite` die transitiv nachgezogene Lib in einer Version mitschleppte, die CVE-2025-6965 (Memory-Corruption durch Aggregate-Term-Overflow) und CVE-2025-7709 enthielt. Der Managed-Wrapper war neu, die Native-Lib war es nicht. Lektion: Transitive Dependencies prüfen sich nicht von selbst, du musst hinschauen.
|
||||||
|
|
||||||
|
**Lock-File-Drift.** `packages.lock.json` honored bei `dotnet restore` (per `RestorePackagesWithLockFile=true` in der csproj) verhindert, dass transitive Versionen zwischen meiner Maschine und CI silent driften. Erst nach einem Build-Output-Mismatch zwischen lokal und GitHub-Actions hatte ich überhaupt verstanden, warum das nicht der Default ist.
|
||||||
|
|
||||||
|
**WrapText und der CodeQL-Alarm der drei Releases gekostet hat.** CodeQL hat in `ImGuiUtil.WrapText` einen Critical-Alert wegen "unvalidated local pointer arithmetic" geworfen. v0.5.2 hat einen Edge-Case validiert. Alert kam wieder. v0.5.3 hat den Buffer-Length via `GetByteCount` vor der Pointer-Math gecheckt. Alert kam wieder. v0.5.4 hat den ganzen Algorithmus auf `Span` und int-Offsets umgebaut, mit einem 16-KiB-Cap auf den ArrayPool-Rent. Erst da war Ruhe.
|
||||||
|
|
||||||
|
Lektion: Wenn ein statischer Analyzer drei Mal hintereinander meckert, ist nicht der Analyzer überempfindlich. Die Datenflusslogik ist es.
|
||||||
|
|
||||||
|
### CodeRabbit als externer Code-Reviewer
|
||||||
|
|
||||||
|
Der v1.0.0-Sweep hat 3 Critical und 21 Major Findings hochgespült. Drei Klassen davon waren besonders lehrreich:
|
||||||
|
|
||||||
|
- **`Equals`-Methoden die `GetHashCode()` vergleichen.** Klassisches Hash-Kollisions-Anti-Pattern. Klingt nach "ist doch egal, wenn Hashes gleich sind, sind die Objekte auch gleich", ist aber genau falsch. Hashes können kollidieren, Objekte sind dann nicht gleich.
|
||||||
|
- **`Dispose`-Methoden die nur einen Teil der Subscriptions wieder abmelden.** Leak bei jedem Plugin-Reload. Im Nutzer-Alltag merkst du das nicht sofort, im Long-Running-Test schon.
|
||||||
|
- **TOCTOU-Races.** Zwischen Bounds-Check und Read kann ein anderer Thread das Array unter dir austauschen (`GlobalParametersCache`, `AutoTranslate`).
|
||||||
|
|
||||||
|
Davon hatte ich vorher bestenfalls die Theorie gelesen, nicht selbst diagnostiziert. CodeRabbit war für mich der Moment, wo "akademisches Wissen" zu "okay, das ist mein Code, das ist mein Bug" wurde.
|
||||||
|
|
||||||
|
### Externe Tester sind ihr Gewicht in Gold wert
|
||||||
|
|
||||||
|
Carlas Feedback zur Pop-Out-Discoverability hat den Header-Button in v0.6.1 ausgelöst. Dass Pop-Outs nur per Rechtsklick erreichbar waren, hatte ich als Maintainer nicht mehr gesehen, ich kannte den Pfad blind. Carls Wunsch nach Theme-Varianten mit Helligkeits-Abstufungen hat mein Verständnis von "ein Theme = eine Farbe" auf "Theme-Familien mit Stimmungs-Varianten" verschoben. Jingliu hat TempTell-Persistence gefordert, was das Tab-System architektonisch in Frage stellt.
|
||||||
|
|
||||||
|
Solo hätte ich diese drei Dinge nicht erkannt. Punkt.
|
||||||
|
|
||||||
|
### release.yml und die Markdown-Hölle
|
||||||
|
|
||||||
|
Der `release.yml`-Workflow ist beim ersten v0.6.0-Tag-Push einfach nicht losgegangen. Ich habe Stunden in Permissions, Secret-Scopes und Tag-Trigger-Konfiguration gegraben, bevor ich verstand, was eigentlich los war: Der PowerShell-Heredoc-Footer im "Generate release body"-Step enthielt eine `---`-Markdown-Horizontal-Rule an Spalte 1, und genau das hat das YAML-Block-Scalar von `run: |` beendet. GitHub konnte die Workflow-Datei nicht parsen, also hat der Push-Tag-Trigger nie registriert.
|
||||||
|
|
||||||
|
Fix: Footer in eine externe `.github/release-footer.md` extrahiert, Workflow liest sie via `Get-Content` ein. Lektion: Wenn ein Workflow nicht triggert, verifiziere als Erstes, dass GitHub die Datei überhaupt parsen kann. Das war einer der Bugs, bei denen ich nach dem Fix kurz gelacht habe und mich dann gefragt, wie viele andere YAML-Dateien ich noch habe, die so eine Falle drin haben könnten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was ich noch lerne
|
||||||
|
|
||||||
|
### Performance-Profiling im Game-Context
|
||||||
|
|
||||||
|
Der FPS-Drop-Bug aus Upstream Chat 2 ([#145](https://github.com/Infiziert90/ChatTwo/issues/145)) ist auch in Hellion Chat noch nicht reproduziert oder verifiziert. v1.0.0 hat mehrere Fixes auf den verdächtigen Pfaden (DbViewer O(N²) → O(N), AutoTranslate Lock-Serialisierung, EmoteCache HttpClient-Reuse), aber das systematische Vermessen unter Last fehlt mir. Ich muss noch lernen, wie man im Plugin-Kontext sauber misst, was wirklich das Frame-Budget frisst.
|
||||||
|
|
||||||
|
### Native-Interop und Pointer-Math
|
||||||
|
|
||||||
|
Auch nach dem WrapText-Span-Refactor in v0.5.4 ist mir Pointer-Math unsicher. ImGui zwingt einen an mehreren Stellen in `unsafe`-Code, und der Sicherheitsabstand zur "unbounded ArrayPool allocation"-Klasse von Bugs ist schmaler als mir lieb ist. Da will ich besser werden, bevor ich tieferes ImGui-Custom-Drawing anfasse.
|
||||||
|
|
||||||
|
### Test-Disziplin für Plugin-Code
|
||||||
|
|
||||||
|
Aktuell hat das Repo kein Test-Projekt. Das ist eine bewusste Entscheidung, keine vergessene. Plugin-Code mit FFXIV-Hooks und Dalamud-Lifecycle sauber zu testen ist nicht trivial, und ich hatte keinen Ansatz gefunden, der ohne riesiges Mocking-Gerüst sinnvoll wirkte. Privacy-Filter und Configuration-Migration wären gute Testkandidaten, weil sie isoliert sind. Steht auf der Liste, ist aber kein Quick-Win.
|
||||||
|
|
||||||
|
### Linux-Eigenheiten unter Wine
|
||||||
|
|
||||||
|
XDG-Compliance, libnotify-Integration, WireGuard-Network-Detection, alles in der [Roadmap](ROADMAP.md), und alles technisch noch nicht ganz klar. Wine und sandboxed Plugin-Code teilen nicht alle System-APIs, und ich weiß nicht, wo die Stolperfallen liegen, bevor ich sie gefunden habe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Einsatz von AI-Tools
|
||||||
|
|
||||||
|
Ich verwende Claude Code als Hilfsmittel, nicht als Ersatz für eigene Arbeit.
|
||||||
|
|
||||||
|
**Wofür ich AI einsetze:**
|
||||||
|
|
||||||
|
- Debugging von Problemen, bei denen ich nach längerer Eigenrecherche nicht weiterkomme
|
||||||
|
- Mustererkennen über große Codebasen hinweg (z. B. der ChatTwo→HellionChat-Sweep über 80 Dateien)
|
||||||
|
- Verständnisfragen zu C#- und Dalamud-Konzepten, die mir noch nicht geläufig sind
|
||||||
|
- Code-Review-Sparring, bevor ich CodeRabbit drauflasse
|
||||||
|
|
||||||
|
**Was ich selbst mache:**
|
||||||
|
|
||||||
|
- Architektur und Designentscheidungen
|
||||||
|
- Privacy-First-Defaults und das Threat-Model dahinter
|
||||||
|
- Tester-Kommunikation und Roadmap-Priorisierung
|
||||||
|
- Reviewen, Verifizieren, Pushen
|
||||||
|
|
||||||
|
Die Klassifikation und konkrete Beispiele stehen in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). Mir ist wichtig, dass Nutzer und potenzielle Beiträger verstehen, wie der Code zustande gekommen ist, gerade bei einem Plugin, das mit Nutzerdaten arbeitet.
|
||||||
|
|
||||||
|
Ja, AI. Ja, alleine. Beides öfter erwähnt als nötig. Willkommen im Open-Source-Plugin-Klima.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Warum diese Transparenz
|
||||||
|
|
||||||
|
Wer sich den Quellcode ansieht, soll wissen:
|
||||||
|
|
||||||
|
- Ich bin kein professioneller C#- oder Plugin-Entwickler und lerne weiterhin dazu
|
||||||
|
- AI-Unterstützung ist ein Werkzeug, kein Ghostwriter
|
||||||
|
- Die Privacy-Position, die Designentscheidungen und die Roadmap sind meine
|
||||||
|
- Ich versuche, meinen Code so sauber und sicher zu halten, wie meine aktuellen Fähigkeiten es zulassen
|
||||||
|
|
||||||
|
Hellion Chat ist auch ein Lernprojekt, und das soll man dem Repository ansehen dürfen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verlinkungen
|
||||||
|
|
||||||
|
- [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) — KI-Pair-Disclosure mit Klassifikations-Schema
|
||||||
|
- [`CONTRIBUTORS.md`](CONTRIBUTORS.md) — wer hat dieses Plugin neben mir besser gemacht
|
||||||
|
- [`../NOTICE.md`](../NOTICE.md) — Anerkennung an Infi und Anna für das Chat-2-Fundament
|
||||||
|
- [`ROADMAP.md`](ROADMAP.md) — geplante Cycles und Themen
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Hellion Chat — Roadmap
|
||||||
|
|
||||||
|
Geplante Arbeit nach dem v1.0.0 Standalone-Cut. Diese Liste ist absichtlich
|
||||||
|
grob: konkrete Specs, Größenschätzungen und Repro-Steps liegen im
|
||||||
|
internen Backlog. Tracking nach außen läuft über
|
||||||
|
[GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues)
|
||||||
|
mit dem `roadmap`-Label, sobald ein Item für einen Cycle eingeplant ist.
|
||||||
|
|
||||||
|
Reihenfolge ist Priorität, nicht Garantie. Items können sich verschieben
|
||||||
|
oder ganz wegfallen wenn sie sich beim Brainstorm als nicht passend zur
|
||||||
|
Privacy-First-Schnittmenge des Plugins erweisen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nächster Cycle (v1.2.0)
|
||||||
|
|
||||||
|
**Layout Refresh** — sichtbare Modernisierung des Chat-Windows selbst.
|
||||||
|
|
||||||
|
- Top-Tabs-Refresh mit Akzent-Pill-Underline statt Background-Fill
|
||||||
|
- Sidebar-Tabs (existing) bekommen Icons + vertikale Pill am Window-Rand
|
||||||
|
- Bottom-Status-Bar (Channel-Indikator, Privacy-Badge, Tab-Count,
|
||||||
|
Tells, Version)
|
||||||
|
- Card-Rows als Default-Message-Rendering, mit Compact-Density-Toggle
|
||||||
|
- Per-Tab Custom-Icons im Tabs-Settings-Dialog
|
||||||
|
- Removal des deprecated `HellionThemeEnabled`/`HellionThemeWindowOpacity`
|
||||||
|
Configuration-Felder
|
||||||
|
|
||||||
|
Spec liegt in [[Hellion Chat UI Modernisierung Spec]] (Vault).
|
||||||
|
|
||||||
|
## v1.1.0 — Theme Foundation (released 2026-05-05)
|
||||||
|
|
||||||
|
Theme-Engine mit fünf Built-In-Themes, Settings-Card-Grid, Custom-
|
||||||
|
Themes via JSON, Theme-Authoring-Doku. Plugin-Icon auf Hellion Forge.
|
||||||
|
Siehe `docs/CHANGELOG.md` für Details.
|
||||||
|
|
||||||
|
Aus dem ursprünglichen v1.1.0-Plan (Ad-Block / Spam-Filter, Receive-
|
||||||
|
Suppressed-Tells-Toggle) wurden zugunsten der Theme-Engine zurück
|
||||||
|
gestellt — beide Items leben weiter im Mittelfrist-Block.
|
||||||
|
|
||||||
|
## Mittelfristig (v1.2.x – v1.3.0)
|
||||||
|
|
||||||
|
- **Ad-Block / Spam-Filter** — Hybrid-Konzept aus eigenem Light-Filter und
|
||||||
|
optionaler `NoSoliciting`-IPC-Integration. Adressiert Werbe-Spam in
|
||||||
|
öffentlichen Channels und Tells. Aus dem v1.1.0-Plan zurückgestellt.
|
||||||
|
- **Receive-Suppressed-Tells-Toggle** — Auto-Tell-Tabs greift auch wenn ein
|
||||||
|
Drittplugin (z.B. XIVMessenger) die /tell-Anzeige global suppressed.
|
||||||
|
Gleicher Hook-Layer wie Ad-Block, deshalb gebündelt.
|
||||||
|
- **Database-Viewer Inline-Search** — Volltext-Suche im DB-Viewer via
|
||||||
|
SQLite FTS5. Aktuell gibt es nur Datums- und Channel-Filter.
|
||||||
|
- **TempTell Persistence** — Pin-Toggle auf TempTell-Tabs damit ausgewählte
|
||||||
|
Tells einen Relog überleben. Tester-Wunsch von Jingliu.
|
||||||
|
- **FontManager Async-Refactor** — `LoadGameSymFontAsync` aus dem
|
||||||
|
blockierenden Plugin-Constructor herausziehen. Cold-Start-Hitching beim
|
||||||
|
ersten Plugin-Start beheben (Severity niedrig, Plugin ist funktional).
|
||||||
|
- **Separate Opacity Active vs. Inactive** — zweiter Slider für inaktive
|
||||||
|
Fenster-Deckkraft. Upstream lehnt das ab; wir können hier anders
|
||||||
|
entscheiden.
|
||||||
|
- **Failed-Tell-Notification** — sichtbare Nachricht bei /tell-Fail
|
||||||
|
(offline, restricted instance, blacklisted, world-mismatch) statt
|
||||||
|
stillem Failure.
|
||||||
|
- **Per-Tab Sound-Notification** — Sound-Toggle und optional eigene .wav
|
||||||
|
pro Tab, mit Mute-In-Combat-Option.
|
||||||
|
|
||||||
|
## Langfrist (v1.x+)
|
||||||
|
|
||||||
|
### Storage-Backends (drei Stufen Bestätigung)
|
||||||
|
|
||||||
|
- MySQL/MariaDB-Backend für Multi-Device-Setups
|
||||||
|
- PostgreSQL-Backend
|
||||||
|
- AES-256-Verschlüsselung für sensible Channels mit lokalem Key
|
||||||
|
|
||||||
|
### Linux-spezifisch
|
||||||
|
|
||||||
|
- WireGuard-Network-Detection als optionaler Filter-Trigger
|
||||||
|
- libnotify-Integration für native Linux-Toasts
|
||||||
|
- XDG-Compliance (komplex unter Wine)
|
||||||
|
|
||||||
|
### UX und Tab-Management
|
||||||
|
|
||||||
|
- **Regex Tab Routing** — Plugin-Output-Spam in eigene Tabs, Tells
|
||||||
|
bestimmter Personen automatisch sortieren. Klar abgegrenzt zum Ad-Block:
|
||||||
|
Routing sortiert in Views, Block versteckt global.
|
||||||
|
- **Auto-Detect Duties** — Tab-Switch beim Duty-Start via Condition-Flag.
|
||||||
|
- **UX Bundle** — Vertical-Tab-Bar als Layout-Option, Shift+Mousewheel zum
|
||||||
|
Tab-Header-Scrollen ohne Aktivierung, globaler Hotkey zum Schließen des
|
||||||
|
aktiven Tabs.
|
||||||
|
- **Configure Tab Title** — konfigurierbares Tab-Title-Format
|
||||||
|
(Name / Name + abgekürzter World / voller Name / Custom), pro Tab
|
||||||
|
überschreibbar.
|
||||||
|
- **Name Display Options** — analog zu FFXIV-Vanilla (voller Name, Vorname
|
||||||
|
abgekürzt, Initialen), per-Channel-Override möglich.
|
||||||
|
- **Item & Flag Linking** — Outgoing: Shift-Klick auf Item/Flag sendet ins
|
||||||
|
fokussierte Plugin-Input. Incoming: Item-Links und Map-Coords klickbar.
|
||||||
|
- **Color Currently Selected Input Channel** — Channel-Selector-Button im
|
||||||
|
Input-Bar mit Channel-Farbe einfärben.
|
||||||
|
- **Plugin-Disclosure Pre-Send Filter** — konfigurierbare Wort-/Regex-Liste
|
||||||
|
blockiert das Senden mit Pre-Send-Confirm. Schutz vor versehentlicher
|
||||||
|
Plugin-Nennung in öffentlichen Channels.
|
||||||
|
- **Chat Clear on Name Change** — bei Charakter-Namensänderung lokalen
|
||||||
|
Verlauf migrieren oder löschen, Default Wipe für maximale Privacy.
|
||||||
|
- **Hide Plugin Window on NG+ Screen** — Hide-Logik um zusätzliche
|
||||||
|
Addon-Namen erweitern.
|
||||||
|
- **Kick from Novice Network** — Mentor-Nische, Context-Menü-Eintrag mit
|
||||||
|
Confirmation.
|
||||||
|
- **Text-to-Speech für /tell** — eingehende Tells via TTS, optional pro
|
||||||
|
Sender, mit Channel-Filter und Mute-In-Combat. Geringe Priorität.
|
||||||
|
|
||||||
|
### Distribution und Branding
|
||||||
|
|
||||||
|
- Hand-gezeichnetes Hellion-Logo (aktuell Platzhalter aus dem
|
||||||
|
Hellion-Online-Media-Brand-Repo)
|
||||||
|
- GitHub Action für automatischen `repo.json`-Sync nach Tag-Push
|
||||||
|
- Submission ans Dalamud-Main-Plugin-Repo (zusätzlich zum Custom-Repo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug-Verifizierungen
|
||||||
|
|
||||||
|
Aus dem Upstream-Issue-Tracker übernommen, in Hellion Chat 1.0.0 noch
|
||||||
|
nicht reproduziert oder verifiziert. Werden bei Gelegenheit gegen den
|
||||||
|
aktuellen Stand getestet.
|
||||||
|
|
||||||
|
- **Right-Click Whisper Error** in Field Ops / Special Instances (Eureka,
|
||||||
|
Bozja, Occult Crescent, DRS) — Upstream
|
||||||
|
[#168](https://github.com/Infiziert90/ChatTwo/issues/168). Reply-Helper
|
||||||
|
scheint `@World`-Suffix zu schlucken.
|
||||||
|
- **FPS Drops with Plugin active** — Upstream
|
||||||
|
[#145](https://github.com/Infiziert90/ChatTwo/issues/145). 10–20 % Drop
|
||||||
|
seit upstream v1.29.19.0. v1.0.0 hat mehrere Fixes auf den verdächtigen
|
||||||
|
Pfaden, Repro-Test gegen aktuellen Stand offen.
|
||||||
|
- **Add Blacklist from Plugin Window** — Upstream
|
||||||
|
[#140](https://github.com/Infiziert90/ChatTwo/issues/140). Right-Click
|
||||||
|
Add-to-Blacklist wirft "Cannot locate character with that name", via
|
||||||
|
Vanilla-Chat funktioniert es.
|
||||||
|
- **DB-Viewer Column Sort** — sortiert State-Column lexikografisch statt
|
||||||
|
numerisch (10 vor 2). XIVIM
|
||||||
|
[#82](https://github.com/NightmareXIV/XIVInstantMessenger/issues/82),
|
||||||
|
Repro in Hellion Chat offen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lizenz-Boundary
|
||||||
|
|
||||||
|
Hellion Chat ist EUPL-1.2-lizenziert. Konzept-Imports aus AGPL-3.0-Plugins
|
||||||
|
(z.B. XIV Instant Messenger) sind ausschließlich architektonische
|
||||||
|
Inspiration, kein Code-Port. Imports aus dem GPL-3.0-kompatiblen
|
||||||
|
Upstream-Bestand laufen weiter über
|
||||||
|
[`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md).
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# Theme Authoring Guide
|
||||||
|
|
||||||
|
> Built by **Hellion Forge** — the plugin workshop arm of [Hellion Online Media](https://hellion-media.de). HellionChat ships with five built-in themes; this guide walks you through writing your own.
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
1. Open Settings → Themes → **Open themes folder**
|
||||||
|
2. Copy `example-theme.json` to `<your-name>.json` in the same folder
|
||||||
|
3. Edit the file with any text editor
|
||||||
|
4. Reload the plugin (toggle off/on in `/xlplugins`)
|
||||||
|
5. Your theme appears in the Custom-Themes section in Settings → Themes
|
||||||
|
|
||||||
|
That's the whole loop. The rest of this document is reference.
|
||||||
|
|
||||||
|
## File location
|
||||||
|
|
||||||
|
```
|
||||||
|
%APPDATA%\XIVLauncher\pluginConfigs\HellionChat\themes\
|
||||||
|
```
|
||||||
|
|
||||||
|
(or the equivalent path on Linux/macOS — Settings → Themes → "Open themes folder" opens it directly).
|
||||||
|
|
||||||
|
Each `*.json` file in this folder is loaded as one theme. The `example-theme.json` that HellionChat seeds on first launch is your starting template.
|
||||||
|
|
||||||
|
## File format
|
||||||
|
|
||||||
|
Theme JSON has four blocks:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"slug": "your-slug",
|
||||||
|
"name": "Your Theme Name",
|
||||||
|
"author": "You",
|
||||||
|
"description": "One-line description shown under the theme name.",
|
||||||
|
"colors": { ... 21 color slots ... },
|
||||||
|
"layout": { ... 9 layout values ... },
|
||||||
|
"chatChannels": { ... optional, channel-name → hex ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Top-level fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `schemaVersion` | int | yes | Always `1` for HellionChat 1.1.0. The plugin warns and skips themes with a different number. |
|
||||||
|
| `slug` | string | yes | Lowercase, hyphenated. Must be unique across all themes (built-in slugs are reserved). |
|
||||||
|
| `name` | string | yes | Display name in the picker. |
|
||||||
|
| `author` | string | yes | Shown small under the theme name. |
|
||||||
|
| `description` | string | yes | One short sentence. |
|
||||||
|
| `colors` | object | yes | All 21 slots required (see below). |
|
||||||
|
| `layout` | object | yes | All 9 slots required (see below). |
|
||||||
|
| `chatChannels` | object | no | Optional channel-name → hex map (see below). |
|
||||||
|
|
||||||
|
### Color slots
|
||||||
|
|
||||||
|
All values are 6-digit `#RRGGBB` or 8-digit `#RRGGBBAA` hex strings. Six-digit values get an implicit `FF` alpha.
|
||||||
|
|
||||||
|
| Slot | Role |
|
||||||
|
|---|---|
|
||||||
|
| `primary` | Brand color — used on buttons, sliders, check marks, highlighted separators. |
|
||||||
|
| `primaryDark` | Pressed-button stage. |
|
||||||
|
| `primaryLight` | Hovered-button / link-text stage. |
|
||||||
|
| `primaryGlow` | Glow / subtle accent (typically primary with ~60% alpha). |
|
||||||
|
| `accent` | Counter-accent — scrollbar grab on hover/active, resize grip, optional CTA. |
|
||||||
|
| `accentDark` / `accentLight` | Dark/light siblings of accent. |
|
||||||
|
| `identity` | Title-bar active color and active-tab color. Often equals `primaryDark`. |
|
||||||
|
| `windowBg` | Outermost window background. |
|
||||||
|
| `childBg` | Inner panel / popup background. |
|
||||||
|
| `frameBg` | Input fields, sliders, combos. |
|
||||||
|
| `surface` | Card surfaces, headers, selectables. |
|
||||||
|
| `surfaceHover` | Hovered card / header step. |
|
||||||
|
| `border` | Panel borders. Typically primary with ~40% alpha for a brand-tinted edge. |
|
||||||
|
| `textPrimary` | Body text. Soft off-white reads better than pure `#FFFFFF` on dark backgrounds. |
|
||||||
|
| `textMuted` | Captions, secondary lines. |
|
||||||
|
| `textDim` | Disabled / hint text, separators. |
|
||||||
|
| `statusSuccess` | Green-ish for success notifications. |
|
||||||
|
| `statusDanger` | Red for errors. |
|
||||||
|
| `statusWarning` | Amber for warnings. |
|
||||||
|
| `statusInfo` | Cyan-ish info. Often equals primary. |
|
||||||
|
|
||||||
|
### Layout slots
|
||||||
|
|
||||||
|
All values are floats in pixels. `BorderSize` is 0 or 1 (no thicker borders look right with ImGui's edge anti-aliasing).
|
||||||
|
|
||||||
|
| Slot | Typical range | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `windowRounding` | 0–8 | 0 = sharp upstream look; 4–6 = softer "app" feel. |
|
||||||
|
| `childRounding` | 0–6 | Usually 1 less than `windowRounding`. |
|
||||||
|
| `popupRounding` | 0–6 | Same as `childRounding`. |
|
||||||
|
| `frameRounding` | 0–4 | For inputs, sliders. |
|
||||||
|
| `grabRounding` | 0–4 | Slider grab dot. |
|
||||||
|
| `tabRounding` | 0–4 | Tab corners. |
|
||||||
|
| `scrollbarRounding` | 0–4 | Scrollbar grab. |
|
||||||
|
| `windowBorderSize` | 0 or 1 | 1 reads better in dark themes. |
|
||||||
|
| `frameBorderSize` | 0 or 1 | Usually matches windowBorderSize. |
|
||||||
|
|
||||||
|
### Optional `chatChannels`
|
||||||
|
|
||||||
|
If present, your theme proposes its own chat-channel colors. Property names are `ChatType` enum values (case-insensitive). Unknown names are skipped silently — safe for forward-compat.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"chatChannels": {
|
||||||
|
"Say": "#FFFFFF",
|
||||||
|
"Yell": "#FFE066",
|
||||||
|
"Shout": "#FFA040",
|
||||||
|
"TellIncoming": "#FF99CC",
|
||||||
|
"TellOutgoing": "#FF99CC",
|
||||||
|
"Party": "#80C0E8",
|
||||||
|
"FreeCompany": "#4DD9E8",
|
||||||
|
"NoviceNetwork": "#A8E060",
|
||||||
|
"Linkshell1": "#A8E060"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user is asked **once per theme switch** whether to apply these colors — never auto-overwriting existing picks. The banner shows up only if your suggested colors differ from the user's current `Configuration.ChatColours`.
|
||||||
|
|
||||||
|
#### Channel-identity rule
|
||||||
|
|
||||||
|
**Don't break FFXIV channel identity.** Players have used these conventions for over a decade:
|
||||||
|
|
||||||
|
| Channel | Convention | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Say | white / off-white | Default-readable speech. |
|
||||||
|
| Yell | yellow | Urgent broadcast. |
|
||||||
|
| Shout | orange | Local urgent. |
|
||||||
|
| Tell | pink-magenta | Whisper, must stand out. |
|
||||||
|
| Party | light blue | Group ops. |
|
||||||
|
| FreeCompany | cyan-teal | Guild ops. |
|
||||||
|
| NoviceNetwork | lime-green | Mentor channel. |
|
||||||
|
|
||||||
|
A theme can tint these toward its brand family (e.g., a purple theme can shift Tell from `#FF99CC` to `#E090FF`), but **don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual hierarchy.
|
||||||
|
|
||||||
|
The four colored built-in themes (Hellion Arctic, Event Horizon, Moonlit Bloom, Mint Grove) all follow this rule — read their JSON for reference. Chat 2 Klassik intentionally ships without `chatChannels` so the user keeps their existing picks.
|
||||||
|
|
||||||
|
## Theme families
|
||||||
|
|
||||||
|
Naming convention `<color>-<modifier>` is recommended for theme families. The first member of a family is the lightest/brightest:
|
||||||
|
|
||||||
|
- `mint-grove` (current built-in, light mint)
|
||||||
|
- `forest-grove` (planned, dark emerald)
|
||||||
|
- `moss-grove` (planned, mid muted)
|
||||||
|
|
||||||
|
Code-wise families have no special handling — only the slug naming hints at the relationship. The picker may group families later, but that's not required.
|
||||||
|
|
||||||
|
## Validation and errors
|
||||||
|
|
||||||
|
When HellionChat loads your theme:
|
||||||
|
|
||||||
|
- **Schema mismatch** (`schemaVersion != 1`): theme is skipped, warning written to `/xllog`.
|
||||||
|
- **Missing required field** (e.g., no `slug`): theme is skipped, warning written.
|
||||||
|
- **Invalid hex** (e.g., `#GGHHII`): theme is skipped, warning written.
|
||||||
|
- **Unknown channel name** in `chatChannels`: that one channel is skipped silently, the rest of the theme loads normally.
|
||||||
|
|
||||||
|
Check `/xllog` after a plugin reload to see what loaded and what didn't.
|
||||||
|
|
||||||
|
## Testing your theme
|
||||||
|
|
||||||
|
1. Edit the JSON, save the file.
|
||||||
|
2. Reload the plugin: `/xlplugins` → toggle HellionChat off, then on.
|
||||||
|
3. Settings → Themes → click your theme card.
|
||||||
|
4. Watch every plugin window (chat, settings, pop-out) and pick something to fix.
|
||||||
|
5. Tweak. Reload. Repeat.
|
||||||
|
|
||||||
|
Tip: the **Settings → Themes** picker shows a mini-mockup per theme — your colors are visible before you switch.
|
||||||
|
|
||||||
|
## Sharing themes
|
||||||
|
|
||||||
|
Themes are JSON, so sharing is just a file. Drop it into someone's `pluginConfigs/HellionChat/themes/` folder and their plugin picks it up on next reload.
|
||||||
|
|
||||||
|
A community theme repository is on the Hellion Forge roadmap. Until then: share via Discord or any pastebin.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- `docs/example-theme.json` (seeded automatically on first launch into `pluginConfigs/HellionChat/themes/`) — minimal valid theme.
|
||||||
|
- The five built-in themes live in source under `HellionChat/Themes/Builtin/`. They are a good reference for Color choices that work.
|
||||||
|
- [Hellion Online Media branding](https://hellion-media.de) — the Arctic Cyan + Ember Glow palette that drives the default Hellion Arctic theme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center"><sub>HellionChat is a privacy-focused fork of <a href="https://github.com/Infiziert90/ChatTwo">Chat 2</a>, distributed under the EUPL-1.2.<br/>Theme engine and authoring guide are part of <strong>Hellion Forge</strong>.</sub></p>
|
||||||
@@ -4,21 +4,22 @@ HellionChat ships and depends on a number of third-party components.
|
|||||||
This document lists them, their licences and which of them touch the
|
This document lists them, their licences and which of them touch the
|
||||||
network. It is the inventory referenced by `PRIVACY.md`.
|
network. It is the inventory referenced by `PRIVACY.md`.
|
||||||
|
|
||||||
Last reviewed: 2026-05-03 (HellionChat v0.5.4).
|
Last reviewed: 2026-05-05 (HellionChat v1.1.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Direct NuGet dependencies
|
## Direct NuGet dependencies
|
||||||
|
|
||||||
Pinned in `ChatTwo/ChatTwo.csproj`. Versions reflect the v0.5.4 build.
|
Pinned in `HellionChat/HellionChat.csproj`. Versions reflect the v1.1.0 build.
|
||||||
|
|
||||||
| Package | Version | Licence | Network | Purpose |
|
| Package | Version | Licence | Network | Purpose |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| [MessagePack](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | 3.1.4 | MIT | no | Binary serialisation for the SQLite message store. |
|
| [MessagePack](https://github.com/MessagePack-CSharp/MessagePack-CSharp) | 3.1.4 | MIT | no | Binary serialisation for the SQLite message store. |
|
||||||
| [Microsoft.Data.Sqlite](https://learn.microsoft.com/dotnet/standard/data/sqlite/) | 10.0.7 | MIT | no | Local SQLite access for the message database. |
|
| [Microsoft.Data.Sqlite](https://learn.microsoft.com/dotnet/standard/data/sqlite/) | 10.0.7 | MIT | no | Local SQLite access for the message database. |
|
||||||
| [morelinq](https://github.com/morelinq/MoreLINQ) | 4.4.0 | Apache-2.0 | no | LINQ helper extensions. |
|
| [morelinq](https://github.com/morelinq/MoreLINQ) | 4.4.0 | Apache-2.0 | no | LINQ helper extensions. |
|
||||||
| [Pidgin](https://github.com/benjamin-hodgson/Pidgin) | 3.3.0 | MIT | no | Parser combinator library used for chat-input parsing. |
|
| [Pidgin](https://github.com/benjamin-hodgson/Pidgin) | 3.5.1 | MIT | no | Parser combinator library used for chat-input parsing. CIString Unicode fix relevant for non-ASCII channel/tab names. |
|
||||||
| [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) | 3.1.12 | [Six Labors Split License 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) (OSI-approved; free for open-source / non-commercial use, commercial licence required for closed-source commercial use) | no | Image decoding for cached emotes. |
|
| [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) | 3.1.12 | [Six Labors Split License 1.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) (OSI-approved; free for open-source / non-commercial use, commercial licence required for closed-source commercial use) | no | Image decoding for cached emotes. |
|
||||||
|
| [SQLitePCLRaw.lib.e_sqlite3](https://github.com/ericsink/SQLitePCL.raw) | 3.50.3 | MIT | no | Native SQLite binary, explicitly pinned to override the transitive default for CVE-2025-6965 (memory corruption from aggregate-term overflow) and CVE-2025-7709. |
|
||||||
|
|
||||||
Six Labors note: HellionChat is an EUPL-1.2-licensed open-source
|
Six Labors note: HellionChat is an EUPL-1.2-licensed open-source
|
||||||
project distributed at no cost. Use of ImageSharp 3.x under the
|
project distributed at no cost. Use of ImageSharp 3.x under the
|
||||||
@@ -50,7 +51,7 @@ HellionChat is a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo)
|
|||||||
by Infiziert90 (Infi) and Anna Clemens, also licensed under EUPL-1.2.
|
by Infiziert90 (Infi) and Anna Clemens, also licensed under EUPL-1.2.
|
||||||
The bulk of the code, including the message store architecture, the
|
The bulk of the code, including the message store architecture, the
|
||||||
channel logic, the hook system and the ImGui chat window, originates
|
channel logic, the hook system and the ImGui chat window, originates
|
||||||
from upstream. See `NOTICE.md` and `UPSTREAM_SYNC.md` for the
|
from upstream. See `../NOTICE.md` and `UPSTREAM_SYNC.md` for the
|
||||||
attribution and the cherry-pick policy.
|
attribution and the cherry-pick policy.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -62,9 +63,11 @@ components opens network connections on their own. All outbound
|
|||||||
traffic is initiated explicitly by HellionChat's own source files
|
traffic is initiated explicitly by HellionChat's own source files
|
||||||
and is documented in `PRIVACY.md` under "Outbound network calls":
|
and is documented in `PRIVACY.md` under "Outbound network calls":
|
||||||
|
|
||||||
- `ChatTwo/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting)
|
- `HellionChat/EmoteCache.cs` → BetterTTV API + CDN (opt-out via setting)
|
||||||
- `ChatTwo/FontManager.cs` → Square Enix Lodestone font CDN (one-time
|
|
||||||
download)
|
The earlier Square Enix Lodestone font download (`FontManager.cs`)
|
||||||
|
was removed in v1.0.4 — it was a leftover from upstream's removed
|
||||||
|
webinterface feature and was no longer consumed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ and is documented in `PRIVACY.md` under "Outbound network calls":
|
|||||||
To regenerate the dependency inventory after a version bump:
|
To regenerate the dependency inventory after a version bump:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet list ChatTwo.sln package --include-transitive
|
dotnet list HellionChat.sln package --include-transitive
|
||||||
```
|
```
|
||||||
|
|
||||||
The "direct NuGet dependencies" table above only lists direct
|
The "direct NuGet dependencies" table above only lists direct
|
||||||
@@ -85,7 +88,7 @@ To re-audit the network-call inventory:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep -rn -E "HttpClient|HttpRequest|new Uri\(|https?://" \
|
grep -rn -E "HttpClient|HttpRequest|new Uri\(|https?://" \
|
||||||
--include="*.cs" ChatTwo/
|
--include="*.cs" HellionChat/
|
||||||
```
|
```
|
||||||
|
|
||||||
Any new hit that is not a click-through (`Util.OpenLink`) or a
|
Any new hit that is not a click-through (`Util.OpenLink`) or a
|
||||||
|
After Width: | Height: | Size: 37 KiB |