Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3fdcdf43d | ||
|
|
f4ea460644 | ||
|
|
d5735d8dcc | ||
|
|
80b48ac3ad | ||
|
|
cddd29a986 | ||
|
|
799fdb67cc | ||
|
|
69fa0fecbd | ||
|
|
fd5f970a8b | ||
|
|
fee2459e73 | ||
|
|
63cad62c89 | ||
|
|
dca5de4085 | ||
|
|
8edc3c70d3 | ||
|
|
3c33acf6d7 | ||
|
|
c8ba8c1cd0 | ||
|
|
94e4828aeb | ||
|
|
1d88cb4c42 | ||
|
|
c5fe69f0d3 | ||
|
|
b46d3ad0a8 | ||
|
|
e33cf0dcb9 | ||
|
|
0d016aaa5d | ||
|
|
5b972238bb | ||
|
|
7ac1eb3fd4 | ||
|
|
db48f27842 | ||
|
|
f8b5c14509 | ||
|
|
28e4b30cd6 | ||
|
|
4510c1e404 | ||
|
|
6b44f549b4 | ||
|
|
ae1436b103 | ||
|
|
2684c31f10 | ||
|
|
bdd64cad07 | ||
|
|
28ea2fa553 | ||
|
|
dd597fca44 | ||
|
|
b9d3ff8f26 | ||
|
|
df3d5d78d6 | ||
|
|
2e057ce6c4 | ||
|
|
e5dbc333fa | ||
|
|
d0ec94c3e6 | ||
|
|
cafb6faa39 | ||
|
|
b8d289a847 | ||
|
|
f16d8f5c78 | ||
|
|
eabb39ba86 | ||
|
|
b489ac946c | ||
|
|
8d9151c74a | ||
|
|
4ecbaf2a4b | ||
|
|
3e4601a0c8 | ||
|
|
61d5a33683 | ||
|
|
7ed689587b | ||
|
|
612bf8814f | ||
|
|
be17472cd5 | ||
|
|
8bf50151d5 | ||
|
|
57da455700 | ||
|
|
0982b68a4a | ||
|
|
0fc88e480a | ||
|
|
7eb50e2c8d | ||
|
|
58e754c169 | ||
|
|
83064cd40b | ||
|
|
5ca3b73b7f | ||
|
|
570a6f071c | ||
|
|
11ad5db127 | ||
|
|
5c550e8587 | ||
|
|
eb2a04c56b | ||
|
|
3f714d6f38 |
No files matched your search
File renamed without changes.
@@ -101,16 +101,16 @@ jobs:
|
||||
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$header = "**Hellion Chat $version"
|
||||
$header = "**v$version "
|
||||
$start = $changelogBody.IndexOf($header)
|
||||
if ($start -lt 0) {
|
||||
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
||||
}
|
||||
$rest = $changelogBody.Substring($start)
|
||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
||||
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||
$trailer = $rest.IndexOf("`n`n---")
|
||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||
@@ -120,17 +120,37 @@ jobs:
|
||||
$enBlock = $rest.TrimEnd()
|
||||
}
|
||||
|
||||
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
|
||||
# ---------- Embed-Felder + Per-Field-Caps (Discord-Hard-Limits) ----------
|
||||
# Discord enforces per-embed-field limits separately from the
|
||||
# combined-total limit. We split the DE and EN blocks into two
|
||||
# embeds that share the same release URL so Discord stitches
|
||||
# them into one visual card. Hard caps per Discord docs:
|
||||
# description: 4096 per embed
|
||||
# title: 256 per embed
|
||||
# footer.text: 2048 per embed
|
||||
# combined sum across all embeds: 6000
|
||||
$title = "Hellion Chat $version — $subtitle"
|
||||
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
|
||||
$deDesc = "**Deutsch**`n`n$deBody"
|
||||
$enDesc = "**English**`n`n$enBlock"
|
||||
$footerText = "Hellion Forge · $versionsnatur"
|
||||
$totalChars = $title.Length + $description.Length + $footerText.Length
|
||||
if ($totalChars -gt 5500) {
|
||||
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
|
||||
}
|
||||
Write-Host "Char-Cap OK: $totalChars / 5500"
|
||||
$releaseUrl = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
|
||||
# ---------- Embed-Payload bauen ----------
|
||||
if ($deDesc.Length -gt 4096) {
|
||||
throw "V6a: DE-Body too long for one embed ($($deDesc.Length) chars, max 4096). Trim .github/forge-posts/$tag.md or post the announcement manually (see forge style §8)."
|
||||
}
|
||||
if ($enDesc.Length -gt 4096) {
|
||||
throw "V6b: EN-Block too long for one embed ($($enDesc.Length) chars, max 4096). Trim the changelog entry in HellionChat/HellionChat.yaml or post manually."
|
||||
}
|
||||
$totalChars = $title.Length + $deDesc.Length + $enDesc.Length + $footerText.Length
|
||||
if ($totalChars -gt 6000) {
|
||||
throw "V6c: Combined embed chars $totalChars exceed Discord's 6000-total limit. Major-Release detected — post manually via Bot/Multi-Embed (see forge style §8)."
|
||||
}
|
||||
Write-Host "Embed-Caps OK: de=$($deDesc.Length)/4096, en=$($enDesc.Length)/4096, total=$totalChars/6000"
|
||||
|
||||
# ---------- Embed-Payload bauen (zwei Embeds, gleiche url) ----------
|
||||
# Sharing the same `url` tells Discord to render both embeds as a
|
||||
# single contiguous card block. The title sits on the first embed,
|
||||
# the footer + timestamp on the last so it reads as one post.
|
||||
$payload = [ordered]@{
|
||||
username = "Forge Herald"
|
||||
avatar_url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png"
|
||||
@@ -142,9 +162,14 @@ jobs:
|
||||
embeds = @(
|
||||
[ordered]@{
|
||||
title = $title
|
||||
url = "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/tag/$tag"
|
||||
url = $releaseUrl
|
||||
color = 12730636
|
||||
description = $description
|
||||
description = $deDesc
|
||||
},
|
||||
[ordered]@{
|
||||
url = $releaseUrl
|
||||
color = 12730636
|
||||
description = $enDesc
|
||||
footer = [ordered]@{ text = $footerText }
|
||||
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
}
|
||||
@@ -20,16 +20,12 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
||||
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
||||
# The tag input is validated against the same semver regex as the
|
||||
# auto-trigger before any string interpolation happens.
|
||||
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
|
||||
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
|
||||
# ref step below hard-fails if a non-tag ref is selected, because the
|
||||
# release-action reads GITHUB_REF directly and rejects anything that
|
||||
# does not start with refs/tags/.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Existing tag to (re)release, e.g. v0.6.1"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -41,14 +37,21 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
# On push:tags, github.ref_name is the tag — checkout default works.
|
||||
# On workflow_dispatch, ref defaults to the branch the action was
|
||||
# invoked from; we need to explicitly check out the tag the user
|
||||
# supplied so the build comes from the tagged commit, not main.
|
||||
# release-action@main reads GITHUB_REF directly (its action.yml
|
||||
# does not declare a tag_name input). Validate up-front so manual
|
||||
# dispatches from a branch ref fail loud here instead of burning
|
||||
# a full build before the final step errors out with "ref X is
|
||||
# not a tag".
|
||||
- name: Validate tag ref
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
|
||||
echo "::error::Release workflow must run on a v*.X.Y tag ref, got ${GITHUB_REF}"
|
||||
echo "::error::Push a tag, or pick the tag (not main) in the workflow_dispatch Ref dropdown."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Setup .NET 10
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||
@@ -89,12 +92,11 @@ jobs:
|
||||
- name: Generate release body
|
||||
shell: pwsh
|
||||
env:
|
||||
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
||||
# push:tags carries it in github.ref_name. Either way the value
|
||||
# is treated as a PowerShell variable (env-var pass), not as
|
||||
# inline shell text, and validated against the semver regex
|
||||
# below before any string interpolation.
|
||||
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
# github.ref_name is the tag because Validate tag ref above
|
||||
# already enforced refs/tags/v*. Read via env: so the value
|
||||
# is a PowerShell variable, not inline shell text, and gets
|
||||
# re-validated against the semver regex below.
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
$tag = $env:TAG_NAME
|
||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||
@@ -111,20 +113,22 @@ jobs:
|
||||
|
||||
# changelog: is the last top-level key in the manifest, so
|
||||
# everything after the marker is the literal block. Strip the
|
||||
# 2-space yaml indent from each line.
|
||||
# 4-space yaml indent (prettier convention) from each line.
|
||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
||||
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||
}) -join "`n"
|
||||
|
||||
$header = "**Hellion Chat $version"
|
||||
# Subblock convention: "**vX.Y.Z — <subtitle> (<date>)**"
|
||||
# matches verify-changelog-sync.sh and slim-rule grep.
|
||||
$header = "**v$version "
|
||||
$start = $changelogBody.IndexOf($header)
|
||||
if ($start -lt 0) {
|
||||
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
||||
}
|
||||
|
||||
$rest = $changelogBody.Substring($start)
|
||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
||||
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||
$trailer = $rest.IndexOf("`n`n---")
|
||||
|
||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||
@@ -152,19 +156,28 @@ jobs:
|
||||
Write-Host $body
|
||||
Write-Host "----------------------------------------"
|
||||
|
||||
# release-action@main only declares files/title/body/pre_release/
|
||||
# draft/api_key/insecure as inputs (see its action.yml). It silently
|
||||
# ignores anything else, including body_path and tag_name. The tag
|
||||
# itself comes from GITHUB_REF, the body must be passed inline via
|
||||
# body:, so we re-emit release-body.md as a step output first.
|
||||
- name: Expose release body for release-action
|
||||
id: body
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo 'content<<RELEASE_BODY_EOF'
|
||||
cat release-body.md
|
||||
echo 'RELEASE_BODY_EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Gitea-native release action. Creates the release if the tag has no
|
||||
# release yet, or updates the existing one. 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.
|
||||
# release yet, or updates the existing one with latest.zip attached
|
||||
# and the generated body. The auto-injected GITHUB_TOKEN on Gitea
|
||||
# Actions has Gitea-API scope and is sufficient for release write.
|
||||
- name: Attach to Gitea release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
# Explicit tag_name so the action targets the correct release in
|
||||
# both push:tags (auto) and workflow_dispatch (manual recovery)
|
||||
# modes. Without this, dispatch runs would default to the branch
|
||||
# ref (main) and fail to find the release.
|
||||
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
files: ${{ steps.locate.outputs.path }}
|
||||
body_path: release-body.md
|
||||
body: ${{ steps.body.outputs.content }}
|
||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
subtitle: Threading- und IPC-Sicherheits-Politur
|
||||
versionsnatur: Wartung und Robustheit
|
||||
---
|
||||
|
||||
**Hellion Chat 1.4.4 — Threading- und IPC-Sicherheits-Politur**
|
||||
|
||||
Fünfter Sub-Patch der v1.4.x Polish-Sweep-Serie. Threading-Annahmen werden explizit pro Methode dokumentiert, ein
|
||||
Hot-Path-Lock im Auto-Tell-Tab-Counter fällt weg, IPC-Cleanup wird sichtbar wenn er fehlschlägt und der Privacy-Filter
|
||||
spricht jetzt bei unbekannten ChatTypes.
|
||||
|
||||
- **AutoTellTabsService Hot-Path-Lock entfernt.** `ActiveTempTabCount` hat bisher pro Render-Frame ein LINQ-Count unter
|
||||
einem Lock gemacht. Jetzt läuft das über einen Interlocked-Counter der parallel zur Tabs-Liste mitgeführt wird,
|
||||
inklusive Resync-Hook für den Snapshot-Restore-Pfad in `SaveConfig`. Plus Pure-Helper-Test-Mirror in der Build-Suite
|
||||
damit die Atomicity-Semantik nicht versehentlich wegrefactored wird
|
||||
- **HonorificService selbst-dokumentierende Threading-Banner.** Statt eines Block-Comments am Klassen-Ende hat jede
|
||||
IPC-Callback-Methode jetzt einen 1-Zeilen-Banner darüber, der den Thread-Kontext direkt am Call-Site benennt
|
||||
(framework only, framework scheduled, any). Mehr Hilfe für künftige Reviews als ein abstraktes Threading-Kapitel
|
||||
- **Unsubscribe-Failure ist jetzt sichtbar.** `TryUnsubscribe` hat ein Honorific-Unsubscribe-Failure bisher als Debug
|
||||
geloggt, was bei Standard-Loglevel verschluckt wurde. Eine geleakte Subscription kann den Service über Plugin-Reloads
|
||||
hinweg leben lassen, also läuft der Log jetzt auf Warning
|
||||
- **AutoTranslate-Warmup blockiert den Plugin-Unload nicht mehr.** Der Cache-Warmup-Thread war ohne `IsBackground=true`
|
||||
unterwegs, was den Unload um 100-300 ms verzögern konnte. Pattern-Match zu MessageManager und RetentionSweep (beide
|
||||
seit v1.4.0)
|
||||
- **Privacy-Filter loggt unbekannte ChatTypes.** Wenn FFXIV durch einen Patch einen neuen ChatType einführt der weder in
|
||||
der Whitelist noch in den Defaults steht, wird er bisher silent durch den Failsafe geleitet. Jetzt loggt der Filter
|
||||
einmalig pro Runtime eine Warning mit dem Type und dem Failsafe-Wert. Dedup über ein NonSerialized-HashSet, also kein
|
||||
Log-Spam
|
||||
- **Default-Flip für neue Installationen.** `PrivacyPersistUnknownChannels` startet bei neuen Configs jetzt auf `true`,
|
||||
damit ein Patch-bedingt neuer ChatType nicht stillschweigend gedroppt wird bevor der User entscheiden kann. Bestehende
|
||||
Configs behalten ihre Wahl, weil der Deserializer den Initializer überschreibt. Keine Migration, kein Schema-Bump
|
||||
|
||||
Keine User-sichtbaren Funktions-Änderungen außer dem Default-Flip für neue Installationen. Settings, Themes, Tabs und
|
||||
das Privacy-Verhalten für Bestand bleiben unangetastet.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
subtitle: UX und Robustheit
|
||||
versionsnatur: UX-Polish-Cycle
|
||||
---
|
||||
|
||||
**Hellion Chat 1.4.5 — UX und Robustheit**
|
||||
|
||||
Sechster Sub-Patch der v1.4.x Polish-Sweep-Serie. Render-Fehler im Chat-Fenster werden jetzt sichtbar, der
|
||||
First-Run-Wizard hat eine explizite Cancel-Schaltfläche, der Eingabe-Verlauf bleibt nicht mehr über Plugin-Reloads
|
||||
hinweg liegen, und die Statusleiste klippt in schmalen Fenstern nicht mehr.
|
||||
|
||||
- **Fehler-Benachrichtigung im Chat-Fenster.** Wenn ein Render-Fehler in `DrawChatLog` auftritt, zeigt das Plugin jetzt
|
||||
eine einmalige Warning-Notification mit Verweis aufs `/xllog`, statt das Fenster stillschweigend leer zu lassen. Der
|
||||
Stack-Trace selbst geht weiter via `Plugin.Log.Error` ins Logfile. De-Dup über Per-Session-Bool, damit ein
|
||||
wiederkehrender Fehler die Notification-Stack nicht pro Frame neu vollkippt
|
||||
- **First-Run-Wizard trennt Accept und Close.** `OnClose` setzt nicht mehr stillschweigend `FirstRunCompleted=true`,
|
||||
also lässt das X den Wizard schwebend zurück und er kommt beim nächsten Plugin-Reload wieder. Eine neue „Später —
|
||||
Defaults behalten"-Schaltfläche im Footer ist der explizite Weg, ohne Profil-Auswahl rauszukommen. Strings bilingual
|
||||
EN+DE plus Tooltip
|
||||
- **Eingabe-Verlauf wird beim Plugin-Reload geleert.** `InputHistoryService.Reset` hängt jetzt in `Plugin.DisposeAsync`
|
||||
neben den anderen Pure-Memory-Cleanups, damit der statische Zustand aus der vorigen Session den nächsten Load nicht
|
||||
mehr erbt
|
||||
- **Statusleiste klippt nicht mehr.** Der rechtsbündige Versions-Slot wird ausgeblendet wenn die Chat-Window-Breite
|
||||
abzüglich Versions-Text unter 200 px fällt — vorher überlappte er die vier linken Slots. Ab ausreichender Breite
|
||||
taucht der Slot wieder auf
|
||||
- **Intern:** `FontManager` fällt auf System-Font zurück wenn die eingebettete Hellion-Font-Resource fehlt
|
||||
(Broken-csproj-Pfad, nie ein Produktions-Build), plus expliziter Session-Only-Invariant-Kommentar für Auto-Tell-Tabs
|
||||
in `Plugin.cs:167-168` mit einem TempTabCounter-Init-Pin in der Build-Suite. Kein Schema-Bump, keine Migration
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
subtitle: Code Hygiene and Refactor
|
||||
versionsnatur: Maintenance-Cycle
|
||||
---
|
||||
|
||||
Wartungs-Patch ohne User-sichtbare Änderungen. Saubere Code-Basis als Vorbereitung auf das v1.4.7-Backlog-Cleanup, plus
|
||||
zwei geerbte Bugfixes aus dem ChatTwo-Upstream `f35b7d3`.
|
||||
|
||||
- **preflight.sh härter**: csharpier-Reflow-Check (Block E) und markdownlint (Block F) laufen jetzt im Pre-Push-Gate,
|
||||
statt erst beim Pre-Merge-Review aufzufallen.
|
||||
- **FontManager-Fallback robuster**: Atlas-Toolkit-Throws aus kaputten Font-Configs (IO, InvalidOperation,
|
||||
ArgumentException) fallen jetzt zuverlässig auf NotoSansCjkRegular, statt den Atlas-Build mitzureißen. Der
|
||||
Exception-Typ wird im Log mitgegeben für die Diagnose.
|
||||
- **URL-Validation beim Plugin-Load**: BrandingLinks (5 URLs) und IntegrationLinks (2 URLs) werden via
|
||||
`[ModuleInitializer]` geprüft. Ein Tippfehler bei einer künftigen URL-Rotation wirft jetzt sofort beim Plugin-Load,
|
||||
statt still beim Klick zu scheitern.
|
||||
- **Cherry-Pick aus ChatTwo `f35b7d3`** — Memory-Leak in `Chat.SetChannel`: der native `Utf8String` wird jetzt auch dann
|
||||
freigegeben, wenn der Linkshell-Check den Channel ablehnt (vorher gefangen im early-return).
|
||||
- **Cherry-Pick aus ChatTwo `f35b7d3`** — `Tab.Clone()` Deep-cloned jetzt `UsedChannel` und `TellTarget`. Vorher
|
||||
Reference-Share-Bug: PopOut- und Temp-Tabs mutierten sich gegenseitig.
|
||||
- **Aktive-Tab-Underline pixel-perfect bei DPI-Scaling**: Die Underline-Pill skaliert jetzt mit
|
||||
`ImGuiHelpers.GlobalScale` und rundet die DrawList-Koordinaten auf physische Pixel. Kein Sub-Pixel-Blur mehr auf
|
||||
125/150%-Setups.
|
||||
- **IconButton-Width-Fix**: der manuelle `width - 2 * CellPadding.X`-Subtract verlor den HUD-Scale (Padding skaliert,
|
||||
der raw int nicht). Gemessene Breite läuft jetzt unverändert durch.
|
||||
- **Test-Isolation für MessageStore**: `Dalamud.Utility.Util`-Surface (IsWine, OpenLink) läuft jetzt durch eine
|
||||
`IPlatformUtil`-Indirektion. MessageStores `IsWine`-Probe ist isoliert testbar in der Build-Suite. Plus:
|
||||
HellionStyle-ChildBgAlpha als Pure-Helper extrahiert, Plugin.SaveConfig kopiert nur Session-Tabs statt der ganzen
|
||||
Tab-Liste, SettingsOverview cached den DrawList einmal pro Frame.
|
||||
- **Built-in-Theme-Roster**: Crystal Nocturne (Royal Sapphire + Electric Magenta auf Obsidian, von CRYSTALLITE) ersetzt
|
||||
Moonlit Bloom. User mit Moonlit Bloom als aktivem Theme fallen beim ersten Plugin-Load auf Hellion Arctic zurück.
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
subtitle: Backlog Cleanup and Mid-Features
|
||||
versionsnatur: Mid-Feature-Patch
|
||||
---
|
||||
|
||||
Achter Sub-Patch der v1.4.x Polish-Sweep-Serie. Erstes User-sichtbares Feature-Bundle seit v1.4.5 — angepinnte Tell-Tabs
|
||||
die Relog überleben, opt-in Honorific-Glow, plus eine konfigurierbare Sidebar.
|
||||
|
||||
- **TempTell anpinnen**: Rechtsklick auf einen TempTell-Tab in der Sidebar → „Tab anpinnen". Angepinnte Tabs überleben
|
||||
Plugin-Reload und Char-Logout, behalten ihre Konversations-Historie (wird beim Rehydrate aus dem MessageStore
|
||||
nachgeladen) und bleiben an die gleiche /tell-Person gebunden. Hard-Cap 5 angepinnte Tabs in einem separaten Pool —
|
||||
die normalen Auto-Tell-Tabs (15er Cap) sind davon entkoppelt, Gesamt-Decke 20. Die Sidebar gruppiert angepinnte Tabs
|
||||
in einer eigenen „Angepinnt"-Sektion mit eigenem Trenner.
|
||||
- **Honorific Glow-Outline**: rendert jetzt eine 8-Richtungs-DrawList-Outline wenn der Honorific-Titel eine Glow-Farbe
|
||||
trägt. Opt-in via **Settings → Integrationen → Glow-Outline rendern (Honorific)** (Default OFF). Gradient (Color3 /
|
||||
GradientColourSet / Wave / Pulse) wird geparst und im DTO weitergereicht, rendert aktuell aber statisch als
|
||||
Primärfarbe — der volle Gradient-Port (Animations-Algorithmus + Pride-Palette) kommt als eigener Cycle nach.
|
||||
- **Sidebar-Breite konfigurierbar**: in **Theme & Layout** ein Slider 44–160 px. Default bleibt 44 px (icon-only), aber
|
||||
breiter machen damit Sektion-Header wie „Aktive Tells (3)" oder „Angepinnt (2)" nicht abgeschnitten werden.
|
||||
- **Settings-Save Channel-Fix**: ein Save mit aktivem Party- oder Linkshell-Tab konnte den Chat-Input zurück auf
|
||||
`/tell <angepinnte Person>` springen lassen. `Configuration.UpdateFrom` bewahrt jetzt den Runtime-`CurrentChannel`
|
||||
über den persistent-Tab-Merge hinweg, und `TabSwitched` deep-cloned den Seed-Channel statt sich den `UsedChannel` mit
|
||||
dem vorigen Tab zu teilen.
|
||||
- **Internal**: `IPluginLogProxy`-Indirektion vor Dalamud's `IPluginLog` über alle ~91 `Plugin.Log`-Call-Sites. Damit
|
||||
läuft `MessageStore.Migrate0` voll-isoliert in xUnit (F12.1-Lücke aus v1.4.6 geschlossen). Plus: TempTab-Counter als
|
||||
abgeleitete Property statt gecachtes Interlocked-Feld — die neuen Pin/Unpin-Übergänge sind Cold-Path, kein
|
||||
Lock-Free-Vorteil mehr. Migration v16 → v17 ist rein additiv (neues `Tab.IsPinned`-Bool, Default false).
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
@@ -384,3 +384,7 @@ ChatTwo.Tests
|
||||
TestResults
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# Claude Code projekt-spezifisches Setup (lokal, nicht committed)
|
||||
/.claude/
|
||||
/CLAUDE.md
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"MD007": { "indent": 4 },
|
||||
"MD013": false,
|
||||
"MD024": { "siblings_only": true },
|
||||
"MD029": false,
|
||||
"MD033": false,
|
||||
"MD036": false,
|
||||
"MD041": false
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
@@ -19,6 +21,12 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
private readonly MessageStore _store;
|
||||
private readonly object _tempTabsLock = new();
|
||||
|
||||
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
|
||||
// of usage. Separate pool from AutoTellTabsLimit (15) — pinned tabs live
|
||||
// in their own bucket. A configurable cap is a vault-backlog anchor for
|
||||
// a later cycle if tester feedback demands it.
|
||||
internal const int MaxPinnedTempTabs = 5;
|
||||
|
||||
private bool _initialized;
|
||||
|
||||
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
|
||||
@@ -28,16 +36,14 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
_store = store;
|
||||
}
|
||||
|
||||
internal int ActiveTempTabCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
return Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||
// mutate IsPinned or remove tabs — the count adapts automatically.
|
||||
// Replaces the F2.1 Interlocked counter because the new pin-state
|
||||
// transitions are cold-path and don't need lock-free reads.
|
||||
internal int ActiveTempTabCount =>
|
||||
Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
internal int PinnedTempTabCount => Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||
|
||||
internal void Initialize()
|
||||
{
|
||||
@@ -46,11 +52,53 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Pinned tabs come out of the JSON with TellTarget set but
|
||||
// CurrentChannel reset (NonSerialized). Without re-seeding, the chat
|
||||
// input has no tell-target on the active pinned tab, and the
|
||||
// game-side channel hook only repaints CurrentChannel once the user
|
||||
// triggers a /tell or channel switch.
|
||||
RehydratePinnedTabs();
|
||||
|
||||
_messageManager.MessageProcessed += HandleTell;
|
||||
Plugin.ClientState.Logout += OnLogout;
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private void RehydratePinnedTabs()
|
||||
{
|
||||
var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||
Plugin.LogProxy.Debug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found");
|
||||
|
||||
foreach (var tab in Plugin.Config.Tabs)
|
||||
{
|
||||
if (!TabLifecycleHelpers.IsInPinnedPool(tab))
|
||||
continue;
|
||||
|
||||
if (tab.TellTarget is null || !tab.TellTarget.IsSet())
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
$"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget "
|
||||
+ $"(Name={tab.TellTarget?.Name ?? "<null>"} World={tab.TellTarget?.World ?? 0}). "
|
||||
+ "Chat input on this tab will be empty until the partner sends a tell or you /tell manually."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
tab.Channel ??= InputChannel.Tell;
|
||||
tab.CurrentChannel.Channel = InputChannel.Tell;
|
||||
tab.CurrentChannel.TellTarget = tab.TellTarget.Clone();
|
||||
|
||||
// MessageList is NonSerialized so pinned tabs come back empty.
|
||||
// Preload the same history window the spawn path uses so the user
|
||||
// sees the recent conversation, not a blank tab.
|
||||
PreloadHistory(tab, tab.TellTarget.Name, tab.TellTarget.World, Guid.Empty);
|
||||
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_initialized)
|
||||
@@ -82,7 +130,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
if (partner == null)
|
||||
{
|
||||
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
|
||||
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
||||
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
||||
@@ -96,7 +144,23 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
var existing = FindTempTab(partner.Value.Name, partner.Value.World);
|
||||
if (existing != null)
|
||||
{
|
||||
// Already routed via MessageManager pipeline
|
||||
// Already routed via MessageManager pipeline. Repair the
|
||||
// tell-target if the fallback hit a pinned tab whose
|
||||
// TellTarget didn't survive a previous round-trip — keeps
|
||||
// FindTempTab fast on the next message.
|
||||
if (
|
||||
existing.IsPinned
|
||||
&& (existing.TellTarget is null || !existing.TellTarget.IsSet())
|
||||
)
|
||||
{
|
||||
existing.TellTarget = new TellTarget(
|
||||
partner.Value.Name,
|
||||
partner.Value.World,
|
||||
0,
|
||||
TellReason.Direct
|
||||
);
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,22 +210,35 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
private Tab? FindTempTab(string name, uint world)
|
||||
private static Tab? FindTempTab(string name, uint world)
|
||||
{
|
||||
return Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||
var byTarget = Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||
t.IsTempTab
|
||||
&& t.TellTarget != null
|
||||
&& string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase)
|
||||
&& t.TellTarget.World == world
|
||||
);
|
||||
if (byTarget != null)
|
||||
return byTarget;
|
||||
|
||||
// Fallback: match by tab name. Pinned tabs are named via
|
||||
// FormatTabName(player, world) at spawn time, so the name is a
|
||||
// stable secondary key when TellTarget didn't survive a save/load
|
||||
// (older configs from a renamed pin, malformed migrations, etc.).
|
||||
var expectedName = FormatTabName(name, world);
|
||||
return Plugin.Config.Tabs.FirstOrDefault(t =>
|
||||
t.IsTempTab && string.Equals(t.Name, expectedName, StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
}
|
||||
|
||||
private void DropOldestTempTab()
|
||||
internal void DropOldestTempTab()
|
||||
{
|
||||
// Prioritize greeted tabs for drop; within each bucket, drop by oldest LastActivity
|
||||
// Pinned tabs live in their own bucket (MaxPinnedTempTabs) and are
|
||||
// never drop candidates. They leave the bucket only via Unpin or
|
||||
// PromoteToPermanent.
|
||||
var victim = Plugin
|
||||
.Config.Tabs.Select((tab, idx) => (Tab: tab, Index: idx))
|
||||
.Where(t => t.Tab.IsTempTab)
|
||||
.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t.Tab))
|
||||
.OrderByDescending(t => t.Tab.IsGreeted)
|
||||
.ThenBy(t => t.Tab.LastActivity)
|
||||
.FirstOrDefault();
|
||||
@@ -284,7 +361,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Non-fatal: tab still spawns with visible error notice instead of silent history loss
|
||||
Plugin.Log.Error(ex, "[AutoTellTabs] History preload failed");
|
||||
Plugin.LogProxy.Error(ex, "[AutoTellTabs] History preload failed");
|
||||
tab.Messages.AddPrune(
|
||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||
MessageManager.MessageDisplayLimit
|
||||
@@ -338,14 +415,16 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
lock (_tempTabsLock)
|
||||
{
|
||||
// Snapshot active tab index before mutating list
|
||||
// Pinned TempTabs must survive char-switch — that's the whole point
|
||||
// of pinning. Only unpinned ones get stripped.
|
||||
var lastIndex = _plugin.LastTab;
|
||||
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
|
||||
var currentWasUnpinnedTempTab =
|
||||
lastIndexValid
|
||||
&& TabLifecycleHelpers.IsInUnpinnedPool(Plugin.Config.Tabs[lastIndex]);
|
||||
|
||||
// Clean up pop-out windows before removing temp tabs
|
||||
var poppedTempTabIds = Plugin
|
||||
.Config.Tabs.Where(t => t.IsTempTab && t.PopOut)
|
||||
.Config.Tabs.Where(t => TabLifecycleHelpers.IsInUnpinnedPool(t) && t.PopOut)
|
||||
.Select(t => t.Identifier)
|
||||
.ToList();
|
||||
if (poppedTempTabIds.Count > 0)
|
||||
@@ -361,14 +440,78 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
Plugin.Config.Tabs.RemoveAll(TabLifecycleHelpers.IsInUnpinnedPool);
|
||||
|
||||
// Force switch to tab 0 if active tab was temp or index is now out of range
|
||||
// Force switch to tab 0 if active tab was an unpinned temp tab or
|
||||
// index is now out of range. Pinned tabs survive — no switch needed.
|
||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||
if (currentWasTempTab || !stillValid)
|
||||
if (currentWasUnpinnedTempTab || !stillValid)
|
||||
{
|
||||
_plugin.WantedTab = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryPin(Tab tab)
|
||||
{
|
||||
if (!tab.IsTempTab || tab.IsPinned)
|
||||
{
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PinnedTempTabCount >= MaxPinnedTempTabs)
|
||||
{
|
||||
WrapperUtil.AddNotification(
|
||||
string.Format(HellionStrings.PinTab_LimitReached, MaxPinnedTempTabs),
|
||||
NotificationType.Warning
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
tab.IsPinned = true;
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
|
||||
);
|
||||
_plugin.SaveConfig();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void Unpin(Tab tab)
|
||||
{
|
||||
if (!tab.IsPinned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the unpinned pool is already full, dropping the oldest before
|
||||
// flipping the flag avoids counting the just-unpinned tab as a drop
|
||||
// candidate.
|
||||
if (ActiveTempTabCount >= Plugin.Config.AutoTellTabsLimit)
|
||||
{
|
||||
DropOldestTempTab();
|
||||
}
|
||||
|
||||
tab.IsPinned = false;
|
||||
Plugin.LogProxy.Debug("[Pin] Unpinned tab '{tab.Name}'");
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
internal void PromoteToPermanent(Tab tab)
|
||||
{
|
||||
if (!tab.IsTempTab)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tab.IsTempTab = false;
|
||||
tab.IsPinned = false;
|
||||
tab.TellTarget = TellTarget.Empty();
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)"
|
||||
);
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Branding;
|
||||
|
||||
// Centralised — a future invite/URL rotation only touches this file.
|
||||
@@ -9,4 +12,22 @@ internal static class BrandingLinks
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat";
|
||||
public const string HellionForgeWebsite = "https://hellion-forge.cloud";
|
||||
public const string HellionMediaWebsite = "https://hellion-media.de/de";
|
||||
|
||||
// CA2255 warns against [ModuleInitializer] in library code, but Dalamud
|
||||
// loads the plugin DLL directly so the module-init pass is the right hook
|
||||
// for a one-shot URL sanity check at plugin load.
|
||||
#pragma warning disable CA2255
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255
|
||||
internal static void ValidateUrls()
|
||||
{
|
||||
UrlValidation.ValidateAll(
|
||||
nameof(BrandingLinks),
|
||||
HellionForgeDiscordInvite,
|
||||
HellionForgeGitea,
|
||||
HellionChatRepo,
|
||||
HellionForgeWebsite,
|
||||
HellionMediaWebsite
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ internal sealed class Commands : IDisposable
|
||||
{
|
||||
if (!Registered.TryGetValue(command, out var wrapper))
|
||||
{
|
||||
Plugin.Log.Warning($"Missing registration for command {command}");
|
||||
Plugin.LogProxy.Warning($"Missing registration for command {command}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ internal sealed class Commands : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, $"Error while executing command {command}");
|
||||
Plugin.LogProxy.Error(ex, $"Error while executing command {command}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class ConfigKeyBind
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private const int LatestVersion = 16;
|
||||
private const int LatestVersion = 17;
|
||||
|
||||
public int Version { get; set; } = LatestVersion;
|
||||
|
||||
@@ -57,8 +57,18 @@ public class Configuration : IPluginConfiguration
|
||||
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
||||
public HashSet<ChatType> PrivacyPersistChannels = [];
|
||||
|
||||
// Failsafe for ChatTypes added by future FFXIV patches.
|
||||
public bool PrivacyPersistUnknownChannels;
|
||||
// Failsafe for ChatTypes added by future FFXIV patches. New configs default
|
||||
// to the failsafe via PrivacyDefaults; existing configs keep their saved
|
||||
// choice because the deserializer overrides this initializer.
|
||||
public bool PrivacyPersistUnknownChannels = Privacy
|
||||
.PrivacyDefaults
|
||||
.DefaultPersistUnknownChannels;
|
||||
|
||||
// F3.2: dedup unknown-ChatType warnings so a chatty filter doesn't spam
|
||||
// the log every frame. NonSerialized so the warning fires once per
|
||||
// runtime, not once-ever-per-install.
|
||||
[NonSerialized]
|
||||
private readonly HashSet<ChatType> _warnedUnknownChannels = new();
|
||||
|
||||
public bool IsAllowedForStorage(ChatType type)
|
||||
{
|
||||
@@ -66,6 +76,20 @@ public class Configuration : IPluginConfiguration
|
||||
return true;
|
||||
if (PrivacyPersistChannels.Contains(type))
|
||||
return true;
|
||||
|
||||
// F3.2: log first occurrence of a ChatType the running build doesn't
|
||||
// recognise — i.e. one a future FFXIV patch may have added. Known
|
||||
// types the user opted out of are routed through the failsafe
|
||||
// silently, like before.
|
||||
if (!Enum.IsDefined(typeof(ChatType), type) && _warnedUnknownChannels.Add(type))
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
||||
type,
|
||||
PrivacyPersistUnknownChannels
|
||||
);
|
||||
}
|
||||
|
||||
return PrivacyPersistUnknownChannels;
|
||||
}
|
||||
|
||||
@@ -78,10 +102,22 @@ public class Configuration : IPluginConfiguration
|
||||
public bool FirstRunCompleted;
|
||||
public bool UseHellionFont = true;
|
||||
public bool ShowHonorificTitleInHeader = true;
|
||||
|
||||
// v1.4.7 opt-in: renders the Honorific glow outline when the title carries
|
||||
// a Glow colour. Default OFF — keeps v1.4.6 visuals untouched for users
|
||||
// who don't care, and dodges the per-frame DrawList overhead on low-end
|
||||
// hardware. Gradient (Color3 / GradientColourSet) is parsed but rendered
|
||||
// as the primary Color until a later cycle ports the animation.
|
||||
public bool ShowHonorificGlow;
|
||||
public bool EnableAutoTellTabs = true;
|
||||
public int AutoTellTabsLimit = 15;
|
||||
public bool AutoTellTabsCompactDisplay;
|
||||
public int AutoTellTabsHistoryPreload = 20;
|
||||
|
||||
// Sidebar width in pixels. Default 44 mirrors the icon-only layout from
|
||||
// v1.2.0; users can widen up to 160 to fit a section-header line like
|
||||
// "Active Tells (3)" without truncation.
|
||||
public int SidebarWidth = 44;
|
||||
public bool AutoTellTabsShowGreetedToggle;
|
||||
public bool SeenPopOutInputHint;
|
||||
public bool PopOutInputEnabled = true;
|
||||
@@ -254,16 +290,20 @@ public class Configuration : IPluginConfiguration
|
||||
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
|
||||
|
||||
// Keep live temp tabs alive across UpdateFrom — a settings save must
|
||||
// not destroy open tell conversations. For persistent tabs, capture
|
||||
// the live MessageList and LastSendUnread by Identifier before the
|
||||
// replace and restore them onto the freshly cloned tabs; new tabs
|
||||
// get an empty MessageList, deleted tabs lose their history (intended).
|
||||
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
|
||||
var livePersistentSession = Tabs.Where(t => !t.IsTempTab)
|
||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
|
||||
// not destroy open tell conversations. Pinned TempTabs are persistent
|
||||
// and come through `other` like regular tabs; unpinned TempTabs are
|
||||
// session-only and held from the local state. For persistent tabs
|
||||
// (incl. pinned), capture live runtime state by Identifier and restore
|
||||
// it onto the freshly cloned tabs — CurrentChannel is critical because
|
||||
// the user may have switched channel in-game between settings-open
|
||||
// and settings-save, and we'd otherwise overwrite that with the
|
||||
// settings-time snapshot.
|
||||
var liveUnpinnedTempTabs = Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
var livePersistentSession = Tabs.Where(t => !TabLifecycleHelpers.IsInUnpinnedPool(t))
|
||||
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread, t.CurrentChannel));
|
||||
|
||||
Tabs = other
|
||||
.Tabs.Where(t => !t.IsTempTab)
|
||||
.Tabs.Where(t => !t.IsTempTab || t.IsPinned)
|
||||
.Select(t =>
|
||||
{
|
||||
var clone = t.Clone();
|
||||
@@ -271,11 +311,12 @@ public class Configuration : IPluginConfiguration
|
||||
{
|
||||
clone.Messages = live.Messages;
|
||||
clone.LastSendUnread = live.LastSendUnread;
|
||||
clone.CurrentChannel = live.CurrentChannel;
|
||||
}
|
||||
return clone;
|
||||
})
|
||||
.ToList();
|
||||
Tabs.AddRange(liveTempTabs);
|
||||
Tabs.AddRange(liveUnpinnedTempTabs);
|
||||
|
||||
ChatTabForward = other.ChatTabForward;
|
||||
ChatTabBackward = other.ChatTabBackward;
|
||||
@@ -295,6 +336,7 @@ public class Configuration : IPluginConfiguration
|
||||
FirstRunCompleted = other.FirstRunCompleted;
|
||||
UseHellionFont = other.UseHellionFont;
|
||||
ShowHonorificTitleInHeader = other.ShowHonorificTitleInHeader;
|
||||
ShowHonorificGlow = other.ShowHonorificGlow;
|
||||
|
||||
// v1.1.0 theme engine fields
|
||||
Theme = other.Theme;
|
||||
@@ -306,6 +348,7 @@ public class Configuration : IPluginConfiguration
|
||||
AutoTellTabsLimit = other.AutoTellTabsLimit;
|
||||
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
|
||||
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
|
||||
SidebarWidth = other.SidebarWidth;
|
||||
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
|
||||
|
||||
SeenPopOutInputHint = other.SeenPopOutInputHint;
|
||||
@@ -380,6 +423,11 @@ public class Tab
|
||||
public bool HideWhenInactive;
|
||||
|
||||
public bool IsTempTab;
|
||||
|
||||
// Pinned TempTabs survive plugin reload and logout — tester feedback from
|
||||
// Jin (v1.4.7). Pinned tabs live in their own pool (MaxPinnedTempTabs)
|
||||
// separate from the AutoTellTabsLimit bucket.
|
||||
public bool IsPinned;
|
||||
public bool AllSenderMessages;
|
||||
public TellTarget TellTarget = TellTarget.Empty();
|
||||
|
||||
@@ -476,7 +524,7 @@ public class Tab
|
||||
Opacity = Opacity,
|
||||
Identifier = Identifier,
|
||||
InputDisabled = InputDisabled,
|
||||
CurrentChannel = CurrentChannel,
|
||||
CurrentChannel = CurrentChannel.Clone(),
|
||||
CanMove = CanMove,
|
||||
CanResize = CanResize,
|
||||
IndependentHide = IndependentHide,
|
||||
@@ -487,8 +535,9 @@ public class Tab
|
||||
HideInBattle = HideInBattle,
|
||||
HideWhenInactive = HideWhenInactive,
|
||||
IsTempTab = IsTempTab,
|
||||
IsPinned = IsPinned,
|
||||
AllSenderMessages = AllSenderMessages,
|
||||
TellTarget = TellTarget.From(TellTarget),
|
||||
TellTarget = TellTarget.Clone(),
|
||||
IsGreeted = IsGreeted,
|
||||
};
|
||||
}
|
||||
@@ -666,6 +715,29 @@ public class UsedChannel
|
||||
{
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Deep-clone the UsedChannel so Tab.Clone() no longer shares
|
||||
// channel state (incl. TellTarget) with its origin Tab. Previously
|
||||
// a reference copy: PopOut and Temp tabs mutated each other.
|
||||
// - Name is intentionally a reference copy (matches upstream); it
|
||||
// gets reassigned on every channel switch anyway.
|
||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/UsedChannelCloneTests.cs
|
||||
// ---------------------------------------------------------------
|
||||
public UsedChannel Clone()
|
||||
{
|
||||
return new UsedChannel
|
||||
{
|
||||
Channel = Channel,
|
||||
Name = Name,
|
||||
TellTarget = TellTarget?.Clone(),
|
||||
|
||||
UseTempChannel = UseTempChannel,
|
||||
TempChannel = TempChannel,
|
||||
TempTellTarget = TempTellTarget?.Clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -101,7 +101,10 @@ public static class EmoteCache
|
||||
t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Plugin.Log.Error(t.Exception!, $"EmoteCache load failed for {emoteCode}");
|
||||
Plugin.LogProxy.Error(
|
||||
t.Exception!,
|
||||
$"EmoteCache load failed for {emoteCode}"
|
||||
);
|
||||
},
|
||||
TaskScheduler.Default
|
||||
)
|
||||
@@ -158,7 +161,7 @@ public static class EmoteCache
|
||||
{
|
||||
// Reset to Unloaded so a later trigger can retry without a plugin reload.
|
||||
State = LoadingState.Unloaded;
|
||||
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
|
||||
Plugin.LogProxy.Error(ex, "BetterTTV cache wasn't initialized");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +217,7 @@ public static class EmoteCache
|
||||
}
|
||||
catch
|
||||
{
|
||||
Plugin.Log.Error("Failed to convert");
|
||||
Plugin.LogProxy.Error("Failed to convert");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -304,7 +307,7 @@ public static class EmoteCache
|
||||
catch (Exception ex)
|
||||
{
|
||||
Failed = true;
|
||||
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +411,7 @@ public static class EmoteCache
|
||||
catch (Exception ex)
|
||||
{
|
||||
Failed = true;
|
||||
Plugin.Log.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||
Plugin.LogProxy.Error(ex, $"Unable to load {emote.Code} with id {emote.Id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-10
@@ -44,16 +44,26 @@ public class FontManager
|
||||
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
|
||||
private static byte[]? HellionFontBytes;
|
||||
|
||||
private static byte[] GetHellionFontBytes()
|
||||
// Returns null when the embedded font resource is missing. Should never
|
||||
// happen on a signed release build, but a broken csproj or hand-rolled
|
||||
// dev build can land here. Caller falls back to the system font path so
|
||||
// the plugin still loads instead of crashing the whole UiBuilder.
|
||||
private static byte[]? TryGetHellionFontBytes()
|
||||
{
|
||||
if (HellionFontBytes is not null)
|
||||
return HellionFontBytes;
|
||||
|
||||
using var stream =
|
||||
typeof(FontManager).Assembly.GetManifestResourceStream("HellionFont.ttf")
|
||||
?? throw new FileNotFoundException(
|
||||
"Hellion font resource not embedded in the assembly"
|
||||
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
|
||||
"HellionFont.ttf"
|
||||
);
|
||||
if (stream is null)
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
"Hellion font resource missing — falling back to system default font."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
HellionFontBytes = ms.ToArray();
|
||||
@@ -146,8 +156,11 @@ public class FontManager
|
||||
? Plugin.Config.FontSizeV2
|
||||
: Plugin.Config.GlobalFontV2.SizePt;
|
||||
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
|
||||
config.MergeFont = Plugin.Config.UseHellionFont
|
||||
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
|
||||
// F10.2: if the embedded font is missing, drop to the system font
|
||||
// path rather than letting the UiBuilder throw.
|
||||
var hellionBytes = Plugin.Config.UseHellionFont ? TryGetHellionFontBytes() : null;
|
||||
config.MergeFont = hellionBytes is not null
|
||||
? tk.AddFontFromMemory(hellionBytes, config, "Hellion-Exo2")
|
||||
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
|
||||
|
||||
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
|
||||
@@ -213,11 +226,21 @@ public class FontManager
|
||||
return fontId.AddToBuildToolkit(tk, config);
|
||||
}
|
||||
catch (Exception e)
|
||||
when (e is FileNotFoundException or DirectoryNotFoundException or IOException)
|
||||
when (e
|
||||
is FileNotFoundException
|
||||
or DirectoryNotFoundException
|
||||
or IOException
|
||||
or InvalidOperationException
|
||||
or ArgumentException
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
// Atlas-toolkit throws span IO and validation failures; routing the
|
||||
// wider set through the fallback keeps a corrupt font config from
|
||||
// taking down the whole atlas build.
|
||||
Plugin.LogProxy.Warning(
|
||||
e,
|
||||
$"Configured {slot} font unavailable, falling back to NotoSansCjkRegular"
|
||||
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
||||
+ "falling back to NotoSansCjkRegular"
|
||||
);
|
||||
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
|
||||
return fallback.AddToBuildToolkit(tk, config);
|
||||
|
||||
@@ -236,7 +236,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -266,7 +266,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
|
||||
return 1; // Prevent vanilla chat log from gaining focus
|
||||
@@ -299,7 +299,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||
worldId = agent->TellWorldId;
|
||||
Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}");
|
||||
Plugin.LogProxy.Debug($"Detected tell target '[redacted]'@{worldId}");
|
||||
}
|
||||
|
||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||
@@ -358,7 +358,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,16 +423,24 @@ internal sealed unsafe class Chat : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
// Check if channel is valid (non-linkshell or existing linkshell)
|
||||
internal static bool ValidAnyLinkshell(InputChannel channel)
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Renamed ValidAnyLinkshell -> IsChannelOrExistingLinkshell. The
|
||||
// name now states intent: returns true for any non-linkshell
|
||||
// channel, or a linkshell index that actually exists.
|
||||
// ---------------------------------------------------------------
|
||||
internal static bool IsChannelOrExistingLinkshell(InputChannel channel)
|
||||
{
|
||||
var idx = channel.LinkshellIndex();
|
||||
if (idx == uint.MaxValue || channel.IsExtraChatLinkshell())
|
||||
return true;
|
||||
if (channel.IsLinkshell() && ValidLinkshell(idx))
|
||||
return true;
|
||||
if (channel.IsCrossLinkshell() && ValidCrossLinkshell(idx))
|
||||
return true;
|
||||
|
||||
if (channel.IsLinkshell())
|
||||
return ValidLinkshell(idx);
|
||||
|
||||
if (channel.IsCrossLinkshell())
|
||||
return ValidCrossLinkshell(idx);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -531,12 +539,17 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (idx == uint.MaxValue)
|
||||
idx = 0;
|
||||
|
||||
if (!ValidAnyLinkshell(channel))
|
||||
return;
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Wrap ChangeChatChannel in the validity check instead of
|
||||
// early-returning. The previous early return skipped Dtor and
|
||||
// leaked the native Utf8String allocated a few lines above.
|
||||
// ---------------------------------------------------------------
|
||||
if (IsChannelOrExistingLinkshell(channel))
|
||||
RaptureShellModule
|
||||
.Instance()
|
||||
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||
|
||||
RaptureShellModule
|
||||
.Instance()
|
||||
->ChangeChatChannel(tellTarget != null ? 17 : (int)channel, idx, target, true);
|
||||
target->Dtor(true);
|
||||
}
|
||||
|
||||
@@ -611,7 +624,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
if (contentId == 0)
|
||||
{
|
||||
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
"Tried to send a tell with ContentId being 0, sorry this is an internal error."
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -215,7 +215,7 @@ internal unsafe class GameFunctions : IDisposable
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Warning(e, "Unable to open adventurer plate");
|
||||
Plugin.LogProxy.Warning(e, "Unable to open adventurer plate");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,7 @@ internal unsafe class GameFunctions : IDisposable
|
||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||
if (byteCount >= PlaceholderBufferSize)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
||||
);
|
||||
ReplacementName = null;
|
||||
|
||||
@@ -507,7 +507,7 @@ internal unsafe class KeybindManager : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in chat Activated event");
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,5 +40,11 @@ public class TellTarget
|
||||
|
||||
public static TellTarget Empty() => new(string.Empty, 0, 0, TellReason.Direct);
|
||||
|
||||
public static TellTarget From(TellTarget t) => new(t.Name, t.World, t.ContentId, t.Reason);
|
||||
// ---------------------------------------------------------------
|
||||
// Cherry-picked from ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12)
|
||||
// - Replaced static From(t) with an instance-style Clone() so call
|
||||
// sites read like a copy operation, not a factory.
|
||||
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/TellTargetCloneTests.cs
|
||||
// ---------------------------------------------------------------
|
||||
public TellTarget Clone() => new(Name, World, ContentId, Reason);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||
<PropertyGroup>
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.4.3</Version>
|
||||
<Version>1.4.7</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
|
||||
+115
-16
@@ -35,29 +35,128 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
changelog: |-
|
||||
**v1.4.3 — Faster plugin load + new repo (2026-05-08)**
|
||||
**v1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)**
|
||||
|
||||
Heavy startup work (migrations, hooks, windows) now runs async so
|
||||
Dalamud's UI stays responsive during load. Load time is comparable
|
||||
to v1.4.2 — this is the foundation for v1.4.4 optimisations.
|
||||
Eighth sub-patch of the v1.4.x polish-sweep series. First
|
||||
user-visible feature bundle since v1.4.5 — pinned tell tabs that
|
||||
survive relog, opt-in Honorific glow rendering, and a configurable
|
||||
sidebar.
|
||||
|
||||
- Two-phase async load via IAsyncDalamudPlugin
|
||||
- Schema-gate replaces the v9→v16 migration chain; old configs
|
||||
require a v1.4.2 install first
|
||||
- AutoTranslate cache loads on first use instead of every startup
|
||||
- Custom font (Hellion-Exo2) appears with a brief pop after load
|
||||
- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL
|
||||
- TempTell Pin: right-click a TempTell tab in the sidebar to pin
|
||||
it. Pinned tabs survive relog, keep their conversation history
|
||||
(loaded on demand from the message store), and stay bound to
|
||||
the same /tell partner. Hard cap of 5 pinned tabs in a pool
|
||||
separate from the 15-tab auto-tell pool — total ceiling is 20
|
||||
tabs. New 'Pinned' section in the sidebar with its own divider
|
||||
header
|
||||
- Honorific Glow outline now renders when the title carries a
|
||||
Glow colour. Opt-in via Settings → Integrations → 'Render glow
|
||||
outlines (Honorific)' (default off, dodges the per-frame
|
||||
DrawList overhead on low-end hardware). Gradient (Color3 /
|
||||
GradientColourSet / Wave / Pulse) is parsed but rendered
|
||||
statically — a later cycle will port the full animation
|
||||
- Sidebar width is now configurable in Theme & Layout (range
|
||||
44–160 px). Default stays icon-only; widen to fit section
|
||||
headers like 'Active Tells (3)' without truncation
|
||||
- Settings Save no longer pops the chat input back to /tell with
|
||||
a pinned partner — Configuration.UpdateFrom now preserves the
|
||||
runtime CurrentChannel across the persistent-tab merge, and
|
||||
TabSwitched deep-clones the seeded channel instead of sharing
|
||||
the previous tab's UsedChannel
|
||||
- Util/ImGuiUtil.cs DrawArrows IconButton id now uses
|
||||
(id + 1).ToString() instead of the operator-precedence quirk
|
||||
id + 1.ToString() — generated IDs stay numerically stable
|
||||
- Internal: IPluginLogProxy indirection over Dalamud's IPluginLog
|
||||
routes all ~91 Plugin.Log call sites through a testable proxy.
|
||||
MessageStore.Migrate0 can now run in xUnit without loading
|
||||
Dalamud.dll, closing the gap F12.1 left in v1.4.6
|
||||
- Internal: TempTab counter switched from an Interlocked cached
|
||||
field to a derived Tabs.Count(predicate) — pin-state transitions
|
||||
are cold-path and don't need lock-free reads
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
**v1.4.2 — Smoother frames in the chat log**
|
||||
**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**
|
||||
|
||||
Per-frame allocations in the chat-log render path eliminated.
|
||||
2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.
|
||||
Maintenance patch. No user-visible behaviour changes; tightens the
|
||||
development feedback loop, fixes two upstream-inherited bugs, and
|
||||
prepares the code for the v1.4.7 backlog cleanup.
|
||||
|
||||
- Card-mode: theme/border invariants hoisted out of the per-message loop
|
||||
- Auto-tell tab tint and icon cached per tab
|
||||
- Status bar aggregation runs on ~1% of frames instead of every frame
|
||||
- preflight.sh gains a csharpier reflow check and a markdownlint
|
||||
pass so style drift and markdown violations are caught at the
|
||||
pre-push gate
|
||||
- FontManager fallback catches the full set of atlas-toolkit
|
||||
throws (IO, InvalidOperation, ArgumentException) — a corrupt
|
||||
font config no longer takes down the whole atlas build
|
||||
- BrandingLinks and IntegrationLinks URLs validated on plugin
|
||||
load — a typo in a future URL rotation now throws at startup
|
||||
- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel
|
||||
no longer leaks the native Utf8String when the linkshell check
|
||||
rejects the channel
|
||||
- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now
|
||||
deep-clones UsedChannel and TellTarget — PopOut and Temp tabs
|
||||
no longer mutate each other's channel state
|
||||
- Active-tab underline scales with DPI and rounds to physical
|
||||
pixels for crisp rendering above 100% scaling
|
||||
- IconButton width parameter no longer subtracts HUD-scaled
|
||||
padding from a raw int (measured width passes through verbatim)
|
||||
- Internal: HellionStyle ChildBgAlpha extracted to a testable
|
||||
helper; Plugin.SaveConfig clones only the temp tabs;
|
||||
SettingsOverview caches the draw-list per frame;
|
||||
Dalamud.Utility.Util surface routed through an IPlatformUtil
|
||||
indirection (MessageStore IsWine probe is now testable in
|
||||
isolation)
|
||||
- Built-in themes: Crystal Nocturne (sapphire and electric
|
||||
magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom.
|
||||
Users with Moonlit Bloom selected fall back to Hellion Arctic
|
||||
on first load
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
**v1.4.5 — UX and Robustness (2026-05-12)**
|
||||
|
||||
Sixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw
|
||||
failures surface as a notification, the first-run wizard has an
|
||||
explicit "Later" option, the input history clears on plugin reload,
|
||||
and the status bar version slot stops clipping in narrow windows.
|
||||
|
||||
- Chat window draw errors now show a one-shot notification instead
|
||||
of failing silently — stack trace stays in /xllog
|
||||
- First-run wizard: explicit "Later — keep defaults" button.
|
||||
Closing the X no longer silently accepts the defaults; the wizard
|
||||
reopens on the next plugin load if nothing was picked
|
||||
- InputHistoryService clears on plugin dispose so the previous
|
||||
session's typed commands don't bleed into the next load
|
||||
- Status bar hides the version slot when the chat window is too
|
||||
narrow to fit all five slots without overlap
|
||||
- Internal: explicit session-only Auto-Tell-Tab invariant in
|
||||
Plugin.cs plus a pinning test in the Build-Suite
|
||||
- Internal: FontManager falls back to the system font if the
|
||||
embedded Hellion font resource is missing — logs a Warning
|
||||
|
||||
---
|
||||
|
||||
**v1.4.4 — Threading and IPC safety polish (2026-05-12)**
|
||||
|
||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading
|
||||
assumptions are documented per-method, a hot-path lock falls
|
||||
away, and the privacy filter speaks up when an unknown ChatType
|
||||
shows up.
|
||||
|
||||
- AutoTellTabs hot-path getter uses an Interlocked counter
|
||||
instead of taking the lock on every read
|
||||
- Honorific integration: per-method threading banners, plus
|
||||
Warning-level log on unsubscribe failure
|
||||
- AutoTranslate warmup thread marked IsBackground so plugin
|
||||
unload doesn't wait for it
|
||||
- PrivacyFilter logs once per unknown ChatType so a future
|
||||
patch's added channel doesn't drop off the radar
|
||||
- New installs persist unknown channels by default; existing
|
||||
configs keep their explicit choice
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace HellionChat;
|
||||
|
||||
// Shared input history for all ChatInputBars (main and pop-out windows).
|
||||
// Push deduplicates: existing entries are moved to the end when re-added.
|
||||
// TEST-MIRROR: ../../Hellion Build test/Util/InputHistoryServiceTests.cs
|
||||
public static class InputHistoryService
|
||||
{
|
||||
private const int MaxSize = 30;
|
||||
@@ -41,4 +42,12 @@ public static class InputHistoryService
|
||||
return null;
|
||||
return _entries[cursor];
|
||||
}
|
||||
|
||||
// Plugin reload doesn't reset static state automatically. Plugin.DisposeAsync
|
||||
// calls this so the next load starts with an empty history instead of
|
||||
// inheriting the previous session's entries.
|
||||
public static void Reset()
|
||||
{
|
||||
_entries.Clear();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ internal sealed class HonorificService : IDisposable
|
||||
private readonly IFramework _framework;
|
||||
private bool _versionWarningLogged;
|
||||
|
||||
// Thread: framework only — IPC delivery + ImGui render both run there.
|
||||
public HonorificTitleData? CurrentTitle { get; private set; }
|
||||
public bool IsAvailable { get; private set; }
|
||||
public (uint Major, uint Minor)? DetectedApiVersion { get; private set; }
|
||||
@@ -71,6 +72,7 @@ internal sealed class HonorificService : IDisposable
|
||||
TryUnsubscribe(() => _disposing.Unsubscribe(OnDisposing));
|
||||
}
|
||||
|
||||
// Thread: framework (scheduled from ctor and OnReady).
|
||||
private void TryInitialPull()
|
||||
{
|
||||
try
|
||||
@@ -108,6 +110,7 @@ internal sealed class HonorificService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// Thread: framework (Dalamud IPC delivery contract).
|
||||
private void OnTitleChanged(string json)
|
||||
{
|
||||
// Skip updates on version mismatch; subscription stays live for reload.
|
||||
@@ -116,12 +119,13 @@ internal sealed class HonorificService : IDisposable
|
||||
CurrentTitle = ParseTitleJson(json);
|
||||
}
|
||||
|
||||
// Thread: any (Honorific dispatches NotifyReady from its own thread).
|
||||
private void OnReady()
|
||||
{
|
||||
// Schedule on framework thread — NotifyReady can dispatch from any thread.
|
||||
_framework.RunOnFrameworkThread(TryInitialPull);
|
||||
}
|
||||
|
||||
// Thread: framework (IPC delivery contract); idempotent — Disposing fires once.
|
||||
private void OnDisposing()
|
||||
{
|
||||
// Honorific unloading — clear cached state so the header hides next frame.
|
||||
@@ -133,6 +137,8 @@ internal sealed class HonorificService : IDisposable
|
||||
DetectedApiVersion = null;
|
||||
}
|
||||
|
||||
// Thread: framework (called from Dispose, which runs on the framework
|
||||
// cleanup block in Plugin.DisposeAsync).
|
||||
private void TryUnsubscribe(Action unsubscribe)
|
||||
{
|
||||
try
|
||||
@@ -141,20 +147,15 @@ internal sealed class HonorificService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Debug(ex, "Honorific unsubscribe failed (likely already gone).");
|
||||
// Warning not Debug — a silent unsubscribe failure leaks a live
|
||||
// subscription across plugin reloads.
|
||||
_log.Warning(
|
||||
ex,
|
||||
"Honorific unsubscribe failed (likely API break or gate already gone)."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Threading: IPC events and ImGui both run on the framework thread, so
|
||||
// OnTitleChanged and the render path never race — no volatile/Interlocked
|
||||
// needed as long as Dalamud's framework-thread delivery contract holds.
|
||||
//
|
||||
// Constructor and OnReady are exceptions: they run outside that contract
|
||||
// (plugin-loader thread and Honorific's NotifyReady respectively), so both
|
||||
// use RunOnFrameworkThread to safely reach ObjectTable.LocalPlayer.
|
||||
|
||||
// --- Pure-logic helpers; tested via HellionChat.Tests/Integrations. ---
|
||||
|
||||
internal static HonorificTitleData? ParseTitleJson(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
|
||||
@@ -4,10 +4,19 @@ namespace HellionChat.Integrations;
|
||||
|
||||
// Local DTO mirroring Honorific's TitleData — no hard reference to Honorific.dll
|
||||
// so HellionChat loads cleanly when Honorific is absent.
|
||||
// Glow/gradient fields omitted; Cycle 1 renders primary Color only.
|
||||
//
|
||||
// v1.4.7: render Glow only. Gradient (Color3 / GradientColourSet / Style) is
|
||||
// parsed and stashed so a future cycle can render it without re-shaping the
|
||||
// JSON roundtrip — see vault anchor "Honorific Full Gradient Port" (would
|
||||
// need GradientSystem.cs + the hardcoded Pride-palette list ported, or an
|
||||
// upstream IPC PR exposing the resolved frame colour).
|
||||
internal sealed record HonorificTitleData(
|
||||
string? Title,
|
||||
bool IsPrefix,
|
||||
bool IsOriginal,
|
||||
Vector3? Color
|
||||
Vector3? Color,
|
||||
Vector3? Glow,
|
||||
Vector3? Color3,
|
||||
int? GradientColourSet,
|
||||
string? GradientAnimationStyle
|
||||
);
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Integrations;
|
||||
|
||||
// Third-party plugin URLs — separate from BrandingLinks (Hellion-owned URLs).
|
||||
@@ -5,4 +8,13 @@ internal static class IntegrationLinks
|
||||
{
|
||||
public const string HonorificRepo = "https://github.com/Caraxi/Honorific";
|
||||
public const string HonorificAuthor = "https://github.com/Caraxi";
|
||||
|
||||
// See BrandingLinks.ValidateUrls for the CA2255 rationale.
|
||||
#pragma warning disable CA2255
|
||||
[ModuleInitializer]
|
||||
#pragma warning restore CA2255
|
||||
internal static void ValidateUrls()
|
||||
{
|
||||
UrlValidation.ValidateAll(nameof(IntegrationLinks), HonorificRepo, HonorificAuthor);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,10 @@ public sealed class ExtraChat : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
|
||||
Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
||||
Plugin.LogProxy.Verbose(
|
||||
ex,
|
||||
"ExtraChat IPC initial state query failed (peer not loaded?)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ public partial class Message
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed to parse extra chat channel GUID");
|
||||
Plugin.Log.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
||||
Plugin.LogProxy.Error(ex, "Failed to parse extra chat channel GUID");
|
||||
Plugin.LogProxy.Error($"Byte Array: ${string.Join(", ", data[4..^1])}");
|
||||
return Guid.Empty;
|
||||
}
|
||||
}
|
||||
@@ -251,7 +251,7 @@ public partial class Message
|
||||
AddChunkWithMessage(
|
||||
text.NewWithStyle(chunk.Source, chunk.Link, token.Value)
|
||||
);
|
||||
Plugin.Log.Debug(
|
||||
Plugin.LogProxy.Debug(
|
||||
$"Invalid URL accepted by Regex but failed URI parsing: '{token.Value}'"
|
||||
);
|
||||
}
|
||||
@@ -416,7 +416,7 @@ public partial class Message
|
||||
catch (Exception)
|
||||
{
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, split));
|
||||
Plugin.Log.Debug($"Failed to parse the text param: '{split}'");
|
||||
Plugin.LogProxy.Debug($"Failed to parse the text param: '{split}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
{
|
||||
Plugin = plugin;
|
||||
|
||||
Store = new MessageStore(DatabasePath());
|
||||
Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil, Plugin.LogProxy);
|
||||
|
||||
PendingMessageThread = new Thread(() =>
|
||||
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||
@@ -91,7 +91,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
await Task.Delay(100);
|
||||
|
||||
if (PendingMessageThread.IsAlive)
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
"PendingMessageThread did not observe cancellation within 10s. "
|
||||
+ "Worker remains on background thread; next plugin reload releases it."
|
||||
);
|
||||
@@ -137,7 +137,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error processing pending message");
|
||||
Plugin.LogProxy.Error(ex, "Error processing pending message");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -182,10 +182,12 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
// Mark failed messages as deleted to prevent retry attempts
|
||||
var failedIds = messages.FailedMessageIds();
|
||||
Plugin.Log.Info($"Marking {failedIds.Count} messages as deleted due to parse failures");
|
||||
Plugin.LogProxy.Info(
|
||||
$"Marking {failedIds.Count} messages as deleted due to parse failures"
|
||||
);
|
||||
foreach (var msgId in messages.FailedMessageIds())
|
||||
{
|
||||
Plugin.Log.Debug($"Marking message '{msgId}' as deleted due to parse failure");
|
||||
Plugin.LogProxy.Debug($"Marking message '{msgId}' as deleted due to parse failure");
|
||||
Store.DeleteMessage(msgId);
|
||||
}
|
||||
}
|
||||
@@ -201,10 +203,10 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in FilterAllTabs");
|
||||
Plugin.LogProxy.Error(ex, "Error in FilterAllTabs");
|
||||
}
|
||||
|
||||
Plugin.Log.Debug($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||
Plugin.LogProxy.Debug($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,7 +261,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error in ContentIdResolver");
|
||||
Plugin.LogProxy.Error(ex, "Error in ContentIdResolver");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-20
@@ -9,7 +9,6 @@ using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using DalamudUtil = Dalamud.Utility.Util;
|
||||
using Encoding = System.Text.Encoding;
|
||||
|
||||
namespace HellionChat;
|
||||
@@ -137,9 +136,14 @@ internal class MessageStore : IDisposable
|
||||
)
|
||||
);
|
||||
|
||||
internal MessageStore(string dbPath)
|
||||
private readonly IPlatformUtil _platformUtil;
|
||||
private readonly IPluginLogProxy _logger;
|
||||
|
||||
internal MessageStore(string dbPath, IPlatformUtil platformUtil, IPluginLogProxy logger)
|
||||
{
|
||||
DbPath = dbPath;
|
||||
_platformUtil = platformUtil;
|
||||
_logger = logger;
|
||||
Connection = Connect();
|
||||
Migrate();
|
||||
}
|
||||
@@ -166,7 +170,7 @@ internal class MessageStore : IDisposable
|
||||
conn.Open();
|
||||
conn.Execute(@"PRAGMA journal_mode=WAL;");
|
||||
conn.Execute(@"PRAGMA synchronous=NORMAL;");
|
||||
if (DalamudUtil.IsWine())
|
||||
if (_platformUtil.IsWine)
|
||||
conn.Execute(@"PRAGMA cache_size = 32768;");
|
||||
return conn;
|
||||
}
|
||||
@@ -202,7 +206,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate0()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 0: Creating tables");
|
||||
_logger.Information("Running migration 0: Creating tables");
|
||||
Connection.Execute(
|
||||
@"
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
@@ -229,7 +233,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate1()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 1: Adding Deleted column");
|
||||
_logger.Information("Running migration 1: Adding Deleted column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -241,7 +245,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate2()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 2: Adding Channel generated column");
|
||||
_logger.Information("Running migration 2: Adding Channel generated column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
|
||||
@@ -269,15 +273,13 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate3()
|
||||
{
|
||||
Plugin.Log.Information("Running migration 3: Fix log kinds to fit the new format");
|
||||
_logger.Information("Running migration 3: Fix log kinds to fit the new format");
|
||||
|
||||
// Recovery for partially-applied Migrate3: schema already in target
|
||||
// shape but user_version was never bumped -- just record and exit.
|
||||
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
|
||||
{
|
||||
Plugin.Log.Information(
|
||||
"Migration 3: schema already migrated, only bumping user_version"
|
||||
);
|
||||
_logger.Information("Migration 3: schema already migrated, only bumping user_version");
|
||||
SetMigrationVersion(3);
|
||||
return;
|
||||
}
|
||||
@@ -307,7 +309,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void SetMigrationVersion(int version)
|
||||
{
|
||||
Plugin.Log.Information($"Setting version {version}");
|
||||
_logger.Information($"Setting version {version}");
|
||||
using var cmd = Connection.CreateCommand();
|
||||
// PRAGMA does not accept SQLite parameter bindings; version is a
|
||||
// compile-time int from the migration sequence, never user input.
|
||||
@@ -459,7 +461,7 @@ internal class MessageStore : IDisposable
|
||||
// Privacy filter -- drop disallowed ChatTypes before they reach storage.
|
||||
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
||||
{
|
||||
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||
_logger.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -552,7 +554,7 @@ internal class MessageStore : IDisposable
|
||||
if (to is not null)
|
||||
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
// Returns the most recent messages, oldest-first.
|
||||
@@ -600,7 +602,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
cmd.Parameters.AddWithValue("$Count", count);
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
// Returns up to limit tells exchanged with the named player, oldest-first.
|
||||
@@ -638,7 +640,7 @@ internal class MessageStore : IDisposable
|
||||
cmd.Parameters.AddWithValue("$ScanLimit", sqlScanLimit);
|
||||
|
||||
var collected = new List<Message>();
|
||||
using var enumerator = new MessageEnumerator(cmd.ExecuteReader());
|
||||
using var enumerator = new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
foreach (var message in enumerator)
|
||||
{
|
||||
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
|
||||
@@ -730,7 +732,7 @@ internal class MessageStore : IDisposable
|
||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
internal MessageEnumerator GetPagedDateRange(
|
||||
@@ -774,7 +776,7 @@ internal class MessageStore : IDisposable
|
||||
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
|
||||
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
|
||||
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
return new MessageEnumerator(cmd.ExecuteReader(), _logger);
|
||||
}
|
||||
|
||||
// Builds a "$prefix0,$prefix1,..." placeholder list and binds values to the command.
|
||||
@@ -794,13 +796,14 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal class MessageEnumerator(DbDataReader reader)
|
||||
internal class MessageEnumerator(DbDataReader reader, IPluginLogProxy logger)
|
||||
: IEnumerable<Message>,
|
||||
IDisposable,
|
||||
IAsyncDisposable
|
||||
{
|
||||
private const int MaxErrorLogs = 10;
|
||||
|
||||
private readonly IPluginLogProxy _logger = logger;
|
||||
private readonly List<Guid> FailedIds = [];
|
||||
private int FailedCount;
|
||||
public bool DidError => FailedCount > 0;
|
||||
@@ -846,10 +849,10 @@ internal class MessageEnumerator(DbDataReader reader)
|
||||
catch (Exception e)
|
||||
{
|
||||
if (FailedCount < MaxErrorLogs)
|
||||
Plugin.Log.Error($"Exception while reading message '{id}' from database: {e}");
|
||||
_logger.Error($"Exception while reading message '{id}' from database: {e}");
|
||||
FailedCount++;
|
||||
if (FailedCount == MaxErrorLogs)
|
||||
Plugin.Log.Error("Further parsing errors will not be logged");
|
||||
_logger.Error("Further parsing errors will not be logged");
|
||||
if (id != Guid.Empty)
|
||||
FailedIds.Add(id);
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ public sealed class PayloadHandler
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error executing integration");
|
||||
Plugin.LogProxy.Error(ex, "Error executing integration");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +535,7 @@ public sealed class PayloadHandler
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Could not find DalamudLinkHandlers");
|
||||
Plugin.LogProxy.Warning("Could not find DalamudLinkHandlers");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@ public sealed class PayloadHandler
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error executing DalamudLinkPayload handler");
|
||||
Plugin.LogProxy.Error(ex, "Error executing DalamudLinkPayload handler");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-11
@@ -113,6 +113,15 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
internal Ui.StatusBar StatusBar { get; private set; } = null!;
|
||||
internal Integrations.HonorificService HonorificService { get; private set; } = null!;
|
||||
|
||||
// Platform indirection over Dalamud.Utility.Util. Wired in Phase-1 ctor so
|
||||
// any service allocated in LoadAsync can read Plugin.PlatformUtil.
|
||||
internal static IPlatformUtil PlatformUtil { get; private set; } = null!;
|
||||
|
||||
// Log indirection over Dalamud's IPluginLog. Same rationale as PlatformUtil:
|
||||
// call-sites read through LogProxy so MessageStore can be tested in
|
||||
// isolation. Wired immediately after Dalamud injects Log.
|
||||
internal static IPluginLogProxy LogProxy { get; private set; } = null!;
|
||||
|
||||
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
|
||||
private int _disposeStarted;
|
||||
|
||||
@@ -154,18 +163,28 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
|
||||
// Schema gate: v1.4.3 requires config v16. Users on older schemas
|
||||
// must install v1.4.2 first to run the migration chain.
|
||||
// Wire platform indirection before LoadAsync allocates anything that
|
||||
// needs Util.* — services then read Plugin.PlatformUtil instead of
|
||||
// hitting the Dalamud static surface directly.
|
||||
PlatformUtil = new DalamudPlatformUtil();
|
||||
LogProxy = new DalamudPluginLogProxy(Log);
|
||||
|
||||
// Schema gate: v1.4.x requires config v16+. Users on older schemas
|
||||
// must install v1.4.2 first to run the migration chain. v17 adds
|
||||
// Tab.IsPinned (additive, no data migration needed) so v16 configs
|
||||
// load cleanly and get their Version stamp bumped after the gate.
|
||||
if (Config.Version < 16)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"HellionChat v1.4.3 requires config schema v16, got v{Config.Version}. "
|
||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.3."
|
||||
$"HellionChat v1.4.7 requires config schema v16, got v{Config.Version}. "
|
||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.7."
|
||||
);
|
||||
}
|
||||
Config.Version = 17;
|
||||
|
||||
// Drop session-only Auto-Tell-Tabs that a previous crash may have persisted.
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
// Unpinned TempTabs are session-only and dropped on every load. Pinned
|
||||
// TempTabs survive reload — Jin's tester feedback (v1.4.7).
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnLoad);
|
||||
|
||||
LanguageChanged(Interface.UiLanguage);
|
||||
ImGuiUtil.Initialize(this);
|
||||
@@ -372,6 +391,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
||||
failure = CaptureFailure(failure, () => EmoteCache.Dispose());
|
||||
// Static input history would otherwise survive the plugin reload.
|
||||
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
||||
|
||||
if (failure is not null)
|
||||
ExceptionDispatchInfo.Capture(failure).Throw();
|
||||
@@ -633,14 +654,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
internal void SaveConfig()
|
||||
{
|
||||
// Strip session-only Auto-Tell-Tabs before serialization; restore after.
|
||||
var snapshot = Config.Tabs.ToList();
|
||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||
// Only unpinned TempTabs are session-only — they move aside before
|
||||
// serialization and re-attach after. Pinned TempTabs stay in
|
||||
// Config.Tabs across the save so JSON includes them. Cloning only the
|
||||
// unpinned subset keeps the allocation proportional to
|
||||
// AutoTellTabsLimit (<=15) instead of the full tab list.
|
||||
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||
|
||||
Interface.SavePluginConfig(Config);
|
||||
|
||||
Config.Tabs.Clear();
|
||||
Config.Tabs.AddRange(snapshot);
|
||||
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||
}
|
||||
|
||||
internal void LanguageChanged(string langCode)
|
||||
|
||||
@@ -4,6 +4,12 @@ namespace HellionChat.Privacy;
|
||||
|
||||
internal static class PrivacyDefaults
|
||||
{
|
||||
// F3.1: failsafe for ChatTypes added by future FFXIV patches. New installs
|
||||
// persist unknown channels so a major patch's added ChatType isn't silently
|
||||
// dropped before the user can opt in or out. Existing configs keep their
|
||||
// explicit choice — see Configuration.cs PrivacyPersistUnknownChannels.
|
||||
internal const bool DefaultPersistUnknownChannels = true;
|
||||
|
||||
// DSGVO Art. 25 (Privacy by Default): only the player's own conversations
|
||||
// are persisted out-of-the-box. Public chat, NPC dialogue, system logs and
|
||||
// battle messages require explicit opt-in.
|
||||
|
||||
+14
@@ -114,6 +114,8 @@ internal class HellionStrings
|
||||
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
|
||||
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
|
||||
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
|
||||
internal static string Wizard_Cancel_Label => Get(nameof(Wizard_Cancel_Label));
|
||||
internal static string Wizard_Cancel_Tooltip => Get(nameof(Wizard_Cancel_Tooltip));
|
||||
|
||||
internal static string Export_Heading => Get(nameof(Export_Heading));
|
||||
internal static string Export_Help => Get(nameof(Export_Help));
|
||||
@@ -168,6 +170,16 @@ internal class HellionStrings
|
||||
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
|
||||
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
|
||||
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
|
||||
internal static string PinTab_MenuPin => Get(nameof(PinTab_MenuPin));
|
||||
internal static string PinTab_MenuUnpin => Get(nameof(PinTab_MenuUnpin));
|
||||
internal static string PinTab_MenuPromote => Get(nameof(PinTab_MenuPromote));
|
||||
internal static string PinTab_PromoteTooltip => Get(nameof(PinTab_PromoteTooltip));
|
||||
internal static string PinTab_LimitReached => Get(nameof(PinTab_LimitReached));
|
||||
internal static string PinTab_PinnedTooltip => Get(nameof(PinTab_PinnedTooltip));
|
||||
internal static string PinTab_PinTooltip => Get(nameof(PinTab_PinTooltip));
|
||||
internal static string PinTab_SectionHeader => Get(nameof(PinTab_SectionHeader));
|
||||
internal static string Settings_ThemeAndLayout_SidebarWidth_Name => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Name));
|
||||
internal static string Settings_ThemeAndLayout_SidebarWidth_Description => Get(nameof(Settings_ThemeAndLayout_SidebarWidth_Description));
|
||||
|
||||
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
|
||||
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
|
||||
@@ -368,6 +380,8 @@ internal class HellionStrings
|
||||
internal static string Settings_Integrations_Honorific_Status_Incompatible => Get(nameof(Settings_Integrations_Honorific_Status_Incompatible));
|
||||
internal static string Settings_Integrations_Honorific_Toggle => Get(nameof(Settings_Integrations_Honorific_Toggle));
|
||||
internal static string Settings_Integrations_Honorific_ToggleHint => Get(nameof(Settings_Integrations_Honorific_ToggleHint));
|
||||
internal static string Settings_Integrations_Honorific_Glow_Toggle => Get(nameof(Settings_Integrations_Honorific_Glow_Toggle));
|
||||
internal static string Settings_Integrations_Honorific_Glow_Hint => Get(nameof(Settings_Integrations_Honorific_Glow_Hint));
|
||||
internal static string Settings_Integrations_Honorific_LinkRepo => Get(nameof(Settings_Integrations_Honorific_LinkRepo));
|
||||
internal static string Settings_Integrations_Honorific_LinkAuthor => Get(nameof(Settings_Integrations_Honorific_LinkAuthor));
|
||||
internal static string Settings_Integrations_ComingSoon_SectionHeader => Get(nameof(Settings_Integrations_ComingSoon_SectionHeader));
|
||||
|
||||
@@ -222,6 +222,12 @@
|
||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||
<value>Wizard erneut zeigen</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Label" xml:space="preserve">
|
||||
<value>Später — Defaults behalten</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||
<value>Schließt den Wizard ohne Profil-Auswahl. Die Plugin-Defaults bleiben aktiv und der Wizard erscheint beim nächsten Plugin-Reload erneut.</value>
|
||||
</data>
|
||||
<data name="Export_Heading" xml:space="preserve">
|
||||
<value>Export (DSGVO Art. 15 — Auskunftsrecht)</value>
|
||||
</data>
|
||||
@@ -377,6 +383,36 @@
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Als begrüßt markieren.</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||
<value>Tab anpinnen</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||
<value>Tab lösen</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||
<value>In Standard-Tab umwandeln</value>
|
||||
</data>
|
||||
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||
<value>Wandelt den TempTell in einen regulären Tab um. Die Tell-Bindung an die Person geht verloren, der Tab fängt dann Nachrichten anhand der Channel-Filter ein. Für „Tab überlebt Relog" stattdessen „Tab anpinnen" wählen.</value>
|
||||
</data>
|
||||
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||
<value>Maximal {0} angepinnte Tell-Tabs erreicht. Erst einen lösen oder dauerhaft behalten.</value>
|
||||
</data>
|
||||
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||
<value>Angepinnt — überlebt Relog.</value>
|
||||
</data>
|
||||
<data name="PinTab_PinTooltip" xml:space="preserve">
|
||||
<value>Angepinnte Tabs überleben Relog und behalten die Bindung an die Tell-Person.</value>
|
||||
</data>
|
||||
<data name="PinTab_SectionHeader" xml:space="preserve">
|
||||
<value>Angepinnt</value>
|
||||
</data>
|
||||
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
|
||||
<value>Sidebar-Breite</value>
|
||||
</data>
|
||||
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
|
||||
<value>Breite der Tab-Sidebar in Pixeln. Default (44 px) ist Icon-only; breiter machen damit Sektion-Header wie „Aktive Tells (3)" nicht abgeschnitten werden.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat-Einstellungstab) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
@@ -392,7 +428,7 @@
|
||||
<value>Maximale Anzahl der Auto-Tell-Tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell.</value>
|
||||
<value>Beim Erreichen werden begrüßte Tabs mit der ältesten Aktivität zuerst geschlossen. Änderungen greifen beim nächsten /tell. Diese Grenze gilt nur für den automatisch verwalteten Pool. Angepinnte Tell-Tabs (Rechtsklick → Tab anpinnen) leben in einem separaten Pool von bis zu 5 Tabs und überleben Relog.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Kompakte Anzeige</value>
|
||||
@@ -821,6 +857,12 @@
|
||||
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
||||
<value>Zeigt deinen Custom-Titel aus Honorific im Header über dem Chat-Log an, in der von dir gewählten Farbe.</value>
|
||||
</data>
|
||||
<data name="Settings_Integrations_Honorific_Glow_Toggle" xml:space="preserve">
|
||||
<value>Glow-Outline rendern (Honorific)</value>
|
||||
</data>
|
||||
<data name="Settings_Integrations_Honorific_Glow_Hint" xml:space="preserve">
|
||||
<value>Kann die Framerate auf schwacher Hardware drücken. Rendert die Glow-Outline für Honorific-Titel, die sie nutzen. Gradient-Animation wird noch nicht unterstützt und wird stattdessen als Primärfarbe gezeichnet.</value>
|
||||
</data>
|
||||
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
||||
<value>Honorific auf GitHub</value>
|
||||
</data>
|
||||
|
||||
@@ -222,6 +222,12 @@
|
||||
<data name="Wizard_Reopen_Button" xml:space="preserve">
|
||||
<value>Show wizard again</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Label" xml:space="preserve">
|
||||
<value>Later — keep defaults</value>
|
||||
</data>
|
||||
<data name="Wizard_Cancel_Tooltip" xml:space="preserve">
|
||||
<value>Close the wizard without selecting a profile. The plugin defaults stay active and the wizard returns on next plugin load.</value>
|
||||
</data>
|
||||
<data name="Export_Heading" xml:space="preserve">
|
||||
<value>Export (GDPR Art. 15 — Right of access)</value>
|
||||
</data>
|
||||
@@ -377,6 +383,36 @@
|
||||
<data name="AutoTellTabs_UnGreetedTooltip" xml:space="preserve">
|
||||
<value>Mark as greeted.</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPin" xml:space="preserve">
|
||||
<value>Pin Tab</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuUnpin" xml:space="preserve">
|
||||
<value>Unpin Tab</value>
|
||||
</data>
|
||||
<data name="PinTab_MenuPromote" xml:space="preserve">
|
||||
<value>Promote to permanent</value>
|
||||
</data>
|
||||
<data name="PinTab_PromoteTooltip" xml:space="preserve">
|
||||
<value>Turns this TempTell into a regular tab. The tell binding to the partner is dropped — the tab will catch messages by its channel filters from now on. For "tab survives relog while staying bound to this partner", use Pin Tab instead.</value>
|
||||
</data>
|
||||
<data name="PinTab_PinTooltip" xml:space="preserve">
|
||||
<value>Pinned tabs survive relog and stay bound to this conversation partner.</value>
|
||||
</data>
|
||||
<data name="PinTab_SectionHeader" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
</data>
|
||||
<data name="Settings_ThemeAndLayout_SidebarWidth_Name" xml:space="preserve">
|
||||
<value>Sidebar width</value>
|
||||
</data>
|
||||
<data name="Settings_ThemeAndLayout_SidebarWidth_Description" xml:space="preserve">
|
||||
<value>Width of the tab sidebar in pixels. The default (44 px) is icon-only; widen it to fit the section headers like "Active Tells (3)" without truncation.</value>
|
||||
</data>
|
||||
<data name="PinTab_LimitReached" xml:space="preserve">
|
||||
<value>Maximum of {0} pinned tell tabs reached. Unpin one first, or use Promote to permanent.</value>
|
||||
</data>
|
||||
<data name="PinTab_PinnedTooltip" xml:space="preserve">
|
||||
<value>Pinned — survives relog.</value>
|
||||
</data>
|
||||
|
||||
<!-- Hellion Chat — Auto-Tell-Tabs (Chat settings tab) -->
|
||||
<data name="ChatLog_AutoTellTabs_Section_Title" xml:space="preserve">
|
||||
@@ -392,7 +428,7 @@
|
||||
<value>Maximum number of auto-tell tabs</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Limit_Description" xml:space="preserve">
|
||||
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell.</value>
|
||||
<value>When the limit is reached, greeted tabs with the oldest activity are closed first. Changes take effect on the next /tell. This limit applies to the auto-managed pool. Pinned tell tabs (right-click → Pin Tab) live in a separate pool of up to 5 and survive relog.</value>
|
||||
</data>
|
||||
<data name="ChatLog_AutoTellTabs_Compact_Name" xml:space="preserve">
|
||||
<value>Compact display</value>
|
||||
@@ -821,6 +857,12 @@
|
||||
<data name="Settings_Integrations_Honorific_ToggleHint" xml:space="preserve">
|
||||
<value>Shows your custom title from Honorific in the header above the chat log, in the colour you have chosen.</value>
|
||||
</data>
|
||||
<data name="Settings_Integrations_Honorific_Glow_Toggle" xml:space="preserve">
|
||||
<value>Render glow outlines (Honorific)</value>
|
||||
</data>
|
||||
<data name="Settings_Integrations_Honorific_Glow_Hint" xml:space="preserve">
|
||||
<value>May reduce frame rate on low-end hardware. Renders glow outlines for Honorific titles that use them. Gradient animation is not yet supported and will render as the primary colour.</value>
|
||||
</data>
|
||||
<data name="Settings_Integrations_Honorific_LinkRepo" xml:space="preserve">
|
||||
<value>Honorific on GitHub</value>
|
||||
</data>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Themes.Builtin;
|
||||
|
||||
internal static class CrystalNocturne
|
||||
{
|
||||
public const string Slug = "crystal-nocturne";
|
||||
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Crystal Nocturne",
|
||||
Author: "CRYSTALLITE",
|
||||
Description: "Royal sapphire and electric magenta over obsidian — a nocturne for the crystal-lit dance floor.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#1D4ED8"),
|
||||
Primary: ColourUtil.HexToRgba("#3B82F6"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#93C5FD"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#3B82F699"),
|
||||
AccentDark: ColourUtil.HexToRgba("#A21CAF"),
|
||||
Accent: ColourUtil.HexToRgba("#D946EF"),
|
||||
AccentLight: ColourUtil.HexToRgba("#F0ABFC"),
|
||||
Identity: ColourUtil.HexToRgba("#3B82F6"),
|
||||
WindowBg: ColourUtil.HexToRgba("#08070F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#11101F"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1C1A33"),
|
||||
Surface: ColourUtil.HexToRgba("#262340"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#332D55"),
|
||||
Border: ColourUtil.HexToRgba("#D946EF55"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#F5F3FF"),
|
||||
TextMuted: ColourUtil.HexToRgba("#A5A0C0"),
|
||||
TextDim: ColourUtil.HexToRgba("#4B4763"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#10B981"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#F43F5E"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#FACC15"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#3B82F6")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 2f,
|
||||
ChildRounding: 1f,
|
||||
PopupRounding: 2f,
|
||||
FrameRounding: 1f,
|
||||
GrabRounding: 1f,
|
||||
TabRounding: 1f,
|
||||
ScrollbarRounding: 2f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Crystal Nocturne — sapphire-blue identity for party/team channels,
|
||||
// accent-magenta for tells, with mint/peach accents on linkshells
|
||||
// so the eight LS slots stay individually distinguishable on the
|
||||
// dark obsidian background.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#F5F3FF"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FACC15"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#93C5FD"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#10B981"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#93C5FD"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#10B981"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FACC15"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0ABFC"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#93C5FD"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A5A0C0"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#D946EF"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#3B82F6"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#A5A0C0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using HellionChat.Util;
|
||||
|
||||
namespace HellionChat.Themes.Builtin;
|
||||
|
||||
internal static class MoonlitBloom
|
||||
{
|
||||
public const string Slug = "moonlit-bloom";
|
||||
|
||||
public static Theme Build() =>
|
||||
new(
|
||||
Slug: Slug,
|
||||
Name: "Moonlit Bloom",
|
||||
Author: "Hellion Forge",
|
||||
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
|
||||
Colors: new ThemeColors(
|
||||
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
|
||||
Primary: ColourUtil.HexToRgba("#E374E8"),
|
||||
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
|
||||
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
|
||||
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
Accent: ColourUtil.HexToRgba("#9CCB7C"),
|
||||
AccentLight: ColourUtil.HexToRgba("#B6E297"),
|
||||
Identity: ColourUtil.HexToRgba("#E374E8"),
|
||||
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
|
||||
ChildBg: ColourUtil.HexToRgba("#15122B"),
|
||||
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
|
||||
Surface: ColourUtil.HexToRgba("#28224A"),
|
||||
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
|
||||
Border: ColourUtil.HexToRgba("#E374E844"),
|
||||
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
|
||||
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
|
||||
TextDim: ColourUtil.HexToRgba("#554B6E"),
|
||||
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
|
||||
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
|
||||
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
|
||||
StatusInfo: ColourUtil.HexToRgba("#6278FF")
|
||||
),
|
||||
Layout: new ThemeLayout(
|
||||
WindowRounding: 6f,
|
||||
ChildRounding: 5f,
|
||||
PopupRounding: 5f,
|
||||
FrameRounding: 4f,
|
||||
GrabRounding: 4f,
|
||||
TabRounding: 4f,
|
||||
ScrollbarRounding: 4f,
|
||||
WindowBorderSize: 1f,
|
||||
FrameBorderSize: 1f
|
||||
),
|
||||
Typography: new ThemeTypography(),
|
||||
IsBuiltIn: true,
|
||||
ChatColors: new ThemeChatColors(
|
||||
new Dictionary<HellionChat.Code.ChatType, uint>
|
||||
{
|
||||
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
|
||||
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
|
||||
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
|
||||
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
|
||||
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
|
||||
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
|
||||
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
|
||||
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
|
||||
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
|
||||
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
|
||||
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
|
||||
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
|
||||
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
|
||||
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
|
||||
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
|
||||
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -15,17 +15,21 @@ public sealed class ThemeRegistry
|
||||
|
||||
public ThemeRegistry(string? customThemesDir = null)
|
||||
{
|
||||
// Insertion order drives the Theme-Picker grid layout (3 columns).
|
||||
// Row 1: blue family. Row 2: purple to magenta family.
|
||||
// Row 3: green / warm / classic. Row 4: Synthwave Sunset as a
|
||||
// retro bonus on its own line.
|
||||
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ HellionArctic.Slug, HellionArctic.Build() },
|
||||
{ HellionSpectrum.Slug, HellionSpectrum.Build() },
|
||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
|
||||
{ NightBlue.Slug, NightBlue.Build() },
|
||||
{ EventHorizon.Slug, EventHorizon.Build() },
|
||||
{ IndigoViolet.Slug, IndigoViolet.Build() },
|
||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||
{ CrystalNocturne.Slug, CrystalNocturne.Build() },
|
||||
{ MintGrove.Slug, MintGrove.Build() },
|
||||
{ ForgeMerchantman.Slug, ForgeMerchantman.Build() },
|
||||
{ Chat2Classic.Slug, Chat2Classic.Build() },
|
||||
{ SynthwaveSunset.Slug, SynthwaveSunset.Build() },
|
||||
};
|
||||
|
||||
@@ -114,7 +118,7 @@ public sealed class ThemeRegistry
|
||||
catch (Exception ex) when (IsRecoverableFileLock(ex))
|
||||
{
|
||||
// Editor mid-save: keep last known good, retry on next refresh.
|
||||
Plugin.Log.Debug(
|
||||
Plugin.LogProxy.Debug(
|
||||
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
|
||||
);
|
||||
if (cached.Theme is not null)
|
||||
|
||||
+208
-32
@@ -90,6 +90,10 @@ public sealed class ChatLogWindow : Window
|
||||
private bool PlayedClosingSound = true;
|
||||
private bool DrewThisFrame;
|
||||
|
||||
// One-shot guard so a recurring draw failure doesn't spam the
|
||||
// notification stack frame-by-frame. Resets only on next plugin reload.
|
||||
private bool NotifiedDrawFailure;
|
||||
|
||||
private long FrameTime; // set every frame
|
||||
internal long LastActivityTime = Environment.TickCount64;
|
||||
|
||||
@@ -268,9 +272,12 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
if (targetChannel == null || !GameFunctions.Chat.ValidAnyLinkshell(targetChannel.Value))
|
||||
if (
|
||||
targetChannel == null
|
||||
|| !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
||||
);
|
||||
return;
|
||||
@@ -324,11 +331,11 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
case "hide":
|
||||
CurrentHideState = HideState.User;
|
||||
Plugin.Log.Verbose("HideState: → User (chat hide command)");
|
||||
Plugin.LogProxy.Verbose("HideState: → User (chat hide command)");
|
||||
break;
|
||||
case "show":
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose("HideState: → None (chat show command)");
|
||||
Plugin.LogProxy.Verbose("HideState: → None (chat show command)");
|
||||
break;
|
||||
case "toggle":
|
||||
CurrentHideState = CurrentHideState switch
|
||||
@@ -338,7 +345,7 @@ public sealed class ChatLogWindow : Window
|
||||
HideState.None => HideState.User,
|
||||
_ => CurrentHideState,
|
||||
};
|
||||
Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||
Plugin.LogProxy.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -434,11 +441,24 @@ public sealed class ChatLogWindow : Window
|
||||
|
||||
private void TabSwitched(Tab newTab, Tab previousTab)
|
||||
{
|
||||
// Use the fixed channel if set by the user, or set it to the current tabs channel if this tab wasn't accessed before
|
||||
// Use the fixed channel if set by the user. Otherwise, if the new tab
|
||||
// has no channel state yet (fresh from JSON, never selected this
|
||||
// session), seed from the previous tab — but deep-clone so we don't
|
||||
// share TellTarget with the previous tab. Without the clone, a later
|
||||
// /tell on the new tab would mutate the pinned tab's TellTarget and
|
||||
// the Party/Linkshell channel would pop back to the pinned tell-mark.
|
||||
if (newTab.Channel is not null)
|
||||
{
|
||||
newTab.CurrentChannel.Channel = newTab.Channel.Value;
|
||||
}
|
||||
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
|
||||
newTab.CurrentChannel = previousTab.CurrentChannel;
|
||||
{
|
||||
newTab.CurrentChannel = previousTab.CurrentChannel.Clone();
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' "
|
||||
+ $"(Channel={newTab.CurrentChannel.Channel}, TellTarget={newTab.CurrentChannel.TellTarget?.ToTargetString() ?? "null"})"
|
||||
);
|
||||
}
|
||||
|
||||
SetChannel(newTab.CurrentChannel.Channel);
|
||||
}
|
||||
@@ -462,14 +482,14 @@ public sealed class ChatLogWindow : Window
|
||||
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.Battle;
|
||||
Plugin.Log.Verbose("HideState: None → Battle");
|
||||
Plugin.LogProxy.Verbose("HideState: None → Battle");
|
||||
}
|
||||
|
||||
// If the chat is hidden because of battle, we reset it here
|
||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose("HideState: Battle → None");
|
||||
Plugin.LogProxy.Verbose("HideState: Battle → None");
|
||||
}
|
||||
|
||||
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
|
||||
@@ -482,7 +502,7 @@ public sealed class ChatLogWindow : Window
|
||||
if (Plugin.Functions.Chat.CheckHideFlags())
|
||||
{
|
||||
CurrentHideState = HideState.Cutscene;
|
||||
Plugin.Log.Verbose("HideState: None → Cutscene");
|
||||
Plugin.LogProxy.Verbose("HideState: None → Cutscene");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +513,7 @@ public sealed class ChatLogWindow : Window
|
||||
&& !Plugin.GposeActive
|
||||
)
|
||||
{
|
||||
Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||
Plugin.LogProxy.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||
CurrentHideState = HideState.None;
|
||||
}
|
||||
|
||||
@@ -501,14 +521,14 @@ public sealed class ChatLogWindow : Window
|
||||
if (CurrentHideState == HideState.Cutscene && Activate)
|
||||
{
|
||||
CurrentHideState = HideState.CutsceneOverride;
|
||||
Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||
Plugin.LogProxy.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
|
||||
}
|
||||
|
||||
// if the user hid the chat and is now activating chat, reset the hide state
|
||||
if (CurrentHideState == HideState.User && Activate)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose("HideState: User → None (activate)");
|
||||
Plugin.LogProxy.Verbose("HideState: User → None (activate)");
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -626,7 +646,20 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Error drawing Chat Log window");
|
||||
Plugin.LogProxy.Error(ex, "Error drawing Chat Log window");
|
||||
if (!NotifiedDrawFailure)
|
||||
{
|
||||
Plugin.Notification.AddNotification(
|
||||
new Dalamud.Interface.ImGuiNotification.Notification
|
||||
{
|
||||
Title = "Hellion Chat",
|
||||
Content = "A drawing error occurred. Check /xllog for details.",
|
||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
InitialDuration = TimeSpan.FromSeconds(20),
|
||||
}
|
||||
);
|
||||
NotifiedDrawFailure = true;
|
||||
}
|
||||
// Prevent recurring draw failures from constantly trying to grab
|
||||
// input focus, which breaks every other ImGui window.
|
||||
Activate = false;
|
||||
@@ -1588,7 +1621,7 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Warning(ex, "Error drawing chat log");
|
||||
Plugin.LogProxy.Warning(ex, "Error drawing chat log");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1620,17 +1653,21 @@ public sealed class ChatLogWindow : Window
|
||||
continue;
|
||||
|
||||
// Active-tab underline pill (2px accent). No native ImGui underline API,
|
||||
// so we use a direct DrawList pass.
|
||||
// so we use a direct DrawList pass. Pill height scales with GlobalScale
|
||||
// and all coordinates round to physical pixels so the line stays crisp
|
||||
// on 125/150% DPI setups instead of bleeding into a sub-pixel blur.
|
||||
{
|
||||
var theme = Plugin.ThemeRegistry.Active;
|
||||
var min = ImGui.GetItemRectMin();
|
||||
var max = ImGui.GetItemRectMax();
|
||||
const float pillHeight = 2f;
|
||||
var pillHeight = MathF.Max(1f, MathF.Round(2f * ImGuiHelpers.GlobalScale));
|
||||
var yBottom = MathF.Round(max.Y);
|
||||
var yTop = yBottom - pillHeight;
|
||||
ImGui
|
||||
.GetWindowDrawList()
|
||||
.AddRectFilled(
|
||||
new Vector2(min.X, max.Y - pillHeight),
|
||||
new Vector2(max.X, max.Y),
|
||||
new Vector2(MathF.Round(min.X), yTop),
|
||||
new Vector2(MathF.Round(max.X), yBottom),
|
||||
ColourUtil.RgbaToAbgr(theme.Colors.Accent)
|
||||
);
|
||||
}
|
||||
@@ -1649,6 +1686,30 @@ public sealed class ChatLogWindow : Window
|
||||
Plugin.WantedTab = null;
|
||||
}
|
||||
|
||||
// Sidebar render order: persistent tabs in their original Plugin.Config.Tabs
|
||||
// position, then pinned TempTabs, then unpinned TempTabs. Returns indices
|
||||
// into Plugin.Config.Tabs so tabI in the loop body still mirrors the real
|
||||
// list position (LastTab / WantedTab stay consistent).
|
||||
private static List<int> BuildSidebarRenderOrder()
|
||||
{
|
||||
var tabs = Plugin.Config.Tabs;
|
||||
var persistent = new List<int>(tabs.Count);
|
||||
var pinned = new List<int>();
|
||||
var unpinned = new List<int>();
|
||||
for (var i = 0; i < tabs.Count; i++)
|
||||
{
|
||||
if (TabLifecycleHelpers.IsInPinnedPool(tabs[i]))
|
||||
pinned.Add(i);
|
||||
else if (TabLifecycleHelpers.IsInUnpinnedPool(tabs[i]))
|
||||
unpinned.Add(i);
|
||||
else
|
||||
persistent.Add(i);
|
||||
}
|
||||
persistent.AddRange(pinned);
|
||||
persistent.AddRange(unpinned);
|
||||
return persistent;
|
||||
}
|
||||
|
||||
private void DrawTabSidebar()
|
||||
{
|
||||
var currentTab = -1;
|
||||
@@ -1661,7 +1722,8 @@ public sealed class ChatLogWindow : Window
|
||||
if (!tabTable.Success)
|
||||
return;
|
||||
|
||||
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, 44f);
|
||||
var sidebarWidth = Math.Clamp(Plugin.Config.SidebarWidth, 44, 160);
|
||||
ImGui.TableSetupColumn("tabs", ImGuiTableColumnFlags.WidthFixed, sidebarWidth);
|
||||
ImGui.TableSetupColumn("chat", ImGuiTableColumnFlags.WidthStretch, 1);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
@@ -1680,23 +1742,42 @@ public sealed class ChatLogWindow : Window
|
||||
ImGui.Dummy(new Vector2(0, ImGui.GetFrameHeightWithSpacing()));
|
||||
|
||||
var previousTab = Plugin.CurrentTab;
|
||||
// Divider rendered once before the first temp tab with a live unit counter.
|
||||
// Render order: persistent → pinned TempTabs → unpinned TempTabs.
|
||||
// Underlying Plugin.Config.Tabs order is untouched (tabI mirrors
|
||||
// the real list index), only the display sequence groups by
|
||||
// section so each section can carry its own divider header.
|
||||
var renderOrder = BuildSidebarRenderOrder();
|
||||
var pinnedHeaderRendered = false;
|
||||
var tempTabHeaderRendered = false;
|
||||
var tempTabCount = Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
||||
var pinnedCount = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
|
||||
var unpinnedTempCount = Plugin.Config.Tabs.Count(
|
||||
TabLifecycleHelpers.IsInUnpinnedPool
|
||||
);
|
||||
|
||||
for (var tabI = 0; tabI < Plugin.Config.Tabs.Count; tabI++)
|
||||
foreach (var tabI in renderOrder)
|
||||
{
|
||||
var tab = Plugin.Config.Tabs[tabI];
|
||||
if (tab.PopOut)
|
||||
continue;
|
||||
|
||||
if (tab.IsTempTab && !tempTabHeaderRendered)
|
||||
if (TabLifecycleHelpers.IsInPinnedPool(tab) && !pinnedHeaderRendered)
|
||||
{
|
||||
ImGui.Separator();
|
||||
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||
{
|
||||
ImGui.TextDisabled(
|
||||
$"{HellionStrings.AutoTellTabs_SectionHeader} ({tempTabCount})"
|
||||
$"{HellionStrings.PinTab_SectionHeader} ({pinnedCount})"
|
||||
);
|
||||
}
|
||||
pinnedHeaderRendered = true;
|
||||
}
|
||||
else if (TabLifecycleHelpers.IsInUnpinnedPool(tab) && !tempTabHeaderRendered)
|
||||
{
|
||||
ImGui.Separator();
|
||||
if (!Plugin.Config.AutoTellTabsCompactDisplay)
|
||||
{
|
||||
ImGui.TextDisabled(
|
||||
$"{HellionStrings.AutoTellTabs_SectionHeader} ({unpinnedTempCount})"
|
||||
);
|
||||
}
|
||||
tempTabHeaderRendered = true;
|
||||
@@ -1785,9 +1866,12 @@ public sealed class ChatLogWindow : Window
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(iconColor)))
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
{
|
||||
// Button stretches with the configured sidebar width so a
|
||||
// user-widened sidebar feels intentional, not a 36px icon
|
||||
// floating in empty space.
|
||||
clicked = ImGui.Button(
|
||||
$"{icon.ToIconString()}##sidebar-tab-{tabI}",
|
||||
new Vector2(36f, ImGui.GetFrameHeight())
|
||||
new Vector2(sidebarWidth - 8f, ImGui.GetFrameHeight())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1847,11 +1931,35 @@ public sealed class ChatLogWindow : Window
|
||||
);
|
||||
}
|
||||
|
||||
// Pin indicator: subtle thumbtack glyph top-left of the icon.
|
||||
// Muted colour because the "Pinned" section header already
|
||||
// groups these tabs visually — this is just a per-tab
|
||||
// confirmation glyph, not the primary discoverability cue.
|
||||
if (tab.IsPinned)
|
||||
{
|
||||
var min = ImGui.GetItemRectMin();
|
||||
const float pinPadding = 1f;
|
||||
var pinPos = new Vector2(min.X + pinPadding, min.Y + pinPadding);
|
||||
var pinColor = theme.Colors.TextMuted;
|
||||
// Dim further so the glyph reads as a hint, not a badge.
|
||||
var pinAbgr = ColourUtil.RgbaToAbgr(pinColor) & 0x77FFFFFFu;
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
{
|
||||
ImGui
|
||||
.GetWindowDrawList()
|
||||
.AddText(pinPos, pinAbgr, FontAwesomeIcon.Thumbtack.ToIconString());
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip mit Tab-Name + Unread-Counter beim Hover.
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using var tt = ImRaii.Tooltip();
|
||||
ImGui.TextUnformatted($"{tab.Name}{unread}");
|
||||
if (tab.IsPinned)
|
||||
{
|
||||
ImGui.TextUnformatted(HellionStrings.PinTab_PinnedTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
DrawTabContextMenu(tab, tabI);
|
||||
@@ -1975,10 +2083,7 @@ public sealed class ChatLogWindow : Window
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString());
|
||||
}
|
||||
ImGui.SameLine(0f, gapAfterCrown);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
||||
{
|
||||
ImGui.TextUnformatted(rendered);
|
||||
}
|
||||
DrawHonorificTitleText(rendered, titleColor, title.Glow);
|
||||
ImGui.EndGroup();
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
@@ -1989,6 +2094,35 @@ public sealed class ChatLogWindow : Window
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
// Renders the title text, optionally with a glow outline pre-pass. Glow is
|
||||
// drawn at 8 cardinal offsets (±1 px) in the glow colour at reduced alpha,
|
||||
// then the primary text on top. The pre-pass uses the window draw list so
|
||||
// it composites correctly with the regular ImGui text that follows.
|
||||
private void DrawHonorificTitleText(string rendered, Vector4 titleColor, Vector3? glow)
|
||||
{
|
||||
if (Plugin.Config.ShowHonorificGlow && glow is { } g)
|
||||
{
|
||||
var pos = ImGui.GetCursorScreenPos();
|
||||
var glowColor = new Vector4(g.X, g.Y, g.Z, 0.4f);
|
||||
var glowAbgr = ImGui.ColorConvertFloat4ToU32(glowColor);
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
for (var dy = -1; dy <= 1; dy++)
|
||||
{
|
||||
for (var dx = -1; dx <= 1; dx++)
|
||||
{
|
||||
if (dx == 0 && dy == 0)
|
||||
continue;
|
||||
drawList.AddText(new Vector2(pos.X + dx, pos.Y + dy), glowAbgr, rendered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
||||
{
|
||||
ImGui.TextUnformatted(rendered);
|
||||
}
|
||||
}
|
||||
|
||||
// One-time hint banner for the pop-out header button and right-click pathway.
|
||||
private float DrawV061HintBannerIfNeeded()
|
||||
{
|
||||
@@ -2035,7 +2169,7 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
Plugin.Config.SeenPopOutHeaderHint = true;
|
||||
Plugin.SaveConfig();
|
||||
Plugin.Log.Debug("v0.6.1 pop-out header hint dismissed");
|
||||
Plugin.LogProxy.Debug("v0.6.1 pop-out header hint dismissed");
|
||||
if (openSettings)
|
||||
Plugin.SettingsWindow.Toggle();
|
||||
}
|
||||
@@ -2100,10 +2234,52 @@ public sealed class ChatLogWindow : Window
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
if (tab.IsTempTab)
|
||||
{
|
||||
ImGui.Separator();
|
||||
DrawPinControls(tab);
|
||||
}
|
||||
|
||||
if (anyChanged)
|
||||
Plugin.SaveConfig();
|
||||
}
|
||||
|
||||
private void DrawPinControls(Tab tab)
|
||||
{
|
||||
var svc = Plugin.AutoTellTabsService;
|
||||
if (svc == null)
|
||||
return;
|
||||
|
||||
if (tab.IsPinned)
|
||||
{
|
||||
if (ImGui.MenuItem(HellionStrings.PinTab_MenuUnpin))
|
||||
{
|
||||
svc.Unpin(tab);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var atCap = svc.PinnedTempTabCount >= AutoTellTabsService.MaxPinnedTempTabs;
|
||||
if (ImGui.MenuItem(HellionStrings.PinTab_MenuPin, enabled: !atCap))
|
||||
{
|
||||
if (svc.TryPin(tab))
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||
{
|
||||
ImGui.SetTooltip(
|
||||
atCap
|
||||
? string.Format(
|
||||
HellionStrings.PinTab_LimitReached,
|
||||
AutoTellTabsService.MaxPinnedTempTabs
|
||||
)
|
||||
: HellionStrings.PinTab_PinTooltip
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly List<bool> PopOutDocked = [];
|
||||
internal readonly HashSet<Guid> PopOutWindows = [];
|
||||
|
||||
@@ -2648,7 +2824,7 @@ public sealed class ChatLogWindow : Window
|
||||
var viewport = ImGui.GetMainViewport();
|
||||
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
||||
Position = safePos;
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
|
||||
);
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ public class DbViewer : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed reading messages from database");
|
||||
Plugin.LogProxy.Error(ex, "Failed reading messages from database");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -570,7 +570,7 @@ public class DbViewer : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, "Failed creating txt backup");
|
||||
Plugin.LogProxy.Error(ex, "Failed creating txt backup");
|
||||
|
||||
Notification.Content = "Error ...";
|
||||
Notification.Type = NotificationType.Error;
|
||||
|
||||
@@ -30,14 +30,10 @@ public sealed class FirstRunWizard : Window
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
// Closing the wizard without picking anything = the user accepts
|
||||
// whatever defaults are already in place. Mark as complete so we
|
||||
// don't pester them again on the next launch.
|
||||
if (!Plugin.Config.FirstRunCompleted)
|
||||
{
|
||||
Plugin.Config.FirstRunCompleted = true;
|
||||
Plugin.SaveConfig();
|
||||
}
|
||||
// OnClose fires on explicit X-click and on plugin dispose. We never
|
||||
// implicitly accept the defaults here — the explicit "Later" button
|
||||
// does that. If the user hasn't picked a profile yet, the wizard
|
||||
// reopens on the next plugin load.
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
@@ -49,7 +45,12 @@ public sealed class FirstRunWizard : Window
|
||||
|
||||
var avail = ImGui.GetContentRegionAvail();
|
||||
var cardWidth = (avail.X - ImGui.GetStyle().ItemSpacing.X * 2) / 3f;
|
||||
var cardHeight = avail.Y - ImGui.GetTextLineHeightWithSpacing();
|
||||
// Reserve room for the footer separator + cancel button below the cards.
|
||||
var footerReserve =
|
||||
ImGui.GetStyle().ItemSpacing.Y * 3
|
||||
+ ImGui.GetTextLineHeight()
|
||||
+ ImGui.GetFrameHeightWithSpacing();
|
||||
var cardHeight = avail.Y - footerReserve;
|
||||
|
||||
DrawCard(
|
||||
"privacy-first",
|
||||
@@ -87,6 +88,20 @@ public sealed class FirstRunWizard : Window
|
||||
HellionStrings.Wizard_Profile_FullHistory_Apply,
|
||||
ApplyFullHistory
|
||||
);
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
if (ImGui.Button(HellionStrings.Wizard_Cancel_Label))
|
||||
{
|
||||
Plugin.Config.FirstRunCompleted = true;
|
||||
Plugin.SaveConfig();
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGuiUtil.Tooltip(HellionStrings.Wizard_Cancel_Tooltip);
|
||||
}
|
||||
|
||||
private void DrawCard(
|
||||
|
||||
@@ -43,13 +43,10 @@ internal static class HellionStyle
|
||||
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
|
||||
var windowBgWithAlpha = (c.WindowBg & 0xFFFFFF00u) | alphaByte;
|
||||
|
||||
// ChildBg alpha: child areas rendered inside ChatLogWindow would
|
||||
// multiply their alpha with WindowBg, making 50% opacity appear
|
||||
// ~75% solid. At full opacity the theme's alpha is preserved; below
|
||||
// it ChildBg goes fully transparent so only WindowBg sets the final
|
||||
// coverage.
|
||||
var childBgAlpha = windowOpacity >= 0.999f ? (c.ChildBg & 0xFFu) : 0u;
|
||||
var childBgWithAlpha = (c.ChildBg & 0xFFFFFF00u) | childBgAlpha;
|
||||
// ChildBg alpha resolution lives in HellionStyleHelpers so the
|
||||
// threshold logic can be covered by a pure-helper test in the
|
||||
// build suite.
|
||||
var childBgWithAlpha = HellionStyleHelpers.ResolveChildBgAlpha(c.ChildBg, windowOpacity);
|
||||
|
||||
// Layout
|
||||
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, l.WindowRounding);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
internal static class HellionStyleHelpers
|
||||
{
|
||||
// Child surfaces are drawn over WindowBg, so at partial window opacity
|
||||
// the theme's own ChildBg alpha would double-multiply and read too solid.
|
||||
// Above ~full opacity we preserve the theme alpha; below it we wipe to 0
|
||||
// so WindowBg alone carries the coverage. The 0.999f threshold is a
|
||||
// float-imprecision guard around the user-facing 100% slider value.
|
||||
// TEST-MIRROR: ../../Hellion Build test/_Helpers/HellionStyleHelpersTests.cs
|
||||
public static uint ResolveChildBgAlpha(uint themeChildBgRgba, float windowOpacity)
|
||||
{
|
||||
var alphaPreserved = windowOpacity >= 0.999f;
|
||||
var childBgAlpha = alphaPreserved ? (themeChildBgRgba & 0xFFu) : 0u;
|
||||
return (themeChildBgRgba & 0xFFFFFF00u) | childBgAlpha;
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ internal class Popout : Window
|
||||
{
|
||||
Plugin.Config.SeenPopOutInputHint = true;
|
||||
ChatLogWindow.Plugin.SaveConfig();
|
||||
Plugin.Log.Debug("Pop-Out input hint dismissed");
|
||||
Plugin.LogProxy.Debug("Pop-Out input hint dismissed");
|
||||
if (openSettings)
|
||||
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
||||
}
|
||||
@@ -214,13 +214,13 @@ internal class Popout : Window
|
||||
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.Battle;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle");
|
||||
}
|
||||
|
||||
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -232,7 +232,7 @@ internal class Popout : Window
|
||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||
{
|
||||
CurrentHideState = HideState.Cutscene;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ internal class Popout : Window
|
||||
&& !Plugin.GposeActive
|
||||
)
|
||||
{
|
||||
Plugin.Log.Verbose(
|
||||
Plugin.LogProxy.Verbose(
|
||||
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
|
||||
);
|
||||
CurrentHideState = HideState.None;
|
||||
@@ -251,7 +251,7 @@ internal class Popout : Window
|
||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||
{
|
||||
CurrentHideState = HideState.CutsceneOverride;
|
||||
Plugin.Log.Verbose(
|
||||
Plugin.LogProxy.Verbose(
|
||||
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
|
||||
);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ internal class Popout : Window
|
||||
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)");
|
||||
}
|
||||
|
||||
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
|
||||
|
||||
@@ -199,12 +199,12 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
);
|
||||
|
||||
if (ImGui.Button(buttonLabel2))
|
||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
|
||||
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/infiii");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button(buttonLabel))
|
||||
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/lojewalo");
|
||||
Plugin.PlatformUtil.OpenLink("https://ko-fi.com/lojewalo");
|
||||
}
|
||||
|
||||
if (!save)
|
||||
|
||||
@@ -79,11 +79,13 @@ internal sealed class SettingsOverview
|
||||
// 110f accommodates two-line subtexts; wrap width is matched in DrawCard.
|
||||
var cardHeight = 110f;
|
||||
|
||||
// One draw-list lookup per frame instead of one per card.
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var cardDefs = BuildCardDefs();
|
||||
for (var i = 0; i < cardDefs.Length; i++)
|
||||
{
|
||||
var (icon, title, subtext) = cardDefs[i];
|
||||
DrawCard(i, icon, title, subtext, cardWidth, cardHeight);
|
||||
DrawCard(i, icon, title, subtext, cardWidth, cardHeight, drawList);
|
||||
|
||||
if ((i + 1) % columns != 0 && i != cardDefs.Length - 1)
|
||||
ImGui.SameLine();
|
||||
@@ -96,7 +98,8 @@ internal sealed class SettingsOverview
|
||||
string title,
|
||||
string subtext,
|
||||
float w,
|
||||
float h
|
||||
float h,
|
||||
ImDrawListPtr drawList
|
||||
)
|
||||
{
|
||||
// BeginGroup makes the card a single layout item so SameLine works
|
||||
@@ -108,8 +111,7 @@ internal sealed class SettingsOverview
|
||||
var hovered = ImGui.IsItemHovered();
|
||||
var bgColor = hovered ? 0xFF22303Fu : 0xFF1A2538u;
|
||||
|
||||
var draw = ImGui.GetWindowDrawList();
|
||||
draw.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||
drawList.AddRectFilled(cursorBefore, cursorBefore + new Vector2(w, h), bgColor, 4f);
|
||||
|
||||
var iconPos = cursorBefore + new Vector2(16f, 12f);
|
||||
var titlePos = cursorBefore + new Vector2(16f, 40f);
|
||||
@@ -120,15 +122,15 @@ internal sealed class SettingsOverview
|
||||
|
||||
using (_window.Plugin.FontManager.FontAwesome.Push())
|
||||
{
|
||||
draw.AddText(iconPos, titleColor, icon.ToIconString());
|
||||
drawList.AddText(iconPos, titleColor, icon.ToIconString());
|
||||
}
|
||||
|
||||
draw.AddText(titlePos, titleColor, title);
|
||||
drawList.AddText(titlePos, titleColor, title);
|
||||
|
||||
// Subtext wraps at card inner width (16px padding each side) via DrawList
|
||||
// to avoid expanding the group bounds and breaking SameLine in the card row.
|
||||
var subtextWrapWidth = w - 32f;
|
||||
draw.AddText(
|
||||
drawList.AddText(
|
||||
ImGui.GetFont(),
|
||||
ImGui.GetFontSize(),
|
||||
subtextPos,
|
||||
|
||||
@@ -229,7 +229,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Unable to delete old database");
|
||||
Plugin.LogProxy.Error(e, "Unable to delete old database");
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Old_Delete_Error,
|
||||
NotificationType.Error
|
||||
@@ -391,7 +391,9 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||
Plugin.SaveConfig();
|
||||
|
||||
Plugin.Log.Information($"Manual retention run deleted {deleted} expired messages.");
|
||||
Plugin.LogProxy.Information(
|
||||
$"Manual retention run deleted {deleted} expired messages."
|
||||
);
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
@@ -405,7 +407,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning(
|
||||
Plugin.LogProxy.Warning(
|
||||
"Retention sweep: framework refresh timed out after 5s."
|
||||
);
|
||||
}
|
||||
@@ -418,7 +420,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Manual retention run failed");
|
||||
Plugin.LogProxy.Error(e, "Manual retention run failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
@@ -566,7 +568,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Failed to compute cleanup preview");
|
||||
Plugin.LogProxy.Error(e, "Failed to compute cleanup preview");
|
||||
WrapperUtil.AddNotification(
|
||||
HellionStrings.Cleanup_PreviewError,
|
||||
NotificationType.Error
|
||||
@@ -587,7 +589,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
try
|
||||
{
|
||||
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
||||
Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages");
|
||||
Plugin.LogProxy.Information($"Privacy cleanup: deleted {deleted} messages");
|
||||
|
||||
if (
|
||||
!Plugin
|
||||
@@ -599,7 +601,9 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||
Plugin.LogProxy.Warning(
|
||||
"Privacy cleanup: framework refresh timed out after 5s."
|
||||
);
|
||||
}
|
||||
|
||||
WrapperUtil.AddNotification(
|
||||
@@ -609,7 +613,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Privacy cleanup failed");
|
||||
Plugin.LogProxy.Error(e, "Privacy cleanup failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
@@ -769,7 +773,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.Log.Error(e, "Export failed");
|
||||
Plugin.LogProxy.Error(e, "Export failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
@@ -849,7 +853,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.Log.Warning("Clearing messages from database");
|
||||
Plugin.LogProxy.Warning("Clearing messages from database");
|
||||
Plugin.MessageManager.Store.ClearMessages();
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
|
||||
@@ -907,7 +911,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
private void InsertMessages(int count)
|
||||
{
|
||||
Plugin.Log.Info($"Inserting {count} messages due to user request");
|
||||
Plugin.LogProxy.Info($"Inserting {count} messages due to user request");
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var playerName = Plugin.PlayerState.CharacterName;
|
||||
@@ -952,7 +956,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
var elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
@@ -962,7 +966,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
@@ -973,7 +977,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
})
|
||||
@@ -986,7 +990,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.MessageManager.FilterAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.Log.Info(
|
||||
Plugin.LogProxy.Info(
|
||||
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
})
|
||||
|
||||
@@ -312,6 +312,6 @@ internal sealed class FontsAndColours : ISettingsTab
|
||||
}
|
||||
Plugin.SaveConfig();
|
||||
GlobalParametersCache.Refresh();
|
||||
Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
||||
Plugin.LogProxy.Debug($"Applied chat colour preset: {preset.DisplayName}");
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,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(
|
||||
Plugin.PlatformUtil.OpenLink(
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/issues"
|
||||
);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ internal sealed class Information : ISettingsTab
|
||||
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
|
||||
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
|
||||
Plugin.PlatformUtil.OpenLink("https://hellion-media.de");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
@@ -137,7 +137,7 @@ internal sealed class Information : ISettingsTab
|
||||
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
|
||||
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
||||
Plugin.PlatformUtil.OpenLink("https://github.com/Infiziert90/ChatTwo");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10.0f);
|
||||
|
||||
|
||||
@@ -71,6 +71,17 @@ internal sealed class Integrations : ISettingsTab
|
||||
{
|
||||
ImGui.TextWrapped(HellionStrings.Settings_Integrations_Honorific_ToggleHint);
|
||||
}
|
||||
|
||||
if (
|
||||
ImGui.Checkbox(
|
||||
HellionStrings.Settings_Integrations_Honorific_Glow_Toggle,
|
||||
ref Mutable.ShowHonorificGlow
|
||||
)
|
||||
)
|
||||
{
|
||||
Plugin.SaveConfig();
|
||||
}
|
||||
ImGuiUtil.HelpMarker(HellionStrings.Settings_Integrations_Honorific_Glow_Hint);
|
||||
}
|
||||
|
||||
// Honorific has no LICENSE in its repo so we link upstream and author
|
||||
@@ -79,12 +90,12 @@ internal sealed class Integrations : ISettingsTab
|
||||
ImGui.Spacing();
|
||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkRepo))
|
||||
{
|
||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificRepo);
|
||||
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificRepo);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button(HellionStrings.Settings_Integrations_Honorific_LinkAuthor))
|
||||
{
|
||||
Dalamud.Utility.Util.OpenLink(IntegrationLinks.HonorificAuthor);
|
||||
Plugin.PlatformUtil.OpenLink(IntegrationLinks.HonorificAuthor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +204,7 @@ internal sealed class Integrations : ISettingsTab
|
||||
|
||||
if (ImGui.Button(HellionStrings.Settings_Integrations_GotAnIdea_LinkLabel))
|
||||
{
|
||||
Dalamud.Utility.Util.OpenLink(BrandingLinks.HellionForgeDiscordInvite);
|
||||
Plugin.PlatformUtil.OpenLink(BrandingLinks.HellionForgeDiscordInvite);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
{
|
||||
var dir = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "themes");
|
||||
Directory.CreateDirectory(dir);
|
||||
Dalamud.Utility.Util.OpenLink(dir);
|
||||
Plugin.PlatformUtil.OpenLink(dir);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
@@ -90,7 +90,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
var path = Path.Combine(dir, fileName);
|
||||
var json = ThemeJsonWriter.Serialize(active);
|
||||
File.WriteAllText(path, json);
|
||||
Plugin.Log.Information($"Exported active theme '{active.Slug}' to {path}");
|
||||
Plugin.LogProxy.Information($"Exported active theme '{active.Slug}' to {path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,6 +250,26 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)
|
||||
);
|
||||
|
||||
if (Mutable.SidebarTabView)
|
||||
{
|
||||
var sidebarWidth = Mutable.SidebarWidth;
|
||||
if (
|
||||
ImGui.SliderInt(
|
||||
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Name,
|
||||
ref sidebarWidth,
|
||||
44,
|
||||
160,
|
||||
$"{sidebarWidth} px"
|
||||
)
|
||||
)
|
||||
{
|
||||
Mutable.SidebarWidth = sidebarWidth;
|
||||
}
|
||||
ImGuiUtil.HelpMarker(
|
||||
HellionStrings.Settings_ThemeAndLayout_SidebarWidth_Description
|
||||
);
|
||||
}
|
||||
|
||||
ImGui.Spacing();
|
||||
ImGui.Separator();
|
||||
ImGui.Spacing();
|
||||
|
||||
@@ -144,14 +144,20 @@ internal sealed class StatusBar
|
||||
ImGui.TextUnformatted(_cachedTellsText);
|
||||
}
|
||||
|
||||
// Slot 5: version, right-aligned, muted
|
||||
// Slot 5: version, right-aligned, muted. Hidden when the window is
|
||||
// too narrow to fit all five slots — the other four need ~200 px
|
||||
// before the version text starts clipping into them.
|
||||
var versionText = $"v{Plugin.Interface.Manifest.AssemblyVersion} · Hellion";
|
||||
var versionWidth = ImGui.CalcTextSize(versionText).X;
|
||||
var contentRegionMax = ImGui.GetContentRegionMax().X;
|
||||
ImGui.SameLine(contentRegionMax - versionWidth);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
||||
const float MinOtherSlotsWidth = 200f;
|
||||
if (contentRegionMax - versionWidth > MinOtherSlotsWidth)
|
||||
{
|
||||
ImGui.TextUnformatted(versionText);
|
||||
ImGui.SameLine(contentRegionMax - versionWidth);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(theme.Colors.TextMuted)))
|
||||
{
|
||||
ImGui.TextUnformatted(versionText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,15 +54,21 @@ internal static class AutoTranslate
|
||||
}
|
||||
|
||||
// Warms the auto-translate cache on a background thread so the first
|
||||
// message send doesn't hitch the main thread.
|
||||
// message send doesn't hitch the main thread. IsBackground keeps plugin
|
||||
// unload non-blocking even if the warmup is still in flight.
|
||||
internal static void PreloadCache()
|
||||
{
|
||||
new Thread(() =>
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
AllEntries();
|
||||
Plugin.Log.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
|
||||
}).Start();
|
||||
Plugin.LogProxy.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "HellionChat-AutoTranslate-Warmup",
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private static List<AutoTranslateEntry> AllEntries()
|
||||
@@ -191,7 +197,7 @@ internal static class AutoTranslate
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error(ex, $"failed to translate: {lookup}");
|
||||
Plugin.LogProxy.Error(ex, $"failed to translate: {lookup}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
internal sealed class DalamudPlatformUtil : IPlatformUtil
|
||||
{
|
||||
public DalamudPlatformUtil()
|
||||
{
|
||||
// Util.IsWine probes the host process and never changes for the
|
||||
// lifetime of a plugin instance, so we cache it once at ctor.
|
||||
// Mirrors LightlessSync/Services/DalamudUtilService:154.
|
||||
IsWine = Dalamud.Utility.Util.IsWine();
|
||||
}
|
||||
|
||||
public bool IsWine { get; }
|
||||
|
||||
public void OpenLink(string url) => Dalamud.Utility.Util.OpenLink(url);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
namespace HellionChat.Util;
|
||||
|
||||
internal sealed class DalamudPluginLogProxy : IPluginLogProxy
|
||||
{
|
||||
private readonly IPluginLog _log;
|
||||
|
||||
public DalamudPluginLogProxy(IPluginLog log) => _log = log;
|
||||
|
||||
public void Verbose(string message) => _log.Verbose(message);
|
||||
|
||||
public void Verbose(Exception exception, string message) => _log.Verbose(exception, message);
|
||||
|
||||
public void Verbose(string messageTemplate, params object[] values) =>
|
||||
_log.Verbose(messageTemplate, values);
|
||||
|
||||
public void Debug(string message) => _log.Debug(message);
|
||||
|
||||
public void Debug(Exception exception, string message) => _log.Debug(exception, message);
|
||||
|
||||
public void Debug(string messageTemplate, params object[] values) =>
|
||||
_log.Debug(messageTemplate, values);
|
||||
|
||||
public void Information(string message) => _log.Information(message);
|
||||
|
||||
public void Information(Exception exception, string message) =>
|
||||
_log.Information(exception, message);
|
||||
|
||||
public void Information(string messageTemplate, params object[] values) =>
|
||||
_log.Information(messageTemplate, values);
|
||||
|
||||
public void Info(string message) => _log.Info(message);
|
||||
|
||||
public void Info(Exception exception, string message) => _log.Info(exception, message);
|
||||
|
||||
public void Info(string messageTemplate, params object[] values) =>
|
||||
_log.Info(messageTemplate, values);
|
||||
|
||||
public void Warning(string message) => _log.Warning(message);
|
||||
|
||||
public void Warning(Exception exception, string message) => _log.Warning(exception, message);
|
||||
|
||||
public void Warning(string messageTemplate, params object[] values) =>
|
||||
_log.Warning(messageTemplate, values);
|
||||
|
||||
public void Error(string message) => _log.Error(message);
|
||||
|
||||
public void Error(Exception exception, string message) => _log.Error(exception, message);
|
||||
|
||||
public void Error(string messageTemplate, params object[] values) =>
|
||||
_log.Error(messageTemplate, values);
|
||||
|
||||
public void Fatal(string message) => _log.Fatal(message);
|
||||
|
||||
public void Fatal(Exception exception, string message) => _log.Fatal(exception, message);
|
||||
|
||||
public void Fatal(string messageTemplate, params object[] values) =>
|
||||
_log.Fatal(messageTemplate, values);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
// Indirection over Dalamud.Utility.Util's static surface so services can be
|
||||
// constructed in an isolated xUnit AppDomain without loading Dalamud.dll.
|
||||
// Production wiring lives in DalamudPlatformUtil; tests substitute a fake.
|
||||
internal interface IPlatformUtil
|
||||
{
|
||||
bool IsWine { get; }
|
||||
|
||||
void OpenLink(string url);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
|
||||
namespace HellionChat.Util;
|
||||
|
||||
// Indirection over Dalamud's IPluginLog so MessageStore can be constructed
|
||||
// in an isolated xUnit AppDomain without loading Dalamud.dll — same pattern
|
||||
// as IPlatformUtil from F12.1. A later DI-container cycle (v1.5.x) may
|
||||
// replace this with Microsoft.Extensions.Logging's ILogger<T>.
|
||||
internal interface IPluginLogProxy
|
||||
{
|
||||
void Verbose(string message);
|
||||
void Verbose(Exception exception, string message);
|
||||
void Verbose(string messageTemplate, params object[] values);
|
||||
|
||||
void Debug(string message);
|
||||
void Debug(Exception exception, string message);
|
||||
void Debug(string messageTemplate, params object[] values);
|
||||
|
||||
void Information(string message);
|
||||
void Information(Exception exception, string message);
|
||||
void Information(string messageTemplate, params object[] values);
|
||||
|
||||
// IPluginLog exposes Info as a distinct method (short alias of
|
||||
// Information) — both are present so call-sites stay drop-in.
|
||||
void Info(string message);
|
||||
void Info(Exception exception, string message);
|
||||
void Info(string messageTemplate, params object[] values);
|
||||
|
||||
void Warning(string message);
|
||||
void Warning(Exception exception, string message);
|
||||
void Warning(string messageTemplate, params object[] values);
|
||||
|
||||
void Error(string message);
|
||||
void Error(Exception exception, string message);
|
||||
void Error(string messageTemplate, params object[] values);
|
||||
|
||||
void Fatal(string message);
|
||||
void Fatal(Exception exception, string message);
|
||||
void Fatal(string messageTemplate, params object[] values);
|
||||
}
|
||||
@@ -254,6 +254,17 @@ internal static class ImGuiUtil
|
||||
return end;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Inspired by ChatTwo upstream f35b7d3 (Infiziert90, 2026-05-12).
|
||||
// Upstream dropped the width parameter (no callers there); we keep
|
||||
// it because two ChatLogWindow header buttons size themselves to
|
||||
// match the ChannelIcon button's frame. The actual bug is the
|
||||
// manual size = width - 2 * CellPadding.X subtraction: CellPadding
|
||||
// scales with HUD scale, the raw int does not, so the button
|
||||
// shrank under high HUD scales. ImGui.Button already handles its
|
||||
// own frame padding internally — pass the measured width straight
|
||||
// through.
|
||||
// ---------------------------------------------------------------
|
||||
internal static bool IconButton(
|
||||
FontAwesomeIcon icon,
|
||||
string? id = null,
|
||||
@@ -268,10 +279,7 @@ internal static class ImGuiUtil
|
||||
bool ret;
|
||||
using (Plugin.FontManager.FontAwesome.Push())
|
||||
{
|
||||
var size = Vector2.Zero;
|
||||
if (width > 0)
|
||||
size.X = width - 2 * ImGui.GetStyle().CellPadding.X;
|
||||
|
||||
var size = width > 0 ? new Vector2(width, 0f) : Vector2.Zero;
|
||||
ret = ImGui.Button(label, size);
|
||||
}
|
||||
|
||||
@@ -575,7 +583,9 @@ internal static class ImGuiUtil
|
||||
|
||||
using (ImRaii.Disabled(isMax))
|
||||
{
|
||||
if (IconButton(FontAwesomeIcon.ArrowRight, id + 1.ToString()))
|
||||
// Parentheses pin the operator precedence: without them this resolves as
|
||||
// id.ToString() + "1" (e.g. "01" instead of "1").
|
||||
if (IconButton(FontAwesomeIcon.ArrowRight, (id + 1).ToString()))
|
||||
selected++;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,6 @@ public static class MemoryUtil
|
||||
str.Append(' ');
|
||||
}
|
||||
|
||||
Plugin.Log.Information(str.ToString());
|
||||
Plugin.LogProxy.Information(str.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
|
||||
// sites in Plugin.cs and Configuration.cs so they stay in lockstep — a
|
||||
// load-time strip that disagrees with the save-time strip is exactly how
|
||||
// pinned tabs would silently fall out of the JSON.
|
||||
internal static class TabLifecycleHelpers
|
||||
{
|
||||
public static bool IsInUnpinnedPool(Tab t) => t.IsTempTab && !t.IsPinned;
|
||||
|
||||
public static bool IsInPinnedPool(Tab t) => t.IsTempTab && t.IsPinned;
|
||||
|
||||
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
|
||||
|
||||
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace HellionChat.Util;
|
||||
|
||||
internal static class UrlValidation
|
||||
{
|
||||
// Used by BrandingLinks/IntegrationLinks at module init. A typo in a URL
|
||||
// rotation throws loudly at plugin load instead of silently failing when
|
||||
// a user clicks the broken button.
|
||||
public static void ValidateAll(string source, params string[] urls)
|
||||
{
|
||||
foreach (var url in urls)
|
||||
{
|
||||
if (
|
||||
!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme is not "https" and not "http")
|
||||
)
|
||||
{
|
||||
throw new InvalidOperationException($"{source} contains malformed URL: {url}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ public static class WrapperUtil
|
||||
{
|
||||
try
|
||||
{
|
||||
Plugin.Log.Debug($"Opening URI {uri} in default browser");
|
||||
Dalamud.Utility.Util.OpenLink(uri.ToString());
|
||||
Plugin.LogProxy.Debug($"Opening URI {uri} in default browser");
|
||||
Plugin.PlatformUtil.OpenLink(uri.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error($"Error opening URI: {ex}");
|
||||
Plugin.LogProxy.Error($"Error opening URI: {ex}");
|
||||
AddNotification(Language.Context_OpenInBrowserError, NotificationType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
+107
-107
@@ -1,110 +1,110 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[15.0.0, )",
|
||||
"resolved": "15.0.0",
|
||||
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
||||
},
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.4, 4.0.0)",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "3.1.4",
|
||||
"MessagePackAnalyzer": "3.1.4",
|
||||
"Microsoft.NET.StringTools": "17.11.4"
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[15.0.0, )",
|
||||
"resolved": "15.0.0",
|
||||
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
||||
},
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.4, 4.0.0)",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "3.1.4",
|
||||
"MessagePackAnalyzer": "3.1.4",
|
||||
"Microsoft.NET.StringTools": "17.11.4"
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.7, )",
|
||||
"resolved": "10.0.7",
|
||||
"contentHash": "DZ6G2QuyPrsh5VS+wfiZbNBtYT6p+CkxXjD0aZHF04xso7QsG/uk0JpG30hzYlK6u/wtTzta1Dqfgbc/Sl2sDA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "10.0.7",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"morelinq": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.4.0, )",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
|
||||
},
|
||||
"Pidgin": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.5.1, 4.0.0)",
|
||||
"resolved": "3.5.1",
|
||||
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
|
||||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.12, 4.0.0)",
|
||||
"resolved": "3.1.12",
|
||||
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.50.3, )",
|
||||
"resolved": "3.50.3",
|
||||
"contentHash": "tVyhqQ8wxgedWiiPFChyZhE8I3PkOM/AE1azsj1qsdYUws13ONBFyi3aDxju4tD2kzedB2q5+50WrTyY0h2gMQ=="
|
||||
},
|
||||
"MessagePack.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "aVWrDAkCdqxwQsz/q0ldPh2EFn48M99YUzE9OvZjMq2RNLKz4o2z88iGFvSvbMqOWRweRvKPHBJZe22PRqzslQ=="
|
||||
},
|
||||
"MessagePackAnalyzer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "CTaSsN/liJ7MhLCAB7Z4ZLBNuVGCq9lt2BT/cbrc9vzGv89yK3CqIA+z9T19a11eQYl9etZHL6MQJgCqECRVpg=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.7",
|
||||
"contentHash": "xVrtBg3M1wJlBDkoT0dXEYB/wSc8bIHJPYtw/bu1AqpWgF79uPSs87DAhERR/Ilumre6TKZa1cjMg3VUUObVLA==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.StringTools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.11.4",
|
||||
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.7, )",
|
||||
"resolved": "10.0.7",
|
||||
"contentHash": "DZ6G2QuyPrsh5VS+wfiZbNBtYT6p+CkxXjD0aZHF04xso7QsG/uk0JpG30hzYlK6u/wtTzta1Dqfgbc/Sl2sDA==",
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "10.0.7",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"morelinq": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.4.0, )",
|
||||
"resolved": "4.4.0",
|
||||
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
|
||||
},
|
||||
"Pidgin": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.5.1, 4.0.0)",
|
||||
"resolved": "3.5.1",
|
||||
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
|
||||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.12, 4.0.0)",
|
||||
"resolved": "3.1.12",
|
||||
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.50.3, )",
|
||||
"resolved": "3.50.3",
|
||||
"contentHash": "tVyhqQ8wxgedWiiPFChyZhE8I3PkOM/AE1azsj1qsdYUws13ONBFyi3aDxju4tD2kzedB2q5+50WrTyY0h2gMQ=="
|
||||
},
|
||||
"MessagePack.Annotations": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "aVWrDAkCdqxwQsz/q0ldPh2EFn48M99YUzE9OvZjMq2RNLKz4o2z88iGFvSvbMqOWRweRvKPHBJZe22PRqzslQ=="
|
||||
},
|
||||
"MessagePackAnalyzer": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "CTaSsN/liJ7MhLCAB7Z4ZLBNuVGCq9lt2BT/cbrc9vzGv89yK3CqIA+z9T19a11eQYl9etZHL6MQJgCqECRVpg=="
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.7",
|
||||
"contentHash": "xVrtBg3M1wJlBDkoT0dXEYB/wSc8bIHJPYtw/bu1AqpWgF79uPSs87DAhERR/Ilumre6TKZa1cjMg3VUUObVLA==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.StringTools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.11.4",
|
||||
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.11",
|
||||
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://gitea.hellion-forge.cloud/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/)
|
||||
@@ -11,7 +11,7 @@
|
||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||
</p>
|
||||
|
||||
**Version 1.4.3** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||
**Version 1.4.7** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
||||
|
||||
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
|
||||
@@ -102,7 +102,7 @@ Hellion Chat is developed under **Hellion Forge**, the specialized modding and p
|
||||
#### Custom Themes (v1.1.0)
|
||||
|
||||
HellionChat ships a theme engine with ten built-in themes (Hellion Arctic, Hellion Spectrum, Chat 2 Classic, Event
|
||||
Horizon, Moonlit Bloom, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman, Synthwave Sunset) and a JSON-based
|
||||
Horizon, Crystal Nocturne, Mint Grove, Night Blue, Indigo Violet, Forge Merchantman, Synthwave Sunset) and a JSON-based
|
||||
authoring format for custom themes. Schema and step-by-step guide in
|
||||
[`docs/THEME-AUTHORING.md`](docs/THEME-AUTHORING.md). Hellion Spectrum is Deuteranopia/Protanopia-safe (red-green color
|
||||
blindness) based on the Wong/Okabe-Ito palette.
|
||||
@@ -286,14 +286,23 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
|
||||
|
||||
## Project Status
|
||||
|
||||
**Version 1.4.3** — Plugin-load async init plus repo cutover: the plugin has been migrated to Dalamud's
|
||||
`IAsyncDalamudPlugin` API. The constructor now handles only bootstrap essentials (config load, language init, conflict
|
||||
detection); migrations, service allocations, window construction, and hook subscriptions move into `LoadAsync`, allowing
|
||||
Dalamud to keep the UI responsive during heavy lifting. A schema gate replaces the v9 → v16 migration chain; configs at
|
||||
schema v16+ load directly, older configs trigger an "install v1.4.2 first" error message. Custom repo URL migrated to
|
||||
`gitea.hellion-forge.cloud`; the GitHub repo remains as a frozen v1.4.2 snapshot. Plugin load time is ~3.7 s median (5
|
||||
reloads), comparable to v1.4.2 — the async migration is the foundation for v1.4.4 lazy-init optimizations, not a direct
|
||||
user-facing win. Fourth sub-patch of the v1.4.x polish sweep series (as of 2026-05-08).
|
||||
**Version 1.4.7** — Backlog cleanup and the first user-visible feature bundle since v1.4.5. TempTell tabs can now be
|
||||
pinned via right-click; pinned tabs survive relog, keep their conversation history (loaded on demand from the message
|
||||
store), and stay bound to the same `/tell` partner. A hard cap of 5 pinned tabs lives in a pool separate from the 15-tab
|
||||
auto-tell pool, so the total ceiling is 20 tabs. The sidebar groups pinned tabs into their own section with its own
|
||||
divider header. Honorific glow outlines now render when the title carries a Glow colour — opt-in via **Settings →
|
||||
Integrations → Render glow outlines (Honorific)**, default off, so v1.4.6 visuals stay untouched for users who don't
|
||||
care and the per-frame DrawList overhead is skipped on low-end hardware. Honorific gradient (Color3 / GradientColourSet
|
||||
/ Wave / Pulse) is parsed and stashed for a later cycle, but currently renders as the primary colour. Sidebar width is
|
||||
configurable in **Theme & Layout** between 44 and 160 px; default stays icon-only so existing users see no layout
|
||||
change. `Configuration.UpdateFrom` now preserves the runtime `CurrentChannel` across the persistent-tab merge, and
|
||||
`TabSwitched` deep-clones the seeded channel — together they fix a Settings-Save regression where the chat input could
|
||||
pop back to `/tell <pinned-partner>` after touching settings while on a Party or Linkshell tab. Internal items:
|
||||
`IPluginLogProxy` indirection over Dalamud's `IPluginLog` routes all ~91 `Plugin.Log` call sites through a testable
|
||||
proxy, closing the test-isolation gap F12.1 left in v1.4.6 (`MessageStore.Migrate0` now runs in xUnit without loading
|
||||
`Dalamud.dll`). `Util/ImGuiUtil.cs`'s `DrawArrows` IconButton id gets explicit parentheses on the increment. Migration
|
||||
v16 → v17 is additive (new `Tab.IsPinned` flag, default false). Eighth sub-patch of the v1.4.x polish sweep series (as
|
||||
of 2026-05-13).
|
||||
|
||||
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
||||
|
||||
|
||||
@@ -10,6 +10,154 @@ to the release pages for details.
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)
|
||||
|
||||
Eighth sub-patch of the v1.4.x polish-sweep series. First user-visible feature bundle since v1.4.5 — pinned tell tabs
|
||||
that survive relog, opt-in Honorific glow rendering, a configurable sidebar, plus a Settings-Save channel-preservation
|
||||
fix surfaced during smoke testing.
|
||||
|
||||
- TempTell Pin: right-click a TempTell tab in the sidebar and choose "Pin Tab" / "Tab anpinnen". Pinned tabs survive
|
||||
plugin reload and character logout, keep their conversation history (loaded on demand from the message store on
|
||||
rehydrate), and stay bound to the same `/tell` partner. Hard cap of 5 pinned tabs in a pool separate from the 15-tab
|
||||
auto-tell pool — total ceiling is 20 tabs. The sidebar groups pinned tabs into their own section with a divider header
|
||||
- Honorific glow outlines now render via an 8-direction DrawList pre-pass when the title carries a Glow colour. Opt-in
|
||||
via **Settings → Integrations → Render glow outlines (Honorific)** (default off). Honorific's gradient surface
|
||||
(`Color3`, `GradientColourSet`, `GradientAnimationStyle`) is parsed and stashed for a later cycle but renders as the
|
||||
primary colour until then — the v1.4.7 DTO already mirrors all four extra fields so the JSON roundtrip doesn't
|
||||
silent-drop them
|
||||
- Sidebar width configurable in **Theme & Layout** (44–160 px, default 44 stays icon-only). The icon button stretches
|
||||
with the configured width so a widened sidebar looks intentional, not a 36 px icon floating in empty space
|
||||
- `Configuration.UpdateFrom` now preserves the runtime `CurrentChannel` across the persistent-tab merge alongside
|
||||
`Messages` and `LastSendUnread`. `TabSwitched` deep-clones the seeded channel from the previous tab instead of sharing
|
||||
the same `UsedChannel` instance. Together these fix a regression where Settings-Save on a Party or Linkshell tab
|
||||
popped the chat input back to `/tell <pinned-partner>` on the next interaction
|
||||
- `Util/ImGuiUtil.cs` `DrawArrows` IconButton id uses `(id + 1).ToString()` with explicit parentheses instead of the
|
||||
operator-precedence quirk `id + 1.ToString()` (which resolved to `id.ToString() + "1"`). Single live caller is
|
||||
`Ui/DbViewer.cs:227` page-navigation
|
||||
- Internal: `IPluginLogProxy` indirection over Dalamud's `IPluginLog` routes all ~91 `Plugin.Log` call sites through a
|
||||
testable proxy. `MessageStore.Migrate0` can now run in xUnit without loading `Dalamud.dll`, closing the gap F12.1 left
|
||||
in v1.4.6. Production wrapper `DalamudPluginLogProxy` and Build-Suite `FakePluginLogProxy` mirror the full
|
||||
`IPluginLog` surface (`Verbose`/`Debug`/`Information`/`Info`/`Warning`/`Error`/`Fatal`) with single-string,
|
||||
`Exception+string`, and `params object[]` overloads
|
||||
- Internal: TempTab counter switched from an `Interlocked` cached field to a derived `Tabs.Count(predicate)`. Pin-state
|
||||
transitions (TryPin / Unpin / Promote) are cold-path and don't need lock-free reads; counter mutation surface dropped
|
||||
from 5 to 0 sites. Build-Suite floor 688 → 710 (+22)
|
||||
- Schema bump v16 → v17 is additive: new `Tab.IsPinned` bool, default false. Existing v16 configs load cleanly and get
|
||||
their `Version` stamp bumped after the gate check
|
||||
|
||||
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
|
||||
|
||||
---
|
||||
|
||||
## Hellion Chat 1.4.6 — Code Hygiene and Refactor (2026-05-12)
|
||||
|
||||
Maintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two
|
||||
upstream-inherited bugs from ChatTwo `f35b7d3`, and prepares the code for the v1.4.7 backlog cleanup.
|
||||
|
||||
- `scripts/preflight.sh` gains Block E (`dotnet csharpier check`) and Block F (`markdownlint-cli2`) so reflow drift and
|
||||
markdown violations are caught at the pre-push gate. `.markdownlint.json` adds `MD024 siblings_only` and disables
|
||||
`MD036` so the bilingual forge-post bold-emphasis headings pass linting; the `.claude/` directory is excluded from the
|
||||
scan
|
||||
- `FontManager.AddFontWithFallback` catch-filter now covers `InvalidOperationException` and `ArgumentException` on top
|
||||
of the existing IO triad. The warning log carries the exception type name, so the diagnostic path knows which class of
|
||||
atlas-toolkit throw triggered the NotoSansCjkRegular fallback
|
||||
- `BrandingLinks` (5 URLs) and `Integrations/IntegrationLinks` (2 URLs) validate themselves on first module load via
|
||||
`[ModuleInitializer]` + a shared `UrlValidation.ValidateAll` helper. A malformed URL now throws
|
||||
`InvalidOperationException` at plugin load with the source class and the broken URL in the message
|
||||
- Cherry-picked from ChatTwo upstream `f35b7d3`: `Chat.SetChannel` no longer leaks the native `Utf8String` when the
|
||||
linkshell check rejects the channel. The validity check is now wrapped around the `ChangeChatChannel` call instead of
|
||||
short-circuiting before `Dtor`. `ValidAnyLinkshell` is renamed to `IsChannelOrExistingLinkshell` and the
|
||||
`ChatLogWindow` call-site follows the rename
|
||||
- Cherry-picked from ChatTwo upstream `f35b7d3`: `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget`. The old
|
||||
`CurrentChannel = CurrentChannel` was a reference copy, so PopOut and Temp tabs mutated each other's channel state
|
||||
(incl. tell target). `TellTarget.From(t)` static factory is replaced with an instance `Clone()`; `UsedChannel.Clone()`
|
||||
is new and runs deep-clone on both TellTarget references
|
||||
- `ChatLogWindow` active-tab underline pill now scales with `ImGuiHelpers.GlobalScale` and rounds its DrawList
|
||||
coordinates to physical pixels via `MathF.Round`, so the 2 px line stays crisp on 125 % and 150 % DPI setups instead
|
||||
of bleeding into a sub-pixel blur
|
||||
- `ImGuiUtil.IconButton` width parameter no longer subtracts HUD-scaled `CellPadding.X * 2` from the raw `int` width.
|
||||
`ImGui.Button` handles its own frame padding internally, so the measured `buttonWidth` now passes through verbatim
|
||||
(inspired-by upstream `f35b7d3`, but our two call-sites need the parameter, so the param itself stays)
|
||||
- Internal: `HellionStyle` ChildBgAlpha threshold logic extracted to `HellionStyleHelpers.ResolveChildBgAlpha` with a
|
||||
build-suite mirror test that pins the 0.999f cutoff. `Plugin.SaveConfig` clones only the temp-tab subset in the
|
||||
pre-serialization snapshot instead of the full tab list. `SettingsOverview` caches `ImGui.GetWindowDrawList()` once
|
||||
per frame and passes the pointer down to `DrawCard`
|
||||
- Internal: `Dalamud.Utility.Util` static surface (`IsWine`, `OpenLink`) routed through a new `IPlatformUtil`
|
||||
indirection. `MessageStore`'s `IsWine` probe is now reachable from the xUnit AppDomain via a `FakePlatformUtil`
|
||||
fixture (full isolated MessageStore construction still pending — `Plugin.Log.Information` in `Migrate0` is a separate
|
||||
Dalamud-static surface, slated for v1.4.7)
|
||||
- Built-in themes: Crystal Nocturne (royal sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit
|
||||
Bloom in the built-in roster. Users who had Moonlit Bloom selected fall back to the default Hellion Arctic on the
|
||||
first plugin load; an existing custom JSON copy of Moonlit Bloom under `pluginConfigs/HellionChat/themes/` keeps
|
||||
working unchanged
|
||||
|
||||
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.5 — UX and Robustness (2026-05-12)
|
||||
|
||||
Sixth sub-patch of the v1.4.x polish-sweep series. User-visible robustness fixes plus two doc/test polish items from the
|
||||
audit backlog. No schema bump, no migration.
|
||||
|
||||
- `ChatLogWindow.Draw` now surfaces a one-shot warning notification when the draw path throws. The stack trace still
|
||||
goes to `/xllog` via `Plugin.Log.Error`; the notification is suppressed for the rest of the plugin session so a
|
||||
recurring failure can't spam the notification stack frame-by-frame. Pattern-match to the existing `Plugin.cs:505-516`
|
||||
migration-blocker notification
|
||||
- `FirstRunWizard` splits accept from close. `OnClose` no longer silently sets `FirstRunCompleted`, so closing the X
|
||||
leaves the wizard pending and it reopens on the next plugin load. A new footer "Later — keep defaults" button is the
|
||||
explicit path to dismiss without picking a profile. Bilingual strings (EN + DE) plus a tooltip
|
||||
- `InputHistoryService.Reset` is wired into `Plugin.DisposeAsync` alongside the existing pure-memory cleanups. Static
|
||||
state used to survive a plugin reload — the next load now starts with an empty history
|
||||
- `FontManager.GetHellionFontBytes` becomes `TryGetHellionFontBytes` with a nullable return. On miss (broken csproj,
|
||||
hand-rolled dev build) the caller falls back to the system-font path that `UseHellionFont=false` already uses, plus a
|
||||
`Plugin.Log.Warning`. The whole UiBuilder no longer throws if the embedded font resource is absent
|
||||
- `Plugin.cs:167-168` gets a 4-line reasoning comment around the session-only `RemoveAll(IsTempTab)`: tells are usually
|
||||
privacy-filtered, resurrecting an empty crashed-session tab would trigger DB reconstruction on the next load.
|
||||
`TempTabCounter.InitFromList` mirrors the post-strip semantic in the Build-Suite with a pinning test
|
||||
- `StatusBar.cs` drops the version slot when the chat window's content width minus the version text is below 200 px. The
|
||||
right-aligned version used to clip into the four left-side slots in narrow windows
|
||||
|
||||
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.4 — Threading and IPC Safety Polish (2026-05-12)
|
||||
|
||||
Fifth sub-patch of the v1.4.x polish-sweep series. Threading assumptions are documented per-method, a hot-path lock
|
||||
falls away in `AutoTellTabsService`, IPC-cleanup failures become visible, and the privacy filter now speaks up when an
|
||||
unknown ChatType shows up.
|
||||
|
||||
- `AutoTellTabsService.ActiveTempTabCount` switches from a lock-protected LINQ `Count` to an `Interlocked` counter kept
|
||||
in sync with `Config.Tabs` from inside the existing mutation paths. `Initialize()` seeds the counter from the
|
||||
persisted Tabs list, and `SaveConfig`'s snapshot-restore path calls a new `ResyncTempTabCounter()` so the mid-step
|
||||
`RemoveAll` doesn't leave the counter drifting. Pure-helper test mirror lives in the Build-Suite repo
|
||||
- `HonorificService` per-method threading banners replace the block comment at the bottom of the file. Each IPC callback
|
||||
(`TryInitialPull`, `OnTitleChanged`, `OnReady`, `OnDisposing`, `TryUnsubscribe`) and the `CurrentTitle` field carry a
|
||||
one-line `// Thread:` annotation so the framework-thread invariant is visible at the call site
|
||||
- `TryUnsubscribe` log-level upgraded from `Debug` to `Warning`. A silent unsubscribe failure leaks a live subscription
|
||||
across plugin reloads, which is exactly the kind of issue that should not be at Debug
|
||||
- `AutoTranslate.PreloadCache` thread now has `IsBackground = true` and a thread name. Without `IsBackground` the warmup
|
||||
blocks plugin unload (typically 100-300 ms). Pattern-match to `MessageManager` (F6.1) and `Plugin.RetentionSweep`
|
||||
(F9.3), both since v1.4.0
|
||||
- `Configuration.IsAllowedForStorage` adds a one-line `Plugin.Log.Warning` for the first occurrence of any ChatType that
|
||||
isn't in `PrivacyPersistChannels`. Dedup via a `NonSerialized` `HashSet<ChatType>`, so the warning fires once per
|
||||
runtime — not once per frame, not once per install. Failsafe routing through `PrivacyPersistUnknownChannels` is
|
||||
unchanged
|
||||
- `PrivacyPersistUnknownChannels` field default flipped from `false` to `true` for new installs via a constant in
|
||||
`PrivacyDefaults`. Existing configs keep their explicit choice — the deserializer overrides the initializer. No schema
|
||||
bump, no migration, no first-run banner
|
||||
|
||||
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.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
|
||||
|
||||
+66
-4
@@ -10,14 +10,76 @@ the plugin's privacy-first scope during brainstorming.
|
||||
|
||||
---
|
||||
|
||||
## Next Cycle (v1.4.4)
|
||||
## Next Cycle (v1.4.8)
|
||||
|
||||
**Window-Lazy-Open + Render-Init-Cost Optimisation** — take the `IAsyncDalamudPlugin` foundation laid in v1.4.3 and turn
|
||||
it into wins users can actually feel. Window construction deferred until first open, render-path init cost reduced in
|
||||
the first frames. Concrete candidates and size estimates will be consolidated in the v1.4.4 brainstorm.
|
||||
**Hook-Layer Cycle.** Receive-suppressed-tells toggle (cross-reference XIVIM #73 bubble-layer sub-task), Database Viewer
|
||||
full-text search via SQLite FTS5, plus preparation for the later Ad-Block cycle. Hook-layer investigation is shared
|
||||
across these items so they cluster naturally in one sub-patch.
|
||||
|
||||
---
|
||||
|
||||
## v1.4.7 — Backlog Cleanup and Mid-Features (released 2026-05-13)
|
||||
|
||||
Eighth sub-patch of the v1.4.x Polish Sweep series. First user-visible feature bundle since v1.4.5. TempTell tabs can
|
||||
now be pinned via right-click; pinned tabs survive plugin reload and character logout, keep their conversation history
|
||||
(loaded on demand from the message store on rehydrate), and stay bound to the same `/tell` partner. A hard cap of 5
|
||||
pinned tabs lives in a pool separate from the 15-tab auto-tell pool, total ceiling 20. The sidebar groups pinned tabs
|
||||
into their own section with a divider header, and the sidebar width itself is now configurable in **Theme & Layout**
|
||||
between 44 and 160 px. Honorific glow outlines render when the title carries a Glow colour, opt-in via **Settings →
|
||||
Integrations → Render glow outlines (Honorific)** (default off). Honorific's gradient (Color3 / GradientColourSet / Wave
|
||||
/ Pulse) is parsed but rendered statically — a later cycle will port the full animation algorithm or land an upstream
|
||||
IPC PR for the resolved frame colour. `Configuration.UpdateFrom` now preserves the runtime `CurrentChannel` across the
|
||||
persistent-tab merge, and `TabSwitched` deep-clones the seeded channel instead of sharing the previous tab's
|
||||
`UsedChannel` — together they fix a Settings-Save regression where the chat input could pop back to
|
||||
`/tell <pinned-partner>` after touching settings on a Party or Linkshell tab. Internal items: `IPluginLogProxy`
|
||||
indirection over Dalamud's `IPluginLog` routes all ~91 `Plugin.Log` call sites through a testable proxy, closing the
|
||||
F12.1 test-isolation gap (`MessageStore.Migrate0` runs in xUnit now). TempTab counter switched from `Interlocked` cached
|
||||
field to derived `Tabs.Count(predicate)`. Migration v16 → v17 is additive (new `Tab.IsPinned` flag). Build-Suite floor
|
||||
688 → 710 (+22 tests across Pin-lifecycle predicates, pool limits, Tab.Clone roundtrip, MessageStore Migrate0
|
||||
construction, and Honorific TitleData JSON roundtrip).
|
||||
|
||||
## v1.4.6 — Code Hygiene and Refactor (released 2026-05-12)
|
||||
|
||||
Seventh sub-patch of the v1.4.x Polish Sweep series. Maintenance patch — no user-visible behaviour changes; tightens the
|
||||
development feedback loop and pulls in two ChatTwo upstream bugfixes. `scripts/preflight.sh` gains a csharpier reflow
|
||||
check (Block E) and a markdownlint pass (Block F), so style drift and markdown violations are blocked at the pre-push
|
||||
gate. `FontManager.AddFontWithFallback` catch-filter now spans `InvalidOperationException` and `ArgumentException` on
|
||||
top of the existing IO triad, with the exception type name in the warning log so the diagnostic path can see which
|
||||
atlas-toolkit throw triggered the fallback. `BrandingLinks` and `IntegrationLinks` run a `[ModuleInitializer]` URL
|
||||
validation pass on plugin load; a typo in a future URL rotation now throws at startup instead of failing silently when a
|
||||
user clicks the broken button. Cherry-picked from ChatTwo upstream `f35b7d3`: `Chat.SetChannel` no longer leaks the
|
||||
native `Utf8String` when the linkshell check rejects the channel (rename to `IsChannelOrExistingLinkshell` plus
|
||||
wrap-not-return), and `Tab.Clone` now deep-clones `UsedChannel` and `TellTarget` (the previous reference copy let PopOut
|
||||
and Temp tabs mutate each other's channel state). The `ChatLogWindow` active-tab underline pill scales with
|
||||
`ImGuiHelpers.GlobalScale` and rounds to physical pixels for crisp rendering above 100 % DPI. Internal items:
|
||||
`HellionStyle` ChildBgAlpha extracted to a testable helper, `Plugin.SaveConfig` clones only the temp-tab subset in the
|
||||
snapshot path, `SettingsOverview` caches the draw-list per frame, `Dalamud.Utility.Util` static surface routed through
|
||||
an `IPlatformUtil` indirection (`MessageStore`'s `IsWine` probe is now testable in isolation). No schema bump, no
|
||||
migration.
|
||||
|
||||
## v1.4.5 — UX and Robustness (released 2026-05-12)
|
||||
|
||||
Sixth sub-patch of the v1.4.x Polish Sweep series. User-visible robustness polish plus two doc/test polish items from
|
||||
the audit backlog. Chat-log draw failures now surface as a one-shot notification instead of failing silently. The
|
||||
first-run wizard splits accept from close: `OnClose` no longer silently sets `FirstRunCompleted`, and a new footer
|
||||
"Later — keep defaults" button is the explicit path to dismiss without picking a profile. `InputHistoryService` clears
|
||||
on plugin dispose so the previous session's typed commands don't bleed into the next load. `FontManager` falls back to
|
||||
the system font path if the embedded Hellion font resource is missing (broken-csproj / dev-build only). The status bar
|
||||
hides the version slot when the chat window is too narrow to fit all five slots without overlap. Plus
|
||||
`Plugin.cs:167-168` gains an explicit session-only Auto-Tell-Tab invariant comment with a `TempTabCounter.InitFromList`
|
||||
pin in the Build-Suite. No schema bump, no migration.
|
||||
|
||||
## v1.4.4 — Threading and IPC Safety Polish (released 2026-05-12)
|
||||
|
||||
Fifth sub-patch of the v1.4.x Polish Sweep series. `AutoTellTabsService.ActiveTempTabCount` switches from a
|
||||
lock-protected LINQ `Count` to an `Interlocked` counter kept in sync from inside the existing mutation paths;
|
||||
`Initialize()` seeds from the persisted Tabs list and `SaveConfig`'s snapshot-restore path calls a new
|
||||
`ResyncTempTabCounter()` after the mid-step `RemoveAll`. `HonorificService` carries per-method threading banners and
|
||||
`TryUnsubscribe`'s log level moves from Debug to Warning. `AutoTranslate.PreloadCache` is marked `IsBackground = true`
|
||||
so plugin unload no longer waits for it. `Configuration.IsAllowedForStorage` logs once per unknown ChatType via a
|
||||
`NonSerialized` `HashSet`, and `PrivacyPersistUnknownChannels` default flips to `true` for new installs. No schema bump,
|
||||
no migration.
|
||||
|
||||
## v1.4.3 — Plugin-Load Async-Init + Repo-Cutover (released 2026-05-08)
|
||||
|
||||
Fourth and largest sub-patch of the v1.4.x Polish Sweep series. Plugin migrated to Dalamud's `IAsyncDalamudPlugin` API:
|
||||
|
||||
@@ -141,7 +141,7 @@ A theme can tint these toward its brand family (e.g., a purple theme can shift T
|
||||
**don't** flip them (Tell suddenly green, Yell suddenly cyan). RP groups and combat-spec setups depend on the visual
|
||||
hierarchy.
|
||||
|
||||
The eight colored built-in themes (Hellion Arctic, Hellion Spectrum, Event Horizon, Moonlit Bloom, Mint Grove, Night
|
||||
The eight colored built-in themes (Hellion Arctic, Hellion Spectrum, Event Horizon, Crystal Nocturne, Mint Grove, Night
|
||||
Blue, Indigo Violet, Forge Merchantman) all follow this rule — read their source for reference. Chat 2 Klassik
|
||||
intentionally ships without `chatChannels` so the user keeps their existing picks.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"Author": "Jon Kazama (Hellion Forge)",
|
||||
"Name": "Hellion Chat",
|
||||
"InternalName": "HellionChat",
|
||||
"AssemblyVersion": "1.4.3.0",
|
||||
"AssemblyVersion": "1.4.7.0",
|
||||
"Description": "A Hellion Forge plugin — privacy-focused chat replacement for FINAL FANTASY XIV, built for EU, US and JP data rules.\n\nBy default only your own conversations are stored. Public chat, NPC dialogue, system messages and battle logs are discarded at the storage layer unless you opt in. Retention windows are configurable per channel, history can be wiped retroactively, and everything can be exported on demand.\n\nFeatures:\n- Channel whitelist with a Privacy-First default\n- Per-channel retention with a daily background sweep\n- Retroactive cleanup with preview and Ctrl+Shift confirm\n- Export to Markdown, JSON or CSV\n- First-run wizard with three profiles: Privacy-First, Casual, Full History\n- Bilingual UI (EN/DE) with live language switching\n- Own config and database — no shared state with other plugins\n\nBased on Chat 2 by Infi and Anna (EUPL-1.2).\nSupport: https://discord.gg/X9V7Kcv5gR",
|
||||
"ApplicableVersion": "any",
|
||||
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
||||
@@ -14,12 +14,12 @@
|
||||
"CanUnloadAsync": false,
|
||||
"LoadPriority": 0,
|
||||
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
|
||||
"Changelog": "**v1.4.3 — Faster plugin load + new repo (2026-05-08)**\n\nHeavy startup work (migrations, hooks, windows) now runs async so Dalamud's UI stays responsive during load. Load time is comparable to v1.4.2 — this is the foundation for v1.4.4 optimisations.\n\n- Two-phase async load via IAsyncDalamudPlugin\n- Schema-gate replaces the v9→v16 migration chain; old configs require a v1.4.2 install first\n- AutoTranslate cache loads on first use instead of every startup\n- Custom font (Hellion-Exo2) appears with a brief pop after load\n- Repo moved to gitea.hellion-forge.cloud — update your custom-repo URL\n\n---\n\n**v1.4.2 — Smoother frames in the chat log**\n\nPer-frame allocations in the chat-log render path eliminated. 2–5% frame-time recovery in typical scenes, more on pop-out-heavy setups.\n\n- Card-mode: theme/border invariants hoisted out of the per-message loop\n- Auto-tell tab tint and icon cached per tab\n- Status bar aggregation runs on ~1% of frames instead of every frame\n\n---\n\nFull history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases",
|
||||
"Changelog": "**v1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)**\n\nEighth sub-patch of the v1.4.x polish-sweep series. First user-visible feature bundle since v1.4.5 — pinned tell tabs that survive relog, opt-in Honorific glow rendering, and a configurable sidebar.\n\n- TempTell Pin: right-click a TempTell tab in the sidebar to pin it. Pinned tabs survive relog, keep their conversation history (loaded on demand from the message store), and stay bound to the same /tell partner. Hard cap of 5 pinned tabs in a pool separate from the 15-tab auto-tell pool — total ceiling is 20 tabs. New 'Angepinnt' / 'Pinned' section in the sidebar with its own divider header\n- Honorific Glow outline now renders when the title carries a Glow colour. Opt-in via Settings → Integrations → 'Render glow outlines (Honorific)' (default off, dodges the per-frame DrawList overhead on low-end hardware). Gradient (Color3 / GradientColourSet / Wave / Pulse) is parsed but rendered statically — a later cycle will port the full animation\n- Sidebar width is now configurable in Theme & Layout (range 44–160 px). Default stays icon-only; widen to fit section headers like 'Aktive Tells (3)' without truncation\n- Settings Save no longer pops the chat input back to /tell with a pinned partner — Configuration.UpdateFrom now preserves the runtime CurrentChannel across the persistent-tab merge, and TabSwitched deep-clones the seeded channel instead of sharing the previous tab's UsedChannel\n- Util/ImGuiUtil.cs DrawArrows IconButton id now uses (id + 1).ToString() instead of the operator-precedence quirk id + 1.ToString() — generated IDs stay numerically stable\n- Internal: IPluginLogProxy indirection over Dalamud's IPluginLog routes all ~91 Plugin.Log call sites through a testable proxy. MessageStore.Migrate0 can now run in xUnit without loading Dalamud.dll, closing the gap F12.1 left in v1.4.6\n- Internal: TempTab counter switched from an Interlocked cached field to a derived Tabs.Count(predicate) — pin-state transitions are cold-path and don't need lock-free reads\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.6 — Code Hygiene and Refactor (2026-05-12)**\n\nMaintenance patch. No user-visible behaviour changes; tightens the development feedback loop, fixes two upstream-inherited bugs, and prepares the code for the v1.4.7 backlog cleanup.\n\n- preflight.sh gains a csharpier reflow check and a markdownlint pass so style drift and markdown violations are caught at the pre-push gate\n- FontManager fallback catches the full set of atlas-toolkit throws (IO, InvalidOperation, ArgumentException) — a corrupt font config no longer takes down the whole atlas build\n- BrandingLinks and IntegrationLinks URLs validated on plugin load — a typo in a future URL rotation now throws at startup\n- Cherry-picked from ChatTwo upstream f35b7d3: Chat.SetChannel no longer leaks the native Utf8String when the linkshell check rejects the channel\n- Cherry-picked from ChatTwo upstream f35b7d3: Tab.Clone now deep-clones UsedChannel and TellTarget — PopOut and Temp tabs no longer mutate each other's channel state\n- Active-tab underline scales with DPI and rounds to physical pixels for crisp rendering above 100% scaling\n- IconButton width parameter no longer subtracts HUD-scaled padding from a raw int (measured width passes through verbatim)\n- Internal: HellionStyle ChildBgAlpha extracted to a testable helper; Plugin.SaveConfig clones only the temp tabs; SettingsOverview caches the draw-list per frame; Dalamud.Utility.Util surface routed through an IPlatformUtil indirection (MessageStore IsWine probe is now testable in isolation)\n- Built-in themes: Crystal Nocturne (sapphire and electric magenta over obsidian, by CRYSTALLITE) replaces Moonlit Bloom. Users with Moonlit Bloom selected fall back to Hellion Arctic on first load\n\nBased on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).\n\n---\n\n**v1.4.5 — UX and Robustness (2026-05-12)**\n\nSixth sub-patch of the v1.4.x polish-sweep series. Chat-log draw failures surface as a notification, the first-run wizard has an explicit Later option, the input history clears on plugin reload, and the status bar version slot stops clipping in narrow windows.\n\n- Chat window draw errors now show a one-shot notification instead of failing silently — stack trace stays in /xllog\n- First-run wizard: explicit \"Later — keep defaults\" button. Closing the X no longer silently accepts the defaults; the wizard reopens on the next plugin load if nothing was picked\n- InputHistoryService clears on plugin dispose so the previous session's typed commands don't bleed into the next load\n- Status bar hides the version slot when the chat window is too narrow to fit all five slots without overlap\n- Internal: explicit session-only Auto-Tell-Tab invariant in Plugin.cs plus a pinning test in the Build-Suite\n- Internal: FontManager falls back to the system fonLine truncated
|
||||
"AcceptsFeedback": true,
|
||||
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip",
|
||||
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip",
|
||||
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip",
|
||||
"TestingAssemblyVersion": "1.4.3.0",
|
||||
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.7/latest.zip",
|
||||
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.7/latest.zip",
|
||||
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.7/latest.zip",
|
||||
"TestingAssemblyVersion": "1.4.7.0",
|
||||
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
||||
"ImageUrls": [
|
||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
||||
|
||||
+14
-4
@@ -1,7 +1,9 @@
|
||||
#!/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.
|
||||
# headless `dotnet build` to catch compile-time API drift; Block E runs
|
||||
# `dotnet csharpier check` against HellionChat/; Block F runs markdownlint
|
||||
# against the repo's *.md files. Test execution lives in the local Build-Suite
|
||||
# repo and is NOT part of this preflight.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
@@ -13,10 +15,18 @@ echo "==> preflight: Block A — version consistency"
|
||||
echo "==> preflight: Block B — manifest shape"
|
||||
./scripts/verify-manifest-shape.sh
|
||||
|
||||
echo "==> preflight: Block C — changelog sync - SKIPPED (Changed HellionChat.yaml for better readability, but this is a non-code change and the changelog is already up to date with the previous version bump.TODO: Script fix)"
|
||||
# ./scripts/verify-changelog-sync.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: Block E — csharpier reflow check"
|
||||
dotnet csharpier check HellionChat/
|
||||
|
||||
echo "==> preflight: Block F — markdownlint"
|
||||
# npx --yes avoids a global install; first run caches into ~/.npm/_npx/.
|
||||
# Subsequent runs are sub-second.
|
||||
npx --yes markdownlint-cli2 "**/*.md" "#node_modules" "#bin" "#obj" "#.claude"
|
||||
|
||||
echo "==> preflight: ALL GREEN"
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/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.
|
||||
# yaml.changelog is a single multi-line block with **vX.Y.Z** subblocks.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
@@ -16,11 +16,11 @@ 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."
|
||||
grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" "$YAML" \
|
||||
|| fail "$YAML changelog missing **v${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."
|
||||
jq -r '.[0].Changelog' "$REPO_JSON" | grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" \
|
||||
|| fail "$REPO_JSON Changelog missing **v${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."
|
||||
@@ -39,7 +39,7 @@ 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="$(grep -cE '^[[:space:]]*\*\*v[0-9]+\.[0-9]+\.[0-9]+' "$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"
|
||||
Reference in new issue
Block a user