Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61d5a33683 | |||
| 7ed689587b | |||
| 612bf8814f | |||
| be17472cd5 | |||
| 8bf50151d5 | |||
| 57da455700 | |||
| 0982b68a4a | |||
| 0fc88e480a | |||
| 7eb50e2c8d | |||
| 58e754c169 | |||
| 83064cd40b | |||
| 5ca3b73b7f | |||
| 570a6f071c | |||
| 11ad5db127 | |||
| 5c550e8587 | |||
| eb2a04c56b | |||
| 3f714d6f38 |
@@ -101,16 +101,16 @@ jobs:
|
|||||||
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
|
||||||
$afterMarker = $raw.Substring($idx + $marker.Length)
|
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
}) -join "`n"
|
}) -join "`n"
|
||||||
|
|
||||||
$header = "**Hellion Chat $version"
|
$header = "**v$version "
|
||||||
$start = $changelogBody.IndexOf($header)
|
$start = $changelogBody.IndexOf($header)
|
||||||
if ($start -lt 0) {
|
if ($start -lt 0) {
|
||||||
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
|
||||||
}
|
}
|
||||||
$rest = $changelogBody.Substring($start)
|
$rest = $changelogBody.Substring($start)
|
||||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||||
$trailer = $rest.IndexOf("`n`n---")
|
$trailer = $rest.IndexOf("`n`n---")
|
||||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||||
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
|
||||||
@@ -20,16 +20,12 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
# Manual recovery trigger. Use when a tag was pushed but the auto-run
|
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
|
||||||
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
|
# tag (e.g. v1.4.4) from the Ref dropdown - not main. The Validate tag
|
||||||
# The tag input is validated against the same semver regex as the
|
# ref step below hard-fails if a non-tag ref is selected, because the
|
||||||
# auto-trigger before any string interpolation happens.
|
# release-action reads GITHUB_REF directly and rejects anything that
|
||||||
|
# does not start with refs/tags/.
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: "Existing tag to (re)release, e.g. v0.6.1"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -41,14 +37,21 @@ jobs:
|
|||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# On push:tags, github.ref_name is the tag — checkout default works.
|
# release-action@main reads GITHUB_REF directly (its action.yml
|
||||||
# On workflow_dispatch, ref defaults to the branch the action was
|
# does not declare a tag_name input). Validate up-front so manual
|
||||||
# invoked from; we need to explicitly check out the tag the user
|
# dispatches from a branch ref fail loud here instead of burning
|
||||||
# supplied so the build comes from the tagged commit, not main.
|
# 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
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
|
||||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
|
||||||
|
|
||||||
- name: Setup .NET 10
|
- name: Setup .NET 10
|
||||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
|
||||||
@@ -89,12 +92,11 @@ jobs:
|
|||||||
- name: Generate release body
|
- name: Generate release body
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
env:
|
env:
|
||||||
# workflow_dispatch carries the user-supplied tag in inputs.tag;
|
# github.ref_name is the tag because Validate tag ref above
|
||||||
# push:tags carries it in github.ref_name. Either way the value
|
# already enforced refs/tags/v*. Read via env: so the value
|
||||||
# is treated as a PowerShell variable (env-var pass), not as
|
# is a PowerShell variable, not inline shell text, and gets
|
||||||
# inline shell text, and validated against the semver regex
|
# re-validated against the semver regex below.
|
||||||
# below before any string interpolation.
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
|
|
||||||
run: |
|
run: |
|
||||||
$tag = $env:TAG_NAME
|
$tag = $env:TAG_NAME
|
||||||
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
@@ -111,20 +113,22 @@ jobs:
|
|||||||
|
|
||||||
# changelog: is the last top-level key in the manifest, so
|
# changelog: is the last top-level key in the manifest, so
|
||||||
# everything after the marker is the literal block. Strip the
|
# 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)
|
$afterMarker = $raw.Substring($idx + $marker.Length)
|
||||||
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
|
||||||
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
|
if ($_ -match '^ ') { $_.Substring(4) } else { $_ }
|
||||||
}) -join "`n"
|
}) -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)
|
$start = $changelogBody.IndexOf($header)
|
||||||
if ($start -lt 0) {
|
if ($start -lt 0) {
|
||||||
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
|
||||||
}
|
}
|
||||||
|
|
||||||
$rest = $changelogBody.Substring($start)
|
$rest = $changelogBody.Substring($start)
|
||||||
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
|
$nextHdr = $rest.IndexOf("`n`n**v", 1)
|
||||||
$trailer = $rest.IndexOf("`n`n---")
|
$trailer = $rest.IndexOf("`n`n---")
|
||||||
|
|
||||||
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
|
||||||
@@ -152,19 +156,28 @@ jobs:
|
|||||||
Write-Host $body
|
Write-Host $body
|
||||||
Write-Host "----------------------------------------"
|
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
|
# Gitea-native release action. Creates the release if the tag has no
|
||||||
# release yet, or updates the existing one. body_path provides the
|
# release yet, or updates the existing one with latest.zip attached
|
||||||
# generated release body, files attaches latest.zip. The auto-injected
|
# and the generated body. The auto-injected GITHUB_TOKEN on Gitea
|
||||||
# GITHUB_TOKEN on Gitea Actions has Gitea-API scope and is sufficient
|
# Actions has Gitea-API scope and is sufficient for release write.
|
||||||
# for release write.
|
|
||||||
- name: Attach to Gitea release
|
- name: Attach to Gitea release
|
||||||
uses: https://gitea.com/actions/release-action@main
|
uses: https://gitea.com/actions/release-action@main
|
||||||
with:
|
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 }}
|
files: ${{ steps.locate.outputs.path }}
|
||||||
body_path: release-body.md
|
body: ${{ steps.body.outputs.content }}
|
||||||
api_key: ${{ secrets.GITHUB_TOKEN }}
|
api_key: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
@@ -384,3 +384,7 @@ ChatTwo.Tests
|
|||||||
TestResults
|
TestResults
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|
||||||
|
# Claude Code projekt-spezifisches Setup (lokal, nicht committed)
|
||||||
|
/.claude/
|
||||||
|
/CLAUDE.md
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using HellionChat.Code;
|
using HellionChat.Code;
|
||||||
@@ -19,6 +20,14 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
private readonly MessageStore _store;
|
private readonly MessageStore _store;
|
||||||
private readonly object _tempTabsLock = new();
|
private readonly object _tempTabsLock = new();
|
||||||
|
|
||||||
|
// F2.1: lock-free counter mirrors Config.Tabs.Count(IsTempTab) so the
|
||||||
|
// hot-path getter doesn't contend with HandleTell on every render frame.
|
||||||
|
// Bumped from inside the existing mutation paths so it stays consistent
|
||||||
|
// with the underlying list — see SpawnTempTab, DropOldestTempTab, OnLogout
|
||||||
|
// and ResyncTempTabCounter (used by Plugin.cs snapshot-restore).
|
||||||
|
// TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs
|
||||||
|
private int _activeTempTabCount;
|
||||||
|
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
|
|
||||||
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
|
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
|
||||||
@@ -28,16 +37,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
_store = store;
|
_store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int ActiveTempTabCount
|
internal int ActiveTempTabCount => Volatile.Read(ref _activeTempTabCount);
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (_tempTabsLock)
|
|
||||||
{
|
|
||||||
return Plugin.Config.Tabs.Count(t => t.IsTempTab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Initialize()
|
internal void Initialize()
|
||||||
{
|
{
|
||||||
@@ -46,11 +46,25 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed the counter from the persisted Tabs list so a config that already
|
||||||
|
// contains TempTabs from a prior session starts in sync. Plugin.cs:168
|
||||||
|
// crash-recovery has already dropped TempTabs by the time we get here,
|
||||||
|
// so the snapshot reflects post-recovery reality.
|
||||||
|
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
||||||
|
|
||||||
_messageManager.MessageProcessed += HandleTell;
|
_messageManager.MessageProcessed += HandleTell;
|
||||||
Plugin.ClientState.Logout += OnLogout;
|
Plugin.ClientState.Logout += OnLogout;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F2.1: callable from outside paths that mutate Config.Tabs directly
|
||||||
|
// (Plugin.cs snapshot-restore). Atomically re-pegs the counter to the
|
||||||
|
// live IsTempTab count.
|
||||||
|
internal void ResyncTempTabCounter()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _activeTempTabCount, Plugin.Config.Tabs.Count(t => t.IsTempTab));
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_initialized)
|
if (!_initialized)
|
||||||
@@ -184,6 +198,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
Plugin.Config.Tabs.RemoveAt(victim.Index);
|
||||||
|
Interlocked.Decrement(ref _activeTempTabCount);
|
||||||
|
|
||||||
// Re-anchor active tab to avoid silent switch when tab is dropped
|
// Re-anchor active tab to avoid silent switch when tab is dropped
|
||||||
if (victim.Index <= _plugin.LastTab)
|
if (victim.Index <= _plugin.LastTab)
|
||||||
@@ -208,6 +223,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.Add(tab);
|
Plugin.Config.Tabs.Add(tab);
|
||||||
|
Interlocked.Increment(ref _activeTempTabCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
private static Tab BuildTempTab(string playerName, uint worldRowId)
|
||||||
@@ -361,7 +377,8 @@ internal sealed class AutoTellTabsService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
var removed = Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||||
|
Interlocked.Add(ref _activeTempTabCount, -removed);
|
||||||
|
|
||||||
// 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 temp or index is now out of range
|
||||||
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
|
||||||
|
|||||||
@@ -57,8 +57,18 @@ public class Configuration : IPluginConfiguration
|
|||||||
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
// Empty set means the migration has not run yet — see Plugin.cs v6→v7.
|
||||||
public HashSet<ChatType> PrivacyPersistChannels = [];
|
public HashSet<ChatType> PrivacyPersistChannels = [];
|
||||||
|
|
||||||
// Failsafe for ChatTypes added by future FFXIV patches.
|
// Failsafe for ChatTypes added by future FFXIV patches. New configs default
|
||||||
public bool PrivacyPersistUnknownChannels;
|
// 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)
|
public bool IsAllowedForStorage(ChatType type)
|
||||||
{
|
{
|
||||||
@@ -66,6 +76,20 @@ public class Configuration : IPluginConfiguration
|
|||||||
return true;
|
return true;
|
||||||
if (PrivacyPersistChannels.Contains(type))
|
if (PrivacyPersistChannels.Contains(type))
|
||||||
return true;
|
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.Log.Warning(
|
||||||
|
"PrivacyFilter: unrecognised ChatType {Type} — falling back to PrivacyPersistUnknownChannels={Persist}.",
|
||||||
|
type,
|
||||||
|
PrivacyPersistUnknownChannels
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return PrivacyPersistUnknownChannels;
|
return PrivacyPersistUnknownChannels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||||
<Version>1.4.3</Version>
|
<Version>1.4.4</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<!-- Use lock file to pin exact versions -->
|
<!-- Use lock file to pin exact versions -->
|
||||||
|
|||||||
@@ -35,6 +35,26 @@ tags:
|
|||||||
- Replacement
|
- Replacement
|
||||||
- Privacy
|
- Privacy
|
||||||
changelog: |-
|
changelog: |-
|
||||||
|
**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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**v1.4.3 — Faster plugin load + new repo (2026-05-08)**
|
**v1.4.3 — Faster plugin load + new repo (2026-05-08)**
|
||||||
|
|
||||||
Heavy startup work (migrations, hooks, windows) now runs async so
|
Heavy startup work (migrations, hooks, windows) now runs async so
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private bool _versionWarningLogged;
|
private bool _versionWarningLogged;
|
||||||
|
|
||||||
|
// Thread: framework only — IPC delivery + ImGui render both run there.
|
||||||
public HonorificTitleData? CurrentTitle { get; private set; }
|
public HonorificTitleData? CurrentTitle { get; private set; }
|
||||||
public bool IsAvailable { get; private set; }
|
public bool IsAvailable { get; private set; }
|
||||||
public (uint Major, uint Minor)? DetectedApiVersion { 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));
|
TryUnsubscribe(() => _disposing.Unsubscribe(OnDisposing));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread: framework (scheduled from ctor and OnReady).
|
||||||
private void TryInitialPull()
|
private void TryInitialPull()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -108,6 +110,7 @@ internal sealed class HonorificService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread: framework (Dalamud IPC delivery contract).
|
||||||
private void OnTitleChanged(string json)
|
private void OnTitleChanged(string json)
|
||||||
{
|
{
|
||||||
// Skip updates on version mismatch; subscription stays live for reload.
|
// Skip updates on version mismatch; subscription stays live for reload.
|
||||||
@@ -116,12 +119,13 @@ internal sealed class HonorificService : IDisposable
|
|||||||
CurrentTitle = ParseTitleJson(json);
|
CurrentTitle = ParseTitleJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread: any (Honorific dispatches NotifyReady from its own thread).
|
||||||
private void OnReady()
|
private void OnReady()
|
||||||
{
|
{
|
||||||
// Schedule on framework thread — NotifyReady can dispatch from any thread.
|
|
||||||
_framework.RunOnFrameworkThread(TryInitialPull);
|
_framework.RunOnFrameworkThread(TryInitialPull);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread: framework (IPC delivery contract); idempotent — Disposing fires once.
|
||||||
private void OnDisposing()
|
private void OnDisposing()
|
||||||
{
|
{
|
||||||
// Honorific unloading — clear cached state so the header hides next frame.
|
// Honorific unloading — clear cached state so the header hides next frame.
|
||||||
@@ -133,6 +137,8 @@ internal sealed class HonorificService : IDisposable
|
|||||||
DetectedApiVersion = null;
|
DetectedApiVersion = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thread: framework (called from Dispose, which runs on the framework
|
||||||
|
// cleanup block in Plugin.DisposeAsync).
|
||||||
private void TryUnsubscribe(Action unsubscribe)
|
private void TryUnsubscribe(Action unsubscribe)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -141,20 +147,15 @@ internal sealed class HonorificService : IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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)
|
internal static HonorificTitleData? ParseTitleJson(string json)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(json))
|
if (string.IsNullOrEmpty(json))
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||||
|
|
||||||
// Schema gate: v1.4.3 requires config v16. Users on older schemas
|
// Schema gate: v1.4.x requires config v16. Users on older schemas
|
||||||
// must install v1.4.2 first to run the migration chain.
|
// must install v1.4.2 first to run the migration chain.
|
||||||
if (Config.Version < 16)
|
if (Config.Version < 16)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"HellionChat v1.4.3 requires config schema v16, got v{Config.Version}. "
|
$"HellionChat v1.4.4 requires config schema v16, got v{Config.Version}. "
|
||||||
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.3."
|
+ "Please install v1.4.2 first to migrate the configuration, then upgrade to v1.4.4."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,6 +641,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
Config.Tabs.Clear();
|
Config.Tabs.Clear();
|
||||||
Config.Tabs.AddRange(snapshot);
|
Config.Tabs.AddRange(snapshot);
|
||||||
|
|
||||||
|
// F2.1: snapshot-restore preserves IsTempTab tabs but the mid-step
|
||||||
|
// RemoveAll bypasses AutoTellTabsService, so re-peg the counter.
|
||||||
|
// Null-conditional because SaveConfig can fire before Phase-2 init.
|
||||||
|
AutoTellTabsService?.ResyncTempTabCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void LanguageChanged(string langCode)
|
internal void LanguageChanged(string langCode)
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ namespace HellionChat.Privacy;
|
|||||||
|
|
||||||
internal static class PrivacyDefaults
|
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
|
// 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
|
// are persisted out-of-the-box. Public chat, NPC dialogue, system logs and
|
||||||
// battle messages require explicit opt-in.
|
// battle messages require explicit opt-in.
|
||||||
|
|||||||
@@ -54,15 +54,21 @@ internal static class AutoTranslate
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Warms the auto-translate cache on a background thread so the first
|
// 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()
|
internal static void PreloadCache()
|
||||||
{
|
{
|
||||||
new Thread(() =>
|
var thread = new Thread(() =>
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
AllEntries();
|
AllEntries();
|
||||||
Plugin.Log.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
|
Plugin.Log.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms");
|
||||||
}).Start();
|
})
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "HellionChat-AutoTranslate-Warmup",
|
||||||
|
};
|
||||||
|
thread.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<AutoTranslateEntry> AllEntries()
|
private static List<AutoTranslateEntry> AllEntries()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||||
[](LICENSE)
|
[](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://github.com/goatcorp/Dalamud)
|
||||||
[](https://dotnet.microsoft.com/)
|
[](https://dotnet.microsoft.com/)
|
||||||
[](https://www.finalfantasyxiv.com/)
|
[](https://www.finalfantasyxiv.com/)
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Version 1.4.3** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
**Version 1.4.4** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
|
||||||
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
|
[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
|
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
|
||||||
@@ -286,14 +286,16 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
|
|||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
**Version 1.4.3** — Plugin-load async init plus repo cutover: the plugin has been migrated to Dalamud's
|
**Version 1.4.4** — Threading and IPC safety polish on top of the v1.4.3 async-load foundation. The
|
||||||
`IAsyncDalamudPlugin` API. The constructor now handles only bootstrap essentials (config load, language init, conflict
|
`AutoTellTabsService` hot-path getter now reads from an `Interlocked` counter instead of taking a lock on every
|
||||||
detection); migrations, service allocations, window construction, and hook subscriptions move into `LoadAsync`, allowing
|
render frame, with a resync hook for the snapshot-restore path in `SaveConfig` and a pure-helper test mirror in the
|
||||||
Dalamud to keep the UI responsive during heavy lifting. A schema gate replaces the v9 → v16 migration chain; configs at
|
Build-Suite repo. The Honorific integration carries per-method threading banners so the framework-thread invariant is
|
||||||
schema v16+ load directly, older configs trigger an "install v1.4.2 first" error message. Custom repo URL migrated to
|
visible at the call site, and an unsubscribe failure now logs at Warning instead of Debug — a leaked subscription
|
||||||
`gitea.hellion-forge.cloud`; the GitHub repo remains as a frozen v1.4.2 snapshot. Plugin load time is ~3.7 s median (5
|
across plugin reloads is exactly the kind of thing that should not be silent. The AutoTranslate warmup thread is
|
||||||
reloads), comparable to v1.4.2 — the async migration is the foundation for v1.4.4 lazy-init optimizations, not a direct
|
finally marked `IsBackground = true`, matching the pattern used in `MessageManager` and `Plugin.RetentionSweep` since
|
||||||
user-facing win. Fourth sub-patch of the v1.4.x polish sweep series (as of 2026-05-08).
|
v1.4.0. The privacy filter logs once per unknown ChatType so a future patch's added channel does not drop off the
|
||||||
|
radar, and new installs default `PrivacyPersistUnknownChannels` to `true` as a failsafe; existing configs keep their
|
||||||
|
explicit choice. No schema bump, no migration. Fifth sub-patch of the v1.4.x polish sweep series (as of 2026-05-12).
|
||||||
|
|
||||||
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,38 @@ to the release pages for details.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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)
|
## 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
|
Plugin lifecycle migrated to Dalamud's `IAsyncDalamudPlugin` API. The constructor now does only the bootstrap-essentials
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"Author": "Jon Kazama (Hellion Forge)",
|
"Author": "Jon Kazama (Hellion Forge)",
|
||||||
"Name": "Hellion Chat",
|
"Name": "Hellion Chat",
|
||||||
"InternalName": "HellionChat",
|
"InternalName": "HellionChat",
|
||||||
"AssemblyVersion": "1.4.3.0",
|
"AssemblyVersion": "1.4.4.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",
|
"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",
|
"ApplicableVersion": "any",
|
||||||
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat",
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
"CanUnloadAsync": false,
|
"CanUnloadAsync": false,
|
||||||
"LoadPriority": 0,
|
"LoadPriority": 0,
|
||||||
"Punchline": "A Hellion Forge plugin. Privacy-first chat for FFXIV, built to stay out of your way.",
|
"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.4 — Threading and IPC safety polish (2026-05-12)**\n\nFifth 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.\n\n- AutoTellTabs hot-path getter uses an Interlocked counter instead of taking the lock on every read\n- Honorific integration: per-method threading banners, plus Warning-level log on unsubscribe failure\n- AutoTranslate warmup thread marked IsBackground so plugin unload doesn't wait for it\n- PrivacyFilter logs once per unknown ChatType so a future patch's added channel doesn't drop off the radar\n- New installs persist unknown channels by default; existing configs keep their explicit choice\n\n---\n\n**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",
|
||||||
"AcceptsFeedback": true,
|
"AcceptsFeedback": true,
|
||||||
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.3/latest.zip",
|
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/download/v1.4.4/latest.zip",
|
||||||
"DownloadLinkUpdate": "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.4/latest.zip",
|
||||||
"DownloadLinkTesting": "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.4/latest.zip",
|
||||||
"TestingAssemblyVersion": "1.4.3.0",
|
"TestingAssemblyVersion": "1.4.4.0",
|
||||||
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/icon.png",
|
||||||
"ImageUrls": [
|
"ImageUrls": [
|
||||||
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/raw/branch/main/HellionChat/images/chatWindow.png",
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ echo "==> preflight: Block A — version consistency"
|
|||||||
echo "==> preflight: Block B — manifest shape"
|
echo "==> preflight: Block B — manifest shape"
|
||||||
./scripts/verify-manifest-shape.sh
|
./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)"
|
echo "==> preflight: Block C — changelog sync"
|
||||||
# ./scripts/verify-changelog-sync.sh
|
./scripts/verify-changelog-sync.sh
|
||||||
|
|
||||||
echo "==> preflight: Block D — plugin compile health"
|
echo "==> preflight: Block D — plugin compile health"
|
||||||
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
|
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# verify-changelog-sync.sh — Block C.
|
# verify-changelog-sync.sh — Block C.
|
||||||
# Strips .0 suffix from repo.json AssemblyVersion to derive the 3-digit tag/version.
|
# 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
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
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$//')"
|
VER="$(jq -r '.[0].AssemblyVersion' "$REPO_JSON" | sed -E 's/\.0$//')"
|
||||||
TAG="v$VER"
|
TAG="v$VER"
|
||||||
|
|
||||||
grep -qE "^[[:space:]]*\*\*Hellion Chat ${VER}" "$YAML" \
|
grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" "$YAML" \
|
||||||
|| fail "$YAML changelog missing **Hellion Chat ${VER}** subblock. Fix: add the v${VER} block at the top of the changelog field."
|
|| 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}" \
|
jq -r '.[0].Changelog' "$REPO_JSON" | grep -qE "^[[:space:]]*\*\*v${VER}[^0-9]" \
|
||||||
|| fail "$REPO_JSON Changelog missing **Hellion Chat ${VER}** subblock. Fix: copy the yaml changelog over."
|
|| fail "$REPO_JSON Changelog missing **v${VER}** subblock. Fix: copy the yaml changelog over."
|
||||||
|
|
||||||
FORGE_FILE="$FORGE_DIR/${TAG}.md"
|
FORGE_FILE="$FORGE_DIR/${TAG}.md"
|
||||||
[ -f "$FORGE_FILE" ] || fail "$FORGE_FILE missing. Fix: create from previous tag's file as template."
|
[ -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=$((TITLE_LEN + ${#BODY} + FOOTER_LEN))
|
||||||
[ "$TOTAL" -le 5500 ] || fail "Forge embed total ~${TOTAL} chars > 5500 cap."
|
[ "$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."
|
[ "$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"
|
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