ThemeCrossfadeSelfTestStep walks Switch -> crossfade-observed ->
mid-crossfade-switch -> crossfade-end -> restore using
TryGetActiveCrossfade, returns Waiting frame-by-frame and Pass after
the restore concludes. The mid-switch phase fires a second Switch
within ~100ms of the first observed crossfade and asserts the lerped
value is neither identity-from nor identity-to, exercising the
ArmCrossfade mid-flight-origin override.
QuickPickerSelfTestStep verifies the three new resource strings, the
built-in theme floor (>=10), and Config.Tabs non-empty.
Switch picks a lerped AbgrCache during the 300ms crossfade window
(ReduceMotion bypass keeps the snap path). Plugin-load init path
switches to SwitchSilent so opening the plugin no longer fades from
the default theme. WindowBg/ChildBg RGBA path stays bound to the
user's per-window opacity override and never fades.
PushGlobal takes the ThemeRegistry as a parameter -- it is an instance
member on Plugin, not static, so the single Plugin.Draw call-site
threads it through alongside the active theme.
Plugin.cs:937 only pushed RegularFont when Config.FontsEnabled was true.
FontsAndColours.cs:50 forces FontsEnabled=false whenever UseHellionFont is
enabled (to hide the chooser UI), so the bundled-font path was silently
dead and the FFXIV Axis game-font took over. Exo 2 looked "almost right"
because it overlaps Axis on basic Latin, so the regression went unnoticed
for the entire v1.5.x series.
The fix routes RegularFont through draw whenever either FontsEnabled or
UseHellionFont is on. First-frame HITCH dropped from ~74 ms to ~20 ms
median (5-reload Linux/Wine sample 17.9-23.6 ms) as a side effect — the
v1.5.1 "too optimistic" defer-pattern hypothesis was actually a symptom
of this bug, not bad math.
Font-stack overhaul on top:
- Inter Light (Static 18pt-Light, 343 KB, SIL OFL 1.1) replaces Exo 2 as
the bundled font. Inter ships full Latin Extended-A/B, Greek polytonic
and Cyrillic Supplement coverage.
- NotoSansCjkRegular added as a third merge layer for Hangul,
Simplified-Chinese-specific Han glyphs, and CJK fallbacks the FFXIV
Japanese font does not ship.
- Two new ExtraGlyphRanges flags (LatinExtended, Greek) implemented via
AddChar pair lists in SetUpRanges.
- Settings.Apply auto-activates the matching ExtraGlyphRanges flag on
language change. Plugin.LoadAsync runs a one-shot migration that ORs
in the required flag for an already-selected language.
- ExtraGlyphRanges CollapsingHeader reachable regardless of
UseHellionFont (was hidden in the early-return branch).
- New WarningText below the language combo: FFXIV's chat engine only
fully supports EN/DE/FR/JA. Other scripts render in the HellionChat
UI but may garble in in-game chat input/send.
Localisation wave (originally a FR-only cycle):
- 24 selectable UI languages. LanguageOverride enum gains 10 new locales
plus 3 previously commented-out (Italian, Korean, Norwegian with ISO
code `nb` instead of `no`). All new values append to keep existing
user-config integer serialisation stable.
- Resource bundle split: HellionStrings.resx (24 locales, 328 keys) for
fork-added strings, Language.resx (24 locales, 456 keys) for the
ChatTwo-Crowdin-heritage. 4 post-sync Crowdin keys backfilled into
13 legacy locales with per-key AI-assisted comment marker.
- Em-dash sweep on EN source plus 18 translations. Russian and Ukrainian
keep their typographic norm.
Old HellionFont.ttf + HellionFont-OFL.txt removed; Inter-Light.ttf +
Inter-OFL.txt take their place. Configuration field UseHellionFont keeps
its name for backwards-compat. Migration v17 stays.
Bestehende User haben FirstRunCompleted=true vom alten Single-Page
Wizard und würden den neuen Multi-Step-Flow nie zu sehen bekommen.
Neues Config-Feld WizardLastShownVersion (Default leer) trägt die
Version, deren Wizard zuletzt gezeigt wurde. Plugin.LoadAsync
vergleicht gegen die Konstante WizardReshowVersion ("1.5.2") und
setzt FirstRunCompleted einmalig zurück, wenn die Werte abweichen.
SaveConfig sofort danach, damit ein Pre-Finish-Crash die Re-Show
nicht endlos wiederholt. Künftige Cycles bumpen die Konstante nur
wenn der Wizard wirklich umstrukturiert wird.
Variant 1 walks the FirstRunWizard state machine through Step 1 →
4 and commits with no pending values to verify the no-op
write-back path. Variant 2 picks Roleplay on Step 2, skips Step 3,
commits, and asserts LoadPreviousSession /
FilterIncludePreviousSessions stayed on their pre-test value —
pinning the null-semantics from Spec Z.176. ApplyRoleplay would
overwrite six privacy / retention fields, so the step snapshots
them before Variant 2 and CleanUp() restores them, keeping the
self-test idempotent across /xlperf runs. Catches state-machine
throws and CommitPending NREs that would otherwise surface as a
hard plugin crash during Finish ✓ clicks. Runs alongside the
existing three FontManager / ThemeSwitch self-test steps.
Two new self-test steps for the hybrid FontManager:
- FontManagerCtorSmokeStep proves all five handles land on the manager
after Phase-1 resolve (ItalicFont nullable per Config.ItalicEnabled)
and that no atlas-load exception is sitting on any of them
- FontPushSmokeStep proves IFontHandle.Push() returns without throwing
for the two main delegate handles right after plugin load
Both steps run on the framework thread via the xlperf self-test path
and are registered alongside the existing theme-switch step in
SelfTestRegistry.
Move font handle creation from BuildFonts() into the FontManager ctor
inside a single atlas.SuppressAutoRebuild() block. Axis, AxisItalic and
FontAwesome become init-only IFontHandle properties; RegularFont and
ItalicFont stay mutable so the live font-settings rebuild path keeps
working without a plugin reload.
- BuildFonts() renamed to RebuildDelegateFonts(), scope reduced to the
delegate fonts only
- BuildFontsAsync() removed; Task.Run had no purpose with ctor-init
- FontManagerInitHostedService deleted; PluginHostFactory drops the
matching AddHostedService registration
- PluginHostFactory FontManager registration takes IDalamudPluginInterface
via factory lambda
- Settings save path now calls RebuildDelegateFonts() instead of
BuildFonts()
- Plugin.Draw push site gets a null-forgiving for the nullable
RegularFont with a one-line WHY
Code comments were drifting into plan-internal shorthand (DI-2a,
Slice B, "see plan §9") that nobody outside the cycle authors can
decode. They also tended toward AI-generated paragraph blocks where a
two-line WHY would have done.
This commit tightens the comment surface from the v1.5.0 work:
- IPluginLogProxy header lists the consumer buckets without naming
the cycle items that decided them.
- DalamudLogger / DalamudLoggingProvider provenance markers explain
themselves in two lines each; the long EUPL-rationale paragraph
moves to the commit message.
- PluginHostFactory block headers shrink to one line each, ASCII
dividers come out, plan-internal codes go.
- Plugin.cs field doc and Phase-1 / DisposeAsync comments lose the
cycle-name references; the file gains nothing from "C3 surfaced X"
in code.
- FontManager / GameFunctions static-method notes shrink to one
sentence each.
- InitHostedServices class header keeps the eager-resolve WHY in
three lines, drops the constraint label.
Csharpier reformatted the .csproj layout (long PackageReference
multi-lined). No functional change, no behavior change.
C3's Phase-1 bridge in Plugin.ctor already pulls IPlatformUtil and
IPluginLogProxy out of the container right after the host builds, so
the manual `new DalamudPlatformUtil()` / `new DalamudPluginLogProxy`
assignments in Phase-0 were just allocating throwaway instances that
got overwritten a few lines later.
Phase-0 helpers that run before the container build
(MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize) do
not touch Plugin.PlatformUtil or Plugin.LogProxy, so the brief
null-window between the schema gate and the container build is safe.
The DalamudPlatformUtil and DalamudPluginLogProxy wrapper classes
themselves stay in the code; DI-4 (logger migration to ILogger<T>)
will eventually retire the proxy for new sites but EmoteCache,
AutoTranslate, MemoryUtil and WrapperUtil keep using it.
Smoke 1 of C3 surfaced MessageManager.DisposeAsync throwing on unload:
Plugin.DisposeAsync ran the manual MessageManager teardown (CTS
cancel + dispose at MessageManager.cs:84-99), then awaited
_lifecycle.DisposeAsync which routed Host.Dispose through the
container, which hit MessageManager.DisposeAsync a second time and
threw ObjectDisposedException on the already-disposed CTS.
Plugin.DisposeAsync now drops every manual service dispose - the
container owns those singletons end-to-end. The framework-thread block
keeps the three calls the container has no handle on
(TearDownCommands, GameFunctions.SetChatInteractable,
WindowSystem.RemoveAllWindows), plus the static-class cleanups
(EmoteCache.Dispose, InputHistoryService.Reset) stay outside the
container entirely.
This changes the teardown order versus v1.4.10: the container disposes
in reverse-registration order, which puts Windows ahead of IPC
services. The v1.4.10 ordering ("IPC before Windows so a final IPC
event cannot hit a half-torn ChatLogWindow") is no longer enforced.
Host.Dispose runs synchronously on the framework thread, so no
Framework.Update or Draw event fires during teardown; the remaining
risk is an external IPC plugin invoking a subscriber mid-dispose,
which is not something v1.4.10 actually prevented either.
Flips the container live. Plugin.ctor now builds the host after the
schema gate clears, pulls PluginLifecycle out of the container, and
backfills the Plugin.X static surface plus the instance properties
(11 services + 8 windows) so existing consumers reach the same
instances the container holds.
Plugin.LoadAsync gets thinner: service and window allocations are gone
(the container owns them), BuildFonts / Switch / FilterAllTabsAsync /
Initialize moved to their hosted-service adapters inside
Host.StartAsync, WindowSystem.AddWindow moved into
PluginLifecycle.LoadAsync on the framework thread. Plugin-internal
init (SelfTestRegistry, FirstRunWizard, SetupCommands +
Commands.Initialise, RetentionSweep, EmoteCache.LoadData, FTS5 rebuild
worker, UiBuilder.Disable*UiHide, AutoTranslate.PreloadCache,
Framework / Draw / LanguageChanged subscribes) stays in Plugin.LoadAsync
because each step reaches Plugin-private members or fields.
Plugin.DisposeAsync keeps the manual teardown for ordering (IPC before
windows, hooks first) and awaits _lifecycle.DisposeAsync at the end to
stop the host and dispose the container on the framework thread.
Double-disposes against container singletons are no-ops for the
services that hold real resources (Dispose idempotency is the standard
pattern).
PluginLifecycle takes Plugin as a constructor arg so it can iterate
the Window properties and call WindowSystem.AddWindow on the framework
thread; v1.4.9 Stage-2 verified that AddWindow's backing List<> is
not thread-safe.
Plan drift D4 noted: Plugin.cs ends at 1050 lines instead of the
150-220 vision because helper methods (MigrateFromChatTwoLayout,
SeedExampleThemeIfEmpty, RunRetentionSweepIfDue, FrameworkUpdate,
Draw, LanguageChanged, SetupCommands, slash handlers, FTS worker)
stay in Plugin.cs. Extracting them is DI-2b or a dedicated service
refactor in v1.5.1+. C3 still hits the DI-2a goal: bootstrap is
container-driven and LoadAsync is allocation-free.
PlatformUtil and LogProxy keep the manual `new` for now; C5 (DI-3)
removes those once C3 stabilises in the smoke test.
TearDownCommands attached the same instance via re-Register with identical
args, which was functionally a no-op but masked a latent bug if Description
or ShowInHelp ever diverged between Setup and Teardown. Hold the wrapper
instances as nullable fields so Teardown can detach the live registration
directly. Mirrors the cached-wrapper pattern in ChatLogWindow.
Manifest version bump for the v1.4.9 release cut. Schema-required v16
stays unchanged (R1/R2/R3 are all config-neutral refactors).
Files touched:
- HellionChat/HellionChat.csproj: <Version> 1.4.8 -> 1.4.9
- HellionChat/Plugin.cs: schema-migration error string self-reference
(v1.4.8 -> v1.4.9, required schema v16 stays)
- repo.json: AssemblyVersion, TestingAssemblyVersion, 3x DownloadLink*
URLs all bumped to 1.4.9 / v1.4.9. Changelog field is still on v1.4.8;
the v1.4.9 block plus v1.4.5 slim-drop land in the next commit.
- README.md: shield badge, version header in lead paragraph, project-
status block rewritten for v1.4.9 (Plugin-Load Render Polish).
- docs/CHANGELOG.md: v1.4.9 block inserted above v1.4.8.
- docs/ROADMAP.md: v1.4.9 moved into the released-versions list,
"Next Cycle" header now targets v1.4.10 (Render Clipper + Symbol
Picker reserves carried over from the v1.4.9 plan).
yaml changelog block and repo.json Changelog field follow in the
docs commit so the slim-drop of v1.4.5 stays atomic with the v1.4.9
block insert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the four user-triggered slash-commands (/hellion, /hellionView,
/hellionDebugger, /hellionSeString) plus the two Plugin-Manager
UiBuilder hooks (OpenConfigUi, OpenMainUi) out of their window
constructors and into a central Plugin.SetupCommands method so they
work before their target window has been opened the first time. A
matching TearDownCommands runs as the first CaptureFailure inside the
framework-thread teardown lambda. /hellion and /hellionSeString stay
under the same #if DEBUG guard SeStringDebugger had before. The four
window classes keep their public Dispose method signatures so the
existing Plugin.DisposeAsync method-group binding still resolves —
the bodies are now empty pointers to TearDownCommands. The pre-v1.4.9
`OpenMainUi` body that flipped SettingsWindow.IsOpen and the three
private Toggle(string, string) method-group wrappers are gone since
the central handlers call SettingsWindow.Toggle() / DbViewer.Toggle()
etc. directly.
The properties stay eager in stage 1 — the lazy-init switch lands in
stage 2 with the matching `_lazyWindowLock` guard around AddWindow
and RemoveAllWindows. Doing it in two commits keeps the slash-command
correctness verifiable on its own.
Smoke (release build): /hellion, /hellionView, /hellionDebugger,
/clearhellion plus Plugin-Manager Settings and Open buttons all
toggle their target window. /hellionSeString remains DEBUG-only as
before.
- csproj <Version>, Plugin.cs schema-gate self-reference, repo.json
(AssemblyVersion, TestingAssemblyVersion, 3x DownloadLink URLs).
- README.md shield badge, version header, Project Status body.
- docs/CHANGELOG.md gains a v1.4.8 section above v1.4.7.
- docs/ROADMAP.md flips Next Cycle to v1.4.9 (Plugin-Load Render
Polish), v1.4.8 moves into the released history above v1.4.7.
- Config schema stays at v17, Migration v17 stays additive.
repo.json Changelog field and HellionChat.yaml changelog block plus
the new forge-posts/v1.4.8.md follow in a separate commit (slim-drop
of v1.4.4 happens there).
Retention sweep no longer blocks for ~194ms on Framework.Run().Wait().
The clear+refilter pair is now scheduled on the next framework tick, so
it still runs on the framework thread (keeping the Tabs-list mutation
serialisation invariant -- Plugin.Config.Tabs is plain List<Tab> and
AutoTellTabsService can mutate it from background paths) but does not
block the sweep thread while the framework finishes the current frame.
A new _isDisposing volatile bool is set as the first statement in
DisposeAsync so a deferred tick that fires after teardown bails before
it touches MessageManager / Log / static fields the dispose path has
already cleared. The retention worker is IsBackground=true so plugin
unload can race against a still-pending tick.
The existing RetentionSweepLock / RetentionSweepRunning serialisation
covers the not-two-sweeps-at-once invariant; we don't add a CTS here
because RunOnTick is fire-and-forget and the framework service owns
the tick lifecycle.
v1.4.8 B3. Coverage via in-game smoke (frame-time trace during a
retention sweep run) in Task 9 -- no Build-Suite test because the
suite has no FakeFramework fixture and the change is a schedule-form
swap rather than new behaviour.
When the user edits their active custom theme JSON in an external editor
and saves, the change now propagates to HellionChat within ~1 second
without re-selecting the theme in the picker.
RefreshActiveIfStale runs from Plugin.Draw on every frame but the actual
File.GetLastWriteTimeUtc stat is 1Hz-throttled -- 60fps would otherwise
mean 3600 stats/min, more on Wine. Built-in themes short-circuit on the
IsBuiltIn check; custom themes without a captured source path (Switch
fell to default) short-circuit on the null check.
Switch() now captures the source path of custom themes via an out-param
on LoadCustomBySlug, which now reverse-looks-up against the existing
_customCache (no re-parse, no extra disk IO). Plugin.LoadAsync warms the
cache via AllCustom() once before the first Switch so a Config.Theme
pointing at a custom slug does not fall through to the built-in default
on a cold registry.
Switch's lookup order is now built-in-first to match Get(slug), so a
user-authored JSON that declares a built-in slug is consistently
ignored in both code paths.
Pure-helper ThemeStampDiff isolates the stamp-diff rules for the
Build-Suite (covers DateTime.MinValue hold-the-line semantics).
v1.4.8 B2.
Adds the worker that fills the messages_fts virtual table after Migrate4.
The bulk-insert runs off the framework thread on its own SqliteConnection
opened via OpenSecondaryConnection -- WAL lets the live UpsertMessage
path on the primary Connection keep flowing, and the worker's writer
lock yields every 500 rows with a 5ms breather so PendingMessageThread
does not hit "database is locked" after DefaultTimeout=5s.
InitFtsReadyCache runs in the ctor and short-circuits to ready=true when
the index is already populated or when the messages table is empty. The
DbViewer (Task 4.4) reads IsFtsIndexBuilt per frame as a single volatile
field read.
Plugin.cs LoadAsync kicks the worker after FilterAllTabsAsync, gated on
IsFtsIndexBuilt and a CancellationTokenSource that DisposeAsync cancels
before MessageManager tears down. Progress reports back via IActiveNotification,
marshalled onto the framework thread via Framework.RunOnTick. Success path
finishes the notification as Success with a 5s linger; cancellation
dismisses it; an error swaps the type to Error with a fallback hint.
Tester-Request from Jin (2026-05-03): TempTabs should be pinnable so a
key conversation partner survives a relog. Right-click a TempTab and
choose Pin Tab / Unpin Tab / Promote to permanent.
Pool semantics:
- AutoTellTabsLimit (15) still gates the auto-managed unpinned pool.
- Pinned TempTabs live in their own pool, hard-capped at 5.
- The 6th pin attempt fails with a notification; users can unpin first
or promote to permanent.
- Unpinning into a full unpinned pool drops the oldest unpinned (no
user friction).
Mechanics:
- Tab.IsPinned (default false); Tab.Clone() carries it.
- Migration v16 -> v17 (additive; existing tabs default to unpinned).
- Three strip-sites synchronised through TabLifecycleHelpers:
Plugin.cs load-time, Plugin.SaveConfig, Configuration.UpdateFrom.
- AutoTellTabsService:
* MaxPinnedTempTabs constant.
* F2.1 _activeTempTabCount counter retired — ActiveTempTabCount is
now Tabs.Count(predicate). Pin/Unpin/Promote transitions are
cold-path and don't need lock-free reads.
* DropOldestTempTab filters on IsInUnpinnedPool so pinned tabs are
never drop candidates.
* OnLogout strips only the unpinned pool; pinned popouts and the
active-tab switch behave correspondingly.
* TryPin / Unpin / PromoteToPermanent service methods.
- ChatLogWindow tab context menu: Pin / Unpin / Promote with disabled-
state at-cap tooltip + Promote tooltip explaining the channel-filter
side effect.
- HellionStrings (EN+DE) for menu labels, tooltips, the limit warning.
- AutoTellTabsLimit slider description now flags the separate pinned
pool so users aren't surprised by 18 tabs when the limit reads 15.
F12.2 closes the gap that F12.1 left open: MessageStore's ctor calls
Plugin.Log.Information inside Migrate0, which prevents an isolated xUnit
construction test (Dalamud.dll cannot load in the test AppDomain).
The proxy mirrors IPluginLog's full surface (Verbose/Debug/Information/
Info/Warning/Error/Fatal — both Info and Information as Dalamud exposes
them) with both single-string and Exception+string overloads, so the
~91 existing Plugin.Log.* call-sites become a drop-in rewrite to
Plugin.LogProxy.*.
A later DI-container adoption cycle (v1.5.x) may swap this for
Microsoft.Extensions.Logging's ILogger<T>; this commit is the
intermediate decorator step.
Plugin.cs:171-172 hardcoded the version into the schema-gate
InvalidOperationException string. The follow-up rename in v1.4.7 will
move this to Plugin.Interface.Manifest.AssemblyVersion so this commit
stops happening every cycle, but for v1.4.6 the bare version bump is
the smallest change.
Also picks up a one-line csharpier reflow on UrlValidation.cs
collapsed by the format pass.
Introduces a thin interface around Util.IsWine and Util.OpenLink so
services can be constructed in an isolated xUnit AppDomain without
forcing Dalamud.dll onto the assembly search path. Production wiring
(DalamudPlatformUtil) caches IsWine at ctor time — it's a runtime
probe that never changes for the lifetime of a plugin instance,
mirroring the Lightless DalamudUtilService pattern.
Plugin.PlatformUtil is wired in the Phase-1 ctor so any service that
LoadAsync allocates can resolve the platform indirection without
plumbing the instance through additional constructor params.
Follow-up commits route MessageStore and the OpenLink call-sites
through this interface.
The pre-serialization snapshot used to clone the entire Config.Tabs
list, then Clear/AddRange the snapshot back. With a typical config of
~30 user-defined tabs plus up to 15 session-only temp tabs, that's a
45-item clone on every save. The persistent tabs never leave the list
during this routine, so cloning only the temp subset is functionally
identical and keeps the allocation proportional to AutoTellTabsLimit.
Pattern-adherence pass after the cycle's code commits:
- ChatLogWindow.cs: NotifiedDrawFailure renamed from
_notifiedDrawFailure. The file's per-window state flags (DrewThisFrame,
WasDocked, Activate, PlayedClosingSound, …) all use PascalCase
without underscore prefix; the new flag now matches that
- Plugin.cs: trim the session-only RemoveAll comment from 5 lines to 2
and add the standard TEST-MIRROR pointer line. Same shape as
AutoTellTabsService.cs:28 and the other six TEST-MIRROR sites
- InputHistoryService.cs: add the TEST-MIRROR pointer for the new
Build-Suite tests
Manifest sync across csproj, yaml, repo.json, README, CHANGELOG,
ROADMAP and the Plugin.cs schema-gate error message. ROADMAP also gets
the v1.4.4 release block that was missed in that cycle's closure.
Forge-post v1.4.5.md follows the established frontmatter + DE-body
convention; the EN block is sourced from the yaml changelog by the
forge-announce workflow.
Expands the one-liner above Plugin.cs:167-168 to spell out *why* the
RemoveAll runs before AutoTellTabsService.Initialize: tells are
typically privacy-filtered, so resurrecting a tab from a crashed
session would trigger DB reconstruction on the next load. Also links
to the TEST-MIRROR pin in the Build-Suite for future readers.
Static InputHistoryService entries used to survive a plugin reload
because static field state doesn't get cleaned up on its own. The new
Reset() method clears the list and is wired into Plugin.DisposeAsync
alongside the existing pure-memory cleanups, so the next plugin load
starts with an empty history instead of inheriting the previous
session's typed commands.
Pre-push grep-verification found four stale v1.4.3 mentions outside the
Slim-Rule history files:
- Plugin.cs schema-gate error message referenced v1.4.3 by name in both
the comment and the user-facing exception text. Schema stays at v16,
but the message now points at the current release
- README.md latest-release badge bumped to v1.4.4
- README.md version header bumped to v1.4.4
- README.md Project Status block rewritten for v1.4.4 with the threading
and IPC safety items as the lead
ROADMAP.md historical references to v1.4.3 are intentional (released-tag,
foundation-reference) and stay.
F2.1: ActiveTempTabCount was doing a LINQ Count under _tempTabsLock on
every read, including the hot-path HandleTell guard. Replace with an
Interlocked counter kept in sync with Config.Tabs from inside the
existing mutation paths (SpawnTempTab, DropOldestTempTab, OnLogout).
Initialize from the persisted Tabs list on Initialize() to handle
configs that already contain TempTabs from a prior session.
Plugin.cs SaveConfig snapshot-restore mutates Config.Tabs outside of
AutoTellTabsService; expose ResyncTempTabCounter() and call it after
AddRange so the counter stays consistent. Plugin.cs:168 crash-recovery
RemoveAll runs before Initialize() and is covered by the init snapshot.
Updated .editorconfig to set indent_style=space and indent_size=4 for C# files. Reformat all .cs files to apply the new indentation settings. No code logic changes, just whitespace reformatting.
also updated some comments in files in shorter and Precise way. No logic changes, just comment rewording for clarity and conciseness.
Add .prettierrc.json, .markdownlint.json, .yamllint.yaml, .gitattributes
Run CSharpier, Prettier and markdownlint across the entire codebase.
No logic changes — formatting, using order and line endings only.
Migrations: all current users are on schema v16, the v9 to v16 migration
chain ran in v1.2.1 and earlier. Replace the seven in-LoadAsync migration
blocks with a hard schema-gate in the Phase-1 ctor; older configs trigger
a clear "install v1.4.2 first" error. Code-hygiene change, fast-path
saving is negligible. Remove the now-unused TryReadPreV13ThemeOpacity
helper that only served the v13 to v14 block.
AutoTranslate.PreloadCache: was sync ~300 ms in LoadAsync. Move to
Task.Run so plugin-load returns ~300 ms earlier. Trade-off: first
auto-translate use of a session may have a sub-second hitch if the
cache hasn't finished warming. Acceptable, it is first-use cost
instead of every-load cost.
The previous fire-and-forget Task.Run pattern could leave Plugin.FontManager
null when the first UiBuilder.Draw tick fires (ChatLogWindow dereferences
FontManager.FontAwesome / RegularFont / ItalicFont in its draw paths).
Allocate FontManager and call BuildFonts() synchronously, mirroring
ChatTwo Plugin.cs:152. BuildFonts itself is non-blocking — it just
registers IFontHandles with Dalamud's atlas; the actual atlas rebuild
runs on Dalamud's pipeline a few frames later, so the perceived-load
win still holds (LoadAsync no longer waits for atlas build).
BuildFontsAsync in FontManager.cs stays for the Settings-driven manual
rebuild path.
Phase-1 was still doing 7 schema migrations and 25+ service allocations
synchronously, blocking the ctor return. Move all of that to LoadAsync,
keeping only bootstrap-essentials in the ctor: conflict detection,
config load, language init, ImGui init, WindowSystem skeleton.
Decouple the font task from the LoadAsync await — font-build runs
fire-and-forget, so first frames render with Dalamud's default font
until the Hellion-Exo2/NotoSans atlas rebuild completes (visible
"font-pop"). Mirrors ChatTwo's pattern; the perceived-load win comes
from "Finished loading" landing earlier, not from a faster atlas build.
Smoke test in Task 6 surfaced a NullReferenceException at Plugin.cs:885 —
the retention sweep was scheduled in Phase 1 but dereferences
MessageManager.Store, which is only allocated in Phase 2 (LoadAsync).
Move the call after MessageManager init. Drop the comment that wrongly
claimed independence from Phase-2 services.
I-1: rewrite property-shape comment to reflect that all properties (not
just Phase-2 ones) moved to { get; private set; } = null!;.
I-3: drop plan-jargon (Q1=A / Q3=B / Task 5) from source comments;
replace with durable rationale and a version-anchored TODO for the
FontManager.BuildFontsAsync follow-up.
I-4: remove German-word leak ("pflicht") from English comment in
DisposeAsync.
M-5: wrap each cleanup line inside Framework.RunOnFrameworkThread with
CaptureFailure so a single Dispose throw no longer strands subsequent
cleanup. Drops the inline try/swallow on SetChatInteractable. Mirrors
Lightless DisposeFrameworkBoundServicesAsync pattern.
Renames HellionChat/SelfTest/ to HellionChat/SelfTests/ (plural) to
match the folder convention used throughout the Build Suite Plan v4
Phase 6 file list. The singular name was introduced as a known
discrepancy in cb327b8 and is now resolved.
- git mv preserves full history via rename detection
- Namespace updated: HellionChat.SelfTest → HellionChat.SelfTests
- Plugin.cs qualifier updated: SelfTest. → SelfTests.
- Build: 0 errors, 0 warnings
Registers a single SelfTestStep that exercises Plugin.ThemeRegistry.Switch
through the live theme list. Verified in-game via /xldev SelfTest tab on
2026-05-08; Plugin loads cleanly with the RegisterTestSteps call and the
step runs the theme cycle as expected.
Folder is HellionChat/SelfTest/ (singular). Future steps may rename to
SelfTests/ to match the local Plan v4 convention.
UI:
- SettingsOverview cards now wrap subtext to two lines (DrawList wrap-
width) and the card height grew from 96 to 110 px. Single-line
fitting clipped most of the bilingual subtitles.
- HellionStyle pushes ChildBg with alpha 0 when WindowOpacity < 1.0
to keep stacked BeginChild layers from compounding the deckgrade
past what the slider suggests.
- WindowOpacity slider helpmarker now points to Dalamud's per-window
hamburger menu for opacity / blur / pin / click-through overrides.
UX defaults (v15 → v16 migration adopts new values only when the user
is still on the previous default — bool flips are heuristic, the prior
defaults are from the v1.2.0 cycle and rarely toggled):
- UseCompactDensity false → true (single-line message style is cleaner)
- HideInNewGamePlusMenu false → true (consistent with other hide-flags)
- HideSameTimestamps false → true (cleaner log)
- MaxLinesToRender 5000 → 2500 (mid-range hardware friendlier)
- ChatColours empty → Hellion brand preset (the first-run wizard does
not offer a preset choice, so fresh installs get the brand colours
out of the box)