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:
+189
-159
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user