Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd01fa63a1 | |||
| b81c50b433 | |||
| 355a57089b | |||
| cf7ab6226c | |||
| 03da6d58a4 | |||
| 90a4544ab2 | |||
| 9b4557f197 | |||
| e594258cf3 | |||
| bb863c5b32 | |||
| 0797d1a517 | |||
| 8dc8b87580 | |||
| baeec369e6 | |||
| a1f2b22b19 | |||
| 5931f2f301 | |||
| 0b25df0ea7 | |||
| b75c7b177a | |||
| ccc5a4e17a | |||
| daa800c8b1 | |||
| a531973c0d | |||
| 4c8b0da3da | |||
| 9a8a014795 | |||
| 9640d336a6 | |||
| 12ce015d83 | |||
| f455bf4736 | |||
| 9bc66c7cf3 | |||
| e9022de150 | |||
| cb327b8073 | |||
| 1c354d18bb | |||
| 0ed88691c2 | |||
| c64fcfd4d1 | |||
| 6689cdb968 | |||
| 345aa3ea2a | |||
| 1ffc41f97d | |||
| 36b92f0520 |
@@ -0,0 +1,21 @@
|
||||
name: Security
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
uses: JonKazama-Hellion/security-workflows/.gitea/workflows/security-scan.yml@main
|
||||
with:
|
||||
# MessageStore.cs uses string-interpolation in CommandText for table
|
||||
# names and clause-joins that come from internal code constants, not
|
||||
# user input. Values are bound via SqlParameter, the SQL surface is
|
||||
# local-only inside a Dalamud plugin. Semgrep matches the pattern
|
||||
# without dataflow, so it flags those eight call sites; CodeQL
|
||||
# would not. Suppressed for this repo only.
|
||||
semgrep-exclude-rules: 'csharp.lang.security.sqli.csharp-sqli.csharp-sqli'
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
# .githooks/pre-push — invokes preflight.sh (A/B/C/D=build).
|
||||
exec scripts/preflight.sh
|
||||
@@ -8,7 +8,7 @@ body:
|
||||
value: |
|
||||
Thanks for reporting. Please fill in the fields below so I can
|
||||
reproduce the issue. If this is a security issue, stop here and
|
||||
use the [private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
|
||||
report it privately to [kontakt@hellion-media.de](mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D)
|
||||
instead.
|
||||
|
||||
- type: input
|
||||
|
||||
@@ -2,8 +2,8 @@ blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: Security vulnerability
|
||||
url: https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
||||
about: Do not open a public issue for security problems. Use the private advisory instead.
|
||||
url: mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||
about: Do not open a public issue for security problems. Report by e-mail instead.
|
||||
|
||||
- name: Upstream Chat 2 issue
|
||||
url: https://github.com/Infiziert90/ChatTwo/issues
|
||||
|
||||
@@ -3,9 +3,9 @@ Thanks for contributing to HellionChat. Please fill in the sections
|
||||
below so the review goes quickly. Delete sections that genuinely do
|
||||
not apply, but do not delete the whole template.
|
||||
|
||||
If this is a security fix, stop here and use a private security
|
||||
advisory instead:
|
||||
https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
||||
If this is a security fix, stop here and report it privately by
|
||||
e-mail instead of opening a public PR:
|
||||
mailto:kontakt@hellion-media.de?subject=%5BHellionChat%20Security%5D
|
||||
-->
|
||||
|
||||
## Summary
|
||||
@@ -23,7 +23,6 @@ https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
|
||||
- [ ] Documentation only
|
||||
- [ ] Translation update
|
||||
- [ ] Build, CI or tooling change
|
||||
- [ ] Upstream cherry-pick from Chat 2
|
||||
|
||||
## Linked issue
|
||||
|
||||
@@ -53,7 +52,6 @@ new commands, new translations, removed behaviour. If none, write
|
||||
bump and is it covered by the existing migration tests?
|
||||
- Does this change the schema in MessageStore?
|
||||
- Does this change the repo.json or HellionChat.yaml manifest fields?
|
||||
- Does this affect the upstream cherry-pick path? See docs/UPSTREAM_SYNC.md.
|
||||
-->
|
||||
|
||||
## Checklist
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
subtitle: ChatLog Frame-Hot-Path
|
||||
versionsnatur: Performance-Patch
|
||||
---
|
||||
|
||||
**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**
|
||||
|
||||
Dritter Sub-Patch der v1.4.x Polish-Sweep-Serie. Drei
|
||||
Per-Frame-Allokations-Quellen aus dem ChatLogWindow-Render-
|
||||
Pfad und der Settings-StatusBar eliminiert.
|
||||
|
||||
- **Card-Mode-Border-Loop entlastet.** DrawMessages hebt
|
||||
Theme, DrawList, Window-Left, Window-Right und die ABGR-
|
||||
Border-Color einmalig vor den Per-Message-Loop. Bei 100
|
||||
sichtbaren Messages sind das gut 500 redundante P/Invokes
|
||||
und Property-Reads, die der Hoist eliminiert. Pop-Out-
|
||||
Heavy-Setups (mehrere parallele Chat-Windows) profitieren
|
||||
proportional, weil der Hoist pro DrawMessages-Call greift,
|
||||
also pro Window
|
||||
- **Auto-Tell Tab-Tint und Icon gecached.** Die Hash-Color-
|
||||
Berechnung für Auto-Tell-Tabs lief pro Tab pro Frame, mit
|
||||
zwei String-Allokationen pro Tab (eine für Tint-Hash, eine
|
||||
für Icon-Hash). Der neue TabTintCache liest pre-computed
|
||||
Werte aus dem Tab und rechnet nur neu wenn das Tell-Target
|
||||
drifted. Beide Caches haben separate Validation-Keys, also
|
||||
keine Cross-Invalidation zwischen Tint- und Icon-Pfad.
|
||||
AutoTellTabTint selbst bleibt pure Hash-Helper, weiterhin
|
||||
ohne Tab-Awareness
|
||||
- **StatusBar-Aggregation hinter Cache-Gate.** Die Status-
|
||||
Leiste am unteren Window-Rand summiert die Tab-Message-
|
||||
Counts und zählt die Auto-Tell-Tabs pro Frame. Der Cache-
|
||||
Gate (1 Sekunde) lag bisher hinter den LINQ-Pfaden, also
|
||||
liefen Sum und Count trotzdem pro Frame. Jetzt vor dem
|
||||
Gate, plus die LINQ-Pfade durch eine Single-Pass-Foreach
|
||||
ersetzt. Die Aggregation läuft auf etwa 1 % der Frames
|
||||
|
||||
Realistische Frame-Time-Recovery: 2-5 % in typischen Szenen,
|
||||
Pop-Out-Heavy-Setups potenziell mehr durch die Card-Border-
|
||||
Multiplikation pro Window.
|
||||
|
||||
Keine Schema-Bumps, keine User-sichtbaren Funktions-
|
||||
Änderungen außer dass die Frames im Chat-Log und in der
|
||||
Settings-Statusleiste merklich glatter laufen.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
subtitle: Async-Lifecycle + Gitea-Cutover
|
||||
versionsnatur: Architecture-Refactor
|
||||
---
|
||||
|
||||
**Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover**
|
||||
|
||||
Vierter Sub-Patch der v1.4.x Polish-Sweep-Serie. Plugin-
|
||||
Lifecycle auf Dalamud's `IAsyncDalamudPlugin`-API migriert
|
||||
und das Custom-Repo zieht von GitHub auf Gitea um.
|
||||
|
||||
- **Async-Plugin-Architektur.** Konstruktor übernimmt nur
|
||||
noch die Bootstrap-Essentials (Config-Load, Language-Init,
|
||||
Conflict-Detection). Migrationen, Service-Allokationen,
|
||||
Window-Konstruktion und Hook-Subscription wandern in
|
||||
LoadAsync, sodass Dalamud die UI während der schweren
|
||||
Arbeit responsive halten kann. Per-Line-CaptureFailure in
|
||||
DisposeAsync mirrort LightlessSync's Pattern, plus
|
||||
Idempotency-Guard gegen Reload-Races
|
||||
- **Custom-Repo-URL umgezogen auf Gitea.** Bestehende Tester
|
||||
müssen einmalig in XIVLauncher die Custom-Repo-URL auf
|
||||
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||
umstellen, dann XIVLauncher neu starten. Das alte
|
||||
GitHub-Repo bleibt als eingefrorener v1.4.2-Snapshot
|
||||
stehen und wird nicht mehr aktualisiert
|
||||
- **Schema-Gate statt Migrations-Kette.** Die v9 → v16
|
||||
Migrationen sind raus, ersetzt durch einen harten
|
||||
Schema-Check in Phase 1. Configs auf Schema v16+ laden
|
||||
direkt; ältere Configs (vor v1.2.1) bekommen jetzt eine
|
||||
klare „install v1.4.2 first"-Fehlermeldung statt eines
|
||||
impliziten Migrations-Pfads
|
||||
- **AutoTranslate-Cache läuft im Hintergrund.** Der Cache
|
||||
füllt sich jetzt fire-and-forget statt blockierend im
|
||||
Plugin-Load. Trade-off: die erste Auto-Translate-Nutzung
|
||||
einer Session kann einen kurzen Hitch haben, dafür kein
|
||||
300-ms-Block beim Plugin-Start
|
||||
- **Plugin-Load-Zeit ehrlich.** Median 3,7 s über fünf
|
||||
Reloads, vergleichbar mit v1.4.2. Der Async-Refactor ist
|
||||
Foundation für künftige Lazy-Init-Optimierungen (v1.4.4)
|
||||
und Code-Architektur-Hygiene, kein direkter
|
||||
User-spürbarer Speed-Win in dieser Release
|
||||
|
||||
Keine User-sichtbaren Funktions-Änderungen außer dem
|
||||
Repo-URL-Update. Settings, Themes und Tabs bleiben
|
||||
unangetastet.
|
||||
@@ -8,19 +8,19 @@ Dalamud main plugin repo. To install:
|
||||
|
||||
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
|
||||
2. Add the URL:
|
||||
`https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json`
|
||||
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json`
|
||||
3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install
|
||||
|
||||
## Project documents
|
||||
|
||||
- [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
|
||||
- [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
|
||||
- [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths
|
||||
- [README](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/README.md) — features, architecture, build
|
||||
- [Privacy notice](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/PRIVACY.md) — what the plugin stores and sends
|
||||
- [Third-party notices](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/docs/THIRD_PARTY_NOTICES.md) — dependencies and licences
|
||||
- [Security policy](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SECURITY.md) — vulnerability reporting
|
||||
- [Support](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/SUPPORT.md) — bug reports, questions, contact paths
|
||||
|
||||
## Licence
|
||||
|
||||
[EUPL-1.2](https://github.com/JonKazama-Hellion/HellionChat/blob/main/LICENSE).
|
||||
[EUPL-1.2](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/src/branch/main/LICENSE).
|
||||
Based on [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna,
|
||||
also EUPL-1.2.
|
||||
|
||||
@@ -3,6 +3,12 @@ name: Build
|
||||
# Verifies that every push to main and every PR still builds against the
|
||||
# current Dalamud staging branch. Does not produce release artefacts; the
|
||||
# release workflow handles that on tag.
|
||||
#
|
||||
# Linux runner: gitea.com Cloud Actions provides ubuntu-latest. The plugin
|
||||
# csproj targets net10.0-windows, but `dotnet build` cross-compiles on
|
||||
# Linux as long as the Dalamud staging assemblies are present at the
|
||||
# expected lookup path ($(HOME)/.xlcore/dalamud/Hooks/dev/, which the
|
||||
# Dalamud SDK 15 uses on Linux).
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -21,7 +27,7 @@ permissions:
|
||||
jobs:
|
||||
build:
|
||||
name: Build (Release)
|
||||
runs-on: windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
@@ -34,23 +40,14 @@ jobs:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Download Dalamud staging
|
||||
shell: pwsh
|
||||
run: |
|
||||
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
||||
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||
mkdir -p "$hooks"
|
||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||
unzip -oq dalamud.zip -d "$hooks"
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore HellionChat/HellionChat.csproj
|
||||
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||
|
||||
- name: Upload build output
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HellionChat-build-${{ github.run_number }}
|
||||
path: HellionChat/bin/Release/**/HellionChat/**
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
name: CodeQL
|
||||
|
||||
# Replaces the GitHub default-setup CodeQL scan. The default setup runs
|
||||
# without resolving the Dalamud assemblies (they live in a user-AppData
|
||||
# path) and reports "Low C# analysis quality" because call-target
|
||||
# resolution sits at ~64%. This workflow downloads the Dalamud staging
|
||||
# distribution before the build, runs a manual dotnet build, and then
|
||||
# lets CodeQL analyse the fully-resolved compilation. Quality climbs
|
||||
# back above the 85% thresholds.
|
||||
#
|
||||
# This workflow only consumes trusted inputs: the tag/branch ref via
|
||||
# the standard checkout action, and the Dalamud distribution URL which
|
||||
# is pinned to a goatcorp-controlled GitHub Pages target. No user-
|
||||
# controlled event payload (issue title, PR body, commit message) flows
|
||||
# into a run-step.
|
||||
#
|
||||
# Disable the default setup in the repo before this workflow lands:
|
||||
# Settings -> Code security -> Code scanning -> "CodeQL analysis" tile
|
||||
# -> Switch to advanced.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '17 6 * * 1'
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze-csharp:
|
||||
name: Analyze (csharp)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Download Dalamud staging
|
||||
shell: pwsh
|
||||
run: |
|
||||
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
||||
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
languages: csharp
|
||||
build-mode: manual
|
||||
queries: security-extended
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore HellionChat/HellionChat.csproj
|
||||
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
|
||||
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
category: /language:csharp
|
||||
|
||||
analyze-actions:
|
||||
name: Analyze (actions)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
languages: actions
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
category: /language:actions
|
||||
@@ -34,10 +34,9 @@ jobs:
|
||||
announce:
|
||||
name: Post changelog to Hellion Forge
|
||||
runs-on: ubuntu-latest
|
||||
# The DISCORD_FORGE_WEBHOOK secret lives under Settings → Environments
|
||||
# → Webhook (case-sensitive). Without this declaration the secret is
|
||||
# not in scope for the job.
|
||||
environment: Webhook
|
||||
# The DISCORD_FORGE_WEBHOOK secret is set as a repo-level Actions Secret
|
||||
# on Gitea (Settings → Actions → Secrets). Repo-level secrets are in
|
||||
# scope for every job by default, no environment: declaration needed.
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
@@ -134,7 +133,7 @@ jobs:
|
||||
# ---------- Embed-Payload bauen ----------
|
||||
$payload = [ordered]@{
|
||||
username = "Forge Herald"
|
||||
avatar_url = "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png"
|
||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||
content = "<@&1500489631555260446>"
|
||||
allowed_mentions = [ordered]@{
|
||||
parse = @()
|
||||
@@ -143,7 +142,7 @@ jobs:
|
||||
embeds = @(
|
||||
[ordered]@{
|
||||
title = $title
|
||||
url = "https://github.com/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
color = 12730636
|
||||
description = $description
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
|
||||
@@ -2,15 +2,19 @@ name: Release
|
||||
|
||||
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
|
||||
# current Dalamud staging branch, locates the latest.zip produced by
|
||||
# DalamudPackager and attaches it to the matching GitHub Release.
|
||||
# DalamudPackager and attaches it to the matching Gitea Release.
|
||||
#
|
||||
# User-controlled inputs touched by this workflow:
|
||||
# - the tag name (filtered by on.tags = v*, validated again at runtime
|
||||
# against ^v\d+\.\d+\.\d+$ before being used in any string)
|
||||
# All other values are either repo-controlled (paths under
|
||||
# HellionChat/bin/Release derived from Get-ChildItem) or pinned URLs to
|
||||
# goatcorp / GitHub. Nothing from a webhook event payload (issue/PR
|
||||
# HellionChat/bin/Release derived from find / Get-ChildItem) or pinned
|
||||
# URLs to goatcorp / gitea. Nothing from a webhook event payload (issue/PR
|
||||
# titles, commit messages, etc.) flows into a run-step.
|
||||
#
|
||||
# Linux runner: gitea.com Cloud Actions only ships ubuntu-latest. The
|
||||
# plugin csproj targets net10.0-windows, `dotnet build` cross-compiles on
|
||||
# Linux when the Dalamud staging assemblies sit under $(HOME)/.xlcore/...
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -33,7 +37,7 @@ permissions:
|
||||
jobs:
|
||||
release:
|
||||
name: Build and attach release ZIP
|
||||
runs-on: windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
@@ -52,27 +56,25 @@ jobs:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Download Dalamud staging
|
||||
shell: pwsh
|
||||
run: |
|
||||
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
|
||||
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
|
||||
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
|
||||
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
|
||||
mkdir -p "$hooks"
|
||||
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
|
||||
unzip -oq dalamud.zip -d "$hooks"
|
||||
|
||||
- name: Build (Release)
|
||||
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
|
||||
|
||||
- name: Locate latest.zip
|
||||
id: locate
|
||||
shell: pwsh
|
||||
run: |
|
||||
$zip = Get-ChildItem -Path HellionChat\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1
|
||||
if (-not $zip)
|
||||
{
|
||||
throw "latest.zip not found under HellionChat\bin\Release"
|
||||
}
|
||||
Write-Host "Found: $($zip.FullName)"
|
||||
"path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
zip="$(find HellionChat/bin/Release -name latest.zip -print -quit)"
|
||||
if [ -z "$zip" ]; then
|
||||
echo "latest.zip not found under HellionChat/bin/Release" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Found: $zip"
|
||||
echo "path=$zip" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Build a release body from the matching changelog block in
|
||||
# HellionChat.yaml plus a static install / docs footer. Fails the
|
||||
@@ -150,8 +152,13 @@ jobs:
|
||||
Write-Host $body
|
||||
Write-Host "----------------------------------------"
|
||||
|
||||
- name: Attach to GitHub release
|
||||
uses: softprops/action-gh-release@v3
|
||||
# Gitea-native release action. Creates the release if the tag has no
|
||||
# release yet, or updates the existing one. body_path provides the
|
||||
# generated release body, files attaches latest.zip. The auto-injected
|
||||
# GITHUB_TOKEN on Gitea Actions has Gitea-API scope and is sufficient
|
||||
# for release write.
|
||||
- name: Attach to Gitea release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
# Explicit tag_name so the action targets the correct release in
|
||||
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
||||
@@ -160,5 +167,4 @@ jobs:
|
||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
body_path: release-body.md
|
||||
fail_on_unmatched_files: true
|
||||
generate_release_notes: false
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
.envrc
|
||||
!.env.example
|
||||
.vscode/
|
||||
scripts/
|
||||
scripts/setup-dev-env.sh
|
||||
|
||||
# Local test project (stays out of the published plugin repo;
|
||||
# pure-function safety net for refactor cycles)
|
||||
|
||||
@@ -15,9 +15,11 @@ to make a contribution land smoothly.
|
||||
- Read the [README](README.md) so you understand the scope: a
|
||||
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
|
||||
removes the upstream webinterface and ships privacy-first defaults.
|
||||
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Cherry-picks
|
||||
from upstream Chat 2 are selective and deliberate; not everything
|
||||
that lands there belongs here.
|
||||
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Active
|
||||
cherry-picking from upstream Chat 2 has ended in the v1.4.x cycle;
|
||||
HellionChat continues as an independent codebase. Existing
|
||||
upstream-derived code keeps its attribution. New contributions
|
||||
stand on their own and do not need to be cherry-pick-compatible.
|
||||
- Read [`SECURITY.md`](SECURITY.md). Anything security-sensitive goes
|
||||
through a private advisory, never a public issue or PR.
|
||||
- Read the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
@@ -43,9 +45,11 @@ to make a contribution land smoothly.
|
||||
"Was gegenüber Chat 2 fehlt".
|
||||
- Features that bypass the privacy filter or weaken the default
|
||||
retention behaviour without an explicit, documented opt-in.
|
||||
- Sweeping refactors that touch large parts of the codebase. They make
|
||||
selective upstream cherry-picks much harder and the maintenance cost
|
||||
outweighs the benefit for a one-person project.
|
||||
- Sweeping refactors that touch large parts of the codebase. The
|
||||
maintenance cost outweighs the benefit for a one-person project.
|
||||
(This used to be doubly important because of the upstream
|
||||
cherry-pick path; that path is closed now, but the rule still
|
||||
holds on its own merits.)
|
||||
- AI-generated code dropped in without disclosure or human review. See
|
||||
[`docs/AI_DISCLOSURE.md`](docs/AI_DISCLOSURE.md) for how I handle
|
||||
AI assistance on my side; I expect comparable transparency from
|
||||
@@ -117,9 +121,15 @@ Hellion-specific strings live in
|
||||
direct pull requests.
|
||||
|
||||
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx`
|
||||
are **not** translated here. They are owned by the upstream project
|
||||
and synced in via cherry-pick. Please contribute those to
|
||||
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) instead.
|
||||
are **not** translated here. They are kept as-is from the last
|
||||
upstream sync and remain the work of the Chat 2 Crowdin community.
|
||||
Active cherry-picking from upstream ended in the v1.4.x cycle (see
|
||||
[`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md)), so future
|
||||
translation improvements to those upstream strings will not flow
|
||||
into HellionChat automatically anymore. If you have improvements
|
||||
for the original Chat 2 strings, please contribute them to
|
||||
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo)
|
||||
directly.
|
||||
|
||||
## Licensing
|
||||
|
||||
@@ -145,3 +155,19 @@ I respond on weekdays during European business hours and take weekends
|
||||
and FFXIV patch days off. A pull request that sits for a few days has
|
||||
not been ignored. Pinging once after a week is fine; please do not
|
||||
ping daily.
|
||||
|
||||
## First-time setup
|
||||
|
||||
After cloning, run once:
|
||||
|
||||
```bash
|
||||
./scripts/setup-hooks.sh
|
||||
```
|
||||
|
||||
This wires `core.hooksPath` to `.githooks/`. The pre-push hook runs preflight
|
||||
(versions/manifest/changelog/build).
|
||||
|
||||
### Test suite
|
||||
|
||||
The plugin's test suite lives in a separate local repository and is not part of
|
||||
this codebase. If you need access for development, contact the maintainer.
|
||||
|
||||
@@ -4,8 +4,7 @@ namespace HellionChat.Branding;
|
||||
// Centralised so a future invite rotation only touches one file. The same
|
||||
// link is currently hard-coded in repo.json, README.md, SUPPORT.md,
|
||||
// CONTRIBUTORS.md and HellionChat.yaml — those will be migrated to consume
|
||||
// this constant in a separate housekeeping sweep, but that's out of scope
|
||||
// for this Cycle.
|
||||
// this constant in a separate housekeeping sweep
|
||||
internal static class BrandingLinks
|
||||
{
|
||||
public const string HellionForgeDiscordInvite = "https://discord.gg/X9V7Kcv5gR";
|
||||
|
||||
@@ -482,6 +482,16 @@ public class Tab
|
||||
// session. NonSerialized because the temp tab itself is session-only.
|
||||
[NonSerialized] public bool IsGreeted;
|
||||
|
||||
// v1.4.2 — TabTintCache uses separate validation keys per cache so a
|
||||
// TellTarget change picked up by GetTint can't strand GetIcon (or vice
|
||||
// versa) with a stale entry that looks fresh on the shared key.
|
||||
[NonSerialized] internal string? _cachedTintTellName;
|
||||
[NonSerialized] internal uint _cachedTintTellWorld;
|
||||
[NonSerialized] internal uint _cachedTellTint;
|
||||
[NonSerialized] internal string? _cachedIconTellName;
|
||||
[NonSerialized] internal uint _cachedIconTellWorld;
|
||||
[NonSerialized] internal string? _cachedTellIcon;
|
||||
|
||||
public bool Matches(Message message)
|
||||
{
|
||||
if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels))
|
||||
|
||||
@@ -100,6 +100,19 @@ public class FontManager
|
||||
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async wrapper around <see cref="BuildFonts"/> for the Phase-1 LoadAsync
|
||||
/// path. The font-atlas build is CPU-bound, so we offload via Task.Run and
|
||||
/// honour the cancellation token at the scheduling boundary; this lets the
|
||||
/// font build run in parallel with the theme init without blocking the
|
||||
/// loader. Settings-driven manual rebuilds keep using the sync entry point.
|
||||
/// </summary>
|
||||
public async Task BuildFontsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Task.Run(BuildFonts, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void BuildFonts()
|
||||
{
|
||||
SetUpRanges();
|
||||
|
||||
@@ -16,6 +16,18 @@ public unsafe class ChatBox
|
||||
}
|
||||
|
||||
public static void SendMessage(string message)
|
||||
{
|
||||
var bytes = ValidateMessage(message);
|
||||
SendMessageUnsafe(bytes);
|
||||
}
|
||||
|
||||
// Validation split out so the deterministic checks (UTF-8 length, sanitise
|
||||
// round-trip) can run in xUnit without ClientStructs game memory. The
|
||||
// sanitiser is injectable so tests can pin throw behaviour without invoking
|
||||
// Utf8String->SanitizeString, which only resolves in-process. Returns the
|
||||
// already-encoded bytes so SendMessage doesn't pay GetBytes twice.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/GameFunctions/ChatBoxTests.cs
|
||||
internal static byte[] ValidateMessage(string message, Func<string, string>? sanitiserOverride = null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
if (bytes.Length == 0)
|
||||
@@ -24,10 +36,11 @@ public unsafe class ChatBox
|
||||
if (bytes.Length > 500)
|
||||
throw new ArgumentException(Language.ChatBox_Error_Too_Long, nameof(message));
|
||||
|
||||
if (message.Length != SanitiseText(message).Length)
|
||||
var sanitiser = sanitiserOverride ?? SanitiseText;
|
||||
if (message.Length != sanitiser(message).Length)
|
||||
throw new ArgumentException(Language.ChatBox_Error_Invalid, nameof(message));
|
||||
|
||||
SendMessageUnsafe(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static string SanitiseText(string text)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
0.1.0 is our bootstrap release; the underlying Chat 2 base is
|
||||
called out in the yaml changelog so users can see what it
|
||||
derives from. -->
|
||||
<Version>1.4.1</Version>
|
||||
<Version>1.4.3</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Honor packages.lock.json on restore so floating version ranges
|
||||
|
||||
@@ -31,11 +31,6 @@ description: |-
|
||||
- Independent plugin state — own config file and database directory,
|
||||
so Hellion Chat does not share state with upstream Chat 2
|
||||
|
||||
v1.2.3 — Theme catalogue grown to nine built-in themes:
|
||||
Hellion Arctic, Hellion Spectrum (CVD-safe Deuteran/Protan),
|
||||
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove,
|
||||
Night Blue, Indigo Violet, Forge Merchantman.
|
||||
|
||||
v1.3.0 First plugin integration cycle. Honorific custom titles
|
||||
are shown in the chat header above the message log, with auto-detect
|
||||
and silent fallback when Honorific is not installed.
|
||||
@@ -56,18 +51,34 @@ description: |-
|
||||
as the tenth built-in theme — Hot Magenta + Cyan on midnight
|
||||
violet, 80s neon-grid vibes.
|
||||
|
||||
v1.4.2 — ChatLog Frame-Hot-Path. Three per-frame allocation
|
||||
patterns gone from the chat-log render path: card-mode borders
|
||||
hoist invariants out of the per-message loop, auto-tell tab
|
||||
tint and icon get a per-tab cache, and the status bar gates
|
||||
its tab aggregation behind the same one-second cache it uses
|
||||
for the format strings.
|
||||
|
||||
v1.4.3 — Plugin-Load Async-Init plus Repo-Cutover. Plugin
|
||||
migrated to Dalamud's IAsyncDalamudPlugin so the heavy work
|
||||
(migrations, service allocations, window construction, hook
|
||||
subscription) runs in LoadAsync without blocking Dalamud's
|
||||
UI. Schema-gate replaces the v9 → v16 migration chain;
|
||||
configs on schema v16+ load directly. Custom-repo URL moves
|
||||
to gitea.hellion-forge.cloud, the GitHub repo stays as a
|
||||
frozen v1.4.2 snapshot.
|
||||
|
||||
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
|
||||
|
||||
Modding & support: join the Hellion Forge Discord at
|
||||
https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
|
||||
other Hellion Online Media plugins/tools.
|
||||
repo_url: https://github.com/JonKazama-Hellion/HellionChat
|
||||
repo_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat
|
||||
accepts_feedback: true
|
||||
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png
|
||||
icon_url: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png
|
||||
image_urls:
|
||||
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.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
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/settingsOverview.png
|
||||
- https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/themesPicker.png
|
||||
tags:
|
||||
- Social
|
||||
- UI
|
||||
@@ -75,6 +86,68 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
changelog: |-
|
||||
**Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover (2026-05-08)**
|
||||
|
||||
Plugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin`
|
||||
API. The constructor now does only the bootstrap-essentials
|
||||
(config load, language init, conflict detection); migrations,
|
||||
service allocations, window construction and hook subscription
|
||||
move to LoadAsync. Dalamud can keep its UI responsive while the
|
||||
heavy work runs.
|
||||
|
||||
- IAsyncDalamudPlugin two-phase load with per-line CaptureFailure
|
||||
in DisposeAsync (mirrors LightlessSync's pattern); idempotency
|
||||
guard protects against reload races
|
||||
- Schema-gate replaces the v9 → v16 migration chain. Configs
|
||||
on schema v16+ load directly; older configs trigger an
|
||||
"install v1.4.2 first" error so the historic migration
|
||||
path stays intact
|
||||
- AutoTranslate.PreloadCache moved off the load path. First
|
||||
use may have a sub-second hitch instead of every-load; the
|
||||
upstream chose differently, we accept first-use latency
|
||||
- FontManager.BuildFonts is called sync at the start of
|
||||
LoadAsync; Dalamud rebuilds the font atlas on its own
|
||||
pipeline so the custom Hellion-Exo2 font appears with a
|
||||
brief font-pop after load (matches ChatTwo's behaviour)
|
||||
- Custom-repo URL moved to gitea.hellion-forge.cloud/
|
||||
JonKazama-Hellion/HellionChat. GitHub repo stays as a
|
||||
frozen v1.4.2 snapshot; new releases ship from Gitea.
|
||||
Existing testers need to update the custom-repo URL once
|
||||
- Plugin-load time in this release sits at ~3.7 s median
|
||||
(5 reloads), comparable to v1.4.2. Async migration is
|
||||
foundational for v1.4.4 Lazy-Init optimisations rather
|
||||
than an immediate user-perceived win
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path**
|
||||
|
||||
Third sub-patch of the v1.4.x Polish Sweep series. Per-frame
|
||||
allocations from the chat-log render path eliminated.
|
||||
|
||||
- DrawMessages card-mode hoists theme/drawList/winLeft/winRight/
|
||||
borderColorAbgr out of the per-message loop. About 500
|
||||
redundant calls per frame at 100 visible messages, multiplied
|
||||
by every pop-out window
|
||||
- Auto-tell tab tint and icon use a per-tab cache. Hash
|
||||
computation and string allocation only happen when the tell
|
||||
target name or world drifts. AutoTellTabTint stays a pure
|
||||
hash helper; cache lives in a thin TabTintCache wrapper
|
||||
- Status bar gates its tab aggregation behind the same
|
||||
one-second cache it already used for the format strings.
|
||||
LINQ Sum and Count replaced with a single foreach pass
|
||||
that runs on roughly 1% of frames
|
||||
|
||||
Realistic frame-time recovery: 2-5% in typical scenes, more
|
||||
on pop-out-heavy setups because the card-border hoist scales
|
||||
per window.
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 1.4.1 — Theme Engine Performance**
|
||||
|
||||
Second sub-patch of the v1.4.x Polish Sweep series. Heap
|
||||
@@ -143,52 +216,6 @@ changelog: |-
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 1.3.0 - Plugin Integrations: Honorific**
|
||||
|
||||
First step on the plugin-integration roadmap. HellionChat now
|
||||
listens to Honorific and shows your custom title in the chat
|
||||
header. The slot auto-hides when Honorific is not installed,
|
||||
when no custom title is active, or when you are using the
|
||||
original FFXIV title.
|
||||
|
||||
- New "Integrations" settings tab
|
||||
- Honorific integration with auto-detection and live updates
|
||||
- "Coming soon" preview of the next five planned integrations:
|
||||
context menu actions, smart notifications, RP status block,
|
||||
ExtraChat channels, and quick DM compose
|
||||
- Maintainer attribution buttons for Honorific repo and Caraxi
|
||||
- New service-class pattern under HellionChat/Integrations/
|
||||
|
||||
Modding and support: join Hellion Forge - https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
**Hellion Chat 1.2.3 — Theme Expansion**
|
||||
|
||||
Four new built-in themes round out the picker. No engine changes,
|
||||
no settings touched — just more colour options.
|
||||
|
||||
- **Night Blue** — Royal Blue on deep marine. Cool tech-dashboard
|
||||
mood, distinct from the brand themes.
|
||||
- **Indigo Violet** — Royal Violet on deep indigo with a turquoise-
|
||||
mint counter for an aurora glitter feel. Sister to Event Horizon
|
||||
but darker and denser; the turquoise accent keeps the two
|
||||
distinguishable.
|
||||
- **Forge Merchantman** — Patina bronze on workshop slate, warm
|
||||
amber counter. Hellion Forge given a theme of its own — sister
|
||||
to Hellion Arctic but greener and warmer instead of cold cyan.
|
||||
- **Hellion Spectrum** — Deuteran/Protan-safe channel colours
|
||||
using Wong/Okabe-Ito palette tones. Channel identity (Tell pink,
|
||||
Yell yellow, Shout orange, Party blue, FC green) is preserved;
|
||||
tones are chosen so each channel stays distinguishable under
|
||||
red-green colour vision deficiency. Covers the ~99% of CVD cases
|
||||
that are red-green.
|
||||
|
||||
No schema bump, no migration. Default theme is unchanged (Hellion
|
||||
Arctic). Existing custom themes keep working.
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
|
||||
Earlier history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using HellionChat.Ipc;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui;
|
||||
@@ -17,7 +18,7 @@ using Dalamud.Interface.ImGuiFileDialog;
|
||||
namespace HellionChat;
|
||||
|
||||
// ReSharper disable once ClassNeverInstantiated.Global
|
||||
public sealed class Plugin : IDalamudPlugin
|
||||
public sealed class Plugin : IAsyncDalamudPlugin
|
||||
{
|
||||
public const string PluginName = "Hellion Chat";
|
||||
|
||||
@@ -41,32 +42,42 @@ public sealed class Plugin : IDalamudPlugin
|
||||
[PluginService] public static IAddonLifecycle AddonLifecycle { get; private set; } = null!;
|
||||
[PluginService] public static IPlayerState PlayerState { get; private set; } = null!;
|
||||
[PluginService] public static ISeStringEvaluator Evaluator { get; private set; } = null!;
|
||||
[PluginService] public static ISelfTestRegistry SelfTestRegistry { get; private set; } = null!;
|
||||
|
||||
public static Configuration Config = null!;
|
||||
public static FileDialogManager FileDialogManager { get; private set; } = null!;
|
||||
|
||||
public readonly WindowSystem WindowSystem = new(PluginName);
|
||||
public SettingsWindow SettingsWindow { get; }
|
||||
public ChatLogWindow ChatLogWindow { get; }
|
||||
public DbViewer DbViewer { get; }
|
||||
public InputPreview InputPreview { get; }
|
||||
public CommandHelpWindow CommandHelpWindow { get; }
|
||||
public SeStringDebugger SeStringDebugger { get; }
|
||||
public FirstRunWizard FirstRunWizard { get; }
|
||||
public DebuggerWindow DebuggerWindow { get; }
|
||||
|
||||
internal Commands Commands { get; }
|
||||
internal GameFunctions.GameFunctions Functions { get; }
|
||||
internal MessageManager MessageManager { get; }
|
||||
internal AutoTellTabsService AutoTellTabsService { get; }
|
||||
internal IpcManager Ipc { get; }
|
||||
internal ExtraChat ExtraChat { get; }
|
||||
internal TypingIpc TypingIpc { get; }
|
||||
internal FontManager FontManager { get; }
|
||||
// v1.4.3: properties moved from { get; } to { get; private set; } = null!;
|
||||
// because LoadAsync now owns construction of the Phase-2 services.
|
||||
// Phase-1 services use the same shape for consistency, even though
|
||||
// they're still allocated in the ctor.
|
||||
public SettingsWindow SettingsWindow { get; private set; } = null!;
|
||||
public ChatLogWindow ChatLogWindow { get; private set; } = null!;
|
||||
public DbViewer DbViewer { get; private set; } = null!;
|
||||
public InputPreview InputPreview { get; private set; } = null!;
|
||||
public CommandHelpWindow CommandHelpWindow { get; private set; } = null!;
|
||||
public SeStringDebugger SeStringDebugger { get; private set; } = null!;
|
||||
public FirstRunWizard FirstRunWizard { get; private set; } = null!;
|
||||
public DebuggerWindow DebuggerWindow { get; private set; } = null!;
|
||||
|
||||
internal Commands Commands { get; private set; } = null!;
|
||||
internal GameFunctions.GameFunctions Functions { get; private set; } = null!;
|
||||
internal MessageManager MessageManager { get; private set; } = null!;
|
||||
internal AutoTellTabsService AutoTellTabsService { get; private set; } = null!;
|
||||
internal IpcManager Ipc { get; private set; } = null!;
|
||||
internal ExtraChat ExtraChat { get; private set; } = null!;
|
||||
internal TypingIpc TypingIpc { get; private set; } = null!;
|
||||
internal FontManager FontManager { get; private set; } = null!;
|
||||
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
|
||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
||||
|
||||
// (B3) Lightless idempotency guard — Dalamud may fire DisposeAsync twice
|
||||
// in a reload race; second call short-circuits.
|
||||
private int _disposeStarted;
|
||||
|
||||
internal int DeferredSaveFrames = -1;
|
||||
|
||||
// Serialises retention sweeps. The 24h auto-sweep on plugin load and
|
||||
@@ -97,14 +108,18 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
public Plugin()
|
||||
{
|
||||
// Phase-1 ctor stays minimal: bootstrap-essentials only (conflict
|
||||
// gate, config load, language + ImGui init, WindowSystem skeleton).
|
||||
// Schema migrations and every service / window allocation moved to
|
||||
// LoadAsync so the sync ctor returns fast. On failure here nothing
|
||||
// is initialized yet, so just throw — there is nothing to clean up.
|
||||
|
||||
// Refuse to start if upstream Chat 2 is loaded — prevents IPC
|
||||
// channel collisions and double-replacement of the in-game chat
|
||||
// window. Throwing here makes Dalamud abort the load cleanly with
|
||||
// our localized message instead of crashing FFXIV mid-frame.
|
||||
ChatTwoConflictDetector.ThrowIfChatTwoIsLoaded(Interface);
|
||||
|
||||
try
|
||||
{
|
||||
GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime();
|
||||
|
||||
// Hellion Chat: take over config + database from upstream ChatTwo
|
||||
@@ -114,307 +129,38 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
|
||||
// Schema-gate: v1.4.3 only supports config schema v16. Older configs
|
||||
// went through their migrations in v1.2.1 (v15→v16) and earlier; users
|
||||
// who skipped past those releases need to install v1.4.2 first to run
|
||||
// the migration chain, then upgrade to v1.4.3.
|
||||
if (Config.Version < 16)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"HellionChat v1.4.3 requires config schema v16, got v{Config.Version}. " +
|
||||
"Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.3.");
|
||||
}
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs Defense-in-Depth. SaveConfig
|
||||
// already strips temp tabs before persistence, but a previous
|
||||
// crash or external write could have left them in the JSON.
|
||||
// Drop them on load to guarantee the session-only invariant.
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
|
||||
// Hellion Chat v9 → v10 — wipes the configuration so the new 8-tab
|
||||
// layout starts from defaults instead of mapping every previous setting
|
||||
// to its new position. Backup-Failure ist non-fatal, der Wipe läuft
|
||||
// trotzdem; dem User fehlt dann nur das manuelle Restore-Sicherheitsnetz.
|
||||
if (Config.Version < 10)
|
||||
LanguageChanged(Interface.UiLanguage);
|
||||
ImGuiUtil.Initialize(this);
|
||||
|
||||
DeferredSaveFrames = -1;
|
||||
|
||||
// WindowSystem skeleton is initialised by the readonly field above —
|
||||
// no AddWindow yet; window construction lives in LoadAsync.
|
||||
}
|
||||
|
||||
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
if (pluginConfigsDir is not null)
|
||||
{
|
||||
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
|
||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v10-backup");
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(liveConfigPath))
|
||||
{
|
||||
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "HellionChat: pre-v10 config backup failed");
|
||||
}
|
||||
}
|
||||
|
||||
Config = new Configuration
|
||||
{
|
||||
Version = 10,
|
||||
FirstRunCompleted = true,
|
||||
};
|
||||
SaveConfig();
|
||||
|
||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = HellionStrings.SettingsRefactor_Migration_Title,
|
||||
Content = HellionStrings.SettingsRefactor_Migration_Content,
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||
InitialDuration = TimeSpan.FromSeconds(25),
|
||||
});
|
||||
}
|
||||
|
||||
// Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled
|
||||
// master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out
|
||||
// input feature. Lightweight migration: defaults both fields,
|
||||
// no user-facing notification because the change is opt-in only.
|
||||
if (Config.Version < 11)
|
||||
{
|
||||
Config.PopOutInputEnabled = false;
|
||||
Config.SeenPopOutInputHint = false;
|
||||
Config.Version = 11;
|
||||
SaveConfig();
|
||||
Log.Information(
|
||||
"Migrated config v10 → v11: PopOutInputEnabled added (global, default off), " +
|
||||
"SeenPopOutInputHint added (default false)");
|
||||
}
|
||||
|
||||
// Hellion Chat v11 → v12 — flips Configuration.PopOutInputEnabled from
|
||||
// the v0.6.0 opt-in default (false) to opt-out (true) per v0.6.1 UX
|
||||
// polish. Hard-flip is a deliberate design call (see Spec section 5.7);
|
||||
// users are notified via the v0.6.1 hint banner (SeenPopOutHeaderHint
|
||||
// reset). Re-toggle after migration is preserved because this block
|
||||
// only fires for Version < 12.
|
||||
if (Config.Version < 12)
|
||||
{
|
||||
Config.PopOutInputEnabled = true;
|
||||
Config.SeenPopOutHeaderHint = false;
|
||||
Config.Version = 12;
|
||||
SaveConfig();
|
||||
Log.Information(
|
||||
"Migrated config v11 → v12: PopOutInputEnabled hard-flipped to true (v0.6.1 default), " +
|
||||
"SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)");
|
||||
}
|
||||
|
||||
// Hellion Chat v12 → v13 — hard-resets the tab layout to the
|
||||
// sharpened v1.0.0 defaults (5 thematic tabs, see TabsUtil and
|
||||
// the default-fill block below). Existing tab state is wiped
|
||||
// because per-channel mapping from the old General preset to
|
||||
// the new General/System split would be ambiguous and would
|
||||
// produce subtly wrong results for users who tweaked the old
|
||||
// layout. A timestamped backup of the live config is written
|
||||
// alongside it as a manual restore safety net. The wipe scope
|
||||
// is intentionally narrow: only Config.Tabs is reset; Privacy,
|
||||
// Retention, Theme and every other knob keeps its current value.
|
||||
if (Config.Version < 13)
|
||||
{
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
if (pluginConfigsDir is not null)
|
||||
{
|
||||
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
|
||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup");
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(liveConfigPath))
|
||||
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "HellionChat: pre-v13 config backup failed");
|
||||
}
|
||||
}
|
||||
|
||||
Config.Tabs.Clear();
|
||||
Config.Version = 13;
|
||||
SaveConfig();
|
||||
|
||||
Log.Information(
|
||||
"Migrated config v12 → v13: tab layout hard-reset to v1.0.0 defaults; " +
|
||||
"pre-v13 config backup written next to the live file. " +
|
||||
"Default tabs will be populated by the Tabs.Count == 0 block.");
|
||||
|
||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = HellionStrings.SettingsRefactor_Migration_Title,
|
||||
Content = HellionStrings.SettingsRefactor_Migration_Content,
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||
InitialDuration = TimeSpan.FromSeconds(25),
|
||||
});
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// v1.4.0 (F5.4): Pre-v13-Backup wird gelesen, HellionThemeWindowOpacity
|
||||
// ins neue Feld gezogen. Override nur wenn WindowOpacity noch beim
|
||||
// Default sitzt — sonst hat der User in der Zwischenzeit (z.B. via
|
||||
// WindowAlpha → WindowOpacity in v15→v16) explizit etwas gesetzt.
|
||||
if (Config.Version < 14)
|
||||
{
|
||||
Config.Theme = "hellion-arctic";
|
||||
|
||||
var oldThemeOpacity = TryReadPreV13ThemeOpacity();
|
||||
if (oldThemeOpacity is { } legacy
|
||||
&& Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f)
|
||||
{
|
||||
Config.WindowOpacity = Math.Clamp(legacy, 0.5f, 1.0f);
|
||||
Log.Information(
|
||||
$"Migrated pre-v13 HellionThemeWindowOpacity {legacy} to WindowOpacity {Config.WindowOpacity}");
|
||||
}
|
||||
|
||||
Config.ReduceMotion = false;
|
||||
Config.UseCompactDensity = 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");
|
||||
}
|
||||
|
||||
if (Config.Version < 15)
|
||||
{
|
||||
// v1.2.0 — keine Datenmigration nötig. Removal der deprecated
|
||||
// Theme-Felder ist reine Schema-Bereinigung (System.Text.Json
|
||||
// ignoriert unbekannte Felder im JSON, daher kein Crash bei
|
||||
// Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity
|
||||
// serialisiert haben — die Werte verfallen einfach).
|
||||
Config.Version = 15;
|
||||
SaveConfig();
|
||||
Log.Information(
|
||||
"Migrated config v14 → v15: legacy theme fields removed " +
|
||||
"(HellionThemeEnabled, HellionThemeWindowOpacity)");
|
||||
}
|
||||
|
||||
// Hellion Chat v15 → v16 — Settings Cleanup. Re-Sortierung der
|
||||
// Tabs auf der UI-Seite (datenneutral). 4 tote Felder verfallen
|
||||
// beim System.Text.Json-Deserialize (OverrideStyle, ChosenStyle,
|
||||
// WindowAlpha, ShowThemeQuickPicker — sind alle nicht mehr im
|
||||
// Configuration-Schema definiert). WindowAlpha wird zuvor auf
|
||||
// WindowOpacity gemappt damit User die ihn gesetzt hatten ihre
|
||||
// Transparenz-Einstellung behalten.
|
||||
if (Config.Version < 16)
|
||||
{
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
var liveConfigPath = pluginConfigsDir is not null
|
||||
? Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json")
|
||||
: null;
|
||||
|
||||
// Backup-Datei neben der live Config — Pattern aus v13 Branch.
|
||||
if (pluginConfigsDir is not null && liveConfigPath is not null)
|
||||
{
|
||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v16-backup");
|
||||
try
|
||||
{
|
||||
if (File.Exists(liveConfigPath))
|
||||
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "HellionChat: pre-v16 config backup failed");
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-v16 Felder einmalig roh aus dem JSON lesen, da sie nicht
|
||||
// mehr im Configuration-Schema sind (und damit aus Config nicht
|
||||
// mehr abrufbar). WindowAlpha → WindowOpacity Mapping nur wenn
|
||||
// User WindowOpacity noch nicht selbst angefasst hat (Default
|
||||
// 0.85), sonst gewinnt der User-Wert.
|
||||
float oldWindowAlpha = 100f;
|
||||
bool oldOverrideStyle = false;
|
||||
if (liveConfigPath is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(liveConfigPath))
|
||||
{
|
||||
var rawJson = File.ReadAllText(liveConfigPath);
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(rawJson);
|
||||
if (doc.RootElement.TryGetProperty("WindowAlpha", out var alphaProp)
|
||||
&& alphaProp.ValueKind == System.Text.Json.JsonValueKind.Number)
|
||||
{
|
||||
oldWindowAlpha = alphaProp.GetSingle();
|
||||
}
|
||||
if (doc.RootElement.TryGetProperty("OverrideStyle", out var ovProp)
|
||||
&& ovProp.ValueKind is System.Text.Json.JsonValueKind.True)
|
||||
{
|
||||
oldOverrideStyle = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "HellionChat: pre-v16 legacy-field lookup failed, defaults assumed");
|
||||
}
|
||||
}
|
||||
|
||||
if (oldWindowAlpha != 100f
|
||||
&& Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f)
|
||||
{
|
||||
Config.WindowOpacity = Math.Clamp(oldWindowAlpha / 100f, 0.5f, 1.0f);
|
||||
Log.Information(
|
||||
$"Migrated WindowAlpha {oldWindowAlpha} to WindowOpacity {Config.WindowOpacity}");
|
||||
}
|
||||
else if (oldWindowAlpha != 100f)
|
||||
{
|
||||
Log.Information(
|
||||
$"Skipped WindowAlpha→WindowOpacity migration: WindowOpacity already user-set " +
|
||||
$"({Config.WindowOpacity}), legacy WindowAlpha value {oldWindowAlpha} dropped.");
|
||||
}
|
||||
|
||||
if (oldOverrideStyle)
|
||||
{
|
||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = "Hellion Chat 1.2.1",
|
||||
Content = HellionStrings.Migration_v16_OverrideStyle_Toast,
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||
InitialDuration = TimeSpan.FromSeconds(25),
|
||||
});
|
||||
}
|
||||
|
||||
// v1.2.1 Default-Bumps für UX-Verbesserungen. Pattern: nur
|
||||
// migrieren wenn der User noch auf dem alten Default ist.
|
||||
// Bei bool-Werten ist die Erkennung pragmatisch — wer den
|
||||
// alten Default aktiv ausgeschaltet hatte, erlebt das als
|
||||
// Regression und stellt es einmal in den Settings zurück.
|
||||
// Der Trade-Off ist akzeptabel weil die alten Defaults in
|
||||
// v1.2.0 erst neu eingeführt wurden und kaum jemand aktiv
|
||||
// umgeschaltet hat.
|
||||
if (!Config.UseCompactDensity)
|
||||
{
|
||||
Config.UseCompactDensity = true;
|
||||
Log.Information("v16 default-bump: UseCompactDensity false → true");
|
||||
}
|
||||
if (!Config.HideInNewGamePlusMenu)
|
||||
{
|
||||
Config.HideInNewGamePlusMenu = true;
|
||||
Log.Information("v16 default-bump: HideInNewGamePlusMenu false → true");
|
||||
}
|
||||
if (!Config.HideSameTimestamps)
|
||||
{
|
||||
Config.HideSameTimestamps = true;
|
||||
Log.Information("v16 default-bump: HideSameTimestamps false → true");
|
||||
}
|
||||
if (Config.MaxLinesToRender == 5000)
|
||||
{
|
||||
Config.MaxLinesToRender = 2500;
|
||||
Log.Information("v16 default-bump: MaxLinesToRender 5000 → 2500");
|
||||
}
|
||||
if (Config.ChatColours.Count == 0)
|
||||
{
|
||||
foreach (var (channel, colour) in Resources.ChatColourPresets.All["Hellion"].Colours)
|
||||
Config.ChatColours[channel] = colour;
|
||||
Log.Information("v16 default-bump: ChatColours empty → Hellion brand preset");
|
||||
}
|
||||
|
||||
Config.Version = 16;
|
||||
SaveConfig();
|
||||
Log.Information(
|
||||
"Migrated config v15 → v16: settings cleanup, " +
|
||||
"OverrideStyle/ChosenStyle/WindowAlpha/ShowThemeQuickPicker dropped from schema");
|
||||
}
|
||||
|
||||
// Hellion v1.0.0 default tab layout. Five thematically separated
|
||||
// tabs: General catches the immediate-surroundings public chat
|
||||
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
||||
@@ -432,48 +178,54 @@ public sealed class Plugin : IDalamudPlugin
|
||||
Config.Tabs.Add(TabsUtil.HellionLinkshell);
|
||||
}
|
||||
|
||||
LanguageChanged(Interface.UiLanguage);
|
||||
ImGuiUtil.Initialize(this);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
FileDialogManager = new FileDialogManager();
|
||||
|
||||
Commands = new Commands();
|
||||
Functions = new GameFunctions.GameFunctions(this);
|
||||
Ipc = new IpcManager();
|
||||
TypingIpc = new TypingIpc(this);
|
||||
ExtraChat = new ExtraChat();
|
||||
// Sync allocation + handle registration. BuildFonts() registers
|
||||
// IFontHandles with Dalamud's UiBuilder.FontAtlas — registration
|
||||
// itself is non-blocking (handles stored, lambdas queued). Dalamud
|
||||
// rebuilds the atlas on its own pipeline a few frames later; first
|
||||
// frames render with the default font until the rebuild lands and
|
||||
// ImGui switches to Hellion-Exo2 / NotoSans (visible "font-pop").
|
||||
// Mirrors ChatTwo Plugin.cs:152.
|
||||
FontManager = new FontManager();
|
||||
FontManager.BuildFonts();
|
||||
|
||||
// v1.1.0 — Theme-Engine init. Custom-Themes liegen in
|
||||
// pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get.
|
||||
// Theme init stays sync on the LoadAsync continuation — cheap,
|
||||
// and Active is read every Draw frame, so the registry must be
|
||||
// wired before the first hook fires.
|
||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||
Directory.CreateDirectory(customThemesDir);
|
||||
SeedExampleThemeIfEmpty(customThemesDir);
|
||||
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
||||
ThemeRegistry.Switch(Config.Theme);
|
||||
|
||||
// Plugin integrations register their IPC subscribers up-front so
|
||||
// Ready/Disposing events from the target plugins are caught from
|
||||
// the very first frame, even if the user's Honorific reloads
|
||||
// mid-session. See HellionChat/Integrations/HonorificService.cs.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Service allocations: order encodes dependencies. Commands is
|
||||
// alloc-only here; Initialise() runs after windows exist so the
|
||||
// slash-commands can toggle their visibility. HonorificService
|
||||
// registers IPC subscribers up-front so Ready/Disposing events
|
||||
// are caught from the very first frame.
|
||||
FileDialogManager = new FileDialogManager();
|
||||
Commands = new Commands();
|
||||
Functions = new GameFunctions.GameFunctions(this);
|
||||
Ipc = new IpcManager();
|
||||
TypingIpc = new TypingIpc(this);
|
||||
ExtraChat = new ExtraChat();
|
||||
HonorificService = new Integrations.HonorificService(Interface, Log, Framework);
|
||||
|
||||
StatusBar = new Ui.StatusBar();
|
||||
MessageManager = new MessageManager(this);
|
||||
|
||||
MessageManager = new MessageManager(this); // Does it require UI?
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
||||
// MessageManager's MessageProcessed event for live tells and
|
||||
// to ClientState.Logout for the cleanup pass. Created after
|
||||
// MessageManager so the constructor can hand off the live
|
||||
// store and event source.
|
||||
// Auto-Tell-Tabs subscribes to MessageManager.MessageProcessed for
|
||||
// live tells and to ClientState.Logout for cleanup; needs the live
|
||||
// store handed in at construction.
|
||||
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
|
||||
AutoTellTabsService.Initialize();
|
||||
|
||||
// Hellion Chat — daily retention sweep, off-thread so it never
|
||||
// blocks plugin load. Skips itself when disabled or already ran
|
||||
// within the past 24 hours.
|
||||
RunRetentionSweepIfDue();
|
||||
// SelfTest steps poll Active per frame and need the registry wired.
|
||||
SelfTestRegistry.RegisterTestSteps([
|
||||
new SelfTests.ThemeSwitchSelfTestStep(this),
|
||||
]);
|
||||
|
||||
ChatLogWindow = new ChatLogWindow(this);
|
||||
SettingsWindow = new SettingsWindow(this);
|
||||
@@ -498,17 +250,38 @@ public sealed class Plugin : IDalamudPlugin
|
||||
if (!Config.FirstRunCompleted)
|
||||
FirstRunWizard.IsOpen = true;
|
||||
|
||||
FontManager.BuildFonts();
|
||||
|
||||
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
||||
Interface.UiBuilder.DisableGposeUiHide = true;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// let all the other components register, then initialize commands
|
||||
Commands.Initialise();
|
||||
|
||||
// Daily retention sweep, fire-and-forget. Skips itself when
|
||||
// disabled or when it already ran within the past 24 hours.
|
||||
RunRetentionSweepIfDue();
|
||||
|
||||
if (Config.ShowEmotes)
|
||||
_ = EmoteCache.LoadData(); // Fire-and-forget, exceptions caught inside
|
||||
|
||||
if (Interface.Reason is not PluginLoadReason.Boot)
|
||||
MessageManager.FilterAllTabsAsync();
|
||||
|
||||
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
||||
Interface.UiBuilder.DisableGposeUiHide = true;
|
||||
|
||||
#if !DEBUG
|
||||
// Fire-and-forget on a worker thread. The first auto-translate use of
|
||||
// a session may have a sub-second hitch if the cache hasn't filled yet,
|
||||
// but that's preferable to making every user wait ~300 ms during
|
||||
// plugin load for a cache they may never touch. ChatTwo (upstream)
|
||||
// does this sync; we trade load-time for first-use latency.
|
||||
_ = Task.Run(AutoTranslate.PreloadCache, cancellationToken);
|
||||
#endif
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// (B1) Hooks last: every service and window must be live before
|
||||
// Dalamud fires our first Draw / FrameworkUpdate tick. Anything
|
||||
// earlier risks rendering against null FontManager / ThemeRegistry.
|
||||
Framework.Update += FrameworkUpdate;
|
||||
Interface.UiBuilder.Draw += Draw;
|
||||
Interface.LanguageChanged += LanguageChanged;
|
||||
@@ -517,102 +290,127 @@ public sealed class Plugin : IDalamudPlugin
|
||||
// most useful landing place; OpenConfigUi is already wired to
|
||||
// the same toggle inside SettingsWindow.
|
||||
Interface.UiBuilder.OpenMainUi += OpenMainUi;
|
||||
|
||||
if (Config.ShowEmotes)
|
||||
_ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
|
||||
|
||||
#if !DEBUG
|
||||
// Avoid 300ms hitch when sending first message by preloading the
|
||||
// auto-translate cache. Don't do this in debug because it makes
|
||||
// profiling difficult.
|
||||
AutoTranslate.PreloadCache();
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Log.Error(ex, "Plugin load threw an error, turning off plugin");
|
||||
Dispose();
|
||||
|
||||
// Re-throw the exception to fail the plugin load.
|
||||
// Mirror the v1.4.0 load-failure recovery: hand off to DisposeAsync
|
||||
// so partially-built services are torn down. Swallow the cleanup
|
||||
// exception so the original load failure stays the visible cause.
|
||||
try { await DisposeAsync().ConfigureAwait(false); }
|
||||
catch { /* keep original failure */ }
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Suppressing this warning because Dispose() is called in Plugin() if the
|
||||
// load fails, so some values may not be initialized.
|
||||
// Suppressing this warning because DisposeAsync may run after a partial
|
||||
// LoadAsync, so some properties may not be initialized.
|
||||
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract")]
|
||||
public void Dispose()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
Interface.UiBuilder.OpenMainUi -= OpenMainUi;
|
||||
Interface.LanguageChanged -= LanguageChanged;
|
||||
Interface.UiBuilder.Draw -= Draw;
|
||||
Framework.Update -= FrameworkUpdate;
|
||||
GameFunctions.GameFunctions.SetChatInteractable(true);
|
||||
// (B3) Idempotency guard — Dalamud may reload-race us; second
|
||||
// call short-circuits so we don't double-dispose services.
|
||||
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
|
||||
return;
|
||||
|
||||
// FrameworkUpdate would have fired the pending save in N frames,
|
||||
// but we just unsubscribed it. -1 is the idle sentinel.
|
||||
Exception? failure = null;
|
||||
|
||||
// Hooks unsubscribe FIRST so no Draw / FrameworkUpdate / LanguageChanged
|
||||
// tick can fire while we're tearing services down. Mirrors the
|
||||
// hooks-last subscribe order in LoadAsync.
|
||||
failure = CaptureFailure(failure, () => Interface.UiBuilder.OpenMainUi -= OpenMainUi);
|
||||
failure = CaptureFailure(failure, () => Interface.LanguageChanged -= LanguageChanged);
|
||||
failure = CaptureFailure(failure, () => Interface.UiBuilder.Draw -= Draw);
|
||||
failure = CaptureFailure(failure, () => Framework.Update -= FrameworkUpdate);
|
||||
|
||||
// v1.4.0 F5.3 — flush a pending DeferredSave before service teardown,
|
||||
// since FrameworkUpdate just got unsubscribed and won't fire it.
|
||||
failure = CaptureFailure(failure, () =>
|
||||
{
|
||||
if (DeferredSaveFrames >= 0)
|
||||
{
|
||||
SaveConfig();
|
||||
DeferredSaveFrames = -1;
|
||||
}
|
||||
});
|
||||
|
||||
HonorificService?.Dispose();
|
||||
// Auto-Tell-Tabs unsubscribes from MessageProcessed before MessageManager
|
||||
// goes away. Pure-memory cleanup, no framework-thread requirement.
|
||||
failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose());
|
||||
|
||||
WindowSystem?.RemoveAllWindows();
|
||||
ChatLogWindow?.Dispose();
|
||||
DbViewer?.Dispose();
|
||||
InputPreview?.Dispose();
|
||||
SettingsWindow?.Dispose();
|
||||
DebuggerWindow?.Dispose();
|
||||
SeStringDebugger?.Dispose();
|
||||
|
||||
TypingIpc?.Dispose();
|
||||
ExtraChat?.Dispose();
|
||||
Ipc?.Dispose();
|
||||
// Dispose the Auto-Tell-Tabs service before MessageManager so it
|
||||
// can cleanly unsubscribe from the MessageProcessed event before
|
||||
// its source goes away.
|
||||
AutoTellTabsService?.Dispose();
|
||||
MessageManager?.DisposeAsync().AsTask().Wait();
|
||||
Functions?.Dispose();
|
||||
Commands?.Dispose();
|
||||
|
||||
EmoteCache.Dispose();
|
||||
// v1.4.0 F6.2 — MessageManager has its own async dispose path
|
||||
// (DB flush, pending-message thread shutdown). Run it before the
|
||||
// framework-block so the worker threads are quiesced first.
|
||||
if (MessageManager is not null)
|
||||
{
|
||||
failure = await CaptureFailureAsync(failure, () => MessageManager.DisposeAsync().AsTask())
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Reads HellionThemeWindowOpacity from the pre-v13 backup the v12→v13
|
||||
// block writes alongside the live config. Null when absent, unreadable,
|
||||
// or schema-incompatible — all valid steady states (fresh install,
|
||||
// backup pruned, pre-v12 config). Errors log at Warning so a corrupted
|
||||
// backup stays visible in /xllog without breaking the migration.
|
||||
private static float? TryReadPreV13ThemeOpacity()
|
||||
{
|
||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||
if (pluginConfigsDir is null)
|
||||
return null;
|
||||
|
||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup");
|
||||
if (!File.Exists(backupPath))
|
||||
return null;
|
||||
|
||||
// (B4) Game-Function / IPC / UI-Window cleanup MUST run on the
|
||||
// framework thread. WindowSystem mutations and IPC subscriber
|
||||
// disposes touch Dalamud state that's only safe from the framework.
|
||||
// Worker-thread DisposeAsync would race the next Draw tick.
|
||||
// Per-line CaptureFailure so a single throw can't strand the lines
|
||||
// behind it; mirrors Lightless DisposeFrameworkBoundServicesAsync.
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(backupPath);
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(stream);
|
||||
if (doc.RootElement.TryGetProperty("HellionThemeWindowOpacity", out var prop)
|
||||
&& prop.ValueKind == System.Text.Json.JsonValueKind.Number
|
||||
&& prop.TryGetSingle(out var value))
|
||||
await Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
// Game-Functions first — other services may still query
|
||||
// chat-interactable state during their Dispose.
|
||||
failure = CaptureFailure(failure, () => GameFunctions.GameFunctions.SetChatInteractable(true));
|
||||
|
||||
// IPC subscribers — dispose before windows so any final
|
||||
// event firing from the IPC source can't reach a half-torn
|
||||
// ChatLogWindow.
|
||||
failure = CaptureFailure(failure, () => HonorificService?.Dispose());
|
||||
failure = CaptureFailure(failure, () => TypingIpc?.Dispose());
|
||||
failure = CaptureFailure(failure, () => ExtraChat?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Ipc?.Dispose());
|
||||
|
||||
// Windows — RemoveAllWindows first, then per-window Dispose.
|
||||
// Order matches the pre-v1.4.3 Dispose body byte-for-byte.
|
||||
// CommandHelpWindow and FirstRunWizard don't implement
|
||||
// IDisposable; their resources are reclaimed via WindowSystem.
|
||||
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
|
||||
failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => DbViewer?.Dispose());
|
||||
failure = CaptureFailure(failure, () => InputPreview?.Dispose());
|
||||
failure = CaptureFailure(failure, () => SettingsWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => DebuggerWindow?.Dispose());
|
||||
failure = CaptureFailure(failure, () => SeStringDebugger?.Dispose());
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "HellionChat: pre-v13 backup lookup failed, defaulting WindowOpacity");
|
||||
return null;
|
||||
failure ??= ex;
|
||||
}
|
||||
|
||||
// Pure-memory cleanups — no Framework / UI / IPC touch, so they
|
||||
// run on whatever thread DisposeAsync resumes on.
|
||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
||||
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
||||
|
||||
if (failure is not null)
|
||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||
}
|
||||
|
||||
// Lightless-pattern capture helpers: run cleanup, remember the FIRST
|
||||
// exception, keep going. Without these one mid-teardown failure would
|
||||
// skip every cleanup behind it and leave services half-torn.
|
||||
private static Exception? CaptureFailure(Exception? failure, Action action)
|
||||
{
|
||||
try { action(); }
|
||||
catch (Exception ex) { failure ??= ex; }
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static async ValueTask<Exception?> CaptureFailureAsync(Exception? failure, Func<Task> action)
|
||||
{
|
||||
try { await action().ConfigureAwait(false); }
|
||||
catch (Exception ex) { failure ??= ex; }
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static void MigrateFromChatTwoLayout()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin.SelfTest;
|
||||
using HellionChat.Themes;
|
||||
|
||||
namespace HellionChat.SelfTests;
|
||||
|
||||
// Validates the runtime theme-switch contract from the user side. The
|
||||
// caller toggles the active theme via Settings -> Theme & Layout, the
|
||||
// step polls ThemeRegistry.Active per frame and only passes once the
|
||||
// slug has moved away from the initial value and back. The ABGR cache
|
||||
// is sanity-checked on every frame: a freshly switched theme must carry
|
||||
// a populated cache, otherwise Switch() forgot the recompute and the UI
|
||||
// would still draw, just with all-transparent slots.
|
||||
internal sealed class ThemeSwitchSelfTestStep : ISelfTestStep
|
||||
{
|
||||
private readonly Plugin plugin;
|
||||
private string? initialSlug;
|
||||
private bool switchedAway;
|
||||
|
||||
public ThemeSwitchSelfTestStep(Plugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public string Name => "Hellion Chat - Theme switch";
|
||||
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
var registry = this.plugin.ThemeRegistry;
|
||||
if (registry is null)
|
||||
return SelfTestStepResult.Fail;
|
||||
|
||||
var active = registry.Active;
|
||||
if (active is null)
|
||||
return SelfTestStepResult.Fail;
|
||||
|
||||
if (!HasPopulatedCache(active))
|
||||
return SelfTestStepResult.Fail;
|
||||
|
||||
if (this.initialSlug is null)
|
||||
{
|
||||
this.initialSlug = active.Slug;
|
||||
ImGui.Text($"Initial theme: \"{this.initialSlug}\". Open Settings -> Theme & Layout and pick a different theme.");
|
||||
return SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
if (!this.switchedAway)
|
||||
{
|
||||
if (!string.Equals(active.Slug, this.initialSlug, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
this.switchedAway = true;
|
||||
return SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
ImGui.Text($"Switch the active theme away from \"{this.initialSlug}\".");
|
||||
return SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
if (!string.Equals(active.Slug, this.initialSlug, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ImGui.Text($"Switch back to \"{this.initialSlug}\" to finish the test.");
|
||||
return SelfTestStepResult.Waiting;
|
||||
}
|
||||
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
public void CleanUp()
|
||||
{
|
||||
this.initialSlug = null;
|
||||
this.switchedAway = false;
|
||||
}
|
||||
|
||||
// Any non-zero slot proves the cache was actually recomputed for the
|
||||
// current theme. We don't compare against a reference, because custom
|
||||
// themes can legitimately share slot values with a built-in.
|
||||
private static bool HasPopulatedCache(Theme theme)
|
||||
{
|
||||
var cache = theme.AbgrCache;
|
||||
return (cache.Primary
|
||||
| cache.WindowBg
|
||||
| cache.TextPrimary
|
||||
| cache.Border) != 0u;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.Util;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
@@ -89,60 +90,42 @@ public sealed class ChatInputBar
|
||||
}
|
||||
}
|
||||
|
||||
private void SubmitCompact(Tab tab)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_state.Buffer))
|
||||
return;
|
||||
// TEST-MIRROR: ../_Helpers/CompactInputSubmitter.cs
|
||||
private void SubmitCompact(Tab tab) =>
|
||||
CompactInputSubmitter.TrySubmit(_state, tab, _host.SendChatBoxFromExternal);
|
||||
|
||||
var text = _state.Buffer;
|
||||
_state.Buffer = string.Empty;
|
||||
_state.HistoryCursor = -1;
|
||||
_host.SendChatBoxFromExternal(tab, text);
|
||||
}
|
||||
|
||||
// History-navigation callback for the compact input. Mirrors the main
|
||||
// window's logic but operates on _state.HistoryCursor and the shared
|
||||
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
|
||||
// 0 = oldest, Count-1 = newest.
|
||||
// History-navigation callback for the compact input. Cursor math is
|
||||
// delegated to CompactInputHistoryNavigator; only the ImGui buffer
|
||||
// splice stays here because it needs the live callback data.
|
||||
// TEST-MIRROR: ../_Helpers/CompactInputHistoryNavigator.cs
|
||||
private int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
|
||||
{
|
||||
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
|
||||
return 0;
|
||||
|
||||
var prev = _state.HistoryCursor;
|
||||
switch (data.EventKey)
|
||||
var direction = data.EventKey switch
|
||||
{
|
||||
case ImGuiKey.UpArrow:
|
||||
switch (_state.HistoryCursor)
|
||||
{
|
||||
case -1:
|
||||
var offset = 0;
|
||||
if (!string.IsNullOrWhiteSpace(_state.Buffer))
|
||||
{
|
||||
InputHistoryService.Push(_state.Buffer);
|
||||
offset = 1;
|
||||
}
|
||||
_state.HistoryCursor = InputHistoryService.Count - 1 - offset;
|
||||
break;
|
||||
case > 0:
|
||||
_state.HistoryCursor--;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ImGuiKey.DownArrow:
|
||||
if (_state.HistoryCursor != -1)
|
||||
if (++_state.HistoryCursor >= InputHistoryService.Count)
|
||||
_state.HistoryCursor = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev == _state.HistoryCursor)
|
||||
ImGuiKey.UpArrow => CompactInputHistoryNavigator.Direction.Up,
|
||||
ImGuiKey.DownArrow => CompactInputHistoryNavigator.Direction.Down,
|
||||
_ => (CompactInputHistoryNavigator.Direction?)null,
|
||||
};
|
||||
if (direction is null)
|
||||
return 0;
|
||||
|
||||
var historyStr = InputHistoryService.GetByCursor(_state.HistoryCursor) ?? string.Empty;
|
||||
data.DeleteChars(0, data.BufTextLen);
|
||||
data.InsertChars(0, historyStr);
|
||||
var (cursor, replacement) = CompactInputHistoryNavigator.Navigate(
|
||||
direction.Value,
|
||||
_state.HistoryCursor,
|
||||
_state.Buffer,
|
||||
() => InputHistoryService.Count,
|
||||
InputHistoryService.Push,
|
||||
InputHistoryService.GetByCursor);
|
||||
|
||||
_state.HistoryCursor = cursor;
|
||||
if (replacement is null)
|
||||
return 0;
|
||||
|
||||
data.DeleteChars(0, data.BufTextLen);
|
||||
data.InsertChars(0, replacement);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1203,6 +1203,15 @@ public sealed class ChatLogWindow : Window
|
||||
|
||||
var maxLines = Plugin.Config.MaxLinesToRender;
|
||||
var startLine = messages.Count > maxLines ? messages.Count - maxLines : 0;
|
||||
|
||||
// Card-mode pre-loop hoist: theme/drawList/winLeft/winRight/border
|
||||
// are invariant per DrawMessages call; only cursorY moves per row.
|
||||
var theme = Plugin.ThemeRegistry.Active;
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var winLeft = ImGui.GetWindowPos().X;
|
||||
var winRight = winLeft + ImGui.GetWindowSize().X;
|
||||
var borderColorAbgr = ColourUtil.RgbaToAbgr((theme.Colors.Border & 0xFFFFFF00u) | 0x33u);
|
||||
|
||||
for (var i = startLine; i < messages.Count; i++)
|
||||
{
|
||||
var message = messages[i];
|
||||
@@ -1344,7 +1353,6 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
if (message.Sender.Count > 0)
|
||||
{
|
||||
var theme = Plugin.ThemeRegistry.Active;
|
||||
var senderColor = Plugin.Functions.Chat.GetChannelColor(message.Code.Type)
|
||||
?? theme.Colors.TextPrimary;
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(senderColor)))
|
||||
@@ -1363,15 +1371,11 @@ public sealed class ChatLogWindow : Window
|
||||
// Subtile Border-Bottom als Card-Trenner. Border-Farbe mit
|
||||
// reduzierter Alpha (RGBA → 0x33) für dezente Trennung.
|
||||
{
|
||||
var theme = Plugin.ThemeRegistry.Active;
|
||||
var rowEndY = ImGui.GetCursorScreenPos().Y;
|
||||
var winLeft = ImGui.GetWindowPos().X;
|
||||
var winRight = winLeft + ImGui.GetWindowSize().X;
|
||||
var borderRgba = (theme.Colors.Border & 0xFFFFFF00u) | 0x33u;
|
||||
ImGui.GetWindowDrawList().AddLine(
|
||||
drawList.AddLine(
|
||||
new Vector2(winLeft + 4, rowEndY - 1),
|
||||
new Vector2(winRight - 4, rowEndY - 1),
|
||||
ColourUtil.RgbaToAbgr(borderRgba),
|
||||
borderColorAbgr,
|
||||
1f);
|
||||
ImGui.Dummy(new Vector2(0, 2));
|
||||
}
|
||||
@@ -1567,7 +1571,7 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
// v1.2.0 — Hash-Color-Tint differenziert parallele Auto-Tell-Tabs
|
||||
// visuell ohne dass User pro Tab manuell ein Custom-Icon setzen muss.
|
||||
iconColor = AutoTellTabTint.For(tab.TellTarget.Name, tab.TellTarget.World);
|
||||
iconColor = TabTintCache.GetTint(tab);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -76,7 +76,7 @@ internal sealed class Information : ISettingsTab
|
||||
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
|
||||
Dalamud.Utility.Util.OpenLink("https://github.com/JonKazama-Hellion/HellionChat/issues");
|
||||
Dalamud.Utility.Util.OpenLink("https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,19 @@ internal sealed class StatusBar
|
||||
return $"{count} {(count == 1 ? "tell" : "tells")}";
|
||||
}
|
||||
|
||||
// Single-pass replacement for the LINQ Sum+Count pair in Draw. Pure
|
||||
// helper so a future LINQ regression gets pinned by xUnit.
|
||||
internal static (int messages, int tells) AggregateForStatusBar(IList<Tab> tabs)
|
||||
{
|
||||
int messages = 0, tells = 0;
|
||||
foreach (var t in tabs)
|
||||
{
|
||||
messages += t.Messages.Count;
|
||||
if (t.IsTempTab) tells++;
|
||||
}
|
||||
return (messages, tells);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test-Hook: Cache-Logic ohne reale Time-Source verifizieren.
|
||||
/// Nicht für Production-Render.
|
||||
@@ -80,12 +93,13 @@ internal sealed class StatusBar
|
||||
var theme = plugin.ThemeRegistry.Active;
|
||||
var now = Environment.TickCount64;
|
||||
|
||||
// Counts pro Frame berechnen ist günstig (List<>.Count, kleine
|
||||
// Sums); Format-String wird gecached.
|
||||
var tabs = Plugin.Config.Tabs.Count;
|
||||
var messages = Plugin.Config.Tabs.Sum(t => t.Messages.Count);
|
||||
var tells = Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
||||
UpdateCacheIfDue(now, tabs, messages, tells);
|
||||
// Outer gate keeps the foreach out of the hot path 99% of frames.
|
||||
// UpdateCacheIfDue runs the same check internally — idempotent.
|
||||
if (now - _lastUpdateMs >= UpdateIntervalMs)
|
||||
{
|
||||
var (messages, tells) = AggregateForStatusBar(Plugin.Config.Tabs);
|
||||
UpdateCacheIfDue(now, Plugin.Config.Tabs.Count, messages, tells);
|
||||
}
|
||||
|
||||
// BorderTop als Trenner — DrawList-Line, ImGui-Separator hat zu viel Padding.
|
||||
var cursorY = ImGui.GetCursorScreenPos().Y;
|
||||
|
||||
@@ -61,7 +61,7 @@ internal static class TabIconMapping
|
||||
string? autoTellGlyph = null;
|
||||
if (tab.IsTempTab && tab.TellTarget != null && tab.TellTarget.IsSet())
|
||||
{
|
||||
autoTellGlyph = AutoTellTabTint.IconFor(tab.TellTarget.Name, tab.TellTarget.World);
|
||||
autoTellGlyph = TabTintCache.GetIcon(tab);
|
||||
}
|
||||
|
||||
var glyph = TabIconGlyphResolver.ResolveGlyphName(tab, autoTellGlyph);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
// Per-Tab cache wrapper around the pure AutoTellTabTint hash helpers.
|
||||
// Each cache (tint, icon) carries its own name+world validation key so
|
||||
// neither read path mutates the other's state — refilling one never
|
||||
// invalidates the other. No string allocation in the steady-state lookup.
|
||||
internal static class TabTintCache
|
||||
{
|
||||
public static uint GetTint(Tab tab)
|
||||
{
|
||||
var name = tab.TellTarget.Name;
|
||||
var world = tab.TellTarget.World;
|
||||
if (tab._cachedTintTellName != name || tab._cachedTintTellWorld != world)
|
||||
{
|
||||
tab._cachedTintTellName = name;
|
||||
tab._cachedTintTellWorld = world;
|
||||
tab._cachedTellTint = AutoTellTabTint.For(name, world);
|
||||
}
|
||||
return tab._cachedTellTint;
|
||||
}
|
||||
|
||||
public static string GetIcon(Tab tab)
|
||||
{
|
||||
var name = tab.TellTarget.Name;
|
||||
var world = tab.TellTarget.World;
|
||||
if (tab._cachedTellIcon is null
|
||||
|| tab._cachedIconTellName != name
|
||||
|| tab._cachedIconTellWorld != world)
|
||||
{
|
||||
tab._cachedIconTellName = name;
|
||||
tab._cachedIconTellWorld = world;
|
||||
tab._cachedTellIcon = AutoTellTabTint.IconFor(name, world);
|
||||
}
|
||||
return tab._cachedTellIcon;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
|
||||
namespace HellionChat._Helpers;
|
||||
|
||||
// Pure-helper mirror of the compact pop-out history-navigation cursor
|
||||
// math. The original CompactCallback was tangled with ImGuiInputTextCallbackData
|
||||
// (DeleteChars/InsertChars), which can't be exercised in xUnit. The
|
||||
// ImGui buffer mutation stays at the call site; only the deterministic
|
||||
// cursor-and-replacement decision lives here.
|
||||
//
|
||||
// Index semantics match InputHistoryService:
|
||||
// index 0 = oldest entry
|
||||
// index Count - 1 = newest entry
|
||||
// cursor == -1 = "not browsing history"
|
||||
//
|
||||
// TEST-MIRROR: ../../../Hellion Build test/Ui/CompactInputHistoryNavigatorTests.cs
|
||||
public static class CompactInputHistoryNavigator
|
||||
{
|
||||
public enum Direction { Up, Down }
|
||||
|
||||
// replacement == null means: caller must NOT touch the buffer. This
|
||||
// distinguishes "cursor unchanged, leave the user's typing alone"
|
||||
// from "cursor moved to an empty slot, clear the buffer".
|
||||
public static (int cursor, string? replacement) Navigate(
|
||||
Direction direction,
|
||||
int currentCursor,
|
||||
string currentBuffer,
|
||||
Func<int> getCount,
|
||||
Action<string> push,
|
||||
Func<int, string?> getByCursor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(getCount);
|
||||
ArgumentNullException.ThrowIfNull(push);
|
||||
ArgumentNullException.ThrowIfNull(getByCursor);
|
||||
|
||||
var prev = currentCursor;
|
||||
var next = currentCursor;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.Up:
|
||||
if (currentCursor == -1)
|
||||
{
|
||||
// First Up press from a fresh buffer: stash whatever
|
||||
// the user typed so they can recover it after browsing.
|
||||
var offset = 0;
|
||||
if (!string.IsNullOrWhiteSpace(currentBuffer))
|
||||
{
|
||||
push(currentBuffer);
|
||||
offset = 1;
|
||||
}
|
||||
next = getCount() - 1 - offset;
|
||||
}
|
||||
else if (currentCursor > 0)
|
||||
{
|
||||
next--;
|
||||
}
|
||||
break;
|
||||
case Direction.Down:
|
||||
if (currentCursor != -1)
|
||||
{
|
||||
next++;
|
||||
if (next >= getCount())
|
||||
next = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (prev == next)
|
||||
return (next, null);
|
||||
|
||||
var replacement = getByCursor(next) ?? string.Empty;
|
||||
return (next, replacement);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using HellionChat.Ui;
|
||||
|
||||
namespace HellionChat._Helpers;
|
||||
|
||||
// Pure-helper mirror of the compact pop-out submit flow. ChatInputBar's
|
||||
// SubmitCompact used to inline this against a sealed ChatLogWindow, which
|
||||
// blocks Moq-based isolation. Lifting the deterministic part into a POCO
|
||||
// keeps the production call site a one-liner while letting xUnit assert
|
||||
// the buffer/cursor reset and the sender contract directly.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/Ui/CompactInputSubmitterTests.cs
|
||||
public static class CompactInputSubmitter
|
||||
{
|
||||
public static bool TrySubmit(InputState state, Tab tab, Action<Tab, string> sender)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
ArgumentNullException.ThrowIfNull(tab);
|
||||
ArgumentNullException.ThrowIfNull(sender);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(state.Buffer))
|
||||
return false;
|
||||
|
||||
var text = state.Buffer;
|
||||
state.Buffer = string.Empty;
|
||||
state.HistoryCursor = -1;
|
||||
sender(tab, text);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 45 KiB |
@@ -35,9 +35,19 @@ edits minimal, isolated to clearly-marked Hellion files, and reversible.
|
||||
|
||||
Concrete example: when API 15 hit, I cherry-picked your fix for the
|
||||
BetterTTV emote regression with `git cherry-pick -x` so authorship and
|
||||
co-author trail stay intact. That is the standard I want to keep using as
|
||||
long as both projects are alive. You should never have to look at this
|
||||
fork and wonder if I quietly ate your work.
|
||||
co-author trail stay intact. That was the standard I held to as long
|
||||
as cherry-picking was viable, and you should never have to look at
|
||||
this fork and wonder if I quietly ate your work.
|
||||
|
||||
With ChatTwo entering its rework cycle, the active cherry-pick
|
||||
pipeline is closed since v1.4.x — see [docs/UPSTREAM_SYNC.md](docs/UPSTREAM_SYNC.md)
|
||||
for the full reasoning. The attribution standard stays exactly the
|
||||
same: every existing `(cherry picked from commit ...)` line remains
|
||||
in the git history, the EUPL-1.2 anchor lines in source files are
|
||||
untouched, and this NOTICE.md remains canonical. If anything from
|
||||
this point forward originates from Chat 2 it will be a hand-port at
|
||||
most, called out as such in the commit message and source comments,
|
||||
not a `git cherry-pick`.
|
||||
|
||||
If anything in this fork ever steps on something you would not be okay
|
||||
with, please reach out and I will fix it. Genuinely. The list of contacts
|
||||
@@ -48,7 +58,7 @@ is below.
|
||||
If something in HellionChat causes problems, especially if it relates back
|
||||
to Chat 2 or to anything Infi or Anna would want flagged:
|
||||
|
||||
- **GitHub Issues:** [JonKazama-Hellion/HellionChat/issues](https://github.com/JonKazama-Hellion/HellionChat/issues)
|
||||
- **Gitea Issues:** [JonKazama-Hellion/HellionChat/issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues)
|
||||
- **Discord:** `@j.j_kazama`
|
||||
- **Email (business):** kontakt@hellion-media.de
|
||||
|
||||
@@ -62,8 +72,10 @@ full-history-by-default position fits a much larger one, including the
|
||||
roleplaying community where chat archive is part of the play experience.
|
||||
Trying to upstream HellionChat's defaults would have meant arguing that
|
||||
Chat 2's defaults are wrong, and they are not. They are right for the
|
||||
user base ChatTwo serves. So I keep the fork separate, attribute clearly,
|
||||
and pull selected upstream patches when they apply.
|
||||
user base ChatTwo serves. So I keep the fork separate and attribute
|
||||
clearly. Active cherry-picking from upstream stopped in the v1.4.x
|
||||
cycle once Chat 2's rework made selective patches no longer portable;
|
||||
the existing cherry-pick trail stays in the git history.
|
||||
|
||||
## Why HellionChat left the GitHub fork network
|
||||
|
||||
@@ -72,8 +84,9 @@ that a fork is either a development branch or a dead mirror. HellionChat
|
||||
is neither. It is an independently-maintained EUPL-1.2 fork with its own
|
||||
release cadence, its own custom repo, its own user base. Detaching the
|
||||
fork-network relation just makes the situation honest. The git history,
|
||||
the cherry-pick trail, and the attribution stay exactly the same. The
|
||||
only thing that changes is the GitHub UI no longer says "forked from".
|
||||
the existing cherry-pick trail, and the attribution stay exactly the
|
||||
same. The only thing that changes is the GitHub UI no longer says
|
||||
"forked from".
|
||||
|
||||
## Trademarks and naming
|
||||
|
||||
|
||||
@@ -121,10 +121,10 @@ Adjust the channel whitelist or set retention to a low value. Both take effect i
|
||||
| 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> |
|
||||
| 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> |
|
||||
| Hellion Forge (Gitea, self-hosted by Hellion Online Media) | Plugin distribution via custom repo, issue tracker | Whatever the Gitea instance sees from any HTTPS request to a public repo | <https://hellion-media.de/datenschutz> |
|
||||
| Dalamud / XIVLauncher (goatcorp) | Plugin loader, font subsystem, repo polling | Whatever Dalamud reports for itself; out of HellionChat's scope | <https://github.com/goatcorp/Dalamud> |
|
||||
|
||||
GitHub and the Dalamud/XIVLauncher loader are unavoidable for anyone playing FFXIV through Dalamud at all. BetterTTV is the only third party HellionChat introduces on top of that baseline, and it is opt-out via settings.
|
||||
The Hellion Forge Gitea instance and the Dalamud/XIVLauncher loader are unavoidable for anyone using HellionChat through Dalamud at all. BetterTTV is the only third party HellionChat introduces on top of that baseline, and it is opt-out via settings.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# Hellion Chat
|
||||
|
||||
[](https://github.com/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](https://github.com/JonKazama-Hellion/HellionChat/security/code-scanning)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://github.com/goatcorp/Dalamud)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://www.finalfantasyxiv.com/)
|
||||
@@ -12,13 +11,13 @@
|
||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||
</p>
|
||||
|
||||
**Version 1.4.1** — Privacy-First-Chat-Plugin für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||
**Version 1.4.3** — Privacy-First-Chat-Plugin für FINAL FANTASY XIV / Dalamud, basierend auf [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||
|
||||
Hellion Chat ist ein Privacy-First-Plugin auf dem Chat-2-Fundament. Der größte Teil der Engine kommt aus Chat 2 (Message-Store, Channel-Logik, Hook-System), die meisten Tastenkürzel funktionieren weiterhin wie gewohnt. Was sich ändert: schärfere Privacy-Defaults von Haus aus, eigene Slash-Commands unter `/hellionchat`, kein Webinterface mehr, und mit v1.1.0 eine Theme-Engine als Schritt in Richtung eigenes UI-Look-and-Feel.
|
||||
|
||||
Der Daten-Handling-Fokus liegt auf den DSGVO/EU-, US- und JP-Regelungen, soweit für ein Chat-Plugin praktisch umsetzbar: Speicherzeit pro Kanal, granulare Filter, Selbstauskunft per Export. Eine ausführliche Auflistung steht in [`PRIVACY.md`](PRIVACY.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).
|
||||
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. Aktiver Upstream-Sync ist mit dem v1.4.x-Cycle beendet: Chat 2 befindet sich in einem grundlegenden Rework und Cherry-Picks sind nicht mehr portierbar. Hellion Chat geht ab da als unabhängige Codebase weiter, Hintergrund und Attribution in [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -170,7 +169,7 @@ Hellion Chat wird über ein Dalamud-**Custom-Repository** verteilt.
|
||||
1. Dalamud-Settings (`/xlsettings`) → **Experimental** öffnen.
|
||||
2. Neuen Eintrag unter **Custom Plugin Repositories** anlegen:
|
||||
```
|
||||
https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json
|
||||
https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/repo.json
|
||||
```
|
||||
3. **Save**, dann in `/xlplugins` → **All Plugins** → Refresh.
|
||||
4. Hellion Chat taucht in der Liste auf, dann installieren wie jedes andere Plugin.
|
||||
@@ -225,7 +224,7 @@ Eine optionale Submission ans Dalamud-Main-Plugin-Repo (zusätzlich zum eigenen
|
||||
|
||||
## Projektstatus
|
||||
|
||||
**Version 1.4.1** — Theme Engine Performance: ABGR-Cache auf den Theme-Records pre-computed, HellionStyle.PushGlobal liest aus dem Cache statt pro Slot pro Frame zu konvertieren (~13 % Render-Time-Recovery im Smoke-Test). Custom-Theme-Hot-Reload überlebt transient File-Locks via Last-Known-Good-Snapshot. Plus: Synthwave Sunset als zehnter Built-In, Author-Credits konsolidiert. Zweiter Sub-Patch der v1.4.x Polish-Sweep-Serie (Stand: 2026-05-07).
|
||||
**Version 1.4.3** — Plugin-Load Async-Init plus Repo-Cutover: Plugin auf Dalamud's IAsyncDalamudPlugin-API migriert. Der Konstruktor übernimmt nur noch Bootstrap-Essentials (Config-Load, Language-Init, Conflict-Detection); Migrationen, Service-Allokationen, Window-Konstruktion und Hook-Subscription wandern in LoadAsync, sodass Dalamud die UI während der schweren Arbeit responsive halten kann. Schema-Gate ersetzt die v9 → v16 Migrations-Kette; Configs auf Schema v16+ laden direkt, ältere Configs triggern eine "install v1.4.2 first"-Fehlermeldung. Custom-Repo-URL auf `gitea.hellion-forge.cloud` migriert; das GitHub-Repo bleibt als eingefrorener v1.4.2-Snapshot stehen. Plugin-Load-Zeit liegt bei ~3.7 s Median (5 Reloads), vergleichbar mit v1.4.2: Async-Migration ist Foundation für v1.4.4 Lazy-Init-Optimierungen, kein direkter User-spürbarer Win. Vierter Sub-Patch der v1.4.x Polish-Sweep-Serie (Stand: 2026-05-08).
|
||||
|
||||
Hellion Chat ist ein eigenständiges Plugin, kein Fork mehr im Repository-Sinne. Vollständig abgeschlossen:
|
||||
|
||||
@@ -244,7 +243,7 @@ Hellion Chat ist ein eigenständiges Plugin, kein Fork mehr im Repository-Sinne.
|
||||
- Theme-Engine mit zehn eingebauten Themes plus JSON-Authoring-Format (Engine v1.1.0, Katalog erweitert in v1.2.3, inkl. CVD-safe Hellion Spectrum; Synthwave Sunset in v1.4.1)
|
||||
- ABGR-Cache auf den Theme-Records: HellionStyle.PushGlobal liest pre-computed ABGR statt RGBA→ABGR pro Slot pro Frame (v1.4.1, ~13 % Render-Time-Recovery)
|
||||
|
||||
In Arbeit: schrittweise Modernisierung des UI-Look-and-Feel über die Theme-Engine hinaus. 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.
|
||||
In Arbeit: schrittweise Modernisierung des UI-Look-and-Feel über die Theme-Engine hinaus. 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 [Gitea-Issue-Tracker](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues) mit dem `roadmap`-Label geführt.
|
||||
|
||||
### Zur Release-Kadenz
|
||||
|
||||
@@ -255,7 +254,7 @@ Wer den Repo zum ersten Mal sieht, bemerkt schnell viele Releases und sehr viele
|
||||
## Community und Support
|
||||
|
||||
- **Hellion Forge Discord** (Modding- und Plugin-Community von Hellion Online Media): https://discord.gg/X9V7Kcv5gR
|
||||
- Bug-Reports und Feature-Requests: [GitHub Issues](https://github.com/JonKazama-Hellion/HellionChat/issues)
|
||||
- Bug-Reports und Feature-Requests: [Gitea Issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues)
|
||||
- Discord DM: `@j.j_kazama`
|
||||
- Weitere Kontaktwege (Security, Privacy, Quick-Questions): siehe [SUPPORT.md](SUPPORT.md)
|
||||
|
||||
@@ -311,7 +310,7 @@ Im Repo-Root liegen die Standard-Repository-Dokumente, vertiefende Dokumentation
|
||||
| [`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- und Layout-Slots, Channel-Identity-Regeln, Validierung. |
|
||||
| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Cherry-Pick-Policy gegenüber Chat 2. |
|
||||
| [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md) | Upstream-Sync-Stand: Cherry-Pick-Pipeline seit v1.4.x geschlossen, Attribution intakt. |
|
||||
| [`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. |
|
||||
|
||||
|
||||
@@ -3,24 +3,20 @@
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you find a security issue in HellionChat, please do not open a
|
||||
public GitHub issue. Use one of the private channels below so I can
|
||||
public Gitea issue. Use one of the private channels below so I can
|
||||
investigate and ship a fix before the details go public.
|
||||
|
||||
**Preferred:**
|
||||
[Privately report a vulnerability](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
|
||||
via GitHub Security Advisories. This routes the report directly to me
|
||||
and keeps the conversation off the public timeline.
|
||||
|
||||
**Alternative:**
|
||||
|
||||
| Channel | Address |
|
||||
| ---------- | -------------------------- |
|
||||
| Email | `kontakt@hellion-media.de` |
|
||||
| Discord DM | `@j.j_kazama` |
|
||||
|
||||
I respond on weekdays during European business hours. For urgent
|
||||
disclosures (active exploitation, user-data exposure) email is the
|
||||
fastest path.
|
||||
For urgent disclosures (active exploitation, user-data exposure) email
|
||||
is the fastest path.
|
||||
|
||||
I respond on weekdays during European business hours.
|
||||
|
||||
## Scope
|
||||
|
||||
|
||||
@@ -4,19 +4,19 @@ HellionChat is a small hobby project maintained by one person. There are a few d
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
GitHub issues, using the templates:
|
||||
Gitea issues, using the templates:
|
||||
|
||||
- [Bug report](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=bug_report.yml)
|
||||
- [Feature request](https://github.com/JonKazama-Hellion/HellionChat/issues/new?template=feature_request.yml)
|
||||
- [Bug report](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues/new?template=bug_report.yml)
|
||||
- [Feature request](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues/new?template=feature_request.yml)
|
||||
|
||||
Please search [existing issues](https://github.com/JonKazama-Hellion/HellionChat/issues?q=is%3Aissue) first. Duplicates get closed and pointed at the original.
|
||||
Please search [existing issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues?type=issue) first. Duplicates get closed and pointed at the original.
|
||||
|
||||
## Security
|
||||
|
||||
Do **not** open a public issue for security-relevant findings. Use the private advisory route described in [SECURITY.md](SECURITY.md):
|
||||
|
||||
- [Private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
|
||||
- Email `kontakt@hellion-media.de`
|
||||
- Email `kontakt@hellion-media.de` (preferred for security reports)
|
||||
- Discord DM `@j.j_kazama` for time-sensitive findings
|
||||
|
||||
## Privacy questions
|
||||
|
||||
|
||||
@@ -82,4 +82,4 @@ Both are good projects. Use what fits you best.
|
||||
## Contact
|
||||
|
||||
Questions about this disclosure:
|
||||
<https://github.com/JonKazama-Hellion/HellionChat/issues>
|
||||
<https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues>
|
||||
|
||||
@@ -5,13 +5,80 @@ 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)
|
||||
[Gitea-Release](https://gitea.hellion-forge.cloud/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.
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.4.3 — Plugin-Load Async-Init + Repo-Cutover (2026-05-08)
|
||||
|
||||
Plugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin`
|
||||
API. The constructor now does only the bootstrap-essentials
|
||||
(config load, language init, conflict detection); migrations,
|
||||
service allocations, window construction and hook subscription
|
||||
move to `LoadAsync`. Dalamud can keep its UI responsive while
|
||||
the heavy work runs.
|
||||
|
||||
- `IAsyncDalamudPlugin` two-phase load with per-line
|
||||
`CaptureFailure` in `DisposeAsync` (mirrors LightlessSync's
|
||||
pattern); idempotency guard protects against reload races
|
||||
- Schema-gate replaces the v9 → v16 migration chain. Configs
|
||||
on schema v16+ load directly; older configs trigger an
|
||||
"install v1.4.2 first" error so the historic migration
|
||||
path stays intact
|
||||
- `AutoTranslate.PreloadCache` moved off the load path. First
|
||||
use may have a sub-second hitch instead of every-load; the
|
||||
upstream chose differently, we accept first-use latency
|
||||
- `FontManager.BuildFonts` is called sync at the start of
|
||||
`LoadAsync`; Dalamud rebuilds the font atlas on its own
|
||||
pipeline so the custom Hellion-Exo2 font appears with a
|
||||
brief font-pop after load (matches ChatTwo's behaviour)
|
||||
- Custom-repo URL moved to
|
||||
`gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat`.
|
||||
GitHub repo stays as a frozen v1.4.2 snapshot; new
|
||||
releases ship from Gitea. Existing testers need to
|
||||
update the custom-repo URL once
|
||||
- Plugin-load time in this release sits at ~3.7 s median
|
||||
(5 reloads), comparable to v1.4.2. Async migration is
|
||||
foundational for v1.4.4 Lazy-Init optimisations rather
|
||||
than an immediate user-perceived win
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.4.2 — ChatLog Frame-Hot-Path
|
||||
|
||||
Third sub-patch of the v1.4.x Polish Sweep series. Per-frame
|
||||
allocations from the chat-log render path eliminated.
|
||||
|
||||
- `DrawMessages` card-mode hoists `theme`/`drawList`/`winLeft`/
|
||||
`winRight`/`borderColorAbgr` out of the per-message loop. About
|
||||
500 redundant calls per frame at 100 visible messages, multiplied
|
||||
by every pop-out window
|
||||
- Auto-tell tab tint and icon use a per-tab cache. Hash
|
||||
computation and string allocation only happen when the tell
|
||||
target name or world drifts. `AutoTellTabTint` stays a pure
|
||||
hash helper; cache lives in a thin `TabTintCache` wrapper
|
||||
- Status bar gates its tab aggregation behind the same
|
||||
one-second cache it already used for the format strings.
|
||||
LINQ `Sum` and `Count` replaced with a single `foreach` pass
|
||||
that runs on roughly 1 % of frames
|
||||
|
||||
Realistic frame-time recovery: 2-5 % in typical scenes, more
|
||||
on pop-out-heavy setups because the card-border hoist scales
|
||||
per window.
|
||||
|
||||
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.4.1 — Theme Engine Performance
|
||||
|
||||
Second sub-patch of the v1.4.x Polish Sweep series. Heap
|
||||
@@ -251,7 +318,7 @@ Vier kleine Polish-Items aus dem Backlog gebündelt:
|
||||
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)
|
||||
[Release-Notes 1.0.3](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.0.3)
|
||||
|
||||
## [1.0.1] — 2026-05-04 — Window Position Recovery
|
||||
|
||||
@@ -267,7 +334,7 @@ Bundled housekeeping since v1.0.0: documentation restructured into
|
||||
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)
|
||||
[Release-Notes 1.0.1](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.0.1)
|
||||
|
||||
## [1.0.0] — 2026-05-03 — Standalone Major Release
|
||||
|
||||
@@ -280,7 +347,7 @@ 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)
|
||||
[Release-Notes 1.0.0](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v1.0.0)
|
||||
|
||||
## [0.6.1] — 2026-05-03 — Pop-Out Discoverability & /tell Auto-Pop-Out
|
||||
|
||||
@@ -290,7 +357,7 @@ 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)
|
||||
[Release-Notes 0.6.1](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.6.1)
|
||||
|
||||
## [0.6.0] — 2026-05-03 — UX Polish: Pop-Out Input + Colour Presets
|
||||
|
||||
@@ -300,7 +367,7 @@ 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)
|
||||
[Release-Notes 0.6.0](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.6.0)
|
||||
|
||||
## [0.5.4] — 2026-05-02 — WrapText Hardening
|
||||
|
||||
@@ -310,7 +377,7 @@ 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)
|
||||
[Release-Notes 0.5.4](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.5.4)
|
||||
|
||||
## [0.5.3] — 2026-05-02 — Pointer Arithmetic Hardening
|
||||
|
||||
@@ -318,7 +385,7 @@ 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)
|
||||
[Release-Notes 0.5.3](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/v0.5.3)
|
||||
|
||||
---
|
||||
|
||||
@@ -327,7 +394,7 @@ Pointer-Arithmetik via `GetByteCount` validiert.
|
||||
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)
|
||||
[Alle Releases](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Die Upstream-Sprach-Dateien (`Language.<lang>.resx`) sind nicht Teil dieser Date
|
||||
|
||||
## 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).
|
||||
Bug-Reports, Feature-Wünsche und Pull-Requests laufen über [Gitea Issues](https://gitea.hellion-forge.cloud/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.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
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)
|
||||
[Gitea Issues](https://gitea.hellion-forge.cloud/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
|
||||
@@ -12,13 +12,43 @@ Privacy-First-Schnittmenge des Plugins erweisen.
|
||||
|
||||
---
|
||||
|
||||
## Nächster Cycle (v1.4.2)
|
||||
## Nächster Cycle (v1.4.4)
|
||||
|
||||
**ChatLog Frame-Hot-Path** — Card-Mode Border-Loop in
|
||||
ChatLogWindow vom Per-Message GetWindowDrawList befreien,
|
||||
AutoTellTabTint Per-Tab-Cache mit Tell-Target-Invalidierung,
|
||||
StatusBar LINQ Sum/Count durch klassische for-Loops mit
|
||||
TickCount64-Cache-Gate ersetzen. Direkter FPS-Impact.
|
||||
**Window-Lazy-Open + Render-Init-Cost-Optimisation** — die in v1.4.3
|
||||
gelegte IAsyncDalamudPlugin-Foundation jetzt für die echten User-
|
||||
spürbaren Wins nutzen. Window-Konstruktion erst beim ersten Open,
|
||||
Render-Path-Init-Kosten in den ersten Frames runter. Konkrete
|
||||
Kandidaten und Größenschätzungen werden im v1.4.4-Brainstorm
|
||||
konsolidiert.
|
||||
|
||||
## v1.4.3 — Plugin-Load Async-Init + Repo-Cutover (released 2026-05-08)
|
||||
|
||||
Vierter und größter Sub-Patch der v1.4.x Polish-Sweep-Serie. Plugin
|
||||
auf Dalamud's IAsyncDalamudPlugin-API migriert: der Konstruktor
|
||||
übernimmt nur noch Bootstrap-Essentials (Config-Load, Language-Init,
|
||||
Conflict-Detection), Migrationen, Service-Allokationen, Window-
|
||||
Konstruktion und Hook-Subscription wandern in LoadAsync. Schema-
|
||||
Gate ersetzt die v9 → v16 Migrations-Kette; Configs auf Schema
|
||||
v16+ laden direkt, ältere Configs triggern eine "install v1.4.2
|
||||
first"-Fehlermeldung. AutoTranslate.PreloadCache vom Load-Pfad
|
||||
runter. FontManager.BuildFonts läuft sync am Start von LoadAsync,
|
||||
Dalamud baut den Font-Atlas auf seiner eigenen Pipeline.
|
||||
Custom-Repo-URL auf `gitea.hellion-forge.cloud` cut-over, das
|
||||
GitHub-Repo bleibt als eingefrorener v1.4.2-Snapshot stehen.
|
||||
Plugin-Load-Zeit liegt bei ~3.7 s Median (5 Reloads), vergleichbar
|
||||
mit v1.4.2: Async-Migration ist Foundation für v1.4.4 Lazy-Init-
|
||||
Optimierungen, kein direkter User-spürbarer Win.
|
||||
|
||||
## v1.4.2 — ChatLog Frame-Hot-Path (released <Datum>)
|
||||
|
||||
Dritter Sub-Patch der v1.4.x Polish-Sweep-Serie. Per-Frame-
|
||||
Allokationen aus dem ChatLogWindow-Render-Pfad und der
|
||||
Settings-StatusBar eliminiert. Card-Mode-Border-Loop in
|
||||
DrawMessages hebt fünf Invarianten in einen Pre-Loop-Hoist,
|
||||
AutoTellTabTint bekommt einen Per-Tab-Cache via TabTintCache
|
||||
(separate Validation-Keys pro Cache, kein Cross-Invalidation),
|
||||
StatusBar zieht den Cache-Gate-Check vor die Aggregations
|
||||
und ersetzt LINQ Sum+Count durch eine Single-Pass-Foreach.
|
||||
|
||||
## v1.4.1 — Theme Engine Performance (released <Datum>)
|
||||
|
||||
@@ -193,6 +223,7 @@ aktuellen Stand getestet.
|
||||
|
||||
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).
|
||||
Inspiration, kein Code-Port. Code-Imports aus dem Upstream-Bestand
|
||||
sind seit v1.4.x abgeschlossen, weil Chat 2 in einem grundlegenden
|
||||
Rework ist und selektive Patches nicht mehr sauber portierbar sind.
|
||||
Stand und Begründung in [`UPSTREAM_SYNC.md`](UPSTREAM_SYNC.md).
|
||||
|
||||
@@ -51,8 +51,9 @@ HellionChat is a fork of [Chat 2](https://github.com/Infiziert90/ChatTwo)
|
||||
by Infiziert90 (Infi) and Anna Clemens, also licensed under EUPL-1.2.
|
||||
The bulk of the code, including the message store architecture, the
|
||||
channel logic, the hook system and the ImGui chat window, originates
|
||||
from upstream. See `../NOTICE.md` and `UPSTREAM_SYNC.md` for the
|
||||
attribution and the cherry-pick policy.
|
||||
from upstream. See `../NOTICE.md` for the attribution; `UPSTREAM_SYNC.md`
|
||||
documents the upstream-sync history, including the close of active
|
||||
cherry-picking in the v1.4.x cycle.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
HellionChat is a standalone EUPL-1.2 plugin that originated from
|
||||
[Chat 2](https://github.com/Infiziert90/ChatTwo). Since v1.0.0 it
|
||||
lives under its own namespace, IPC channels and source tree. I no
|
||||
longer track upstream as a Git fork, but I do monitor Chat 2 commits
|
||||
regularly and cherry-pick selectively where it makes sense.
|
||||
lives under its own namespace, IPC channels and source tree. The
|
||||
active cherry-pick pipeline from upstream Chat 2 is closed since
|
||||
the v1.4.x cycle.
|
||||
|
||||
This document covers how that works so anyone (including future-me)
|
||||
can do it cleanly.
|
||||
This document covers what that means, why I closed it, and what
|
||||
stays in place.
|
||||
|
||||
## A Word on Intent
|
||||
|
||||
@@ -28,99 +28,77 @@ new UI from scratch and making deliberate architectural decisions that
|
||||
pull in a different direction. Some upstream patches will simply stop
|
||||
applying cleanly and that is expected.
|
||||
|
||||
## One-Time Setup
|
||||
## Why Cherry-Picking Stopped in v1.4.x
|
||||
|
||||
Add the upstream repo as a remote on a fresh clone:
|
||||
Two things converged:
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/Infiziert90/ChatTwo.git
|
||||
git fetch upstream
|
||||
```
|
||||
1. **Chat 2 is in a rework cycle.** Infi mentioned directly that
|
||||
parts of ChatTwo are being reworked and "stuff may not be able to
|
||||
be cherry picked anymore." Once the upstream code paths I would
|
||||
pull from no longer exist in the same shape, `git cherry-pick`
|
||||
stops being a meaningful tool — what would land would not be the
|
||||
change Infi wrote, it would be a hand-port of his concept.
|
||||
2. **HellionChat has drifted enough that selective patches require
|
||||
adaptation anyway.** The UI is being rebuilt, the theme engine
|
||||
sits on top of HellionStyle which has no upstream equivalent, the
|
||||
privacy filter changes how messages flow through MessageManager.
|
||||
Even before the rework was announced, more and more upstream
|
||||
patches needed adaptation rather than a clean apply.
|
||||
|
||||
Verify both remotes are wired up:
|
||||
Together those two points mean continuing to call this an "active
|
||||
cherry-pick pipeline" was no longer honest. So I closed it.
|
||||
|
||||
```bash
|
||||
git remote -v
|
||||
# origin https://github.com/JonKazama-Hellion/HellionChat.git (fetch)
|
||||
# origin https://github.com/JonKazama-Hellion/HellionChat.git (push)
|
||||
# upstream https://github.com/Infiziert90/ChatTwo.git (fetch)
|
||||
# upstream https://github.com/Infiziert90/ChatTwo.git (push)
|
||||
```
|
||||
## What Closing the Pipeline Means in Practice
|
||||
|
||||
`upstream` is read-only. Never push to it.
|
||||
- The `upstream` git remote was removed locally on 2026-05-08.
|
||||
Anyone setting up a fresh clone does **not** add it back.
|
||||
- New commits will not carry `(cherry picked from commit ...)`
|
||||
trailers. Anything that originates from Chat 2 from this point
|
||||
forward will be a hand-port at most, and it gets called out as
|
||||
such in its own commit message and in the relevant source comments.
|
||||
- The existing cherry-pick trail stays in the git history exactly as
|
||||
it is. Every `(cherry picked from commit ...)` line that was added
|
||||
with `-x` in earlier releases remains intact; that is the
|
||||
attribution paper trail and removing it would be wrong.
|
||||
|
||||
## Reviewing What Is New Upstream
|
||||
## What Does Not Change
|
||||
|
||||
Before any feature cycle I run a quick check:
|
||||
- **EUPL-1.2 anchor lines in source files.** Files that originated
|
||||
from Chat 2 keep their licence headers and any "based on
|
||||
Infiziert90/ChatTwo" notice exactly as they are. The licence
|
||||
obligations under EUPL-1.2 do not lapse because cherry-picking
|
||||
stopped.
|
||||
- **NOTICE.md** stays canonical. Attribution to Infi and Anna for the
|
||||
message store, channel logic, hook system, ImGui chat window and
|
||||
the localisation infrastructure remains the foundation statement of
|
||||
this fork.
|
||||
- **README acknowledgements.** The Acknowledgements section in
|
||||
`README.md`, the maintainer thanks in the About tab, and the
|
||||
`Language.*.resx` Crowdin translator credit list all stay as they
|
||||
are.
|
||||
- **The original `Language.*.resx` files** remain in the source tree
|
||||
in their last upstream-sync state. They are the work of the Chat 2
|
||||
Crowdin community and the existing translations stay valuable. They
|
||||
will not receive automatic upstream updates anymore — see
|
||||
CONTRIBUTING.md for what that means for translators.
|
||||
|
||||
```bash
|
||||
git fetch upstream
|
||||
git log --oneline main..upstream/main | head -30
|
||||
```
|
||||
## What Could Re-Open Later
|
||||
|
||||
That shows every commit Infi or contributors landed since the last
|
||||
sync. I read the messages and decide which ones apply to HellionChat.
|
||||
If Chat 2's rework lands and stabilises, and there is a piece of
|
||||
upstream code that I genuinely want in HellionChat, the path forward
|
||||
is **study and re-implement**, not cherry-pick. That means:
|
||||
|
||||
## What I Cherry-Pick
|
||||
- Read the upstream change, understand the design, port the concept
|
||||
to HellionChat's actual code paths.
|
||||
- Credit the upstream author in the commit message and, if the
|
||||
ported code is non-trivial, in a source-file comment.
|
||||
- Pre-clear with Infi if the port is large enough to warrant a
|
||||
conversation.
|
||||
|
||||
**Always:** security fixes, Dalamud API compatibility patches,
|
||||
BetterTTV and emote-cache fixes, regression fixes for upstream
|
||||
behaviour HellionChat still relies on.
|
||||
|
||||
**Sometimes:** small bug fixes in `MessageManager.cs`,
|
||||
`MessageStore.cs`, `ChatLogWindow.cs`, the Tabs system. These come in
|
||||
when they touch code I have not heavily modified.
|
||||
|
||||
**Never:** webinterface changes (the entire webinterface tree is gone
|
||||
in HellionChat), changes that conflict with the privacy filter, changes
|
||||
that re-add upstream defaults I deliberately reversed (full-history
|
||||
logging, Tell Exclusive defaults, etc.).
|
||||
|
||||
As HellionChat's UI moves further from the Chat 2 baseline, upstream
|
||||
patches will increasingly require adaptation rather than a clean
|
||||
apply. If a patch cannot be ported without breaking HellionChat
|
||||
behaviour or the privacy model, I skip it rather than force a
|
||||
compromised version in.
|
||||
|
||||
## How I Cherry-Pick
|
||||
|
||||
Always with `-x` so authorship and the original commit hash stay
|
||||
visible:
|
||||
|
||||
```bash
|
||||
git checkout -b sync/upstream-<topic> main
|
||||
git cherry-pick -x <upstream-commit-sha>
|
||||
```
|
||||
|
||||
`-x` appends a `(cherry picked from commit <sha>)` line to the commit
|
||||
message. That preserves upstream-author credit and lets anyone reading
|
||||
`git log` trace the change back to Chat 2. Commit messages stay
|
||||
identical to the upstream original; I do not rewrite them to match the
|
||||
HellionChat format.
|
||||
|
||||
## Conflict Handling
|
||||
|
||||
When a cherry-pick conflicts:
|
||||
|
||||
1. Resolve by hand. Do not rewrite upstream code to match HellionChat
|
||||
conventions; that is what the merge marker showed.
|
||||
2. If the conflict is fundamental (touches code that no longer exists
|
||||
in HellionChat), abort the cherry-pick and note why in the
|
||||
relevant GitHub issue or backlog item. Some upstream patches are
|
||||
simply not portable and that is fine.
|
||||
3. After a clean resolve the commit message stays as-is, with the
|
||||
`-x` footer Git appends automatically.
|
||||
|
||||
## Pushing the Sync
|
||||
|
||||
Cherry-picked commits go through the same review as any other change.
|
||||
The sync branch lands in `main` via a no-fast-forward merge, then gets
|
||||
a release tag if user-visible behaviour changed:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git merge --no-ff sync/upstream-<topic> -m "merge: upstream sync — <topic>"
|
||||
```
|
||||
This is heavier than `git cherry-pick -x` and that is the point.
|
||||
Cherry-picking was light because both codebases shared structure;
|
||||
once they do not, the proper attribution costs a real conversation
|
||||
rather than a flag on a git command.
|
||||
|
||||
## Contributing Back
|
||||
|
||||
@@ -138,17 +116,12 @@ A few things to note about that process:
|
||||
not push that decision onto his codebase.
|
||||
- This is not guaranteed for every change, only where it makes sense
|
||||
and where I am confident the fix is clean and self-contained.
|
||||
|
||||
## When Upstream Goes Silent
|
||||
|
||||
If Chat 2 stops receiving updates the remote stays configured and this
|
||||
workflow stays documented. The moment maintenance picks back up I am
|
||||
ready to pull again.
|
||||
- Whether it gets accepted is Infi's call, and a "no" is fine.
|
||||
|
||||
## When Upstream Takes a Direction I Cannot Follow
|
||||
|
||||
If a future Chat 2 release breaks compatibility with the HellionChat
|
||||
privacy philosophy in a way that cannot be resolved (mandatory cloud
|
||||
sync, removal of the local message store, an incompatible license
|
||||
change), HellionChat continues from the last compatible cherry-pick.
|
||||
The inherited history stays under EUPL-1.2 and stays attributed.
|
||||
sync, removal of the local message store, an incompatible licence
|
||||
change), HellionChat continues from where it is. The inherited
|
||||
history stays under EUPL-1.2 and stays attributed.
|
||||
|
||||
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":dependencyDashboard",
|
||||
":semanticCommits",
|
||||
":timezone(Europe/Berlin)",
|
||||
"schedule:weekly"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies",
|
||||
"renovate"
|
||||
],
|
||||
"assignees": [
|
||||
"JonKazama-Hellion"
|
||||
],
|
||||
"prHourlyLimit": 10,
|
||||
"prConcurrentLimit": 20,
|
||||
"rebaseWhen": "behind-base-branch",
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Group all minor and patch updates per ecosystem in one PR",
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
],
|
||||
"groupName": "minor and patch updates ({{manager}})"
|
||||
},
|
||||
{
|
||||
"description": "Major updates always get their own PR with breaking-change label",
|
||||
"matchUpdateTypes": [
|
||||
"major"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies",
|
||||
"major-update",
|
||||
"breaking-change"
|
||||
],
|
||||
"addLabels": [
|
||||
"needs-review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "TypeScript type definitions stay grouped with each other",
|
||||
"matchPackagePrefixes": [
|
||||
"@types/"
|
||||
],
|
||||
"groupName": "type definitions"
|
||||
},
|
||||
{
|
||||
"description": "Dev dependencies in their own group",
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"groupName": "dev dependencies"
|
||||
},
|
||||
{
|
||||
"description": "Pin GitHub Action versions by SHA for supply-chain hygiene",
|
||||
"matchManagers": [
|
||||
"github-actions"
|
||||
],
|
||||
"pinDigests": true
|
||||
}
|
||||
],
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": [
|
||||
"security",
|
||||
"vulnerability"
|
||||
],
|
||||
"schedule": [
|
||||
"at any time"
|
||||
],
|
||||
"prPriority": 10
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": [
|
||||
"before 6am on monday"
|
||||
],
|
||||
"commitMessageAction": "Refresh"
|
||||
},
|
||||
"osvVulnerabilityAlerts": true
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# preflight.sh — pre-push gate. Blocks A/B/C verify config drift; Block D is a
|
||||
# headless `dotnet build` to catch compile-time API drift. Test execution lives
|
||||
# in the local Build-Suite repo and is NOT part of this preflight.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
echo "==> preflight: Block A — version consistency"
|
||||
./scripts/verify-version-consistency.sh
|
||||
|
||||
echo "==> preflight: Block B — manifest shape"
|
||||
./scripts/verify-manifest-shape.sh
|
||||
|
||||
echo "==> preflight: Block C — changelog sync"
|
||||
./scripts/verify-changelog-sync.sh
|
||||
|
||||
echo "==> preflight: Block D — plugin compile health"
|
||||
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
|
||||
|
||||
echo "==> preflight: ALL GREEN"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# setup-hooks.sh — installs pre-push hook via core.hooksPath. Idempotent.
|
||||
# Note: NO pre-commit hook — test execution is local-only in the Build-Suite repo,
|
||||
# so the plugin repo's pre-commit cannot run tests. Versions/manifest/changelog
|
||||
# checks happen on pre-push only.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
git config core.hooksPath .githooks
|
||||
chmod +x .githooks/pre-push
|
||||
echo "setup-hooks: core.hooksPath set to .githooks. pre-push live."
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-changelog-sync.sh — Block C.
|
||||
# Strips .0 suffix from repo.json AssemblyVersion to derive the 3-digit tag/version.
|
||||
# yaml.changelog is a single multi-line block with **Hellion Chat X.Y.Z** subblocks.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
YAML="$ROOT/HellionChat/HellionChat.yaml"
|
||||
REPO_JSON="$ROOT/repo.json"
|
||||
FORGE_DIR="$ROOT/.github/forge-posts"
|
||||
|
||||
fail() { echo "verify-changelog-sync: FAIL — $1" >&2; exit 1; }
|
||||
ok() { echo "verify-changelog-sync: OK — $1"; }
|
||||
|
||||
VER="$(jq -r '.[0].AssemblyVersion' "$REPO_JSON" | sed -E 's/\.0$//')"
|
||||
TAG="v$VER"
|
||||
|
||||
grep -qE "^[[:space:]]*\*\*Hellion Chat ${VER}" "$YAML" \
|
||||
|| fail "$YAML changelog missing **Hellion Chat ${VER}** subblock. Fix: add the v${VER} block at the top of the changelog field."
|
||||
|
||||
jq -r '.[0].Changelog' "$REPO_JSON" | grep -qE "^[[:space:]]*\*\*Hellion Chat ${VER}" \
|
||||
|| fail "$REPO_JSON Changelog missing **Hellion Chat ${VER}** subblock. Fix: copy the yaml changelog over."
|
||||
|
||||
FORGE_FILE="$FORGE_DIR/${TAG}.md"
|
||||
[ -f "$FORGE_FILE" ] || fail "$FORGE_FILE missing. Fix: create from previous tag's file as template."
|
||||
|
||||
SUBTITLE="$(awk '/^---$/{f=!f; next} f && /^subtitle:/' "$FORGE_FILE" | sed -E 's/^subtitle:[[:space:]]*//' | tr -d '\"')"
|
||||
[ -n "$SUBTITLE" ] || fail "$FORGE_FILE frontmatter missing 'subtitle'."
|
||||
[ "${#SUBTITLE}" -le 60 ] || fail "$FORGE_FILE subtitle is ${#SUBTITLE} chars (max 60)."
|
||||
|
||||
NATUR="$(awk '/^---$/{f=!f; next} f && /^versionsnatur:/' "$FORGE_FILE" | sed -E 's/^versionsnatur:[[:space:]]*//' | tr -d '\"')"
|
||||
[ -n "$NATUR" ] || fail "$FORGE_FILE frontmatter missing 'versionsnatur'."
|
||||
[ "${#NATUR}" -le 40 ] || fail "$FORGE_FILE versionsnatur is ${#NATUR} chars (max 40)."
|
||||
|
||||
BODY="$(awk '/^---$/{f++; next} f==2' "$FORGE_FILE")"
|
||||
TITLE_LEN=$((${#VER} + 16 + ${#SUBTITLE}))
|
||||
FOOTER_LEN=80
|
||||
TOTAL=$((TITLE_LEN + ${#BODY} + FOOTER_LEN))
|
||||
[ "$TOTAL" -le 5500 ] || fail "Forge embed total ~${TOTAL} chars > 5500 cap."
|
||||
|
||||
YAML_VERSIONS="$(grep -cE '^[[:space:]]*\*\*Hellion Chat' "$YAML" || true)"
|
||||
[ "$YAML_VERSIONS" -le 4 ] || fail "$YAML changelog has $YAML_VERSIONS version subblocks (max 4). Fix: move oldest to docs/CHANGELOG.md."
|
||||
|
||||
ok "yaml/repo.json/forge-post in sync for $TAG, embed-total ~${TOTAL}/5500, $YAML_VERSIONS/4 subblocks"
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-manifest-shape.sh — Block B. Required fields in lowercase yaml,
|
||||
# DalamudApiLevel sanity in repo.json, no own DalamudPackager.targets,
|
||||
# Icon AND every ImageUrl reachable.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
YAML="$ROOT/HellionChat/HellionChat.yaml"
|
||||
REPO_JSON="$ROOT/repo.json"
|
||||
TARGETS_OWN="$ROOT/HellionChat/DalamudPackager.targets"
|
||||
|
||||
fail() { echo "verify-manifest-shape: FAIL — $1" >&2; exit 1; }
|
||||
ok() { echo "verify-manifest-shape: OK — $1"; }
|
||||
|
||||
for FIELD in name author punchline description repo_url icon_url image_urls tags changelog; do
|
||||
grep -qE "^${FIELD}:" "$YAML" || fail "$YAML missing required field: $FIELD"
|
||||
done
|
||||
|
||||
[ -f "$TARGETS_OWN" ] && fail "Own DalamudPackager.targets at $TARGETS_OWN strips Icon/ImageUrls. DELETE it; SDK default works."
|
||||
|
||||
API_LEVEL="$(jq -r '.[0].DalamudApiLevel' "$REPO_JSON")"
|
||||
case "$API_LEVEL" in
|
||||
''|null) fail "$REPO_JSON missing DalamudApiLevel" ;;
|
||||
esac
|
||||
[[ "$API_LEVEL" =~ ^[0-9]+$ ]] || fail "$REPO_JSON DalamudApiLevel must be integer, got: $API_LEVEL"
|
||||
[ "$API_LEVEL" -ge 12 ] || fail "$REPO_JSON DalamudApiLevel=$API_LEVEL is below SDK 15 floor (12)."
|
||||
|
||||
if [ "${HOOKS_OFFLINE:-0}" != "1" ]; then
|
||||
ICON_URL="$(jq -r '.[0].IconUrl' "$REPO_JSON")"
|
||||
curl -fsI "$ICON_URL" > /dev/null || fail "IconUrl unreachable: $ICON_URL"
|
||||
ok "IconUrl reachable: $ICON_URL"
|
||||
|
||||
COUNT="$(jq -r '.[0].ImageUrls | length' "$REPO_JSON")"
|
||||
[ "$COUNT" -ge 1 ] || fail "repo.json ImageUrls is empty"
|
||||
for i in $(seq 0 $((COUNT - 1))); do
|
||||
URL="$(jq -r ".[0].ImageUrls[$i]" "$REPO_JSON")"
|
||||
curl -fsI "$URL" > /dev/null || fail "ImageUrls[$i] unreachable: $URL"
|
||||
done
|
||||
ok "$COUNT ImageUrl(s) reachable, DalamudApiLevel=$API_LEVEL"
|
||||
else
|
||||
ok "skipped URL reachability (HOOKS_OFFLINE=1), DalamudApiLevel=$API_LEVEL"
|
||||
fi
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-version-consistency.sh — Block A of preflight.
|
||||
# csproj <Version> is 3-digit SemVer; repo.json AssemblyVersion is 4-digit (.0 suffix).
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
CSPROJ="$ROOT/HellionChat/HellionChat.csproj"
|
||||
REPO_JSON="$ROOT/repo.json"
|
||||
|
||||
fail() { echo "verify-version-consistency: FAIL — $1" >&2; exit 1; }
|
||||
ok() { echo "verify-version-consistency: OK — $1"; }
|
||||
|
||||
CSPROJ_VER="$(grep -oE '<Version>[^<]+</Version>' "$CSPROJ" | head -1 | sed -E 's/<[^>]+>//g')"
|
||||
[ -n "$CSPROJ_VER" ] || fail "$CSPROJ has no <Version> element"
|
||||
|
||||
EXPECTED_4DIGIT="${CSPROJ_VER}.0"
|
||||
|
||||
REPO_VER="$(jq -r '.[0].AssemblyVersion' "$REPO_JSON")"
|
||||
[ "$REPO_VER" = "$EXPECTED_4DIGIT" ] \
|
||||
|| fail "csproj=$CSPROJ_VER expects repo.json AssemblyVersion=$EXPECTED_4DIGIT but got $REPO_VER. Fix: align in $REPO_JSON."
|
||||
|
||||
TEST_VER="$(jq -r '.[0].TestingAssemblyVersion' "$REPO_JSON")"
|
||||
[ "$TEST_VER" = "$EXPECTED_4DIGIT" ] \
|
||||
|| fail "TestingAssemblyVersion=$TEST_VER must match $EXPECTED_4DIGIT. Fix: align in $REPO_JSON."
|
||||
|
||||
TAG="v$CSPROJ_VER"
|
||||
for KEY in DownloadLinkInstall DownloadLinkUpdate DownloadLinkTesting; do
|
||||
URL="$(jq -r ".[0].$KEY" "$REPO_JSON")"
|
||||
case "$URL" in
|
||||
*"/$TAG/"*) ;;
|
||||
*) fail "$KEY=$URL does not contain tag $TAG. Fix: update $REPO_JSON $KEY to releases/download/$TAG/latest.zip." ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ok "csproj=$CSPROJ_VER, repo.json=$EXPECTED_4DIGIT, tag $TAG present in DownloadLinks"
|
||||