c4c85cf4b8
- Translated project documentation (LEARNING-JOURNEY, CONTRIBUTORS, AI_DISCLOSURE) to English for better accessibility. - Standardized internal code documentation by converting XML-doc blocks to standard comment format. - Cleaned up inline comments and removed redundant versioning metadata across the codebase. - Refactored non-functional text elements to improve readability and maintain a consistent style.
332 lines
19 KiB
Markdown
332 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
|