chore(linting): refresh configs and sweep auto-fix

Pull in the refreshed linter and tooling configs (editorconfig,
gitignore, gitattributes, prettierignore, prettierrc, markdownlint,
yamllint, env.example, dotnet-tools) and run prettier and markdownlint
in --fix / --write mode across the repo so the existing tree matches
the new rules.

- prettier 2-space indent on yaml/yml and json overrides, asterisk
  strong, underscore emphasis, proseWrap always
- markdownlint MD007 indent aligned to 2 and MD049 to underscore so
  prettier output stays passing
- preflight Block F also ignores CLAUDE.md (gitignored personal file)
- prettierignore extended to keep HellionChat.yaml manifest and the
  NuGet packages.lock.json out of the formatter

No semantic content changed; csharpier, build, full build-suite
(729/729) and the new prettier/markdownlint/yamllint checks all green.
This commit is contained in:
2026-05-17 17:20:55 +02:00
parent 2315f10d91
commit 0220e5d756
53 changed files with 3501 additions and 2630 deletions
+189 -159
View File
@@ -2,40 +2,43 @@
## Background
I am self-taught. Hellion Chat is my first FFXIV plugin and my first larger C# project. My professional background is
web development (Next.js, React, TypeScript, Prisma, MySQL) — browser world with a JavaScript toolchain. I knew C# only
superficially before this project, ImGui not at all, and Dalamud only as an end user through other plugins.
I am self-taught. Hellion Chat is my first FFXIV plugin and my first larger C# project. My
professional background is web development (Next.js, React, TypeScript, Prisma, MySQL) — browser
world with a JavaScript toolchain. I knew C# only superficially before this project, ImGui not at
all, and Dalamud only as an end user through other plugins.
When I get stuck somewhere, I use AI tools like Claude Code as a pair assistant. What that looks like exactly and which
classification I use is documented transparently in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md).
When I get stuck somewhere, I use AI tools like Claude Code as a pair assistant. What that looks
like exactly and which classification I use is documented transparently in
[`AI_DISCLOSURE.md`](AI_DISCLOSURE.md).
---
## Why a chat plugin at all?
Hellion Chat is not meant to replace Chat 2. Chat 2 delivers a complete chat experience with full history, filters,
search and replay. For most users that is exactly the right thing.
Hellion Chat is not meant to replace Chat 2. Chat 2 delivers a complete chat experience with full
history, filters, search and replay. For most users that is exactly the right thing.
### Two million messages in two years
My desire for a tighter default was honestly personal at first. After two years with Chat 2 my database had grown to
over two million messages, the majority of them /say, /shout and /yell from complete strangers in Limsa. That is exactly
what makes Chat 2's full history useful, and most users are happy to keep it. My own preference wanted a smaller
default. So I built this fork.
My desire for a tighter default was honestly personal at first. After two years with Chat 2 my
database had grown to over two million messages, the majority of them /say, /shout and /yell from
complete strangers in Limsa. That is exactly what makes Chat 2's full history useful, and most users
are happy to keep it. My own preference wanted a smaller default. So I built this fork.
### Greeter in several clubs
There was a second use case: I am active as a greeter in several FFXIV clubs. The vanilla chat interface is not enough
for greeter work. Parallel /tell conversations write into a single tab at the same time, and I constantly lose track of
who wrote what. Auto-Tell-Tabs (one of the early Hellion Chat features) came directly from this workflow: one tab per
conversation partner, automatically spawned, with a manual greeted status. The privacy hygiene benefit was a nice bonus,
There was a second use case: I am active as a greeter in several FFXIV clubs. The vanilla chat
interface is not enough for greeter work. Parallel /tell conversations write into a single tab at
the same time, and I constantly lose track of who wrote what. Auto-Tell-Tabs (one of the early
Hellion Chat features) came directly from this workflow: one tab per conversation partner,
automatically spawned, with a manual greeted status. The privacy hygiene benefit was a nice bonus,
not the trigger.
### Hellion Online Media
The privacy defaults also reflect a position from my main work. Hellion Online Media is my sole proprietorship, and data
protection toward clients is not a marketing slogan there but operationally relevant. This fork is the plugin form of
the same stance.
The privacy defaults also reflect a position from my main work. Hellion Online Media is my sole
proprietorship, and data protection toward clients is not a marketing slogan there but operationally
relevant. This fork is the plugin form of the same stance.
---
@@ -45,90 +48,99 @@ Three reasons, in descending order of importance.
### Defaults are not negotiable, including mine
Privacy-first as a default is a minority position. Chat 2 rightly serves the broad majority with full history as the
default. Changing those defaults upstream would have been wrong. I would have flipped the standard for a large user base
that wanted it as it was. A clean separation through a dedicated plugin slot was the more respectful path.
Privacy-first as a default is a minority position. Chat 2 rightly serves the broad majority with
full history as the default. Changing those defaults upstream would have been wrong. I would have
flipped the standard for a large user base that wanted it as it was. A clean separation through a
dedicated plugin slot was the more respectful path.
### The web interface had to go
It is a central Chat 2 feature for remote access from a second device. A PR removing it has no chance in a
well-maintained upstream project, and that is correct. But exactly that web interface conflicts with the privacy-first
premise of this fork: a chat plugin that starts a local HTTP server is too large an attack surface for my threat model.
So out it went.
It is a central Chat 2 feature for remote access from a second device. A PR removing it has no
chance in a well-maintained upstream project, and that is correct. But exactly that web interface
conflicts with the privacy-first premise of this fork: a chat plugin that starts a local HTTP server
is too large an attack surface for my threat model. So out it went.
### Velocity
A solo-maintainer project with a small tester pool can iterate faster than an established plugin with a large user base.
That is not a criticism of upstream but a different optimization. I do not need roadmap alignment, reviewer
availability, or to spread audit consequences like the web interface removal across multiple releases.
A solo-maintainer project with a small tester pool can iterate faster than an established plugin
with a large user base. That is not a criticism of upstream but a different optimization. I do not
need roadmap alignment, reviewer availability, or to spread audit consequences like the web
interface removal across multiple releases.
EUPL-1.2 explicitly allows all of this with clear attribution. The code is open under the same license as Chat 2. Infi,
Anna, or anyone else can look in, take ideas, ask questions, or simply ignore the fork. All three are fine with me.
EUPL-1.2 explicitly allows all of this with clear attribution. The code is open under the same
license as Chat 2. Infi, Anna, or anyone else can look in, take ideas, ask questions, or simply
ignore the fork. All three are fine with me.
---
## How I release this fast
Anyone looking at the repo sees a lot of releases and a high commit count in a short time. Both tend to read as red
flags from the outside: AI slop, salami tactics, code spam. In Hellion Chat both are deliberate decisions, and I would
rather explain them once than justify them later.
Anyone looking at the repo sees a lot of releases and a high commit count in a short time. Both tend
to read as red flags from the outside: AI slop, salami tactics, code spam. In Hellion Chat both are
deliberate decisions, and I would rather explain them once than justify them later.
### Groundwork, long before the fork existed
Before I typed the first line into `HellionChat/`, I spent weeks as a reader. Using Chat 2 in-game and playing around
with it. Going through issues in the upstream tracker, especially the closed ones, because that is where you see how
Infi and Anna narrow down bugs. Reading commits, including older ones, to understand _why_ an architecture decision was
made, not just _that_ it was made. If I know today where things live in the codebase, it is not because I navigate
codebases particularly fast but because I read the code beforehand.
Before I typed the first line into `HellionChat/`, I spent weeks as a reader. Using Chat 2 in-game
and playing around with it. Going through issues in the upstream tracker, especially the closed
ones, because that is where you see how Infi and Anna narrow down bugs. Reading commits, including
older ones, to understand _why_ an architecture decision was made, not just _that_ it was made. If I
know today where things live in the codebase, it is not because I navigate codebases particularly
fast but because I read the code beforehand.
That sounds obvious. It is not. The usual order for solo forks is fork first, understand later. I did it the other way
around.
That sounds obvious. It is not. The usual order for solo forks is fork first, understand later. I
did it the other way around.
One thing I noticed reading the codebase closely: some patterns felt familiar in ways I had not expected, structural
choices and comment styles that show up across a lot of modern plugin and tooling code regardless of how it was written.
Nothing worth reading into. Coding workflows have changed a lot in the last few years across the board, and the traces
of that show up everywhere. It did make me less self-conscious about my own workflow.
One thing I noticed reading the codebase closely: some patterns felt familiar in ways I had not
expected, structural choices and comment styles that show up across a lot of modern plugin and
tooling code regardless of how it was written. Nothing worth reading into. Coding workflows have
changed a lot in the last few years across the board, and the traces of that show up everywhere. It
did make me less self-conscious about my own workflow.
### Infi and Anna's codebase
Hellion Chat builds on a foundation that is already flat. Chat 2 is cleanly structured, naming conventions are
consistent, and the separation between layers (storage, UI, game hooks, IPC) is clearly drawn. That is not a given in
open-source plugin land, and it is the main reason Hellion-specific features often slot in "almost natively". I do not
have to untangle spaghetti before I can put something of my own next to it.
Hellion Chat builds on a foundation that is already flat. Chat 2 is cleanly structured, naming
conventions are consistent, and the separation between layers (storage, UI, game hooks, IPC) is
clearly drawn. That is not a given in open-source plugin land, and it is the main reason
Hellion-specific features often slot in "almost natively". I do not have to untangle spaghetti
before I can put something of my own next to it.
Side note: even during the first codebase walkthrough with Claude, the comment came up several times that the
architecture is unusually tidy and has several extension points prepared. That carries weight because it comes from
outside, but the actual credit goes to Infi and Anna, not Claude.
Side note: even during the first codebase walkthrough with Claude, the comment came up several times
that the architecture is unusually tidy and has several extension points prepared. That carries
weight because it comes from outside, but the actual credit goes to Infi and Anna, not Claude.
### Atomic work, small commits
One commit, one logical change. If I fix a bug, rename a variable and add a comment at the same time, that is three
commits, not one. Sounds like micro-management, it is not. If a bug surfaces in six months and I need `git bisect`, I
find the broken change in two minutes instead of two hours. With a 4000-line mega-commit I get to guess which of the
hundred changes is the broken one.
One commit, one logical change. If I fix a bug, rename a variable and add a comment at the same
time, that is three commits, not one. Sounds like micro-management, it is not. If a bug surfaces in
six months and I need `git bisect`, I find the broken change in two minutes instead of two hours.
With a 4000-line mega-commit I get to guess which of the hundred changes is the broken one.
I kept this style deliberately also because Infi works the same way upstream. Sometimes a six-line commit, sometimes
just a typo fix. That is not a weakness, it is a decision for readable Git history. Keeping the style in the fork is a
respect move: anyone comparing both repos should have the same reading rhythm.
I kept this style deliberately also because Infi works the same way upstream. Sometimes a six-line
commit, sometimes just a typo fix. That is not a weakness, it is a decision for readable Git
history. Keeping the style in the fork is a respect move: anyone comparing both repos should have
the same reading rhythm.
Personal bonus: small commits force me to think through and name each step individually. If I cannot explain what a
commit does in two sentences, the change is probably not clear enough yet. At beginner level that is a built-in sanity
check I would not have with a big-bang commit.
Personal bonus: small commits force me to think through and name each step individually. If I cannot
explain what a commit does in two sentences, the change is probably not clear enough yet. At
beginner level that is a built-in sanity check I would not have with a big-bang commit.
### AI as an accelerator, honestly
Yes, AI helps with velocity, and not a little. Without CodeRabbit I would not have found critical bugs like
`Equals/GetHashCode` anti-patterns, hook subscription leaks and TOCTOU races. I am simply too inexperienced for that
class of findings, and I write that exactly as it is.
Yes, AI helps with velocity, and not a little. Without CodeRabbit I would not have found critical
bugs like `Equals/GetHashCode` anti-patterns, hook subscription leaks and TOCTOU races. I am simply
too inexperienced for that class of findings, and I write that exactly as it is.
What I do not do: blindly take code because a tool marked it as a fix. On several CodeRabbit findings, the original
commits from Infi or Anna even included a Stack Overflow link explaining why a particular spot looks the way it does. I
read those before touching anything. Understand first, then change, then commit. That is the difference between "AI
gives me code, I push" and "AI shows me where it breaks, I decide".
What I do not do: blindly take code because a tool marked it as a fix. On several CodeRabbit
findings, the original commits from Infi or Anna even included a Stack Overflow link explaining why
a particular spot looks the way it does. I read those before touching anything. Understand first,
then change, then commit. That is the difference between "AI gives me code, I push" and "AI shows me
where it breaks, I decide".
Classification and concrete examples of AI usage are in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). This section was only
about the velocity aspect: research plus a clean codebase plus atomic commits plus AI-assisted review sparring are the
four factors together. No single one explains the pace on its own.
Classification and concrete examples of AI usage are in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). This
section was only about the velocity aspect: research plus a clean codebase plus atomic commits plus
AI-assisted review sparring are the four factors together. No single one explains the pace on its
own.
---
@@ -136,48 +148,53 @@ four factors together. No single one explains the pace on its own.
### Type system? Less of a shock than expected
C# after TypeScript was more comfortable than expected. Properties instead of getters/setters are clean, nullable
reference types feel like `strict: true` in TypeScript. What was unfamiliar was having to think explicitly about value
types versus reference types (`struct` vs. `class` with real behavioural consequences), and generics with constraints
are syntactically different enough that I stumble on them while reading. `async`/`await` is semantically similar, but
threading models are more explicit in C#: `Task.Run`, `ConfigureAwait`, synchronization contexts. That cost me several
bugs before I understood when the main thread (in plugin land: the framework tick) is actually critical.
C# after TypeScript was more comfortable than expected. Properties instead of getters/setters are
clean, nullable reference types feel like `strict: true` in TypeScript. What was unfamiliar was
having to think explicitly about value types versus reference types (`struct` vs. `class` with real
behavioural consequences), and generics with constraints are syntactically different enough that I
stumble on them while reading. `async`/`await` is semantically similar, but threading models are
more explicit in C#: `Task.Run`, `ConfigureAwait`, synchronization contexts. That cost me several
bugs before I understood when the main thread (in plugin land: the framework tick) is actually
critical.
### Build toolchain: similar, but different
`dotnet` CLI, csproj XML, NuGet are functionally not far from npm and tsconfig. But the XML format of csproj is a
different language than JSON configs. The lock file (`packages.lock.json`) had to be actively enabled
(`RestorePackagesWithLockFile=true`); that is not the default. In the web stack, lock-file-first is standard, in the
.NET stack apparently not. That was a real surprise.
`dotnet` CLI, csproj XML, NuGet are functionally not far from npm and tsconfig. But the XML format
of csproj is a different language than JSON configs. The lock file (`packages.lock.json`) had to be
actively enabled (`RestorePackagesWithLockFile=true`); that is not the default. In the web stack,
lock-file-first is standard, in the .NET stack apparently not. That was a real surprise.
### ImGui is a different world
Immediate-mode rendering has nothing in common with React component trees. There is no virtual DOM, no reconciliation,
no "component state". Every frame the code redraws the UI from scratch, and state lives either in local variables I
manage myself or in ImGui's own ID stack logic.
Immediate-mode rendering has nothing in common with React component trees. There is no virtual DOM,
no reconciliation, no "component state". Every frame the code redraws the UI from scratch, and state
lives either in local variables I manage myself or in ImGui's own ID stack logic.
What is two lines of `useState` in React is a member field plus manual ID stamps on widgets in ImGui, otherwise two
selectables in the same loop collide because they fall back to the same ID. The ID stack collision in `SearchSelector`
(fixed in v1.0.0) was exactly that symptom: all selectables fell back to the same ambiguous ID until I mixed the row
index into the PushID. Classic "why is the wrong entry getting clicked" bug that you only find once you understand how
ImGui handles IDs internally.
What is two lines of `useState` in React is a member field plus manual ID stamps on widgets in
ImGui, otherwise two selectables in the same loop collide because they fall back to the same ID. The
ID stack collision in `SearchSelector` (fixed in v1.0.0) was exactly that symptom: all selectables
fell back to the same ambiguous ID until I mixed the row index into the PushID. Classic "why is the
wrong entry getting clicked" bug that you only find once you understand how ImGui handles IDs
internally.
### Dalamud specifics
Plugin lifecycle, IPC subscriber pattern, hook system for game functions, game object threading. Much of that was only
understandable through reading the upstream codebase and through [dalamud.dev](https://dalamud.dev). Search results for
"Dalamud" often turn up outdated API examples from old versions. dalamud.dev is the reliable source. If someone is just
starting out: go there, not to Stack Overflow.
Plugin lifecycle, IPC subscriber pattern, hook system for game functions, game object threading.
Much of that was only understandable through reading the upstream codebase and through
[dalamud.dev](https://dalamud.dev). Search results for "Dalamud" often turn up outdated API examples
from old versions. dalamud.dev is the reliable source. If someone is just starting out: go there,
not to Stack Overflow.
### The day DalamudPackager cost me a day
Dalamud SDK 15 ships its own default packager that writes icons and image URLs into the manifest. I had carried over a
`DalamudPackager.targets` file from the upstream repo with a `HandleImages` override, and it was overriding the SDK
default. Result: the manifest had no `IconUrl` anymore, and the plugin appeared in the plugin list without an icon.
Dalamud SDK 15 ships its own default packager that writes icons and image URLs into the manifest. I
had carried over a `DalamudPackager.targets` file from the upstream repo with a `HandleImages`
override, and it was overriding the SDK default. Result: the manifest had no `IconUrl` anymore, and
the plugin appeared in the plugin list without an icon.
The symptom was easy to spot, the cause cost a day. I had treated the override file as mandatory when it was not.
Removed in v0.5.2, SDK default running since then. Lesson: start with defaults, add overrides only when the default
demonstrably does not fit.
The symptom was easy to spot, the cause cost a day. I had treated the override file as mandatory
when it was not. Removed in v0.5.2, SDK default running since then. Lesson: start with defaults, add
overrides only when the default demonstrably does not fit.
---
@@ -185,73 +202,82 @@ demonstrably does not fit.
### Refactoring in an unfamiliar codebase
The standalone cut in v1.0.0 migrated the entire `ChatTwo.*` identity to `HellionChat.*`. That sounds like find and
replace. It was not.
The standalone cut in v1.0.0 migrated the entire `ChatTwo.*` identity to `HellionChat.*`. That
sounds like find and replace. It was not.
In concrete terms: code namespace across all 80 source files plus 100 using directives plus two FQN aliases plus the
resource designer strings. Six IPC channels renamed (breaking change for third-party plugins, no known integrations).
Repo folder structure (`ChatTwo/` -> `HellionChat/`) including csproj, sln, all GitHub workflows and dependabot.yml.
Public-facing branding in README, repo.json and yaml reformulated to standalone framing.
In concrete terms: code namespace across all 80 source files plus 100 using directives plus two FQN
aliases plus the resource designer strings. Six IPC channels renamed (breaking change for
third-party plugins, no known integrations). Repo folder structure (`ChatTwo/` -> `HellionChat/`)
including csproj, sln, all GitHub workflows and dependabot.yml. Public-facing branding in README,
repo.json and yaml reformulated to standalone framing.
It was not a solo find-and-replace because Unicode string paths in workflow YAMLs need different quoting than C#
strings. Because resource designer files have generated content that not every toolchain tracks. And because the
`ChatTwo.*` IPC channel names are strings in `GetIpcSubscriber` calls: no symbol, no compile error if you miss one. That
is when you find out what stays quiet.
It was not a solo find-and-replace because Unicode string paths in workflow YAMLs need different
quoting than C# strings. Because resource designer files have generated content that not every
toolchain tracks. And because the `ChatTwo.*` IPC channel names are strings in `GetIpcSubscriber`
calls: no symbol, no compile error if you miss one. That is when you find out what stays quiet.
### Security is no longer abstract
Before this project, supply chain security was academic for me. Three concrete lessons changed that.
**SQLite native binary.** I had to pin to 3.50.3 (`SQLitePCLRaw.lib.e_sqlite3` override) because `Microsoft.Data.Sqlite`
was pulling in a transitively referenced library at a version containing CVE-2025-6965 (memory corruption via aggregate
term overflow) and CVE-2025-7709. The managed wrapper was new; the native library was not. Lesson: transitive
dependencies do not audit themselves, you have to look.
**SQLite native binary.** I had to pin to 3.50.3 (`SQLitePCLRaw.lib.e_sqlite3` override) because
`Microsoft.Data.Sqlite` was pulling in a transitively referenced library at a version containing
CVE-2025-6965 (memory corruption via aggregate term overflow) and CVE-2025-7709. The managed wrapper
was new; the native library was not. Lesson: transitive dependencies do not audit themselves, you
have to look.
**Lock file drift.** `packages.lock.json` honoured via `RestorePackagesWithLockFile=true` in the csproj prevents
transitive versions from silently drifting between my machine and CI. I only understood why this is not the default
after a build output mismatch between local and GitHub Actions.
**Lock file drift.** `packages.lock.json` honoured via `RestorePackagesWithLockFile=true` in the
csproj prevents transitive versions from silently drifting between my machine and CI. I only
understood why this is not the default after a build output mismatch between local and GitHub
Actions.
**WrapText and the CodeQL alert that cost three releases.** CodeQL flagged a critical alert in `ImGuiUtil.WrapText` for
unvalidated local pointer arithmetic. v0.5.2 validated an edge case. Alert came back. v0.5.3 checked buffer length via
`GetByteCount` before the pointer math. Alert came back. v0.5.4 rebuilt the whole algorithm on `Span` and int offsets
with a 16 KiB cap on the ArrayPool rent. Only then did it go quiet.
**WrapText and the CodeQL alert that cost three releases.** CodeQL flagged a critical alert in
`ImGuiUtil.WrapText` for unvalidated local pointer arithmetic. v0.5.2 validated an edge case. Alert
came back. v0.5.3 checked buffer length via `GetByteCount` before the pointer math. Alert came back.
v0.5.4 rebuilt the whole algorithm on `Span` and int offsets with a 16 KiB cap on the ArrayPool
rent. Only then did it go quiet.
Lesson: when a static analyser complains three times in a row, the analyser is not oversensitive. The data flow logic
is.
Lesson: when a static analyser complains three times in a row, the analyser is not oversensitive.
The data flow logic is.
### CodeRabbit as an external code reviewer
The v1.0.0 sweep surfaced 3 critical and 21 major findings. Three classes were particularly instructive:
The v1.0.0 sweep surfaced 3 critical and 21 major findings. Three classes were particularly
instructive:
- **`Equals` methods comparing `GetHashCode()`.** Classic hash collision anti-pattern. Sounds like "if hashes are equal
the objects are equal", which is exactly backwards. Hashes can collide; the objects are not equal.
- **`Dispose` methods that only unsubscribe part of their subscriptions.** Leak on every plugin reload. In normal use
you do not notice it immediately; in a long-running test you do.
- **TOCTOU races.** Between a bounds check and a read another thread can swap out the array underneath you
(`GlobalParametersCache`, `AutoTranslate`).
- **`Equals` methods comparing `GetHashCode()`.** Classic hash collision anti-pattern. Sounds like
"if hashes are equal the objects are equal", which is exactly backwards. Hashes can collide; the
objects are not equal.
- **`Dispose` methods that only unsubscribe part of their subscriptions.** Leak on every plugin
reload. In normal use you do not notice it immediately; in a long-running test you do.
- **TOCTOU races.** Between a bounds check and a read another thread can swap out the array
underneath you (`GlobalParametersCache`, `AutoTranslate`).
I had at best read the theory on all of these before, never diagnosed them in my own code. CodeRabbit was the moment
where "academic knowledge" became "okay, that is my code, that is my bug".
I had at best read the theory on all of these before, never diagnosed them in my own code.
CodeRabbit was the moment where "academic knowledge" became "okay, that is my code, that is my bug".
### External testers are worth their weight
Carla's feedback on pop-out discoverability triggered the header button in v0.6.1. That pop-outs were only reachable via
right-click was something I as maintainer had stopped seeing; I knew the path by heart. Carl's request for theme
variants with brightness gradations shifted my thinking from "one theme = one colour" to "theme families with mood
variants". Jingliu asked for TempTell persistence, which puts the tab system architecturally into question.
Carla's feedback on pop-out discoverability triggered the header button in v0.6.1. That pop-outs
were only reachable via right-click was something I as maintainer had stopped seeing; I knew the
path by heart. Carl's request for theme variants with brightness gradations shifted my thinking from
"one theme = one colour" to "theme families with mood variants". Jingliu asked for TempTell
persistence, which puts the tab system architecturally into question.
Solo I would not have seen any of those three things. Full stop.
### release.yml and the YAML rabbit hole
The `release.yml` workflow simply did not fire on the first v0.6.0 tag push. I dug through permissions, secret scopes
and tag trigger configuration for hours before I understood what was actually happening: the PowerShell heredoc footer
in the "Generate release body" step contained a `---` Markdown horizontal rule at column 1, and that terminated the YAML
block scalar of `run: |`. GitHub could not parse the workflow file, so the push-tag trigger never registered.
The `release.yml` workflow simply did not fire on the first v0.6.0 tag push. I dug through
permissions, secret scopes and tag trigger configuration for hours before I understood what was
actually happening: the PowerShell heredoc footer in the "Generate release body" step contained a
`---` Markdown horizontal rule at column 1, and that terminated the YAML block scalar of `run: |`.
GitHub could not parse the workflow file, so the push-tag trigger never registered.
Fix: extracted the footer into an external `.github/release-footer.md`, workflow reads it via `Get-Content`. Lesson: if
a workflow does not trigger, verify first that GitHub can even parse the file. That was one of the bugs where I laughed
briefly after the fix and then asked myself how many other YAML files I had that might have the same trap in them.
Fix: extracted the footer into an external `.github/release-footer.md`, workflow reads it via
`Get-Content`. Lesson: if a workflow does not trigger, verify first that GitHub can even parse the
file. That was one of the bugs where I laughed briefly after the fix and then asked myself how many
other YAML files I had that might have the same trap in them.
---
@@ -259,29 +285,31 @@ briefly after the fix and then asked myself how many other YAML files I had that
### Performance profiling in a game context
The FPS drop bug from upstream Chat 2 ([#145](https://github.com/Infiziert90/ChatTwo/issues/145)) has not been
reproduced or verified in Hellion Chat. v1.0.0 applied several fixes on the suspected paths (DbViewer O(N²) -> O(N),
AutoTranslate lock serialisation, EmoteCache HttpClient reuse), but systematic measurement under load is missing. I
still need to learn how to properly measure what is actually consuming the frame budget in a plugin context.
The FPS drop bug from upstream Chat 2 ([#145](https://github.com/Infiziert90/ChatTwo/issues/145))
has not been reproduced or verified in Hellion Chat. v1.0.0 applied several fixes on the suspected
paths (DbViewer O(N²) -> O(N), AutoTranslate lock serialisation, EmoteCache HttpClient reuse), but
systematic measurement under load is missing. I still need to learn how to properly measure what is
actually consuming the frame budget in a plugin context.
### Native interop and pointer math
Even after the WrapText Span refactor in v0.5.4, pointer math makes me uneasy. ImGui forces you into `unsafe` code in
several places, and the safety margin from the "unbounded ArrayPool allocation" class of bugs is narrower than I would
like. I want to get better at that before touching deeper ImGui custom drawing.
Even after the WrapText Span refactor in v0.5.4, pointer math makes me uneasy. ImGui forces you into
`unsafe` code in several places, and the safety margin from the "unbounded ArrayPool allocation"
class of bugs is narrower than I would like. I want to get better at that before touching deeper
ImGui custom drawing.
### Test discipline for plugin code
The repo currently has no test project. That is a deliberate decision, not a forgotten one. Testing plugin code with
FFXIV hooks and Dalamud lifecycle cleanly is non-trivial, and I had not found an approach that made sense without a
large mocking scaffold. Privacy filter and configuration migration would be good test candidates because they are
isolated. On the list, but not a quick win.
The repo currently has no test project. That is a deliberate decision, not a forgotten one. Testing
plugin code with FFXIV hooks and Dalamud lifecycle cleanly is non-trivial, and I had not found an
approach that made sense without a large mocking scaffold. Privacy filter and configuration
migration would be good test candidates because they are isolated. On the list, but not a quick win.
### Linux quirks under Wine
XDG compliance, libnotify integration, WireGuard network detection, all on the [roadmap](ROADMAP.md), and all
technically still unclear. Wine and sandboxed plugin code do not share all system APIs, and I do not know where the
pitfalls are until I have found them.
XDG compliance, libnotify integration, WireGuard network detection, all on the
[roadmap](ROADMAP.md), and all technically still unclear. Wine and sandboxed plugin code do not
share all system APIs, and I do not know where the pitfalls are until I have found them.
---
@@ -303,10 +331,12 @@ I use Claude Code as an assistant, not as a replacement for my own work.
- Tester communication and roadmap prioritisation
- Reviewing, verifying, pushing
Classification and concrete examples are in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). It matters to me that users and
potential contributors understand how the code came together, especially for a plugin that handles user data.
Classification and concrete examples are in [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md). It matters to me
that users and potential contributors understand how the code came together, especially for a plugin
that handles user data.
Yes, AI. Yes, alone. Both mentioned more than strictly necessary. Welcome to the open-source plugin climate.
Yes, AI. Yes, alone. Both mentioned more than strictly necessary. Welcome to the open-source plugin
climate.
---