0220e5d756
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.
362 lines
19 KiB
Markdown
362 lines
19 KiB
Markdown
# Development History and Learning Process
|
|
|
|
## 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.
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
### 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,
|
|
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.
|
|
|
|
---
|
|
|
|
## Why not contribute to the original?
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## From the web stack to C# / Dalamud
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## What I learned from the fork
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
**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.
|
|
|
|
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:
|
|
|
|
- **`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".
|
|
|
|
### 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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## What I am still learning
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
### 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.
|
|
|
|
---
|
|
|
|
## Use of AI tools
|
|
|
|
I use Claude Code as an assistant, not as a replacement for my own work.
|
|
|
|
**What I use AI for:**
|
|
|
|
- Debugging problems where I am stuck after extended research of my own
|
|
- Pattern recognition across large codebases (e.g. the ChatTwo -> HellionChat sweep across 80 files)
|
|
- Understanding questions on C# and Dalamud concepts I am not yet familiar with
|
|
- Code review sparring before I run CodeRabbit on something
|
|
|
|
**What I do myself:**
|
|
|
|
- Architecture and design decisions
|
|
- Privacy-first defaults and the threat model behind them
|
|
- 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.
|
|
|
|
Yes, AI. Yes, alone. Both mentioned more than strictly necessary. Welcome to the open-source plugin
|
|
climate.
|
|
|
|
---
|
|
|
|
## Why this transparency
|
|
|
|
Anyone reading the source code should know:
|
|
|
|
- I am not a professional C# or plugin developer and am still learning
|
|
- AI assistance is a tool, not a ghostwriter
|
|
- The privacy position, the design decisions and the roadmap are mine
|
|
- I try to keep my code as clean and secure as my current skills allow
|
|
|
|
Hellion Chat is also a learning project, and that should be visible in the repository.
|
|
|
|
---
|
|
|
|
## Links
|
|
|
|
- [`AI_DISCLOSURE.md`](AI_DISCLOSURE.md) -- AI pair disclosure with classification schema
|
|
- [`CONTRIBUTORS.md`](CONTRIBUTORS.md) -- who has made this plugin better alongside me
|
|
- [`../NOTICE.md`](../NOTICE.md) -- attribution to Infi and Anna for the Chat 2 foundation
|
|
- [`ROADMAP.md`](ROADMAP.md) -- planned cycles and topics
|